Create new sticker management screen with tabbed interface.

Adds a skeleton implementation of `StickerManagementActivityV2`. This new activity is not currently connected to anything, but once complete it will replace `StickerManagementActivity`.
This commit is contained in:
Jeffrey Starke
2025-04-10 17:36:09 -04:00
committed by Alex Hart
parent 90a356b29d
commit 885588db86
4 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.stickers
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
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.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.viewModel
class StickerManagementActivityV2 : PassphraseRequiredActivity() {
companion object {
@JvmStatic
fun createIntent(context: Context): Intent = Intent(context, StickerManagementActivityV2::class.java)
}
private val viewModel by viewModel { StickerManagementViewModelV2() }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
setContent {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
SignalTheme {
StickerManagementScreen(
uiState = uiState,
onNavigateBack = ::supportFinishAfterTransition
)
}
}
}
}
private data class Page(
val title: String,
val getContent: @Composable () -> Unit
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StickerManagementScreen(
uiState: StickerManagementUiState,
onNavigateBack: () -> Unit = {},
modifier: Modifier = Modifier
) {
Scaffold(
topBar = { TopAppBar(onBackPress = onNavigateBack) }
) { padding ->
val pages = listOf(
Page(
title = stringResource(R.string.StickerManagement_available_tab_label),
getContent = { AvailableStickersContent(uiState.availablePacks) }
),
Page(
title = stringResource(R.string.StickerManagement_installed_tab_label),
getContent = { InstalledStickersContent(uiState.installedPacks) }
)
)
val pagerState = rememberPagerState(pageCount = { pages.size })
val coroutineScope = rememberCoroutineScope()
Column(
modifier = modifier.padding(padding)
) {
SecondaryTabRow(
selectedTabIndex = pagerState.currentPage,
indicator = {
TabRowDefaults.SecondaryIndicator(
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.tabIndicatorOffset(pagerState.currentPage)
)
}
) {
repeat(pages.size) { pageIndex ->
PagerTab(
title = pages[pageIndex].title,
selected = pagerState.currentPage == pageIndex,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(pageIndex) } },
modifier = Modifier.weight(1f)
)
}
}
HorizontalPager(
state = pagerState,
beyondViewportPageCount = 1
) { pageIndex ->
pages[pageIndex].getContent()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopAppBar(
onBackPress: () -> 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),
onNavigationClick = onBackPress
)
}
@Composable
private fun PagerTab(
title: String,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Tab(
text = {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
selected = selected,
onClick = onClick,
modifier = modifier
)
}
@Composable
private fun AvailableStickersContent(
packs: List<AvailableStickerPack>
) {
if (packs.isEmpty()) {
EmptyView(text = stringResource(R.string.StickerManagement_available_tab_empty_text))
} else {
// TODO show available stickers list
}
}
@Composable
private fun InstalledStickersContent(
packs: List<InstalledStickerPack>
) {
if (packs.isEmpty()) {
EmptyView(text = stringResource(R.string.StickerManagement_installed_tab_empty_text))
} else {
// TODO show installed stickers list
}
}
@Composable
private fun EmptyView(
text: String
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(align = Alignment.CenterVertically)
)
}
@SignalPreview
@Composable
private fun StickerManagementScreenEmptyStatePreview() {
Previews.Preview {
StickerManagementScreen(
StickerManagementUiState(
availablePacks = emptyList(),
installedPacks = emptyList()
)
)
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.stickers
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.thoughtcrime.securesms.database.model.StickerPackRecord
class StickerManagementViewModelV2 : ViewModel() {
private val _uiState = MutableStateFlow(StickerManagementUiState())
val uiState: StateFlow<StickerManagementUiState> = _uiState.asStateFlow()
}
data class StickerManagementUiState(
val availablePacks: List<AvailableStickerPack> = emptyList(),
val installedPacks: List<InstalledStickerPack> = emptyList(),
val isMultiSelectMode: Boolean = false
)
data class AvailableStickerPack(
val record: StickerPackRecord,
val isBlessed: Boolean,
val downloadStatus: DownloadStatus
) {
sealed class DownloadStatus {
data object NotDownloaded : DownloadStatus()
data class InProgress(val progressPercent: Double) : DownloadStatus()
data object Downloaded : DownloadStatus()
}
}
data class InstalledStickerPack(
private val record: StickerPackRecord,
val sortOrder: Int,
val isSelected: Boolean
)