From 2455aa78e73e656a0c9586702f8c23b349eeceee Mon Sep 17 00:00:00 2001 From: klockky Date: Wed, 24 Jun 2026 22:59:09 +0300 Subject: [PATCH] =?UTF-8?q?feat(chats):=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B8=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=B2=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B0?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/messages.dart | 61 ++++++++++ lib/frontend/screens/chats/chat_screen.dart | 36 ++++++ lib/frontend/widgets/message_bubble.dart | 120 ++++++++++++++------ 3 files changed, 180 insertions(+), 37 deletions(-) diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index c0d2d51..630c778 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -1380,4 +1380,65 @@ class MessagesModule { } return null; } + + Future ensureContactNames(Iterable ids) async { + final missing = ids + .where((id) => id != 0 && ContactCache.get(id) == null) + .toSet(); + if (missing.isEmpty) return false; + if (_api.state != SessionState.online) return false; + + try { + final response = await _api.sendRequest(Opcode.contactInfo, { + 'contactIds': missing.toList(), + }); + + if (!response.isOk) return false; + final data = response.payload; + if (data is! Map) return false; + + final contacts = data['contacts']; + if (contacts is! List) return false; + + var resolvedAny = false; + for (final raw in contacts.whereType()) { + final id = raw['id']; + if (id is! int) continue; + + final names = raw['names']; + if (names is List && names.isNotEmpty) { + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is Map) { + final firstName = (nameRaw['firstName'] as String?) ?? ''; + final lastName = nameRaw['lastName'] as String?; + final fullName = (lastName != null && lastName.isNotEmpty) + ? '$firstName $lastName' + : firstName; + if (fullName.isNotEmpty) ContactCache.put(id, fullName); + } + } + + final baseUrl = raw['baseUrl'] as String?; + if (baseUrl != null && baseUrl.isNotEmpty) { + ContactCache.putAvatar(id, baseUrl); + } + + final rawOpts = raw['options']; + if (rawOpts is List) { + ContactCache.putOptions(id, rawOpts.whereType().toSet()); + } + + ChatsModule.applyContactUpdate(id); + resolvedAny = true; + } + + return resolvedAny; + } catch (e) { + logger.e('ensureContactNames error: $e'); + return false; + } + } } diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index b05d34b..5d91d17 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -16,6 +16,7 @@ import 'package:komet/core/media/gallery_source.dart'; import 'package:komet/core/utils/format.dart'; import 'package:komet/core/utils/logger.dart'; import 'package:komet/frontend/screens/chats/chat_info_screen.dart'; +import 'package:komet/frontend/screens/contacts/contact_profile_screen.dart'; import 'package:komet/frontend/screens/chats/forward_picker_screen.dart'; import 'package:komet/frontend/screens/chats/poll_create_screen.dart'; import 'package:komet/frontend/widgets/custom_notification.dart'; @@ -456,6 +457,7 @@ class _ChatScreenState extends State }); } _loadForwardedSenderNames(); + _loadGroupSenderNames(); return; } @@ -492,6 +494,7 @@ class _ChatScreenState extends State ChatsModule.reconcileLastMessageIfPlaceholder(_myId, widget.chatId), ); _loadForwardedSenderNames(); + _loadGroupSenderNames(); } catch (e) { logger.e('Error fetching history: $e'); if (mounted) { @@ -551,6 +554,8 @@ class _ChatScreenState extends State if (added == 0) _hasMoreHistory = false; }); _persistSessionCache(); + _loadForwardedSenderNames(); + _loadGroupSenderNames(); } catch (e) { logger.e('Error loading more history: $e'); if (mounted) setState(() => _isLoadingMore = false); @@ -2681,6 +2686,22 @@ class _ChatScreenState extends State } catch (_) {} } + Future _loadGroupSenderNames() async { + if (widget.chatType != 'CHAT' && widget.chatType != 'CHANNEL') return; + + final unknownIds = {}; + for (final msg in _messages) { + if (msg.isControl) continue; + final id = msg.senderId; + if (id == 0 || id == _myId) continue; + if (ContactCache.get(id) == null) unknownIds.add(id); + } + if (unknownIds.isEmpty) return; + + final resolved = await messagesModule.ensureContactNames(unknownIds); + if (resolved && mounted) _bumpMessages(); + } + Future _loadForwardedSenderNames() async { final forwardIds = {}; for (final msg in _messages) { @@ -2775,6 +2796,20 @@ class _ChatScreenState extends State _replyTo.value = null; } + void _openSenderProfile(int senderId) { + if (senderId == 0 || senderId == _myId) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ContactProfileScreen( + contactId: senderId, + initialName: ContactCache.get(senderId), + initialAvatarUrl: ContactCache.getAvatar(senderId), + ), + ), + ); + } + void _jumpToMessage(String messageId) { final index = _messages.indexWhere((m) => m.id == messageId); if (index == -1) { @@ -3121,6 +3156,7 @@ class _ChatScreenState extends State reactionsListenable: _reactionNotifierFor(message), uploadProgress: _photoProgressFor(message), onReplyTap: _jumpToMessage, + onAvatarTap: _openSenderProfile, ); final pressable = _SelectableMessageRow( diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 647f36d..d87e282 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -103,6 +103,7 @@ class MessageBubble extends StatelessWidget { final ValueListenable?>? reactionsListenable; final ValueListenable>? uploadProgress; final void Function(String messageId)? onReplyTap; + final void Function(int senderId)? onAvatarTap; const MessageBubble({ super.key, @@ -116,6 +117,7 @@ class MessageBubble extends StatelessWidget { this.reactionsListenable, this.uploadProgress, this.onReplyTap, + this.onAvatarTap, }); bool _computeHasPhotoWithCaption() { @@ -296,11 +298,53 @@ class MessageBubble extends StatelessWidget { ); } + static const List _senderPalette = [ + Color(0xFFE57373), + Color(0xFF64B5F6), + Color(0xFF81C784), + Color(0xFFFFB74D), + Color(0xFFBA68C8), + Color(0xFF4DD0E1), + Color(0xFFF06292), + Color(0xFFA1887F), + ]; + + Color _senderColor(int id) => _senderPalette[id.abs() % _senderPalette.length]; + + Widget _buildSenderHeader(ColorScheme cs, bool needsInset) { + final name = ContactCache.get(message.senderId); + if (name == null || name.isEmpty) return const SizedBox.shrink(); + final header = Padding( + padding: needsInset + ? const EdgeInsets.fromLTRB(12, 6, 12, 2) + : const EdgeInsets.only(bottom: 2), + child: Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _senderColor(message.senderId), + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ); + + final cb = onAvatarTap; + if (cb == null) return header; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => cb(message.senderId), + child: header, + ); + } + Widget _buildLeadingAvatar(ColorScheme cs) { final senderAvatar = ContactCache.getAvatar(message.senderId); final displaySender = ContactCache.get(message.senderId); + final Widget avatar; if (senderAvatar != null && senderAvatar.isNotEmpty) { - return CircleAvatar( + avatar = CircleAvatar( radius: 15, backgroundImage: CachedNetworkImageProvider( senderAvatar, @@ -309,16 +353,25 @@ class MessageBubble extends StatelessWidget { ), backgroundColor: cs.primaryContainer, ); + } else { + avatar = CircleAvatar( + radius: 15, + backgroundColor: cs.primaryContainer, + child: Text( + displaySender != null && displaySender.isNotEmpty + ? displaySender[0].toUpperCase() + : '?', + style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), + ), + ); } - return CircleAvatar( - radius: 15, - backgroundColor: cs.primaryContainer, - child: Text( - displaySender != null && displaySender.isNotEmpty - ? displaySender[0].toUpperCase() - : '?', - style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), - ), + + final cb = onAvatarTap; + if (cb == null) return avatar; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => cb(message.senderId), + child: avatar, ); } @@ -362,6 +415,10 @@ class MessageBubble extends StatelessWidget { showAvatarSlot && chatType == "CHAT" && nextMessage?.senderId != message.senderId; + final showSenderName = + showAvatarSlot && + chatType == "CHAT" && + prevMessage?.senderId != message.senderId; final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75; final bubbleColor = isMe ? cs.primaryContainer : cs.surfaceContainerHighest; @@ -461,14 +518,22 @@ class MessageBubble extends StatelessWidget { padding: padding, child: child, ), - child: withReply( - reactionsInside - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [bubbleContent, _reactionsBar(cs)], - ) - : bubbleContent, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showSenderName) + _buildSenderHeader(cs, padding == EdgeInsets.zero), + withReply( + reactionsInside + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [bubbleContent, _reactionsBar(cs)], + ) + : bubbleContent, + ), + ], ), ), if (reactionsUnder) _reactionsBar(cs), @@ -652,8 +717,6 @@ class MessageBubble extends StatelessWidget { final forwarded = _getForwardedAttachment(); final isForwarded = forwarded != null && !isForwardedContact; - final displaySender = ContactCache.get(message.senderId); - final reactionChips = _buildReactionChipsFor(ctx.cs, ctx.reactionInfo); final hasReactions = reactionChips.isNotEmpty; @@ -669,22 +732,11 @@ class MessageBubble extends StatelessWidget { style: TextStyle(color: ctx.dim, fontSize: 10), ); - final showSender = - message.senderId != message.accountId && - prevMessage?.senderId != message.senderId && - chatType == "CHAT"; - if (hasReactions) { return IntrinsicWidth( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showSender) - Text( - displaySender ?? "", - textAlign: TextAlign.left, - style: TextStyle(color: ctx.text), - ), textWidget, const SizedBox(height: 6), Row( @@ -717,12 +769,6 @@ class MessageBubble extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showSender) - Text( - displaySender ?? "", - textAlign: TextAlign.left, - style: TextStyle(color: ctx.text), - ), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end,