Files
fuckKomet/lib/widgets/message_preview_dialog.dart

655 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:gwid/models/chat.dart';
import 'package:gwid/models/message.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/widgets/chat_message_bubble.dart';
import 'package:gwid/widgets/contact_name_widget.dart';
import 'package:gwid/screens/chat_screen.dart';
class ControlMessageChip extends StatelessWidget {
final Message message;
final Map<int, Contact> contacts;
final int myId;
const ControlMessageChip({
super.key,
required this.message,
required this.contacts,
required this.myId,
});
String _formatControlMessage() {
final controlAttach = message.attaches.firstWhere(
(a) => a['_type'] == 'CONTROL',
);
final eventType = controlAttach['event'];
final senderContact = contacts[message.senderId];
final senderName = senderContact != null
? getContactDisplayName(
contactId: senderContact.id,
originalName: senderContact.name,
originalFirstName: senderContact.firstName,
originalLastName: senderContact.lastName,
)
: 'ID ${message.senderId}';
final isMe = message.senderId == myId;
final senderDisplayName = isMe ? 'Вы' : senderName;
String _formatUserList(List<int> userIds) {
if (userIds.isEmpty) {
return '';
}
final userNames = userIds
.map((id) {
if (id == myId) {
return 'Вы';
}
final contact = contacts[id];
if (contact != null) {
return getContactDisplayName(
contactId: contact.id,
originalName: contact.name,
originalFirstName: contact.firstName,
originalLastName: contact.lastName,
);
}
return 'участник с ID $id';
})
.where((name) => name.isNotEmpty)
.join(', ');
return userNames;
}
switch (eventType) {
case 'new':
final title = controlAttach['title'] ?? 'Новая группа';
return '$senderDisplayName создал(а) группу "$title"';
case 'add':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return 'К чату присоединились новые участники';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return 'К чату присоединились новые участники';
}
return '$senderDisplayName добавил(а) в чат: $userNames';
case 'remove':
case 'kick':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return '$senderDisplayName удалил(а) участников из чата';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return '$senderDisplayName удалил(а) участников из чата';
}
if (userIds.contains(myId)) {
return 'Вы были удалены из чата';
}
return '$senderDisplayName удалил(а) из чата: $userNames';
case 'leave':
if (isMe) {
return 'Вы покинули группу';
}
return '$senderName покинул(а) группу';
case 'title':
final newTitle = controlAttach['title'] ?? '';
if (newTitle.isEmpty) {
return '$senderDisplayName изменил(а) название группы';
}
return '$senderDisplayName изменил(а) название группы на "$newTitle"';
case 'avatar':
case 'photo':
return '$senderDisplayName изменил(а) фото группы';
case 'description':
return '$senderDisplayName изменил(а) описание группы';
case 'admin':
case 'promote':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return '$senderDisplayName назначил(а) администраторов';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return '$senderDisplayName назначил(а) администраторов';
}
if (userIds.contains(myId) && userIds.length == 1) {
return 'Вас назначили администратором';
}
return '$senderDisplayName назначил(а) администраторов: $userNames';
case 'demote':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return '$senderDisplayName снял(а) администраторов';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return '$senderDisplayName снял(а) администраторов';
}
if (userIds.contains(myId) && userIds.length == 1) {
return 'С вас сняли права администратора';
}
return '$senderDisplayName снял(а) права администратора с: $userNames';
case 'ban':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return '$senderDisplayName заблокировал(а) участников';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return '$senderDisplayName заблокировал(а) участников';
}
if (userIds.contains(myId)) {
return 'Вы были заблокированы в чате';
}
return '$senderDisplayName заблокировал(а): $userNames';
case 'unban':
final userIds = List<int>.from(
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
);
if (userIds.isEmpty) {
return '$senderDisplayName разблокировал(а) участников';
}
final userNames = _formatUserList(userIds);
if (userNames.isEmpty) {
return '$senderDisplayName разблокировал(а) участников';
}
return '$senderDisplayName разблокировал(а): $userNames';
case 'join':
if (isMe) {
return 'Вы присоединились к группе';
}
return '$senderName присоединился(ась) к группе';
case 'pin':
final pinnedMessage = controlAttach['pinnedMessage'];
if (pinnedMessage != null && pinnedMessage is Map<String, dynamic>) {
final pinnedText = pinnedMessage['text'] as String?;
if (pinnedText != null && pinnedText.isNotEmpty) {
return '$senderDisplayName закрепил(а) сообщение: "$pinnedText"';
}
}
return '$senderDisplayName закрепил(а) сообщение';
default:
final eventTypeStr = eventType?.toString() ?? 'неизвестное';
return 'Событие: $eventTypeStr';
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: colors.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_formatControlMessage(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: colors.onPrimaryContainer,
fontWeight: FontWeight.w500,
),
),
),
);
}
}
class MessagePreviewDialog {
static String _formatTimestamp(int timestamp) {
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
final now = DateTime.now();
if (now.day == dt.day && now.month == dt.month && now.year == dt.year) {
return DateFormat('HH:mm', 'ru').format(dt);
} else {
final yesterday = now.subtract(const Duration(days: 1));
if (dt.day == yesterday.day &&
dt.month == yesterday.month &&
dt.year == yesterday.year) {
return 'Вчера';
} else {
return DateFormat('d MMM', 'ru').format(dt);
}
}
}
static bool _isSavedMessages(Chat chat) {
return chat.id == 0;
}
static bool _isGroupChat(Chat chat) {
return chat.type == 'CHAT' || chat.participantIds.length > 2;
}
static bool _isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
static bool _isMessageGrouped(
Message currentMessage,
Message? previousMessage,
) {
if (previousMessage == null) return false;
final currentTime = DateTime.fromMillisecondsSinceEpoch(
currentMessage.time,
);
final previousTime = DateTime.fromMillisecondsSinceEpoch(
previousMessage.time,
);
final timeDifference = currentTime.difference(previousTime).inMinutes;
return currentMessage.senderId == previousMessage.senderId &&
timeDifference <= 5;
}
static String _formatDateSeparator(DateTime date) {
final now = DateTime.now();
if (_isSameDay(date, now)) {
return 'Сегодня';
} else {
final yesterday = now.subtract(const Duration(days: 1));
if (_isSameDay(date, yesterday)) {
return 'Вчера';
} else {
return DateFormat('d MMM yyyy', 'ru').format(date);
}
}
}
static String _getChatTitle(Chat chat, Map<int, Contact> contacts) {
final bool isSavedMessages = _isSavedMessages(chat);
final bool isGroupChat = _isGroupChat(chat);
final bool isChannel = chat.type == 'CHANNEL';
if (isSavedMessages) {
return "Избранное";
} else if (isChannel) {
return chat.title ?? "Канал";
} else if (isGroupChat) {
return chat.title?.isNotEmpty == true ? chat.title! : "Группа";
} else {
final myId = chat.ownerId;
final otherParticipantId = chat.participantIds.firstWhere(
(id) => id != myId,
orElse: () => myId,
);
final contact = contacts[otherParticipantId];
if (contact != null) {
return contact.name;
} else if (chat.title?.isNotEmpty == true) {
return chat.title!;
} else {
return "ID $otherParticipantId";
}
}
}
static Future<void> show(
BuildContext context,
Chat chat,
Map<int, Contact> contacts,
Profile? myProfile,
VoidCallback? onClose,
Widget Function(BuildContext)? menuBuilder,
) async {
final colors = Theme.of(context).colorScheme;
List<Message> messages = [];
bool isLoading = true;
try {
messages = await ApiService.instance.getMessageHistory(
chat.id,
force: false,
);
if (messages.length > 10) {
messages = messages.sublist(messages.length - 10);
}
} catch (e) {
print('Ошибка загрузки сообщений для предпросмотра: $e');
} finally {
isLoading = false;
}
final Set<int> senderIds = messages.map((m) => m.senderId).toSet();
senderIds.remove(0);
final Set<int> forwardedSenderIds = {};
for (final message in messages) {
if (message.isForwarded && message.link != null) {
final link = message.link;
if (link is Map<String, dynamic>) {
final chatName = link['chatName'] as String?;
if (chatName == null) {
final forwardedMessage = link['message'] as Map<String, dynamic>?;
final originalSenderId = forwardedMessage?['sender'] as int?;
if (originalSenderId != null) {
forwardedSenderIds.add(originalSenderId);
}
}
}
}
}
final allIdsToFetch = {
...senderIds,
...forwardedSenderIds,
}.where((id) => !contacts.containsKey(id)).toList();
if (allIdsToFetch.isNotEmpty) {
try {
final newContacts = await ApiService.instance.fetchContactsByIds(
allIdsToFetch,
);
for (final contact in newContacts) {
contacts[contact.id] = contact;
}
} catch (e) {
print('Ошибка загрузки контактов для предпросмотра: $e');
}
}
final chatTitle = _getChatTitle(chat, contacts);
final bool isGroupChat = _isGroupChat(chat);
final bool isChannel = chat.type == 'CHANNEL';
final myId = myProfile?.id ?? chat.ownerId;
if (!context.mounted) return;
List<ChatItem> chatItems = [];
for (int i = 0; i < messages.length; i++) {
final currentMessage = messages[i];
final previousMessage = (i > 0) ? messages[i - 1] : null;
final currentDate = DateTime.fromMillisecondsSinceEpoch(
currentMessage.time,
).toLocal();
final previousDate = previousMessage != null
? DateTime.fromMillisecondsSinceEpoch(previousMessage.time).toLocal()
: null;
if (previousMessage == null || !_isSameDay(currentDate, previousDate!)) {
chatItems.add(DateSeparatorItem(currentDate));
}
final isGrouped = _isMessageGrouped(currentMessage, previousMessage);
final isFirstInGroup = previousMessage == null || !isGrouped;
final isLastInGroup =
i == messages.length - 1 ||
!_isMessageGrouped(messages[i + 1], currentMessage);
chatItems.add(
MessageItem(
currentMessage,
isFirstInGroup: isFirstInGroup,
isLastInGroup: isLastInGroup,
isGrouped: isGrouped,
),
);
}
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return DraggableScrollableSheet(
initialChildSize: 0.75,
minChildSize: 0.5,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: colors.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 8, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: colors.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(2),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.outline.withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: [
Expanded(
child: Text(
chatTitle,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.close),
color: colors.onSurfaceVariant,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
Navigator.of(context).pop();
onClose?.call();
},
),
],
),
),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: messages.isEmpty
? Center(
child: Text(
'Нет сообщений',
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 14,
),
),
)
: ListView.builder(
controller: scrollController,
reverse: true,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
itemCount: chatItems.length,
itemBuilder: (context, index) {
final mappedIndex = chatItems.length - 1 - index;
final item = chatItems[mappedIndex];
if (item is DateSeparatorItem) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: colors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Text(
_formatDateSeparator(item.date),
style: TextStyle(
fontSize: 12,
color: colors.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
if (item is MessageItem) {
final message = item.message;
final isControlMessage = message.attaches.any(
(a) => a['_type'] == 'CONTROL',
);
if (isControlMessage) {
return ControlMessageChip(
message: message,
contacts: contacts,
myId: myId,
);
}
final isMe = message.senderId == myId;
final senderContact =
contacts[message.senderId];
final senderName = isMe
? 'Вы'
: (senderContact?.name ??
'ID ${message.senderId}');
String? forwardedFrom;
String? forwardedFromAvatarUrl;
if (message.isForwarded) {
final link = message.link;
if (link is Map<String, dynamic>) {
final chatName =
link['chatName'] as String?;
final chatIconUrl =
link['chatIconUrl'] as String?;
if (chatName != null) {
forwardedFrom = chatName;
forwardedFromAvatarUrl = chatIconUrl;
} else {
final forwardedMessage =
link['message']
as Map<String, dynamic>?;
final originalSenderId =
forwardedMessage?['sender'] as int?;
if (originalSenderId != null) {
final originalSenderContact =
contacts[originalSenderId];
forwardedFrom =
originalSenderContact?.name ??
'Участник $originalSenderId';
forwardedFromAvatarUrl =
originalSenderContact?.photoBaseUrl;
}
}
}
}
return ChatMessageBubble(
message: message,
isMe: isMe,
readStatus: null,
deferImageLoading: true,
myUserId: myId,
chatId: chat.id,
onReply: null,
onEdit: null,
canEditMessage: null,
onDeleteForMe: null,
onDeleteForAll: null,
onReaction: null,
onRemoveReaction: null,
isGroupChat: isGroupChat,
isChannel: isChannel,
senderName: senderName,
forwardedFrom: forwardedFrom,
forwardedFromAvatarUrl:
forwardedFromAvatarUrl,
contactDetailsCache: contacts,
onReplyTap: null,
useAutoReplyColor: false,
customReplyColor: null,
isFirstInGroup: item.isFirstInGroup,
isLastInGroup: item.isLastInGroup,
isGrouped: item.isGrouped,
avatarVerticalOffset: -8.0,
);
}
return const SizedBox.shrink();
},
),
),
if (menuBuilder != null) ...[
Divider(height: 1, color: colors.outline.withOpacity(0.2)),
menuBuilder(context),
],
],
),
);
},
);
},
);
}
}