Починка показа людей для перессылки(не отдельный экран, потом), возможность написать тому кто переслал сообщение, какая то дохлая система показа и перехода к последнему прочитанному сообщению(не работает), копирование пересланных сообщений
This commit is contained in:
@@ -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;
|
||||||
@@ -68,12 +106,14 @@ class _EmptyChatWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
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}');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -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,15 +153,17 @@ 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 - это для анимации, но пока используем статичное изображение
|
||||||
// Если есть url, используем его, иначе пробуем lottieUrl
|
// Если есть url, используем его, иначе пробуем lottieUrl
|
||||||
final imageUrl = url ?? lottieUrl;
|
final imageUrl = url ?? lottieUrl;
|
||||||
|
|
||||||
print('🎨 Используемый URL для стикера: $imageUrl');
|
print('🎨 Используемый URL для стикера: $imageUrl');
|
||||||
|
|
||||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
@@ -136,12 +176,14 @@ 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
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
loadingProgress.expectedTotalBytes!
|
loadingProgress.expectedTotalBytes!
|
||||||
: null,
|
: 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) {
|
||||||
@@ -964,13 +1024,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_messages.clear();
|
_messages.clear();
|
||||||
_messages.addAll(cachedMessages);
|
_messages.addAll(cachedMessages);
|
||||||
|
|
||||||
// Устанавливаем _oldestLoadedTime и _hasMore для кэшированных сообщений
|
// Устанавливаем _oldestLoadedTime и _hasMore для кэшированных сообщений
|
||||||
if (_messages.isNotEmpty) {
|
if (_messages.isNotEmpty) {
|
||||||
_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) {
|
||||||
@@ -982,7 +1044,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_isLoadingHistory = false;
|
_isLoadingHistory = false;
|
||||||
});
|
});
|
||||||
_updatePinnedMessage();
|
_updatePinnedMessage();
|
||||||
|
|
||||||
// Если чат пустой, загружаем стикер для пустого состояния
|
// Если чат пустой, загружаем стикер для пустого состояния
|
||||||
if (_messages.isEmpty && !widget.isChannel) {
|
if (_messages.isEmpty && !widget.isChannel) {
|
||||||
_loadEmptyChatSticker();
|
_loadEmptyChatSticker();
|
||||||
@@ -1066,13 +1128,38 @@ 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();
|
||||||
|
|
||||||
// Если чат пустой, загружаем стикер для пустого состояния
|
// Если чат пустой, загружаем стикер для пустого состояния
|
||||||
if (_messages.isEmpty && !widget.isChannel) {
|
if (_messages.isEmpty && !widget.isChannel) {
|
||||||
_loadEmptyChatSticker();
|
_loadEmptyChatSticker();
|
||||||
@@ -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,14 +1215,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime');
|
print(
|
||||||
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
|
'📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime',
|
||||||
final olderMessages = await ApiService.instance.loadOlderMessagesByTimestamp(
|
|
||||||
widget.chatId,
|
|
||||||
_oldestLoadedTime!,
|
|
||||||
backward: 30,
|
|
||||||
);
|
);
|
||||||
|
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
|
||||||
|
final olderMessages = await ApiService.instance
|
||||||
|
.loadOlderMessagesByTimestamp(
|
||||||
|
widget.chatId,
|
||||||
|
_oldestLoadedTime!,
|
||||||
|
backward: 30,
|
||||||
|
);
|
||||||
|
|
||||||
print('📜 Получено ${olderMessages.length} старых сообщений');
|
print('📜 Получено ${olderMessages.length} старых сообщений');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -1144,8 +1240,10 @@ 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) {
|
||||||
// Все сообщения уже есть в списке
|
// Все сообщения уже есть в списке
|
||||||
_hasMore = false;
|
_hasMore = false;
|
||||||
@@ -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,9 +1391,10 @@ 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)...');
|
||||||
final seq = ApiService.instance.sendRawRequest(28, {
|
final seq = ApiService.instance.sendRawRequest(28, {
|
||||||
"type": "STICKER",
|
"type": "STICKER",
|
||||||
@@ -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} уже существует, пропускаем добавление');
|
||||||
@@ -1663,16 +1815,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
try {
|
try {
|
||||||
print('🎨 Отправляем стикер (ID: $stickerId) в чат ${widget.chatId}');
|
print('🎨 Отправляем стикер (ID: $stickerId) в чат ${widget.chatId}');
|
||||||
final cid = DateTime.now().millisecondsSinceEpoch;
|
final cid = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
final payload = {
|
final payload = {
|
||||||
"chatId": widget.chatId,
|
"chatId": widget.chatId,
|
||||||
"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,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2725,20 +2893,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
)
|
||||||
: _messages.isEmpty && !widget.isChannel
|
: _messages.isEmpty && !widget.isChannel
|
||||||
? _EmptyChatWidget(
|
? _EmptyChatWidget(
|
||||||
sticker: _emptyChatSticker,
|
sticker: _emptyChatSticker,
|
||||||
onStickerTap: _sendEmptyChatSticker,
|
onStickerTap: _sendEmptyChatSticker,
|
||||||
)
|
)
|
||||||
: AnimatedPadding(
|
: AnimatedPadding(
|
||||||
key: const ValueKey('chat_list'),
|
key: const ValueKey('chat_list'),
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOutCubic,
|
curve: Curves.easeInOutCubic,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: MediaQuery.of(
|
bottom: MediaQuery.of(
|
||||||
context,
|
context,
|
||||||
).viewInsets.bottom,
|
).viewInsets.bottom,
|
||||||
),
|
),
|
||||||
child: ScrollablePositionedList.builder(
|
child: ScrollablePositionedList.builder(
|
||||||
itemScrollController: _itemScrollController,
|
itemScrollController: _itemScrollController,
|
||||||
itemPositionsListener: _itemPositionsListener,
|
itemPositionsListener: _itemPositionsListener,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,117 +480,152 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
// Пытаемся определить userId оригинального отправителя для открытия панели профиля
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
int? originalSenderId;
|
||||||
decoration: BoxDecoration(
|
if (forwardedMessage['sender'] is int) {
|
||||||
color: textColor.withOpacity(0.08 * messageTextOpacity),
|
originalSenderId = forwardedMessage['sender'] as int;
|
||||||
border: Border(
|
}
|
||||||
left: BorderSide(
|
|
||||||
color: textColor.withOpacity(0.3 * messageTextOpacity),
|
void handleTap() {
|
||||||
width: 3, // Делаем рамку жирнее для отличия от ответа
|
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(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
// "Заголовок" с именем автора и аватаркой
|
||||||
// "Заголовок" с именем автора и аватаркой
|
Row(
|
||||||
Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Icon(
|
||||||
Icon(
|
Icons.forward,
|
||||||
Icons.forward,
|
size: 14,
|
||||||
size: 14,
|
color: textColor.withOpacity(0.6 * messageTextOpacity),
|
||||||
color: textColor.withOpacity(0.6 * messageTextOpacity),
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
const SizedBox(width: 6),
|
if (forwardedSenderAvatarUrl != null)
|
||||||
if (forwardedSenderAvatarUrl != null)
|
Container(
|
||||||
Container(
|
width: 20,
|
||||||
width: 20,
|
height: 20,
|
||||||
height: 20,
|
margin: const EdgeInsets.only(right: 6),
|
||||||
margin: const EdgeInsets.only(right: 6),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
shape: BoxShape.circle,
|
||||||
shape: BoxShape.circle,
|
border: Border.all(
|
||||||
border: Border.all(
|
color: textColor.withOpacity(0.2 * messageTextOpacity),
|
||||||
color: textColor.withOpacity(0.2 * messageTextOpacity),
|
width: 1,
|
||||||
width: 1,
|
),
|
||||||
),
|
),
|
||||||
),
|
child: ClipOval(
|
||||||
child: ClipOval(
|
child: Image.network(
|
||||||
child: Image.network(
|
forwardedSenderAvatarUrl,
|
||||||
forwardedSenderAvatarUrl,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
errorBuilder: (context, error, stackTrace) {
|
||||||
errorBuilder: (context, error, stackTrace) {
|
return Container(
|
||||||
return Container(
|
|
||||||
color: textColor.withOpacity(
|
|
||||||
0.1 * messageTextOpacity,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 12,
|
|
||||||
color: textColor.withOpacity(
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
Flexible(
|
||||||
else
|
child: Text(
|
||||||
Container(
|
forwardedSenderName,
|
||||||
width: 20,
|
style: TextStyle(
|
||||||
height: 20,
|
fontSize: 13,
|
||||||
margin: const EdgeInsets.only(right: 6),
|
fontWeight: FontWeight.bold,
|
||||||
decoration: BoxDecoration(
|
color: textColor.withOpacity(0.9 * messageTextOpacity),
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: textColor.withOpacity(0.1 * messageTextOpacity),
|
|
||||||
border: Border.all(
|
|
||||||
color: textColor.withOpacity(0.2 * messageTextOpacity),
|
|
||||||
width: 1,
|
|
||||||
),
|
),
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 12,
|
|
||||||
color: textColor.withOpacity(0.5 * messageTextOpacity),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
const SizedBox(height: 6),
|
||||||
],
|
|
||||||
if (text.isNotEmpty)
|
// Содержимое пересланного сообщения (фото и/или текст)
|
||||||
Text(
|
if (attaches.isNotEmpty) ...[
|
||||||
text,
|
..._buildPhotosWithCaption(
|
||||||
style: TextStyle(
|
context,
|
||||||
color: textColor.withOpacity(0.9 * messageTextOpacity),
|
attaches, // Передаем вложения из вложенного сообщения
|
||||||
fontSize: 14,
|
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.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: 'Копировать',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user