feat(emoji): modernize pack metadata screen (hero image, inline upload)
Replaces the plain stacked-form EmojiPackMetadataScreen with the badge- definition layout: a large square hero preview at the top, Name and Description fields below, and a single Create/Save action. Picking an image now launches the gallery directly (no URL paste step). If the user submits with a freshly picked local image, the ViewModel uploads to the account's default file server first, then publishes the EmojiPackEvent with the uploaded URL in `image`. Existing remote URLs keep rendering as the hero until replaced. The signer contract and ownedEmojiPacks create/update calls are unchanged.
This commit is contained in:
+175
-81
@@ -20,38 +20,59 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
|
||||
@Composable
|
||||
fun EmojiPackMetadataScreen(
|
||||
@@ -82,34 +103,65 @@ private fun EmojiPackMetadataScaffold(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
var wantsToPickImage by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToPickImage) {
|
||||
GallerySelectSingle(
|
||||
onImageUri = { media ->
|
||||
wantsToPickImage = false
|
||||
if (media != null) {
|
||||
viewModel.pickMedia(media)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val onSubmit: () -> Unit = {
|
||||
viewModel.submit(
|
||||
context = context,
|
||||
onSuccess = { nav.popBack() },
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
EmojiPackMetadataTopBar(
|
||||
viewModel = viewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onSubmit = onSubmit,
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 10.dp,
|
||||
end = 10.dp,
|
||||
top = pad.calculateTopPadding(),
|
||||
bottom = pad.calculateBottomPadding(),
|
||||
).consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
item {
|
||||
PackName(viewModel)
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
PackImagePicker(
|
||||
viewModel = viewModel,
|
||||
onPickImage = { wantsToPickImage = true },
|
||||
)
|
||||
|
||||
PackImage(viewModel, accountViewModel)
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
PackDescription(viewModel)
|
||||
PackFormFields(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,8 +170,8 @@ private fun EmojiPackMetadataScaffold(
|
||||
@Composable
|
||||
private fun EmojiPackMetadataTopBar(
|
||||
viewModel: EmojiPackMetadataViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onSubmit: () -> Unit,
|
||||
) {
|
||||
if (viewModel.isNewPack) {
|
||||
CreatingTopBar(
|
||||
@@ -129,17 +181,7 @@ private fun EmojiPackMetadataTopBar(
|
||||
viewModel.clear()
|
||||
nav.popBack()
|
||||
},
|
||||
onPost = {
|
||||
try {
|
||||
viewModel.createOrUpdate()
|
||||
nav.popBack()
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||
)
|
||||
}
|
||||
},
|
||||
onPost = onSubmit,
|
||||
)
|
||||
} else {
|
||||
SavingTopBar(
|
||||
@@ -149,83 +191,135 @@ private fun EmojiPackMetadataTopBar(
|
||||
viewModel.clear()
|
||||
nav.popBack()
|
||||
},
|
||||
onPost = {
|
||||
try {
|
||||
viewModel.createOrUpdate()
|
||||
nav.popBack()
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||
)
|
||||
}
|
||||
},
|
||||
onPost = onSubmit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PackName(viewModel: EmojiPackMetadataViewModel) {
|
||||
private fun PackImagePicker(
|
||||
viewModel: EmojiPackMetadataViewModel,
|
||||
onPickImage: () -> Unit,
|
||||
) {
|
||||
val picked = viewModel.pickedMedia
|
||||
val currentUrl = viewModel.picture.value.text
|
||||
|
||||
when {
|
||||
picked != null -> {
|
||||
HeroImagePreview(
|
||||
model = picked.uri,
|
||||
onClick = onPickImage,
|
||||
)
|
||||
}
|
||||
|
||||
currentUrl.isNotBlank() -> {
|
||||
HeroImagePreview(
|
||||
model = currentUrl,
|
||||
onClick = onPickImage,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
UploadPlaceholder(onClick = onPickImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroImagePreview(
|
||||
model: Any,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = stringRes(R.string.emoji_pack_image_label),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = onClick),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UploadPlaceholder(onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
).clickable(onClick = onClick)
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddPhotoAlternate,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(56.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.emoji_pack_upload_image_cta),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.emoji_pack_upload_image_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PackFormFields(viewModel: EmojiPackMetadataViewModel) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.emoji_pack_name_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = viewModel.name.value,
|
||||
onValueChange = { viewModel.name.value = it },
|
||||
label = { Text(text = stringRes(R.string.emoji_pack_name_label)) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.emoji_pack_name_label),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PackImage(
|
||||
viewModel: EmojiPackMetadataViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.emoji_pack_image_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = viewModel.picture.value,
|
||||
onValueChange = { viewModel.picture.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "https://example.com/cover.jpg",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
val context = LocalContext.current
|
||||
SelectSingleFromGallery(
|
||||
isUploading = viewModel.isUploadingImageForPicture,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 2.dp),
|
||||
) {
|
||||
viewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
@Composable
|
||||
private fun PackDescription(viewModel: EmojiPackMetadataViewModel) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.emoji_pack_description_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = viewModel.description.value,
|
||||
onValueChange = { viewModel.description.value = it },
|
||||
label = { Text(text = stringRes(R.string.emoji_pack_description_label)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
minLines = 2,
|
||||
maxLines = 6,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
minLines = 3,
|
||||
)
|
||||
}
|
||||
|
||||
+119
-49
@@ -59,12 +59,25 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
val picture = mutableStateOf(TextFieldValue())
|
||||
val description = mutableStateOf(TextFieldValue())
|
||||
|
||||
var isUploadingImageForPicture by mutableStateOf(false)
|
||||
/**
|
||||
* Local image the user just picked from the gallery but hasn't uploaded yet.
|
||||
* When non-null the hero preview shows this file and `submit()` will upload
|
||||
* it before publishing the emoji pack event. Mutated only via [pickMedia] /
|
||||
* [clearPickedMedia] so the setter name doesn't collide on the JVM.
|
||||
*/
|
||||
var pickedMedia by mutableStateOf<SelectedMedia?>(null)
|
||||
private set
|
||||
|
||||
/** True while upload-then-publish is running. Disables the submit button and shows a spinner. */
|
||||
var isWorking by mutableStateOf(false)
|
||||
|
||||
val canPost by derivedStateOf {
|
||||
name.value.text.isNotBlank()
|
||||
!isWorking && name.value.text.isNotBlank()
|
||||
}
|
||||
|
||||
/** True when either a remote cover URL exists OR the user has picked a local image. */
|
||||
fun hasImage(): Boolean = pickedMedia != null || picture.value.text.isNotBlank()
|
||||
|
||||
fun init(accountViewModel: AccountViewModel) {
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
@@ -81,25 +94,90 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
name.value = TextFieldValue(existing?.title ?: "")
|
||||
picture.value = TextFieldValue(existing?.image ?: "")
|
||||
description.value = TextFieldValue(existing?.description ?: "")
|
||||
pickedMedia = null
|
||||
}
|
||||
|
||||
fun pickMedia(media: SelectedMedia) {
|
||||
pickedMedia = media
|
||||
}
|
||||
|
||||
fun clearPickedMedia() {
|
||||
pickedMedia = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off the full create/update flow:
|
||||
* 1. If a local image was picked, upload it first and update `picture`.
|
||||
* 2. Build & sign the EmojiPackEvent with the (possibly newly uploaded) URL.
|
||||
*
|
||||
* Mirrors the badge-definition flow where the user never sees the URL and the
|
||||
* image upload is implicit in pressing "Create" / "Save".
|
||||
*/
|
||||
fun submit(
|
||||
context: Context,
|
||||
onSuccess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
if (isWorking) return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
isWorking = true
|
||||
try {
|
||||
val local = pickedMedia
|
||||
if (local != null) {
|
||||
val uploadedUrl = uploadImage(local, context, onError)
|
||||
if (uploadedUrl == null) {
|
||||
isWorking = false
|
||||
return@launch
|
||||
}
|
||||
picture.value = TextFieldValue(uploadedUrl)
|
||||
pickedMedia = null
|
||||
}
|
||||
|
||||
try {
|
||||
publish()
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
isWorking = false
|
||||
return@launch
|
||||
}
|
||||
clear()
|
||||
onSuccess()
|
||||
} finally {
|
||||
isWorking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun publish() {
|
||||
val currentPack = pack
|
||||
if (currentPack == null) {
|
||||
account.createOwnedEmojiPack(
|
||||
title = name.value.text,
|
||||
description = description.value.text,
|
||||
image = picture.value.text,
|
||||
)
|
||||
} else {
|
||||
account.updateOwnedEmojiPackMetadata(
|
||||
dTag = currentPack.identifier,
|
||||
newTitle = name.value.text,
|
||||
newDescription = description.value.text,
|
||||
newImage = picture.value.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retained for backward compatibility with the old "paste URL + upload button"
|
||||
* flow. New UI goes through [submit]. The signer contract is unchanged: the
|
||||
* final signed EmojiPackEvent still carries the published URL in `image`.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun createOrUpdate() {
|
||||
accountViewModel.launchSigner {
|
||||
val currentPack = pack
|
||||
if (currentPack == null) {
|
||||
account.createOwnedEmojiPack(
|
||||
title = name.value.text,
|
||||
description = description.value.text,
|
||||
image = picture.value.text,
|
||||
)
|
||||
} else {
|
||||
account.updateOwnedEmojiPackMetadata(
|
||||
dTag = currentPack.identifier,
|
||||
newTitle = name.value.text,
|
||||
newDescription = description.value.text,
|
||||
newImage = picture.value.text,
|
||||
)
|
||||
}
|
||||
publish()
|
||||
clear()
|
||||
}
|
||||
}
|
||||
@@ -108,33 +186,21 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
name.value = TextFieldValue()
|
||||
picture.value = TextFieldValue()
|
||||
description.value = TextFieldValue()
|
||||
pickedMedia = null
|
||||
}
|
||||
|
||||
fun uploadForPicture(
|
||||
uri: SelectedMedia,
|
||||
context: Context,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
upload(
|
||||
uri,
|
||||
context,
|
||||
onUploading = { isUploadingImageForPicture = it },
|
||||
onUploaded = { picture.value = TextFieldValue(it) },
|
||||
onError = onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun upload(
|
||||
/**
|
||||
* Uploads [galleryUri] using the user's configured default file server,
|
||||
* respecting the account's strip-location-on-upload preference. Returns the
|
||||
* published URL or null on failure (having already called [onError]).
|
||||
*
|
||||
* Mirrors the NIP-96/Blossom block used by `BookmarkGroupMetadataViewModel.upload`.
|
||||
*/
|
||||
private suspend fun uploadImage(
|
||||
galleryUri: SelectedMedia,
|
||||
context: Context,
|
||||
onUploading: (Boolean) -> Unit,
|
||||
onUploaded: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
onUploading(true)
|
||||
|
||||
): String? {
|
||||
val sourceUri =
|
||||
if (account.settings.stripLocationOnUpload) {
|
||||
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||
@@ -143,8 +209,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
stringRes(context, R.string.metadata_strip_failed_title),
|
||||
stringRes(context, R.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
onUploading(false)
|
||||
return
|
||||
return null
|
||||
}
|
||||
result.uri
|
||||
} else {
|
||||
@@ -152,7 +217,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
}
|
||||
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||
|
||||
try {
|
||||
return try {
|
||||
val result =
|
||||
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||
Nip96Uploader().upload(
|
||||
@@ -182,19 +247,24 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
if (result.url != null) {
|
||||
onUploading(false)
|
||||
onUploaded(result.url)
|
||||
result.url
|
||||
} else {
|
||||
onUploading(false)
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||
onError(
|
||||
stringRes(context, R.string.failed_to_upload_media_no_details),
|
||||
stringRes(context, R.string.server_did_not_provide_a_url_after_uploading),
|
||||
)
|
||||
null
|
||||
}
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onUploading(false)
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||
onError(
|
||||
stringRes(context, R.string.failed_to_upload_media_no_details),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload),
|
||||
)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onUploading(false)
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2387,6 +2387,8 @@
|
||||
<string name="emoji_pack_name_label">Pack name</string>
|
||||
<string name="emoji_pack_description_label">Description (optional)</string>
|
||||
<string name="emoji_pack_image_label">Cover image URL (optional)</string>
|
||||
<string name="emoji_pack_upload_image_cta">Upload a cover image</string>
|
||||
<string name="emoji_pack_upload_image_hint">Pick a square image to represent this emoji pack.</string>
|
||||
<string name="no_emoji_packs">You don\'t have any emoji packs yet</string>
|
||||
<string name="emoji_pack_count">%1$d emojis</string>
|
||||
<string name="my_emoji_list_title">My Emoji List</string>
|
||||
|
||||
Reference in New Issue
Block a user