mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Sticker management v2 - Implement sticker pack installation.
Adds the ability to install sticker packs using `StickerManagementActivityV2`. When the install button is clicked, it will morph into an indeterminate progress bar, which will then animate into a checkmark once the installation completes successfully. Then a couple seconds later, the sticker pack row will be removed from the available sticker packs list.
This commit is contained in:
committed by
Cody Henthorne
parent
9f40bfc645
commit
a5f766a333
@@ -5,6 +5,14 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.transfercontrols
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -28,10 +36,22 @@ fun TransferProgressIndicator(
|
||||
state: TransferProgressState,
|
||||
modifier: Modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
when (state) {
|
||||
is TransferProgressState.Ready -> StartTransferButton(state, modifier)
|
||||
is TransferProgressState.InProgress -> ProgressIndicator(state, modifier)
|
||||
is TransferProgressState.Complete -> CompleteIcon(state, modifier)
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
transitionSpec = {
|
||||
val startDelay = 200
|
||||
val enterTransition = fadeIn(tween(delayMillis = startDelay, durationMillis = 500)) + scaleIn(tween(delayMillis = startDelay, durationMillis = 400))
|
||||
val exitTransition = fadeOut(tween(delayMillis = startDelay, durationMillis = 600)) + scaleOut(tween(delayMillis = startDelay, durationMillis = 800))
|
||||
enterTransition
|
||||
.togetherWith(exitTransition)
|
||||
.using(SizeTransform(clip = false))
|
||||
}
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
is TransferProgressState.Ready -> StartTransferButton(targetState, modifier)
|
||||
is TransferProgressState.InProgress -> ProgressIndicator(targetState, modifier)
|
||||
is TransferProgressState.Complete -> CompleteIcon(targetState, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ 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.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.Previews
|
||||
@@ -62,13 +65,20 @@ class StickerManagementActivityV2 : PassphraseRequiredActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onScreenVisible()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SignalTheme {
|
||||
StickerManagementScreen(
|
||||
uiState = uiState,
|
||||
onNavigateBack = ::supportFinishAfterTransition
|
||||
onNavigateBack = ::supportFinishAfterTransition,
|
||||
onInstallClick = viewModel::installStickerPack
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,6 +95,7 @@ private data class Page(
|
||||
private fun StickerManagementScreen(
|
||||
uiState: StickerManagementUiState,
|
||||
onNavigateBack: () -> Unit = {},
|
||||
onInstallClick: (AvailableStickerPack) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -94,7 +105,13 @@ private fun StickerManagementScreen(
|
||||
val pages = listOf(
|
||||
Page(
|
||||
title = stringResource(R.string.StickerManagement_available_tab_label),
|
||||
getContent = { AvailableStickersContent(blessedPacks = uiState.availableBlessedPacks, availablePacks = uiState.availablePacks) }
|
||||
getContent = {
|
||||
AvailableStickersContent(
|
||||
blessedPacks = uiState.availableBlessedPacks,
|
||||
notBlessedPacks = uiState.availableNotBlessedPacks,
|
||||
onInstallClick = onInstallClick
|
||||
)
|
||||
}
|
||||
),
|
||||
Page(
|
||||
title = stringResource(R.string.StickerManagement_installed_tab_label),
|
||||
@@ -175,10 +192,11 @@ private fun PagerTab(
|
||||
@Composable
|
||||
private fun AvailableStickersContent(
|
||||
blessedPacks: List<AvailableStickerPack>,
|
||||
availablePacks: List<AvailableStickerPack>,
|
||||
notBlessedPacks: List<AvailableStickerPack>,
|
||||
onInstallClick: (AvailableStickerPack) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (blessedPacks.isEmpty() && availablePacks.isEmpty()) {
|
||||
if (blessedPacks.isEmpty() && notBlessedPacks.isEmpty()) {
|
||||
EmptyView(text = stringResource(R.string.StickerManagement_available_tab_empty_text))
|
||||
} else {
|
||||
LazyColumn(
|
||||
@@ -191,21 +209,29 @@ private fun AvailableStickersContent(
|
||||
items = blessedPacks,
|
||||
key = { it.record.packId }
|
||||
) {
|
||||
AvailableStickerPackRow(it)
|
||||
AvailableStickerPackRow(
|
||||
pack = it,
|
||||
onInstallClick = { onInstallClick(it) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (blessedPacks.isNotEmpty() && availablePacks.isNotEmpty()) {
|
||||
if (blessedPacks.isNotEmpty() && notBlessedPacks.isNotEmpty()) {
|
||||
item { Dividers.Default() }
|
||||
}
|
||||
|
||||
if (availablePacks.isNotEmpty()) {
|
||||
if (notBlessedPacks.isNotEmpty()) {
|
||||
item { StickerPackSectionHeader(text = stringResource(R.string.StickerManagement_stickers_you_received_header)) }
|
||||
items(
|
||||
items = availablePacks,
|
||||
items = notBlessedPacks,
|
||||
key = { it.record.packId }
|
||||
) {
|
||||
AvailableStickerPackRow(it)
|
||||
AvailableStickerPackRow(
|
||||
pack = it,
|
||||
onInstallClick = { onInstallClick(it) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,10 +280,7 @@ private fun EmptyView(
|
||||
private fun StickerManagementScreenEmptyStatePreview() {
|
||||
Previews.Preview {
|
||||
StickerManagementScreen(
|
||||
StickerManagementUiState(
|
||||
availablePacks = emptyList(),
|
||||
installedPacks = emptyList()
|
||||
)
|
||||
StickerManagementUiState()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -274,7 +297,7 @@ private fun AvailableStickersContentPreview() {
|
||||
isBlessed = true
|
||||
)
|
||||
),
|
||||
availablePacks = listOf(
|
||||
notBlessedPacks = listOf(
|
||||
StickerPreviewDataFactory.availablePack(
|
||||
title = "Bandit the Cat",
|
||||
author = "Agnes Lee",
|
||||
|
||||
@@ -69,19 +69,22 @@ object StickerManagementRepository {
|
||||
val installedPacks = mutableListOf<StickerPackRecord>()
|
||||
val availablePacks = mutableListOf<StickerPackRecord>()
|
||||
val blessedPacks = mutableListOf<StickerPackRecord>()
|
||||
val sortOrderById = mutableMapOf<String, Int>()
|
||||
|
||||
reader.asSequence().forEach { record ->
|
||||
reader.asSequence().forEachIndexed { index, record ->
|
||||
when {
|
||||
record.isInstalled -> installedPacks.add(record)
|
||||
BlessedPacks.contains(record.packId) -> blessedPacks.add(record)
|
||||
else -> availablePacks.add(record)
|
||||
}
|
||||
sortOrderById[record.packId] = index
|
||||
}
|
||||
|
||||
StickerPackResult(
|
||||
installedPacks = installedPacks,
|
||||
availablePacks = availablePacks,
|
||||
blessedPacks = blessedPacks
|
||||
blessedPacks = blessedPacks,
|
||||
sortOrderByPackId = sortOrderById
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -159,5 +162,6 @@ object StickerManagementRepository {
|
||||
data class StickerPackResult(
|
||||
val installedPacks: List<StickerPackRecord>,
|
||||
val availablePacks: List<StickerPackRecord>,
|
||||
val blessedPacks: List<StickerPackRecord>
|
||||
val blessedPacks: List<StickerPackRecord>,
|
||||
val sortOrderByPackId: Map<String, Int>
|
||||
)
|
||||
|
||||
@@ -7,10 +7,13 @@ package org.thoughtcrime.securesms.stickers
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord
|
||||
import org.thoughtcrime.securesms.stickers.AvailableStickerPack.DownloadStatus
|
||||
@@ -21,32 +24,82 @@ class StickerManagementViewModelV2 : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(StickerManagementUiState())
|
||||
val uiState: StateFlow<StickerManagementUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val downloadStatusByPackId: MutableStateFlow<Map<String, DownloadStatus>> = MutableStateFlow(emptyMap())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
stickerManagementRepo.deleteOrphanedStickerPacks()
|
||||
stickerManagementRepo.fetchUnretrievedReferencePacks()
|
||||
loadStickerPacks()
|
||||
}
|
||||
}
|
||||
|
||||
fun onScreenVisible() {
|
||||
viewModelScope.launch {
|
||||
stickerManagementRepo.deleteOrphanedStickerPacks()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadStickerPacks() {
|
||||
StickerManagementRepository.getStickerPacks()
|
||||
.collectLatest { result ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
availableBlessedPacks = result.blessedPacks
|
||||
.map { AvailableStickerPack(record = it, isBlessed = true, downloadStatus = DownloadStatus.NotDownloaded) },
|
||||
availablePacks = result.availablePacks
|
||||
.map { AvailableStickerPack(record = it, isBlessed = false, downloadStatus = DownloadStatus.NotDownloaded) },
|
||||
installedPacks = result.installedPacks
|
||||
.mapIndexed { index, record -> InstalledStickerPack(record = record, isBlessed = BlessedPacks.contains(record.packId), sortOrder = index) }
|
||||
)
|
||||
combine(stickerManagementRepo.getStickerPacks(), downloadStatusByPackId, ::Pair)
|
||||
.collectLatest { (stickerPacksResult, downloadStatuses) ->
|
||||
val recentlyInstalledPacks = stickerPacksResult.installedPacks.filter { downloadStatuses.contains(it.packId) }
|
||||
val allAvailablePacks = (stickerPacksResult.blessedPacks + stickerPacksResult.availablePacks + recentlyInstalledPacks)
|
||||
.map { record ->
|
||||
AvailableStickerPack(
|
||||
record = record,
|
||||
isBlessed = BlessedPacks.contains(record.packId),
|
||||
downloadStatus = downloadStatuses.getOrElse(record.packId) {
|
||||
downloadStatusByPackId.value.getOrDefault(record.packId, DownloadStatus.NotDownloaded)
|
||||
}
|
||||
)
|
||||
}
|
||||
.sortedBy { stickerPacksResult.sortOrderByPackId.getValue(it.record.packId) }
|
||||
|
||||
val (availableBlessedPacks, availableNotBlessedPacks) = allAvailablePacks.partition { it.isBlessed }
|
||||
val installedPacks = stickerPacksResult.installedPacks.map { record ->
|
||||
InstalledStickerPack(
|
||||
record = record,
|
||||
isBlessed = BlessedPacks.contains(record.packId),
|
||||
sortOrder = stickerPacksResult.sortOrderByPackId.getValue(record.packId)
|
||||
)
|
||||
}
|
||||
|
||||
_uiState.update { previousState ->
|
||||
previousState.copy(
|
||||
availableBlessedPacks = availableBlessedPacks,
|
||||
availableNotBlessedPacks = availableNotBlessedPacks,
|
||||
installedPacks = installedPacks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun installStickerPack(pack: AvailableStickerPack) = viewModelScope.launch {
|
||||
updatePackDownloadStatus(pack.record.packId, DownloadStatus.InProgress)
|
||||
|
||||
StickerManagementRepository.installStickerPack(packId = pack.record.packId, packKey = pack.record.packKey, notify = true)
|
||||
updatePackDownloadStatus(pack.record.packId, 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.record.packId, null)
|
||||
}
|
||||
|
||||
private fun updatePackDownloadStatus(packId: String, newStatus: DownloadStatus?) {
|
||||
downloadStatusByPackId.value = if (newStatus == null) {
|
||||
downloadStatusByPackId.value.minus(packId)
|
||||
} else {
|
||||
downloadStatusByPackId.value.plus(packId to newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallStickerPack(pack: AvailableStickerPack) = viewModelScope.launch {
|
||||
StickerManagementRepository.uninstallStickerPack(packId = pack.record.packId, packKey = pack.record.packKey)
|
||||
}
|
||||
}
|
||||
|
||||
data class StickerManagementUiState(
|
||||
val availableBlessedPacks: List<AvailableStickerPack> = emptyList(),
|
||||
val availablePacks: List<AvailableStickerPack> = emptyList(),
|
||||
val availableNotBlessedPacks: List<AvailableStickerPack> = emptyList(),
|
||||
val installedPacks: List<InstalledStickerPack> = emptyList(),
|
||||
val isMultiSelectMode: Boolean = false
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -52,7 +53,7 @@ fun StickerPackSectionHeader(
|
||||
@Composable
|
||||
fun AvailableStickerPackRow(
|
||||
pack: AvailableStickerPack,
|
||||
onStartDownloadClick: () -> Unit = {},
|
||||
onInstallClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -69,23 +70,32 @@ fun AvailableStickerPackRow(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
TransferProgressIndicator(
|
||||
state = when (pack.downloadStatus) {
|
||||
DownloadStatus.NotDownloaded -> TransferProgressState.Ready(
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_arrow_circle_down_24),
|
||||
startButtonContentDesc = stringResource(R.string.StickerManagement_accessibility_download),
|
||||
startButtonOnClickLabel = stringResource(R.string.StickerManagement_accessibility_download_pack, pack.record.title),
|
||||
onStartClick = onStartDownloadClick
|
||||
val readyIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_circle_down_24)
|
||||
val downloadedIcon = ImageVector.vectorResource(R.drawable.symbol_check_24)
|
||||
|
||||
val startButtonContentDesc = stringResource(R.string.StickerManagement_accessibility_download)
|
||||
val startButtonOnClickLabel = stringResource(R.string.StickerManagement_accessibility_download_pack, pack.record.title)
|
||||
val downloadedContentDesc = stringResource(R.string.StickerManagement_accessibility_downloaded_checkmark, pack.record.title)
|
||||
|
||||
val transferState = remember(pack.downloadStatus) {
|
||||
when (pack.downloadStatus) {
|
||||
is DownloadStatus.NotDownloaded -> TransferProgressState.Ready(
|
||||
icon = readyIcon,
|
||||
startButtonContentDesc = startButtonContentDesc,
|
||||
startButtonOnClickLabel = startButtonOnClickLabel,
|
||||
onStartClick = onInstallClick
|
||||
)
|
||||
|
||||
is DownloadStatus.InProgress -> TransferProgressState.InProgress()
|
||||
|
||||
DownloadStatus.Downloaded -> TransferProgressState.Complete(
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_check_24),
|
||||
iconContentDesc = stringResource(R.string.StickerManagement_accessibility_downloaded_checkmark, pack.record.title)
|
||||
is DownloadStatus.Downloaded -> TransferProgressState.Complete(
|
||||
icon = downloadedIcon,
|
||||
iconContentDesc = downloadedContentDesc
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
TransferProgressIndicator(state = transferState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user