Merge branch 'feature/FullStack' of https://github.com/KometTeam/Komet into feature/FullStack

This commit is contained in:
klockky
2026-05-14 17:06:26 +03:00
7 changed files with 205 additions and 35 deletions
+1 -5
View File
@@ -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';
+19 -2
View File
@@ -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 -1
View File
@@ -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 {
@@ -403,6 +403,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]
+87 -22
View File
@@ -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 {
+86 -4
View File
@@ -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;
+10 -1
View File
@@ -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,
};
}