Sticker management v2 - Implement context menus.

Adds the context menus that appear when long pressing available or installed sticker pack list items.
This commit is contained in:
Jeffrey Starke
2025-04-28 20:41:30 -04:00
committed by Cody Henthorne
parent fe853f7b65
commit fd47d28026
4 changed files with 231 additions and 13 deletions

View File

@@ -9,6 +9,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
@@ -30,11 +32,14 @@ import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -48,6 +53,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
@@ -58,6 +64,11 @@ import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.model.StickerPackId
import org.thoughtcrime.securesms.database.model.StickerPackKey
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus
import org.thoughtcrime.securesms.util.viewModel
@@ -89,9 +100,13 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
uiState = uiState,
onNavigateBack = ::supportFinishAfterTransition,
availableTabCallbacks = object : AvailableStickersContentCallbacks {
override fun onForwardClick(pack: AvailableStickerPack) = openShareSheet(pack.id, pack.key)
override fun onInstallClick(pack: AvailableStickerPack) = viewModel.installStickerPack(pack)
},
installedTabCallbacks = object : InstalledStickersContentCallbacks {
override fun onForwardClick(pack: InstalledStickerPack) = openShareSheet(pack.id, pack.key)
override fun onSelectClick(pack: InstalledStickerPack) = viewModel.toggleSelection(pack)
override fun onRemoveClick(pack: InstalledStickerPack) = viewModel.uninstallStickerPack(pack)
override fun onDragAndDropEvent(event: DragAndDropEvent) {
when (event) {
is DragAndDropEvent.OnItemMove -> viewModel.updatePosition(event.fromIndex, event.toIndex)
@@ -104,6 +119,20 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
}
}
}
private fun openShareSheet(packId: StickerPackId, packKey: StickerPackKey) {
MultiselectForwardFragment.showBottomSheet(
supportFragmentManager = supportFragmentManager,
multiselectForwardFragmentArgs = MultiselectForwardFragmentArgs(
multiShareArgs = listOf(
MultiShareArgs.Builder()
.withDraftText(StickerUrl.createShareLink(packId.value, packKey.value))
.build()
),
title = R.string.StickerManagement_share_sheet_title
)
)
}
}
private data class Page(
@@ -112,17 +141,25 @@ private data class Page(
)
interface AvailableStickersContentCallbacks {
fun onForwardClick(pack: AvailableStickerPack)
fun onInstallClick(pack: AvailableStickerPack)
object Empty : AvailableStickersContentCallbacks {
override fun onForwardClick(pack: AvailableStickerPack) = Unit
override fun onInstallClick(pack: AvailableStickerPack) = Unit
}
}
interface InstalledStickersContentCallbacks {
fun onForwardClick(pack: InstalledStickerPack)
fun onSelectClick(pack: InstalledStickerPack)
fun onRemoveClick(pack: InstalledStickerPack)
fun onDragAndDropEvent(event: DragAndDropEvent)
object Empty : InstalledStickersContentCallbacks {
override fun onForwardClick(pack: InstalledStickerPack) = Unit
override fun onSelectClick(pack: InstalledStickerPack) = Unit
override fun onRemoveClick(pack: InstalledStickerPack) = Unit
override fun onDragAndDropEvent(event: DragAndDropEvent) = Unit
}
}
@@ -232,6 +269,7 @@ private fun PagerTab(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AvailableStickersContent(
blessedPacks: List<AvailableStickerPack>,
@@ -242,6 +280,8 @@ private fun AvailableStickersContent(
if (blessedPacks.isEmpty() && notBlessedPacks.isEmpty()) {
EmptyView(text = stringResource(R.string.StickerManagement_available_tab_empty_text))
} else {
val haptics = LocalHapticFeedback.current
LazyColumn(
contentPadding = PaddingValues(top = 8.dp),
modifier = modifier.fillMaxHeight()
@@ -252,10 +292,22 @@ private fun AvailableStickersContent(
items = blessedPacks,
key = { it.id.value }
) {
val menuController = remember { DropdownMenus.MenuController() }
AvailableStickerPackRow(
pack = it,
onInstallClick = { callbacks.onInstallClick(it) },
modifier = Modifier.animateItem()
menuController = menuController,
onForwardClick = callbacks::onForwardClick,
onInstallClick = callbacks::onInstallClick,
modifier = Modifier
.animateItem()
.combinedClickable(
onClick = {},
onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
menuController.show()
},
onLongClickLabel = stringResource(R.string.StickerManagement_accessibility_open_context_menu)
)
)
}
}
@@ -270,10 +322,21 @@ private fun AvailableStickersContent(
items = notBlessedPacks,
key = { it.id.value }
) {
val menuController = remember { DropdownMenus.MenuController() }
AvailableStickerPackRow(
pack = it,
onInstallClick = { callbacks.onInstallClick(it) },
modifier = Modifier.animateItem()
menuController = menuController,
onForwardClick = callbacks::onForwardClick,
onInstallClick = callbacks::onInstallClick,
modifier = Modifier
.animateItem()
.combinedClickable(
onClick = {},
onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
menuController.show()
}
)
)
}
}
@@ -281,6 +344,7 @@ private fun AvailableStickersContent(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InstalledStickersContent(
packs: List<InstalledStickerPack>,
@@ -295,6 +359,7 @@ private fun InstalledStickersContent(
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val haptics = LocalHapticFeedback.current
LazyColumn(
contentPadding = PaddingValues(top = 8.dp),
@@ -317,13 +382,28 @@ private fun InstalledStickersContent(
items = packs,
key = { _, pack -> pack.id.value }
) { index, pack ->
val menuController = remember { DropdownMenus.MenuController() }
DraggableItem(
index = index + 1,
dragDropState = dragDropState
) { isDragging ->
InstalledStickerPackRow(
pack = pack,
modifier = Modifier.shadow(if (isDragging) 1.dp else 0.dp)
menuController = menuController,
onForwardClick = { callbacks.onForwardClick(pack) },
onSelectClick = { callbacks.onSelectClick(pack) },
onRemoveClick = { callbacks.onRemoveClick(pack) },
modifier = Modifier
.shadow(if (isDragging) 1.dp else 0.dp)
.combinedClickable(
onClick = {},
onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
menuController.show()
},
onLongClickLabel = stringResource(R.string.StickerManagement_accessibility_open_context_menu)
)
)
}
}

View File

@@ -98,7 +98,7 @@ class StickerManagementViewModelV2 : ViewModel() {
}
}
fun uninstallStickerPack(pack: AvailableStickerPack) {
fun uninstallStickerPack(pack: InstalledStickerPack) {
viewModelScope.launch {
StickerManagementRepository.uninstallStickerPack(packId = pack.id, packKey = pack.key)
}
@@ -113,13 +113,23 @@ class StickerManagementViewModelV2 : ViewModel() {
StickerManagementRepository.setStickerPacksOrder(_uiState.value.installedPacks.map { it.record })
}
}
fun toggleSelection(pack: InstalledStickerPack) {
_uiState.update { previousState ->
previousState.copy(
isMultiSelectMode = true,
selectedPackIds = if (pack.isSelected) previousState.selectedPackIds.minus(pack.id) else previousState.selectedPackIds.plus(pack.id)
)
}
}
}
data class StickerManagementUiState(
val availableBlessedPacks: List<AvailableStickerPack> = emptyList(),
val availableNotBlessedPacks: List<AvailableStickerPack> = emptyList(),
val installedPacks: List<InstalledStickerPack> = emptyList(),
val isMultiSelectMode: Boolean = false
val isMultiSelectMode: Boolean = false,
val selectedPackIds: Set<StickerPackId> = emptySet()
)
data class AvailableStickerPack(

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.stickers
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -24,6 +25,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.nullIfBlank
@@ -53,7 +56,9 @@ fun StickerPackSectionHeader(
@Composable
fun AvailableStickerPackRow(
pack: AvailableStickerPack,
onInstallClick: () -> Unit = {},
menuController: DropdownMenus.MenuController,
onForwardClick: (AvailableStickerPack) -> Unit = {},
onInstallClick: (AvailableStickerPack) -> Unit = {},
modifier: Modifier = Modifier
) {
Row(
@@ -83,7 +88,7 @@ fun AvailableStickerPackRow(
icon = readyIcon,
startButtonContentDesc = startButtonContentDesc,
startButtonOnClickLabel = startButtonOnClickLabel,
onStartClick = onInstallClick
onStartClick = { onInstallClick(pack) }
)
is DownloadStatus.InProgress -> TransferProgressState.InProgress()
@@ -96,6 +101,31 @@ fun AvailableStickerPackRow(
}
TransferProgressIndicator(state = transferState)
DropdownMenus.Menu(
controller = menuController,
offsetX = 0.dp,
offsetY = 12.dp,
modifier = modifier.background(SignalTheme.colors.colorSurface2)
) {
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_arrow_circle_down_24),
text = stringResource(R.string.StickerManagement_menu_install_pack),
onClick = {
onInstallClick(pack)
menuController.hide()
}
)
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_forward_24),
text = stringResource(R.string.StickerManagement_menu_forward_pack),
onClick = {
onForwardClick(pack)
menuController.hide()
}
)
}
}
}
@@ -105,6 +135,10 @@ fun InstalledStickerPackRow(
multiSelectModeEnabled: Boolean = false,
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {},
menuController: DropdownMenus.MenuController,
onForwardClick: (InstalledStickerPack) -> Unit = {},
onRemoveClick: (InstalledStickerPack) -> Unit = {},
onSelectClick: (InstalledStickerPack) -> Unit = {},
modifier: Modifier = Modifier
) {
Row(
@@ -137,6 +171,40 @@ fun InstalledStickerPackRow(
.padding(horizontal = 12.dp)
.size(24.dp)
)
DropdownMenus.Menu(
controller = menuController,
offsetX = 0.dp,
offsetY = 12.dp,
modifier = modifier.background(SignalTheme.colors.colorSurface2)
) {
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_forward_24),
text = stringResource(R.string.StickerManagement_menu_forward_pack),
onClick = {
onForwardClick(pack)
menuController.hide()
}
)
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_check_circle_24),
text = stringResource(R.string.StickerManagement_menu_select_pack),
onClick = {
onSelectClick(pack)
menuController.hide()
}
)
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_trash_24),
text = stringResource(R.string.StickerManagement_menu_remove_pack),
onClick = {
onRemoveClick(pack)
menuController.hide()
}
)
}
}
}
@@ -205,7 +273,8 @@ private fun AvailableStickerPackRowPreviewBlessed() = SignalTheme {
title = "Swoon / Faces",
author = "Swoon",
isBlessed = true
)
),
menuController = DropdownMenus.MenuController()
)
}
@@ -218,7 +287,8 @@ private fun AvailableStickerPackRowPreviewNotBlessed() = SignalTheme {
author = "Miguel Ángel Camprubí",
isBlessed = false,
downloadStatus = DownloadStatus.NotDownloaded
)
),
menuController = DropdownMenus.MenuController()
)
}
@@ -231,7 +301,8 @@ private fun AvailableStickerPackRowPreviewDownloading() = SignalTheme {
author = "Agnes Lee",
isBlessed = false,
downloadStatus = DownloadStatus.InProgress
)
),
menuController = DropdownMenus.MenuController()
)
}
@@ -244,7 +315,8 @@ private fun AvailableStickerPackRowPreviewDownloaded() = SignalTheme {
author = "Agnes Lee",
isBlessed = false,
downloadStatus = DownloadStatus.Downloaded
)
),
menuController = DropdownMenus.MenuController()
)
}
@@ -253,6 +325,7 @@ private fun AvailableStickerPackRowPreviewDownloaded() = SignalTheme {
private fun InstalledStickerPackRowPreview() = SignalTheme {
InstalledStickerPackRow(
multiSelectModeEnabled = false,
menuController = DropdownMenus.MenuController(),
pack = StickerPreviewDataFactory.installedPack(
title = "Bandit the Cat",
author = "Agnes Lee",
@@ -266,6 +339,7 @@ private fun InstalledStickerPackRowPreview() = SignalTheme {
private fun InstalledStickerPackRowSelectModePreview() = SignalTheme {
InstalledStickerPackRow(
multiSelectModeEnabled = true,
menuController = DropdownMenus.MenuController(),
pack = StickerPreviewDataFactory.installedPack(
title = "Bandit the Cat",
author = "Agnes Lee",
@@ -273,3 +347,43 @@ private fun InstalledStickerPackRowSelectModePreview() = SignalTheme {
)
)
}
@Composable
private fun MenuItem(
icon: ImageVector,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
DropdownMenus.Item(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
},
onClick = onClick,
modifier = modifier
)
}
@SignalPreview
@Composable
private fun MenuItemPreview() = Previews.Preview {
MenuItem(
icon = ImageVector.vectorResource(R.drawable.symbol_forward_24),
text = "Forward",
onClick = { }
)
}