Bookmark group metadata edit screen, with VM and string resources. Add methods to LabeledBookmarkListEvent and LabeledBookmarkListsState. Support image urls in LabeledBookmarkList.
This commit is contained in:
+1
-2
@@ -30,11 +30,10 @@ data class LabeledBookmarkList(
|
|||||||
val identifier: String,
|
val identifier: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val image: String?,
|
||||||
val privateBookmarks: Set<BookmarkIdTag> = emptySet(),
|
val privateBookmarks: Set<BookmarkIdTag> = emptySet(),
|
||||||
val publicBookmarks: Set<BookmarkIdTag> = emptySet(),
|
val publicBookmarks: Set<BookmarkIdTag> = emptySet(),
|
||||||
) {
|
) {
|
||||||
// TODO: Add methods for LInk and Hashtag, after their implementation.
|
|
||||||
|
|
||||||
val privatePostBookmarks = privateBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
|
val privatePostBookmarks = privateBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
|
||||||
val publicPostBookmarks = publicBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
|
val publicPostBookmarks = publicBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
|
||||||
|
|
||||||
|
|||||||
+30
@@ -30,9 +30,13 @@ import com.vitorpamplona.amethyst.model.events
|
|||||||
import com.vitorpamplona.amethyst.model.filter
|
import com.vitorpamplona.amethyst.model.filter
|
||||||
import com.vitorpamplona.amethyst.model.updateFlow
|
import com.vitorpamplona.amethyst.model.updateFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -85,6 +89,7 @@ class LabeledBookmarkListsState(
|
|||||||
identifier = dTag(),
|
identifier = dTag(),
|
||||||
title = nameOrTitle() ?: dTag(),
|
title = nameOrTitle() ?: dTag(),
|
||||||
description = description(),
|
description = description(),
|
||||||
|
image = image(),
|
||||||
privateBookmarks = privateBookmarks(signer)?.toSet() ?: emptySet(),
|
privateBookmarks = privateBookmarks(signer)?.toSet() ?: emptySet(),
|
||||||
publicBookmarks = publicBookmarks().toSet(),
|
publicBookmarks = publicBookmarks().toSet(),
|
||||||
)
|
)
|
||||||
@@ -103,6 +108,8 @@ class LabeledBookmarkListsState(
|
|||||||
it.identifier == bookmarkListId
|
it.identifier == bookmarkListId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getBookmarkList(dTag: String) = listFeedFlow.value.getList(bookmarkListId = dTag)
|
||||||
|
|
||||||
fun DeletionEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(LabeledBookmarkListEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value)
|
fun DeletionEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(LabeledBookmarkListEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value)
|
||||||
|
|
||||||
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
fun hasItemInNoteList(notes: Set<Note>): Boolean =
|
||||||
@@ -146,6 +153,7 @@ class LabeledBookmarkListsState(
|
|||||||
suspend fun addLabeledBookmarkList(
|
suspend fun addLabeledBookmarkList(
|
||||||
listName: String,
|
listName: String,
|
||||||
listDescription: String? = null,
|
listDescription: String? = null,
|
||||||
|
listImage: String? = null,
|
||||||
firstBookmark: BookmarkIdTag? = null,
|
firstBookmark: BookmarkIdTag? = null,
|
||||||
isBookmarkPrivate: Boolean = false,
|
isBookmarkPrivate: Boolean = false,
|
||||||
account: Account,
|
account: Account,
|
||||||
@@ -154,6 +162,7 @@ class LabeledBookmarkListsState(
|
|||||||
LabeledBookmarkListEvent.create(
|
LabeledBookmarkListEvent.create(
|
||||||
name = listName,
|
name = listName,
|
||||||
description = listDescription,
|
description = listDescription,
|
||||||
|
image = listImage,
|
||||||
publicBookmarks = if (!isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(),
|
publicBookmarks = if (!isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(),
|
||||||
privateBookmarks = if (isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(),
|
privateBookmarks = if (isBookmarkPrivate && firstBookmark != null) listOf(firstBookmark) else emptyList(),
|
||||||
signer = account.signer,
|
signer = account.signer,
|
||||||
@@ -161,6 +170,27 @@ class LabeledBookmarkListsState(
|
|||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
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(
|
suspend fun renameBookmarkList(
|
||||||
newName: String,
|
newName: String,
|
||||||
bookmarkList: LabeledBookmarkList,
|
bookmarkList: LabeledBookmarkList,
|
||||||
|
|||||||
+247
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
+188
@@ -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<LabeledBookmarkList?>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
<string name="channel_name">Channel Name</string>
|
<string name="channel_name">Channel Name</string>
|
||||||
<string name="my_awesome_group">My Awesome Group</string>
|
<string name="my_awesome_group">My Awesome Group</string>
|
||||||
<string name="picture_url">Picture Url</string>
|
<string name="picture_url">Picture Url</string>
|
||||||
|
<string name="optional_picture_url">Picture Url(Optional)</string>
|
||||||
<string name="description">Description</string>
|
<string name="description">Description</string>
|
||||||
<string name="no_description">Description not found</string>
|
<string name="no_description">Description not found</string>
|
||||||
<string name="about_us">"About us.. "</string>
|
<string name="about_us">"About us.. "</string>
|
||||||
@@ -391,6 +392,8 @@
|
|||||||
|
|
||||||
<string name="bookmark_lists">Bookmark Lists</string>
|
<string name="bookmark_lists">Bookmark Lists</string>
|
||||||
<string name="bookmark_list_icon_label">Icon for bookmark list</string>
|
<string name="bookmark_list_icon_label">Icon for bookmark list</string>
|
||||||
|
<string name="bookmark_list_creation_screen_title">New Bookmark List</string>
|
||||||
|
<string name="bookmark_list_edit_sub_title">Bookmark List Metadata</string>
|
||||||
<string name="bookmark_list_clone_btn_label">Clone Bookmark List</string>
|
<string name="bookmark_list_clone_btn_label">Clone Bookmark List</string>
|
||||||
<string name="bookmark_list_broadcast_btn_label">Broadcast Bookmark List</string>
|
<string name="bookmark_list_broadcast_btn_label">Broadcast Bookmark List</string>
|
||||||
<string name="bookmark_list_delete_btn_label">Delete Bookmark List</string>
|
<string name="bookmark_list_delete_btn_label">Delete Bookmark List</string>
|
||||||
@@ -418,6 +421,7 @@
|
|||||||
<string name="public_bookmark_add_action_label">Add as public bookmark</string>
|
<string name="public_bookmark_add_action_label">Add as public bookmark</string>
|
||||||
<string name="private_bookmark_add_action_label">Add as private bookmark</string>
|
<string name="private_bookmark_add_action_label">Add as private bookmark</string>
|
||||||
<string name="bookmark_remove_action_label">Remove from Bookmark List</string>
|
<string name="bookmark_remove_action_label">Remove from Bookmark List</string>
|
||||||
|
<string name="bookmark_list_explainer">Bookmark lists metadata can be seen by anyone on Nostr. Only your private members are encrypted.</string>
|
||||||
|
|
||||||
<string name="wallet_connect_service">Wallet Connect Service</string>
|
<string name="wallet_connect_service">Wallet Connect Service</string>
|
||||||
<string name="wallet_connect_service_explainer">Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible</string>
|
<string name="wallet_connect_service_explainer">Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible</string>
|
||||||
|
|||||||
+6
-1
@@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||||
import com.vitorpamplona.quartz.nip51Lists.replaceAll
|
import com.vitorpamplona.quartz.nip51Lists.replaceAll
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
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.NameTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
@@ -75,6 +76,8 @@ class LabeledBookmarkListEvent(
|
|||||||
|
|
||||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||||
|
|
||||||
|
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||||
|
|
||||||
fun countBookmarks() = tags.count(BookmarkIdTag::isTagged)
|
fun countBookmarks() = tags.count(BookmarkIdTag::isTagged)
|
||||||
|
|
||||||
fun publicBookmarks(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
|
fun publicBookmarks(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
|
||||||
@@ -261,6 +264,7 @@ class LabeledBookmarkListEvent(
|
|||||||
suspend fun create(
|
suspend fun create(
|
||||||
name: String = "",
|
name: String = "",
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
|
image: String? = null,
|
||||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||||
dTag: String = Uuid.random().toString(),
|
dTag: String = Uuid.random().toString(),
|
||||||
@@ -269,7 +273,8 @@ class LabeledBookmarkListEvent(
|
|||||||
): LabeledBookmarkListEvent {
|
): LabeledBookmarkListEvent {
|
||||||
val template =
|
val template =
|
||||||
build(name, publicBookmarks, privateBookmarks, signer, dTag, createdAt) {
|
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)
|
return signer.sign(template)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -22,8 +22,14 @@ package com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList
|
|||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
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
|
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||||
|
|
||||||
fun TagArrayBuilder<LabeledBookmarkListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
fun TagArrayBuilder<LabeledBookmarkListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
|
||||||
|
|
||||||
fun TagArrayBuilder<LabeledBookmarkListEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = addAll(bookmarks.map { it.toTagArray() })
|
fun TagArrayBuilder<LabeledBookmarkListEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = addAll(bookmarks.map { it.toTagArray() })
|
||||||
|
|
||||||
|
fun TagArrayBuilder<LabeledBookmarkListEvent>.description(listDescription: String) = addUnique(DescriptionTag.assemble(listDescription))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<LabeledBookmarkListEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
|
||||||
|
|||||||
Reference in New Issue
Block a user