diff --git a/mobile/lib/features/channels/channels_provider.dart b/mobile/lib/features/channels/channels_provider.dart index c91be499e..e6c892f16 100644 --- a/mobile/lib/features/channels/channels_provider.dart +++ b/mobile/lib/features/channels/channels_provider.dart @@ -56,7 +56,7 @@ class ChannelsNotifier extends AsyncNotifier> { Future> _fetch({bool subscribeLive = false}) async { final myPk = ref.read(myPubkeyProvider); - if (myPk == null) return const []; + if (myPk == null) throw StateError('No signing identity available'); final session = ref.read(relaySessionProvider.notifier); @@ -126,9 +126,14 @@ class ChannelsNotifier extends AsyncNotifier> { final channels = []; for (final event in dedupedMetas) { - channels.add( - _channelFromMeta(event, isMember: true, displayNames: displayNames), + final channel = _channelFromMeta( + event, + isMember: true, + displayNames: displayNames, ); + if (!channel.isEphemeral) { + channels.add(channel); + } } channels.sort((left, right) { @@ -180,6 +185,8 @@ class ChannelsNotifier extends AsyncNotifier> { participants: participants, participantPubkeys: data.participantPubkeys, isMember: isMember, + ttlSeconds: data.ttlSeconds, + ttlDeadline: data.ttlDeadline, ); } diff --git a/mobile/lib/features/channels/compose_bar.dart b/mobile/lib/features/channels/compose_bar.dart index ca68a1d9f..3b5d6c1d8 100644 --- a/mobile/lib/features/channels/compose_bar.dart +++ b/mobile/lib/features/channels/compose_bar.dart @@ -76,6 +76,17 @@ class ComposeBar extends HookConsumerWidget { final currentPubkey = ref.watch(currentPubkeyProvider); final userCache = ref.watch(userCacheProvider); + // Preload profiles for channel members so @mention suggestions show names. + useEffect(() { + final memberList = membersAsync.asData?.value ?? []; + if (memberList.isNotEmpty) { + ref + .read(userCacheProvider.notifier) + .preload(memberList.map((m) => m.pubkey).toList()); + } + return null; + }, [membersAsync.asData?.value.length]); + // Typing indicator broadcast — throttled to one event per 3 seconds. final lastTypingSentMs = useRef(0); @@ -149,6 +160,7 @@ class ComposeBar extends HookConsumerWidget { members, mentionQuery.value, currentPubkey, + userCache, ); // Filter channels against the query. @@ -157,9 +169,10 @@ class ComposeBar extends HookConsumerWidget { // Insert a selected mention into the text field. void insertMention(ChannelMember member) { - final name = member.displayName?.trim().isNotEmpty == true - ? member.displayName!.trim() - : member.pubkey.substring(0, 8); + final cached = ref.read(userCacheProvider)[member.pubkey.toLowerCase()]; + final name = cached?.displayName?.trim().isNotEmpty == true + ? cached!.displayName!.trim() + : '${member.pubkey.substring(0, 8)}\u2026'; // Track the resolved pubkey so we can pass it at send time. mentionMap.value[name] = member.pubkey; @@ -616,6 +629,7 @@ List _filterMembers( List members, String? query, String? currentPubkey, + Map userCache, ) { if (query == null) return const []; final q = query.toLowerCase(); @@ -627,7 +641,9 @@ List _filterMembers( ) .where((m) { if (q.isEmpty) return true; - final name = (m.displayName ?? '').toLowerCase(); + final profile = userCache[m.pubkey.toLowerCase()]; + final name = (profile?.displayName ?? m.displayName ?? '') + .toLowerCase(); final firstName = name.split(RegExp(r'\s+')).first; return name.startsWith(q) || firstName.startsWith(q) || @@ -675,11 +691,16 @@ class _MentionSuggestions extends StatelessWidget { separatorBuilder: (_, _) => const SizedBox.shrink(), itemBuilder: (context, index) { final member = suggestions[index]; - final name = member.labelFor(currentPubkey); final profile = userCache[member.pubkey.toLowerCase()]; + final name = profile?.displayName?.trim().isNotEmpty == true + ? profile!.displayName!.trim() + : member.labelFor(currentPubkey); final avatarUrl = profile?.avatarUrl; - final initial = (member.displayName ?? member.pubkey)[0] - .toUpperCase(); + final initial = + (profile?.displayName?.trim().isNotEmpty == true + ? profile!.displayName!.trim() + : member.pubkey)[0] + .toUpperCase(); return ListTile( dense: true, diff --git a/mobile/lib/features/channels/members_sheet.dart b/mobile/lib/features/channels/members_sheet.dart index 43552cc89..9899a32f7 100644 --- a/mobile/lib/features/channels/members_sheet.dart +++ b/mobile/lib/features/channels/members_sheet.dart @@ -222,7 +222,11 @@ class _MemberTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final label = member.labelFor(currentPubkey); + final label = isSelf + ? 'You' + : (profile?.displayName?.trim().isNotEmpty == true + ? profile!.displayName!.trim() + : member.labelFor(currentPubkey)); final initial = label.substring(0, 1).toUpperCase(); final showManagementActions = canManage && !isSelf && !member.isOwner; final showMenu = showManagementActions || onViewActivity != null; @@ -288,7 +292,11 @@ class _MemberTile extends ConsumerWidget { WidgetRef ref, { required bool showManagementActions, }) { - final label = member.labelFor(currentPubkey); + final label = isSelf + ? 'You' + : (profile?.displayName?.trim().isNotEmpty == true + ? profile!.displayName!.trim() + : member.labelFor(currentPubkey)); final canChangeRole = showManagementActions && !member.isBot; showModalBottomSheet( context: context, diff --git a/mobile/lib/shared/relay/nostr_models.dart b/mobile/lib/shared/relay/nostr_models.dart index b13b60d7f..f0a78906d 100644 --- a/mobile/lib/shared/relay/nostr_models.dart +++ b/mobile/lib/shared/relay/nostr_models.dart @@ -246,6 +246,8 @@ class ChannelData { final String description; final String? topic; final List participantPubkeys; + final int? ttlSeconds; + final DateTime? ttlDeadline; const ChannelData({ required this.id, @@ -255,6 +257,8 @@ class ChannelData { required this.description, this.topic, this.participantPubkeys = const [], + this.ttlSeconds, + this.ttlDeadline, }); factory ChannelData.fromEvent(NostrEvent event) { @@ -280,6 +284,12 @@ class ChannelData { for (final t in event.tags) if (t.length >= 2 && t[0] == 'p') t[1], ]; + final ttlRaw = event.getTagValue('ttl'); + final ttlSeconds = ttlRaw != null ? int.tryParse(ttlRaw) : null; + final ttlDeadlineRaw = event.getTagValue('ttl_deadline'); + final ttlDeadline = ttlDeadlineRaw != null + ? DateTime.tryParse(ttlDeadlineRaw) + : null; return ChannelData( id: id, name: name, @@ -288,6 +298,8 @@ class ChannelData { description: description, topic: topic, participantPubkeys: participants, + ttlSeconds: ttlSeconds, + ttlDeadline: ttlDeadline, ); } }