diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index 7339ba1bb..4aae58459 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -240,6 +240,11 @@ + + + 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 c3160d01e..2ccd1a069 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 @@ -183,7 +183,7 @@ class EventNotificationConsumer( } } - private fun notify( + private suspend fun notify( event: ChatMessageEncryptedFileHeaderEvent, account: Account, ) { @@ -210,13 +210,13 @@ class EventNotificationConsumer( val content = chatNote.event?.content ?: "" val user = chatNote.author?.toBestDisplayName() ?: "" val userPicture = chatNote.author?.profilePicture() - val noteUri = - chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + - account.signer.pubKey - .hexToByteArray() - .toNpub() + val accountNpub = + account.signer.pubKey + .hexToByteArray() + .toNpub() + val chatroomMembers = chatRoom.users.joinToString(",") + val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub - // TODO: Show Image on notification notificationManager() .sendDMNotification( event.id, @@ -226,12 +226,15 @@ class EventNotificationConsumer( userPicture, noteUri, applicationContext, + accountNpub = accountNpub, + accountPictureUrl = account.userProfile().profilePicture(), + chatroomMembers = chatroomMembers, ) } } } - private fun notify( + private suspend fun notify( event: ChatMessageEvent, account: Account, ) { @@ -255,20 +258,25 @@ class EventNotificationConsumer( val content = chatNote.event?.content ?: "" val user = chatNote.author?.toBestDisplayName() ?: "" val userPicture = chatNote.author?.profilePicture() - val noteUri = - chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + - account.signer.pubKey - .hexToByteArray() - .toNpub() + val accountNpub = + account.signer.pubKey + .hexToByteArray() + .toNpub() + val chatroomMembers = chatRoom.users.joinToString(",") + val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub + notificationManager() .sendDMNotification( - event.id, - content, - user, - event.createdAt, - userPicture, - noteUri, - applicationContext, + id = event.id, + messageBody = content, + senderName = user, + time = event.createdAt, + pictureUrl = userPicture, + uri = noteUri, + applicationContext = applicationContext, + accountNpub = accountNpub, + accountPictureUrl = account.userProfile().profilePicture(), + chatroomMembers = chatroomMembers, ) } } @@ -297,13 +305,25 @@ class EventNotificationConsumer( decryptContent(note, account.signer)?.let { content -> val user = note.author?.toBestDisplayName() ?: "" val userPicture = note.author?.profilePicture() - val noteUri = - note.toNEvent() + ACCOUNT_QUERY_PARAM + - account.signer.pubKey - .hexToByteArray() - .toNpub() + val accountNpub = + account.signer.pubKey + .hexToByteArray() + .toNpub() + val noteUri = note.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub + notificationManager() - .sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext) + .sendDMNotification( + id = event.id, + messageBody = content, + senderName = user, + time = event.createdAt, + pictureUrl = userPicture, + uri = noteUri, + applicationContext = applicationContext, + accountNpub = accountNpub, + accountPictureUrl = account.userProfile().profilePicture(), + chatroomMembers = null, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt new file mode 100644 index 000000000..f5009118e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.notifications + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +class NotificationReplyReceiver : BroadcastReceiver() { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onReceive( + context: Context, + intent: Intent, + ) { + val notificationId = intent.getIntExtra(NotificationUtils.KEY_NOTIFICATION_ID, 0) + val notificationManager = + ContextCompat.getSystemService(context, NotificationManager::class.java) + as NotificationManager + + when (intent.action) { + NotificationUtils.MARK_READ_ACTION -> { + notificationManager.cancel(notificationId) + } + + NotificationUtils.REPLY_ACTION -> { + val replyText = + RemoteInput + .getResultsFromIntent(intent) + ?.getCharSequence(NotificationUtils.KEY_REPLY_TEXT) + ?.toString() + + if (replyText.isNullOrBlank()) return + + val accountNpub = intent.getStringExtra(NotificationUtils.KEY_ACCOUNT_NPUB) ?: return + val chatroomMembersStr = intent.getStringExtra(NotificationUtils.KEY_CHATROOM_MEMBERS) ?: return + val members = chatroomMembersStr.split(",").filter { it.isNotBlank() } + + if (members.isEmpty()) return + + val pendingResult = goAsync() + + scope.launch { + // activates the relay to send the message. + val collectionJob = + scope.launch { + Amethyst.instance.relayProxyClientConnector.relayServices + .collect() + } + + try { + sendReply(accountNpub, members, replyText) + notificationManager.cancel(notificationId) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("NotificationReply", "Failed to send reply: ${e.message}") + } finally { + pendingResult.finish() + + // closes the relay connection. + collectionJob.cancel() + } + } + } + } + } + + private suspend fun sendReply( + accountNpub: String, + chatroomMembers: List, + replyText: String, + ) { + val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage(accountNpub) ?: return + val account = Amethyst.instance.accountsCache.loadAccount(accountSettings) + + val recipients = chatroomMembers.map { PTag(it) } + val template = ChatMessageEvent.build(msg = replyText, to = recipients) + + account.sendNip17PrivateMessage(template) + } +} 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 2472c097d..9c16a2e64 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 @@ -25,23 +25,37 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import coil3.ImageLoader import coil3.asDrawable -import coil3.executeBlocking import coil3.request.ImageRequest import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext object NotificationUtils { private var dmChannel: NotificationChannel? = null private var zapChannel: NotificationChannel? = null private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION" private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION" + const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION" + const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION" + const val KEY_REPLY_TEXT = "key_reply_text" + const val KEY_NOTIFICATION_ID = "key_notification_id" + const val KEY_ACCOUNT_NPUB = "key_account_npub" + const val KEY_CHATROOM_MEMBERS = "key_chatroom_members" + + private const val DM_SUMMARY_ID = 0x10000 + private const val ZAP_SUMMARY_ID = 0x20000 fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel { if (dmChannel != null) return dmChannel!! @@ -50,13 +64,12 @@ object NotificationUtils { NotificationChannel( stringRes(applicationContext, R.string.app_notification_dms_channel_id), stringRes(applicationContext, R.string.app_notification_dms_channel_name), - NotificationManager.IMPORTANCE_DEFAULT, + NotificationManager.IMPORTANCE_HIGH, ).apply { description = stringRes(applicationContext, R.string.app_notification_dms_channel_description) } - // Register the channel with the system val notificationManager: NotificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -78,7 +91,6 @@ object NotificationUtils { stringRes(applicationContext, R.string.app_notification_zaps_channel_description) } - // Register the channel with the system val notificationManager: NotificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -87,7 +99,7 @@ object NotificationUtils { return zapChannel!! } - fun NotificationManager.sendZapNotification( + suspend fun NotificationManager.sendZapNotification( id: String, messageBody: String, messageTitle: String, @@ -96,109 +108,109 @@ object NotificationUtils { uri: String, applicationContext: Context, ) { - val zapChannel = getOrCreateZapChannel(applicationContext) + getOrCreateZapChannel(applicationContext) val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id) sendNotification( - id, - messageBody, - messageTitle, - time, - pictureUrl, - uri, - channelId, - ZAP_GROUP_KEY, - applicationContext, + id = id, + messageBody = messageBody, + messageTitle = messageTitle, + time = time, + pictureUrl = pictureUrl, + uri = uri, + channelId = channelId, + notificationGroupKey = ZAP_GROUP_KEY, + category = NotificationCompat.CATEGORY_SOCIAL, + summaryId = ZAP_SUMMARY_ID, + summaryText = stringRes(applicationContext, R.string.app_notification_zaps_summary), + applicationContext = applicationContext, ) } - fun NotificationManager.sendDMNotification( + suspend fun NotificationManager.sendDMNotification( id: String, messageBody: String, - messageTitle: String, + senderName: String, time: Long, pictureUrl: String?, uri: String, applicationContext: Context, + accountNpub: String? = null, + accountPictureUrl: String? = null, + chatroomMembers: String? = null, ) { - val dmChannel = getOrCreateDMChannel(applicationContext) + getOrCreateDMChannel(applicationContext) val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id) - sendNotification( - id, - messageBody, - messageTitle, - time, - pictureUrl, - uri, - channelId, - DM_GROUP_KEY, - applicationContext, + sendDMNotificationStyled( + id = id, + messageBody = messageBody, + senderName = senderName, + time = time, + pictureUrl = pictureUrl, + uri = uri, + channelId = channelId, + applicationContext = applicationContext, + accountNpub = accountNpub, + accountPictureUrl = accountPictureUrl, + chatroomMembers = chatroomMembers, ) } - fun NotificationManager.sendNotification( + private suspend fun loadBitmap( + pictureUrl: String, + applicationContext: Context, + ): Bitmap? = + withContext(Dispatchers.IO) { + try { + val request = ImageRequest.Builder(applicationContext).data(pictureUrl).build() + val imageLoader = ImageLoader(applicationContext) + val result = imageLoader.execute(request) + (result.image?.asDrawable(applicationContext.resources) as? BitmapDrawable)?.bitmap + } catch (_: Exception) { + null + } + } + + private suspend fun NotificationManager.sendDMNotificationStyled( id: String, messageBody: String, - messageTitle: String, + senderName: String, time: Long, pictureUrl: String?, uri: String, channelId: String, - notificationGroupKey: String, - applicationContext: Context, - ) { - if (pictureUrl != null) { - val request = ImageRequest.Builder(applicationContext).data(pictureUrl).build() - - val imageLoader = ImageLoader(applicationContext) - val imageResult = imageLoader.executeBlocking(request) - sendNotificationInner( - id = id, - messageBody = messageBody, - messageTitle = messageTitle, - time = time, - picture = imageResult.image?.asDrawable(applicationContext.resources) as? BitmapDrawable, - uri = uri, - channelId, - notificationGroupKey, - applicationContext = applicationContext, - ) - } else { - sendNotificationInner( - id = id, - messageBody = messageBody, - messageTitle = messageTitle, - time = time, - picture = null, - uri = uri, - channelId, - notificationGroupKey, - applicationContext = applicationContext, - ) - } - } - - private fun NotificationManager.sendNotificationInner( - id: String, - messageBody: String, - messageTitle: String, - time: Long, - picture: BitmapDrawable?, - uri: String, - channelId: String, - notificationGroupKey: String, applicationContext: Context, + accountNpub: String?, + accountPictureUrl: String?, + chatroomMembers: String?, ) { val notId = id.hashCode() - // dont notify twice - val notifications: Array = getActiveNotifications() - for (notification in notifications) { - if (notification.id == notId) { - return - } - } + if (isDuplicate(notId)) return + + val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) } + val accountBitmap = accountPictureUrl?.let { loadBitmap(it, applicationContext) } + + val senderIcon = bitmap?.let { IconCompat.createWithBitmap(it) } + val accountIcon = accountBitmap?.let { IconCompat.createWithBitmap(it) } + + val sender = + Person + .Builder() + .setName(senderName) + .apply { senderIcon?.let { setIcon(it) } } + .build() + + val messagingStyle = + NotificationCompat + .MessagingStyle( + Person + .Builder() + .setName("Me") + .setIcon(accountIcon) + .build(), + ).addMessage(messageBody, time * 1000, sender) val contentIntent = Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() } @@ -208,41 +220,198 @@ object NotificationUtils { applicationContext, notId, contentIntent, - PendingIntent.FLAG_MUTABLE, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) - // Build the notification val builderPublic = NotificationCompat - .Builder( - applicationContext, - channelId, - ).setSmallIcon(R.drawable.amethyst) - .setContentTitle(messageTitle) + .Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.amethyst) + .setContentTitle(senderName) .setContentText(stringRes(applicationContext, R.string.app_notification_private_message)) - .setLargeIcon(picture?.bitmap) + .setLargeIcon(bitmap) .setContentIntent(contentPendingIntent) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setWhen(time * 1000) - // Build the notification val builder = NotificationCompat - .Builder( - applicationContext, - channelId, - ).setSmallIcon(R.drawable.amethyst) - .setContentTitle(messageTitle) - .setContentText(messageBody) - .setLargeIcon(picture?.bitmap) + .Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.amethyst) + .setLargeIcon(bitmap) + .setStyle(messagingStyle) .setContentIntent(contentPendingIntent) .setPublicVersion(builderPublic.build()) .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setGroup(DM_GROUP_KEY) + .setAutoCancel(true) + .setWhen(time * 1000) + + // Direct Reply action + if (accountNpub != null && chatroomMembers != null) { + val remoteInput = + RemoteInput + .Builder(KEY_REPLY_TEXT) + .setLabel(stringRes(applicationContext, R.string.app_notification_reply_label)) + .build() + + val replyIntent = + Intent(applicationContext, NotificationReplyReceiver::class.java).apply { + action = REPLY_ACTION + putExtra(KEY_NOTIFICATION_ID, notId) + putExtra(KEY_ACCOUNT_NPUB, accountNpub) + putExtra(KEY_CHATROOM_MEMBERS, chatroomMembers) + } + + val replyPendingIntent = + PendingIntent.getBroadcast( + applicationContext, + notId, + replyIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + val replyAction = + NotificationCompat.Action + .Builder(R.drawable.amethyst, stringRes(applicationContext, R.string.app_notification_reply_label), replyPendingIntent) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .build() + + builder.addAction(replyAction) + } + + // Mark as Read action + val markReadIntent = + Intent(applicationContext, NotificationReplyReceiver::class.java).apply { + action = MARK_READ_ACTION + putExtra(KEY_NOTIFICATION_ID, notId) + } + + val markReadPendingIntent = + PendingIntent.getBroadcast( + applicationContext, + notId + 1, + markReadIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + val markReadAction = + NotificationCompat.Action + .Builder(R.drawable.amethyst, stringRes(applicationContext, R.string.app_notification_mark_read_label), markReadPendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .build() + + builder.addAction(markReadAction) + + notify(notId, builder.build()) + + // Group summary notification + sendGroupSummary(channelId, DM_GROUP_KEY, DM_SUMMARY_ID, stringRes(applicationContext, R.string.app_notification_dms_summary), applicationContext) + } + + private suspend fun NotificationManager.sendNotification( + id: String, + messageBody: String, + messageTitle: String, + time: Long, + pictureUrl: String?, + uri: String, + channelId: String, + notificationGroupKey: String, + category: String, + summaryId: Int, + summaryText: String, + applicationContext: Context, + ) { + val notId = id.hashCode() + + if (isDuplicate(notId)) return + + val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) } + + val contentIntent = + Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() } + + val contentPendingIntent = + PendingIntent.getActivity( + applicationContext, + notId, + contentIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + val builderPublic = + NotificationCompat + .Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.amethyst) + .setContentTitle(messageTitle) + .setContentText(stringRes(applicationContext, R.string.app_notification_private_message)) + .setLargeIcon(bitmap) + .setContentIntent(contentPendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setWhen(time * 1000) + + val builder = + NotificationCompat + .Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.amethyst) + .setContentTitle(messageTitle) + .setContentText(messageBody) + .setStyle(NotificationCompat.BigTextStyle().bigText(messageBody)) + .setLargeIcon(bitmap) + .setContentIntent(contentPendingIntent) + .setPublicVersion(builderPublic.build()) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(category) + .setGroup(notificationGroupKey) .setAutoCancel(true) .setWhen(time * 1000) notify(notId, builder.build()) + + sendGroupSummary(channelId, notificationGroupKey, summaryId, summaryText, applicationContext) + } + + private fun NotificationManager.sendGroupSummary( + channelId: String, + groupKey: String, + summaryId: Int, + summaryText: String, + applicationContext: Context, + ) { + val activeCount = activeNotifications.count { it.notification.group == groupKey && it.id != summaryId } + + if (activeCount < 2) return + + val summaryBuilder = + NotificationCompat + .Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.amethyst) + .setGroup(groupKey) + .setGroupSummary(true) + .setAutoCancel(true) + .setStyle( + NotificationCompat + .InboxStyle() + .setSummaryText(summaryText), + ) + + notify(summaryId, summaryBuilder.build()) + } + + private fun NotificationManager.isDuplicate(notId: Int): Boolean { + val notifications: Array = activeNotifications + for (notification in notifications) { + if (notification.id == notId) { + return true + } + } + return false } /** Cancels all notifications. */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index ec65f5c14..ec2ad0893 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -428,7 +428,9 @@ private fun NavigateIfIntentRequested( actionableNextPage?.let { nextRoute -> val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() if (npub != null && accountSessionManager.currentAccountNPub() != npub) { - accountSessionManager.checkAndSwitchUserSync(npub, nextRoute) + accountSessionManager.checkAndSwitchUserSync(npub) { account -> + uriToRoute(intentNextPage, account) + } } else { val currentRoute = getRouteWithArguments(nextRoute::class, nav.controller) if (!isSameRoute(currentRoute, nextRoute)) { @@ -484,7 +486,9 @@ private fun NavigateIfIntentRequested( scope.launch { val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() if (npub != null && accountSessionManager.currentAccountNPub() != npub) { - accountSessionManager.checkAndSwitchUserSync(npub, newPage) + accountSessionManager.checkAndSwitchUserSync(npub) { newAccount -> + uriToRoute(uri, newAccount) + } } else { val currentRoute = getRouteWithArguments(newPage::class, nav.controller) if (!isSameRoute(currentRoute, newPage)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt index 3549f612c..9e9fcf4b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt @@ -112,11 +112,11 @@ class AccountSessionManager( } } - private suspend fun loginWithDefaultAccount(route: Route? = null) { + private suspend fun loginWithDefaultAccount(routeBuilder: ((account: Account) -> Route?)? = null) { val accountSettings = localPreferences.loadAccountConfigFromEncryptedStorage() if (accountSettings != null) { - startUI(accountSettings, route) + startUI(accountSettings, routeBuilder) } else { requestLoginUI() } @@ -184,11 +184,11 @@ class AccountSessionManager( fun startUI( accountSettings: AccountSettings, - route: Route? = null, + routeBuilder: ((account: Account) -> Route?)? = null, ) { val account = accountsCache.loadAccount(accountSettings) _accountContent.update { - AccountState.LoggedIn(account, route) + AccountState.LoggedIn(account, routeBuilder?.invoke(account)) } } @@ -280,7 +280,7 @@ class AccountSessionManager( localPreferences.setDefaultAccount(accountSettings) - startUI(accountSettings, route = Route.ImportFollowsSelectUser) + startUI(accountSettings, routeBuilder = { Route.ImportFollowsSelectUser }) scope.launch(Dispatchers.IO) { delay(2000) // waits for the new user to connect to the new relays. @@ -329,12 +329,12 @@ class AccountSessionManager( suspend fun checkAndSwitchUserSync( npub: String, - route: Route, + routeBuilder: ((account: Account) -> Route?)? = null, ): Boolean { if (npub != localPreferences.currentAccount()) { val account = localPreferences.allSavedAccounts().firstOrNull { it.npub == npub } if (account != null) { - switchUserSync(account, route) + switchUserSync(account, routeBuilder) return true } } @@ -343,10 +343,10 @@ class AccountSessionManager( private suspend fun switchUserSync( accountInfo: AccountInfo, - route: Route? = null, + routeBuilder: ((account: Account) -> Route?)? = null, ) { localPreferences.switchToAccount(accountInfo) - loginWithDefaultAccount(route) + loginWithDefaultAccount(routeBuilder) } fun currentAccountNPub() = diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d0f119791..a93534129 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -732,6 +732,11 @@ From %1$s for %1$s + Reply + Mark Read + New messages + New zaps + Notify: Join Conversation