diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt index ac33fb0dd..74333236f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt @@ -30,11 +30,10 @@ data class LabeledBookmarkList( val identifier: String, val title: String, val description: String?, + val image: String?, val privateBookmarks: Set = emptySet(), val publicBookmarks: Set = emptySet(), ) { - // TODO: Add methods for LInk and Hashtag, after their implementation. - val privatePostBookmarks = privateBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark } val publicPostBookmarks = publicBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 6d980d63a..e753fc751 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -30,9 +30,13 @@ import com.vitorpamplona.amethyst.model.events import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.amethyst.model.updateFlow import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.name import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -85,6 +89,7 @@ class LabeledBookmarkListsState( identifier = dTag(), title = nameOrTitle() ?: dTag(), description = description(), + image = image(), privateBookmarks = privateBookmarks(signer)?.toSet() ?: emptySet(), publicBookmarks = publicBookmarks().toSet(), ) @@ -103,6 +108,8 @@ class LabeledBookmarkListsState( it.identifier == bookmarkListId } + fun getBookmarkList(dTag: String) = listFeedFlow.value.getList(bookmarkListId = dTag) + fun DeletionEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(LabeledBookmarkListEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value) fun hasItemInNoteList(notes: Set): Boolean = @@ -146,6 +153,7 @@ class LabeledBookmarkListsState( suspend fun addLabeledBookmarkList( listName: String, listDescription: String? = null, + listImage: String? = null, firstBookmark: BookmarkIdTag? = null, isBookmarkPrivate: Boolean = false, account: Account, @@ -154,6 +162,7 @@ class LabeledBookmarkListsState( LabeledBookmarkListEvent.create( name = listName, description = listDescription, + image = listImage, publicBookmarks = if (!isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(), privateBookmarks = if (isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(), signer = account.signer, @@ -161,6 +170,27 @@ class LabeledBookmarkListsState( account.sendMyPublicAndPrivateOutbox(newList) } + suspend fun updateMetadata( + listName: String?, + listDescription: String?, + listImage: String?, + bookmarkList: LabeledBookmarkList, + account: Account, + ) { + val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) + + val template = + listEvent.update { + if (listName != null) name(listName) + if (listDescription != null) description(listDescription) + if (listImage != null) image(listImage) + } + + val newList = signer.sign(template) + + account.sendMyPublicAndPrivateOutbox(newList) + } + suspend fun renameBookmarkList( newName: String, bookmarkList: LabeledBookmarkList, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt new file mode 100644 index 000000000..7025a70ae --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery +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.screen.loggedIn.relays.SettingsCategory +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions + +@Composable +fun BookmarkGroupMetadataScreen( + bookmarkGroupIdentifier: String?, + accountViewModel: AccountViewModel, + nav: INav, +) { + val bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel = viewModel() + bookmarkGroupInfoViewModel.init(accountViewModel) + + if (bookmarkGroupIdentifier != null) { + LaunchedEffect(bookmarkGroupInfoViewModel) { + bookmarkGroupInfoViewModel.load(bookmarkGroupIdentifier) + } + } else { + LaunchedEffect(bookmarkGroupInfoViewModel) { + bookmarkGroupInfoViewModel.new() + } + } + + BookmarkGroupMetadataScaffold(bookmarkGroupInfoViewModel, accountViewModel, nav) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun BookmarkGroupMetadataScaffold( + bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + topBar = { + BookmarkGroupMetadataTopBar( + bookmarkGroupInfoViewModel = bookmarkGroupInfoViewModel, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) { pad -> + LazyColumn( + Modifier + .fillMaxSize() + .padding( + start = 10.dp, + end = 10.dp, + top = pad.calculateTopPadding(), + bottom = pad.calculateBottomPadding(), + ).consumeWindowInsets(pad) + .imePadding(), + ) { + item { + SettingsCategory( + R.string.bookmark_list_edit_sub_title, + R.string.bookmark_list_explainer, + SettingsCategoryFirstModifier, + ) + + ListName(bookmarkGroupInfoViewModel) + + Spacer(modifier = DoubleVertSpacer) + + Picture(bookmarkGroupInfoViewModel, accountViewModel) + + Spacer(modifier = DoubleVertSpacer) + + Description(bookmarkGroupInfoViewModel) + } + } + } +} + +@Composable +fun BookmarkGroupMetadataTopBar( + bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (bookmarkGroupInfoViewModel.isNewList) { + CreatingTopBar( + titleRes = R.string.bookmark_list_creation_screen_title, + isActive = bookmarkGroupInfoViewModel::canPost, + onCancel = { + bookmarkGroupInfoViewModel.clear() + nav.popBack() + }, + onPost = { + try { + bookmarkGroupInfoViewModel.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, + ) + } + }, + ) + } else { + SavingTopBar( + titleRes = R.string.follow_set_edit_list_metadata, + isActive = bookmarkGroupInfoViewModel::canPost, + onCancel = { + bookmarkGroupInfoViewModel.clear() + nav.popBack() + }, + onPost = { + try { + bookmarkGroupInfoViewModel.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, + ) + } + }, + ) + } +} + +@Composable +private fun Description(bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.follow_set_creation_desc_label)) }, + modifier = Modifier.fillMaxWidth(), + value = bookmarkGroupInfoViewModel.description.value, + onValueChange = { bookmarkGroupInfoViewModel.description.value = it }, + placeholder = { + Text( + text = stringRes(R.string.about_us), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + minLines = 3, + ) +} + +@Composable +private fun Picture( + bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel, + accountViewModel: AccountViewModel, +) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.optional_picture_url)) }, + modifier = Modifier.fillMaxWidth(), + value = bookmarkGroupInfoViewModel.picture.value, + onValueChange = { bookmarkGroupInfoViewModel.picture.value = it }, + placeholder = { + Text( + text = "http://mygroup.com/logo.jpg", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + leadingIcon = { + val context = LocalContext.current + SelectSingleFromGallery( + isUploading = bookmarkGroupInfoViewModel.isUploadingImageForPicture, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(start = 2.dp), + ) { + bookmarkGroupInfoViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + } + }, + ) +} + +@Composable +private fun ListName(bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.follow_set_creation_name_label)) }, + modifier = Modifier.fillMaxWidth(), + value = bookmarkGroupInfoViewModel.name.value, + onValueChange = { bookmarkGroupInfoViewModel.name.value = it }, + placeholder = { + Text( + text = stringRes(R.string.follow_set_copy_name_label), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt new file mode 100644 index 000000000..9a345e783 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata + +import android.content.Context +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.service.uploads.MediaCompressor +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader +import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +@Stable +class BookmarkGroupMetadataViewModel : ViewModel() { + private lateinit var accountViewModel: AccountViewModel + private lateinit var account: Account + + var bookmarkGroup by mutableStateOf(null) + val isNewList by derivedStateOf { bookmarkGroup == null } + + val name = mutableStateOf(TextFieldValue()) + val picture = mutableStateOf(TextFieldValue()) + val description = mutableStateOf(TextFieldValue()) + + var isUploadingImageForPicture by mutableStateOf(false) + + val canPost by derivedStateOf { + name.value.text.isNotBlank() + } + + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + } + + fun new() { + bookmarkGroup = null + clear() + } + + fun load(dTag: String) { + bookmarkGroup = account.labeledBookmarkLists.getBookmarkList(dTag) + name.value = TextFieldValue(bookmarkGroup?.title ?: "") + picture.value = TextFieldValue(bookmarkGroup?.image ?: "") + description.value = TextFieldValue(bookmarkGroup?.description ?: "") + } + + fun isNewChannel() = bookmarkGroup == null + + fun createOrUpdate() { + accountViewModel.launchSigner { + val bookmarkGroup = bookmarkGroup + if (bookmarkGroup == null) { + accountViewModel.account.labeledBookmarkLists.addLabeledBookmarkList( + listName = name.value.text, + listDescription = description.value.text, + listImage = picture.value.text, + account = accountViewModel.account, + ) + } else { + accountViewModel.account.labeledBookmarkLists.updateMetadata( + listName = name.value.text, + listDescription = description.value.text, + listImage = picture.value.text, + bookmarkList = bookmarkGroup, + account = accountViewModel.account, + ) + } + + clear() + } + } + + fun clear() { + name.value = TextFieldValue() + picture.value = TextFieldValue() + description.value = TextFieldValue() + } + + 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( + galleryUri: SelectedMedia, + context: Context, + onUploading: (Boolean) -> Unit, + onUploaded: (String) -> Unit, + onError: (String, String) -> Unit, + ) { + onUploading(true) + + val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) + + try { + val result = + if (account.settings.defaultFileServer.type == ServerType.NIP96) { + Nip96Uploader().upload( + uri = compResult.uri, + contentType = compResult.contentType, + size = compResult.size, + alt = null, + sensitiveContent = null, + serverBaseUrl = account.settings.defaultFileServer.baseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + onProgress = {}, + httpAuth = account::createHTTPAuthorization, + context = context, + ) + } else { + BlossomUploader().upload( + uri = compResult.uri, + contentType = compResult.contentType, + size = compResult.size, + alt = null, + sensitiveContent = null, + serverBaseUrl = account.settings.defaultFileServer.baseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + httpAuth = account::createBlossomUploadAuth, + context = context, + ) + } + + if (result.url != null) { + onUploading(false) + onUploaded(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)) + } + } 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)) + } 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) + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6282089f3..e09c8d68a 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -117,6 +117,7 @@ Channel Name My Awesome Group Picture Url + Picture Url(Optional) Description Description not found "About us.. " @@ -391,6 +392,8 @@ Bookmark Lists Icon for bookmark list + New Bookmark List + Bookmark List Metadata Clone Bookmark List Broadcast Bookmark List Delete Bookmark List @@ -418,6 +421,7 @@ Add as public bookmark Add as private bookmark Remove from Bookmark List + Bookmark lists metadata can be seen by anyone on Nostr. Only your private members are encrypted. Wallet Connect Service Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt index 7fe79140d..73b5684f6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.nip51Lists.replaceAll import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag import com.vitorpamplona.quartz.nip51Lists.tags.NameTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -75,6 +76,8 @@ class LabeledBookmarkListEvent( fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + fun countBookmarks() = tags.count(BookmarkIdTag::isTagged) fun publicBookmarks(): List = tags.mapNotNull(BookmarkIdTag::parse) @@ -261,6 +264,7 @@ class LabeledBookmarkListEvent( suspend fun create( name: String = "", description: String? = null, + image: String? = null, publicBookmarks: List = emptyList(), privateBookmarks: List = emptyList(), dTag: String = Uuid.random().toString(), @@ -269,7 +273,8 @@ class LabeledBookmarkListEvent( ): LabeledBookmarkListEvent { val template = build(name, publicBookmarks, privateBookmarks, signer, dTag, createdAt) { - if (description != null) addUnique(DescriptionTag.assemble(description)) + if (description != null) description(description) + if (image != null) image(image) } return signer.sign(template) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt index f0f91e669..8f42feca5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt @@ -22,8 +22,14 @@ package com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag import com.vitorpamplona.quartz.nip51Lists.tags.NameTag fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) fun TagArrayBuilder.bookmarks(bookmarks: List) = addAll(bookmarks.map { it.toTagArray() }) + +fun TagArrayBuilder.description(listDescription: String) = addUnique(DescriptionTag.assemble(listDescription)) + +fun TagArrayBuilder.image(url: String) = addUnique(ImageTag.assemble(url))