From e2b4ba33adf2d7d0d166f56b8adddb68940c12ff Mon Sep 17 00:00:00 2001 From: Jganenok Date: Thu, 14 May 2026 20:49:22 +0700 Subject: [PATCH] =?UTF-8?q?=D1=8D=D0=B2=D0=B5=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/api.dart | 6 +- lib/backend/modules/messages.dart | 21 +++- lib/core/storage/spoofing_service.dart | 2 +- lib/frontend/screens/chats/chat_screen.dart | 1 + lib/frontend/widgets/attachment_panel.dart | 109 ++++++++++++++++---- lib/frontend/widgets/message_bubble.dart | 90 +++++++++++++++- lib/models/attachment.dart | 11 +- 7 files changed, 205 insertions(+), 35 deletions(-) diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 1fdd71f..d170153 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -122,11 +122,7 @@ class Api { Future sendHandshake() async { final deviceInfo = DeviceInfoPlugin(); - String deviceType = (Platform.isLinux || Platform.isWindows) - ? 'DESKTOP' - : (Platform.isAndroid) - ? 'ANDROID' - : 'IOS'; + String deviceType = 'ANDROID'; String osVersion = ''; String deviceName = 'Unknown'; String architecture = 'arm64'; diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 51af04e..06ac5c7 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -100,6 +100,7 @@ class CachedMessage { final String? status; final Map? payload; final List? attachments; + final bool isControl; const CachedMessage({ required this.id, @@ -111,6 +112,7 @@ class CachedMessage { this.status, this.payload, this.attachments, + this.isControl = false, }); factory CachedMessage.fromDbRow(Map row) { @@ -151,6 +153,7 @@ class CachedMessage { status: row['status']?.toString(), payload: payload, attachments: attachments, + isControl: attachments?.any((a) => a.type == AttachmentType.control) ?? false, ); } @@ -261,6 +264,7 @@ class MessagesModule { } List? attachments; + bool isControl = false; if (linkType == 'FORWARD') { final fwdMap = Map.from(m.cast()); attachments = [ForwardedMessageAttachment.fromMap(fwdMap)]; @@ -271,6 +275,11 @@ class MessagesModule { .whereType() .map((a) => MessageAttachment.fromMap(Map.from(a))) .toList(); + // Detect CONTROL + if (attachments.any((a) => a.type == AttachmentType.control)) { + isControl = true; + debugPrint('CONTROL detected: ${attachments.where((a) => a.type == AttachmentType.control).first}'); + } } } @@ -284,6 +293,7 @@ class MessagesModule { status: m['status']?.toString(), payload: Map.from(m.cast()), attachments: attachments, + isControl: isControl, ); } @@ -367,14 +377,21 @@ class MessagesModule { Future sendFileMessage( int chatId, int fileId, { + String? token, bool notify = true, }) async { final payload = { 'chatId': chatId, 'message': { - 'cid': DateTime.now().millisecondsSinceEpoch * -1, + 'isLive': false, + 'detectShare': false, + 'elements': [], + 'cid': DateTime.now().millisecondsSinceEpoch, 'attaches': [ - {'_type': 'FILE', 'fileId': fileId} + if (token != null) + {'_type': 'FILE', 'token': token} + else + {'_type': 'FILE', 'fileId': fileId} ], }, 'notify': notify, diff --git a/lib/core/storage/spoofing_service.dart b/lib/core/storage/spoofing_service.dart index c27240b..548f5ad 100644 --- a/lib/core/storage/spoofing_service.dart +++ b/lib/core/storage/spoofing_service.dart @@ -1,7 +1,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SpoofingService { - static const String hardcodedAppVersion = '26.15.3'; + static const String hardcodedAppVersion = '26.14.1'; static const int hardcodedBuildNumber = 6606; static Future?> getSpoofedSessionData() async { diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 9d50c57..73f1aed 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -384,6 +384,7 @@ class _ChatScreenState extends State itemCount: _messages.length, itemBuilder: (context, index) { final message = _messages[_messages.length - 1 - index]; + debugPrint('LIST_ITEM: ${message.id} isControl=${message.isControl} hasAttach=${message.attachments != null}'); final isMe = message.senderId == _myId; final prevMessage = index < _messages.length - 1 ? _messages[_messages.length - 2 - index] diff --git a/lib/frontend/widgets/attachment_panel.dart b/lib/frontend/widgets/attachment_panel.dart index f32a964..ffe3533 100644 --- a/lib/frontend/widgets/attachment_panel.dart +++ b/lib/frontend/widgets/attachment_panel.dart @@ -46,17 +46,6 @@ class _AttachmentPanelState extends State { return; } - final completer = Completer(); - void Function(Packet)? handler; - handler = (Packet packet) { - final payload = packet.payload; - if (payload is Map && payload['fileId'] == uploadInfo.fileId) { - api.unregisterPushHandler(Opcode.notifAttach); - completer.complete(); - } - }; - api.registerPushHandler(Opcode.notifAttach, (Packet p) => handler!(p)); - await api.sendRequest(Opcode.msgTyping, { 'chatId': widget.chatId, 'type': 'FILE', @@ -90,17 +79,61 @@ class _AttachmentPanelState extends State { statusCode = await _rawPost(secureSocket, uri, fileBytes, file.name); } - if (statusCode == 200) { - await completer.future.timeout( - const Duration(seconds: 30), + if (statusCode != 200) { + if (mounted) showCustomNotification(context, 'Ошибка загрузки: $statusCode'); + return; + } + + // Wait for notifAttach push + final pushCompleter = Completer(); + void Function(Packet)? pushHandler; + pushHandler = (Packet packet) { + final payload = packet.payload; + if (payload is Map && payload['fileId'] == uploadInfo.fileId) { + api.unregisterPushHandler(Opcode.notifAttach); + pushCompleter.complete(); + } + }; + api.registerPushHandler(Opcode.notifAttach, (Packet p) => pushHandler!(p)); + + await pushCompleter.future.timeout( + const Duration(seconds: 30), + onTimeout: () { + api.unregisterPushHandler(Opcode.notifAttach); + throw TimeoutException('Тайм-аут подтверждения загрузки'); + }, + ); + + // Retry loop: server may say "attachment in progress" (cmd=3) + for (var attempt = 0; attempt < 5; attempt++) { + final sent = await messagesModule.sendFileMessage( + widget.chatId, + uploadInfo.fileId, + token: uploadInfo.token, + ); + + // Listen for push again (another notifAttach may come) + final msgCompleter = Completer(); + void Function(Packet)? msgHandler; + msgHandler = (Packet packet) { + final payload = packet.payload; + if (payload is Map && payload['fileId'] == uploadInfo.fileId) { + api.unregisterPushHandler(Opcode.notifAttach); + msgCompleter.complete(true); + } + }; + api.registerPushHandler(Opcode.notifAttach, (Packet p) => msgHandler!(p)); + + final pushFuture = msgCompleter.future.timeout( + const Duration(seconds: 5), onTimeout: () { api.unregisterPushHandler(Opcode.notifAttach); - throw TimeoutException('Тайм-аут подтверждения загрузки'); + return false; }, ); - final sent = await messagesModule.sendFileMessage(widget.chatId, uploadInfo.fileId); - if (sent) { + final pushReceived = await pushFuture; + if (pushReceived && sent) { FileHistoryCache.add(FileHistoryEntry( fileId: uploadInfo.fileId, url: uploadInfo.url, @@ -111,13 +144,45 @@ class _AttachmentPanelState extends State { showCustomNotification(context, 'Файл отправлен'); widget.onClose(); } - } else { - if (mounted) showCustomNotification(context, 'Ошибка отправки сообщения'); + return; } - } else { - api.unregisterPushHandler(Opcode.notifAttach); - if (mounted) showCustomNotification(context, 'Ошибка загрузки: $statusCode'); + + // If push was received, check if message was sent + if (pushReceived) { + FileHistoryCache.add(FileHistoryEntry( + fileId: uploadInfo.fileId, + url: uploadInfo.url, + token: uploadInfo.token, + sentAt: DateTime.now(), + )); + if (mounted) { + showCustomNotification(context, 'Файл отправлен'); + widget.onClose(); + } + return; + } + + if (!sent) { + // msgSend failed, maybe server still processing — wait and retry + await Future.delayed(Duration(seconds: 1 + attempt)); + continue; + } + + // Sent ok, no push received (already processed earlier) + FileHistoryCache.add(FileHistoryEntry( + fileId: uploadInfo.fileId, + url: uploadInfo.url, + token: uploadInfo.token, + sentAt: DateTime.now(), + )); + if (mounted) { + showCustomNotification(context, 'Файл отправлен'); + widget.onClose(); + } + return; } + + if (mounted) showCustomNotification(context, 'Не удалось отправить сообщение'); } catch (e) { if (mounted) showCustomNotification(context, 'Ошибка: $e'); } finally { diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 949ae9f..a945ccc 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -8,7 +8,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../backend/modules/messages.dart'; import '../../models/attachment.dart'; -enum MessageType { text, attachment, voice } +enum MessageType { text, attachment, voice, control } enum BubbleShape { singleTop, singleBottom, singleMiddle, groupedMiddle } @@ -56,18 +56,23 @@ class MessageBubble extends StatelessWidget { bool get isGroupedWithNext { if (nextMessage == null) return false; + if (message.isControl) return false; if (nextMessage!.senderId != message.senderId) return false; final timeDiff = nextMessage!.time - message.time; return timeDiff < 300000; } BubbleShape get shape { - final hasPrevFromMe = prevMessage?.senderId == message.senderId; + if (message.isControl) { + return BubbleShape.singleMiddle; + } + + final hasPrevFromMe = prevMessage?.senderId == message.senderId && !prevMessage!.isControl; final prevTimeDiff = hasPrevFromMe ? message.time - prevMessage!.time : 999999999; - final hasNextFromMe = nextMessage?.senderId == message.senderId; + final hasNextFromMe = nextMessage?.senderId == message.senderId && !nextMessage!.isControl; final nextTimeDiff = hasNextFromMe ? nextMessage!.time - message.time : 999999999; @@ -83,6 +88,7 @@ class MessageBubble extends StatelessWidget { } MessageType get contentType { + if (message.isControl) return MessageType.control; if (message.attachments != null && message.attachments!.isNotEmpty) { final first = message.attachments!.first; if (first is ForwardedMessageAttachment) { @@ -214,6 +220,8 @@ class MessageBubble extends StatelessWidget { case BubbleShape.groupedMiddle: return 1; } + case MessageType.control: + return 4; } return 4; } @@ -242,6 +250,17 @@ class MessageBubble extends StatelessWidget { case BubbleShape.groupedMiddle: return 1; } + case MessageType.attachment: + switch (shape) { + case BubbleShape.singleTop: + return 1; + case BubbleShape.singleBottom: + return 1; + case BubbleShape.singleMiddle: + return 4; + case BubbleShape.groupedMiddle: + return 1; + } case MessageType.voice: switch (shape) { case BubbleShape.singleTop: @@ -253,6 +272,8 @@ class MessageBubble extends StatelessWidget { case BubbleShape.groupedMiddle: return 1; } + case MessageType.control: + return 4; } return 4; } @@ -284,12 +305,22 @@ class MessageBubble extends StatelessWidget { case BubbleShape.singleMiddle: return const EdgeInsets.symmetric(horizontal: 14, vertical: 4); } + case MessageType.control: + return const EdgeInsets.symmetric(horizontal: 14, vertical: 4); } return const EdgeInsets.symmetric(horizontal: 14, vertical: 10); } @override Widget build(BuildContext context) { + if (message.isControl) { + debugPrint('BUILD CONTROL: ${message.id}'); + return Padding( + padding: EdgeInsets.only(top: topMargin, bottom: bottomMargin), + child: Center(child: _buildControlContent(context)), + ); + } + final cs = Theme.of(context).colorScheme; final isDark = cs.brightness == Brightness.dark; @@ -362,16 +393,67 @@ class MessageBubble extends StatelessWidget { Widget _buildContent(BuildContext context) { switch (contentType) { + case MessageType.control: + return _buildControlContent(context); case MessageType.attachment: return _buildAttachmentContent(context); case MessageType.voice: return _buildVoiceContent(context); case MessageType.text: - default: return _buildTextContent(context); } } + Widget _buildControlContent(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final attachments = message.attachments; + if (attachments == null || attachments.isEmpty) return const SizedBox.shrink(); + + final control = attachments.first; + if (control is! ControlAttachment) return const SizedBox.shrink(); + + String? text; + switch (control.event) { + case 'system': + text = control.title; + break; + case 'new': + text = '${ContactCache.get(message.senderId) ?? 'Пользователь'} создал(а) чат'; + break; + case 'add': + final names = (control.userIds ?? []).map((id) => ContactCache.get(id) ?? 'Пользователь').join(', '); + text = '${ContactCache.get(message.senderId) ?? 'Пользователь'} добавил(а) $names'; + break; + case 'leave': + text = '${ContactCache.get(message.senderId) ?? 'Пользователь'} покинул(а) чат'; + break; + case 'joinByLink': + text = '${ContactCache.get(message.senderId) ?? 'Пользователь'} присоединился(-ась) к чату'; + break; + default: + text = control.title; + } + + if (text == null || text.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ); + } + Widget _buildTextContent(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = cs.brightness == Brightness.dark; diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 795db87..dd76314 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -431,6 +431,7 @@ class ControlAttachment extends MessageAttachment { final String? event; final String? title; final List? userIds; + final int? userId; const ControlAttachment({ super.previewData, @@ -439,15 +440,22 @@ class ControlAttachment extends MessageAttachment { this.event, this.title, this.userIds, + this.userId, }) : super(type: AttachmentType.control); factory ControlAttachment.fromMap(Map map) { + String? title = map['title']?.toString(); + if ((title == null || title.isEmpty) && map['shortMessage'] != null) { + title = map['shortMessage'].toString(); + } + return ControlAttachment( previewData: map['previewData']?.toString(), baseUrl: map['baseUrl']?.toString(), event: map['event']?.toString(), - title: map['title']?.toString(), + title: title, userIds: (map['userIds'] as List?)?.map((e) => e is int ? e : int.tryParse(e?.toString() ?? '') ?? 0).toList(), + userId: map['userId'] is int ? map['userId'] as int : int.tryParse(map['userId']?.toString() ?? ''), ); } @@ -459,6 +467,7 @@ class ControlAttachment extends MessageAttachment { 'event': event, 'title': title, 'userIds': userIds, + 'userId': userId, }; }