Fix missing profile names and pictures due to stream timing

When a view subscribes to profile updates via streamProfile() or
streamProfiles(), the stream now immediately yields any existing
profile data from NostrDB before waiting for network updates.

Previously, subscribers had to wait up to ~1 second for the
subscriptionSwitcherTask to restart the profile listener before
receiving any data. During this window, views would display
abbreviated pubkeys (e.g., "npub1abc...") or robohash placeholders
instead of the cached profile name and picture.

The fix adds a simple NDB lookup when creating the stream. This has
negligible performance impact since:
- It's a one-time operation per subscription (not per update)
- The same lookup was already happening in view bodies anyway
- NDB lookups are fast local queries

A new `yieldCached` parameter (default: true) allows callers to opt
out of the initial cached emission. NoteContentView uses this to
avoid redundant artifact re-renders — it only needs network updates
since its initial render already uses cached profile data.

Furthermore, when a profile has no metadata, the display name now shows
"npub1yrse...q9ye" instead of "1yrsedhw:8q0pq9ye" for a better UX.

Closes: https://github.com/damus-io/damus/issues/3454
Closes: https://github.com/damus-io/damus/issues/3455
Changelog-Changed: Changed abbreviated pubkey format to npub1...xyz for better readability
Changelog-Fixed: Fixed instances where a profile would not display profile name and picture for a few seconds
Signed-off-by: alltheseas <64376233+alltheseas@users.noreply.github.com>
Co-authored-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
alltheseas
2025-12-19 19:21:49 -06:00
committed by GitHub
parent 5058fb33d7
commit a0cecdc8ad
5 changed files with 310 additions and 11 deletions

View File

@@ -116,31 +116,72 @@ extension NostrNetworkManager {
// MARK: - Streaming interface
func streamProfile(pubkey: Pubkey) -> AsyncStream<ProfileStreamItem> {
/// Streams profile updates for a single pubkey.
///
/// By default, the stream immediately yields the existing profile from NostrDB
/// (if available), then continues yielding updates as they arrive from the network.
///
/// This immediate yield is essential for views that display profile data (names,
/// pictures) because the subscription restart has a ~1 second delay. Without it,
/// views would flash abbreviated pubkeys or robohash placeholders.
///
/// Set `yieldCached: false` for subscribers that only need network updates (e.g.,
/// re-rendering content when profiles change) and already handle initial state
/// through other means.
///
/// - Parameters:
/// - pubkey: The pubkey to stream profile updates for
/// - yieldCached: Whether to immediately yield the cached profile. Defaults to `true`.
/// - Returns: An AsyncStream that yields Profile objects
func streamProfile(pubkey: Pubkey, yieldCached: Bool = true) -> AsyncStream<ProfileStreamItem> {
return AsyncStream<ProfileStreamItem> { continuation in
let stream = ProfileStreamInfo(continuation: continuation)
self.add(pubkey: pubkey, stream: stream)
// Yield cached profile immediately so views don't flash placeholder content.
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
if yieldCached, let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
continuation.yield(existingProfile)
}
continuation.onTermination = { @Sendable _ in
Task { await self.removeStream(pubkey: pubkey, id: stream.id) }
}
}
}
func streamProfiles(pubkeys: Set<Pubkey>) -> AsyncStream<ProfileStreamItem> {
/// Streams profile updates for multiple pubkeys.
///
/// Same behavior as `streamProfile(_:yieldCached:)` but for a set of pubkeys.
///
/// - Parameters:
/// - pubkeys: The set of pubkeys to stream profile updates for
/// - yieldCached: Whether to immediately yield cached profiles. Defaults to `true`.
/// - Returns: An AsyncStream that yields Profile objects
func streamProfiles(pubkeys: Set<Pubkey>, yieldCached: Bool = true) -> AsyncStream<ProfileStreamItem> {
guard !pubkeys.isEmpty else {
return AsyncStream<ProfileStreamItem> { continuation in
continuation.finish()
}
}
return AsyncStream<ProfileStreamItem> { continuation in
let stream = ProfileStreamInfo(continuation: continuation)
for pubkey in pubkeys {
self.add(pubkey: pubkey, stream: stream)
}
// Yield cached profiles immediately so views render correctly from the start.
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
if yieldCached {
for pubkey in pubkeys {
if let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
continuation.yield(existingProfile)
}
}
}
continuation.onTermination = { @Sendable _ in
Task {
for pubkey in pubkeys {