добавил перессылку сообщений

This commit is contained in:
jganenok
2025-11-16 09:29:27 +07:00
parent cb2e8c3009
commit e31a017d30
6 changed files with 255 additions and 110 deletions

View File

@@ -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} сообщений из очереди...");

View File

@@ -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<int, List<Message>> _messageCache = {};
final Map<int, Contact> _contactCache = {};
Map<String, dynamic>? _lastChatsPayload;
@@ -39,41 +34,30 @@ class ApiServiceV2 {
final Duration _chatsCacheTtl = const Duration(seconds: 5);
bool _chatsFetchedInThisSession = false;
final Map<String, dynamic> _presenceData = {};
final StreamController<Contact> _contactUpdatesController =
StreamController<Contact>.broadcast();
final StreamController<Map<String, dynamic>> _messageController =
StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get messages => _messageController.stream;
Stream<Contact> get contactUpdates => _contactUpdatesController.stream;
Stream<ConnectionInfo> get connectionState => _connectionManager.stateStream;
Stream<LogEntry> get logs => _connectionManager.logStream;
Stream<HealthMetrics> get healthMetrics =>
_connectionManager.healthMetricsStream;
ConnectionInfo get currentConnectionState => _connectionManager.currentState;
bool get isOnline => _connectionManager.isConnected;
bool get canSendMessages => _connectionManager.canSendMessages;
Future<void> 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<String, dynamic> 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<String, dynamic> payload) {
try {
final contact = Contact.fromJson(payload);
@@ -157,7 +133,6 @@ class ApiServiceV2 {
}
}
void _handlePresenceUpdate(Map<String, dynamic> payload) {
try {
_presenceData.addAll(payload);
@@ -173,7 +148,6 @@ class ApiServiceV2 {
}
}
Future<void> connect() async {
_logger.logConnection('Запрос подключения к серверу');
@@ -186,7 +160,6 @@ class ApiServiceV2 {
}
}
Future<void> reconnect() async {
_logger.logConnection('Запрос переподключения');
@@ -199,18 +172,15 @@ class ApiServiceV2 {
}
}
Future<void> 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<void> _performFullAuthenticationSequence() async {
_logger.logConnection(
'Выполнение полной последовательности аутентификации',
);
try {
await _waitForConnectionReady();
await _sendAuthenticationToken();
await _waitForAuthenticationConfirmation();
await _sendPingToConfirmSession();
await _requestChatsAndContacts();
_logger.logConnection(
@@ -249,14 +213,12 @@ class ApiServiceV2 {
}
}
Future<void> _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<void> _sendAuthenticationToken() async {
if (_authToken == null) {
_logger.logError('Токен аутентификации отсутствует');
@@ -295,17 +256,14 @@ class ApiServiceV2 {
_connectionManager.sendMessage(19, payload);
await _waitForAuthenticationConfirmation();
}
Future<void> _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<void> _sendPingToConfirmSession() async {
_logger.logConnection('Отправка ping для подтверждения готовности сессии');
final payload = {"interactive": true};
_connectionManager.sendMessage(1, payload);
await Future.delayed(const Duration(milliseconds: 500));
_logger.logConnection('Ping отправлен, сессия готова');
}
Future<void> _waitForSessionReady() async {
const maxWaitTime = Duration(seconds: 30);
final startTime = DateTime.now();
@@ -345,22 +300,18 @@ class ApiServiceV2 {
throw Exception('Таймаут ожидания готовности сессии');
}
Future<void> _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<void> disconnect() async {
_logger.logConnection('Отключение от сервера');
@@ -372,7 +323,6 @@ class ApiServiceV2 {
}
}
int _sendMessage(int opcode, Map<String, dynamic> 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<void> 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<Map<String, dynamic>> authenticateWithToken(String token) async {
_logger.logConnection('Аутентификация с токеном');
@@ -511,11 +454,9 @@ class ApiServiceV2 {
}
}
Future<Map<String, dynamic>> 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 = <int>{};
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<Map<String, dynamic>>());
await chatService.cacheContacts(contacts);
_preloadContactAvatars(contacts);
_logger.logConnection(
@@ -639,7 +574,6 @@ class ApiServiceV2 {
}
}
Future<List<Message>> 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<void> 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<void> blockContact(int contactId) async {
_logger.logConnection(
'Блокировка контакта',
@@ -841,7 +792,6 @@ class ApiServiceV2 {
_sendMessage(34, {'contactId': contactId, 'action': 'BLOCK'});
}
Future<void> 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<int> participantIds) {
_logger.logConnection(
'Создание группы',
@@ -867,7 +815,6 @@ class ApiServiceV2 {
_sendMessage(48, payload);
}
void addGroupMember(
int chatId,
List<int> userIds, {
@@ -887,7 +834,6 @@ class ApiServiceV2 {
_sendMessage(77, payload);
}
void removeGroupMember(
int chatId,
List<int> 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<Contact> 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<void> clearAllCaches() async {
_messageCache.clear();
_contactCache.clear();
clearChatsCache();
try {
await CacheService().clear();
await AvatarCacheService().clearAvatarCache();
@@ -1004,25 +939,21 @@ class ApiServiceV2 {
_logger.logConnection('Все кэши очищены');
}
Future<void> saveToken(String token) async {
_authToken = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('authToken', token);
_logger.logConnection('Токен сохранен');
}
Future<bool> hasToken() async {
final prefs = await SharedPreferences.getInstance();
_authToken = prefs.getString('authToken');
return _authToken != null;
}
Future<void> logout() async {
_logger.logConnection('Выход из системы');
@@ -1038,7 +969,6 @@ class ApiServiceV2 {
}
}
Future<void> _preloadContactAvatars(List<Contact> contacts) async {
try {
final avatarUrls = contacts
@@ -1059,7 +989,6 @@ class ApiServiceV2 {
}
}
Future<void> _preloadMessageImages(List<Message> messages) async {
try {
final imageUrls = <String>[];
@@ -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<Map<String, dynamic>> 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();

View File

@@ -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<ChatScreen> {
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<dynamic>;
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<String, dynamic>;
return _buildForwardChatTile(context, chat, message);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'),
),
],
),
);
}
Widget _buildForwardChatTile(
BuildContext context,
Map<String, dynamic> 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<String, dynamic>? ?? {};
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<ChatScreen> {
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!)

View File

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