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

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);
}
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 {
final Map<String, dynamic>? sticker;
final VoidCallback? onStickerTap;
@@ -69,7 +107,9 @@ class _EmptyChatWidget extends StatelessWidget {
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
print('🎨 _EmptyChatWidget.build: sticker=${sticker != null ? "есть" : "null"}');
print(
'🎨 _EmptyChatWidget.build: sticker=${sticker != null ? "есть" : "null"}',
);
if (sticker != null) {
print('🎨 Стикер данные: ${sticker}');
}
@@ -90,9 +130,7 @@ class _EmptyChatWidget extends StatelessWidget {
const SizedBox(
width: 170,
height: 170,
child: Center(
child: CircularProgressIndicator(),
),
child: Center(child: CircularProgressIndicator()),
),
const SizedBox(height: 24),
],
@@ -115,7 +153,9 @@ class _EmptyChatWidget extends StatelessWidget {
final width = (sticker['width'] 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 (статичное изображение)
// lottieUrl - это для анимации, но пока используем статичное изображение
@@ -136,12 +176,14 @@ class _EmptyChatWidget extends StatelessWidget {
print('✅ Стикер успешно загружен');
return child;
}
print('⏳ Загрузка стикера: ${loadingProgress.cumulativeBytesLoaded}/${loadingProgress.expectedTotalBytes}');
print(
'⏳ Загрузка стикера: ${loadingProgress.cumulativeBytesLoaded}/${loadingProgress.expectedTotalBytes}',
);
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
loadingProgress.expectedTotalBytes!
: null,
),
);
@@ -149,22 +191,14 @@ class _EmptyChatWidget extends StatelessWidget {
errorBuilder: (context, error, stackTrace) {
print('❌ Ошибка загрузки стикера: $error');
print('❌ StackTrace: $stackTrace');
return Icon(
Icons.emoji_emotions,
size: width,
color: Colors.grey,
);
return Icon(Icons.emoji_emotions, size: width, color: Colors.grey);
},
),
);
}
print('❌ URL стикера пустой или null');
return Icon(
Icons.emoji_emotions,
size: width,
color: Colors.grey,
);
return Icon(Icons.emoji_emotions, size: width, color: Colors.grey);
}
}
@@ -182,6 +216,7 @@ class ChatScreen extends StatefulWidget {
final bool isChannel;
final int? participantCount;
final bool isDesktopMode;
final int initialUnreadCount;
const ChatScreen({
super.key,
@@ -194,6 +229,7 @@ class ChatScreen extends StatefulWidget {
this.isChannel = false,
this.participantCount,
this.isDesktopMode = false,
this.initialUnreadCount = 0,
});
@override
@@ -225,7 +261,10 @@ class _ChatScreenState extends State<ChatScreen> {
final Map<int, Contact> _contactDetailsCache = {};
final Set<int> _loadingContactIds = {};
final Map<String, String> _lastReadMessageIdByParticipant = {};
String?
_lastReadMessageId; // последнее прочитанное нами сообщение в этом чате
int _initialUnreadCount = 0;
bool _hasUnreadSeparator = false;
int? _actualMyId;
@@ -463,6 +502,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void initState() {
super.initState();
_initialUnreadCount = widget.initialUnreadCount;
_currentContact = widget.contact;
_pinnedMessage =
null; // Будет установлено при получении CONTROL сообщения с event 'pin'
@@ -781,17 +821,37 @@ class _ChatScreenState extends State<ChatScreen> {
_isUserAtBottom = isAtBottom;
_showScrollToBottomNotifier.value = !isAtBottom;
// Если мы внизу и была плашка непрочитанных — считаем, что всё прочитано:
// сбрасываем lastRead и просто скрываем саму плашку (без изменения списка).
if (isAtBottom && _hasUnreadSeparator) {
_lastReadMessageId = null;
_hasUnreadSeparator = false;
if (mounted) {
setState(() {});
}
}
// Проверяем, доскроллил ли пользователь до самого старого сообщения (вверх)
// При reverse: true, последний визуальный элемент (самый большой index) = самое старое сообщение
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, мы вверху (старые сообщения)
final threshold = _chatItems.length > 5 ? 3 : 1; // Загружаем когда осталось 3 элемента до верха
final threshold = _chatItems.length > 5
? 3
: 1; // Загружаем когда осталось 3 элемента до верха
final isNearTop = maxIndex >= _chatItems.length - threshold;
// Если доскроллили близко к верху и есть еще сообщения, загружаем
if (isNearTop && _hasMore && !_isLoadingMore && _messages.isNotEmpty && _oldestLoadedTime != null) {
print('📜 Пользователь доскроллил близко к верху (maxIndex: $maxIndex, total: ${_chatItems.length}), загружаем старые сообщения...');
if (isNearTop &&
_hasMore &&
!_isLoadingMore &&
_messages.isNotEmpty &&
_oldestLoadedTime != null) {
print(
'📜 Пользователь доскроллил близко к верху (maxIndex: $maxIndex, total: ${_chatItems.length}), загружаем старые сообщения...',
);
// Вызываем после build фазы, чтобы избежать setState() во время build
Future.microtask(() {
if (mounted && _hasMore && !_isLoadingMore) {
@@ -970,7 +1030,9 @@ class _ChatScreenState extends State<ChatScreen> {
_oldestLoadedTime = _messages.first.time;
// Предполагаем, что могут быть еще сообщения (будет обновлено после загрузки с сервера)
_hasMore = true;
print('📜 Загружено из кэша: ${_messages.length} сообщений, _oldestLoadedTime=$_oldestLoadedTime');
print(
'📜 Загружено из кэша: ${_messages.length} сообщений, _oldestLoadedTime=$_oldestLoadedTime',
);
}
if (widget.isGroupChat) {
@@ -1066,11 +1128,36 @@ class _ChatScreenState extends State<ChatScreen> {
_oldestLoadedTime = _messages.isNotEmpty ? _messages.first.time : null;
// Если получили максимальное количество сообщений (1000), возможно есть еще
// Также проверяем, есть ли сообщения старше самого старого загруженного
_hasMore = allMessages.length >= 1000 || allMessages.length > _messages.length;
print('📜 Первая загрузка: загружено ${allMessages.length} сообщений, показано ${_messages.length}, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime');
_hasMore =
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();
_isLoadingHistory = false;
});
// После первой загрузки истории скроллим к последнему прочитанному
if (_lastReadMessageId != null) {
_scrollToLastReadMessage();
} else {
// Если нечего читать (нет lastRead), просто остаёмся внизу
_scrollToBottom();
}
_updatePinnedMessage();
// Если чат пустой, загружаем стикер для пустого состояния
@@ -1105,15 +1192,21 @@ class _ChatScreenState extends State<ChatScreen> {
}
Future<void> _loadMore() async {
print('📜 _loadMore() вызвана: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime');
print(
'📜 _loadMore() вызвана: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime',
);
if (_isLoadingMore || !_hasMore) {
print('📜 _loadMore() пропущена: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore');
print(
'📜 _loadMore() пропущена: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore',
);
return;
}
if (_messages.isEmpty || _oldestLoadedTime == null) {
print('📜 _loadMore() пропущена: _messages.isEmpty=${_messages.isEmpty}, _oldestLoadedTime=$_oldestLoadedTime');
print(
'📜 _loadMore() пропущена: _messages.isEmpty=${_messages.isEmpty}, _oldestLoadedTime=$_oldestLoadedTime',
);
_hasMore = false;
return;
}
@@ -1122,13 +1215,16 @@ class _ChatScreenState extends State<ChatScreen> {
setState(() {});
try {
print('📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime');
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
final olderMessages = await ApiService.instance.loadOlderMessagesByTimestamp(
widget.chatId,
_oldestLoadedTime!,
backward: 30,
print(
'📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime',
);
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
final olderMessages = await ApiService.instance
.loadOlderMessagesByTimestamp(
widget.chatId,
_oldestLoadedTime!,
backward: 30,
);
print('📜 Получено ${olderMessages.length} старых сообщений');
@@ -1144,7 +1240,9 @@ class _ChatScreenState extends State<ChatScreen> {
// Фильтруем дубликаты - оставляем только те сообщения, которых еще нет в списке
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) {
// Все сообщения уже есть в списке
@@ -1154,7 +1252,9 @@ class _ChatScreenState extends State<ChatScreen> {
return;
}
print('📜 Добавляем ${newMessages.length} новых старых сообщений (отфильтровано ${olderMessages.length - newMessages.length} дубликатов)');
print(
'📜 Добавляем ${newMessages.length} новых старых сообщений (отфильтровано ${olderMessages.length - newMessages.length} дубликатов)',
);
// Добавляем старые сообщения в начало списка
_messages.insertAll(0, newMessages);
@@ -1207,6 +1307,16 @@ class _ChatScreenState extends State<ChatScreen> {
final List<ChatItem> items = [];
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++) {
final currentMessage = source[i];
final previousMessage = (i > 0) ? source[i - 1] : null;
@@ -1254,12 +1364,20 @@ class _ChatScreenState extends State<ChatScreen> {
isGrouped: isGrouped,
),
);
// Если это последнее прочитанное сообщение, сразу после него вставляем разделитель
if (lastReadIndex != null && i == lastReadIndex) {
items.add(UnreadSeparatorItem());
_hasUnreadSeparator = true;
}
}
_chatItems = items;
// Очищаем ключи для сообщений, которых больше нет в списке
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) {
_messageKeys.remove(id);
}
@@ -1273,7 +1391,8 @@ class _ChatScreenState extends State<ChatScreen> {
// Список доступных ID стикеров для пустого чата
final availableStickerIds = [272821, 295349, 13571];
// Выбираем случайный ID
final random = DateTime.now().millisecondsSinceEpoch % availableStickerIds.length;
final random =
DateTime.now().millisecondsSinceEpoch % availableStickerIds.length;
final selectedStickerId = availableStickerIds[random];
print('🎨 Загружаем стикер для пустого чата (ID: $selectedStickerId)...');
@@ -1310,7 +1429,9 @@ class _ChatScreenState extends State<ChatScreen> {
final sticker = stickers.first as Map<String, dynamic>;
// Сохраняем также stickerId для отправки
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) {
setState(() {
_emptyChatSticker = {
@@ -1318,7 +1439,9 @@ class _ChatScreenState extends State<ChatScreen> {
'stickerId': stickerId, // Сохраняем ID для отправки
};
});
print('✅ Стикер для пустого чата загружен и сохранен (ID: $stickerId)');
print(
'✅ Стикер для пустого чата загружен и сохранен (ID: $stickerId)',
);
}
} else {
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}) {
if (_messages.any((m) => m.id == message.id)) {
print('Сообщение ${message.id} уже существует, пропускаем добавление');
@@ -1669,10 +1821,7 @@ class _ChatScreenState extends State<ChatScreen> {
"message": {
"cid": cid,
"attaches": [
{
"_type": "STICKER",
"stickerId": stickerId,
}
{"_type": "STICKER", "stickerId": stickerId},
],
},
"notify": true,
@@ -1988,12 +2137,31 @@ class _ChatScreenState extends State<ChatScreen> {
);
subtitle = '${participants.length} участников';
} else {
final otherParticipantId = participants.keys
.map((id) => int.parse(id))
.firstWhere((id) => id != _actualMyId, orElse: () => 0);
// Обычный диалог «1 на 1»
final participantIds = participants.keys
.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];
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;
@@ -2001,7 +2169,7 @@ class _ChatScreenState extends State<ChatScreen> {
avatarUrl,
userId: otherParticipantId,
size: 48,
fallbackText: contact?.name ?? chatTitle ?? 'Чат $chatId',
fallbackText: chatName,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
);
@@ -2725,20 +2893,20 @@ class _ChatScreenState extends State<ChatScreen> {
child: CircularProgressIndicator(),
)
: _messages.isEmpty && !widget.isChannel
? _EmptyChatWidget(
sticker: _emptyChatSticker,
onStickerTap: _sendEmptyChatSticker,
)
: AnimatedPadding(
key: const ValueKey('chat_list'),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubic,
padding: EdgeInsets.only(
bottom: MediaQuery.of(
context,
).viewInsets.bottom,
),
child: ScrollablePositionedList.builder(
? _EmptyChatWidget(
sticker: _emptyChatSticker,
onStickerTap: _sendEmptyChatSticker,
)
: AnimatedPadding(
key: const ValueKey('chat_list'),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubic,
padding: EdgeInsets.only(
bottom: MediaQuery.of(
context,
).viewInsets.bottom,
),
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemPositionsListener,
reverse: true,
@@ -3069,6 +3237,10 @@ class _ChatScreenState extends State<ChatScreen> {
return finalMessageWidget;
} else if (item is DateSeparatorItem) {
return _DateSeparatorChip(date: item.date);
} else if (item is UnreadSeparatorItem) {
return _hasUnreadSeparator
? const _UnreadSeparatorChip()
: const SizedBox.shrink();
}
if (isLastVisual && _isLoadingMore) {
return TweenAnimationBuilder<double>(

View File

@@ -4334,6 +4334,7 @@ class _ChatsScreenState extends State<ChatsScreen>
isGroupChat: isGroupChat,
isChannel: isChannel,
participantCount: participantCount,
initialUnreadCount: chat.newMessages,
onChatRemoved: () {
_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:gwid/screens/chat_screen.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 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
@@ -479,117 +480,152 @@ class ChatMessageBubble extends StatelessWidget {
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: textColor.withOpacity(0.08 * messageTextOpacity),
border: Border(
left: BorderSide(
color: textColor.withOpacity(0.3 * messageTextOpacity),
width: 3, // Делаем рамку жирнее для отличия от ответа
// Пытаемся определить 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),
decoration: BoxDecoration(
color: textColor.withOpacity(0.08 * messageTextOpacity),
border: Border(
left: BorderSide(
color: textColor.withOpacity(0.3 * messageTextOpacity),
width: 3, // Делаем рамку жирнее для отличия от ответа
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// "Заголовок" с именем автора и аватаркой
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.forward,
size: 14,
color: textColor.withOpacity(0.6 * messageTextOpacity),
),
const SizedBox(width: 6),
if (forwardedSenderAvatarUrl != null)
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// "Заголовок" с именем автора и аватаркой
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.forward,
size: 14,
color: textColor.withOpacity(0.6 * messageTextOpacity),
),
const SizedBox(width: 6),
if (forwardedSenderAvatarUrl != null)
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
),
),
),
child: ClipOval(
child: Image.network(
forwardedSenderAvatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: textColor.withOpacity(
0.1 * messageTextOpacity,
),
child: Icon(
Icons.person,
size: 12,
child: ClipOval(
child: Image.network(
forwardedSenderAvatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: textColor.withOpacity(
0.5 * messageTextOpacity,
0.1 * messageTextOpacity,
),
),
);
},
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(
0.5 * messageTextOpacity,
),
),
);
},
),
),
)
else
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: textColor.withOpacity(0.1 * messageTextOpacity),
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
),
),
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(0.5 * messageTextOpacity),
),
),
)
else
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: textColor.withOpacity(0.1 * messageTextOpacity),
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
Flexible(
child: Text(
forwardedSenderName,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: textColor.withOpacity(0.9 * messageTextOpacity),
),
),
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(0.5 * messageTextOpacity),
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
forwardedSenderName,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: textColor.withOpacity(0.9 * messageTextOpacity),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 6),
// Содержимое пересланного сообщения (фото и/или текст)
if (attaches.isNotEmpty) ...[
..._buildPhotosWithCaption(
context,
attaches, // Передаем вложения из вложенного сообщения
textColor,
isUltraOptimized,
messageTextOpacity,
],
),
const SizedBox(height: 6),
],
if (text.isNotEmpty)
Text(
text,
style: TextStyle(
color: textColor.withOpacity(0.9 * messageTextOpacity),
fontSize: 14,
// Содержимое пересланного сообщения (фото и/или текст)
if (attaches.isNotEmpty) ...[
..._buildPhotosWithCaption(
context,
attaches, // Передаем вложения из вложенного сообщения
textColor,
isUltraOptimized,
messageTextOpacity,
),
),
],
const SizedBox(height: 6),
],
if (text.isNotEmpty)
Text(
text,
style: TextStyle(
color: textColor.withOpacity(0.9 * messageTextOpacity),
fontSize: 14,
),
),
],
),
),
);
}
@@ -1207,7 +1243,12 @@ class ChatMessageBubble extends StatelessWidget {
!message.isReply &&
!message.isForwarded;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context);
final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor(
isMe,
bubbleColor,
@@ -1774,7 +1815,12 @@ class ChatMessageBubble extends StatelessWidget {
final themeProvider = Provider.of<ThemeProvider>(context);
final isUltraOptimized = themeProvider.ultraOptimizeChats;
final messageOpacity = themeProvider.messageBubbleOpacity;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context);
final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor(
isMe,
bubbleColor,
@@ -3879,7 +3925,8 @@ class ChatMessageBubble extends StatelessWidget {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isMe
? (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);
}
@@ -5146,7 +5193,31 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
}
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();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -5294,7 +5365,9 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.message.text.isNotEmpty)
// Для пересланных сообщений разрешаем копирование даже при пустом text,
// т.к. текст может быть внутри link['message'].
if (widget.message.text.isNotEmpty || widget.message.isForwarded)
_buildActionButton(
icon: Icons.copy_rounded,
text: 'Копировать',

View File

@@ -1,9 +1,11 @@
import 'dart:async';
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_avatar_widget.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 {
final int userId;
@@ -39,6 +41,7 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
final ScrollController _nameScrollController = ScrollController();
String? _localDescription;
StreamSubscription? _changesSubscription;
bool _isOpeningChat = false;
String get _displayName {
final displayName = getContactDisplayName(
@@ -243,8 +246,9 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
_buildActionButton(
icon: Icons.message,
label: 'Написать',
onPressed: null,
onPressed: _isOpeningChat ? null : _handleWriteMessage,
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;
});
}
}
}
}