эвенты
This commit is contained in:
@@ -122,11 +122,7 @@ class Api {
|
||||
Future<Packet> 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';
|
||||
|
||||
@@ -100,6 +100,7 @@ class CachedMessage {
|
||||
final String? status;
|
||||
final Map<String, dynamic>? payload;
|
||||
final List<MessageAttachment>? 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<String, dynamic> 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<MessageAttachment>? attachments;
|
||||
bool isControl = false;
|
||||
if (linkType == 'FORWARD') {
|
||||
final fwdMap = Map<String, dynamic>.from(m.cast());
|
||||
attachments = [ForwardedMessageAttachment.fromMap(fwdMap)];
|
||||
@@ -271,6 +275,11 @@ class MessagesModule {
|
||||
.whereType<Map>()
|
||||
.map((a) => MessageAttachment.fromMap(Map<String, dynamic>.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<String, dynamic>.from(m.cast()),
|
||||
attachments: attachments,
|
||||
isControl: isControl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -367,14 +377,21 @@ class MessagesModule {
|
||||
Future<bool> 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': <dynamic>[],
|
||||
'cid': DateTime.now().millisecondsSinceEpoch,
|
||||
'attaches': [
|
||||
{'_type': 'FILE', 'fileId': fileId}
|
||||
if (token != null)
|
||||
{'_type': 'FILE', 'token': token}
|
||||
else
|
||||
{'_type': 'FILE', 'fileId': fileId}
|
||||
],
|
||||
},
|
||||
'notify': notify,
|
||||
|
||||
@@ -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<Map<String, dynamic>?> getSpoofedSessionData() async {
|
||||
|
||||
@@ -384,6 +384,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
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]
|
||||
|
||||
@@ -46,17 +46,6 @@ class _AttachmentPanelState extends State<AttachmentPanel> {
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
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<AttachmentPanel> {
|
||||
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>();
|
||||
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<bool>();
|
||||
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<AttachmentPanel> {
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -431,6 +431,7 @@ class ControlAttachment extends MessageAttachment {
|
||||
final String? event;
|
||||
final String? title;
|
||||
final List<int>? 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<String, dynamic> 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user