diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index c9dd72cb5b..065754cc4f 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -218,6 +218,9 @@ + + withContext(Dispatchers.IO) { + SelectedMedia(uri, context.contentResolver.getType(uri)) + } + } 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 1b4a577dc5..b05ec5d244 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 @@ -625,6 +625,14 @@ private fun NavigateIfIntentRequested( } else { nav.newStack(Route.NewShortNote(message = message, attachment = media.toString())) } + + // Consume the launch intent so a later recomposition can't re-fire + // newStack for the same share (the isBaseRoute guard is a non-reactive + // snapshot and stops guarding once we navigate past the destination, + // e.g. into a chat via the one-shot picker). Clearing the action also + // lets the else-branch register the onNewIntent listener for the rest + // of this session. + activity.intent.action = null } else { var newAccount by remember { mutableStateOf(null) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/ShareIntentRouting.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/ShareIntentRouting.kt index 92cc13b41e..c7d3f06194 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/ShareIntentRouting.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/ShareIntentRouting.kt @@ -26,8 +26,13 @@ package com.vitorpamplona.amethyst.ui.navigation * component class name of the launching intent (the activity-alias name). */ object ShareIntentRouting { - /** Simple class name of the activity-alias declared in AndroidManifest.xml. */ + /** + * Simple class name of the `` declared in AndroidManifest.xml + * (android:name=".ui.ShareAsDMAlias"). MUST stay in sync with the manifest — + * renaming the alias there without updating this constant silently routes + * "Send as DM" shares to the New Post composer (no build error). + */ const val SHARE_AS_DM_ALIAS_SIMPLE_NAME = "ShareAsDMAlias" - fun isShareAsDm(componentClassName: String?): Boolean = componentClassName?.endsWith(SHARE_AS_DM_ALIAS_SIMPLE_NAME) == true + fun isShareAsDm(componentClassName: String?): Boolean = componentClassName?.endsWith(".$SHARE_AS_DM_ALIAS_SIMPLE_NAME") == true } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index a0f1c4df76..06d9bf0532 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -31,10 +31,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote @@ -50,9 +49,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Composable fun ChatroomView( @@ -122,11 +119,8 @@ fun ChatroomView( val context = LocalContext.current if (attachmentUri != null) { LaunchedEffect(key1 = attachmentUri) { - attachmentUri.ifBlank { null }?.toUri()?.let { uri -> - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(uri) - newPostModel.pickedMedia(persistentListOf(SelectedMedia(uri, mediaType))) - } + resolveSharedMedia(context, attachmentUri)?.let { + newPostModel.pickedMedia(persistentListOf(it)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 6c59988b49..a089688b21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -114,9 +114,15 @@ fun PrivateMessageEditFieldRow( if (channelScreenModel.message.text.isNotBlank()) { accountViewModel.launchSigner { channelScreenModel.sendDraftSync() + // Rotate the draft tag only AFTER the async save completes. Doing it + // synchronously here (before launchSigner runs) would make sendDraftSync + // persist under a freshly-rotated tag, duplicating the draft. See the + // matching order in NewGroupDMScreen. + channelScreenModel.cancel() } + } else { + channelScreenModel.cancel() } - channelScreenModel.cancel() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt index b2a4a157d8..f88a4e936e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt @@ -36,6 +36,14 @@ class ShareToDMNav( private val attachment: String?, ) : INav by delegate { override fun nav(route: Route) { - delegate.nav(ShareToDMRouteRewriter.rewrite(route, message, attachment)) + val rewritten = ShareToDMRouteRewriter.rewrite(route, message, attachment) + if (route is Route.Room) { + // One-shot: replace the picker in the back stack so backing out of the + // chat exits the share flow instead of returning to the picker, which + // would re-inject the shared text on re-tap and create duplicate drafts. + delegate.popUpTo(rewritten, Route.ShareToDM::class) + } else { + delegate.nav(rewritten) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt index 24c132066c..4cf7f907a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt @@ -55,6 +55,12 @@ fun ShareToDMScreen( accountViewModel: AccountViewModel, nav: INav, ) { + // Deliberately a screen-scoped, transient FeedContentState (not wired into + // AccountFeedContentStates like dmKnown/dmNew). The share picker is a one-shot, + // short-lived screen, so it owns its feed via viewModelScope and relies on + // WatchLifecycleAndUpdateModel to load/refresh on entry and resume rather than + // on the always-on additive update loop. Account switch recreates it (the + // remember key), which is correct for a transient picker. val feedContentState = remember(accountViewModel) { FeedContentState( @@ -85,7 +91,7 @@ fun ShareToDMScreen( .clickable( role = Role.Button, onClickLabel = stringRes(R.string.share_to_dm_start_new), - ) { nav.nav(Route.NewGroupDM(message = message, attachment = attachment)) } + ) { nav.popUpTo(Route.NewGroupDM(message = message, attachment = attachment), Route.ShareToDM::class) } .padding(16.dp), ) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareIntentRoutingTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareIntentRoutingTest.kt index 4022e9ff37..9ffcdd761e 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareIntentRoutingTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareIntentRoutingTest.kt @@ -46,4 +46,9 @@ class ShareIntentRoutingTest { fun rejectsNull() { assertFalse(ShareIntentRouting.isShareAsDm(null)) } + + @Test + fun rejectsSuffixThatIsNotASimpleName() { + assertFalse(ShareIntentRouting.isShareAsDm("com.evil.XShareAsDMAlias")) + } }