Починка показа людей для перессылки(не отдельный экран, потом), возможность написать тому кто переслал сообщение, какая то дохлая система показа и перехода к последнему прочитанному сообщению(не работает), копирование пересланных сообщений

This commit is contained in:
jganenok
2025-12-04 14:49:59 +07:00
parent bcc7e499de
commit 61f0eb349a
4 changed files with 506 additions and 183 deletions

View File

@@ -59,6 +59,44 @@ class DateSeparatorItem extends ChatItem {
DateSeparatorItem(this.date); DateSeparatorItem(this.date);
} }
class UnreadSeparatorItem extends ChatItem {}
class _UnreadSeparatorChip extends StatelessWidget {
const _UnreadSeparatorChip();
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const Expanded(child: Divider(thickness: 1)),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: colors.primary.withOpacity(0.12),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'НЕПРОЧИТАННЫЕ СООБЩЕНИЯ',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: colors.primary,
),
),
),
const SizedBox(width: 8),
const Expanded(child: Divider(thickness: 1)),
],
),
);
}
}
class _EmptyChatWidget extends StatelessWidget { class _EmptyChatWidget extends StatelessWidget {
final Map<String, dynamic>? sticker; final Map<String, dynamic>? sticker;
final VoidCallback? onStickerTap; final VoidCallback? onStickerTap;
@@ -69,7 +107,9 @@ class _EmptyChatWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
print('🎨 _EmptyChatWidget.build: sticker=${sticker != null ? "есть" : "null"}'); print(
'🎨 _EmptyChatWidget.build: sticker=${sticker != null ? "есть" : "null"}',
);
if (sticker != null) { if (sticker != null) {
print('🎨 Стикер данные: ${sticker}'); print('🎨 Стикер данные: ${sticker}');
} }
@@ -90,9 +130,7 @@ class _EmptyChatWidget extends StatelessWidget {
const SizedBox( const SizedBox(
width: 170, width: 170,
height: 170, height: 170,
child: Center( child: Center(child: CircularProgressIndicator()),
child: CircularProgressIndicator(),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
@@ -115,7 +153,9 @@ class _EmptyChatWidget extends StatelessWidget {
final width = (sticker['width'] as num?)?.toDouble() ?? 170.0; final width = (sticker['width'] as num?)?.toDouble() ?? 170.0;
final height = (sticker['height'] as num?)?.toDouble() ?? 170.0; final height = (sticker['height'] as num?)?.toDouble() ?? 170.0;
print('🎨 _buildSticker: url=$url, lottieUrl=$lottieUrl, width=$width, height=$height'); print(
'🎨 _buildSticker: url=$url, lottieUrl=$lottieUrl, width=$width, height=$height',
);
// Для отображения используем обычный url (статичное изображение) // Для отображения используем обычный url (статичное изображение)
// lottieUrl - это для анимации, но пока используем статичное изображение // lottieUrl - это для анимации, но пока используем статичное изображение
@@ -136,7 +176,9 @@ class _EmptyChatWidget extends StatelessWidget {
print('✅ Стикер успешно загружен'); print('✅ Стикер успешно загружен');
return child; return child;
} }
print('⏳ Загрузка стикера: ${loadingProgress.cumulativeBytesLoaded}/${loadingProgress.expectedTotalBytes}'); print(
'⏳ Загрузка стикера: ${loadingProgress.cumulativeBytesLoaded}/${loadingProgress.expectedTotalBytes}',
);
return Center( return Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null value: loadingProgress.expectedTotalBytes != null
@@ -149,22 +191,14 @@ class _EmptyChatWidget extends StatelessWidget {
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
print('❌ Ошибка загрузки стикера: $error'); print('❌ Ошибка загрузки стикера: $error');
print('❌ StackTrace: $stackTrace'); print('❌ StackTrace: $stackTrace');
return Icon( return Icon(Icons.emoji_emotions, size: width, color: Colors.grey);
Icons.emoji_emotions,
size: width,
color: Colors.grey,
);
}, },
), ),
); );
} }
print('❌ URL стикера пустой или null'); print('❌ URL стикера пустой или null');
return Icon( return Icon(Icons.emoji_emotions, size: width, color: Colors.grey);
Icons.emoji_emotions,
size: width,
color: Colors.grey,
);
} }
} }
@@ -182,6 +216,7 @@ class ChatScreen extends StatefulWidget {
final bool isChannel; final bool isChannel;
final int? participantCount; final int? participantCount;
final bool isDesktopMode; final bool isDesktopMode;
final int initialUnreadCount;
const ChatScreen({ const ChatScreen({
super.key, super.key,
@@ -194,6 +229,7 @@ class ChatScreen extends StatefulWidget {
this.isChannel = false, this.isChannel = false,
this.participantCount, this.participantCount,
this.isDesktopMode = false, this.isDesktopMode = false,
this.initialUnreadCount = 0,
}); });
@override @override
@@ -225,7 +261,10 @@ class _ChatScreenState extends State<ChatScreen> {
final Map<int, Contact> _contactDetailsCache = {}; final Map<int, Contact> _contactDetailsCache = {};
final Set<int> _loadingContactIds = {}; final Set<int> _loadingContactIds = {};
final Map<String, String> _lastReadMessageIdByParticipant = {}; String?
_lastReadMessageId; // последнее прочитанное нами сообщение в этом чате
int _initialUnreadCount = 0;
bool _hasUnreadSeparator = false;
int? _actualMyId; int? _actualMyId;
@@ -463,6 +502,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initialUnreadCount = widget.initialUnreadCount;
_currentContact = widget.contact; _currentContact = widget.contact;
_pinnedMessage = _pinnedMessage =
null; // Будет установлено при получении CONTROL сообщения с event 'pin' null; // Будет установлено при получении CONTROL сообщения с event 'pin'
@@ -781,17 +821,37 @@ class _ChatScreenState extends State<ChatScreen> {
_isUserAtBottom = isAtBottom; _isUserAtBottom = isAtBottom;
_showScrollToBottomNotifier.value = !isAtBottom; _showScrollToBottomNotifier.value = !isAtBottom;
// Если мы внизу и была плашка непрочитанных — считаем, что всё прочитано:
// сбрасываем lastRead и просто скрываем саму плашку (без изменения списка).
if (isAtBottom && _hasUnreadSeparator) {
_lastReadMessageId = null;
_hasUnreadSeparator = false;
if (mounted) {
setState(() {});
}
}
// Проверяем, доскроллил ли пользователь до самого старого сообщения (вверх) // Проверяем, доскроллил ли пользователь до самого старого сообщения (вверх)
// При reverse: true, последний визуальный элемент (самый большой index) = самое старое сообщение // При reverse: true, последний визуальный элемент (самый большой index) = самое старое сообщение
if (positions.isNotEmpty && _chatItems.isNotEmpty) { if (positions.isNotEmpty && _chatItems.isNotEmpty) {
final maxIndex = positions.map((p) => p.index).reduce((a, b) => a > b ? a : b); final maxIndex = positions
.map((p) => p.index)
.reduce((a, b) => a > b ? a : b);
// При reverse: true, когда maxIndex близок к _chatItems.length - 1, мы вверху (старые сообщения) // При reverse: true, когда maxIndex близок к _chatItems.length - 1, мы вверху (старые сообщения)
final threshold = _chatItems.length > 5 ? 3 : 1; // Загружаем когда осталось 3 элемента до верха final threshold = _chatItems.length > 5
? 3
: 1; // Загружаем когда осталось 3 элемента до верха
final isNearTop = maxIndex >= _chatItems.length - threshold; final isNearTop = maxIndex >= _chatItems.length - threshold;
// Если доскроллили близко к верху и есть еще сообщения, загружаем // Если доскроллили близко к верху и есть еще сообщения, загружаем
if (isNearTop && _hasMore && !_isLoadingMore && _messages.isNotEmpty && _oldestLoadedTime != null) { if (isNearTop &&
print('📜 Пользователь доскроллил близко к верху (maxIndex: $maxIndex, total: ${_chatItems.length}), загружаем старые сообщения...'); _hasMore &&
!_isLoadingMore &&
_messages.isNotEmpty &&
_oldestLoadedTime != null) {
print(
'📜 Пользователь доскроллил близко к верху (maxIndex: $maxIndex, total: ${_chatItems.length}), загружаем старые сообщения...',
);
// Вызываем после build фазы, чтобы избежать setState() во время build // Вызываем после build фазы, чтобы избежать setState() во время build
Future.microtask(() { Future.microtask(() {
if (mounted && _hasMore && !_isLoadingMore) { if (mounted && _hasMore && !_isLoadingMore) {
@@ -970,7 +1030,9 @@ class _ChatScreenState extends State<ChatScreen> {
_oldestLoadedTime = _messages.first.time; _oldestLoadedTime = _messages.first.time;
// Предполагаем, что могут быть еще сообщения (будет обновлено после загрузки с сервера) // Предполагаем, что могут быть еще сообщения (будет обновлено после загрузки с сервера)
_hasMore = true; _hasMore = true;
print('📜 Загружено из кэша: ${_messages.length} сообщений, _oldestLoadedTime=$_oldestLoadedTime'); print(
'📜 Загружено из кэша: ${_messages.length} сообщений, _oldestLoadedTime=$_oldestLoadedTime',
);
} }
if (widget.isGroupChat) { if (widget.isGroupChat) {
@@ -1066,11 +1128,36 @@ class _ChatScreenState extends State<ChatScreen> {
_oldestLoadedTime = _messages.isNotEmpty ? _messages.first.time : null; _oldestLoadedTime = _messages.isNotEmpty ? _messages.first.time : null;
// Если получили максимальное количество сообщений (1000), возможно есть еще // Если получили максимальное количество сообщений (1000), возможно есть еще
// Также проверяем, есть ли сообщения старше самого старого загруженного // Также проверяем, есть ли сообщения старше самого старого загруженного
_hasMore = allMessages.length >= 1000 || allMessages.length > _messages.length; _hasMore =
print('📜 Первая загрузка: загружено ${allMessages.length} сообщений, показано ${_messages.length}, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime'); allMessages.length >= 1000 || allMessages.length > _messages.length;
print(
'📜 Первая загрузка: загружено ${allMessages.length} сообщений, показано ${_messages.length}, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime',
);
// Если есть непрочитанные, пытаемся вычислить последнее прочитанное сообщение
// очень грубо: берём N сообщений перед концом списка (N = initialUnreadCount),
// и считаем последним прочитанным то, что стоит ровно перед ними.
if (widget.initialUnreadCount > 0 &&
allMessages.length > widget.initialUnreadCount) {
final lastReadGlobalIndex =
allMessages.length - widget.initialUnreadCount - 1;
final lastReadMessage = allMessages[lastReadGlobalIndex];
_lastReadMessageId = lastReadMessage.id;
} else {
_lastReadMessageId = null;
}
_buildChatItems(); _buildChatItems();
_isLoadingHistory = false; _isLoadingHistory = false;
}); });
// После первой загрузки истории скроллим к последнему прочитанному
if (_lastReadMessageId != null) {
_scrollToLastReadMessage();
} else {
// Если нечего читать (нет lastRead), просто остаёмся внизу
_scrollToBottom();
}
_updatePinnedMessage(); _updatePinnedMessage();
// Если чат пустой, загружаем стикер для пустого состояния // Если чат пустой, загружаем стикер для пустого состояния
@@ -1105,15 +1192,21 @@ class _ChatScreenState extends State<ChatScreen> {
} }
Future<void> _loadMore() async { Future<void> _loadMore() async {
print('📜 _loadMore() вызвана: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime'); print(
'📜 _loadMore() вызвана: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime',
);
if (_isLoadingMore || !_hasMore) { if (_isLoadingMore || !_hasMore) {
print('📜 _loadMore() пропущена: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore'); print(
'📜 _loadMore() пропущена: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore',
);
return; return;
} }
if (_messages.isEmpty || _oldestLoadedTime == null) { if (_messages.isEmpty || _oldestLoadedTime == null) {
print('📜 _loadMore() пропущена: _messages.isEmpty=${_messages.isEmpty}, _oldestLoadedTime=$_oldestLoadedTime'); print(
'📜 _loadMore() пропущена: _messages.isEmpty=${_messages.isEmpty}, _oldestLoadedTime=$_oldestLoadedTime',
);
_hasMore = false; _hasMore = false;
return; return;
} }
@@ -1122,9 +1215,12 @@ class _ChatScreenState extends State<ChatScreen> {
setState(() {}); setState(() {});
try { try {
print('📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime'); print(
'📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime',
);
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения // Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
final olderMessages = await ApiService.instance.loadOlderMessagesByTimestamp( final olderMessages = await ApiService.instance
.loadOlderMessagesByTimestamp(
widget.chatId, widget.chatId,
_oldestLoadedTime!, _oldestLoadedTime!,
backward: 30, backward: 30,
@@ -1144,7 +1240,9 @@ class _ChatScreenState extends State<ChatScreen> {
// Фильтруем дубликаты - оставляем только те сообщения, которых еще нет в списке // Фильтруем дубликаты - оставляем только те сообщения, которых еще нет в списке
final existingMessageIds = _messages.map((m) => m.id).toSet(); final existingMessageIds = _messages.map((m) => m.id).toSet();
final newMessages = olderMessages.where((m) => !existingMessageIds.contains(m.id)).toList(); final newMessages = olderMessages
.where((m) => !existingMessageIds.contains(m.id))
.toList();
if (newMessages.isEmpty) { if (newMessages.isEmpty) {
// Все сообщения уже есть в списке // Все сообщения уже есть в списке
@@ -1154,7 +1252,9 @@ class _ChatScreenState extends State<ChatScreen> {
return; return;
} }
print('📜 Добавляем ${newMessages.length} новых старых сообщений (отфильтровано ${olderMessages.length - newMessages.length} дубликатов)'); print(
'📜 Добавляем ${newMessages.length} новых старых сообщений (отфильтровано ${olderMessages.length - newMessages.length} дубликатов)',
);
// Добавляем старые сообщения в начало списка // Добавляем старые сообщения в начало списка
_messages.insertAll(0, newMessages); _messages.insertAll(0, newMessages);
@@ -1207,6 +1307,16 @@ class _ChatScreenState extends State<ChatScreen> {
final List<ChatItem> items = []; final List<ChatItem> items = [];
final source = _messages; final source = _messages;
// Находим индекс последнего прочитанного сообщения (если оно есть)
int? lastReadIndex;
if (_lastReadMessageId != null) {
lastReadIndex = source.indexWhere((m) => m.id == _lastReadMessageId);
if (lastReadIndex == -1) {
lastReadIndex = null;
}
}
_hasUnreadSeparator = false;
for (int i = 0; i < source.length; i++) { for (int i = 0; i < source.length; i++) {
final currentMessage = source[i]; final currentMessage = source[i];
final previousMessage = (i > 0) ? source[i - 1] : null; final previousMessage = (i > 0) ? source[i - 1] : null;
@@ -1254,12 +1364,20 @@ class _ChatScreenState extends State<ChatScreen> {
isGrouped: isGrouped, isGrouped: isGrouped,
), ),
); );
// Если это последнее прочитанное сообщение, сразу после него вставляем разделитель
if (lastReadIndex != null && i == lastReadIndex) {
items.add(UnreadSeparatorItem());
_hasUnreadSeparator = true;
}
} }
_chatItems = items; _chatItems = items;
// Очищаем ключи для сообщений, которых больше нет в списке // Очищаем ключи для сообщений, которых больше нет в списке
final currentMessageIds = _messages.map((m) => m.id).toSet(); final currentMessageIds = _messages.map((m) => m.id).toSet();
final keysToRemove = _messageKeys.keys.where((id) => !currentMessageIds.contains(id)).toList(); final keysToRemove = _messageKeys.keys
.where((id) => !currentMessageIds.contains(id))
.toList();
for (final id in keysToRemove) { for (final id in keysToRemove) {
_messageKeys.remove(id); _messageKeys.remove(id);
} }
@@ -1273,7 +1391,8 @@ class _ChatScreenState extends State<ChatScreen> {
// Список доступных ID стикеров для пустого чата // Список доступных ID стикеров для пустого чата
final availableStickerIds = [272821, 295349, 13571]; final availableStickerIds = [272821, 295349, 13571];
// Выбираем случайный ID // Выбираем случайный ID
final random = DateTime.now().millisecondsSinceEpoch % availableStickerIds.length; final random =
DateTime.now().millisecondsSinceEpoch % availableStickerIds.length;
final selectedStickerId = availableStickerIds[random]; final selectedStickerId = availableStickerIds[random];
print('🎨 Загружаем стикер для пустого чата (ID: $selectedStickerId)...'); print('🎨 Загружаем стикер для пустого чата (ID: $selectedStickerId)...');
@@ -1310,7 +1429,9 @@ class _ChatScreenState extends State<ChatScreen> {
final sticker = stickers.first as Map<String, dynamic>; final sticker = stickers.first as Map<String, dynamic>;
// Сохраняем также stickerId для отправки // Сохраняем также stickerId для отправки
final stickerId = sticker['id'] as int?; final stickerId = sticker['id'] as int?;
print('🎨 Данные стикера: id=$stickerId, url=${sticker['url']}, lottieUrl=${sticker['lottieUrl']}, width=${sticker['width']}, height=${sticker['height']}'); print(
'🎨 Данные стикера: id=$stickerId, url=${sticker['url']}, lottieUrl=${sticker['lottieUrl']}, width=${sticker['width']}, height=${sticker['height']}',
);
if (mounted) { if (mounted) {
setState(() { setState(() {
_emptyChatSticker = { _emptyChatSticker = {
@@ -1318,7 +1439,9 @@ class _ChatScreenState extends State<ChatScreen> {
'stickerId': stickerId, // Сохраняем ID для отправки 'stickerId': stickerId, // Сохраняем ID для отправки
}; };
}); });
print('✅ Стикер для пустого чата загружен и сохранен (ID: $stickerId)'); print(
'✅ Стикер для пустого чата загружен и сохранен (ID: $stickerId)',
);
} }
} else { } else {
print('❌ Стикеры не найдены в ответе'); print('❌ Стикеры не найдены в ответе');
@@ -1402,6 +1525,35 @@ class _ChatScreenState extends State<ChatScreen> {
}); });
} }
void _scrollToLastReadMessage() {
final lastReadId = _lastReadMessageId;
if (lastReadId == null) return;
int? targetChatItemIndex;
for (int i = 0; i < _chatItems.length; i++) {
final item = _chatItems[i];
if (item is MessageItem && item.message.id == lastReadId) {
targetChatItemIndex = i;
break;
}
}
if (targetChatItemIndex == null) return;
if (!_itemScrollController.isAttached) return;
final visualIndex = _chatItems.length - 1 - targetChatItemIndex;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_itemScrollController.isAttached) {
_itemScrollController.scrollTo(
index: visualIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubic,
);
}
});
}
void _addMessage(Message message, {bool forceScroll = false}) { void _addMessage(Message message, {bool forceScroll = false}) {
if (_messages.any((m) => m.id == message.id)) { if (_messages.any((m) => m.id == message.id)) {
print('Сообщение ${message.id} уже существует, пропускаем добавление'); print('Сообщение ${message.id} уже существует, пропускаем добавление');
@@ -1669,10 +1821,7 @@ class _ChatScreenState extends State<ChatScreen> {
"message": { "message": {
"cid": cid, "cid": cid,
"attaches": [ "attaches": [
{ {"_type": "STICKER", "stickerId": stickerId},
"_type": "STICKER",
"stickerId": stickerId,
}
], ],
}, },
"notify": true, "notify": true,
@@ -1988,12 +2137,31 @@ class _ChatScreenState extends State<ChatScreen> {
); );
subtitle = '${participants.length} участников'; subtitle = '${participants.length} участников';
} else { } else {
final otherParticipantId = participants.keys // Обычный диалог «1 на 1»
.map((id) => int.parse(id)) final participantIds = participants.keys
.firstWhere((id) => id != _actualMyId, orElse: () => 0); .map((id) => int.tryParse(id) ?? 0)
.toList();
int otherParticipantId = 0;
if (participantIds.isNotEmpty) {
// Пытаемся найти участника, отличного от нашего собственного ID
otherParticipantId = participantIds.firstWhere(
(id) => _actualMyId == null || id != _actualMyId,
orElse: () => participantIds.first,
);
}
// Пытаемся взять контакт из локального кэша;
// если его нет — загружаем в фоне.
final contact = _contactDetailsCache[otherParticipantId]; final contact = _contactDetailsCache[otherParticipantId];
chatName = contact?.name ?? chatTitle ?? 'Чат $chatId'; if (contact == null && otherParticipantId != 0) {
_loadContactIfNeeded(otherParticipantId);
}
// Для личных чатов никогда не используем server title вида
// «пользователь {id}» — вместо этого показываем имя контакта
// либо хотя бы «ID {userId}».
chatName = contact?.name ?? 'ID $otherParticipantId';
final avatarUrl = contact?.photoBaseUrl; final avatarUrl = contact?.photoBaseUrl;
@@ -2001,7 +2169,7 @@ class _ChatScreenState extends State<ChatScreen> {
avatarUrl, avatarUrl,
userId: otherParticipantId, userId: otherParticipantId,
size: 48, size: 48,
fallbackText: contact?.name ?? chatTitle ?? 'Чат $chatId', fallbackText: chatName,
backgroundColor: Theme.of(context).colorScheme.primaryContainer, backgroundColor: Theme.of(context).colorScheme.primaryContainer,
); );
@@ -3069,6 +3237,10 @@ class _ChatScreenState extends State<ChatScreen> {
return finalMessageWidget; return finalMessageWidget;
} else if (item is DateSeparatorItem) { } else if (item is DateSeparatorItem) {
return _DateSeparatorChip(date: item.date); return _DateSeparatorChip(date: item.date);
} else if (item is UnreadSeparatorItem) {
return _hasUnreadSeparator
? const _UnreadSeparatorChip()
: const SizedBox.shrink();
} }
if (isLastVisual && _isLoadingMore) { if (isLastVisual && _isLoadingMore) {
return TweenAnimationBuilder<double>( return TweenAnimationBuilder<double>(

View File

@@ -4334,6 +4334,7 @@ class _ChatsScreenState extends State<ChatsScreen>
isGroupChat: isGroupChat, isGroupChat: isGroupChat,
isChannel: isChannel, isChannel: isChannel,
participantCount: participantCount, participantCount: participantCount,
initialUnreadCount: chat.newMessages,
onChatRemoved: () { onChatRemoved: () {
_removeChatLocally(chat.id); _removeChatLocally(chat.id);
}, },

View File

@@ -17,6 +17,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:gwid/screens/chat_screen.dart'; import 'package:gwid/screens/chat_screen.dart';
import 'package:gwid/services/avatar_cache_service.dart'; import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/widgets/user_profile_panel.dart';
import 'package:gwid/api/api_service.dart'; import 'package:gwid/api/api_service.dart';
import 'dart:async'; import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -479,7 +480,41 @@ class ChatMessageBubble extends StatelessWidget {
} }
} }
return Container( // Пытаемся определить userId оригинального отправителя для открытия панели профиля
int? originalSenderId;
if (forwardedMessage['sender'] is int) {
originalSenderId = forwardedMessage['sender'] as int;
}
void handleTap() {
final myId = myUserId ?? 0;
if (originalSenderId == null || myId == 0) {
return;
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => UserProfilePanel(
userId: originalSenderId!,
name: forwardedSenderName,
firstName: null,
lastName: null,
avatarUrl: forwardedSenderAvatarUrl,
description: null,
myId: myId,
currentChatId: chatId,
contactData: null,
dialogChatId: null,
),
);
}
return InkWell(
onTap: handleTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: textColor.withOpacity(0.08 * messageTextOpacity), color: textColor.withOpacity(0.08 * messageTextOpacity),
@@ -591,6 +626,7 @@ class ChatMessageBubble extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
@@ -1207,7 +1243,12 @@ class ChatMessageBubble extends StatelessWidget {
!message.isReply && !message.isReply &&
!message.isForwarded; !message.isForwarded;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context); final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor( final textColor = _getTextColor(
isMe, isMe,
bubbleColor, bubbleColor,
@@ -1774,7 +1815,12 @@ class ChatMessageBubble extends StatelessWidget {
final themeProvider = Provider.of<ThemeProvider>(context); final themeProvider = Provider.of<ThemeProvider>(context);
final isUltraOptimized = themeProvider.ultraOptimizeChats; final isUltraOptimized = themeProvider.ultraOptimizeChats;
final messageOpacity = themeProvider.messageBubbleOpacity; final messageOpacity = themeProvider.messageBubbleOpacity;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context); final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor( final textColor = _getTextColor(
isMe, isMe,
bubbleColor, bubbleColor,
@@ -3879,7 +3925,8 @@ class ChatMessageBubble extends StatelessWidget {
final bool isDark = Theme.of(context).brightness == Brightness.dark; final bool isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isMe final baseColor = isMe
? (themeProvider.myBubbleColor ?? const Color(0xFF2b5278)) ? (themeProvider.myBubbleColor ?? const Color(0xFF2b5278))
: (themeProvider.theirBubbleColor ?? (isDark ? const Color(0xFF182533) : const Color(0xFF464646))); : (themeProvider.theirBubbleColor ??
(isDark ? const Color(0xFF182533) : const Color(0xFF464646)));
return baseColor.withOpacity(1.0 - messageOpacity); return baseColor.withOpacity(1.0 - messageOpacity);
} }
@@ -5146,7 +5193,31 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
} }
void _onCopy() { void _onCopy() {
Clipboard.setData(ClipboardData(text: widget.message.text)); String textToCopy = widget.message.text;
// Для пересланных сообщений пробуем взять текст оригинального сообщения
if (textToCopy.isEmpty &&
widget.message.isForwarded &&
widget.message.link is Map<String, dynamic>) {
final link = widget.message.link as Map<String, dynamic>;
final forwardedMessage = link['message'] as Map<String, dynamic>?;
final forwardedText = forwardedMessage?['text'] as String? ?? '';
textToCopy = forwardedText;
}
if (textToCopy.isEmpty) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Нет текста для копирования'),
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 2),
),
);
return;
}
Clipboard.setData(ClipboardData(text: textToCopy));
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
@@ -5294,7 +5365,9 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (widget.message.text.isNotEmpty) // Для пересланных сообщений разрешаем копирование даже при пустом text,
// т.к. текст может быть внутри link['message'].
if (widget.message.text.isNotEmpty || widget.message.isForwarded)
_buildActionButton( _buildActionButton(
icon: Icons.copy_rounded, icon: Icons.copy_rounded,
text: 'Копировать', text: 'Копировать',

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/widgets/contact_name_widget.dart'; import 'package:gwid/widgets/contact_name_widget.dart';
import 'package:gwid/widgets/contact_avatar_widget.dart'; import 'package:gwid/widgets/contact_avatar_widget.dart';
import 'package:gwid/services/contact_local_names_service.dart'; import 'package:gwid/services/contact_local_names_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/screens/chat_screen.dart';
class UserProfilePanel extends StatefulWidget { class UserProfilePanel extends StatefulWidget {
final int userId; final int userId;
@@ -39,6 +41,7 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
final ScrollController _nameScrollController = ScrollController(); final ScrollController _nameScrollController = ScrollController();
String? _localDescription; String? _localDescription;
StreamSubscription? _changesSubscription; StreamSubscription? _changesSubscription;
bool _isOpeningChat = false;
String get _displayName { String get _displayName {
final displayName = getContactDisplayName( final displayName = getContactDisplayName(
@@ -243,8 +246,9 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
_buildActionButton( _buildActionButton(
icon: Icons.message, icon: Icons.message,
label: 'Написать', label: 'Написать',
onPressed: null, onPressed: _isOpeningChat ? null : _handleWriteMessage,
colors: colors, colors: colors,
isLoading: _isOpeningChat,
), ),
], ],
), ),
@@ -309,4 +313,77 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
], ],
); );
} }
Future<void> _handleWriteMessage() async {
if (_isOpeningChat) return;
setState(() {
_isOpeningChat = true;
});
try {
// Сначала пробуем использовать dialogChatId, если он уже есть
int? chatId = widget.dialogChatId;
if (chatId == null || chatId == 0) {
// Если нет, считаем chatId по формуле chatId = userId1 ^ userId2
chatId = await ApiService.instance.getChatIdByUserId(widget.userId);
}
if (chatId == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось открыть чат с пользователем'),
behavior: SnackBarBehavior.floating,
),
);
}
return;
}
if (!mounted) return;
// Закрываем панель профиля
Navigator.of(context).pop();
// Открываем экран чата
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => ChatScreen(
chatId: chatId!,
contact: Contact(
id: widget.userId,
name: widget.name ?? _displayName,
firstName: widget.firstName ?? '',
lastName: widget.lastName ?? '',
description: widget.description,
photoBaseUrl: widget.avatarUrl,
accountStatus: 0,
status: null,
options: const [],
),
myId: widget.myId,
isGroupChat: false,
isChannel: false,
),
),
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при открытии чата: $e'),
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isOpeningChat = false;
});
}
}
}
} }