Кэширование ID пользователей из чатов (на 24 часа)

This commit is contained in:
needle10
2025-11-22 21:38:48 +03:00
parent bf995d8358
commit 321720cd0a
15 changed files with 669 additions and 238 deletions

View File

@@ -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 {
}
}
}

View File

@@ -1,6 +1,119 @@
part of 'api_service.dart';
extension ApiServiceChats on ApiService {
Future<void> _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<Map<String, dynamic>>(),
),
);
unawaited(_chatCacheService.cacheContacts(contacts));
_chatsFetchedInThisSession = true;
}
} catch (e) {
print("Ошибка при автоматической авторизации: $e");
}
}
void createGroup(String name, List<int> participantIds) {
final payload = {"name": name, "participantIds": participantIds};
_sendMessage(48, payload);
@@ -357,12 +470,18 @@ extension ApiServiceChats on ApiService {
return result;
}
List<dynamic> contactListJson =
chatResponse['payload']?['contacts'] ?? [];
if (contactListJson.isEmpty) {
final contactIds = <int>{};
for (var chatJson in chatListJson) {
final participants = chatJson['participants'] as Map<String, dynamic>;
final participants =
chatJson['participants'] as Map<String, dynamic>? ?? {};
contactIds.addAll(participants.keys.map((id) => int.parse(id)));
}
if (contactIds.isNotEmpty) {
final int contactSeq = _sendMessage(32, {
"contactIds": contactIds.toList(),
});
@@ -370,8 +489,9 @@ extension ApiServiceChats on ApiService {
(msg) => msg['seq'] == contactSeq,
);
final List<dynamic> contactListJson =
contactResponse['payload']?['contacts'] ?? [];
contactListJson = contactResponse['payload']?['contacts'] ?? [];
}
}
if (presence != null) {
updatePresenceData(presence);

View File

@@ -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<void>();
_chatsFetchedInThisSession = false;
clearAllCaches();
_currentUrlIndex = 0;
_reconnectDelaySeconds = (_reconnectDelaySeconds * 2).clamp(1, 30);
@@ -721,7 +723,6 @@ extension ApiServiceConnection on ApiService {
_currentUrlIndex = 0;
_onlineCompleter = Completer<void>();
clearAllCaches();
_messageQueue.clear();
_presenceData.clear();

View File

@@ -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<List<Contact>> fetchContactsByIds(List<int> 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 [];
}
}

View File

@@ -164,6 +164,13 @@ class _ChatScreenState extends State<ChatScreen> {
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<ChatScreen> {
}
}
Future<void> _loadGroupParticipants() async {
try {
print(
'🔍 [_loadGroupParticipants] Начинаем загрузку участников группы...',
);
final chatData = ApiService.instance.lastChatsPayload;
if (chatData == null) {
print('❌ [_loadGroupParticipants] chatData == null');
return;
}
final chats = chatData['chats'] as List<dynamic>?;
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<String, dynamic>?;
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<int>()
.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<ChatScreen> {
Future<void> _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<String, dynamic>?;
@@ -193,11 +353,34 @@ class _ChatScreenState extends State<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
}
Future<void> _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<ChatScreen> {
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<void> 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'),
),
],
),

View File

@@ -80,6 +80,7 @@ class _ChatsScreenState extends State<ChatsScreen>
List<SearchResult> _searchResults = [];
String _searchFilter = 'all';
bool _hasRequestedBlockedContacts = false;
final Set<int> _loadingContactIds = {};
List<ChatFolder> _folders = [];
String? _selectedFolderId;
@@ -1503,6 +1504,30 @@ class _ChatsScreenState extends State<ChatsScreen>
}
}
Future<void> _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<ChatsScreen>
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<ChatsScreen>
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<ChatsScreen>
: 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<ChatsScreen>
);
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;
}

View File

@@ -22,10 +22,11 @@ class Channel {
factory Channel.fromJson(Map<String, dynamic> json) {
final names = json['names'] as List<dynamic>?;
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?,

View File

@@ -31,10 +31,11 @@ class Contact {
factory Contact.fromJson(Map<String, dynamic> 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';

View File

@@ -40,7 +40,6 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
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<GroupSettingsScreen> {
}
});
_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<GroupSettingsScreen> {
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<GroupSettingsScreen> {
);
}
_scrollController.addListener(_onScroll);
}
@@ -109,7 +103,6 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
return;
}
List<dynamic> membersRaw = [];
if (currentChat['members'] is List) {
membersRaw = currentChat['members'] as List<dynamic>;
@@ -163,31 +156,6 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
);
}
void _handleGroupMembersResponse(Map<String, dynamic> payload) {
print(
'DEBUG: _handleGroupMembersResponse вызван с payload: ${payload.keys}',
@@ -383,7 +351,6 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
),
),
);
}
},
),
@@ -426,7 +393,7 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
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<GroupSettingsScreen> {
content: Text('Удалено ${selectedMembers.length} участников'),
),
);
}
},
),
@@ -499,7 +465,7 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
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<GroupSettingsScreen> {
ApiService.instance.leaveGroup(widget.chatId);
if (mounted) {
Navigator.of(context)
..pop()
..pop();
@@ -708,7 +673,6 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
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<GroupSettingsScreen> {
const SizedBox(height: 8),
],
SizedBox(
width: double.infinity,
child: FilledButton.icon(
@@ -837,12 +800,12 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
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(

View File

@@ -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<StorageScreen>
}
Future<void> _clearCache() async {
final confirmed = await showDialog<bool>(
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) {

View File

@@ -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<void> cacheChats(List<Map<String, dynamic>> chats) async {
@@ -54,22 +54,32 @@ class ChatCacheService {
.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(_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<void> cacheChatContacts(int chatId, List<Contact> 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<List<Contact>?> getCachedChatContacts(int chatId) async {
try {
final key = 'chat_contacts_$chatId';
final cached = await _cacheService.get<List<dynamic>>(
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');
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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),

View File

@@ -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<UserProfilePanel> {
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<UserProfilePanel> {
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;