mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(mobile): username display, huddle filtering, and channel list stability (#489)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ class ChannelsNotifier extends AsyncNotifier<List<Channel>> {
|
||||
|
||||
Future<List<Channel>> _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<List<Channel>> {
|
||||
|
||||
final channels = <Channel>[];
|
||||
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<List<Channel>> {
|
||||
participants: participants,
|
||||
participantPubkeys: data.participantPubkeys,
|
||||
isMember: isMember,
|
||||
ttlSeconds: data.ttlSeconds,
|
||||
ttlDeadline: data.ttlDeadline,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ?? <ChannelMember>[];
|
||||
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<ChannelMember> _filterMembers(
|
||||
List<ChannelMember> members,
|
||||
String? query,
|
||||
String? currentPubkey,
|
||||
Map<String, UserProfile> userCache,
|
||||
) {
|
||||
if (query == null) return const [];
|
||||
final q = query.toLowerCase();
|
||||
@@ -627,7 +641,9 @@ List<ChannelMember> _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,
|
||||
|
||||
@@ -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<void>(
|
||||
context: context,
|
||||
|
||||
@@ -246,6 +246,8 @@ class ChannelData {
|
||||
final String description;
|
||||
final String? topic;
|
||||
final List<String> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user