mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Sticker management v2 - Implement remaining functionality.
- Fix bottom action bar shadow clipping during visibility animations. - Show snackbar after installing/uninstalling sticker packs. - Navigate to sticker preview on row click. - Add top app bar menu to enable multi-select mode. - Start StickerManagementActivityV2 instead of the old StickerManagementActivity
This commit is contained in:
committed by
Cody Henthorne
parent
51851fa5fe
commit
04c14a82be
@@ -10,6 +10,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -26,13 +28,19 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SecondaryTabRow
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -41,6 +49,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
@@ -50,6 +59,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -66,6 +76,7 @@ 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
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem
|
||||
import org.signal.core.ui.compose.copied.androidx.compose.dragContainer
|
||||
@@ -111,10 +122,12 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
|
||||
StickerManagementScreen(
|
||||
uiState = uiState,
|
||||
onNavigateBack = ::supportFinishAfterTransition,
|
||||
onExitMultiSelectMode = { viewModel.setMultiSelectEnabled(false) },
|
||||
onSetMultiSelectModeEnabled = viewModel::setMultiSelectEnabled,
|
||||
onSnackbarDismiss = { viewModel.onSnackbarDismiss() },
|
||||
availableTabCallbacks = object : AvailableStickersContentCallbacks {
|
||||
override fun onForwardClick(pack: AvailableStickerPack) = openShareSheet(pack.id, pack.key)
|
||||
override fun onInstallClick(pack: AvailableStickerPack) = viewModel.installStickerPack(pack)
|
||||
override fun onShowPreviewClick(pack: AvailableStickerPack) = navigateToStickerPreview(pack.id, pack.key)
|
||||
},
|
||||
installedTabCallbacks = object : InstalledStickersContentCallbacks {
|
||||
override fun onForwardClick(pack: InstalledStickerPack) = openShareSheet(pack.id, pack.key)
|
||||
@@ -130,6 +143,8 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
|
||||
is DragAndDropEvent.OnDragCancel -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShowPreviewClick(pack: InstalledStickerPack) = navigateToStickerPreview(pack.id, pack.key)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -149,6 +164,10 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToStickerPreview(packId: StickerPackId, packKey: StickerPackKey) {
|
||||
startActivity(StickerPackPreviewActivity.getIntent(packId.value, packKey.value))
|
||||
}
|
||||
}
|
||||
|
||||
private data class Page(
|
||||
@@ -159,10 +178,12 @@ private data class Page(
|
||||
interface AvailableStickersContentCallbacks {
|
||||
fun onForwardClick(pack: AvailableStickerPack)
|
||||
fun onInstallClick(pack: AvailableStickerPack)
|
||||
fun onShowPreviewClick(pack: AvailableStickerPack)
|
||||
|
||||
object Empty : AvailableStickersContentCallbacks {
|
||||
override fun onForwardClick(pack: AvailableStickerPack) = Unit
|
||||
override fun onInstallClick(pack: AvailableStickerPack) = Unit
|
||||
override fun onShowPreviewClick(pack: AvailableStickerPack) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +195,7 @@ interface InstalledStickersContentCallbacks {
|
||||
fun onSelectionToggle(pack: InstalledStickerPack)
|
||||
fun onSelectAllToggle()
|
||||
fun onDragAndDropEvent(event: DragAndDropEvent)
|
||||
fun onShowPreviewClick(pack: InstalledStickerPack)
|
||||
|
||||
object Empty : InstalledStickersContentCallbacks {
|
||||
override fun onForwardClick(pack: InstalledStickerPack) = Unit
|
||||
@@ -183,6 +205,7 @@ interface InstalledStickersContentCallbacks {
|
||||
override fun onSelectionToggle(pack: InstalledStickerPack) = Unit
|
||||
override fun onSelectAllToggle() = Unit
|
||||
override fun onDragAndDropEvent(event: DragAndDropEvent) = Unit
|
||||
override fun onShowPreviewClick(pack: InstalledStickerPack) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +214,8 @@ interface InstalledStickersContentCallbacks {
|
||||
private fun StickerManagementScreen(
|
||||
uiState: StickerManagementUiState,
|
||||
onNavigateBack: () -> Unit = {},
|
||||
onExitMultiSelectMode: () -> Unit = {},
|
||||
onSetMultiSelectModeEnabled: (Boolean) -> Unit = {},
|
||||
onSnackbarDismiss: () -> Unit = {},
|
||||
availableTabCallbacks: AvailableStickersContentCallbacks = AvailableStickersContentCallbacks.Empty,
|
||||
installedTabCallbacks: InstalledStickersContentCallbacks = InstalledStickersContentCallbacks.Empty,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -226,14 +250,25 @@ private fun StickerManagementScreen(
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (pagerState.currentPage == 1 && uiState.multiSelectEnabled) {
|
||||
val isInstalledTabActive = pagerState.currentPage == 1
|
||||
if (isInstalledTabActive && uiState.multiSelectEnabled) {
|
||||
MultiSelectTopAppBar(
|
||||
selectedItemCount = uiState.selectedPackIds.size,
|
||||
onExitClick = onExitMultiSelectMode
|
||||
onExitClick = { onSetMultiSelectModeEnabled(false) }
|
||||
)
|
||||
} else {
|
||||
TopAppBar(onBackPress = onNavigateBack)
|
||||
TopAppBar(
|
||||
onBackPress = onNavigateBack,
|
||||
showMenuButton = isInstalledTabActive,
|
||||
onSetMultiSelectModeEnabled = onSetMultiSelectModeEnabled
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
actionConfirmation = uiState.actionConfirmation,
|
||||
onSnackbarDismiss = onSnackbarDismiss
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
@@ -271,14 +306,53 @@ private fun StickerManagementScreen(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopAppBar(
|
||||
onBackPress: () -> Unit
|
||||
showMenuButton: Boolean = false,
|
||||
onBackPress: () -> Unit,
|
||||
onSetMultiSelectModeEnabled: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffolds.DefaultTopAppBar(
|
||||
title = stringResource(R.string.StickerManagementActivity_stickers),
|
||||
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
|
||||
onNavigationClick = onBackPress
|
||||
onNavigationClick = onBackPress,
|
||||
actions = {
|
||||
if (showMenuButton) {
|
||||
val menuController = remember { DropdownMenus.MenuController() }
|
||||
IconButton(
|
||||
onClick = { menuController.show() },
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_more_vertical),
|
||||
contentDescription = stringResource(R.string.StickerManagement_accessibility_open_top_bar_menu)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenus.Menu(
|
||||
controller = menuController,
|
||||
offsetX = 24.dp,
|
||||
offsetY = 0.dp,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.widthIn(min = 200.dp)
|
||||
.background(SignalTheme.colors.colorSurface2)
|
||||
) {
|
||||
DropdownMenus.Item(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.StickerManagement_menu_select_packs),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSetMultiSelectModeEnabled(true)
|
||||
menuController.hide()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -342,17 +416,17 @@ private fun AvailableStickersContent(
|
||||
items(
|
||||
items = blessedPacks,
|
||||
key = { it.id.value }
|
||||
) {
|
||||
) { pack ->
|
||||
val menuController = remember { DropdownMenus.MenuController() }
|
||||
AvailableStickerPackRow(
|
||||
pack = it,
|
||||
pack = pack,
|
||||
menuController = menuController,
|
||||
onForwardClick = callbacks::onForwardClick,
|
||||
onInstallClick = callbacks::onInstallClick,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onClick = { callbacks.onShowPreviewClick(pack) },
|
||||
onLongClick = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
menuController.show()
|
||||
@@ -372,17 +446,17 @@ private fun AvailableStickersContent(
|
||||
items(
|
||||
items = notBlessedPacks,
|
||||
key = { it.id.value }
|
||||
) {
|
||||
) { pack ->
|
||||
val menuController = remember { DropdownMenus.MenuController() }
|
||||
AvailableStickerPackRow(
|
||||
pack = it,
|
||||
pack = pack,
|
||||
menuController = menuController,
|
||||
onForwardClick = callbacks::onForwardClick,
|
||||
onInstallClick = callbacks::onInstallClick,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onClick = { callbacks.onShowPreviewClick(pack) },
|
||||
onLongClick = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
menuController.show()
|
||||
@@ -464,6 +538,8 @@ private fun InstalledStickersContent(
|
||||
onClick = {
|
||||
if (multiSelectEnabled) {
|
||||
callbacks.onSelectionToggle(pack)
|
||||
} else {
|
||||
callbacks.onShowPreviewClick(pack)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
@@ -523,6 +599,43 @@ private fun InstalledStickersContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SnackbarHost(
|
||||
actionConfirmation: StickerManagementConfirmation?,
|
||||
onSnackbarDismiss: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val hostState = remember { SnackbarHostState() }
|
||||
|
||||
val snackbarMessage = when (actionConfirmation) {
|
||||
is StickerManagementConfirmation.InstalledPack -> stringResource(R.string.StickerManagement_installed_pack_s, actionConfirmation.packTitle)
|
||||
is StickerManagementConfirmation.UninstalledPack -> stringResource(R.string.StickerManagement_deleted_pack_s, actionConfirmation.packTitle)
|
||||
is StickerManagementConfirmation.UninstalledPacks -> pluralStringResource(
|
||||
R.plurals.StickerManagement_deleted_n_packs,
|
||||
actionConfirmation.numPacksUninstalled,
|
||||
NumberFormat.getInstance().format(actionConfirmation.numPacksUninstalled)
|
||||
)
|
||||
|
||||
null -> null
|
||||
}
|
||||
|
||||
LaunchedEffect(actionConfirmation) {
|
||||
if (snackbarMessage != null) {
|
||||
val result = hostState.showSnackbar(
|
||||
message = snackbarMessage,
|
||||
duration = SnackbarDuration.Short,
|
||||
withDismissAction = false
|
||||
)
|
||||
|
||||
if (result == SnackbarResult.Dismissed) {
|
||||
onSnackbarDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Snackbars.Host(hostState, modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyView(
|
||||
text: String
|
||||
|
||||
@@ -86,6 +86,12 @@ class StickerManagementViewModelV2 : ViewModel() {
|
||||
StickerManagementRepository.installStickerPack(packId = pack.id, packKey = pack.key, notify = true)
|
||||
updatePackDownloadStatus(pack.id, DownloadStatus.Downloaded)
|
||||
|
||||
_uiState.update { previousState ->
|
||||
previousState.copy(
|
||||
actionConfirmation = StickerManagementConfirmation.InstalledPack(pack.record.title)
|
||||
)
|
||||
}
|
||||
|
||||
delay(1500) // wait, so we show the downloaded status for a bit before removing this row from the available sticker packs list
|
||||
updatePackDownloadStatus(pack.id, null)
|
||||
}
|
||||
@@ -125,14 +131,22 @@ class StickerManagementViewModelV2 : ViewModel() {
|
||||
}
|
||||
|
||||
private fun uninstallStickerPacks(packIds: Set<StickerPackId>) {
|
||||
val packsToUninstall = _uiState.value.installedPacks.filter { packIds.contains(it.id) }
|
||||
viewModelScope.launch {
|
||||
_uiState.value.installedPacks
|
||||
.filter { packIds.contains(it.id) }
|
||||
.forEach { pack -> StickerManagementRepository.uninstallStickerPack(packId = pack.id, packKey = pack.key) }
|
||||
packsToUninstall.forEach { pack ->
|
||||
StickerManagementRepository.uninstallStickerPack(packId = pack.id, packKey = pack.key)
|
||||
_uiState.update { previousState ->
|
||||
previousState.copy(selectedPackIds = previousState.selectedPackIds.minus(pack.id))
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.update { previousState ->
|
||||
previousState.copy(
|
||||
selectedPackIds = previousState.selectedPackIds.minus(packIds)
|
||||
actionConfirmation = if (packsToUninstall.size == 1) {
|
||||
StickerManagementConfirmation.UninstalledPack(packsToUninstall.single().record.title)
|
||||
} else {
|
||||
StickerManagementConfirmation.UninstalledPacks(packsToUninstall.size)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -179,6 +193,12 @@ class StickerManagementViewModelV2 : ViewModel() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSnackbarDismiss() {
|
||||
_uiState.update { previousState ->
|
||||
previousState.copy(actionConfirmation = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class StickerManagementUiState(
|
||||
@@ -187,13 +207,20 @@ data class StickerManagementUiState(
|
||||
val installedPacks: List<InstalledStickerPack> = emptyList(),
|
||||
val multiSelectEnabled: Boolean = false,
|
||||
val selectedPackIds: Set<StickerPackId> = emptySet(),
|
||||
val userPrompt: ConfirmRemoveStickerPacksPrompt? = null
|
||||
val userPrompt: ConfirmRemoveStickerPacksPrompt? = null,
|
||||
val actionConfirmation: StickerManagementConfirmation? = null
|
||||
)
|
||||
|
||||
data class ConfirmRemoveStickerPacksPrompt(
|
||||
val numItemsToDelete: Int
|
||||
)
|
||||
|
||||
sealed interface StickerManagementConfirmation {
|
||||
data class InstalledPack(val packTitle: String) : StickerManagementConfirmation
|
||||
data class UninstalledPack(val packTitle: String) : StickerManagementConfirmation
|
||||
data class UninstalledPacks(val numPacksUninstalled: Int) : StickerManagementConfirmation
|
||||
}
|
||||
|
||||
data class AvailableStickerPack(
|
||||
val record: StickerPackRecord,
|
||||
val isBlessed: Boolean,
|
||||
|
||||
Reference in New Issue
Block a user