feat(chats): имена и аватарки отправителей в группах

This commit is contained in:
klockky
2026-06-24 22:59:09 +03:00
parent d16c908fe0
commit 2455aa78e7
3 changed files with 180 additions and 37 deletions
+61
View File
@@ -1380,4 +1380,65 @@ class MessagesModule {
}
return null;
}
Future<bool> ensureContactNames(Iterable<int> 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<Map>()) {
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<String>().toSet());
}
ChatsModule.applyContactUpdate(id);
resolvedAny = true;
}
return resolvedAny;
} catch (e) {
logger.e('ensureContactNames error: $e');
return false;
}
}
}
@@ -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<ChatScreen>
});
}
_loadForwardedSenderNames();
_loadGroupSenderNames();
return;
}
@@ -492,6 +494,7 @@ class _ChatScreenState extends State<ChatScreen>
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<ChatScreen>
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<ChatScreen>
} catch (_) {}
}
Future<void> _loadGroupSenderNames() async {
if (widget.chatType != 'CHAT' && widget.chatType != 'CHANNEL') return;
final unknownIds = <int>{};
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<void> _loadForwardedSenderNames() async {
final forwardIds = <int>{};
for (final msg in _messages) {
@@ -2775,6 +2796,20 @@ class _ChatScreenState extends State<ChatScreen>
_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<ChatScreen>
reactionsListenable: _reactionNotifierFor(message),
uploadProgress: _photoProgressFor(message),
onReplyTap: _jumpToMessage,
onAvatarTap: _openSenderProfile,
);
final pressable = _SelectableMessageRow(
+83 -37
View File
@@ -103,6 +103,7 @@ class MessageBubble extends StatelessWidget {
final ValueListenable<Map<String, dynamic>?>? reactionsListenable;
final ValueListenable<List<double>>? 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<Color> _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,