Migrate symbols.

This commit is contained in:
Alex Hart
2026-09-23 16:12:22 -04:00
committed by Michelle Tang
parent 827494ac8c
commit ba2a56dd72
21 changed files with 271 additions and 46 deletions
@@ -540,7 +540,7 @@ private fun CallParticipantRow(
}
Icon(
painter = painterResource(id = R.drawable.symbol_minus_circle_24),
painter = SignalIcons.MinusCircle.painter,
contentDescription = null,
modifier = Modifier
.clickable(onClick = onBlockClicked)
@@ -33,6 +33,7 @@ import org.signal.core.ui.compose.AllNightPreviews
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
@@ -119,7 +120,7 @@ private fun ParticipantActionsSheetContent(
Rows.TextRow(
text = stringResource(id = R.string.CallParticipantSheet__remove_from_call),
icon = painterResource(id = R.drawable.symbol_minus_circle_24),
icon = SignalIcons.MinusCircle.painter,
onClick = {
onRemoveFromCall(callParticipant)
onDismiss()
@@ -69,6 +69,7 @@ import kotlinx.coroutines.launch
import org.signal.core.ui.compose.AllNightPreviews
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.TriggerAlignedPopupState
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.DimensionUnit
@@ -660,7 +661,7 @@ private fun ParticipantContextMenu(
if (isSelfAdmin && isCallLink) {
DropdownMenuItem(
text = { Text(stringResource(R.string.CallParticipantSheet__remove_from_call)) },
leadingIcon = { Icon(painter = painterResource(R.drawable.symbol_minus_circle_24), contentDescription = null) },
leadingIcon = { Icon(painter = SignalIcons.MinusCircle.painter, contentDescription = null) },
onClick = {
onRemoveFromCall(resolved)
onDismiss()
@@ -428,7 +428,7 @@ object ContactSearchModels {
private fun getGroupStoryContextMenuActions(model: StoryModel, callbacks: ContactSearchAdapter.StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.symbol_minus_circle_24, context.getString(R.string.ContactSearchItems__remove_story)) {
ActionItem(CoreUiR.drawable.symbol_minus_circle_24, context.getString(R.string.ContactSearchItems__remove_story)) {
callbacks.onRemoveGroupStory(model.story, model.isSelected)
}
)
@@ -170,7 +170,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
if (canCopy) {
items.add(ActionItem(CoreUiR.drawable.symbol_copy_android_24, resources.getString(R.string.conversation_selection__menu_copy), action = { handleCopyMessage(message) }))
}
items.add(ActionItem(R.drawable.symbol_send_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_send_now), action = { handleSendMessageNow(message.messageRecord) }))
items.add(ActionItem(CoreUiR.drawable.symbol_send_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_send_now), action = { handleSendMessageNow(message.messageRecord) }))
items.add(ActionItem(R.drawable.symbol_calendar_24, resources.getString(R.string.ScheduledMessagesBottomSheet_menu_reschedule), action = { handleRescheduleMessage(message.messageRecord) }))
return items
}
@@ -354,11 +354,14 @@ import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stickers.StickerEventListener
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent
import org.thoughtcrime.securesms.stickers.StickerUrl
import org.thoughtcrime.securesms.stickers.manage.StickerManagementRepository
import org.thoughtcrime.securesms.stickers.manage.StickerManagementScreen
import org.thoughtcrime.securesms.stickers.preview.StickerPackPreviewActivityV2
import org.thoughtcrime.securesms.stories.StoryViewerArgs
@@ -714,6 +717,38 @@ class ConversationFragment :
startActivity(StickerPackPreviewActivityV2.createIntent(StickerPackId(action.packId), StickerPackKey(action.packKey)))
}
// Sending a pack means sending its link, so it goes through the same forward sheet as a
// message. A child of this fragment, as with sticker search.
is MediaKeyboardAction.SendStickerPackClicked -> {
container.onHostWindowShown()
MultiselectForwardFragment.showBottomSheet(
supportFragmentManager = childFragmentManager,
multiselectForwardFragmentArgs = MultiselectForwardFragmentArgs(
multiShareArgs = listOf(
MultiShareArgs.Builder()
.withDraftText(StickerUrl.createShareLink(action.packId, action.packKey))
.build()
),
title = R.string.StickerManagement_share_sheet_title
)
)
}
is MediaKeyboardAction.RemoveStickerPackClicked -> {
container.onHostWindowShown()
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.StickerManagement_delete_n_packs_confirmation, 1, 1))
.setMessage(resources.getQuantityString(R.plurals.StickerManagement_delete_n_packs_confirmation_body, 1, 1))
.setPositiveButton(R.string.StickerManagement_menu_remove_pack) { _, _ ->
viewLifecycleOwner.lifecycleScope.launch {
StickerManagementRepository.uninstallStickerPacks(mapOf(StickerPackId(action.packId) to StickerPackKey(action.packKey)))
}
}
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener { container.onHostWindowHidden() }
.show()
}
// A screen of its own rather than a window over this one, and picking a gif carries on into
// media send, so the keyboard has no reason to still be up on the way back.
MediaKeyboardAction.GifSearchClicked -> {
@@ -381,6 +381,17 @@ class StickerTables(
notifyStickerPackListeners()
}
fun clearRecentlyUsedStickers() {
writableDatabase
.update(Sticker.TABLE_NAME)
.values(Sticker.LAST_USED to 0)
.where("${Sticker.LAST_USED} > 0 AND ${Sticker.COVER} = 0")
.run()
notifyStickerListeners()
notifyStickerPackListeners()
}
fun markPackAsInstalled(packId: String, notify: Boolean) {
val transitioned = updatePackInstalled(
db = databaseHelper.signalWritableDatabase,
@@ -62,6 +62,12 @@ class SignalStickerKeyboardRepository(private val context: Context) : StickerKey
}
}
override fun clearRecentStickers() {
SignalExecutors.BOUNDED_IO.execute {
SignalDatabase.stickers.clearRecentlyUsedStickers()
}
}
private fun loadStickerPacks(): List<KeyboardStickerPack> {
val stickerTable = SignalDatabase.stickers
@@ -74,6 +80,7 @@ class SignalStickerKeyboardRepository(private val context: Context) : StickerKey
KeyboardStickerPack(
id = pack.packId,
packKey = pack.packKey,
title = pack.title.nullIfBlank(),
cover = DecryptableUri(pack.cover.uri),
stickers = stickers
@@ -87,6 +94,7 @@ class SignalStickerKeyboardRepository(private val context: Context) : StickerKey
val recentPack = KeyboardStickerPack(
id = StickerKeyboardRepository.RECENT_PACK_ID,
packKey = null,
title = context.getString(R.string.StickerKeyboard__recently_used),
cover = null,
stickers = recentStickers.map { it.toKeyboardSticker() }
@@ -243,7 +243,7 @@ private fun StickerPackPreviewScreen(
if (loadedState.isPackInstalled) {
DropdownMenus.ItemWithIcon(
menuController = menuController,
imageVector = ImageVector.vectorResource(R.drawable.symbol_minus_circle_24),
imageVector = SignalIcons.MinusCircle.imageVector,
stringResId = R.string.StickerManagement_menu_remove_pack,
onClick = { onEvent(StickerPackPreviewEvent.UninstallClicked) }
)
@@ -511,7 +511,7 @@ private fun StickerOptions(
Buttons.ActionButton(
onClick = { onEvent(StickerPackPreviewEvent.UninstallClicked) },
imageVector = ImageVector.vectorResource(R.drawable.symbol_minus_circle_24),
imageVector = SignalIcons.MinusCircle.imageVector,
label = stringResource(R.string.StickerManagement_menu_remove_pack)
)
}
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.5 12.88c0.48 0 0.88-0.4 0.88-0.88s-0.4-0.88-0.88-0.88h-9c-0.48 0-0.88 0.4-0.88 0.88s0.4 0.88 0.88 0.88h9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12 1.13C6 1.13 1.12 5.99 1.12 12 1.13 18 6 22.88 12 22.88c6 0 10.88-4.87 10.88-10.88C22.88 6 18 1.12 12 1.12ZM2.87 12c0-5.04 4.09-9.13 9.13-9.13 5.04 0 9.13 4.09 9.13 9.13 0 5.04-4.09 9.13-9.13 9.13-5.04 0-9.13-4.09-9.13-9.13Z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.16,2.807a2.125,2.125 0,0 0,-3.035 1.92v14.547a2.125,2.125 0,0 0,3.036 1.92l15.333,-7.274c1.62,-0.768 1.62,-3.072 0,-3.84L6.161,2.807ZM4.875,4.727c0,-0.276 0.287,-0.457 0.536,-0.34l15.333,7.274a0.375,0.375 0,0 1,0 0.678L5.411,19.612a0.375,0.375 0,0 1,-0.536 -0.338v-4.972l7.493,-1.816a0.5,0.5 0,0 0,0 -0.972L4.875,9.698L4.875,4.726Z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -80,6 +80,7 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
LockFill(icon(R.drawable.symbol_lock_fill_24)),
Maximize(icon(R.drawable.symbol_maximize_24)),
Mic(icon(R.drawable.symbol_mic_24)),
MinusCircle(icon(R.drawable.symbol_minus_circle_24)),
MobileNextDisplay(icon(R.drawable.symbol_mobile_next_display_48)),
MoreVertical(icon(R.drawable.symbol_more_vertical_24)),
Nighttime(icon(R.drawable.ic_nighttime_26)),
@@ -100,6 +101,7 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
Refresh(icon(R.drawable.symbol_refresh_24)),
Save(icon(R.drawable.symbol_save_android_24)),
Search(icon(R.drawable.symbol_search_24)),
Send(icon(R.drawable.symbol_send_24)),
SendFill(icon(R.drawable.symbol_send_fill_24)),
Settings(icon(R.drawable.symbol_settings_android_24)),
Share(icon(R.drawable.symbol_share_android_24)),
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12 21.1c-5.02 0-9.1-4.08-9.1-9.1 0-5.03 4.08-9.1 9.1-9.1 5.03 0 9.1 4.07 9.1 9.1 0 5.02-4.07 9.1-9.1 9.1m0 1.8c6.03 0 10.9-4.88 10.9-10.9 0-6.03-4.87-10.9-10.9-10.9C5.98 1.1 1.1 5.97 1.1 12c0 6.02 4.88 10.9 10.9 10.9m-4.5-10h9c0.5 0 0.9-0.4 0.9-0.9s-0.4-0.9-0.9-0.9h-9c-0.5 0-0.9 0.4-0.9 0.9s0.4 0.9 0.9 0.9m0 0"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5.71 21.3l16.5-7.81c1.24-0.58 1.24-2.4 0-2.98L5.71 2.7c-1.1-0.53-2.36 0.27-2.36 1.49v5.57c0 0.61 0.4 1.16 1.01 1.33 1.3 0.35 2.53 0.58 5.04 0.86v0.1c-2.51 0.27-3.74 0.51-5.04 0.86-0.61 0.16-1.01 0.71-1.01 1.33v5.57c0 1.23 1.26 2.01 2.36 1.49m-0.56-1.73v-5.01l8.05-2.17c0.6-0.17 0.6-0.62 0-0.78L5.15 9.44V4.43l16 7.57Zm0 0"/>
</vector>
@@ -57,4 +57,20 @@ sealed interface MediaKeyboardAction {
* @param packKey Its key, which identifies the pack alongside [packId].
*/
data class ViewStickerPackClicked(val packId: String, val packKey: String) : MediaKeyboardAction
/**
* Let the user send a sticker pack to someone, as a link.
*
* @param packId The pack to send.
* @param packKey Its key, which identifies the pack alongside [packId].
*/
data class SendStickerPackClicked(val packId: String, val packKey: String) : MediaKeyboardAction
/**
* Uninstall a sticker pack, confirming with the user first.
*
* @param packId The pack to remove.
* @param packKey Its key, which identifies the pack alongside [packId].
*/
data class RemoveStickerPackClicked(val packId: String, val packKey: String) : MediaKeyboardAction
}
@@ -30,6 +30,11 @@ interface StickerKeyboardRepository {
*/
fun onStickerUsed(sticker: KeyboardSticker) = Unit
/**
* Forget every recently-used sticker, emptying the synthetic [RECENT_PACK_ID] pack.
*/
fun clearRecentStickers() = Unit
companion object {
const val RECENT_PACK_ID = "media-keyboard-recents"
}
@@ -53,10 +58,13 @@ data class KeyboardSticker(
)
/**
* @param packKey The pack's key, which identifies it alongside [id] wherever it is opened or
* forwarded. Null for the synthetic recents pack, which is not a real pack.
* @param cover A Glide-loadable model for the pack's cover image.
*/
data class KeyboardStickerPack(
val id: String,
val packKey: String?,
val title: String?,
val cover: Any?,
val stickers: List<KeyboardSticker>
@@ -8,6 +8,8 @@ package org.signal.mediakeyboard.screens.sticker
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -36,9 +38,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.SignalPreviewWrapper
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.glide.compose.GlideImage
import org.signal.mediakeyboard.R
@@ -190,18 +196,7 @@ private fun StickerGrid(
state.packs.forEach { pack ->
item(key = "header:${pack.id}", span = { GridItemSpan(maxLineSpan) }) {
val title = if (pack.id == StickerKeyboardRepository.RECENT_PACK_ID) {
stringResource(R.string.MediaKeyboard__recently_used)
} else {
pack.title.orEmpty()
}
Text(
text = title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 4.dp)
)
StickerPackHeader(pack = pack, onEvent = onEvent)
}
pack.stickers.forEachIndexed { index, sticker ->
@@ -217,6 +212,137 @@ private fun StickerGrid(
}
}
@Composable
private fun StickerPackHeader(
pack: KeyboardStickerPack,
onEvent: (StickerPageScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
val isRecents = pack.id == StickerKeyboardRepository.RECENT_PACK_ID
val menuController = remember { DropdownMenus.MenuController() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 4.dp, top = 12.dp, bottom = 4.dp)
) {
Text(
text = if (isRecents) stringResource(R.string.MediaKeyboard__recently_used) else pack.title.orEmpty(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Box {
IconButton(
onClick = { menuController.show() },
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = SignalIcons.MoreVertical.imageVector,
contentDescription = stringResource(R.string.MediaKeyboard__more_options),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
StickerPackHeaderMenu(
pack = pack,
isRecents = isRecents,
menuController = menuController,
onEvent = onEvent
)
}
}
}
@Composable
private fun StickerPackHeaderMenu(
pack: KeyboardStickerPack,
isRecents: Boolean,
menuController: DropdownMenus.MenuController,
onEvent: (StickerPageScreenEvents) -> Unit
) {
DropdownMenus.Menu(
controller = menuController,
offsetX = 0.dp
) {
if (isRecents) {
DropdownMenus.ItemWithIcon(
menuController = menuController,
imageVector = SignalIcons.Trash.imageVector,
stringResId = R.string.MediaKeyboard__clear_recents,
onClick = {
onEvent(StickerPageScreenEvents.ClearRecentStickersClicked)
}
)
return@Menu
}
// Everything below acts on the pack itself, which needs the key the recents pack does not have.
val packKey = pack.packKey ?: return@Menu
DropdownMenus.ItemWithIcon(
menuController = menuController,
imageVector = SignalIcons.Send.imageVector,
stringResId = R.string.MediaKeyboard__send,
onClick = {
onEvent(StickerPageScreenEvents.SendStickerPackClicked(pack.id, packKey))
}
)
DropdownMenus.ItemWithIcon(
menuController = menuController,
imageVector = SignalIcons.StickerPack.imageVector,
stringResId = R.string.MediaKeyboard__view_pack,
onClick = {
onEvent(StickerPageScreenEvents.ViewStickerPackClicked(pack.id, packKey))
}
)
DropdownMenus.ItemWithIcon(
menuController = menuController,
imageVector = SignalIcons.MinusCircle.imageVector,
stringResId = R.string.MediaKeyboard__remove_pack,
onClick = {
onEvent(StickerPageScreenEvents.RemoveStickerPackClicked(pack.id, packKey))
}
)
}
}
@PreviewWrapper(SignalPreviewWrapper::class)
@DayNightPreviews
@Composable
private fun StickerPackHeaderPreview() {
Column {
StickerPackHeader(
pack = KeyboardStickerPack(
id = StickerKeyboardRepository.RECENT_PACK_ID,
packKey = null,
title = null,
cover = null,
stickers = emptyList()
),
onEvent = {}
)
StickerPackHeader(
pack = KeyboardStickerPack(
id = "pack-1",
packKey = "pack-1-key",
title = "Bandit the Cat",
cover = null,
stickers = emptyList()
),
onEvent = {}
)
}
}
@Composable
private fun StickerCell(
sticker: KeyboardSticker,
@@ -252,7 +378,7 @@ private fun StickerCell(
) {
DropdownMenus.ItemWithIcon(
menuController = controller,
imageVector = SignalIcons.SendFill.imageVector,
imageVector = SignalIcons.Send.imageVector,
stringResId = R.string.MediaKeyboard__send,
onClick = {
onEvent(StickerPageScreenEvents.StickerClicked(sticker))
@@ -17,4 +17,7 @@ sealed interface StickerPageScreenEvents {
data class StickerClicked(val sticker: KeyboardSticker) : StickerPageScreenEvents
data object SearchClicked : StickerPageScreenEvents
data class ViewStickerPackClicked(val packId: String, val packKey: String) : StickerPageScreenEvents
data class SendStickerPackClicked(val packId: String, val packKey: String) : StickerPageScreenEvents
data class RemoveStickerPackClicked(val packId: String, val packKey: String) : StickerPageScreenEvents
data object ClearRecentStickersClicked : StickerPageScreenEvents
}
@@ -83,6 +83,18 @@ class StickerPageViewModel(
is StickerPageScreenEvents.ViewStickerPackClicked -> {
onAction(MediaKeyboardAction.ViewStickerPackClicked(event.packId, event.packKey))
}
is StickerPageScreenEvents.SendStickerPackClicked -> {
onAction(MediaKeyboardAction.SendStickerPackClicked(event.packId, event.packKey))
}
is StickerPageScreenEvents.RemoveStickerPackClicked -> {
onAction(MediaKeyboardAction.RemoveStickerPackClicked(event.packId, event.packKey))
}
is StickerPageScreenEvents.ClearRecentStickersClicked -> {
repository.clearRecentStickers()
}
}
}
@@ -55,10 +55,16 @@
<!-- Sticker page -->
<!-- Accessibility description for the button in the media keyboard top bar that opens the screen where sticker packs are managed. -->
<string name="MediaKeyboard__manage_stickers">Manage stickers</string>
<!-- Menu option on a long-pressed sticker that sends that sticker. Verb, imperative. -->
<!-- Menu option that sends a sticker, on a long-pressed sticker, or the whole pack as a link, in a sticker pack header's menu. Verb, imperative. -->
<string name="MediaKeyboard__send">Send</string>
<!-- Menu option on a long-pressed sticker that opens the pack the sticker belongs to. Verb phrase, imperative. -->
<!-- Menu option, on a long-pressed sticker and in a sticker pack header's menu, that opens the pack the sticker belongs to. Verb phrase, imperative. -->
<string name="MediaKeyboard__view_pack">View pack</string>
<!-- Accessibility description for the three-dot button at the end of a sticker pack header, which opens that pack's menu. -->
<string name="MediaKeyboard__more_options">More options</string>
<!-- Menu option in a sticker pack header's menu that uninstalls the pack. Verb, imperative. -->
<string name="MediaKeyboard__remove_pack">Remove</string>
<!-- Menu option in the recently used header's menu that forgets every recently used sticker. Verb phrase, imperative. -->
<string name="MediaKeyboard__clear_recents">Clear recents</string>
<!-- Gif page -->
<!-- Label of the GIF quick search chip that shows the GIFs trending right now. -->
@@ -31,8 +31,8 @@ class StickerPageViewModelTest {
private val sticker = KeyboardSticker(packId = "pack-1", packKey = "pack-1-key", stickerId = 1, emoji = "😀", image = "image-1")
private val packs = listOf(
KeyboardStickerPack(id = "pack-1", title = "Pack One", cover = null, stickers = listOf(sticker)),
KeyboardStickerPack(id = "pack-2", title = "Pack Two", cover = null, stickers = emptyList())
KeyboardStickerPack(id = "pack-1", packKey = "pack-1-key", title = "Pack One", cover = null, stickers = listOf(sticker)),
KeyboardStickerPack(id = "pack-2", packKey = "pack-2-key", title = "Pack Two", cover = null, stickers = emptyList())
)
private lateinit var repository: StickerKeyboardRepository