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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user