mirror of
https://github.com/block/buzz.git
synced 2026-11-03 16:01:35 +00:00
## Summary Every identity-name source change (a profile batch, a verified name) rebuilds a channel's naming scope over its whole member list. In a 4,000-member channel, each rebuild derived every key's naming fact more than once: owner loading and resolution each derived it, and so did each label for a key outside the scope. It also checked every key against a regular expression several times, and iOS AOT builds run regular expressions in an interpreter. - `IdentityNames` derives each key's naming fact once per scope. - `isHexPubkey`: a character-loop hex check, used by the identity-name code instead of the regular expressions. Labels are unchanged. ## Measurements These were taken with an earlier version of this PR that also cached npub encoding. That cache was removed to keep the change small, so the remaining gain is smaller than shown. I haven't re-measured it. - **Synthetic** (Mac, `flutter test`, 4,391 members, 60% with profiles; scope, `missingOwnerProfiles` and every label for each new sources object): a warm rebuild took 63 ms before and 12 ms with the cache. The first rebuild took 120 ms before and 91 ms with the cache. - **Device** (iPhone 12 mini, Profile build, first 5 s after opening #announcements with 4,391 members; runs not matched): `channelIdentityNamesProvider` plus `watchChannelIdentityLabel` inclusive CPU was 148 ms before and 81 ms with the cache. Of that, `fullNpub` (npub encoding, the cached part) went from 62 ms to 22 ms. The identity path no longer calls `RegExp.hasMatch`. ## Testing - A new test counts profile lookups. Owner loading, every label and an outside key read each member's profile once. Without the memo, each is read 3 times. - New `isHexPubkey` tests cover both cases, wrong lengths, characters just outside each range, and a bad character at every position. - `just mobile-check mobile-test` passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.6 KiB
Dart
70 lines
2.6 KiB
Dart
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
/// Neutral label rendered when an identity string cannot be read as a Nostr
|
|
/// public key — malformed identities never leak truncated raw hex into the UI.
|
|
const String unknownIdentityLabel = 'Unknown identity';
|
|
|
|
/// Whether [value] is a 64-character hex public key (any case).
|
|
///
|
|
/// A character loop rather than a regular expression: AOT builds on iOS run
|
|
/// regular expressions in an interpreter, and identity views check every key
|
|
/// of a large roster on each rebuild.
|
|
bool isHexPubkey(String value) {
|
|
if (value.length != 64) return false;
|
|
for (var i = 0; i < 64; i++) {
|
|
final c = value.codeUnitAt(i);
|
|
if (!((c >= 0x30 && c <= 0x39) || // 0-9
|
|
(c >= 0x61 && c <= 0x66) || // a-f
|
|
(c >= 0x41 && c <= 0x46))) {
|
|
// A-F
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// The canonical full NIP-19 npub for [identity], or null when [identity] is
|
|
/// not a valid public key.
|
|
///
|
|
/// Accepts both canonical public-key forms — a 64-character hex key (any
|
|
/// case) or an `npub1…` string — and returns the full canonical npub.
|
|
/// Already-npub input round-trips through the codec so the result is always
|
|
/// the canonical lowercase encoding. Internal storage stays hex; the npub is
|
|
/// the user-facing display/copy form. Invalid input returns null (never a
|
|
/// passthrough of the raw string) so callers can suppress copy actions.
|
|
String? fullNpub(String identity) {
|
|
final trimmed = identity.trim();
|
|
try {
|
|
if (isHexPubkey(trimmed)) {
|
|
return _encodeNpub(trimmed.toLowerCase());
|
|
}
|
|
final decoded = nostr.Nip19.decode(payload: trimmed);
|
|
if (decoded.prefix != nostr.Nip19Prefix.npub ||
|
|
!isHexPubkey(decoded.data)) {
|
|
return null;
|
|
}
|
|
return _encodeNpub(decoded.data.toLowerCase());
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
String _encodeNpub(String hexPubkey) =>
|
|
nostr.Nip19.encode(prefix: nostr.Nip19Prefix.npub, data: hexPubkey);
|
|
|
|
/// The canonical compact public-key label: the first 8 and last 4 characters
|
|
/// of the full npub joined by an ellipsis — the same truncation desktop uses
|
|
/// for identity surfaces.
|
|
///
|
|
/// A compact label is a recognition aid, never a verification path; copy
|
|
/// actions expose the full npub via [fullNpub]. Accepts hex or npub input
|
|
/// (already-npub input yields the same label as its underlying key).
|
|
/// Returns [unknownIdentityLabel] when [identity] is not a valid public key.
|
|
String shortPubkey(String identity) {
|
|
final npub = fullNpub(identity);
|
|
if (npub == null) return unknownIdentityLabel;
|
|
return npub.length <= 12
|
|
? npub
|
|
: '${npub.substring(0, 8)}…${npub.substring(npub.length - 4)}';
|
|
}
|