diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 02d17cede..6aa8fddc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1757,10 +1757,23 @@ class Account( // Build filter for the member's KeyPackages val filter = manager.subscriptionManager.keyPackageFilter(memberPubKey) - val relays = outboxRelays.flow.value - // Query across outbox relays - val filterMap = relays.associateWith { listOf(filter) } + // KeyPackages are published by the invitee to *their own* outbox + // (see publishMarmotKeyPackage), so look there first. Union with + // our own outbox so we still find packages that ended up on a + // shared relay, and as a fallback when we don't know the + // invitee's outbox yet. + val myOutbox = outboxRelays.flow.value + val memberOutbox = + cache + .getOrCreateUser(memberPubKey) + .outboxRelays() + ?.toSet() + .orEmpty() + val fetchRelays = memberOutbox + myOutbox + + // Query across the combined relay set + val filterMap = fetchRelays.associateWith { listOf(filter) } val event = client.fetchFirst( @@ -1784,7 +1797,10 @@ class Account( kotlin.io.encoding.Base64 .decode(keyPackageBase64) val keyPackageEventId = event.id - val groupRelays = relays.toList() + // The relays embedded in the WelcomeEvent tell the new member + // where to subscribe for subsequent GroupEvents. Use our own + // outbox — that's where we will publish them. + val groupRelays = myOutbox.toList() addMarmotGroupMember( nostrGroupId = nostrGroupId, @@ -1823,9 +1839,32 @@ class Account( // Publish commit first (critical ordering) client.publish(commitEvent.signedEvent, groupRelays.toSet()) - // Then send Welcome gift wrap to the new member + // Then send the Welcome gift wrap to the new member. + // + // We deliberately do NOT route this through + // computeRelayListToBroadcast(): its GiftWrap branch returns an + // empty relay set when the recipient has no NIP-17 kind:10050 and + // no cached NIP-65 inbox relays, which silently drops the welcome + // and leaves the invitee with no way to discover the group. + // + // Instead, mirror sendNip04PrivateMessage's delivery strategy: + // publish to our own outbox (so we keep a copy and the invitee + // may find it via a shared relay) unioned with the recipient's + // DM inbox relays — User.dmInboxRelays() already falls back + // through NIP-17 kind:10050 → NIP-65 read relays, which is where + // the invitee's AccountGiftWrapsEoseManager actually listens. if (welcomeDelivery != null) { - val relayList = computeRelayListToBroadcast(welcomeDelivery.giftWrapEvent) + val recipientInbox = + cache + .getOrCreateUser(memberPubKey) + .dmInboxRelays() + .orEmpty() + val relayList = outboxRelays.flow.value + recipientInbox + if (relayList.isEmpty()) { + Log.w("Marmot") { + "addMarmotGroupMember: no relays to deliver welcome gift wrap to ${memberPubKey.take(8)}…" + } + } client.publish(welcomeDelivery.giftWrapEvent, relayList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 739a71ed9..8216d9b68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -71,8 +71,10 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import java.math.BigDecimal import kotlin.coroutines.cancellation.CancellationException @@ -699,11 +701,32 @@ class EventNotificationConsumer( if (TimeUtils.now() - event.createdAt > CallManager.MAX_EVENT_AGE_SECONDS) return - val callerUser = LocalCache.getUserIfExists(event.pubKey) - val callerName = callerUser?.toBestDisplayName() ?: event.pubKey.take(8) + "..." + val callerUser = LocalCache.getOrCreateUser(event.pubKey) + + // If the caller's metadata hasn't been loaded yet (e.g. fresh process from + // a push notification), briefly subscribe to the user finder so we can + // resolve the user's display name instead of showing the raw pubkey. + if (callerUser.metadataOrNull()?.bestName() == null) { + val authorState = UserFinderQueryState(callerUser, account) + try { + Amethyst.instance.sources.userFinder + .subscribe(authorState) + withTimeoutOrNull(5_000L) { + callerUser + .metadata() + .flow + .first { it?.info?.bestName() != null } + } + } finally { + Amethyst.instance.sources.userFinder + .unsubscribe(authorState) + } + } + + val callerName = callerUser.toBestDisplayName() val callerBitmap = - callerUser?.profilePicture()?.let { pictureUrl -> + callerUser.profilePicture()?.let { pictureUrl -> kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { try { val request = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index 2c6f66497..53ffd2e60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -630,12 +630,12 @@ object NotificationUtils { callerPubKey: String, context: Context, ) { - val callerUser = LocalCache.getUserIfExists(callerPubKey) - val callerName = callerUser?.toBestDisplayName() ?: callerPubKey.take(8) + "..." + val callerUser = LocalCache.getOrCreateUser(callerPubKey) + val callerName = callerUser.toBestDisplayName() val uri = "nostr:${callerPubKey.hexToByteArray().toNpub()}" val callerBitmap = - callerUser?.profilePicture()?.let { pictureUrl -> + callerUser.profilePicture()?.let { pictureUrl -> withContext(Dispatchers.IO) { try { val request =