mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Home's DM sort resolves participant identity names again for every comparator call. This work appeared in a captured 82.163 ms UI frame after foregrounding the full app (Debug/JIT iOS simulator, shared host). Resolve each lowercase display label once per sort, retaining the same label ordering and channel-ID tie-breaker. Sort keys are invocation-local, so later viewer, profile, owner, participant, and community changes cannot reuse stale labels. No matching open mobile sorting PR or issue found. ### Validation - Added synthetic regression coverage for bounded contextual resolution and ordering parity across identity/name/channel changes, ties, unnamed DMs, and empty/singleton lists. The 80-DM fixture performs 80 contextual resolutions instead of the original comparator’s 982; restoring that comparator makes the regression fail. - 133 focused tests passed, including existing Home scrolling/reconnect widget tests and identity-name tests. - Mobile static analysis passed; 3,697 tests passed with 16 existing skips, plus four no-gateway configuration tests. - No new full-app or hardware capture. The existing trace establishes repeated synchronous work, not its release-build magnitude or the cause of every scrolling stall. Signed-off-by: Tom Brow <tomb@block.xyz>
157 lines
5.7 KiB
Dart
157 lines
5.7 KiB
Dart
import '../../shared/identity_names/identity_names.dart';
|
|
import '../../shared/utils/string_utils.dart';
|
|
import 'channel.dart';
|
|
|
|
const int _dmParticipantPreviewLimit = 3;
|
|
|
|
bool isGenericDmChannelName(String name) {
|
|
final normalized = name.trim().toLowerCase();
|
|
if (normalized.isEmpty ||
|
|
normalized == 'dm' ||
|
|
normalized == 'direct message' ||
|
|
normalized == 'direct messages') {
|
|
return true;
|
|
}
|
|
return RegExp(r'^group dm\s*(\(\d+\))?$').hasMatch(normalized);
|
|
}
|
|
|
|
String formatDmParticipantDisplayName(List<String> displayNames) {
|
|
final visible = displayNames.take(_dmParticipantPreviewLimit).toList();
|
|
final hiddenCount = displayNames.length - visible.length;
|
|
return hiddenCount > 0
|
|
? [...visible, '+$hiddenCount more'].join(', ')
|
|
: visible.join(', ');
|
|
}
|
|
|
|
/// Contextual participant labels for a DM: its participants are the whole
|
|
/// comparison context, and relay participant names are fallbacks.
|
|
IdentityNames dmParticipantNames(Channel channel, IdentityNameSources names) =>
|
|
names.scope(
|
|
channel.participantPubkeys,
|
|
fallbackNames: {
|
|
for (var index = 0; index < channel.participantPubkeys.length; index++)
|
|
if (index < channel.participants.length)
|
|
channel.participantPubkeys[index]: channel.participants[index],
|
|
},
|
|
);
|
|
|
|
/// The DM's display label. With [names], participant labels follow the
|
|
/// contextual identity-name policy, so namesakes stay distinguishable.
|
|
String resolveDmChannelDisplayLabel(
|
|
Channel channel, {
|
|
String? currentPubkey,
|
|
IdentityNameSources? names,
|
|
}) {
|
|
if (!channel.isDm || !isGenericDmChannelName(channel.name)) {
|
|
return channel.name;
|
|
}
|
|
|
|
final normalizedCurrent = currentPubkey?.toLowerCase();
|
|
final participantNames = names == null
|
|
? null
|
|
: dmParticipantNames(channel, names);
|
|
final participants = <({String label, String? pubkey})>[
|
|
for (var index = 0; index < channel.participantPubkeys.length; index++)
|
|
(
|
|
label:
|
|
participantNames?.labelFor(channel.participantPubkeys[index]) ??
|
|
(index < channel.participants.length
|
|
? channel.participants[index]
|
|
: shortPubkey(channel.participantPubkeys[index])),
|
|
pubkey: channel.participantPubkeys[index].toLowerCase(),
|
|
),
|
|
];
|
|
|
|
final displayParticipants = normalizedCurrent == null
|
|
? participants
|
|
: participants
|
|
.where((participant) => participant.pubkey != normalizedCurrent)
|
|
.toList();
|
|
final labels = <String>{
|
|
for (final participant
|
|
in displayParticipants.isNotEmpty ? displayParticipants : participants)
|
|
participant.label,
|
|
}.toList();
|
|
|
|
return labels.isNotEmpty
|
|
? formatDmParticipantDisplayName(labels)
|
|
: channel.name;
|
|
}
|
|
|
|
/// Avatar initial for the DM's visible counterpart.
|
|
///
|
|
/// Mirrors [resolveDmChannelDisplayLabel]'s participant selection: the
|
|
/// first participant that is not [currentPubkey], since member order does
|
|
/// not guarantee the counterpart is listed first — otherwise the avatar
|
|
/// could identify the current user while the label beside it identifies
|
|
/// the counterpart. When every participant is the current user (a
|
|
/// self-DM), the label names the current user too, so the avatar keys off
|
|
/// that same first participant instead of its label's first character.
|
|
///
|
|
/// A resolved display name keeps its name-derived initial. A label that
|
|
/// fell back to the compact npub form of the participant's key is keyed to
|
|
/// the hex public key instead — the npub form starts with `npub1`, so every
|
|
/// unnamed participant would otherwise render `N`.
|
|
String dmAvatarInitial(Channel channel, {String? currentPubkey}) {
|
|
final normalizedCurrent = currentPubkey?.toLowerCase();
|
|
var index = 0;
|
|
while (normalizedCurrent != null &&
|
|
index < channel.participantPubkeys.length &&
|
|
channel.participantPubkeys[index].toLowerCase() == normalizedCurrent) {
|
|
index++;
|
|
}
|
|
|
|
if (index >= channel.participantPubkeys.length) {
|
|
// No participant pubkeys (labels only): fall back to the first
|
|
// participant label, like the channel label does when the non-self
|
|
// list is empty.
|
|
if (channel.participantPubkeys.isEmpty) {
|
|
final label = channel.participants.isNotEmpty
|
|
? channel.participants.first
|
|
: '';
|
|
return label.isNotEmpty ? label[0].toUpperCase() : '?';
|
|
}
|
|
// Keys exist but every participant is the current user (a self-DM):
|
|
// the label names the current user, so key the avatar to that same
|
|
// first participant with the shared provenance rule below — the hex
|
|
// key for a compact npub label, the authored name otherwise.
|
|
index = 0;
|
|
}
|
|
|
|
final pubkey = channel.participantPubkeys[index];
|
|
final label = index < channel.participants.length
|
|
? channel.participants[index]
|
|
: shortPubkey(pubkey);
|
|
if (pubkey.isNotEmpty && label == shortPubkey(pubkey)) {
|
|
return pubkey[0].toUpperCase();
|
|
}
|
|
return label.isNotEmpty ? label[0].toUpperCase() : '?';
|
|
}
|
|
|
|
/// Sorts by the visible label, resolving each label only once per invocation.
|
|
List<Channel> sortDmChannelsByDisplayLabel(
|
|
Iterable<Channel> channels, {
|
|
String? currentPubkey,
|
|
IdentityNameSources? names,
|
|
}) {
|
|
// Keep sort keys local: the next call must reflect current viewer, channel
|
|
// participants, and community profile/owner names.
|
|
final sorted = [
|
|
for (final channel in channels)
|
|
(
|
|
channel: channel,
|
|
label: resolveDmChannelDisplayLabel(
|
|
channel,
|
|
currentPubkey: currentPubkey,
|
|
names: names,
|
|
).toLowerCase(),
|
|
),
|
|
];
|
|
sorted.sort((left, right) {
|
|
final labelCompare = left.label.compareTo(right.label);
|
|
if (labelCompare != 0) return labelCompare;
|
|
return left.channel.id.compareTo(right.channel.id);
|
|
});
|
|
return [for (final entry in sorted) entry.channel];
|
|
}
|