Ответ на файлы отображается не как фото а как файл, список чатов теперь не дохлая хуйня, динамично изменяется. Написал левый хуй? Появляется чат сразу, создал группу? Создалось, вышел? Вышел.(+ баг что на desktop режимах отображения при выходах чернеет экран)
This commit is contained in:
@@ -190,6 +190,42 @@ extension ApiServiceChats on ApiService {
|
||||
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 вида:
|
||||
/// {"chatId": -69330645868731, "revokePrivateLink": true}
|
||||
@@ -198,10 +234,7 @@ extension ApiServiceChats on ApiService {
|
||||
int chatId, {
|
||||
bool revokePrivateLink = true,
|
||||
}) async {
|
||||
final payload = {
|
||||
"chatId": chatId,
|
||||
"revokePrivateLink": revokePrivateLink,
|
||||
};
|
||||
final payload = {"chatId": chatId, "revokePrivateLink": revokePrivateLink};
|
||||
|
||||
print('Создаем пригласительную ссылку для группы $chatId: $payload');
|
||||
|
||||
@@ -216,7 +249,9 @@ extension ApiServiceChats on ApiService {
|
||||
final error = response['payload'];
|
||||
print('Ошибка создания пригласительной ссылки: $error');
|
||||
final message =
|
||||
error?['localizedMessage'] ?? error?['message'] ?? 'Неизвестная ошибка';
|
||||
error?['localizedMessage'] ??
|
||||
error?['message'] ??
|
||||
'Неизвестная ошибка';
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
@@ -230,20 +265,8 @@ extension ApiServiceChats on ApiService {
|
||||
}
|
||||
|
||||
// Обновим кэш чатов, если сервер вернул полный объект чата
|
||||
try {
|
||||
if (chat != null) {
|
||||
final chats = _lastChatsPayload?['chats'] as List<dynamic>?;
|
||||
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');
|
||||
updateChatInCacheFromJson(chat);
|
||||
}
|
||||
|
||||
return link;
|
||||
|
||||
@@ -88,6 +88,8 @@ class Message {
|
||||
bool get isDeleted => status == 'DELETED';
|
||||
bool get isReply => link != null && link!['type'] == 'REPLY';
|
||||
bool get isForwarded => link != null && link!['type'] == 'FORWARD';
|
||||
bool get hasFileAttach =>
|
||||
attaches.any((a) => (a['_type'] ?? a['type']) == 'FILE');
|
||||
|
||||
bool canEdit(int currentUserId) {
|
||||
if (isDeleted) return false;
|
||||
|
||||
@@ -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('Очистить историю чата'),
|
||||
content: Text(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Вы уверены, что хотите очистить историю чата с ${_currentContact.name}? Это действие нельзя отменить.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: forAll,
|
||||
onChanged: (value) {
|
||||
setStateDialog(() {
|
||||
forAll = value ?? false;
|
||||
});
|
||||
},
|
||||
title: const Text('Удалить сообщения для всех'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -1707,7 +1728,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
await ApiService.instance.clearChatHistory(widget.chatId);
|
||||
await ApiService.instance.clearChatHistory(
|
||||
widget.chatId,
|
||||
forAll: forAll,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_messages.clear();
|
||||
@@ -1725,7 +1749,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
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('Очистить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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('Удалить чат'),
|
||||
content: Text(
|
||||
'Вы уверены, что хотите удалить чат с ${_currentContact.name}? Это действие нельзя отменить.', //1231231233
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Вы уверены, что хотите удалить чат с ${_currentContact.name}? Это действие нельзя отменить.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: forAll,
|
||||
onChanged: (value) {
|
||||
setStateDialog(() {
|
||||
forAll = value ?? false;
|
||||
});
|
||||
},
|
||||
title: const Text('Удалить сообщения для всех'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -1771,8 +1820,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
print('Имитация удаления чата ID: ${widget.chatId}');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
// Удаляем историю чата (opcode 54)
|
||||
await ApiService.instance.clearChatHistory(
|
||||
widget.chatId,
|
||||
forAll: forAll,
|
||||
);
|
||||
|
||||
// Отписываемся от чата (opcode 75)
|
||||
await ApiService.instance.subscribeToChat(
|
||||
widget.chatId,
|
||||
false,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -1791,7 +1849,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
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('Удалить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3036,7 +3098,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Text(
|
||||
_replyingToMessage!.text.isNotEmpty
|
||||
? _replyingToMessage!.text
|
||||
: 'Фото',
|
||||
: (_replyingToMessage!.hasFileAttach
|
||||
? 'Файл'
|
||||
: 'Фото'),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(
|
||||
@@ -3402,7 +3466,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Text(
|
||||
_replyingToMessage!.text.isNotEmpty
|
||||
? _replyingToMessage!.text
|
||||
: 'Фото',
|
||||
: (_replyingToMessage!.hasFileAttach
|
||||
? 'Файл'
|
||||
: 'Фото'),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(
|
||||
|
||||
@@ -332,7 +332,10 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
final chatIdValue = payload['chatId'];
|
||||
final int? chatId = chatIdValue != null ? chatIdValue as int? : null;
|
||||
|
||||
if (opcode == 272 || opcode == 274) {
|
||||
// Для части опкодов (48, 55, 135, 272, 274) нам не нужен явный chatId в корне
|
||||
// payload, поэтому не отбрасываем их, даже если chatId == null.
|
||||
if (opcode == 272 || opcode == 274 || opcode == 48 || opcode == 55 || opcode == 135) {
|
||||
// продолжаем обработку ниже
|
||||
} else if (chatId == null) {
|
||||
return;
|
||||
}
|
||||
@@ -341,11 +344,44 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
_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) {
|
||||
final newMessage = Message.fromJson(payload['message']);
|
||||
ApiService.instance.clearCacheForChat(chatId);
|
||||
|
||||
final int chatIndex = _allChats.indexWhere((chat) => chat.id == chatId);
|
||||
|
||||
if (chatIndex != -1) {
|
||||
final oldChat = _allChats[chatIndex];
|
||||
final updatedChat = oldChat.copyWith(
|
||||
@@ -377,6 +413,21 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
}
|
||||
_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) {
|
||||
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) {
|
||||
final bool isOnline = payload['online'] == true;
|
||||
|
||||
@@ -536,11 +583,97 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
// Создание/обновление группы (opcode 48) — стараемся обновить список чатов
|
||||
// локально по объекту chat из payload, без повторного getChatsAndContacts.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Изменение параметров чата (rename, invite‑link и т.п.) приходит с 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) {
|
||||
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 Set<int> _typingChats = {};
|
||||
final Set<int> _onlineChats = {};
|
||||
@@ -2367,7 +2508,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
isChannel: isChannel,
|
||||
participantCount: participantCount,
|
||||
onChatUpdated: () {
|
||||
print('Chat updated, но не обновляем список чатов...');
|
||||
_removeChatLocally(chat.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -2513,7 +2654,9 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
} else if (chat.title?.isNotEmpty == true) {
|
||||
title = chat.title!;
|
||||
} else {
|
||||
title = "ID ${otherParticipantId ?? 0}";
|
||||
// Контакт ещё не загружен — показываем плейсхолдер и
|
||||
// параллельно запускаем загрузку.
|
||||
title = "Данные загружаются...";
|
||||
if (otherParticipantId != null && otherParticipantId != 0) {
|
||||
_loadMissingContact(otherParticipantId);
|
||||
}
|
||||
@@ -4001,7 +4144,8 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
} else if (chat.title?.isNotEmpty == true) {
|
||||
title = chat.title!;
|
||||
} else {
|
||||
title = "ID $otherParticipantId";
|
||||
// Контакт ещё не загружен — плейсхолдер и асинхронная подзагрузка.
|
||||
title = "Данные загружаются...";
|
||||
_loadMissingContact(otherParticipantId);
|
||||
}
|
||||
avatarUrl = contact?.photoBaseUrl;
|
||||
@@ -4075,7 +4219,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
isChannel: isChannel,
|
||||
participantCount: participantCount,
|
||||
onChatUpdated: () {
|
||||
print('Chat updated, но не обновляем список чатов...');
|
||||
_removeChatLocally(chat.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -4751,7 +4895,15 @@ class _AddChatsToFolderDialogState extends State<_AddChatsToFolderDialog> {
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(title, style: const TextStyle(fontSize: 16)),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontStyle: title == 'Данные загружаются...'
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
),
|
||||
subtitle: isGroupChat && chat.participantIds.length > 2
|
||||
? Text(
|
||||
'${chat.participantIds.length} участников',
|
||||
|
||||
Reference in New Issue
Block a user