From e31a017d307507994de2ea09f1030c7065e52ebc Mon Sep 17 00:00:00 2001 From: jganenok Date: Sun, 16 Nov 2025 09:29:27 +0700 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D1=83=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + lib/api_service.dart | 23 ++++ lib/api_service_v2.dart | 128 ++++--------------- lib/chat_screen.dart | 184 +++++++++++++++++++++++++++ lib/widgets/chat_message_bubble.dart | 19 ++- pubspec.lock | 8 +- 6 files changed, 255 insertions(+), 110 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..337b1e3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "C:/Users/Kiril/OneDrive/Desktop/KOMET/KOMETABLYATSKAYA/app/linux" +} \ No newline at end of file diff --git a/lib/api_service.dart b/lib/api_service.dart index 1067415..5a797da 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -1384,6 +1384,29 @@ class ApiService { } } + void forwardMessage(int targetChatId, String messageId, int sourceChatId) { + final int clientMessageId = DateTime.now().millisecondsSinceEpoch; + final payload = { + "chatId": targetChatId, + "message": { + "cid": clientMessageId, + "link": { + "type": "FORWARD", + "messageId": messageId, + "chatId": sourceChatId, + }, + "attaches": [], + }, + "notify": true, + }; + + if (_isSessionOnline) { + _sendMessage(64, payload); + } else { + _messageQueue.add({'opcode': 64, 'payload': payload}); + } + } + void _processMessageQueue() { if (_messageQueue.isEmpty) return; print("Отправка ${_messageQueue.length} сообщений из очереди..."); diff --git a/lib/api_service_v2.dart b/lib/api_service_v2.dart index d3d5820..dc2934a 100644 --- a/lib/api_service_v2.dart +++ b/lib/api_service_v2.dart @@ -15,23 +15,18 @@ import 'services/cache_service.dart'; import 'services/avatar_cache_service.dart'; import 'services/chat_cache_service.dart'; - class ApiServiceV2 { ApiServiceV2._privateConstructor(); static final ApiServiceV2 instance = ApiServiceV2._privateConstructor(); - final ConnectionManager _connectionManager = ConnectionManager(); - final ConnectionLogger _logger = ConnectionLogger(); - String? _authToken; bool _isInitialized = false; bool _isAuthenticated = false; - final Map> _messageCache = {}; final Map _contactCache = {}; Map? _lastChatsPayload; @@ -39,41 +34,30 @@ class ApiServiceV2 { final Duration _chatsCacheTtl = const Duration(seconds: 5); bool _chatsFetchedInThisSession = false; - final Map _presenceData = {}; - final StreamController _contactUpdatesController = StreamController.broadcast(); final StreamController> _messageController = StreamController>.broadcast(); - Stream> get messages => _messageController.stream; - Stream get contactUpdates => _contactUpdatesController.stream; - Stream get connectionState => _connectionManager.stateStream; - Stream get logs => _connectionManager.logStream; - Stream get healthMetrics => _connectionManager.healthMetricsStream; - ConnectionInfo get currentConnectionState => _connectionManager.currentState; - bool get isOnline => _connectionManager.isConnected; - bool get canSendMessages => _connectionManager.canSendMessages; - Future initialize() async { if (_isInitialized) { _logger.logConnection('ApiServiceV2 уже инициализирован'); @@ -86,7 +70,6 @@ class ApiServiceV2 { await _connectionManager.initialize(); _setupMessageHandlers(); - _isAuthenticated = false; _isInitialized = true; @@ -98,19 +81,16 @@ class ApiServiceV2 { } } - void _setupMessageHandlers() { _connectionManager.messageStream.listen((message) { _handleIncomingMessage(message); }); } - void _handleIncomingMessage(Map message) { try { _logger.logMessage('IN', message); - if (message['opcode'] == 19 && message['cmd'] == 1 && message['payload'] != null) { @@ -118,17 +98,14 @@ class ApiServiceV2 { _logger.logConnection('Аутентификация успешна'); } - if (message['opcode'] == 128 && message['payload'] != null) { _handleContactUpdate(message['payload']); } - if (message['opcode'] == 129 && message['payload'] != null) { _handlePresenceUpdate(message['payload']); } - _messageController.add(message); } catch (e) { _logger.logError( @@ -138,7 +115,6 @@ class ApiServiceV2 { } } - void _handleContactUpdate(Map payload) { try { final contact = Contact.fromJson(payload); @@ -157,7 +133,6 @@ class ApiServiceV2 { } } - void _handlePresenceUpdate(Map payload) { try { _presenceData.addAll(payload); @@ -173,7 +148,6 @@ class ApiServiceV2 { } } - Future connect() async { _logger.logConnection('Запрос подключения к серверу'); @@ -186,7 +160,6 @@ class ApiServiceV2 { } } - Future reconnect() async { _logger.logConnection('Запрос переподключения'); @@ -199,18 +172,15 @@ class ApiServiceV2 { } } - Future forceReconnect() async { _logger.logConnection('Принудительное переподключение'); try { - _isAuthenticated = false; await _connectionManager.forceReconnect(); _logger.logConnection('Принудительное переподключение успешно'); - await _performFullAuthenticationSequence(); } catch (e) { _logger.logError('Ошибка принудительного переподключения', error: e); @@ -218,26 +188,20 @@ class ApiServiceV2 { } } - Future _performFullAuthenticationSequence() async { _logger.logConnection( 'Выполнение полной последовательности аутентификации', ); try { - await _waitForConnectionReady(); - await _sendAuthenticationToken(); - await _waitForAuthenticationConfirmation(); - await _sendPingToConfirmSession(); - await _requestChatsAndContacts(); _logger.logConnection( @@ -249,14 +213,12 @@ class ApiServiceV2 { } } - Future _waitForConnectionReady() async { const maxWaitTime = Duration(seconds: 30); final startTime = DateTime.now(); while (DateTime.now().difference(startTime) < maxWaitTime) { if (_connectionManager.currentState.isActive) { - await Future.delayed(const Duration(milliseconds: 500)); return; } @@ -266,7 +228,6 @@ class ApiServiceV2 { throw Exception('Таймаут ожидания готовности соединения'); } - Future _sendAuthenticationToken() async { if (_authToken == null) { _logger.logError('Токен аутентификации отсутствует'); @@ -295,17 +256,14 @@ class ApiServiceV2 { _connectionManager.sendMessage(19, payload); - await _waitForAuthenticationConfirmation(); } - Future _waitForAuthenticationConfirmation() async { const maxWaitTime = Duration(seconds: 10); final startTime = DateTime.now(); while (DateTime.now().difference(startTime) < maxWaitTime) { - if (_connectionManager.currentState.isActive && _isAuthenticated) { _logger.logConnection('Аутентификация подтверждена'); return; @@ -316,20 +274,17 @@ class ApiServiceV2 { throw Exception('Таймаут ожидания подтверждения аутентификации'); } - Future _sendPingToConfirmSession() async { _logger.logConnection('Отправка ping для подтверждения готовности сессии'); final payload = {"interactive": true}; _connectionManager.sendMessage(1, payload); - await Future.delayed(const Duration(milliseconds: 500)); _logger.logConnection('Ping отправлен, сессия готова'); } - Future _waitForSessionReady() async { const maxWaitTime = Duration(seconds: 30); final startTime = DateTime.now(); @@ -345,22 +300,18 @@ class ApiServiceV2 { throw Exception('Таймаут ожидания готовности сессии'); } - Future _requestChatsAndContacts() async { _logger.logConnection('Запрос чатов и контактов'); - final chatsPayload = {"chatsCount": 100}; _connectionManager.sendMessage(48, chatsPayload); - final contactsPayload = {"status": "BLOCKED", "count": 100, "from": 0}; _connectionManager.sendMessage(36, contactsPayload); } - Future disconnect() async { _logger.logConnection('Отключение от сервера'); @@ -372,7 +323,6 @@ class ApiServiceV2 { } } - int _sendMessage(int opcode, Map payload) { if (!canSendMessages) { _logger.logConnection( @@ -382,7 +332,6 @@ class ApiServiceV2 { return -1; } - if (_requiresAuthentication(opcode) && !_isAuthenticated) { _logger.logConnection( 'Сообщение не отправлено - требуется аутентификация', @@ -407,9 +356,7 @@ class ApiServiceV2 { } } - bool _requiresAuthentication(int opcode) { - const authRequiredOpcodes = { 19, // Аутентификация 32, // Получение контактов @@ -430,7 +377,6 @@ class ApiServiceV2 { return authRequiredOpcodes.contains(opcode); } - Future sendHandshake() async { _logger.logConnection('Отправка handshake'); @@ -453,7 +399,6 @@ class ApiServiceV2 { _sendMessage(6, payload); } - void requestOtp(String phoneNumber) { _logger.logConnection('Запрос OTP', data: {'phone': phoneNumber}); @@ -465,7 +410,6 @@ class ApiServiceV2 { _sendMessage(17, payload); } - void verifyCode(String token, String code) { _logger.logConnection( 'Проверка кода', @@ -480,7 +424,6 @@ class ApiServiceV2 { _sendMessage(18, payload); } - Future> authenticateWithToken(String token) async { _logger.logConnection('Аутентификация с токеном'); @@ -511,11 +454,9 @@ class ApiServiceV2 { } } - Future> getChatsAndContacts({bool force = false}) async { _logger.logConnection('Запрос чатов и контактов', data: {'force': force}); - if (!force && _lastChatsPayload != null && _lastChatsAt != null) { if (DateTime.now().difference(_lastChatsAt!) < _chatsCacheTtl) { _logger.logConnection('Возвращаем данные из локального кэша'); @@ -523,7 +464,6 @@ class ApiServiceV2 { } } - if (!force) { final chatService = ChatCacheService(); final cachedChats = await chatService.getCachedChats(); @@ -562,7 +502,6 @@ class ApiServiceV2 { } } - await _waitForSessionReady(); try { @@ -582,7 +521,6 @@ class ApiServiceV2 { return result; } - final contactIds = {}; for (var chatJson in chatListJson) { final participants = @@ -610,18 +548,15 @@ class ApiServiceV2 { _lastChatsAt = DateTime.now(); _chatsFetchedInThisSession = true; - final contacts = contactListJson .map((json) => Contact.fromJson(json)) .toList(); updateContactCache(contacts); - final chatService = ChatCacheService(); await chatService.cacheChats(chatListJson.cast>()); await chatService.cacheContacts(contacts); - _preloadContactAvatars(contacts); _logger.logConnection( @@ -639,7 +574,6 @@ class ApiServiceV2 { } } - Future> getMessageHistory( int chatId, { bool force = false, @@ -649,13 +583,11 @@ class ApiServiceV2 { data: {'chat_id': chatId, 'force': force}, ); - if (!force && _messageCache.containsKey(chatId)) { _logger.logConnection('История сообщений загружена из локального кэша'); return _messageCache[chatId]!; } - if (!force) { final chatService = ChatCacheService(); final cachedMessages = await chatService.getCachedChatMessages(chatId); @@ -667,7 +599,6 @@ class ApiServiceV2 { } } - await _waitForSessionReady(); try { @@ -703,11 +634,9 @@ class ApiServiceV2 { _messageCache[chatId] = messagesList; - final chatService = ChatCacheService(); await chatService.cacheChatMessages(chatId, messagesList); - _preloadMessageImages(messagesList); _logger.logConnection( @@ -725,7 +654,6 @@ class ApiServiceV2 { } } - void sendMessage(int chatId, String text, {String? replyToMessageId}) { _logger.logConnection( 'Отправка сообщения', @@ -754,6 +682,33 @@ class ApiServiceV2 { _sendMessage(64, payload); } + void forwardMessage(int targetChatId, String messageId, int sourceChatId) { + _logger.logConnection( + 'Пересылка сообщения', + data: { + 'target_chat_id': targetChatId, + 'message_id': messageId, + 'source_chat_id': sourceChatId, + }, + ); + + final int clientMessageId = DateTime.now().millisecondsSinceEpoch; + final payload = { + "chatId": targetChatId, + "message": { + "cid": clientMessageId, + "link": { + "type": "FORWARD", + "messageId": messageId, + "chatId": sourceChatId, + }, + "attaches": [], + }, + "notify": true, + }; + + _sendMessage(64, payload); + } Future sendPhotoMessage( int chatId, { @@ -777,7 +732,6 @@ class ApiServiceV2 { if (image == null) return; } - final seq80 = _sendMessage(80, {"count": 1}); final resp80 = await messages .firstWhere((m) => m['seq'] == seq80) @@ -785,7 +739,6 @@ class ApiServiceV2 { final String uploadUrl = resp80['payload']['url']; - var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); request.files.add(await http.MultipartFile.fromPath('file', image.path)); var streamed = await request.send(); @@ -802,7 +755,6 @@ class ApiServiceV2 { if (photos.isEmpty) throw Exception('Не получен токен фото'); final String photoToken = (photos.values.first as Map)['token']; - final int cid = cidOverride ?? DateTime.now().millisecondsSinceEpoch; final payload = { "chatId": chatId, @@ -832,7 +784,6 @@ class ApiServiceV2 { } } - Future blockContact(int contactId) async { _logger.logConnection( 'Блокировка контакта', @@ -841,7 +792,6 @@ class ApiServiceV2 { _sendMessage(34, {'contactId': contactId, 'action': 'BLOCK'}); } - Future unblockContact(int contactId) async { _logger.logConnection( 'Разблокировка контакта', @@ -850,13 +800,11 @@ class ApiServiceV2 { _sendMessage(34, {'contactId': contactId, 'action': 'UNBLOCK'}); } - void getBlockedContacts() { _logger.logConnection('Запрос заблокированных контактов'); _sendMessage(36, {'status': 'BLOCKED', 'count': 100, 'from': 0}); } - void createGroup(String name, List participantIds) { _logger.logConnection( 'Создание группы', @@ -867,7 +815,6 @@ class ApiServiceV2 { _sendMessage(48, payload); } - void addGroupMember( int chatId, List userIds, { @@ -887,7 +834,6 @@ class ApiServiceV2 { _sendMessage(77, payload); } - void removeGroupMember( int chatId, List userIds, { @@ -907,13 +853,11 @@ class ApiServiceV2 { _sendMessage(77, payload); } - void leaveGroup(int chatId) { _logger.logConnection('Выход из группы', data: {'chat_id': chatId}); _sendMessage(58, {"chatId": chatId}); } - void sendReaction(int chatId, String messageId, String emoji) { _logger.logConnection( 'Отправка реакции', @@ -928,7 +872,6 @@ class ApiServiceV2 { _sendMessage(178, payload); } - void removeReaction(int chatId, String messageId) { _logger.logConnection( 'Удаление реакции', @@ -939,13 +882,11 @@ class ApiServiceV2 { _sendMessage(179, payload); } - void sendTyping(int chatId, {String type = "TEXT"}) { final payload = {"chatId": chatId, "type": type}; _sendMessage(65, payload); } - DateTime? getLastSeen(int userId) { final userPresence = _presenceData[userId.toString()]; if (userPresence != null && userPresence['seen'] != null) { @@ -955,7 +896,6 @@ class ApiServiceV2 { return null; } - void updateContactCache(List contacts) { _contactCache.clear(); for (final contact in contacts) { @@ -967,12 +907,10 @@ class ApiServiceV2 { ); } - Contact? getCachedContact(int contactId) { return _contactCache[contactId]; } - void clearChatsCache() { _lastChatsPayload = null; _lastChatsAt = null; @@ -980,19 +918,16 @@ class ApiServiceV2 { _logger.logConnection('Кэш чатов очищен'); } - void clearMessageCache(int chatId) { _messageCache.remove(chatId); _logger.logConnection('Кэш сообщений очищен', data: {'chat_id': chatId}); } - Future clearAllCaches() async { _messageCache.clear(); _contactCache.clear(); clearChatsCache(); - try { await CacheService().clear(); await AvatarCacheService().clearAvatarCache(); @@ -1004,25 +939,21 @@ class ApiServiceV2 { _logger.logConnection('Все кэши очищены'); } - Future saveToken(String token) async { _authToken = token; final prefs = await SharedPreferences.getInstance(); - await prefs.setString('authToken', token); _logger.logConnection('Токен сохранен'); } - Future hasToken() async { final prefs = await SharedPreferences.getInstance(); _authToken = prefs.getString('authToken'); return _authToken != null; } - Future logout() async { _logger.logConnection('Выход из системы'); @@ -1038,7 +969,6 @@ class ApiServiceV2 { } } - Future _preloadContactAvatars(List contacts) async { try { final avatarUrls = contacts @@ -1059,7 +989,6 @@ class ApiServiceV2 { } } - Future _preloadMessageImages(List messages) async { try { final imageUrls = []; @@ -1091,14 +1020,12 @@ class ApiServiceV2 { } } - String _generateDeviceId() { final timestamp = DateTime.now().millisecondsSinceEpoch; final random = (timestamp % 1000000).toString().padLeft(6, '0'); return "$timestamp$random"; } - Future> getStatistics() async { final imageCacheStats = await ImageCacheService.instance.getCacheStats(); final cacheServiceStats = await CacheService().getCacheStats(); @@ -1121,7 +1048,6 @@ class ApiServiceV2 { }; } - void dispose() { _logger.logConnection('Освобождение ресурсов ApiServiceV2'); _connectionManager.dispose(); diff --git a/lib/chat_screen.dart b/lib/chat_screen.dart index a53aa39..1e6254b 100644 --- a/lib/chat_screen.dart +++ b/lib/chat_screen.dart @@ -13,6 +13,7 @@ import 'package:gwid/models/message.dart'; import 'package:gwid/widgets/chat_message_bubble.dart'; import 'package:image_picker/image_picker.dart'; import 'package:gwid/services/chat_cache_service.dart'; +import 'package:gwid/services/avatar_cache_service.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:gwid/screens/group_settings_screen.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -951,6 +952,188 @@ class _ChatScreenState extends State { FocusScope.of(context).requestFocus(FocusNode()); } + void _forwardMessage(Message message) { + _showForwardDialog(message); + } + + void _showForwardDialog(Message message) { + final chatData = ApiService.instance.lastChatsPayload; + if (chatData == null || chatData['chats'] == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Список чатов не загружен'), + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + final chats = chatData['chats'] as List; + final availableChats = chats + .where( + (chat) => chat['id'] != widget.chatId || chat['id'] == 0, + ) //шелуха обработка избранного + .toList(); + + if (availableChats.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Нет доступных чатов для пересылки'), + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Переслать сообщение'), + content: SizedBox( + width: double.maxFinite, + height: 400, + child: ListView.builder( + itemCount: availableChats.length, + itemBuilder: (context, index) { + final chat = availableChats[index] as Map; + return _buildForwardChatTile(context, chat, message); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + ], + ), + ); + } + + Widget _buildForwardChatTile( + BuildContext context, + Map chat, + Message message, + ) { + final chatId = chat['id'] as int; + final chatTitle = chat['title'] as String?; + // шелуха отдельная для избранного + String chatName; + Widget avatar; + String subtitle = ''; + + if (chatId == 0) { + chatName = 'Избранное'; + avatar = CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Icon( + Icons.star, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ); + subtitle = 'Сохраненные сообщения'; + } else { + final participants = chat['participants'] as Map? ?? {}; + final isGroupChat = participants.length > 2; + + if (isGroupChat) { + chatName = chatTitle?.isNotEmpty == true ? chatTitle! : 'Группа'; + avatar = CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Icon( + Icons.group, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ); + subtitle = '${participants.length} участников'; + } else { + final otherParticipantId = participants.keys + .map((id) => int.parse(id)) + .firstWhere((id) => id != _actualMyId, orElse: () => 0); + + final contact = _contactDetailsCache[otherParticipantId]; + chatName = contact?.name ?? chatTitle ?? 'Чат $chatId'; + + final avatarUrl = contact?.photoBaseUrl; + + avatar = AvatarCacheService().getAvatarWidget( + avatarUrl, + userId: otherParticipantId, + size: 48, + fallbackText: contact?.name ?? chatTitle ?? 'Чат $chatId', + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + ); + + subtitle = contact?.status ?? ''; + } + } + + return Container( + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surface, + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + width: 1, + ), + ), + child: ClipOval(child: avatar), + ), + title: Text( + chatName, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: subtitle.isNotEmpty + ? Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + : null, + trailing: Icon( + Icons.arrow_forward_ios, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onTap: () { + Navigator.of(context).pop(); + _performForward(message, chatId); + }, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ); + } + + void _performForward(Message message, int targetChatId) { + ApiService.instance.forwardMessage(targetChatId, message.id, widget.chatId); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Сообщение переслано'), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 2), + ), + ); + } + void _cancelReply() { setState(() { _replyingToMessage = null; @@ -1482,6 +1665,7 @@ class _ChatScreenState extends State { myUserId: _actualMyId, chatId: widget.chatId, onReply: () => _replyToMessage(item.message), + onForward: () => _forwardMessage(item.message), onEdit: isMe ? () => _editMessage(item.message) : null, canEditMessage: isMe ? item.message.canEdit(_actualMyId!) diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index fe78e3e..7265fc0 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -23,7 +23,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:open_file/open_file.dart'; import 'package:gwid/full_screen_video_player.dart'; - bool _currentIsDark = false; enum MessageReadStatus { @@ -101,13 +100,10 @@ class FileDownloadProgressService { Color _getUserColor(int userId, BuildContext context) { final bool isDark = Theme.of(context).brightness == Brightness.dark; - if (isDark != _currentIsDark) { - _currentIsDark = isDark; } - final List materialYouColors = isDark ? [ // Темная тема @@ -161,7 +157,6 @@ Color _getUserColor(int userId, BuildContext context) { final colorIndex = userId % materialYouColors.length; final color = materialYouColors[colorIndex]; - return color; } @@ -177,6 +172,7 @@ class ChatMessageBubble extends StatelessWidget { final Function(String)? onReaction; final VoidCallback? onRemoveReaction; final VoidCallback? onReply; + final VoidCallback? onForward; final int? myUserId; final bool? canEditMessage; final bool isGroupChat; @@ -207,6 +203,7 @@ class ChatMessageBubble extends StatelessWidget { this.onReaction, this.onRemoveReaction, this.onReply, + this.onForward, this.myUserId, this.canEditMessage, this.isGroupChat = false, @@ -871,6 +868,7 @@ class ChatMessageBubble extends StatelessWidget { onDeleteForAll: onDeleteForAll, onReaction: onReaction, onRemoveReaction: onRemoveReaction, + onForward: onForward, canEditMessage: canEditMessage ?? false, hasUserReaction: hasUserReaction, ); @@ -3513,6 +3511,7 @@ class _MessageContextMenu extends StatefulWidget { final VoidCallback? onDeleteForAll; final Function(String)? onReaction; final VoidCallback? onRemoveReaction; + final VoidCallback? onForward; final bool canEditMessage; final bool hasUserReaction; @@ -3525,6 +3524,7 @@ class _MessageContextMenu extends StatefulWidget { this.onDeleteForAll, this.onReaction, this.onRemoveReaction, + this.onForward, required this.canEditMessage, required this.hasUserReaction, }); @@ -3799,6 +3799,15 @@ class _MessageContextMenuState extends State<_MessageContextMenu> widget.onReply!(); }, ), + if (widget.onForward != null) + _buildActionButton( + icon: Icons.forward_rounded, + text: 'Переслать', + onTap: () { + Navigator.pop(context); + widget.onForward!(); + }, + ), if (widget.onEdit != null) _buildActionButton( icon: widget.canEditMessage diff --git a/pubspec.lock b/pubspec.lock index bab6763..50b3702 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -777,10 +777,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1286,10 +1286,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" timezone: dependency: "direct main" description: