Sticker management v2 - Implement drag and drop.

Adds the ability to use drag and drop to rearrange installed sticker packs.
This commit is contained in:
Jeffrey Starke
2025-04-24 10:55:55 -04:00
committed by Cody Henthorne
parent a53479e50d
commit 62ed823e42
6 changed files with 178 additions and 37 deletions

View File

@@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,10 +33,14 @@ import androidx.compose.runtime.getValue
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.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -45,6 +51,10 @@ import org.signal.core.ui.compose.Dividers
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.copied.androidx.compose.DragAndDropEvent
import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem
import org.signal.core.ui.compose.copied.androidx.compose.dragContainer
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
@@ -78,7 +88,18 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
StickerManagementScreen(
uiState = uiState,
onNavigateBack = ::supportFinishAfterTransition,
onInstallClick = viewModel::installStickerPack
availableTabCallbacks = object : AvailableStickersContentCallbacks {
override fun onInstallClick(pack: AvailableStickerPack) = viewModel.installStickerPack(pack)
},
installedTabCallbacks = object : InstalledStickersContentCallbacks {
override fun onDragAndDropEvent(event: DragAndDropEvent) {
when (event) {
is DragAndDropEvent.OnItemMove -> viewModel.updatePosition(event.fromIndex, event.toIndex)
is DragAndDropEvent.OnItemDrop -> viewModel.saveInstalledPacksSortOrder()
is DragAndDropEvent.OnDragCancel -> {}
}
}
}
)
}
}
@@ -90,12 +111,29 @@ private data class Page(
val getContent: @Composable () -> Unit
)
interface AvailableStickersContentCallbacks {
fun onInstallClick(pack: AvailableStickerPack)
object Empty : AvailableStickersContentCallbacks {
override fun onInstallClick(pack: AvailableStickerPack) = Unit
}
}
interface InstalledStickersContentCallbacks {
fun onDragAndDropEvent(event: DragAndDropEvent)
object Empty : InstalledStickersContentCallbacks {
override fun onDragAndDropEvent(event: DragAndDropEvent) = Unit
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StickerManagementScreen(
uiState: StickerManagementUiState,
onNavigateBack: () -> Unit = {},
onInstallClick: (AvailableStickerPack) -> Unit = {},
availableTabCallbacks: AvailableStickersContentCallbacks = AvailableStickersContentCallbacks.Empty,
installedTabCallbacks: InstalledStickersContentCallbacks = InstalledStickersContentCallbacks.Empty,
modifier: Modifier = Modifier
) {
Scaffold(
@@ -109,13 +147,18 @@ private fun StickerManagementScreen(
AvailableStickersContent(
blessedPacks = uiState.availableBlessedPacks,
notBlessedPacks = uiState.availableNotBlessedPacks,
onInstallClick = onInstallClick
callbacks = availableTabCallbacks
)
}
),
Page(
title = stringResource(R.string.StickerManagement_installed_tab_label),
getContent = { InstalledStickersContent(uiState.installedPacks) }
getContent = {
InstalledStickersContent(
packs = uiState.installedPacks,
callbacks = installedTabCallbacks
)
}
)
)
@@ -193,7 +236,7 @@ private fun PagerTab(
private fun AvailableStickersContent(
blessedPacks: List<AvailableStickerPack>,
notBlessedPacks: List<AvailableStickerPack>,
onInstallClick: (AvailableStickerPack) -> Unit = {},
callbacks: AvailableStickersContentCallbacks = AvailableStickersContentCallbacks.Empty,
modifier: Modifier = Modifier
) {
if (blessedPacks.isEmpty() && notBlessedPacks.isEmpty()) {
@@ -211,7 +254,7 @@ private fun AvailableStickersContent(
) {
AvailableStickerPackRow(
pack = it,
onInstallClick = { onInstallClick(it) },
onInstallClick = { callbacks.onInstallClick(it) },
modifier = Modifier.animateItem()
)
}
@@ -229,7 +272,7 @@ private fun AvailableStickersContent(
) {
AvailableStickerPackRow(
pack = it,
onInstallClick = { onInstallClick(it) },
onInstallClick = { callbacks.onInstallClick(it) },
modifier = Modifier.animateItem()
)
}
@@ -241,21 +284,48 @@ private fun AvailableStickersContent(
@Composable
private fun InstalledStickersContent(
packs: List<InstalledStickerPack>,
callbacks: InstalledStickersContentCallbacks = InstalledStickersContentCallbacks.Empty,
modifier: Modifier = Modifier
) {
if (packs.isEmpty()) {
EmptyView(text = stringResource(R.string.StickerManagement_installed_tab_empty_text))
} else {
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(lazyListState = listState, includeHeader = true, includeFooter = false, onEvent = callbacks::onDragAndDropEvent)
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
LazyColumn(
contentPadding = PaddingValues(top = 8.dp),
modifier = modifier.fillMaxHeight()
state = listState,
modifier = modifier
.fillMaxHeight()
.dragContainer(
dragDropState = dragDropState,
leftDpOffset = if (isRtl) 0.dp else screenWidth - 56.dp,
rightDpOffset = if (isRtl) 56.dp else screenWidth
)
) {
item { StickerPackSectionHeader(text = stringResource(R.string.StickerManagement_installed_stickers_header)) }
items(
item {
DraggableItem(dragDropState, 0) {
StickerPackSectionHeader(text = stringResource(R.string.StickerManagement_installed_stickers_header))
}
}
itemsIndexed(
items = packs,
key = { it.id.value }
) {
InstalledStickerPackRow(it)
key = { _, pack -> pack.id.value }
) { index, pack ->
DraggableItem(
index = index + 1,
dragDropState = dragDropState
) { isDragging ->
InstalledStickerPackRow(
pack = pack,
modifier = Modifier.shadow(if (isDragging) 1.dp else 0.dp)
)
}
}
}
}

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.swap
import org.thoughtcrime.securesms.database.model.StickerPackId
import org.thoughtcrime.securesms.database.model.StickerPackKey
import org.thoughtcrime.securesms.database.model.StickerPackRecord
@@ -77,14 +78,16 @@ class StickerManagementViewModelV2 : ViewModel() {
}
}
fun installStickerPack(pack: AvailableStickerPack) = viewModelScope.launch {
updatePackDownloadStatus(pack.id, DownloadStatus.InProgress)
fun installStickerPack(pack: AvailableStickerPack) {
viewModelScope.launch {
updatePackDownloadStatus(pack.id, DownloadStatus.InProgress)
StickerManagementRepository.installStickerPack(packId = pack.id, packKey = pack.key, notify = true)
updatePackDownloadStatus(pack.id, DownloadStatus.Downloaded)
StickerManagementRepository.installStickerPack(packId = pack.id, packKey = pack.key, notify = true)
updatePackDownloadStatus(pack.id, DownloadStatus.Downloaded)
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)
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)
}
}
private fun updatePackDownloadStatus(packId: StickerPackId, newStatus: DownloadStatus?) {
@@ -95,8 +98,20 @@ class StickerManagementViewModelV2 : ViewModel() {
}
}
fun uninstallStickerPack(pack: AvailableStickerPack) = viewModelScope.launch {
StickerManagementRepository.uninstallStickerPack(packId = pack.id, packKey = pack.key)
fun uninstallStickerPack(pack: AvailableStickerPack) {
viewModelScope.launch {
StickerManagementRepository.uninstallStickerPack(packId = pack.id, packKey = pack.key)
}
}
fun updatePosition(fromIndex: Int, toIndex: Int) {
_uiState.update { it.copy(installedPacks = _uiState.value.installedPacks.swap(fromIndex, toIndex)) }
}
fun saveInstalledPacksSortOrder() {
viewModelScope.launch {
StickerManagementRepository.setStickerPacksOrder(_uiState.value.installedPacks.map { it.record })
}
}
}

View File

@@ -133,7 +133,7 @@ fun InstalledStickerPackRow(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_drag_handle),
contentDescription = stringResource(R.string.StickerManagement_accessibility_drag_handle),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier
modifier = Modifier
.padding(horizontal = 12.dp)
.size(24.dp)
)