This commit is contained in:
Vitor Pamplona
2026-04-15 16:21:22 -04:00
3 changed files with 74 additions and 12 deletions
@@ -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)
}
}
@@ -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 =
@@ -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 =