diff --git a/lib/api/api_service_chats.dart b/lib/api/api_service_chats.dart index 6371951..cd6383a 100644 --- a/lib/api/api_service_chats.dart +++ b/lib/api/api_service_chats.dart @@ -190,6 +190,42 @@ extension ApiServiceChats on ApiService { print('Переименовываем группу $chatId в: $newName'); } + /// Обновляет/добавляет чат в локный кэш `_lastChatsPayload['chats']`, + /// чтобы остальные экраны (настройки группы, экран чата и т.п.) сразу + /// видели новые поля (admins, options, participants и т.д.). + void updateChatInCacheFromJson(Map chatJson) { + try { + // Если кэш ещё не инициализирован (например, сразу после запуска), + // создаём минимальную структуру, чтобы новый чат тоже оказался в ней. + if (_lastChatsPayload == null) { + _lastChatsPayload = { + 'chats': [], + 'contacts': [], + 'profile': null, + 'presence': null, + 'config': null, + }; + } + + final chats = _lastChatsPayload!['chats'] as List; + + final chatId = chatJson['id']; + if (chatId == null) return; + + final existingIndex = chats.indexWhere( + (c) => c is Map && c['id'] == chatId, + ); + + if (existingIndex != -1) { + chats[existingIndex] = chatJson; + } else { + chats.insert(0, chatJson); + } + } catch (e) { + print('Не удалось обновить кэш чатов из chatJson: $e'); + } + } + /// Создает/перегенерирует пригласительную ссылку для группы. /// Сервер ожидает payload вида: /// {"chatId": -69330645868731, "revokePrivateLink": true} @@ -198,10 +234,7 @@ extension ApiServiceChats on ApiService { int chatId, { bool revokePrivateLink = true, }) async { - final payload = { - "chatId": chatId, - "revokePrivateLink": revokePrivateLink, - }; + final payload = {"chatId": chatId, "revokePrivateLink": revokePrivateLink}; print('Создаем пригласительную ссылку для группы $chatId: $payload'); @@ -216,7 +249,9 @@ extension ApiServiceChats on ApiService { final error = response['payload']; print('Ошибка создания пригласительной ссылки: $error'); final message = - error?['localizedMessage'] ?? error?['message'] ?? 'Неизвестная ошибка'; + error?['localizedMessage'] ?? + error?['message'] ?? + 'Неизвестная ошибка'; throw Exception(message); } @@ -230,20 +265,8 @@ extension ApiServiceChats on ApiService { } // Обновим кэш чатов, если сервер вернул полный объект чата - try { - if (chat != null) { - final chats = _lastChatsPayload?['chats'] as List?; - if (chats != null) { - final index = chats.indexWhere( - (c) => c is Map && c['id'] == chat['id'], - ); - if (index >= 0) { - chats[index] = chat; - } - } - } - } catch (e) { - print('Не удалось обновить кэш чатов после создания ссылки: $e'); + if (chat != null) { + updateChatInCacheFromJson(chat); } return link; diff --git a/lib/models/message.dart b/lib/models/message.dart index 07be68d..316250e 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -88,6 +88,8 @@ class Message { bool get isDeleted => status == 'DELETED'; bool get isReply => link != null && link!['type'] == 'REPLY'; bool get isForwarded => link != null && link!['type'] == 'FORWARD'; + bool get hasFileAttach => + attaches.any((a) => (a['_type'] ?? a['type']) == 'FILE'); bool canEdit(int currentUserId) { if (isDeleted) return false; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index aa43b8d..71037ba 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1693,52 +1693,80 @@ class _ChatScreenState extends State { ), ); }, - pageBuilder: (context, animation, secondaryAnimation) => AlertDialog( - title: const Text('Очистить историю чата'), - content: Text( - 'Вы уверены, что хотите очистить историю чата с ${_currentContact.name}? Это действие нельзя отменить.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Отмена'), - ), - FilledButton( - onPressed: () async { - Navigator.of(context).pop(); - try { - await ApiService.instance.clearChatHistory(widget.chatId); - if (mounted) { - setState(() { - _messages.clear(); - _chatItems.clear(); - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('История чата очищена'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Ошибка очистки истории: $e'), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } - } - }, - style: FilledButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - child: const Text('Очистить'), - ), - ], - ), + pageBuilder: (context, animation, secondaryAnimation) { + bool forAll = false; + return StatefulBuilder( + builder: (context, setStateDialog) { + return AlertDialog( + title: const Text('Очистить историю чата'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Вы уверены, что хотите очистить историю чата с ${_currentContact.name}? Это действие нельзя отменить.', + ), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: forAll, + onChanged: (value) { + setStateDialog(() { + forAll = value ?? false; + }); + }, + title: const Text('Удалить сообщения для всех'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + FilledButton( + onPressed: () async { + Navigator.of(context).pop(); + try { + await ApiService.instance.clearChatHistory( + widget.chatId, + forAll: forAll, + ); + if (mounted) { + setState(() { + _messages.clear(); + _chatItems.clear(); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('История чата очищена'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка очистки истории: $e'), + backgroundColor: + Theme.of(context).colorScheme.error, + ), + ); + } + } + }, + style: FilledButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Очистить'), + ), + ], + ); + }, + ); + }, ); } @@ -1757,54 +1785,88 @@ class _ChatScreenState extends State { ), ); }, - pageBuilder: (context, animation, secondaryAnimation) => AlertDialog( - title: const Text('Удалить чат'), - content: Text( - 'Вы уверены, что хотите удалить чат с ${_currentContact.name}? Это действие нельзя отменить.', //1231231233 - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Отмена'), - ), - FilledButton( - onPressed: () async { - Navigator.of(context).pop(); - try { - print('Имитация удаления чата ID: ${widget.chatId}'); - await Future.delayed(const Duration(milliseconds: 500)); + pageBuilder: (context, animation, secondaryAnimation) { + bool forAll = false; + return StatefulBuilder( + builder: (context, setStateDialog) { + return AlertDialog( + title: const Text('Удалить чат'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Вы уверены, что хотите удалить чат с ${_currentContact.name}? Это действие нельзя отменить.', + ), + const SizedBox(height: 12), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: forAll, + onChanged: (value) { + setStateDialog(() { + forAll = value ?? false; + }); + }, + title: const Text('Удалить сообщения для всех'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + FilledButton( + onPressed: () async { + Navigator.of(context).pop(); + try { + // Удаляем историю чата (opcode 54) + await ApiService.instance.clearChatHistory( + widget.chatId, + forAll: forAll, + ); - if (mounted) { - Navigator.of(context).pop(); + // Отписываемся от чата (opcode 75) + await ApiService.instance.subscribeToChat( + widget.chatId, + false, + ); - widget.onChatUpdated?.call(); + if (mounted) { + Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Чат удален'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Ошибка удаления чата: $e'), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } - } - }, - style: FilledButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - child: const Text('Удалить'), - ), - ], - ), + widget.onChatUpdated?.call(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Чат удален'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка удаления чата: $e'), + backgroundColor: + Theme.of(context).colorScheme.error, + ), + ); + } + } + }, + style: FilledButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Удалить'), + ), + ], + ); + }, + ); + }, ); } @@ -3036,7 +3098,9 @@ class _ChatScreenState extends State { Text( _replyingToMessage!.text.isNotEmpty ? _replyingToMessage!.text - : 'Фото', + : (_replyingToMessage!.hasFileAttach + ? 'Файл' + : 'Фото'), style: TextStyle( fontSize: 13, color: Theme.of( @@ -3402,7 +3466,9 @@ class _ChatScreenState extends State { Text( _replyingToMessage!.text.isNotEmpty ? _replyingToMessage!.text - : 'Фото', + : (_replyingToMessage!.hasFileAttach + ? 'Файл' + : 'Фото'), style: TextStyle( fontSize: 13, color: Theme.of( diff --git a/lib/screens/chats_screen.dart b/lib/screens/chats_screen.dart index b2e92f0..9f9a941 100644 --- a/lib/screens/chats_screen.dart +++ b/lib/screens/chats_screen.dart @@ -332,7 +332,10 @@ class _ChatsScreenState extends State final chatIdValue = payload['chatId']; final int? chatId = chatIdValue != null ? chatIdValue as int? : null; - if (opcode == 272 || opcode == 274) { + // Для части опкодов (48, 55, 135, 272, 274) нам не нужен явный chatId в корне + // payload, поэтому не отбрасываем их, даже если chatId == null. + if (opcode == 272 || opcode == 274 || opcode == 48 || opcode == 55 || opcode == 135) { + // продолжаем обработку ниже } else if (chatId == null) { return; } @@ -341,11 +344,44 @@ class _ChatsScreenState extends State _setTypingForChat(chatId); } + // Ответ на отправку сообщения (opcode 64). + // Если сервер прислал в payload полный объект chat (как при создании группы + // через CONTROL new), добавляем/обновляем чат локально. + if (opcode == 64 && cmd == 1 && payload['chat'] is Map) { + final chatJson = payload['chat'] as Map; + final newChat = Chat.fromJson(chatJson); + + // Обновляем также глобальный кэш ApiService, чтобы настройки группы и + // другие экраны сразу видели корректные права/админов/опции. + ApiService.instance.updateChatInCacheFromJson(chatJson); + + if (mounted) { + setState(() { + final existingIndex = + _allChats.indexWhere((chat) => chat.id == newChat.id); + + if (existingIndex != -1) { + _allChats[existingIndex] = newChat; + } else { + final savedIndex = _allChats.indexWhere(_isSavedMessages); + final insertIndex = savedIndex >= 0 ? savedIndex + 1 : 0; + _allChats.insert(insertIndex, newChat); + } + + _filterChats(); + }); + } + } + + // Новый входящий месседж (opcode 128) — обновляем последний месседж и + // двигаем чат вверх БЕЗ полного рефреша с сервера. Если такого чата ещё нет + // (нам написал новый пользователь), создаём его на основе payload.chat. if (opcode == 128 && chatId != null) { final newMessage = Message.fromJson(payload['message']); ApiService.instance.clearCacheForChat(chatId); final int chatIndex = _allChats.indexWhere((chat) => chat.id == chatId); + if (chatIndex != -1) { final oldChat = _allChats[chatIndex]; final updatedChat = oldChat.copyWith( @@ -377,6 +413,21 @@ class _ChatsScreenState extends State } _filterChats(); }); + } else if (payload['chat'] is Map) { + // Чат ещё не известен клиенту — создаём его на основе payload.chat. + final chatJson = payload['chat'] as Map; + final newChat = Chat.fromJson(chatJson); + + // Обновляем глобальный кэш ApiService, чтобы дальше во всех экранах + // был один и тот же объект чата. + ApiService.instance.updateChatInCacheFromJson(chatJson); + + setState(() { + final savedIndex = _allChats.indexWhere(_isSavedMessages); + final insertIndex = savedIndex >= 0 ? savedIndex + 1 : 0; + _allChats.insert(insertIndex, newChat); + _filterChats(); + }); } } else if (opcode == 67 && chatId != null) { final editedMessage = Message.fromJson(payload['message']); @@ -446,10 +497,6 @@ class _ChatsScreenState extends State } } - if (opcode == 129 && chatId != null) { - _setTypingForChat(chatId); - } - if (opcode == 132) { final bool isOnline = payload['online'] == true; @@ -536,10 +583,96 @@ class _ChatsScreenState extends State if (mounted) setState(() {}); } + // Создание/обновление группы (opcode 48) — стараемся обновить список чатов + // локально по объекту chat из payload, без повторного getChatsAndContacts. if (opcode == 48) { - print('Получен ответ на создание группы: $payload'); + print('Получен ответ на создание/обновление группы: $payload'); - _refreshChats(); + final chatJson = payload['chat'] as Map?; + final chatsJson = payload['chats'] as List?; + + // Приоритет: одиночный chat, дальше — первый из списка chats. + Map? effectiveChatJson = chatJson; + if (effectiveChatJson == null && chatsJson != null && chatsJson.isNotEmpty) { + final first = chatsJson.first; + if (first is Map) { + effectiveChatJson = first; + } + } + + if (effectiveChatJson != null) { + final newChat = Chat.fromJson(effectiveChatJson); + + // Синхронизируем объект чата и в глобальном кэше ApiService. + ApiService.instance.updateChatInCacheFromJson(effectiveChatJson); + if (mounted) { + setState(() { + final existingIndex = + _allChats.indexWhere((chat) => chat.id == newChat.id); + + if (existingIndex != -1) { + _allChats[existingIndex] = newChat; + } else { + // Вставляем новый чат сразу после "Избранного", если оно есть. + final savedIndex = _allChats.indexWhere(_isSavedMessages); + final insertIndex = savedIndex >= 0 ? savedIndex + 1 : 0; + _allChats.insert(insertIndex, newChat); + } + + _filterChats(); + }); + } + } else { + // Fallback: если сервер не прислал chat, обновляемся старым способом. + _refreshChats(); + } + } + + // Изменение параметров чата (rename, invite‑link и т.п.) приходит с opcode 55. + // В payload обычно лежит обновленный объект chat. + if (opcode == 55 && cmd == 1) { + final chatJson = payload['chat'] as Map?; + if (chatJson != null) { + final updatedChat = Chat.fromJson(chatJson); + + // Обновляем глобальный кэш ApiService, чтобы настройки группы и др. + // сразу видели новые права/линки/название. + ApiService.instance.updateChatInCacheFromJson(chatJson); + if (mounted) { + setState(() { + final existingIndex = + _allChats.indexWhere((chat) => chat.id == updatedChat.id); + + if (existingIndex != -1) { + _allChats[existingIndex] = updatedChat; + } else { + final savedIndex = _allChats.indexWhere(_isSavedMessages); + final insertIndex = savedIndex >= 0 ? savedIndex + 1 : 0; + _allChats.insert(insertIndex, updatedChat); + } + + _filterChats(); + }); + } + } + } + + // Выход из группы: сервер сначала шлёт opcode 135 с chat.status = REMOVED, + // а уже потом opcode 58 с CONTROL-сообщением "leave". Для обновления списка + // чатов нам важен именно 135. + if (opcode == 135 && payload['chat'] is Map) { + final removedChat = payload['chat'] as Map; + final int? removedChatId = removedChat['id'] as int?; + final String? status = removedChat['status'] as String?; + + if (removedChatId != null && status == 'REMOVED') { + if (mounted) { + setState(() { + _allChats.removeWhere((chat) => chat.id == removedChatId); + _filteredChats.removeWhere((chat) => chat.id == removedChatId); + }); + } + } } if (opcode == 272) { @@ -672,6 +805,14 @@ class _ChatsScreenState extends State }); } + void _removeChatLocally(int chatId) { + if (!mounted) return; + setState(() { + _allChats.removeWhere((c) => c.id == chatId); + _filteredChats.removeWhere((c) => c.id == chatId); + }); + } + final Map _typingDecayTimers = {}; final Set _typingChats = {}; final Set _onlineChats = {}; @@ -2367,7 +2508,7 @@ class _ChatsScreenState extends State isChannel: isChannel, participantCount: participantCount, onChatUpdated: () { - print('Chat updated, но не обновляем список чатов...'); + _removeChatLocally(chat.id); }, ), ), @@ -2513,7 +2654,9 @@ class _ChatsScreenState extends State } else if (chat.title?.isNotEmpty == true) { title = chat.title!; } else { - title = "ID ${otherParticipantId ?? 0}"; + // Контакт ещё не загружен — показываем плейсхолдер и + // параллельно запускаем загрузку. + title = "Данные загружаются..."; if (otherParticipantId != null && otherParticipantId != 0) { _loadMissingContact(otherParticipantId); } @@ -4001,7 +4144,8 @@ class _ChatsScreenState extends State } else if (chat.title?.isNotEmpty == true) { title = chat.title!; } else { - title = "ID $otherParticipantId"; + // Контакт ещё не загружен — плейсхолдер и асинхронная подзагрузка. + title = "Данные загружаются..."; _loadMissingContact(otherParticipantId); } avatarUrl = contact?.photoBaseUrl; @@ -4075,7 +4219,7 @@ class _ChatsScreenState extends State isChannel: isChannel, participantCount: participantCount, onChatUpdated: () { - print('Chat updated, но не обновляем список чатов...'); + _removeChatLocally(chat.id); }, ), ), @@ -4751,7 +4895,15 @@ class _AddChatsToFolderDialogState extends State<_AddChatsToFolderDialog> { ), ], ), - title: Text(title, style: const TextStyle(fontSize: 16)), + title: Text( + title, + style: TextStyle( + fontSize: 16, + fontStyle: title == 'Данные загружаются...' + ? FontStyle.italic + : FontStyle.normal, + ), + ), subtitle: isGroupChat && chat.participantIds.length > 2 ? Text( '${chat.participantIds.length} участников',