инфо
This commit is contained in:
@@ -278,7 +278,6 @@ class MessagesModule {
|
||||
// Detect CONTROL
|
||||
if (attachments.any((a) => a.type == AttachmentType.control)) {
|
||||
isControl = true;
|
||||
debugPrint('CONTROL detected: ${attachments.where((a) => a.type == AttachmentType.control).first}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +303,7 @@ class MessagesModule {
|
||||
return int.tryParse(value.toString()) ?? 0;
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
Future<String> sendMessage(
|
||||
int accountId,
|
||||
int chatId,
|
||||
String text, {
|
||||
@@ -321,7 +320,22 @@ class MessagesModule {
|
||||
'notify': notify,
|
||||
};
|
||||
|
||||
await _api.sendRequest(Opcode.msgSend, payload);
|
||||
final response = await _api.sendRequest(Opcode.msgSend, payload);
|
||||
if (!response.isOk) {
|
||||
final msg = (response.payload is Map)
|
||||
? (response.payload['localizedMessage'] ?? response.payload['message'] ?? 'Ошибка отправки')
|
||||
: 'Ошибка отправки';
|
||||
throw Exception(msg.toString());
|
||||
}
|
||||
final data = response.payload;
|
||||
if (data is Map) {
|
||||
final msgMap = data['message'];
|
||||
if (msgMap is Map) {
|
||||
final id = msgMap['id'];
|
||||
if (id != null) return id.toString();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<TranscriptionResult> requestTranscription(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -787,7 +787,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 2),
|
||||
padding: const EdgeInsets.fromLTRB(20, 6, 20, 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -933,7 +933,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 2, 20, 8),
|
||||
padding: const EdgeInsets.fromLTRB(20, 3, 20, 4),
|
||||
child: Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/frontend/screens/chats/chat_info_screen.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -51,7 +52,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final GlobalKey _listKey = GlobalKey();
|
||||
bool _hasText = false;
|
||||
bool _isLoading = true;
|
||||
bool _isSending = false;
|
||||
bool _showAttachmentPanel = false;
|
||||
late AnimationController _shimmerController;
|
||||
List<CachedMessage> _messages = [];
|
||||
@@ -64,6 +64,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
late final AnimationController _floatingDateAnimController;
|
||||
final Map<int, GlobalKey> _separatorKeys = {};
|
||||
double _lastScrollOffset = 0;
|
||||
String? _lastSentId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -87,10 +88,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final activeProfile = await AppDatabase.loadActiveProfile();
|
||||
_myId = activeProfile?.id ?? 0;
|
||||
ChatsModule.getChat(_myId, widget.chatId).then((value) {
|
||||
chat = value[0];
|
||||
}).catchError((error) {
|
||||
|
||||
});
|
||||
if (mounted && value.isNotEmpty) {
|
||||
setState(() { chat = value.first; });
|
||||
}
|
||||
}).catchError((_) {});
|
||||
|
||||
final cachedRows = await AppDatabase.loadMessages(
|
||||
_myId,
|
||||
@@ -155,17 +156,29 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
String? _effectiveStatus(CachedMessage msg) {
|
||||
if (msg.senderId != _myId) return null;
|
||||
if (msg.status == 'sending' || msg.status == 'error') return msg.status;
|
||||
final c = chat;
|
||||
if (c == null) return 'sent';
|
||||
int otherReadTime = 0;
|
||||
for (final entry in c.participants.entries) {
|
||||
if (entry.key != _myId && entry.value > otherReadTime) {
|
||||
otherReadTime = entry.value;
|
||||
}
|
||||
}
|
||||
if (otherReadTime > 0 && otherReadTime >= msg.time) return 'read';
|
||||
return 'sent';
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty || _myId == 0) return;
|
||||
|
||||
setState(() {
|
||||
_isSending = true;
|
||||
});
|
||||
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
try {
|
||||
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final tempMessage = CachedMessage(
|
||||
id: tempId,
|
||||
@@ -178,6 +191,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_lastSentId = tempId;
|
||||
_messages.add(tempMessage);
|
||||
_messageController.clear();
|
||||
_hasText = false;
|
||||
@@ -189,13 +203,13 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
_scrollToBottom();
|
||||
|
||||
await messagesModule.sendMessage(_myId, widget.chatId, text);
|
||||
final actualId = await messagesModule.sendMessage(_myId, widget.chatId, text);
|
||||
|
||||
final index = _messages.indexWhere((m) => m.id == tempId);
|
||||
if (index != -1) {
|
||||
if (index != -1 && mounted) {
|
||||
setState(() {
|
||||
_messages[index] = CachedMessage(
|
||||
id: tempId,
|
||||
id: actualId.isNotEmpty ? actualId : tempId,
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
senderId: _myId,
|
||||
@@ -206,12 +220,21 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error sending message: $e');
|
||||
Haptics.error();
|
||||
} finally {
|
||||
setState(() {
|
||||
_isSending = false;
|
||||
});
|
||||
final index = _messages.indexWhere((m) => m.id == tempId);
|
||||
if (index != -1 && mounted) {
|
||||
setState(() {
|
||||
_messages[index] = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
senderId: _myId,
|
||||
text: text,
|
||||
time: now,
|
||||
status: 'error',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +452,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
// TODO: Локализация
|
||||
// TODO: Cклонения
|
||||
String? status = chat?.type == "CHAT" ? "${chat?.participants.length.toString()} участников" : "last seen recently";
|
||||
final String status = chat?.type == "CHAT" ? "${chat?.participants.length ?? 0} участников" : "last seen recently";
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: PreferredSize(
|
||||
@@ -505,7 +528,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
],
|
||||
),
|
||||
Text(
|
||||
status ?? "",
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
@@ -590,8 +613,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final msgItem = item as _MessageItem;
|
||||
final message = msgItem.message;
|
||||
final msgIndex = msgItem.index;
|
||||
debugPrint(
|
||||
'LIST_ITEM: ${message.id} isControl=${message.isControl} hasAttach=${message.attachments != null}');
|
||||
final isMe = message.senderId == _myId;
|
||||
final prevMessage =
|
||||
msgIndex > 0 ? _messages[msgIndex - 1] : null;
|
||||
@@ -599,14 +620,26 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
? _messages[msgIndex + 1]
|
||||
: null;
|
||||
|
||||
return MessageBubble(
|
||||
final bubble = MessageBubble(
|
||||
message: message,
|
||||
isMe: isMe,
|
||||
myId: _myId,
|
||||
prevMessage: prevMessage,
|
||||
nextMessage: nextMessage,
|
||||
chatType: chat?.type ?? 'CHAT',
|
||||
overrideStatus: _effectiveStatus(message),
|
||||
);
|
||||
|
||||
if (isMe && message.id == _lastSentId) {
|
||||
return _SentMessageAnimation(
|
||||
key: ValueKey('anim_${message.id}'),
|
||||
onComplete: () {
|
||||
if (mounted) setState(() => _lastSentId = null);
|
||||
},
|
||||
child: bubble,
|
||||
);
|
||||
}
|
||||
return bubble;
|
||||
},
|
||||
),
|
||||
if (_lastFloatingDate != null)
|
||||
@@ -805,22 +838,33 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
Icon(Symbols.face, color: mutedIcon, size: 24, weight: 400),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 16),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Message',
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
child: Focus(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.enter &&
|
||||
!HardwareKeyboard.instance.isShiftPressed) {
|
||||
if (_hasText) _sendMessage();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 16),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Message',
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -892,3 +936,59 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SentMessageAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onComplete;
|
||||
|
||||
const _SentMessageAnimation({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SentMessageAnimation> createState() => _SentMessageAnimationState();
|
||||
}
|
||||
|
||||
class _SentMessageAnimationState extends State<_SentMessageAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final Animation<double> _opacity;
|
||||
late final Animation<double> _slide;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
);
|
||||
_opacity = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut);
|
||||
_slide = Tween<double>(begin: 16, end: 0).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.easeOut),
|
||||
);
|
||||
_ctrl.forward().whenComplete(widget.onComplete);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _ctrl,
|
||||
builder: (context, child) => Opacity(
|
||||
opacity: _opacity.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slide.value),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class MessageBubble extends StatelessWidget {
|
||||
final CachedMessage? prevMessage;
|
||||
final CachedMessage? nextMessage;
|
||||
final String chatType;
|
||||
final String? overrideStatus;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
@@ -52,7 +53,8 @@ class MessageBubble extends StatelessWidget {
|
||||
required this.myId,
|
||||
this.prevMessage,
|
||||
this.nextMessage,
|
||||
required this.chatType
|
||||
required this.chatType,
|
||||
this.overrideStatus,
|
||||
});
|
||||
|
||||
bool get isGroupedWithNext {
|
||||
@@ -315,7 +317,6 @@ class MessageBubble extends StatelessWidget {
|
||||
@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)),
|
||||
@@ -490,7 +491,7 @@ class MessageBubble extends StatelessWidget {
|
||||
children: [
|
||||
Flexible(
|
||||
child: isForwarded
|
||||
? _buildForwardedInlineText(context, forwarded, textColor)
|
||||
? _buildForwardedInlineText(context, forwarded!, textColor)
|
||||
: Text(
|
||||
message.text ?? '',
|
||||
style: TextStyle(color: textColor, fontSize: 16, height: 1.3),
|
||||
@@ -500,7 +501,9 @@ class MessageBubble extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
_formatTime(message.time),
|
||||
message.status == 'EDITED'
|
||||
? '${_formatTime(message.time)} ред.'
|
||||
: _formatTime(message.time),
|
||||
style: TextStyle(
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 10,
|
||||
@@ -904,13 +907,6 @@ class MessageBubble extends StatelessWidget {
|
||||
ForwardedMessageAttachment forwarded,
|
||||
List<MessageAttachment> attachments,
|
||||
) {
|
||||
debugPrint('DEBUG: _buildForwardedGenericContent called');
|
||||
debugPrint('DEBUG: attachments count: ${attachments.length}');
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
debugPrint('DEBUG: attachment[$i] type: ${attachments[i].runtimeType}');
|
||||
debugPrint('DEBUG: attachment[$i] type field: ${attachments[i].type}');
|
||||
}
|
||||
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final headerColor = isMe
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
@@ -1671,7 +1667,7 @@ class MessageBubble extends StatelessWidget {
|
||||
url: url,
|
||||
textColor: textColor,
|
||||
isMe: isMe,
|
||||
status: message.status,
|
||||
status: overrideStatus ?? message.status,
|
||||
time: message.time,
|
||||
cs: cs,
|
||||
waveData: waveData,
|
||||
@@ -1729,31 +1725,31 @@ class MessageBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(BuildContext context) {
|
||||
final status = message.status;
|
||||
final status = overrideStatus ?? message.status;
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
if (status == null || status == 'sending' || status == 'pending') {
|
||||
icon = Symbols.check;
|
||||
color = Colors.white70;
|
||||
} else {
|
||||
switch (status) {
|
||||
case 'sent':
|
||||
icon = Symbols.check;
|
||||
color = Colors.white70;
|
||||
case 'delivered':
|
||||
icon = Symbols.done_all;
|
||||
color = Colors.white70;
|
||||
case 'read':
|
||||
icon = Symbols.done_all;
|
||||
color = const Color(0xFF34C759);
|
||||
case 'error':
|
||||
icon = Symbols.error;
|
||||
color = Colors.redAccent;
|
||||
default:
|
||||
icon = Symbols.check;
|
||||
color = Colors.white70;
|
||||
}
|
||||
switch (status) {
|
||||
case 'sending':
|
||||
case 'pending':
|
||||
icon = Symbols.schedule;
|
||||
color = Colors.white70;
|
||||
case null:
|
||||
case 'sent':
|
||||
icon = Symbols.check;
|
||||
color = Colors.white70;
|
||||
case 'delivered':
|
||||
icon = Symbols.done_all;
|
||||
color = Colors.white70;
|
||||
case 'read':
|
||||
icon = Symbols.done_all;
|
||||
color = const Color(0xFF4FC3F7);
|
||||
case 'error':
|
||||
icon = Symbols.error;
|
||||
color = Colors.redAccent;
|
||||
default:
|
||||
icon = Symbols.check;
|
||||
color = Colors.white70;
|
||||
}
|
||||
|
||||
return Icon(icon, size: 14, color: color);
|
||||
@@ -1840,11 +1836,15 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
if (status == null || status == 'sending' || status == 'pending') {
|
||||
if (status == null || status == 'sent') {
|
||||
icon = Symbols.check;
|
||||
color = Colors.white54;
|
||||
} else {
|
||||
switch (status) {
|
||||
case 'sending':
|
||||
case 'pending':
|
||||
icon = Symbols.schedule;
|
||||
color = Colors.white54;
|
||||
case 'sent':
|
||||
icon = Symbols.check;
|
||||
color = Colors.white54;
|
||||
@@ -1853,7 +1853,7 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
color = Colors.white54;
|
||||
case 'read':
|
||||
icon = Symbols.done_all;
|
||||
color = const Color(0xFF34C759);
|
||||
color = const Color(0xFF4FC3F7);
|
||||
case 'error':
|
||||
icon = Symbols.error;
|
||||
color = Colors.redAccent;
|
||||
|
||||
Reference in New Issue
Block a user