diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4651bfb3c8..97ffa9d1e9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -895,6 +895,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
+
+
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
+) {
+ 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
+) {
+ 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()
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt
new file mode 100644
index 0000000000..2c636b6e21
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementViewModelV2.kt
@@ -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 = _uiState.asStateFlow()
+}
+
+data class StickerManagementUiState(
+ val availablePacks: List = emptyList(),
+ val installedPacks: List = 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
+)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f7a2ecba67..6957e9b6d9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2781,6 +2781,15 @@
Stickers
+
+ Available
+
+ Installed
+
+ No sticker packs are available
+
+ No sticker packs are installed
+
Installed Stickers
Stickers You Received