diff --git a/lib/api/api_service_auth.dart b/lib/api/api_service_auth.dart index 17635be..ccef8f7 100644 --- a/lib/api/api_service_auth.dart +++ b/lib/api/api_service_auth.dart @@ -190,7 +190,6 @@ extension ApiServiceAuth on ApiService { authToken = currentAccount.token; userId = currentAccount.userId; - _messageCache.clear(); _messageQueue.clear(); _lastChatsPayload = null; _chatsFetchedInThisSession = false; @@ -257,4 +256,3 @@ extension ApiServiceAuth on ApiService { } } } - diff --git a/lib/api/api_service_chats.dart b/lib/api/api_service_chats.dart index a5a68b4..ffe5832 100644 --- a/lib/api/api_service_chats.dart +++ b/lib/api/api_service_chats.dart @@ -1,6 +1,119 @@ part of 'api_service.dart'; extension ApiServiceChats on ApiService { + Future _sendAuthRequestAfterHandshake() async { + if (authToken == null) { + print("Токен не найден, пропускаем автоматическую авторизацию"); + return; + } + + if (_chatsFetchedInThisSession) { + print("Авторизация уже выполнена в этой сессии, пропускаем"); + return; + } + + try { + await _ensureCacheServicesInitialized(); + + final prefs = await SharedPreferences.getInstance(); + final deviceId = + prefs.getString('spoof_deviceid') ?? generateRandomDeviceId(); + + if (prefs.getString('spoof_deviceid') == null) { + await prefs.setString('spoof_deviceid', deviceId); + } + + final payload = { + "chatsCount": 100, + "chatsSync": 0, + "contactsSync": 0, + "draftsSync": 0, + "interactive": true, + "presenceSync": 0, + "token": authToken, + }; + + if (userId != null) { + payload["userId"] = userId; + } + + print("Автоматически отправляем opcode 19 для авторизации..."); + final int chatSeq = _sendMessage(19, payload); + final chatResponse = await messages.firstWhere( + (msg) => msg['seq'] == chatSeq, + ); + + if (chatResponse['cmd'] == 1) { + print("✅ Авторизация (opcode 19) успешна. Сессия ГОТОВА."); + _isSessionReady = true; + + _connectionStatusController.add("ready"); + _updateConnectionState( + conn_state.ConnectionState.ready, + message: 'Авторизация успешна', + ); + + final profile = chatResponse['payload']?['profile']; + final contactProfile = profile?['contact']; + + if (contactProfile != null && contactProfile['id'] != null) { + print( + "[_sendAuthRequestAfterHandshake] ✅ Профиль и ID пользователя найдены. ID: ${contactProfile['id']}. ЗАПУСКАЕМ АНАЛИТИКУ.", + ); + _userId = contactProfile['id']; + _sessionId = DateTime.now().millisecondsSinceEpoch; + _lastActionTime = _sessionId; + + sendNavEvent('COLD_START'); + + _sendInitialSetupRequests(); + } + + if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) { + _onlineCompleter!.complete(); + } + + final chatListJson = chatResponse['payload']?['chats'] ?? []; + final contactListJson = chatResponse['payload']?['contacts'] ?? []; + final presence = chatResponse['payload']?['presence']; + final config = chatResponse['payload']?['config']; + + if (presence != null) { + updatePresenceData(presence); + } + + if (config != null) { + _processServerPrivacyConfig(config); + } + + final result = { + 'chats': chatListJson, + 'contacts': contactListJson, + 'profile': profile, + 'presence': presence, + 'config': config, + }; + _lastChatsPayload = result; + + final contacts = contactListJson + .map((json) => Contact.fromJson(json)) + .toList(); + updateContactCache(contacts); + _lastChatsAt = DateTime.now(); + _preloadContactAvatars(contacts); + unawaited( + _chatCacheService.cacheChats( + chatListJson.cast>(), + ), + ); + unawaited(_chatCacheService.cacheContacts(contacts)); + _chatsFetchedInThisSession = true; + } + } catch (e) { + print("Ошибка при автоматической авторизации: $e"); + } + } + void createGroup(String name, List participantIds) { final payload = {"name": name, "participantIds": participantIds}; _sendMessage(48, payload); @@ -357,22 +470,29 @@ extension ApiServiceChats on ApiService { return result; } - final contactIds = {}; - for (var chatJson in chatListJson) { - final participants = chatJson['participants'] as Map; - contactIds.addAll(participants.keys.map((id) => int.parse(id))); + List contactListJson = + chatResponse['payload']?['contacts'] ?? []; + + if (contactListJson.isEmpty) { + final contactIds = {}; + for (var chatJson in chatListJson) { + final participants = + chatJson['participants'] as Map? ?? {}; + contactIds.addAll(participants.keys.map((id) => int.parse(id))); + } + + if (contactIds.isNotEmpty) { + final int contactSeq = _sendMessage(32, { + "contactIds": contactIds.toList(), + }); + final contactResponse = await messages.firstWhere( + (msg) => msg['seq'] == contactSeq, + ); + + contactListJson = contactResponse['payload']?['contacts'] ?? []; + } } - final int contactSeq = _sendMessage(32, { - "contactIds": contactIds.toList(), - }); - final contactResponse = await messages.firstWhere( - (msg) => msg['seq'] == contactSeq, - ); - - final List contactListJson = - contactResponse['payload']?['contacts'] ?? []; - if (presence != null) { updatePresenceData(presence); } diff --git a/lib/api/api_service_connection.dart b/lib/api/api_service_connection.dart index e6f747b..be95e46 100644 --- a/lib/api/api_service_connection.dart +++ b/lib/api/api_service_connection.dart @@ -363,11 +363,15 @@ extension ApiServiceConnection on ApiService { ); _startHealthMonitoring(); - if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) { - _onlineCompleter!.complete(); - } _startPinging(); _processMessageQueue(); + + if (authToken != null && !_chatsFetchedInThisSession) { + print( + "Токен найден, автоматически запускаем авторизацию (opcode 19)...", + ); + unawaited(_sendAuthRequestAfterHandshake()); + } } if (decodedMessage is Map && decodedMessage['cmd'] == 3) { @@ -670,8 +674,6 @@ extension ApiServiceConnection on ApiService { _onlineCompleter = Completer(); _chatsFetchedInThisSession = false; - clearAllCaches(); - _currentUrlIndex = 0; _reconnectDelaySeconds = (_reconnectDelaySeconds * 2).clamp(1, 30); @@ -721,7 +723,6 @@ extension ApiServiceConnection on ApiService { _currentUrlIndex = 0; _onlineCompleter = Completer(); - clearAllCaches(); _messageQueue.clear(); _presenceData.clear(); diff --git a/lib/api/api_service_contacts.dart b/lib/api/api_service_contacts.dart index a98af52..9c58fb0 100644 --- a/lib/api/api_service_contacts.dart +++ b/lib/api/api_service_contacts.dart @@ -105,6 +105,20 @@ extension ApiServiceContacts on ApiService { return; } + if (!_isSessionOnline || !_isSessionReady) { + print( + 'ApiService: сессия еще не готова для запроса заблокированных контактов, ждем...', + ); + await waitUntilOnline(); + + if (!_isSessionReady) { + print( + 'ApiService: сессия все еще не готова после ожидания, отменяем запрос', + ); + return; + } + } + _isLoadingBlockedContacts = true; print('ApiService: запрашиваем заблокированные контакты'); _sendMessage(36, {'status': 'BLOCKED', 'count': 100, 'from': 0}); @@ -278,12 +292,23 @@ extension ApiServiceContacts on ApiService { Future> fetchContactsByIds(List contactIds) async { if (contactIds.isEmpty) { + print( + '⚠️ [fetchContactsByIds] Пустой список contactIds - пропускаем запрос', + ); return []; } - print('Запрашиваем данные для ${contactIds.length} контактов...'); + print( + '📡 [fetchContactsByIds] Запрашиваем данные для ${contactIds.length} контактов...', + ); + print( + '📡 [fetchContactsByIds] IDs: ${contactIds.take(10).join(', ')}${contactIds.length > 10 ? '...' : ''}', + ); try { final int contactSeq = _sendMessage(32, {"contactIds": contactIds}); + print( + '📤 [fetchContactsByIds] Отправлен опкод 32 с seq=$contactSeq и ${contactIds.length} ID', + ); final contactResponse = await messages .firstWhere((msg) => msg['seq'] == contactSeq) @@ -291,7 +316,7 @@ extension ApiServiceContacts on ApiService { if (contactResponse['cmd'] == 3) { print( - "Ошибка при получении контактов по ID: ${contactResponse['payload']}", + "❌ [fetchContactsByIds] Ошибка при получении контактов: ${contactResponse['payload']}", ); return []; } @@ -302,13 +327,29 @@ extension ApiServiceContacts on ApiService { .map((json) => Contact.fromJson(json)) .toList(); + print( + '📦 [fetchContactsByIds] Получено ${contacts.length} контактов из ${contactIds.length} запрошенных', + ); + + if (contacts.length < contactIds.length) { + final receivedIds = contacts.map((c) => c.id).toSet(); + final missingIds = contactIds + .where((id) => !receivedIds.contains(id)) + .toList(); + print( + '⚠️ [fetchContactsByIds] Отсутствуют ${missingIds.length} контактов: ${missingIds.take(5).join(', ')}${missingIds.length > 5 ? '...' : ''}', + ); + } + for (final contact in contacts) { _contactCache[contact.id] = contact; } - print("Получены и закэшированы данные для ${contacts.length} контактов."); + print( + "✅ [fetchContactsByIds] Закэшированы данные для ${contacts.length} контактов", + ); return contacts; } catch (e) { - print('Исключение при получении контактов по ID: $e'); + print('❌ [fetchContactsByIds] Исключение при получении контактов: $e'); return []; } } diff --git a/lib/chat_screen.dart b/lib/chat_screen.dart index d9588ad..239aed9 100644 --- a/lib/chat_screen.dart +++ b/lib/chat_screen.dart @@ -164,6 +164,13 @@ class _ChatScreenState extends State { if (contacts.isNotEmpty && mounted) { final contact = contacts.first; _contactDetailsCache[contact.id] = contact; + + final allChatContacts = _contactDetailsCache.values.toList(); + await ChatCacheService().cacheChatContacts( + widget.chatId, + allChatContacts, + ); + setState(() {}); } } catch (e) { @@ -173,6 +180,152 @@ class _ChatScreenState extends State { } } + Future _loadGroupParticipants() async { + try { + print( + '🔍 [_loadGroupParticipants] Начинаем загрузку участников группы...', + ); + + final chatData = ApiService.instance.lastChatsPayload; + if (chatData == null) { + print('❌ [_loadGroupParticipants] chatData == null'); + return; + } + + final chats = chatData['chats'] as List?; + if (chats == null) { + print('❌ [_loadGroupParticipants] chats == null'); + return; + } + + print( + '🔍 [_loadGroupParticipants] Ищем чат с ID ${widget.chatId} среди ${chats.length} чатов...', + ); + + final currentChat = chats.firstWhere( + (chat) => chat['id'] == widget.chatId, + orElse: () => null, + ); + + if (currentChat == null) { + print('❌ [_loadGroupParticipants] Чат с ID ${widget.chatId} не найден'); + return; + } + + print( + '✅ [_loadGroupParticipants] Чат найден: ${currentChat['title'] ?? 'Без названия'}', + ); + + final participants = currentChat['participants'] as Map?; + if (participants == null || participants.isEmpty) { + print('❌ [_loadGroupParticipants] Список участников пуст'); + return; + } + + print( + '🔍 [_loadGroupParticipants] Найдено ${participants.length} участников в чате', + ); + + final participantIds = participants.keys + .map((id) => int.tryParse(id)) + .where((id) => id != null) + .cast() + .toList(); + + if (participantIds.isEmpty) { + print('❌ [_loadGroupParticipants] participantIds пуст после парсинга'); + return; + } + + print( + '🔍 [_loadGroupParticipants] Обрабатываем ${participantIds.length} ID участников...', + ); + print( + '🔍 [_loadGroupParticipants] IDs: ${participantIds.take(10).join(', ')}${participantIds.length > 10 ? '...' : ''}', + ); + + final idsToFetch = participantIds + .where((id) => !_contactDetailsCache.containsKey(id)) + .toList(); + + print( + '🔍 [_loadGroupParticipants] В кэше уже есть: ${participantIds.length - idsToFetch.length} контактов', + ); + print( + '🔍 [_loadGroupParticipants] Нужно загрузить: ${idsToFetch.length} контактов', + ); + + if (idsToFetch.isEmpty) { + print('✅ [_loadGroupParticipants] Все участники уже в кэше'); + return; + } + + print( + '📡 [_loadGroupParticipants] Загружаем информацию о ${idsToFetch.length} участниках...', + ); + print( + '📡 [_loadGroupParticipants] IDs для загрузки: ${idsToFetch.take(10).join(', ')}${idsToFetch.length > 10 ? '...' : ''}', + ); + + final contacts = await ApiService.instance.fetchContactsByIds(idsToFetch); + + print( + '📦 [_loadGroupParticipants] Получено ${contacts.length} контактов от API из ${idsToFetch.length} запрошенных', + ); + + if (contacts.isNotEmpty) { + for (final contact in contacts) { + print(' 📇 Контакт: ${contact.name} (ID: ${contact.id})'); + } + + if (mounted) { + setState(() { + for (final contact in contacts) { + _contactDetailsCache[contact.id] = contact; + } + }); + + await ChatCacheService().cacheChatContacts(widget.chatId, contacts); + + print( + '✅ [_loadGroupParticipants] Загружено и сохранено ${contacts.length} контактов', + ); + print( + '✅ [_loadGroupParticipants] Всего в кэше теперь: ${_contactDetailsCache.length} контактов', + ); + + if (contacts.length < idsToFetch.length) { + final receivedIds = contacts.map((c) => c.id).toSet(); + final missingIds = idsToFetch + .where((id) => !receivedIds.contains(id)) + .toList(); + print( + '⚠️ [_loadGroupParticipants] Не получены данные для ${missingIds.length} контактов из ${idsToFetch.length} запрошенных', + ); + print( + '⚠️ [_loadGroupParticipants] Отсутствующие ID: ${missingIds.take(10).join(', ')}${missingIds.length > 10 ? '...' : ''}', + ); + } + } else { + print( + '⚠️ [_loadGroupParticipants] Widget не mounted, контакты не сохранены', + ); + } + } else { + print('❌ [_loadGroupParticipants] API вернул ПУСТОЙ список контактов!'); + print( + '❌ [_loadGroupParticipants] Было запрошено ${idsToFetch.length} ID', + ); + print( + '❌ [_loadGroupParticipants] Запрошенные ID: ${idsToFetch.take(10).join(', ')}${idsToFetch.length > 10 ? '...' : ''}', + ); + } + } catch (e, stackTrace) { + print('❌ [_loadGroupParticipants] Ошибка загрузки участников группы: $e'); + print('❌ [_loadGroupParticipants] StackTrace: $stackTrace'); + } + } + @override void initState() { super.initState(); @@ -185,6 +338,13 @@ class _ChatScreenState extends State { Future _initializeChat() async { await _loadCachedContacts(); + if (!widget.isGroupChat && !widget.isChannel) { + _contactDetailsCache[widget.contact.id] = widget.contact; + print( + '✅ [_initializeChat] Собеседник добавлен в кэш: ${widget.contact.name} (ID: ${widget.contact.id})', + ); + } + final profileData = ApiService.instance.lastChatsPayload?['profile']; final contactProfile = profileData?['contact'] as Map?; @@ -193,11 +353,34 @@ class _ChatScreenState extends State { contactProfile['id'] != 0) { _actualMyId = contactProfile['id']; print('✅ ID пользователя успешно получен из ApiService: $_actualMyId'); + + try { + final myContact = Contact.fromJson(contactProfile); + _contactDetailsCache[_actualMyId!] = myContact; + print( + '✅ [_initializeChat] Собственный профиль добавлен в кэш: ${myContact.name} (ID: $_actualMyId)', + ); + } catch (e) { + print( + '⚠️ [_initializeChat] Не удалось добавить собственный профиль в кэш: $e', + ); + } } else { _actualMyId = widget.myId; print('ПРЕДУПРЕЖДЕНИЕ: Используется ID из виджета: $_actualMyId'); } + if (!widget.isGroupChat && !widget.isChannel) { + final contactsToCache = _contactDetailsCache.values.toList(); + await ChatCacheService().cacheChatContacts( + widget.chatId, + contactsToCache, + ); + print( + '✅ [_initializeChat] Сохранено ${contactsToCache.length} контактов в кэш чата (включая собственный профиль)', + ); + } + if (mounted) { setState(() { _isIdReady = true; @@ -395,6 +578,11 @@ class _ChatScreenState extends State { if (!mounted) return; _messages.clear(); _messages.addAll(cachedMessages); + + if (widget.isGroupChat) { + await _loadGroupParticipants(); + } + _buildChatItems(); setState(() { _isLoadingHistory = false; @@ -427,7 +615,17 @@ class _ChatScreenState extends State { final idsToFetch = senderIds .where((id) => !_contactDetailsCache.containsKey(id)) .toList(); + if (idsToFetch.isNotEmpty) { + print( + '📡 [_paginateInitialLoad] Загружаем ${idsToFetch.length} отсутствующих контактов из ${senderIds.length} отправителей...', + ); + print( + '📡 [_paginateInitialLoad] В кэше: ${senderIds.length - idsToFetch.length}, нужно загрузить: ${idsToFetch.length}', + ); + print( + '📡 [_paginateInitialLoad] IDs для загрузки: ${idsToFetch.take(10).join(', ')}${idsToFetch.length > 10 ? '...' : ''}', + ); final newContacts = await ApiService.instance.fetchContactsByIds( idsToFetch, ); @@ -435,10 +633,29 @@ class _ChatScreenState extends State { for (final contact in newContacts) { _contactDetailsCache[contact.id] = contact; } + + if (newContacts.isNotEmpty) { + final allChatContacts = _contactDetailsCache.values.toList(); + await ChatCacheService().cacheChatContacts( + widget.chatId, + allChatContacts, + ); + print( + '✅ [_paginateInitialLoad] Обновлен кэш: ${allChatContacts.length} контактов для чата ${widget.chatId}', + ); + } + } else { + print( + '✅ [_paginateInitialLoad] Все ${senderIds.length} отправителей уже в кэше', + ); } await chatCacheService.cacheChatMessages(widget.chatId, allMessages); + if (widget.isGroupChat) { + await _loadGroupParticipants(); + } + final page = _anyOptimize ? _optPage : _pageSize; final slice = allMessages.length > page ? allMessages.sublist(allMessages.length - page) @@ -1587,14 +1804,30 @@ class _ChatScreenState extends State { } Future _loadCachedContacts() async { + final chatContacts = await ChatCacheService().getCachedChatContacts( + widget.chatId, + ); + if (chatContacts != null && chatContacts.isNotEmpty) { + for (final contact in chatContacts) { + _contactDetailsCache[contact.id] = contact; + } + print( + '✅ Загружено ${_contactDetailsCache.length} контактов из кэша чата ${widget.chatId}', + ); + return; + } + + // Если нет кэша чата, загружаем глобальный кэш final cachedContacts = await ChatCacheService().getCachedContacts(); if (cachedContacts != null && cachedContacts.isNotEmpty) { for (final contact in cachedContacts) { _contactDetailsCache[contact.id] = contact; } print( - '✅ Кэш контактов для экрана чата заполнен из ChatCacheService: ${_contactDetailsCache.length} контактов.', + '✅ Загружено ${_contactDetailsCache.length} контактов из глобального кэша', ); + } else { + print('⚠️ Кэш контактов пуст, будет загружено с сервера'); } } @@ -1746,9 +1979,12 @@ class _ChatScreenState extends State { if (shouldShowName) { final senderContact = _contactDetailsCache[message.senderId]; - senderName = - senderContact?.name ?? - 'Участник ${message.senderId}'; + if (senderContact != null) { + senderName = senderContact.name; + } else { + senderName = 'ID ${message.senderId}'; + _loadContactIfNeeded(message.senderId); + } } } final hasPhoto = item.message.attaches.any( @@ -4144,7 +4380,8 @@ class _AddMemberDialogState extends State<_AddMemberDialog> { itemBuilder: (context, index) { final contact = widget.contacts[index]; final contactId = contact['id'] as int; - final contactName = contact['names']?[0]?['name'] ?? 'Неизвестный'; + final contactName = + contact['names']?[0]?['name'] ?? 'ID $contactId'; final isSelected = _selectedContacts.contains(contactId); return CheckboxListTile( @@ -4316,7 +4553,8 @@ class _ControlMessageChip extends StatelessWidget { ); final eventType = controlAttach['event']; - final senderName = contacts[message.senderId]?.name ?? 'Неизвестный'; + final senderName = + contacts[message.senderId]?.name ?? 'ID ${message.senderId}'; final isMe = message.senderId == myId; final senderDisplayName = isMe ? 'Вы' : senderName; @@ -4506,18 +4744,41 @@ class _ControlMessageChip extends StatelessWidget { } } -void openUserProfileById(BuildContext context, int userId) { - final contact = ApiService.instance.getCachedContact(userId); +Future openUserProfileById(BuildContext context, int userId) async { + var contact = ApiService.instance.getCachedContact(userId); + + if (contact == null) { + print( + '⚠️ [openUserProfileById] Контакт $userId не найден в кэше, загружаем с сервера...', + ); + + try { + final contacts = await ApiService.instance.fetchContactsByIds([userId]); + if (contacts.isNotEmpty) { + contact = contacts.first; + print( + '✅ [openUserProfileById] Контакт $userId загружен: ${contact.name}', + ); + } else { + print( + '❌ [openUserProfileById] Сервер не вернул данные для контакта $userId', + ); + } + } catch (e) { + print('❌ [openUserProfileById] Ошибка загрузки контакта $userId: $e'); + } + } if (contact != null) { - final isGroup = contact.id < 0; // Groups have negative IDs + final contactData = contact; + final isGroup = contactData.id < 0; if (isGroup) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (context) => GroupProfileDraggableDialog(contact: contact), + builder: (context) => GroupProfileDraggableDialog(contact: contactData), ); } else { Navigator.of(context).push( @@ -4525,7 +4786,7 @@ void openUserProfileById(BuildContext context, int userId) { opaque: false, barrierColor: Colors.transparent, pageBuilder: (context, animation, secondaryAnimation) { - return ContactProfileDialog(contact: contact); + return ContactProfileDialog(contact: contactData); }, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); @@ -4538,12 +4799,12 @@ void openUserProfileById(BuildContext context, int userId) { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('Профиль пользователя $userId'), - content: Text('Информация о пользователе не найдена в кэше'), + title: const Text('Ошибка'), + content: Text('Не удалось загрузить информацию о пользователе $userId'), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('OK'), + child: const Text('OK'), ), ], ), diff --git a/lib/chats_screen.dart b/lib/chats_screen.dart index b98374f..ebd397d 100644 --- a/lib/chats_screen.dart +++ b/lib/chats_screen.dart @@ -80,6 +80,7 @@ class _ChatsScreenState extends State List _searchResults = []; String _searchFilter = 'all'; bool _hasRequestedBlockedContacts = false; + final Set _loadingContactIds = {}; List _folders = []; String? _selectedFolderId; @@ -1503,6 +1504,30 @@ class _ChatsScreenState extends State } } + Future _loadMissingContact(int contactId) async { + if (_loadingContactIds.contains(contactId) || + _contacts.containsKey(contactId)) { + return; + } + + _loadingContactIds.add(contactId); + + try { + final contacts = await ApiService.instance.fetchContactsByIds([ + contactId, + ]); + if (contacts.isNotEmpty && mounted) { + setState(() { + _contacts[contactId] = contacts.first; + }); + } + } catch (e) { + print('Ошибка загрузки контакта $contactId: $e'); + } finally { + _loadingContactIds.remove(contactId); + } + } + String _formatTimestamp(int timestamp) { final dt = DateTime.fromMillisecondsSinceEpoch(timestamp); final now = DateTime.now(); @@ -2343,12 +2368,13 @@ class _ChatsScreenState extends State final bool isSavedMessages = _isSavedMessages(chat); final Contact? contact; + int? otherParticipantId; if (isSavedMessages) { contact = _contacts[chat.ownerId]; } else if (isGroupChat) { contact = null; } else { - final otherParticipantId = chat.participantIds.firstWhere( + otherParticipantId = chat.participantIds.firstWhere( (id) => id != chat.ownerId, orElse: () => 0, ); @@ -2360,11 +2386,23 @@ class _ChatsScreenState extends State child: GestureDetector( onTap: () { final bool isChannel = chat.type == 'CHANNEL'; - final String title = isGroupChat - ? (chat.title?.isNotEmpty == true ? chat.title! : "Группа") - : (isSavedMessages - ? "Избранное" - : contact?.name ?? "Unknown"); + String title; + if (isGroupChat) { + title = chat.title?.isNotEmpty == true + ? chat.title! + : "Группа"; + } else if (isSavedMessages) { + title = "Избранное"; + } else if (contact != null) { + title = contact.name; + } else if (chat.title?.isNotEmpty == true) { + title = chat.title!; + } else { + title = "ID ${otherParticipantId ?? 0}"; + if (otherParticipantId != null && otherParticipantId != 0) { + _loadMissingContact(otherParticipantId); + } + } final String? avatarUrl = isGroupChat ? chat.baseIconUrl : (isSavedMessages ? null : contact?.photoBaseUrl); @@ -2512,7 +2550,12 @@ class _ChatsScreenState extends State : Text( isSavedMessages ? "Избранное" - : (contact?.name ?? 'Unknown'), + : (contact?.name ?? + (chat.title?.isNotEmpty == true + ? chat.title! + : (otherParticipantId != null + ? 'ID $otherParticipantId' + : 'ID 0'))), style: TextStyle( fontSize: 11, color: colors.onSurface, @@ -3779,7 +3822,14 @@ class _ChatsScreenState extends State ); contact = _contacts[otherParticipantId]; - title = contact?.name ?? "Неизвестный чат"; + if (contact != null) { + title = contact.name; + } else if (chat.title?.isNotEmpty == true) { + title = chat.title!; + } else { + title = "ID $otherParticipantId"; + _loadMissingContact(otherParticipantId); + } avatarUrl = contact?.photoBaseUrl; leadingIcon = Icons.person; } @@ -4479,7 +4529,14 @@ class _AddChatsToFolderDialogState extends State<_AddChatsToFolderDialog> { orElse: () => myId, ); contact = widget.contacts[otherParticipantId]; - title = contact?.name ?? "Неизвестный"; + + if (contact != null) { + title = contact.name; + } else if (chat.title?.isNotEmpty == true) { + title = chat.title!; + } else { + title = "ID $otherParticipantId"; + } avatarUrl = contact?.photoBaseUrl; leadingIcon = Icons.person; } diff --git a/lib/models/channel.dart b/lib/models/channel.dart index c8e9fb8..66be9ce 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -22,10 +22,11 @@ class Channel { factory Channel.fromJson(Map json) { final names = json['names'] as List?; final nameData = names?.isNotEmpty == true ? names![0] : null; + final channelId = json['id'] as int; return Channel( - id: json['id'] as int, - name: nameData?['name'] as String? ?? 'Неизвестный канал', + id: channelId, + name: nameData?['name'] as String? ?? 'ID $channelId', description: nameData?['description'] as String?, photoBaseUrl: json['baseUrl'] as String?, link: json['link'] as String?, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index fd21c14..44348f3 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -31,10 +31,11 @@ class Contact { factory Contact.fromJson(Map json) { final nameData = json['names']?[0]; + final userId = json['id'] as int; String finalFirstName = ''; String finalLastName = ''; - String finalName = 'Unknown'; + String finalName = 'ID $userId'; if (nameData != null) { finalFirstName = nameData['firstName'] ?? ''; @@ -42,10 +43,9 @@ class Contact { final fullName = '$finalFirstName $finalLastName'.trim(); finalName = fullName.isNotEmpty ? fullName - : (nameData['name'] ?? 'Unknown'); + : (nameData['name'] ?? 'ID $userId'); } - final status = json['status']; final isBlocked = status == 'BLOCKED'; diff --git a/lib/screens/group_settings_screen.dart b/lib/screens/group_settings_screen.dart index 12614e5..aae25f3 100644 --- a/lib/screens/group_settings_screen.dart +++ b/lib/screens/group_settings_screen.dart @@ -40,7 +40,6 @@ class _GroupSettingsScreenState extends State { super.initState(); _currentContact = widget.initialContact; - _contactSubscription = ApiService.instance.contactUpdates.listen((contact) { if (contact.id == _currentContact.id && mounted) { ApiService.instance.updateCachedContact(contact); @@ -50,17 +49,14 @@ class _GroupSettingsScreenState extends State { } }); - _membersSubscription = ApiService.instance.messages.listen((message) { if (message['type'] == 'group_members' && mounted) { _handleGroupMembersResponse(message['payload']); } }); - _loadMembersFromCache(); - if (_loadedMembers.length < 50) { _loadedMembers.clear(); _loadedMemberIds.clear(); @@ -69,7 +65,6 @@ class _GroupSettingsScreenState extends State { ApiService.instance.getGroupMembers(widget.chatId, marker: 0, count: 50); _isLoadingMembers = true; } else { - _lastMarker = _loadedMembers.isNotEmpty ? _loadedMembers.last['id'] as int? : null; @@ -81,7 +76,6 @@ class _GroupSettingsScreenState extends State { ); } - _scrollController.addListener(_onScroll); } @@ -109,7 +103,6 @@ class _GroupSettingsScreenState extends State { return; } - List membersRaw = []; if (currentChat['members'] is List) { membersRaw = currentChat['members'] as List; @@ -163,31 +156,6 @@ class _GroupSettingsScreenState extends State { ); } - - - - - - - - - - - - - - - - - - - - - - - - - void _handleGroupMembersResponse(Map payload) { print( 'DEBUG: _handleGroupMembersResponse вызван с payload: ${payload.keys}', @@ -383,7 +351,6 @@ class _GroupSettingsScreenState extends State { ), ), ); - } }, ), @@ -426,7 +393,7 @@ class _GroupSettingsScreenState extends State { if (contact != null) { removableMembers.add({ 'id': id, - 'name': contact['names']?[0]?['name'] ?? 'Неизвестный', + 'name': contact['names']?[0]?['name'] ?? 'ID $id', 'contact': contact, }); } @@ -456,7 +423,6 @@ class _GroupSettingsScreenState extends State { content: Text('Удалено ${selectedMembers.length} участников'), ), ); - } }, ), @@ -499,7 +465,7 @@ class _GroupSettingsScreenState extends State { if (contact != null) { promotableMembers.add({ 'id': id, - 'name': contact['names']?[0]?['name'] ?? 'Неизвестный', + 'name': contact['names']?[0]?['name'] ?? 'ID $id', 'contact': contact, }); } @@ -553,7 +519,6 @@ class _GroupSettingsScreenState extends State { ApiService.instance.leaveGroup(widget.chatId); if (mounted) { - Navigator.of(context) ..pop() ..pop(); @@ -708,7 +673,6 @@ class _GroupSettingsScreenState extends State { Widget _buildGroupManagementButtons() { final colorScheme = Theme.of(context).colorScheme; - bool amIAdmin = false; final currentChat = _getCurrentGroupChat(); if (currentChat != null) { @@ -777,7 +741,6 @@ class _GroupSettingsScreenState extends State { const SizedBox(height: 8), ], - SizedBox( width: double.infinity, child: FilledButton.icon( @@ -837,12 +800,12 @@ class _GroupSettingsScreenState extends State { final fullName = '$firstName $lastName'.trim(); name = fullName.isNotEmpty ? fullName - : (nameData['name'] as String? ?? 'Неизвестный'); + : (nameData['name'] as String? ?? 'ID $id'); } } } if (name == null || name.isEmpty) { - name = 'Неизвестный'; + name = 'ID $id'; } avatarUrl = contact?['baseUrl'] as String? ?? contact?['baseRawUrl'] as String?; @@ -1050,7 +1013,8 @@ class _AddMemberDialogState extends State<_AddMemberDialog> { itemBuilder: (context, index) { final contact = widget.contacts[index]; final contactId = contact['id'] as int; - final contactName = contact['names']?[0]?['name'] ?? 'Неизвестный'; + final contactName = + contact['names']?[0]?['name'] ?? 'ID $contactId'; final isSelected = _selectedContacts.contains(contactId); return CheckboxListTile( diff --git a/lib/screens/settings/storage_screen.dart b/lib/screens/settings/storage_screen.dart index 48e47a1..edd8742 100644 --- a/lib/screens/settings/storage_screen.dart +++ b/lib/screens/settings/storage_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; import 'dart:math'; +import 'package:gwid/api/api_service.dart'; class StorageScreen extends StatefulWidget { final bool isModal; @@ -114,12 +115,39 @@ class _StorageScreenState extends State } Future _clearCache() async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Очистить кэш'), + content: const Text( + 'Это действие очистит весь кэш приложения, включая кэш сообщений, медиафайлов и аватаров. ' + 'Это действие нельзя отменить.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Отмена'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.orange), + child: const Text('Очистить'), + ), + ], + ), + ); + + if (confirmed != true) return; + try { + ApiService.instance.clearAllCaches(); + final cacheDir = await getTemporaryDirectory(); if (await cacheDir.exists()) { await cacheDir.delete(recursive: true); await cacheDir.create(); } + await _loadStorageInfo(); if (mounted) { diff --git a/lib/services/chat_cache_service.dart b/lib/services/chat_cache_service.dart index 6d4bcb1..30bdb69 100644 --- a/lib/services/chat_cache_service.dart +++ b/lib/services/chat_cache_service.dart @@ -21,7 +21,7 @@ class ChatCacheService { static const String _chatMessagesKey = 'cached_chat_messages'; static const Duration _chatsTTL = Duration(hours: 1); - static const Duration _contactsTTL = Duration(hours: 6); + static const Duration _contactsTTL = Duration(hours: 24); static const Duration _messagesTTL = Duration(hours: 2); Future cacheChats(List> chats) async { @@ -54,22 +54,32 @@ class ChatCacheService { .map( (contact) => { 'id': contact.id, - 'name': contact.name, - 'firstName': contact.firstName, - 'lastName': contact.lastName, + 'names': [ + { + 'name': contact.name, + 'firstName': contact.firstName, + 'lastName': contact.lastName, + 'type': 'ONEME', + }, + ], 'photoBaseUrl': contact.photoBaseUrl, + 'baseUrl': contact.photoBaseUrl, 'isBlocked': contact.isBlocked, 'isBlockedByMe': contact.isBlockedByMe, 'accountStatus': contact.accountStatus, 'status': contact.status, + 'options': contact.options, + 'description': contact.description, }, ) .toList(); await _cacheService.set(_contactsKey, contactsData, ttl: _contactsTTL); - print('Кэшировано ${contacts.length} контактов'); + print( + '✅ Кэшировано ${contacts.length} контактов (глобально) с описаниями', + ); } catch (e) { - print('Ошибка кэширования контактов: $e'); + print('❌ Ошибка кэширования контактов: $e'); } } @@ -80,10 +90,65 @@ class ChatCacheService { ttl: _contactsTTL, ); if (cached != null) { - return cached.map((data) => Contact.fromJson(data)).toList(); + final contacts = cached.map((data) => Contact.fromJson(data)).toList(); + print('✅ Загружено ${contacts.length} контактов из глобального кэша'); + return contacts; } } catch (e) { - print('Ошибка получения кэшированных контактов: $e'); + print('❌ Ошибка получения кэшированных контактов: $e'); + } + return null; + } + + // Кэширование контактов для конкретного чата + Future cacheChatContacts(int chatId, List contacts) async { + try { + final key = 'chat_contacts_$chatId'; + final contactsData = contacts + .map( + (contact) => { + 'id': contact.id, + 'names': [ + { + 'name': contact.name, + 'firstName': contact.firstName, + 'lastName': contact.lastName, + 'type': 'ONEME', + }, + ], + 'photoBaseUrl': contact.photoBaseUrl, + 'baseUrl': contact.photoBaseUrl, + 'isBlocked': contact.isBlocked, + 'isBlockedByMe': contact.isBlockedByMe, + 'accountStatus': contact.accountStatus, + 'status': contact.status, + 'options': contact.options, + 'description': contact.description, + }, + ) + .toList(); + + await _cacheService.set(key, contactsData, ttl: _contactsTTL); + print('✅ Кэшировано ${contacts.length} контактов для чата $chatId'); + } catch (e) { + print('❌ Ошибка кэширования контактов для чата $chatId: $e'); + } + } + + Future?> getCachedChatContacts(int chatId) async { + try { + final key = 'chat_contacts_$chatId'; + final cached = await _cacheService.get>( + key, + ttl: _contactsTTL, + ); + if (cached != null) { + final contacts = cached.map((data) => Contact.fromJson(data)).toList(); + print('✅ Загружено ${contacts.length} контактов из кэша чата $chatId'); + return contacts; + } + } catch (e) { + print('❌ Ошибка получения кэшированных контактов для чата $chatId: $e'); } return null; } @@ -248,13 +313,14 @@ class ChatCacheService { '$_chatMessagesKey$chatId', 'chat_info_$chatId', 'last_message_$chatId', + 'chat_contacts_$chatId', ]; for (final key in keys) { await _cacheService.remove(key); } - print('Кэш для чата $chatId очищен'); + print('Кэш для чата $chatId очищен (включая контакты)'); } catch (e) { print('Ошибка очистки кэша для чата $chatId: $e'); } diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index 01e1ff4..3f1d4e7 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -283,10 +283,12 @@ class ChatMessageBubble extends StatelessWidget { if (originalSenderId != null && cache != null) { final originalSenderContact = cache[originalSenderId]; forwardedSenderName = - originalSenderContact?.name ?? 'Участник $originalSenderId'; + originalSenderContact?.name ?? 'ID $originalSenderId'; forwardedSenderAvatarUrl ??= originalSenderContact?.photoBaseUrl; + } else if (originalSenderId != null) { + forwardedSenderName = 'ID $originalSenderId'; } else { - forwardedSenderName = 'Неизвестный'; + forwardedSenderName = 'Пользователь'; } } } @@ -613,8 +615,8 @@ class ChatMessageBubble extends StatelessWidget { child: Text( replySenderId != null ? (contactDetailsCache?[replySenderId]?.name ?? - 'Участник $replySenderId') - : 'Неизвестный', + 'ID $replySenderId') + : 'Пользователь', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, @@ -1294,7 +1296,7 @@ class ChatMessageBubble extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 2.0, bottom: 2.0), child: Text( - senderName ?? 'Неизвестный', + senderName!, style: TextStyle( fontWeight: FontWeight.bold, color: _getUserColor( @@ -3623,7 +3625,7 @@ class ChatMessageBubble extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(left: 2.0, bottom: 2.0), child: Text( - senderName ?? 'Неизвестный', + senderName!, style: TextStyle( fontWeight: FontWeight.bold, color: _getUserColor( diff --git a/lib/widgets/message_preview_dialog.dart b/lib/widgets/message_preview_dialog.dart index ef2a7ee..c4c3a5b 100644 --- a/lib/widgets/message_preview_dialog.dart +++ b/lib/widgets/message_preview_dialog.dart @@ -26,7 +26,8 @@ class ControlMessageChip extends StatelessWidget { ); final eventType = controlAttach['event']; - final senderName = contacts[message.senderId]?.name ?? 'Неизвестный'; + final senderName = + contacts[message.senderId]?.name ?? 'ID ${message.senderId}'; final isMe = message.senderId == myId; final senderDisplayName = isMe ? 'Вы' : senderName; @@ -297,7 +298,14 @@ class MessagePreviewDialog { orElse: () => myId, ); final contact = contacts[otherParticipantId]; - return contact?.name ?? "Неизвестный чат"; + + if (contact != null) { + return contact.name; + } else if (chat.title?.isNotEmpty == true) { + return chat.title!; + } else { + return "ID $otherParticipantId"; + } } } @@ -544,7 +552,8 @@ class MessagePreviewDialog { contacts[message.senderId]; final senderName = isMe ? 'Вы' - : (senderContact?.name ?? 'Неизвестный'); + : (senderContact?.name ?? + 'ID ${message.senderId}'); String? forwardedFrom; String? forwardedFromAvatarUrl; diff --git a/lib/widgets/pinned_message_widget.dart b/lib/widgets/pinned_message_widget.dart index 297f4d1..b584ab8 100644 --- a/lib/widgets/pinned_message_widget.dart +++ b/lib/widgets/pinned_message_widget.dart @@ -23,7 +23,9 @@ class PinnedMessageWidget extends StatelessWidget { final colors = Theme.of(context).colorScheme; final senderName = contacts[pinnedMessage.senderId]?.name ?? - (pinnedMessage.senderId == myId ? 'Вы' : 'Неизвестный'); + (pinnedMessage.senderId == myId + ? 'Вы' + : 'ID ${pinnedMessage.senderId}'); return Container( margin: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 0), diff --git a/lib/widgets/user_profile_panel.dart b/lib/widgets/user_profile_panel.dart index c6b3a88..dc85e34 100644 --- a/lib/widgets/user_profile_panel.dart +++ b/lib/widgets/user_profile_panel.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; import 'package:gwid/services/avatar_cache_service.dart'; - - - class UserProfilePanel extends StatefulWidget { final int userId; final String? name; @@ -37,16 +34,16 @@ class UserProfilePanel extends StatefulWidget { class _UserProfilePanelState extends State { final ScrollController _nameScrollController = ScrollController(); - - String get _displayName { if (widget.firstName != null || widget.lastName != null) { final firstName = widget.firstName ?? ''; final lastName = widget.lastName ?? ''; final fullName = '$firstName $lastName'.trim(); - return fullName.isNotEmpty ? fullName : (widget.name ?? 'Неизвестный'); + return fullName.isNotEmpty + ? fullName + : (widget.name ?? 'ID ${widget.userId}'); } - return widget.name ?? 'Неизвестный'; + return widget.name ?? 'ID ${widget.userId}'; } @override @@ -108,122 +105,6 @@ class _UserProfilePanelState extends State { super.dispose(); } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme;