diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 35ddc2ed2d..9ae6954ce8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.note import android.content.Context -import android.content.Intent import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedVisibility @@ -148,6 +147,7 @@ import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorM import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo +import com.vitorpamplona.amethyst.ui.note.elements.ShareOptionsBottomSheet import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog @@ -318,6 +318,7 @@ private fun InnerReactionRow( if (!isPrivateRumor) { ShareReaction( note = baseNote, + nav = nav, grayTint = MaterialTheme.colorScheme.placeholderText, ) } @@ -338,38 +339,26 @@ private fun InnerReactionRow( @Composable fun ShareReaction( note: Note, + nav: INav, grayTint: Color, barChartModifier: Modifier = Size19Modifier, ) { - val context = LocalContext.current + var showShareSheet by remember { mutableStateOf(false) } ClickableBox( modifier = barChartModifier, - onClick = { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - stringRes(context, R.string.quick_action_share_browser_link), - ) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - stringRes(context, R.string.quick_action_share), - ) - context.startActivity(shareIntent) - }, + onClick = { showShareSheet = true }, ) { ShareIcon(barChartModifier, grayTint) } + + if (showShareSheet) { + ShareOptionsBottomSheet( + note = note, + nav = nav, + onDismiss = { showShareSheet = false }, + ) + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 20af25f5f1..892b1f72c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.State @@ -30,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -47,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon -import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog @@ -116,6 +113,23 @@ fun NoteDropDownMenu( ) { var reportDialogShowing by remember { mutableStateOf(false) } var addLabelDialogShowing by remember { mutableStateOf(false) } + var showShareSheet by remember { mutableStateOf(false) } + + // Tapping "Share" hands the note's share options off to the shared Share + // drawer (ShareOptionsBottomSheet). We render it INSTEAD of the menu dialog + // — not on top of it — so the menu doesn't sit dimmed behind the sheet; + // dismissing the drawer closes the whole menu. + if (showShareSheet) { + ShareOptionsBottomSheet( + note = note, + nav = nav, + onDismiss = { + showShareSheet = false + onDismiss() + }, + ) + return + } val state by observeBookmarksFollowsAndAccount(note, accountViewModel).collectAsStateWithLifecycle( DropDownParams( @@ -158,7 +172,6 @@ fun NoteDropDownMenu( onDismiss = onDismiss, ) { val clipboardManager = LocalClipboard.current - val actContext = LocalContext.current val scope = rememberCoroutineScope() // Unsealed rumors (private replies/posts received in gift wraps) are @@ -189,7 +202,8 @@ fun NoteDropDownMenu( } } - // Copy & Share section + // Copy & Share section. The copy-to-clipboard rows live here (and only + // here); the "Share" row hands off to the shared Share drawer. M3ActionSection { M3ActionRow(icon = MaterialSymbols.ContentCopy, text = stringRes(R.string.copy_text)) { val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note @@ -228,26 +242,7 @@ fun NoteDropDownMenu( } if (!isPrivateRumor) { M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(R.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, externalLinkForNote(note)) - putExtra(Intent.EXTRA_TITLE, stringRes(actContext, R.string.quick_action_share_browser_link)) - } - val shareIntent = Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share)) - actContext.startActivity(shareIntent) - onDismiss() - } - M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image)) { - val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex - nav.nav(Route.ShareNoteAsImageFile(shareId)) - onDismiss() - } - M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image_url)) { - val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex - nav.nav(Route.ShareNoteAsImage(shareId)) - onDismiss() + showShareSheet = true } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt new file mode 100644 index 0000000000..15693b2e7e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt @@ -0,0 +1,79 @@ +/* + * 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.ui.note.elements + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.M3ActionRow +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.note.externalLinkForNote +import com.vitorpamplona.amethyst.ui.stringRes + +/** + * The shared Share rows used by the [ShareOptionsBottomSheet] drawer (opened + * both from the reaction-row Share button and from the note's 3-dot menu). + * + * Only the true "send it somewhere" options live here — browser link, image + * file, image URL. The copy-to-clipboard options stay in the 3-dot menu, so + * they are intentionally NOT part of this shared element. + * + * Callers only render these for non-private notes: every option exposes the + * note publicly (a shareable web link, or an image of it), which must never + * happen for a private gift-wrapped rumor. + */ +@Composable +fun ShareActionRows( + note: Note, + nav: INav, + onDismiss: () -> Unit, +) { + val actContext = LocalContext.current + // AddressableNotes are shared by their replaceable address; everything else + // by event id. The two image routes resolve the note from this same id. + val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex + + M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(R.string.quick_action_share)) { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, externalLinkForNote(note)) + putExtra(Intent.EXTRA_TITLE, stringRes(actContext, R.string.quick_action_share_browser_link)) + } + val shareIntent = Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share)) + actContext.startActivity(shareIntent) + onDismiss() + } + M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image)) { + nav.nav(Route.ShareNoteAsImageFile(shareId)) + onDismiss() + } + M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image_url)) { + nav.nav(Route.ShareNoteAsImage(shareId)) + onDismiss() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareOptionsBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareOptionsBottomSheet.kt new file mode 100644 index 0000000000..f65c26bde4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareOptionsBottomSheet.kt @@ -0,0 +1,81 @@ +/* + * 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.ui.note.elements + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.stringRes + +/** + * Bottom drawer that offers the note's Share options ([ShareActionRows]). + * Opened from the reaction-row Share button and from the note's 3-dot menu, + * so both entry points share the exact same list. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareOptionsBottomSheet( + note: Note, + nav: INav, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + ) { + Text( + text = stringRes(R.string.quick_action_share), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 8.dp), + ) + M3ActionSection { + ShareActionRows( + note = note, + nav = nav, + onDismiss = onDismiss, + ) + } + } + } +}