Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mobile/lib/features/activity/activity_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';

import '../../shared/identity_names/identity_names_provider.dart';
import '../../shared/mentions/agent_identity_provider.dart';
import '../../shared/mentions/mention_tags.dart';
import '../../shared/relay/relay.dart';
Expand All @@ -22,6 +23,7 @@ import '../../shared/widgets/message_author_meta.dart';
import '../../shared/widgets/modal_presentation.dart';
import '../channels/channel.dart';
import '../channels/channel_detail_page.dart';
import '../channels/channel_identity_names_provider.dart';
import '../channels/channel_management_provider.dart';
import '../channels/channels_provider.dart';
import '../channels/dm_channel_labels.dart';
Expand Down
22 changes: 21 additions & 1 deletion mobile/lib/features/activity/activity_page/inbox_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,26 @@ class _InboxRow extends HookConsumerWidget {
final profile = profiles[senderPubkey];
// The shared label contract: blank cached names (empty or whitespace-only
// are relay-valid) fall back to the compact npub, never a blank sender.
final senderLabel = profile?.label ?? shortPubkey(item.item.pubkey);
// Rows compare names within their own channel, like the channel itself.
final channelId = channel?.id ?? item.item.channelId;
final Map<String, String> contextualLabels;
if (channelId == null) {
// No channel: the row's own identities are the comparison context.
final names = watchIdentityNames(ref, relevantPubkeys);
contextualLabels = {
for (final key in names.candidates) key: names.labelFor(key),
};
} else {
contextualLabels = watchChannelIdentityLabels(
ref,
channelId,
relevantPubkeys,
);
}
final senderLabel =
contextualLabels[senderPubkey] ??
profile?.label ??
shortPubkey(item.item.pubkey);
final profileMentionNames = {
for (final pubkey in mentionPubkeys)
if (profiles[pubkey]?.displayName?.trim().isNotEmpty == true)
Expand Down Expand Up @@ -325,6 +344,7 @@ class _InboxRow extends HookConsumerWidget {
MessageContent(
content: item.item.displayContent,
mentionNames: mentionNames,
mentionLabels: contextualLabels,
agentMentionPubkeys: agentMentionPubkeys,
tags: item.item.tags,
maxLines: 2,
Expand Down
11 changes: 8 additions & 3 deletions mobile/lib/features/activity/activity_page/lists.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class _RemindersList extends ConsumerWidget {

/// Drafts surface for the Drafts filter — locally saved unsent composer
/// text that reopens the target composer.
class _DraftsList extends StatelessWidget {
class _DraftsList extends ConsumerWidget {
final List<ComposeDraft> drafts;
final ScrollController scrollController;
final Map<String, Channel> channelById;
Expand All @@ -95,7 +95,8 @@ class _DraftsList extends StatelessWidget {
});

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final names = ref.watch(identityNameSourcesProvider);
if (drafts.isEmpty) {
return const _EmptySurface(
icon: LucideIcons.filePen,
Expand All @@ -114,7 +115,11 @@ class _DraftsList extends StatelessWidget {
final destination = channel == null
? 'Unavailable channel'
: channel.isDm
? resolveDmChannelDisplayLabel(channel, currentPubkey: myPubkey)
? resolveDmChannelDisplayLabel(
channel,
currentPubkey: myPubkey,
names: names,
)
: '#${channel.name}';
return ListTile(
key: ValueKey('draft-row-${draft.key}'),
Expand Down
33 changes: 29 additions & 4 deletions mobile/lib/features/channels/add_members_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import '../../shared/widgets/buzz_loading_indicator.dart';
import '../../shared/identity_names/identity_names_provider.dart';
import 'channel_management_provider.dart';

/// Searchable multi-select used to add people or agents to a channel.
Expand Down Expand Up @@ -56,6 +57,30 @@ class AddChannelMembersSheet extends HookConsumerWidget {
)
.toList() ??
const <DirectoryUser>[];
// Compare every shown choice with the channel's members, so a candidate
// who shares a member's name is told apart before being added. Members
// are only compared: their names come from the roster the details page
// already loaded, so only the shown choices' profiles are fetched.
final choices = [...availableUsers, ...selectedUsers.value];
final roster =
ref.watch(channelMembersProvider(channelId)).asData?.value ??
const <ChannelMember>[];
final names = watchIdentityNames(
ref,
[...normalizedExisting, for (final user in choices) user.pubkey],
agentPubkeys: {
for (final member in roster)
if (member.isBot) member.pubkey,
for (final user in choices)
if (user.isAgent) user.pubkey,
},
fallbackNames: {
for (final member in roster) member.pubkey: ?member.displayName,
for (final user in choices) user.pubkey: user.label,
},
shown: [for (final user in choices) user.pubkey],
);
String labelFor(DirectoryUser user) => names.labelFor(user.pubkey);

void toggleUser(DirectoryUser user) {
if (isSubmitting.value) return;
Expand Down Expand Up @@ -145,7 +170,7 @@ class AddChannelMembersSheet extends HookConsumerWidget {
key: ValueKey(
'add-channel-member-selected-${user.pubkey}',
),
label: Text(user.label),
label: Text(labelFor(user)),
onDeleted: isSubmitting.value
? null
: () => toggleUser(user),
Expand Down Expand Up @@ -181,8 +206,8 @@ class AddChannelMembersSheet extends HookConsumerWidget {
button: true,
selected: selected,
label: selected
? '${user.label}, selected'
: user.label,
? '${labelFor(user)}, selected'
: labelFor(user),
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: AvatarImage(
Expand All @@ -194,7 +219,7 @@ class AddChannelMembersSheet extends HookConsumerWidget {
isAgent: user.isAgent,
),
title: Text(
user.label,
labelFor(user),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Expand Down
1 change: 1 addition & 0 deletions mobile/lib/features/channels/channel_actions_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import '../../shared/widgets/modal_presentation.dart';
import '../../shared/widgets/sheet_divider.dart';
import 'channel.dart';
import 'add_members_sheet.dart';
import 'channel_identity_names_provider.dart';
import 'channel_management_provider.dart';
import 'channel_mutes/channel_mutes_provider.dart';
import 'channel_sections/channel_sections_provider.dart';
Expand Down
2 changes: 2 additions & 0 deletions mobile/lib/features/channels/channel_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../shared/animated_avatar.dart';
import '../../shared/emoji/emoji_burst.dart';
import '../../shared/huddle/huddle.dart';
import '../../shared/identity_names/identity_names.dart';
import '../../shared/identity_names/identity_names_provider.dart';
import '../../shared/mentions/agent_identity_provider.dart';
import '../../shared/relay/relay.dart';
import '../../shared/theme/theme.dart';
Expand Down
11 changes: 8 additions & 3 deletions mobile/lib/features/channels/channel_detail_page/app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,14 @@ class _DmAppBarTitle extends ConsumerWidget {
children: [
Flexible(
child: Text(
resolveDmChannelDisplayLabel(
channel,
currentPubkey: currentPubkey,
ref.watch(
identityNameSourcesProvider.select(
(names) => resolveDmChannelDisplayLabel(
channel,
currentPubkey: currentPubkey,
names: names,
),
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ class _HuddleCallAvatar extends HookConsumerWidget {
required this.speakerLevel,
required this.preparingResponse,
required this.onTap,
this.contextualLabel,
this.isSelf = false,
this.frameSize = _huddleAvatarFrameSize,
});

final String pubkey;
final UserProfile? profile;
final String? fallbackLabel;

/// The huddle-scoped identity label, when resolved.
final String? contextualLabel;
final bool active;
final double speakerLevel;
final bool preparingResponse;
Expand Down Expand Up @@ -122,6 +126,7 @@ class _HuddleCallAvatar extends HookConsumerWidget {
pubkey: pubkey,
profile: profile,
fallbackLabel: fallbackLabel,
contextualLabel: contextualLabel,
isSelf: isSelf,
);
final isAgent = profile?.isAgent == true || fallbackLabel != null;
Expand Down Expand Up @@ -265,8 +270,10 @@ String _huddleParticipantLabel({
required UserProfile? profile,
required String? fallbackLabel,
required bool isSelf,
String? contextualLabel,
}) {
if (isSelf) return 'You';
if (contextualLabel != null) return contextualLabel;
final profileName = profile?.displayName?.trim();
final directoryName = fallbackLabel?.trim();
return (profileName?.isNotEmpty == true ? profileName : null) ??
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class _HuddleCallParticipants extends StatelessWidget {
required this.error,
required this.profiles,
required this.fallbackLabels,
required this.contextualLabels,
required this.remotePubkeys,
required this.localPubkey,
required this.activeSpeakerPubkeys,
Expand All @@ -22,6 +23,9 @@ class _HuddleCallParticipants extends StatelessWidget {
final String? error;
final Map<String, UserProfile> profiles;
final Map<String, String> fallbackLabels;

/// Huddle-scoped identity labels, keyed by lowercase pubkey.
final Map<String, String> contextualLabels;
final List<String> remotePubkeys;
final String? localPubkey;
final Set<String> activeSpeakerPubkeys;
Expand Down Expand Up @@ -146,6 +150,7 @@ class _HuddleCallParticipants extends StatelessWidget {
pubkeys: remotePubkeys,
profiles: profiles,
fallbackLabels: fallbackLabels,
contextualLabels: contextualLabels,
activeSpeakerPubkeys: activeSpeakerPubkeys,
speakerLevels: speakerLevels,
workingAgentPubkeys: workingAgentPubkeys,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class _HuddleParticipantCluster extends HookWidget {
required this.pubkeys,
required this.profiles,
required this.fallbackLabels,
required this.contextualLabels,
required this.activeSpeakerPubkeys,
required this.speakerLevels,
required this.workingAgentPubkeys,
Expand All @@ -51,6 +52,9 @@ class _HuddleParticipantCluster extends HookWidget {
final List<String> pubkeys;
final Map<String, UserProfile> profiles;
final Map<String, String> fallbackLabels;

/// Huddle-scoped identity labels, keyed by lowercase pubkey.
final Map<String, String> contextualLabels;
final Set<String> activeSpeakerPubkeys;
final Map<String, double> speakerLevels;
final Set<String> workingAgentPubkeys;
Expand Down Expand Up @@ -139,6 +143,7 @@ class _HuddleParticipantCluster extends HookWidget {
pubkey: pubkey,
profile: profiles[pubkey],
fallbackLabel: fallbackLabels[pubkey],
contextualLabel: contextualLabels[pubkey],
active: activeSpeakerPubkeys.contains(pubkey),
speakerLevel: speakerLevels[pubkey] ?? 0,
preparingResponse: workingAgentPubkeys.contains(pubkey),
Expand Down Expand Up @@ -174,6 +179,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
required this.pubkey,
required this.profile,
required this.fallbackLabel,
required this.contextualLabel,
required this.active,
required this.speakerLevel,
required this.preparingResponse,
Expand All @@ -188,6 +194,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
final String pubkey;
final UserProfile? profile;
final String? fallbackLabel;
final String? contextualLabel;
final bool active;
final double speakerLevel;
final bool preparingResponse;
Expand Down Expand Up @@ -240,6 +247,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
pubkey: pubkey,
profile: profile,
fallbackLabel: fallbackLabel,
contextualLabel: contextualLabel,
active: active,
speakerLevel: speakerLevel,
preparingResponse: preparingResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ class _HuddleParticipantSpotlight extends ConsumerWidget {
pubkey: pubkey,
profile: profile,
fallbackLabel: fallbackLabel,
contextualLabel: ref.watch(
_huddleIdentityNamesProvider(
ephemeralChannelId,
).select((names) => names.labelFor(pubkey)),
),
isSelf: isSelf,
);
final isAgent = profile?.isAgent == true || fallbackLabel != null;
Expand Down Expand Up @@ -269,6 +274,7 @@ class _HuddleParticipantRoster extends ConsumerWidget {
}
final profiles = ref.watch(userCacheProvider);
final fallbackLabels = ref.watch(agentDirectoryDisplayNamesProvider);
final names = ref.watch(_huddleIdentityNamesProvider(ephemeralChannelId));
final activeSpeakerPubkeys = ref.watch(
huddleSessionProvider.select((session) => session.activeSpeakerPubkeys),
);
Expand Down Expand Up @@ -333,6 +339,7 @@ class _HuddleParticipantRoster extends ConsumerWidget {
pubkey: pubkey,
profile: profile,
fallbackLabel: fallbackLabels[pubkey],
contextualLabel: names.labelFor(pubkey),
isSelf: false,
);
return Semantics(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ final _huddleLogicalParticipantPubkeysProvider = Provider.autoDispose
);
});

/// Contextual identity labels for one Huddle: its logical participants are
/// the comparison context, and its bot members are agents.
final _huddleIdentityNamesProvider = Provider.autoDispose
.family<IdentityNames, String>((ref, channelId) {
final sources = ref.watch(identityNameSourcesProvider);
final members =
ref.watch(channelMembersProvider(channelId)).value ??
const <ChannelMember>[];
final names = sources.scope(
ref.watch(_huddleLogicalParticipantPubkeysProvider(channelId)),
agentPubkeys: {
for (final member in members)
if (member.isBot) member.pubkey,
},
fallbackNames: {
for (final member in members) member.pubkey: ?member.displayName,
},
);
loadIdentityNameOwners(ref, names);
return names;
});

class _HuddleParticipantProfileUpdates extends Notifier<int> {
_HuddleParticipantProfileUpdates(this.channelId);

Expand Down Expand Up @@ -573,6 +595,12 @@ class _MobileHuddleCallPage extends ConsumerWidget {
);
final profiles = ref.watch(userCacheProvider);
final directoryDisplayNames = ref.watch(agentDirectoryDisplayNamesProvider);
final huddleNames = ref.watch(
_huddleIdentityNamesProvider(invite.ephemeralChannelId),
);
final huddleLabels = {
for (final pubkey in remotePubkeys) pubkey: huddleNames.labelFor(pubkey),
};
final huddleTypingEntries = ref.watch(
channelTypingProvider(invite.ephemeralChannelId),
);
Expand Down Expand Up @@ -679,6 +707,7 @@ class _MobileHuddleCallPage extends ConsumerWidget {
: null,
profiles: profiles,
fallbackLabels: directoryDisplayNames,
contextualLabels: huddleLabels,
remotePubkeys: remotePubkeys,
localPubkey: localPubkey,
activeSpeakerPubkeys: session.activeSpeakerPubkeys,
Expand Down
Loading
Loading