diff --git a/mobile/lib/shared/utils/string_utils.dart b/mobile/lib/shared/utils/string_utils.dart index df9fa2c238..177a4ec6aa 100644 --- a/mobile/lib/shared/utils/string_utils.dart +++ b/mobile/lib/shared/utils/string_utils.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:nostr/nostr.dart' as nostr; /// Neutral label rendered when an identity string cannot be read as a Nostr @@ -49,8 +50,24 @@ String? fullNpub(String identity) { } } -String _encodeNpub(String hexPubkey) => - nostr.Nip19.encode(prefix: nostr.Nip19Prefix.npub, data: hexPubkey); +/// Encoded npubs by lowercase hex key. Identity views label every roster +/// member without a profile name by npub on each rebuild, and bech32 encoding +/// dominated those rebuilds on large channels. Bounded: once full it keeps +/// what it has, so a roster larger than the limit still reuses most keys. +final _npubs = {}; +@visibleForTesting +const npubCacheLimit = 8192; + +String _encodeNpub(String hexPubkey) { + final cached = _npubs[hexPubkey]; + if (cached != null) return cached; + final npub = nostr.Nip19.encode( + prefix: nostr.Nip19Prefix.npub, + data: hexPubkey, + ); + if (_npubs.length < npubCacheLimit) _npubs[hexPubkey] = npub; + return npub; +} /// 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 diff --git a/mobile/test/shared/utils/string_utils_test.dart b/mobile/test/shared/utils/string_utils_test.dart index d11754d52d..fc5e7f4430 100644 --- a/mobile/test/shared/utils/string_utils_test.dart +++ b/mobile/test/shared/utils/string_utils_test.dart @@ -16,6 +16,23 @@ void main() { const canonicalCompact = 'npub180c\u2026h6w6'; group('fullNpub', () { + test('reuses the encoding of a key it has already encoded', () { + // Identity views relabel a large roster on every rebuild; each key's + // bech32 encoding must happen once, not once per rebuild. + final first = fullNpub(canonicalHex); + expect(first, canonicalNpub); + expect(identical(fullNpub(canonicalHex.toUpperCase()), first), isTrue); + }); + + test('a roster larger than the cache still reuses its encodings', () { + String key(int i) => i.toRadixString(16).padLeft(64, '0'); + final firstPass = [ + for (var i = 0; i <= npubCacheLimit; i++) fullNpub(key(i)), + ]; + // A second scan in the same order finds the early keys still cached. + expect(identical(fullNpub(key(1)), firstPass[1]), isTrue); + }); + test('canonicalizes every accepted input wrapper', () { // Every accepted wrapper — lowercase hex, uppercase hex, whitespace- // padded, and an already-npub key — converges on the canonical npub.