Ответ на файлы отображается не как фото а как файл, список чатов теперь не дохлая хуйня, динамично изменяется. Написал левый хуй? Появляется чат сразу, создал группу? Создалось, вышел? Вышел.(+ баг что на desktop режимах отображения при выходах чернеет экран)

This commit is contained in:
jganenok
2025-12-01 19:40:09 +07:00
parent 7e0d5eba20
commit 11f974c477
4 changed files with 367 additions and 124 deletions

View File

@@ -190,6 +190,42 @@ extension ApiServiceChats on ApiService {
print('Переименовываем группу $chatId в: $newName'); print('Переименовываем группу $chatId в: $newName');
} }
/// Обновляет/добавляет чат в локный кэш `_lastChatsPayload['chats']`,
/// чтобы остальные экраны (настройки группы, экран чата и т.п.) сразу
/// видели новые поля (admins, options, participants и т.д.).
void updateChatInCacheFromJson(Map<String, dynamic> chatJson) {
try {
// Если кэш ещё не инициализирован (например, сразу после запуска),
// создаём минимальную структуру, чтобы новый чат тоже оказался в ней.
if (_lastChatsPayload == null) {
_lastChatsPayload = {
'chats': <dynamic>[],
'contacts': <dynamic>[],
'profile': null,
'presence': null,
'config': null,
};
}
final chats = _lastChatsPayload!['chats'] as List<dynamic>;
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 вида: /// Сервер ожидает payload вида:
/// {"chatId": -69330645868731, "revokePrivateLink": true} /// {"chatId": -69330645868731, "revokePrivateLink": true}
@@ -198,10 +234,7 @@ extension ApiServiceChats on ApiService {
int chatId, { int chatId, {
bool revokePrivateLink = true, bool revokePrivateLink = true,
}) async { }) async {
final payload = { final payload = {"chatId": chatId, "revokePrivateLink": revokePrivateLink};
"chatId": chatId,
"revokePrivateLink": revokePrivateLink,
};
print('Создаем пригласительную ссылку для группы $chatId: $payload'); print('Создаем пригласительную ссылку для группы $chatId: $payload');
@@ -216,7 +249,9 @@ extension ApiServiceChats on ApiService {
final error = response['payload']; final error = response['payload'];
print('Ошибка создания пригласительной ссылки: $error'); print('Ошибка создания пригласительной ссылки: $error');
final message = final message =
error?['localizedMessage'] ?? error?['message'] ?? 'Неизвестная ошибка'; error?['localizedMessage'] ??
error?['message'] ??
'Неизвестная ошибка';
throw Exception(message); throw Exception(message);
} }
@@ -230,20 +265,8 @@ extension ApiServiceChats on ApiService {
} }
// Обновим кэш чатов, если сервер вернул полный объект чата // Обновим кэш чатов, если сервер вернул полный объект чата
try {
if (chat != null) { if (chat != null) {
final chats = _lastChatsPayload?['chats'] as List<dynamic>?; updateChatInCacheFromJson(chat);
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');
} }
return link; return link;

View File

@@ -88,6 +88,8 @@ class Message {
bool get isDeleted => status == 'DELETED'; bool get isDeleted => status == 'DELETED';
bool get isReply => link != null && link!['type'] == 'REPLY'; bool get isReply => link != null && link!['type'] == 'REPLY';
bool get isForwarded => link != null && link!['type'] == 'FORWARD'; bool get isForwarded => link != null && link!['type'] == 'FORWARD';
bool get hasFileAttach =>
attaches.any((a) => (a['_type'] ?? a['type']) == 'FILE');
bool canEdit(int currentUserId) { bool canEdit(int currentUserId) {
if (isDeleted) return false; if (isDeleted) return false;

View File

@@ -1693,11 +1693,32 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
); );
}, },
pageBuilder: (context, animation, secondaryAnimation) => AlertDialog( pageBuilder: (context, animation, secondaryAnimation) {
bool forAll = false;
return StatefulBuilder(
builder: (context, setStateDialog) {
return AlertDialog(
title: const Text('Очистить историю чата'), title: const Text('Очистить историю чата'),
content: Text( content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Вы уверены, что хотите очистить историю чата с ${_currentContact.name}? Это действие нельзя отменить.', 'Вы уверены, что хотите очистить историю чата с ${_currentContact.name}? Это действие нельзя отменить.',
), ),
const SizedBox(height: 12),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: forAll,
onChanged: (value) {
setStateDialog(() {
forAll = value ?? false;
});
},
title: const Text('Удалить сообщения для всех'),
),
],
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
@@ -1707,7 +1728,10 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
try { try {
await ApiService.instance.clearChatHistory(widget.chatId); await ApiService.instance.clearChatHistory(
widget.chatId,
forAll: forAll,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
_messages.clear(); _messages.clear();
@@ -1725,7 +1749,8 @@ class _ChatScreenState extends State<ChatScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Ошибка очистки истории: $e'), content: Text('Ошибка очистки истории: $e'),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor:
Theme.of(context).colorScheme.error,
), ),
); );
} }
@@ -1738,7 +1763,10 @@ class _ChatScreenState extends State<ChatScreen> {
child: const Text('Очистить'), child: const Text('Очистить'),
), ),
], ],
), );
},
);
},
); );
} }
@@ -1757,10 +1785,31 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
); );
}, },
pageBuilder: (context, animation, secondaryAnimation) => AlertDialog( pageBuilder: (context, animation, secondaryAnimation) {
bool forAll = false;
return StatefulBuilder(
builder: (context, setStateDialog) {
return AlertDialog(
title: const Text('Удалить чат'), title: const Text('Удалить чат'),
content: Text( content: Column(
'Вы уверены, что хотите удалить чат с ${_currentContact.name}? Это действие нельзя отменить.', //1231231233 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: [ actions: [
TextButton( TextButton(
@@ -1771,8 +1820,17 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
try { try {
print('Имитация удаления чата ID: ${widget.chatId}'); // Удаляем историю чата (opcode 54)
await Future.delayed(const Duration(milliseconds: 500)); await ApiService.instance.clearChatHistory(
widget.chatId,
forAll: forAll,
);
// Отписываемся от чата (opcode 75)
await ApiService.instance.subscribeToChat(
widget.chatId,
false,
);
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -1791,7 +1849,8 @@ class _ChatScreenState extends State<ChatScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Ошибка удаления чата: $e'), content: Text('Ошибка удаления чата: $e'),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor:
Theme.of(context).colorScheme.error,
), ),
); );
} }
@@ -1804,7 +1863,10 @@ class _ChatScreenState extends State<ChatScreen> {
child: const Text('Удалить'), child: const Text('Удалить'),
), ),
], ],
), );
},
);
},
); );
} }
@@ -3036,7 +3098,9 @@ class _ChatScreenState extends State<ChatScreen> {
Text( Text(
_replyingToMessage!.text.isNotEmpty _replyingToMessage!.text.isNotEmpty
? _replyingToMessage!.text ? _replyingToMessage!.text
: 'Фото', : (_replyingToMessage!.hasFileAttach
? 'Файл'
: 'Фото'),
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Theme.of( color: Theme.of(
@@ -3402,7 +3466,9 @@ class _ChatScreenState extends State<ChatScreen> {
Text( Text(
_replyingToMessage!.text.isNotEmpty _replyingToMessage!.text.isNotEmpty
? _replyingToMessage!.text ? _replyingToMessage!.text
: 'Фото', : (_replyingToMessage!.hasFileAttach
? 'Файл'
: 'Фото'),
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Theme.of( color: Theme.of(

View File

@@ -332,7 +332,10 @@ class _ChatsScreenState extends State<ChatsScreen>
final chatIdValue = payload['chatId']; final chatIdValue = payload['chatId'];
final int? chatId = chatIdValue != null ? chatIdValue as int? : null; 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) { } else if (chatId == null) {
return; return;
} }
@@ -341,11 +344,44 @@ class _ChatsScreenState extends State<ChatsScreen>
_setTypingForChat(chatId); _setTypingForChat(chatId);
} }
// Ответ на отправку сообщения (opcode 64).
// Если сервер прислал в payload полный объект chat (как при создании группы
// через CONTROL new), добавляем/обновляем чат локально.
if (opcode == 64 && cmd == 1 && payload['chat'] is Map<String, dynamic>) {
final chatJson = payload['chat'] as Map<String, dynamic>;
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) { if (opcode == 128 && chatId != null) {
final newMessage = Message.fromJson(payload['message']); final newMessage = Message.fromJson(payload['message']);
ApiService.instance.clearCacheForChat(chatId); ApiService.instance.clearCacheForChat(chatId);
final int chatIndex = _allChats.indexWhere((chat) => chat.id == chatId); final int chatIndex = _allChats.indexWhere((chat) => chat.id == chatId);
if (chatIndex != -1) { if (chatIndex != -1) {
final oldChat = _allChats[chatIndex]; final oldChat = _allChats[chatIndex];
final updatedChat = oldChat.copyWith( final updatedChat = oldChat.copyWith(
@@ -377,6 +413,21 @@ class _ChatsScreenState extends State<ChatsScreen>
} }
_filterChats(); _filterChats();
}); });
} else if (payload['chat'] is Map<String, dynamic>) {
// Чат ещё не известен клиенту — создаём его на основе payload.chat.
final chatJson = payload['chat'] as Map<String, dynamic>;
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) { } else if (opcode == 67 && chatId != null) {
final editedMessage = Message.fromJson(payload['message']); final editedMessage = Message.fromJson(payload['message']);
@@ -446,10 +497,6 @@ class _ChatsScreenState extends State<ChatsScreen>
} }
} }
if (opcode == 129 && chatId != null) {
_setTypingForChat(chatId);
}
if (opcode == 132) { if (opcode == 132) {
final bool isOnline = payload['online'] == true; final bool isOnline = payload['online'] == true;
@@ -536,11 +583,97 @@ class _ChatsScreenState extends State<ChatsScreen>
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
// Создание/обновление группы (opcode 48) — стараемся обновить список чатов
// локально по объекту chat из payload, без повторного getChatsAndContacts.
if (opcode == 48) { if (opcode == 48) {
print('Получен ответ на создание группы: $payload'); print('Получен ответ на создание/обновление группы: $payload');
final chatJson = payload['chat'] as Map<String, dynamic>?;
final chatsJson = payload['chats'] as List<dynamic>?;
// Приоритет: одиночный chat, дальше — первый из списка chats.
Map<String, dynamic>? effectiveChatJson = chatJson;
if (effectiveChatJson == null && chatsJson != null && chatsJson.isNotEmpty) {
final first = chatsJson.first;
if (first is Map<String, dynamic>) {
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(); _refreshChats();
} }
}
// Изменение параметров чата (rename, invitelink и т.п.) приходит с opcode 55.
// В payload обычно лежит обновленный объект chat.
if (opcode == 55 && cmd == 1) {
final chatJson = payload['chat'] as Map<String, dynamic>?;
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<String, dynamic>) {
final removedChat = payload['chat'] as Map<String, dynamic>;
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) { if (opcode == 272) {
print('Получен ответ на обновление папок: $payload'); print('Получен ответ на обновление папок: $payload');
@@ -672,6 +805,14 @@ class _ChatsScreenState extends State<ChatsScreen>
}); });
} }
void _removeChatLocally(int chatId) {
if (!mounted) return;
setState(() {
_allChats.removeWhere((c) => c.id == chatId);
_filteredChats.removeWhere((c) => c.id == chatId);
});
}
final Map<int, Timer> _typingDecayTimers = {}; final Map<int, Timer> _typingDecayTimers = {};
final Set<int> _typingChats = {}; final Set<int> _typingChats = {};
final Set<int> _onlineChats = {}; final Set<int> _onlineChats = {};
@@ -2367,7 +2508,7 @@ class _ChatsScreenState extends State<ChatsScreen>
isChannel: isChannel, isChannel: isChannel,
participantCount: participantCount, participantCount: participantCount,
onChatUpdated: () { onChatUpdated: () {
print('Chat updated, но не обновляем список чатов...'); _removeChatLocally(chat.id);
}, },
), ),
), ),
@@ -2513,7 +2654,9 @@ class _ChatsScreenState extends State<ChatsScreen>
} else if (chat.title?.isNotEmpty == true) { } else if (chat.title?.isNotEmpty == true) {
title = chat.title!; title = chat.title!;
} else { } else {
title = "ID ${otherParticipantId ?? 0}"; // Контакт ещё не загружен — показываем плейсхолдер и
// параллельно запускаем загрузку.
title = "Данные загружаются...";
if (otherParticipantId != null && otherParticipantId != 0) { if (otherParticipantId != null && otherParticipantId != 0) {
_loadMissingContact(otherParticipantId); _loadMissingContact(otherParticipantId);
} }
@@ -4001,7 +4144,8 @@ class _ChatsScreenState extends State<ChatsScreen>
} else if (chat.title?.isNotEmpty == true) { } else if (chat.title?.isNotEmpty == true) {
title = chat.title!; title = chat.title!;
} else { } else {
title = "ID $otherParticipantId"; // Контакт ещё не загружен — плейсхолдер и асинхронная подзагрузка.
title = "Данные загружаются...";
_loadMissingContact(otherParticipantId); _loadMissingContact(otherParticipantId);
} }
avatarUrl = contact?.photoBaseUrl; avatarUrl = contact?.photoBaseUrl;
@@ -4075,7 +4219,7 @@ class _ChatsScreenState extends State<ChatsScreen>
isChannel: isChannel, isChannel: isChannel,
participantCount: participantCount, participantCount: participantCount,
onChatUpdated: () { 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 subtitle: isGroupChat && chat.participantIds.length > 2
? Text( ? Text(
'${chat.participantIds.length} участников', '${chat.participantIds.length} участников',