Merge branch 'main' of https://github.com/vitorpamplona/amethyst
This commit is contained in:
@@ -151,10 +151,13 @@ android {
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this when lightcompressor uses one MP4 parser only
|
||||
packaging {
|
||||
resources {
|
||||
resources.pickFirsts.add('builddef.lst')
|
||||
resources.pickFirsts.add('META-INF/LICENSE.md')
|
||||
resources.pickFirsts.add('META-INF/LICENSE-notice.md')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +364,8 @@ dependencies {
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.junit.ktx
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
androidTestImplementation libs.androidx.ui.test.junit4
|
||||
androidTestImplementation libs.mockk.android
|
||||
|
||||
debugImplementation platform(libs.androidx.compose.bom)
|
||||
debugImplementation libs.androidx.ui.tooling
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||
import com.vitorpamplona.amethyst.ui.components.ParagraphParser
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderContext
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ParagraphParserTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun testMixedImageAndVideoRenderedIndividually() {
|
||||
// Test the bug: when mixed image + video, both should be rendered individually
|
||||
val text =
|
||||
"Renfield (2023)\n" +
|
||||
"https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" +
|
||||
"https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Extract the image segments from the parsed paragraphs
|
||||
val imageSegments = mutableListOf<ImageSegment>()
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
paragraph.words.forEach { word ->
|
||||
if (word is ImageSegment) {
|
||||
imageSegments.add(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should have 2 image segments (image + video URLs)
|
||||
assertEquals(2, imageSegments.size)
|
||||
|
||||
// Track what gets rendered
|
||||
val singleWordRenders = mutableListOf<Segment>()
|
||||
val galleryRenders = mutableListOf<List<MediaUrlImage>>()
|
||||
|
||||
// Set up the test with mocked dependencies
|
||||
val context =
|
||||
RenderContext(
|
||||
state = state,
|
||||
backgroundColor = mutableStateOf(Color.White),
|
||||
quotesLeft = 3,
|
||||
callbackUri = null,
|
||||
accountViewModel = mockk<AccountViewModel>(relaxed = true),
|
||||
nav = mockk<INav>(relaxed = true),
|
||||
)
|
||||
|
||||
// Execute the actual ParagraphParser method
|
||||
composeTestRule.setContent {
|
||||
ParagraphParser().ProcessWordsWithImageGrouping(
|
||||
words = imageSegments.toImmutableList(),
|
||||
context = context,
|
||||
renderSingleWord = { segment, _ ->
|
||||
singleWordRenders.add(segment)
|
||||
},
|
||||
renderGallery = { images, _ ->
|
||||
galleryRenders.add(images)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
assertTrue(
|
||||
"Mixed image/video should be rendered individually (2 renders), not as gallery (0 renders). " +
|
||||
"Found: $singleWordRenders individual, $galleryRenders gallery",
|
||||
singleWordRenders.size == 2 && galleryRenders.isEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleImagesRenderedAsGallery() {
|
||||
// Test that multiple images (no videos) are correctly grouped as gallery
|
||||
val text =
|
||||
"Gallery:\n" +
|
||||
"https://example.com/image1.jpg\n" +
|
||||
"https://example.com/image2.png"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Extract the image segments
|
||||
val imageSegments = mutableListOf<ImageSegment>()
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
paragraph.words.forEach { word ->
|
||||
if (word is ImageSegment) {
|
||||
imageSegments.add(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(2, imageSegments.size)
|
||||
|
||||
// Track renders
|
||||
val singleWordRenders = mutableListOf<Segment>()
|
||||
val galleryRenders = mutableListOf<List<MediaUrlImage>>()
|
||||
|
||||
val context =
|
||||
RenderContext(
|
||||
state = state,
|
||||
backgroundColor = mutableStateOf(Color.White),
|
||||
quotesLeft = 3,
|
||||
callbackUri = null,
|
||||
accountViewModel = mockk<AccountViewModel>(relaxed = true),
|
||||
nav = mockk<INav>(relaxed = true),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
ParagraphParser().ProcessWordsWithImageGrouping(
|
||||
words = imageSegments.toImmutableList(),
|
||||
context = context,
|
||||
renderSingleWord = { segment, _ ->
|
||||
singleWordRenders.add(segment)
|
||||
},
|
||||
renderGallery = { images, _ ->
|
||||
galleryRenders.add(images)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
// Should render as gallery (1 gallery with 2 images)
|
||||
assertEquals("Should render 1 gallery", 1, galleryRenders.size)
|
||||
assertEquals("Gallery should contain 2 images", 2, galleryRenders[0].size)
|
||||
assertEquals("Should not render individually", 0, singleWordRenders.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleImageRenderedIndividually() {
|
||||
// Test that a single image is rendered individually, not as gallery
|
||||
val text = "Single image:\nhttps://example.com/image.jpg"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
val imageSegments = mutableListOf<ImageSegment>()
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
paragraph.words.forEach { word ->
|
||||
if (word is ImageSegment) {
|
||||
imageSegments.add(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(1, imageSegments.size)
|
||||
|
||||
val singleWordRenders = mutableListOf<Segment>()
|
||||
val galleryRenders = mutableListOf<List<MediaUrlImage>>()
|
||||
|
||||
val context =
|
||||
RenderContext(
|
||||
state = state,
|
||||
backgroundColor = mutableStateOf(Color.White),
|
||||
quotesLeft = 3,
|
||||
callbackUri = null,
|
||||
accountViewModel = mockk<AccountViewModel>(relaxed = true),
|
||||
nav = mockk<INav>(relaxed = true),
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
ParagraphParser().ProcessWordsWithImageGrouping(
|
||||
words = imageSegments.toImmutableList(),
|
||||
context = context,
|
||||
renderSingleWord = { segment, _ ->
|
||||
singleWordRenders.add(segment)
|
||||
},
|
||||
renderGallery = { images, _ ->
|
||||
galleryRenders.add(images)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
// Should render individually (not as gallery)
|
||||
assertEquals("Should render 1 individual image", 1, singleWordRenders.size)
|
||||
assertEquals("Should not render as gallery", 0, galleryRenders.size)
|
||||
}
|
||||
}
|
||||
+8
-11
@@ -31,9 +31,10 @@ data class FollowSet(
|
||||
val identifierTag: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val visibility: SetVisibility,
|
||||
val profiles: Set<String>,
|
||||
) : NostrSet(setVisibility = visibility, content = profiles) {
|
||||
val visibility: SetVisibility = SetVisibility.Mixed,
|
||||
val privateProfiles: Set<String> = emptySet(),
|
||||
val publicProfiles: Set<String> = emptySet(),
|
||||
) : NostrSet(setVisibility = visibility, privateContent = privateProfiles, publicContent = publicProfiles) {
|
||||
companion object {
|
||||
fun mapEventToSet(
|
||||
event: PeopleListEvent,
|
||||
@@ -53,26 +54,22 @@ data class FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = SetVisibility.Private,
|
||||
profiles = privateFollows.toSet(),
|
||||
privateProfiles = privateFollows.toSet(),
|
||||
)
|
||||
} else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) {
|
||||
FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = SetVisibility.Public,
|
||||
profiles = publicFollows.toSet(),
|
||||
publicProfiles = publicFollows.toSet(),
|
||||
)
|
||||
} else {
|
||||
// Follow set is empty, so assume public. Why? Nostr limitation.
|
||||
// TODO: Could this be fixed at protocol level?
|
||||
FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = SetVisibility.Public,
|
||||
profiles = publicFollows.toSet(),
|
||||
privateProfiles = privateFollows.toSet(),
|
||||
publicProfiles = publicFollows.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-16
@@ -25,20 +25,16 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FollowSetState(
|
||||
@@ -50,7 +46,7 @@ class FollowSetState(
|
||||
private val isActive = MutableStateFlow(false)
|
||||
|
||||
suspend fun getFollowSetNotes() =
|
||||
withContext(Dispatchers.Default) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val followSetNotes = LocalCache.getFollowSetNotesFor(user)
|
||||
return@withContext followSetNotes
|
||||
}
|
||||
@@ -63,12 +59,12 @@ class FollowSetState(
|
||||
emit(followSets)
|
||||
delay(2000)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
val profilesFlow =
|
||||
getFollowSetNotesFlow()
|
||||
.map { it ->
|
||||
it.flatMapTo(mutableSetOf()) { it.profiles }.toSet()
|
||||
it.flatMapTo(mutableSetOf()) { it.privateProfiles + it.publicProfiles }.toSet()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
fun mapNoteToFollowSet(note: Note): FollowSet =
|
||||
@@ -82,14 +78,5 @@ class FollowSetState(
|
||||
|
||||
init {
|
||||
isActive.update { true }
|
||||
scope.launch(Dispatchers.Default) {
|
||||
getFollowSetNotesFlow()
|
||||
.onCompletion {
|
||||
isActive.update { false }
|
||||
}.catch {
|
||||
Log.e(this@FollowSetState.javaClass.simpleName, "Error on flow collection: ${it.message}")
|
||||
isActive.update { false }
|
||||
}.collect {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.model.nip51Lists.followSets
|
||||
|
||||
sealed class NostrSet(
|
||||
val setVisibility: SetVisibility,
|
||||
val content: Collection<String>,
|
||||
val privateContent: Collection<String>,
|
||||
val publicContent: Collection<String>,
|
||||
)
|
||||
|
||||
class CuratedBookmarkSet(
|
||||
val name: String,
|
||||
val visibility: SetVisibility,
|
||||
val setItems: List<String>,
|
||||
) : NostrSet(visibility, setItems)
|
||||
val privateSetItems: List<String>,
|
||||
val publicSetItems: List<String>,
|
||||
) : NostrSet(visibility, privateSetItems, publicSetItems)
|
||||
|
||||
+1
-8
@@ -257,16 +257,9 @@ object VideoCompressionHelper {
|
||||
"Compressed [$size] ($reductionPercent% reduction)",
|
||||
)
|
||||
|
||||
// Attempt to correct the path: if it contains "_temp" then remove it
|
||||
val correctedPath =
|
||||
if (path.contains("_temp")) {
|
||||
path.replace("_temp", "")
|
||||
} else {
|
||||
path
|
||||
}
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(
|
||||
MediaCompressorResult(Uri.fromFile(File(correctedPath)), contentType, size),
|
||||
MediaCompressorResult(Uri.fromFile(File(path)), contentType, size),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -197,19 +197,26 @@ class ParagraphParser {
|
||||
j++
|
||||
}
|
||||
|
||||
if (imageSegments.size > 1) {
|
||||
val imageContents =
|
||||
imageSegments
|
||||
.mapNotNull { segment ->
|
||||
val imageUrl = segment.segmentText
|
||||
context.state.imagesForPager[imageUrl] as? MediaUrlImage
|
||||
}.toImmutableList()
|
||||
|
||||
if (imageContents.isNotEmpty()) {
|
||||
renderGallery(imageContents, context.accountViewModel)
|
||||
}
|
||||
} else {
|
||||
if (imageSegments.size <= 1) {
|
||||
renderSingleWord(imageSegments.firstOrNull() ?: word, context)
|
||||
} else {
|
||||
val resolvedImages =
|
||||
imageSegments.mapNotNull { segment ->
|
||||
val imageUrl = segment.segmentText
|
||||
context.state.imagesForPager[imageUrl] as? MediaUrlImage
|
||||
}
|
||||
|
||||
// Render gallery only if all segments are images
|
||||
if (resolvedImages.size == imageSegments.size) {
|
||||
renderGallery(
|
||||
resolvedImages.toImmutableList(),
|
||||
context.accountViewModel,
|
||||
)
|
||||
} else {
|
||||
imageSegments.forEach { segment ->
|
||||
renderSingleWord(segment, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = j // jump past processed run
|
||||
|
||||
+52
-80
@@ -30,9 +30,11 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Tab
|
||||
@@ -48,8 +50,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -119,12 +119,11 @@ fun ListsAndSetsScreen(
|
||||
refresh = {
|
||||
followSetsViewModel.invalidateData()
|
||||
},
|
||||
addItem = { title: String, description: String?, listType: SetVisibility ->
|
||||
val isSetPrivate = listType == SetVisibility.Private
|
||||
addItem = { title: String, description: String? ->
|
||||
|
||||
followSetsViewModel.addFollowSet(
|
||||
setName = title,
|
||||
setDescription = description,
|
||||
isListPrivate = isSetPrivate,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
},
|
||||
@@ -138,6 +137,21 @@ fun ListsAndSetsScreen(
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
},
|
||||
changeItemDescription = { followSet, newDescription ->
|
||||
followSetsViewModel.modifyFollowSetDescription(
|
||||
newDescription = newDescription,
|
||||
followSet = followSet,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
},
|
||||
cloneItem = { followSet, customName, customDescription ->
|
||||
followSetsViewModel.cloneFollowSet(
|
||||
currentFollowSet = followSet,
|
||||
customCloneName = customName,
|
||||
customCloneDescription = customDescription,
|
||||
account = accountViewModel.account,
|
||||
)
|
||||
},
|
||||
deleteItem = { followSet ->
|
||||
followSetsViewModel.deleteFollowSet(
|
||||
followSet = followSet,
|
||||
@@ -153,14 +167,16 @@ fun ListsAndSetsScreen(
|
||||
fun CustomListsScreen(
|
||||
followSetFeedState: FollowSetFeedState,
|
||||
refresh: () -> Unit,
|
||||
addItem: (title: String, description: String?, listType: SetVisibility) -> Unit,
|
||||
addItem: (title: String, description: String?) -> Unit,
|
||||
openItem: (identifier: String) -> Unit,
|
||||
renameItem: (followSet: FollowSet, newName: String) -> Unit,
|
||||
changeItemDescription: (followSet: FollowSet, newDescription: String?) -> Unit,
|
||||
cloneItem: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit,
|
||||
deleteItem: (followSet: FollowSet) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val pagerState = rememberPagerState { 3 }
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
DisappearingScaffold(
|
||||
@@ -185,22 +201,14 @@ fun CustomListsScreen(
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = { Text(text = stringRes(R.string.labeled_bookmarks), overflow = TextOverflow.Visible) },
|
||||
)
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 2,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
|
||||
text = { Text(text = stringRes(R.string.general_bookmarks), overflow = TextOverflow.Visible) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingButton = {
|
||||
// TODO: Show components based on current tab
|
||||
FollowSetFabsAndMenu(
|
||||
onAddPrivateSet = { name: String, description: String? ->
|
||||
addItem(name, description, SetVisibility.Private)
|
||||
},
|
||||
onAddPublicSet = { name: String, description: String? ->
|
||||
addItem(name, description, SetVisibility.Public)
|
||||
onAddSet = { name: String, description: String? ->
|
||||
addItem(name, description)
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -222,11 +230,13 @@ fun CustomListsScreen(
|
||||
onRefresh = refresh,
|
||||
onOpenItem = openItem,
|
||||
onRenameItem = renameItem,
|
||||
onItemDescriptionChange = changeItemDescription,
|
||||
onItemClone = cloneItem,
|
||||
onDeleteItem = deleteItem,
|
||||
)
|
||||
|
||||
1 -> LabeledBookmarksFeedView()
|
||||
2 -> GeneralBookmarksFeedView()
|
||||
// 2 -> GeneralBookmarksFeedView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,57 +246,34 @@ fun CustomListsScreen(
|
||||
@Composable
|
||||
private fun FollowSetFabsAndMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
onAddPrivateSet: (name: String, description: String?) -> Unit,
|
||||
onAddPublicSet: (name: String, description: String?) -> Unit,
|
||||
onAddSet: (name: String, description: String?) -> Unit,
|
||||
) {
|
||||
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
||||
val isPrivateOptionTapped = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
isPrivateOptionTapped.value = true
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_create_btn_label))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.lock_plus),
|
||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.earth_plus),
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
isSetAdditionDialogOpen.value = true
|
||||
},
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
if (isSetAdditionDialogOpen.value) {
|
||||
NewSetCreationDialog(
|
||||
onDismiss = {
|
||||
isSetAdditionDialogOpen.value = false
|
||||
isPrivateOptionTapped.value = false
|
||||
},
|
||||
shouldBePrivate = isPrivateOptionTapped.value,
|
||||
onCreateList = { name, description ->
|
||||
if (isPrivateOptionTapped.value) {
|
||||
onAddPrivateSet(name, description)
|
||||
} else {
|
||||
onAddPublicSet(name, description)
|
||||
}
|
||||
onAddSet(name, description)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -296,27 +283,10 @@ private fun FollowSetFabsAndMenu(
|
||||
fun NewSetCreationDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit,
|
||||
shouldBePrivate: Boolean,
|
||||
onCreateList: (name: String, description: String?) -> Unit,
|
||||
) {
|
||||
val newListName = remember { mutableStateOf("") }
|
||||
val newListDescription = remember { mutableStateOf<String?>(null) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val listTypeText =
|
||||
stringRes(
|
||||
context,
|
||||
when (shouldBePrivate) {
|
||||
true -> R.string.follow_set_type_private
|
||||
false -> R.string.follow_set_type_public
|
||||
},
|
||||
)
|
||||
|
||||
val listTypeIcon =
|
||||
when (shouldBePrivate) {
|
||||
true -> R.drawable.lock
|
||||
false -> R.drawable.ic_public
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -325,12 +295,8 @@ fun NewSetCreationDialog(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(listTypeIcon),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.follow_set_creation_dialog_title, listTypeText),
|
||||
text = stringRes(R.string.follow_set_creation_dialog_title),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -347,7 +313,7 @@ fun NewSetCreationDialog(
|
||||
},
|
||||
)
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
// For the list description
|
||||
// For the set description
|
||||
TextField(
|
||||
value =
|
||||
(
|
||||
@@ -425,6 +391,12 @@ private fun SetItemPreview() {
|
||||
onFollowSetRename = {
|
||||
println("Follow set new name: $it")
|
||||
},
|
||||
onFollowSetDescriptionChange = { description ->
|
||||
println("The follow set's description has been changed to $description")
|
||||
},
|
||||
onFollowSetClone = { newName, newDesc ->
|
||||
println("The follow set has been cloned, and has custom name: $newName, Desc: $newDesc")
|
||||
},
|
||||
onFollowSetDelete = {
|
||||
println(" The follow set ${sampleFollowSet.title} has been deleted.")
|
||||
},
|
||||
|
||||
+285
-65
@@ -37,13 +37,10 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -56,15 +53,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import kotlin.let
|
||||
|
||||
@Composable
|
||||
fun CustomSetItem(
|
||||
@@ -72,6 +68,8 @@ fun CustomSetItem(
|
||||
followSet: FollowSet,
|
||||
onFollowSetClick: () -> Unit,
|
||||
onFollowSetRename: (String) -> Unit,
|
||||
onFollowSetDescriptionChange: (String?) -> Unit,
|
||||
onFollowSetClone: (customName: String?, customDescription: String?) -> Unit,
|
||||
onFollowSetDelete: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -96,20 +94,75 @@ fun CustomSetItem(
|
||||
) {
|
||||
Text(followSet.title, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = "${followSet.profiles.size}")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.People,
|
||||
contentDescription = null,
|
||||
if (followSet.publicProfiles.isEmpty() && followSet.privateProfiles.isEmpty()) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = stringRes(R.string.follow_set_empty_label))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.People,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
}
|
||||
if (followSet.publicProfiles.isNotEmpty()) {
|
||||
val publicMemberSize = followSet.publicProfiles.size
|
||||
val membersLabel =
|
||||
stringRes(
|
||||
context,
|
||||
if (publicMemberSize == 1) {
|
||||
R.string.follow_set_single_member_label
|
||||
} else {
|
||||
R.string.follow_set_multiple_member_label
|
||||
},
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = "$publicMemberSize")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_public),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
if (followSet.privateProfiles.isNotEmpty()) {
|
||||
val privateMemberSize = followSet.privateProfiles.size
|
||||
val membersLabel =
|
||||
stringRes(
|
||||
context,
|
||||
if (privateMemberSize == 1) {
|
||||
R.string.follow_set_single_member_label
|
||||
} else {
|
||||
R.string.follow_set_multiple_member_label
|
||||
},
|
||||
)
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = "$privateMemberSize")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.lock),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
Text(
|
||||
@@ -119,34 +172,6 @@ fun CustomSetItem(
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
|
||||
followSet.visibility.let {
|
||||
val text by derivedStateOf {
|
||||
when (it) {
|
||||
SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
|
||||
SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
|
||||
SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 15.dp),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
painter =
|
||||
painterResource(
|
||||
when (it) {
|
||||
SetVisibility.Public -> R.drawable.ic_public
|
||||
SetVisibility.Private -> R.drawable.lock
|
||||
SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
},
|
||||
),
|
||||
contentDescription = stringRes(R.string.follow_set_type_description, text),
|
||||
)
|
||||
Text(text, color = Color.Gray, fontWeight = FontWeight.Light)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -159,7 +184,10 @@ fun CustomSetItem(
|
||||
) {
|
||||
SetOptionsButton(
|
||||
followSetName = followSet.title,
|
||||
onListRename = onFollowSetRename,
|
||||
followSetDescription = followSet.description,
|
||||
onSetRename = onFollowSetRename,
|
||||
onSetDescriptionChange = onFollowSetDescriptionChange,
|
||||
onSetCloneCreate = onFollowSetClone,
|
||||
onListDelete = onFollowSetDelete,
|
||||
)
|
||||
}
|
||||
@@ -167,10 +195,13 @@ fun CustomSetItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetOptionsButton(
|
||||
private fun SetOptionsButton(
|
||||
modifier: Modifier = Modifier,
|
||||
followSetName: String,
|
||||
onListRename: (String) -> Unit,
|
||||
followSetDescription: String?,
|
||||
onSetRename: (String) -> Unit,
|
||||
onSetDescriptionChange: (String?) -> Unit,
|
||||
onSetCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||
onListDelete: () -> Unit,
|
||||
) {
|
||||
val isMenuOpen = remember { mutableStateOf(false) }
|
||||
@@ -181,10 +212,13 @@ fun SetOptionsButton(
|
||||
VerticalDotsIcon()
|
||||
|
||||
SetOptionsMenu(
|
||||
listName = followSetName,
|
||||
setName = followSetName,
|
||||
setDescription = followSetDescription,
|
||||
isExpanded = isMenuOpen.value,
|
||||
onDismiss = { isMenuOpen.value = false },
|
||||
onListRename = onListRename,
|
||||
onSetRename = onSetRename,
|
||||
onSetDescriptionChange = onSetDescriptionChange,
|
||||
onSetClone = onSetCloneCreate,
|
||||
onDelete = onListDelete,
|
||||
)
|
||||
}
|
||||
@@ -194,26 +228,27 @@ fun SetOptionsButton(
|
||||
private fun SetOptionsMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
isExpanded: Boolean,
|
||||
listName: String,
|
||||
onListRename: (String) -> Unit,
|
||||
setName: String,
|
||||
setDescription: String?,
|
||||
onSetRename: (String) -> Unit,
|
||||
onSetDescriptionChange: (String?) -> Unit,
|
||||
onSetClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val isRenameDialogOpen = remember { mutableStateOf(false) }
|
||||
val renameString = remember { mutableStateOf("") }
|
||||
|
||||
val isDescriptionModDialogOpen = remember { mutableStateOf(false) }
|
||||
|
||||
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.quick_action_delete))
|
||||
},
|
||||
onClick = {
|
||||
onDelete()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
||||
@@ -223,25 +258,76 @@ private fun SetOptionsMenu(
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
||||
},
|
||||
onClick = {
|
||||
isDescriptionModDialogOpen.value = true
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
isCopyDialogOpen.value = true
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.quick_action_delete))
|
||||
},
|
||||
onClick = {
|
||||
onDelete()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (isRenameDialogOpen.value) {
|
||||
RenameDialog(
|
||||
currentName = listName,
|
||||
SetRenameDialog(
|
||||
currentName = setName,
|
||||
newName = renameString.value,
|
||||
onStringRenameChange = {
|
||||
renameString.value = it
|
||||
},
|
||||
onDismissDialog = { isRenameDialogOpen.value = false },
|
||||
onListRename = {
|
||||
onListRename(renameString.value)
|
||||
onSetRename(renameString.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (isDescriptionModDialogOpen.value) {
|
||||
SetModifyDescriptionDialog(
|
||||
currentDescription = setDescription,
|
||||
onDismissDialog = { isDescriptionModDialogOpen.value = false },
|
||||
onModifyDescription = onSetDescriptionChange,
|
||||
)
|
||||
}
|
||||
|
||||
if (isCopyDialogOpen.value) {
|
||||
SetCloneDialog(
|
||||
optionalNewName = optionalCloneName.value,
|
||||
optionalNewDesc = optionalCloneDescription.value,
|
||||
onCloneNameChange = {
|
||||
optionalCloneName.value = it
|
||||
},
|
||||
onCloneDescChange = {
|
||||
optionalCloneDescription.value = it
|
||||
},
|
||||
onCloneCreate = { name, description ->
|
||||
onSetClone(optionalCloneName.value, optionalCloneDescription.value)
|
||||
},
|
||||
onDismiss = { isCopyDialogOpen.value = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenameDialog(
|
||||
private fun SetRenameDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
currentName: String,
|
||||
newName: String,
|
||||
@@ -299,3 +385,137 @@ private fun RenameDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetModifyDescriptionDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
currentDescription: String?,
|
||||
onDismissDialog: () -> Unit,
|
||||
onModifyDescription: (String?) -> Unit,
|
||||
) {
|
||||
val updatedDescription = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val modifyIndicatorLabel =
|
||||
if (currentDescription == null) {
|
||||
stringRes(R.string.follow_set_empty_desc_label)
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
append(stringRes(R.string.follow_set_current_desc_label) + " ")
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Normal,
|
||||
fontSize = 15.sp,
|
||||
),
|
||||
) {
|
||||
append("\"" + currentDescription + "\"")
|
||||
}
|
||||
}.text
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissDialog,
|
||||
title = {
|
||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = modifyIndicatorLabel,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
TextField(
|
||||
value = updatedDescription.value ?: "",
|
||||
onValueChange = { updatedDescription.value = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onModifyDescription(updatedDescription.value)
|
||||
onDismissDialog()
|
||||
},
|
||||
) { Text(text = stringRes(R.string.follow_set_desc_modify_btn_label)) }
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = onDismissDialog) { Text(text = stringRes(R.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetCloneDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
optionalNewName: String?,
|
||||
optionalNewDesc: String?,
|
||||
onCloneNameChange: (String?) -> Unit,
|
||||
onCloneDescChange: (String?) -> Unit,
|
||||
onCloneCreate: (customName: String?, customDescription: String?) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_set_copy_dialog_title),
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_set_copy_indicator_description),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
// For the set clone name
|
||||
TextField(
|
||||
value = optionalNewName ?: "",
|
||||
onValueChange = onCloneNameChange,
|
||||
label = {
|
||||
Text(text = stringRes(R.string.follow_set_copy_name_label))
|
||||
},
|
||||
)
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
// For the set clone description
|
||||
TextField(
|
||||
value = optionalNewDesc ?: "",
|
||||
onValueChange = onCloneDescChange,
|
||||
label = {
|
||||
Text(text = stringRes(R.string.follow_set_copy_desc_label))
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onCloneCreate(optionalNewName, optionalNewDesc)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.follow_set_copy_action_btn_label))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
) {
|
||||
Text(stringRes(R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+12
@@ -51,6 +51,8 @@ fun FollowSetFeedView(
|
||||
onRefresh: () -> Unit = {},
|
||||
onOpenItem: (String) -> Unit = {},
|
||||
onRenameItem: (targetSet: FollowSet, newName: String) -> Unit,
|
||||
onItemDescriptionChange: (followSet: FollowSet, newDescription: String?) -> Unit,
|
||||
onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit,
|
||||
onDeleteItem: (followSet: FollowSet) -> Unit,
|
||||
) {
|
||||
when (followSetFeedState) {
|
||||
@@ -63,6 +65,8 @@ fun FollowSetFeedView(
|
||||
onRefresh = onRefresh,
|
||||
onItemClick = onOpenItem,
|
||||
onItemRename = onRenameItem,
|
||||
onItemDescriptionChange = onItemDescriptionChange,
|
||||
onItemClone = onItemClone,
|
||||
onItemDelete = onDeleteItem,
|
||||
)
|
||||
}
|
||||
@@ -91,6 +95,8 @@ fun FollowSetLoaded(
|
||||
onRefresh: () -> Unit = {},
|
||||
onItemClick: (itemIdentifier: String) -> Unit = {},
|
||||
onItemRename: (followSet: FollowSet, newName: String) -> Unit,
|
||||
onItemDescriptionChange: (followSet: FollowSet, newDescription: String?) -> Unit,
|
||||
onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit,
|
||||
onItemDelete: (followSet: FollowSet) -> Unit,
|
||||
) {
|
||||
Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}")
|
||||
@@ -113,6 +119,12 @@ fun FollowSetLoaded(
|
||||
onFollowSetRename = {
|
||||
onItemRename(set, it)
|
||||
},
|
||||
onFollowSetDescriptionChange = { newDescription ->
|
||||
onItemDescriptionChange(set, newDescription)
|
||||
},
|
||||
onFollowSetClone = { cloneName, cloneDescription ->
|
||||
onItemClone(set, cloneName, cloneDescription)
|
||||
},
|
||||
onFollowSetDelete = {
|
||||
onItemDelete(set)
|
||||
},
|
||||
|
||||
+53
-5
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter
|
||||
@@ -117,8 +116,8 @@ class FollowSetFeedViewModel(
|
||||
fun addFollowSet(
|
||||
setName: String,
|
||||
setDescription: String?,
|
||||
isListPrivate: Boolean,
|
||||
optionalFirstMemberHex: String? = null,
|
||||
firstMemberShouldBePrivate: Boolean = false,
|
||||
account: Account,
|
||||
) {
|
||||
if (!account.settings.isWriteable()) {
|
||||
@@ -129,8 +128,9 @@ class FollowSetFeedViewModel(
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
title = setName,
|
||||
description = setDescription,
|
||||
isPrivate = isListPrivate,
|
||||
firstMemberHex = optionalFirstMemberHex,
|
||||
isPrivate = firstMemberShouldBePrivate,
|
||||
firstPublicMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
|
||||
firstPrivateMembers = if (optionalFirstMemberHex != null) listOf(optionalFirstMemberHex) else emptyList(),
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
@@ -160,6 +160,51 @@ class FollowSetFeedViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyFollowSetDescription(
|
||||
newDescription: String?,
|
||||
followSet: FollowSet,
|
||||
account: Account,
|
||||
) {
|
||||
if (!account.settings.isWriteable()) {
|
||||
println("You are in read-only mode. Please login to make modifications.")
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val setEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent
|
||||
PeopleListEvent.modifyDescription(
|
||||
earlierVersion = setEvent,
|
||||
newDescription = newDescription,
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cloneFollowSet(
|
||||
currentFollowSet: FollowSet,
|
||||
customCloneName: String?,
|
||||
customCloneDescription: String?,
|
||||
account: Account,
|
||||
) {
|
||||
if (!account.settings.isWriteable()) {
|
||||
println("You are in read-only mode. Please login to make modifications.")
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
PeopleListEvent.copy(
|
||||
dTag = UUID.randomUUID().toString(),
|
||||
title = customCloneName ?: currentFollowSet.title,
|
||||
description = customCloneDescription ?: currentFollowSet.description,
|
||||
firstPublicMembers = currentFollowSet.publicProfiles.toList(),
|
||||
firstPrivateMembers = currentFollowSet.privateProfiles.toList(),
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFollowSet(
|
||||
followSet: FollowSet,
|
||||
account: Account,
|
||||
@@ -179,6 +224,7 @@ class FollowSetFeedViewModel(
|
||||
fun addUserToSet(
|
||||
userProfileHex: String,
|
||||
followSet: FollowSet,
|
||||
shouldBePrivateMember: Boolean,
|
||||
account: Account,
|
||||
) {
|
||||
if (!account.settings.isWriteable()) {
|
||||
@@ -190,7 +236,7 @@ class FollowSetFeedViewModel(
|
||||
PeopleListEvent.addUser(
|
||||
earlierVersion = followSetEvent,
|
||||
pubKeyHex = userProfileHex,
|
||||
isPrivate = followSet.visibility == SetVisibility.Private,
|
||||
isPrivate = shouldBePrivateMember,
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
@@ -201,6 +247,7 @@ class FollowSetFeedViewModel(
|
||||
|
||||
fun removeUserFromSet(
|
||||
userProfileHex: String,
|
||||
userIsPrivate: Boolean,
|
||||
followSet: FollowSet,
|
||||
account: Account,
|
||||
) {
|
||||
@@ -213,6 +260,7 @@ class FollowSetFeedViewModel(
|
||||
PeopleListEvent.removeUser(
|
||||
earlierVersion = followSetEvent,
|
||||
pubKeyHex = userProfileHex,
|
||||
isUserPrivate = userIsPrivate,
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
|
||||
+68
-20
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Row
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -68,7 +70,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
@@ -76,11 +77,13 @@ import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.VertPadding
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -144,7 +147,8 @@ fun FollowSetScreen(
|
||||
when {
|
||||
selectedSetState.value != null -> {
|
||||
val selectedSet = selectedSetState.value
|
||||
val users = selectedSet!!.profiles.mapToUsers(accountViewModel).filterNotNull()
|
||||
val publicMembers = selectedSet!!.publicProfiles.mapToUsers(accountViewModel).filterNotNull()
|
||||
val privateMembers = selectedSet.privateProfiles.mapToUsers(accountViewModel).filterNotNull()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -171,6 +175,7 @@ fun FollowSetScreen(
|
||||
selectedSet,
|
||||
accountViewModel.account,
|
||||
)
|
||||
navigator.popBack()
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -190,10 +195,12 @@ fun FollowSetScreen(
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
).consumeWindowInsets(padding)
|
||||
.imePadding(),
|
||||
followSetList = users,
|
||||
publicMemberList = publicMembers,
|
||||
privateMemberList = privateMembers,
|
||||
onDeleteUser = {
|
||||
followSetViewModel.removeUserFromSet(
|
||||
it,
|
||||
userIsPrivate = selectedSet.privateProfiles.contains(it),
|
||||
selectedSet,
|
||||
accountViewModel.account,
|
||||
)
|
||||
@@ -233,14 +240,7 @@ fun TitleAndDescription(
|
||||
)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Icon(
|
||||
painter =
|
||||
painterResource(
|
||||
when (followSet.setVisibility) {
|
||||
SetVisibility.Public -> R.drawable.ic_public
|
||||
SetVisibility.Private -> R.drawable.lock
|
||||
SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
},
|
||||
),
|
||||
painter = painterResource(R.drawable.format_list_bulleted_type),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
@@ -260,7 +260,8 @@ fun TitleAndDescription(
|
||||
@Composable
|
||||
private fun FollowSetListView(
|
||||
modifier: Modifier = Modifier,
|
||||
followSetList: List<User>,
|
||||
publicMemberList: List<User>,
|
||||
privateMemberList: List<User>,
|
||||
onDeleteUser: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -272,14 +273,61 @@ private fun FollowSetListView(
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(followSetList, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||
FollowSetListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
user = item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onDeleteUser = onDeleteUser,
|
||||
)
|
||||
if (publicMemberList.isNotEmpty()) {
|
||||
stickyHeader {
|
||||
Column(
|
||||
modifier = VertPadding,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_set_public_members_header_label),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
thickness = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(publicMemberList, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||
FollowSetListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
user = item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onDeleteUser = onDeleteUser,
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
}
|
||||
}
|
||||
if (privateMemberList.isNotEmpty()) {
|
||||
stickyHeader {
|
||||
Text(
|
||||
text = stringRes(R.string.follow_set_private_members_header_label),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
thickness = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
itemsIndexed(privateMemberList, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||
FollowSetListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
user = item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onDeleteUser = onDeleteUser,
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+90
-39
@@ -45,6 +45,8 @@ import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -69,6 +71,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -165,16 +168,18 @@ fun FollowSetsManagementDialog(
|
||||
) {
|
||||
when (followSetsState) {
|
||||
is FollowSetFeedState.Loaded -> {
|
||||
val lists = (followSetsState as FollowSetFeedState.Loaded).feed
|
||||
val sets = (followSetsState as FollowSetFeedState.Loaded).feed
|
||||
|
||||
sets.forEachIndexed { index, set ->
|
||||
|
||||
lists.forEachIndexed { index, list ->
|
||||
Spacer(StdVertSpacer)
|
||||
FollowSetItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listHeader = list.title,
|
||||
setVisibility = list.visibility,
|
||||
listHeader = set.title,
|
||||
setVisibility = set.visibility,
|
||||
userName = userInfo.toBestDisplayName(),
|
||||
isUserInList = list.profiles.contains(userHex),
|
||||
userIsPrivateMember = set.privateProfiles.contains(userHex),
|
||||
userIsPublicMember = set.publicProfiles.contains(userHex),
|
||||
onRemoveUser = {
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
@@ -182,23 +187,31 @@ fun FollowSetsManagementDialog(
|
||||
)
|
||||
followSetsViewModel.removeUserFromSet(
|
||||
userHex,
|
||||
list,
|
||||
userIsPrivate = set.privateProfiles.contains(userHex),
|
||||
set,
|
||||
account,
|
||||
)
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
"Updated List. New size: ${list.profiles.size}",
|
||||
"Updated List. Private profiles size: ${set.privateProfiles.size}," +
|
||||
"Public profiles size: ${set.publicProfiles.size}",
|
||||
)
|
||||
},
|
||||
onAddUser = {
|
||||
onAddUserToList = { userShouldBePrivate ->
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
"ProfileActions: Adding item to list ...",
|
||||
)
|
||||
followSetsViewModel.addUserToSet(userHex, list, account)
|
||||
followSetsViewModel.addUserToSet(
|
||||
userHex,
|
||||
set,
|
||||
userShouldBePrivate,
|
||||
account,
|
||||
)
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
"Updated List. New size: ${list.profiles.size}",
|
||||
"Updated List. Private profiles size: ${set.privateProfiles.size}," +
|
||||
"Public profiles size: ${set.publicProfiles.size}",
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -222,11 +235,11 @@ fun FollowSetsManagementDialog(
|
||||
if (followSetsState != FollowSetFeedState.Loading) {
|
||||
FollowSetsCreationMenu(
|
||||
userName = userInfo.toBestDisplayName(),
|
||||
onSetCreate = { setName, setIsPrivate, description ->
|
||||
onSetCreate = { setName, memberShouldBePrivate, description ->
|
||||
followSetsViewModel.addFollowSet(
|
||||
setName = setName,
|
||||
setDescription = description,
|
||||
isListPrivate = setIsPrivate,
|
||||
firstMemberShouldBePrivate = memberShouldBePrivate,
|
||||
optionalFirstMemberHex = userHex,
|
||||
account = account,
|
||||
)
|
||||
@@ -306,11 +319,13 @@ fun FollowSetItem(
|
||||
listHeader: String,
|
||||
setVisibility: SetVisibility,
|
||||
userName: String,
|
||||
isUserInList: Boolean,
|
||||
onAddUser: () -> Unit,
|
||||
userIsPrivateMember: Boolean,
|
||||
userIsPublicMember: Boolean,
|
||||
onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit,
|
||||
onRemoveUser: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isUserInList = userIsPrivateMember || userIsPublicMember
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
@@ -330,26 +345,10 @@ fun FollowSetItem(
|
||||
) {
|
||||
Text(listHeader, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
setVisibility.let {
|
||||
val text by derivedStateOf {
|
||||
when (it) {
|
||||
SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
|
||||
SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
|
||||
SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
painter =
|
||||
painterResource(
|
||||
when (setVisibility) {
|
||||
SetVisibility.Public -> R.drawable.ic_public
|
||||
SetVisibility.Private -> R.drawable.lock
|
||||
SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
},
|
||||
),
|
||||
contentDescription = stringRes(R.string.follow_set_type_description, text),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.format_list_bulleted_type),
|
||||
contentDescription = stringRes(R.string.follow_set_icon_description),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
@@ -362,7 +361,11 @@ fun FollowSetItem(
|
||||
Text(
|
||||
text =
|
||||
if (isUserInList) {
|
||||
stringRes(R.string.follow_set_presence_indicator, userName)
|
||||
if (userIsPublicMember) {
|
||||
stringRes(R.string.follow_set_public_presence_indicator, userName)
|
||||
} else {
|
||||
stringRes(R.string.follow_set_private_presence_indicator, userName)
|
||||
}
|
||||
} else {
|
||||
stringRes(R.string.follow_set_absence_indicator, userName)
|
||||
},
|
||||
@@ -389,9 +392,14 @@ fun FollowSetItem(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val isUserAddTapped = remember { mutableStateOf(false) }
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (isUserInList) onRemoveUser() else onAddUser()
|
||||
if (isUserInList) {
|
||||
onRemoveUser()
|
||||
} else {
|
||||
isUserAddTapped.value = true
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -423,15 +431,55 @@ fun FollowSetItem(
|
||||
text = stringRes(if (isUserInList) R.string.remove else R.string.add),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
UserAdditionOptionsMenu(
|
||||
isExpanded = isUserAddTapped.value,
|
||||
onUserAdd = { shouldBePrivateMember ->
|
||||
onAddUserToList(shouldBePrivateMember)
|
||||
},
|
||||
onDismiss = { isUserAddTapped.value = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserAdditionOptionsMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
isExpanded: Boolean,
|
||||
onUserAdd: (asPrivateMember: Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_public_member_add_label))
|
||||
},
|
||||
onClick = {
|
||||
onUserAdd(false)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_private_member_add_label))
|
||||
},
|
||||
onClick = {
|
||||
onUserAdd(true)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowSetsCreationMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
userName: String,
|
||||
onSetCreate: (setName: String, setIsPrivate: Boolean, description: String?) -> Unit,
|
||||
onSetCreate: (setName: String, memberShouldBePrivate: Boolean, description: String?) -> Unit,
|
||||
) {
|
||||
val isListAdditionDialogOpen = remember { mutableStateOf(false) }
|
||||
val isPrivateOptionTapped = remember { mutableStateOf(false) }
|
||||
@@ -474,7 +522,6 @@ fun FollowSetsCreationMenu(
|
||||
isListAdditionDialogOpen.value = false
|
||||
isPrivateOptionTapped.value = false
|
||||
},
|
||||
shouldBePrivate = isPrivateOptionTapped.value,
|
||||
onCreateList = { name, description ->
|
||||
onSetCreate(name, isPrivateOptionTapped.value, description)
|
||||
},
|
||||
@@ -528,7 +575,11 @@ fun FollowSetCreationItem(
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
Text(
|
||||
stringRes(R.string.follow_set_creation_item_description, setTypeLabel, userName),
|
||||
stringRes(
|
||||
R.string.follow_set_creation_item_description,
|
||||
userName,
|
||||
setTypeLabel.lowercase(Locale.current.platformLocale),
|
||||
),
|
||||
fontWeight = FontWeight.Light,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
|
||||
@@ -453,9 +453,8 @@
|
||||
<string name="follow_set_empty_feed_msg">Zdá se, že zatím nemáte žádné sady sledování.\nKlepněte níže pro obnovení nebo použijte tlačítko přidat k vytvoření nové.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Přidat autora do sady sledování</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem.</string>
|
||||
<string name="follow_set_type_description">Ikona pro seznam %1$s</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s je v tomto seznamu"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s není v tomto seznamu"</string>
|
||||
<string name="follow_set_icon_description">Ikona pro seznam %1$s</string>
|
||||
<string name="follow_set_absence_indicator">%1$s není v tomto seznamu</string>
|
||||
<string name="follow_set_man_dialog_title">Vaše sady sledování</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové.</string>
|
||||
<string name="follow_set_error_dialog_msg">Došlo k problému při načítání: %1$s</string>
|
||||
@@ -1064,4 +1063,24 @@
|
||||
<string name="crashreport_found_send">Odeslat</string>
|
||||
<string name="this_message_will_disappear_in_days">Tato zpráva zmizí za %1$d dní</string>
|
||||
<string name="select_signer">Vybrat podepisovatele</string>
|
||||
|
||||
<string name="follow_set_create_btn_label">Nový</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s je veřejný člen</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s je soukromý člen</string>
|
||||
<string name="follow_set_public_member_add_label">Přidat jako veřejného člena</string>
|
||||
<string name="follow_set_private_member_add_label">Přidat jako soukromého člena</string>
|
||||
<string name="follow_set_public_members_header_label">Veřejné profily</string>
|
||||
<string name="follow_set_private_members_header_label">Soukromé profily</string>
|
||||
<string name="follow_set_single_member_label">člen</string>
|
||||
<string name="follow_set_multiple_member_label">členové</string>
|
||||
<string name="follow_set_empty_label">Žádní členové</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopírovat/Klonovat sadu sledování</string>
|
||||
<string name="follow_set_desc_modify_label">Upravit popis</string>
|
||||
<string name="follow_set_empty_desc_label">Tento seznam nemá žádný popis</string>
|
||||
<string name="follow_set_current_desc_label">Aktuální popis:</string>
|
||||
<string name="follow_set_copy_indicator_description">Níže můžete nastavit vlastní název/popisek pro tuto klonovanou sadu.</string>
|
||||
<string name="follow_set_copy_name_label">(Původní název sady)</string>
|
||||
<string name="follow_set_copy_desc_label">(Původní popis sady)</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopírovat/Klonovat sadu</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Upravit</string>
|
||||
</resources>
|
||||
|
||||
@@ -961,8 +961,7 @@
|
||||
<string name="follow_set_empty_feed_msg">Zdá se, že zatím nemáte žádné sady sledování.\nKlepněte níže pro obnovení nebo použijte tlačítko přidat k vytvoření nové.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Přidat autora do sady sledování</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Přidat nebo odebrat uživatele ze seznamů, nebo vytvořit nový seznam s tímto uživatelem.</string>
|
||||
<string name="follow_set_type_description">Ikona pro seznam %1$s</string>
|
||||
<string name="follow_set_presence_indicator">%1$s je v tomto seznamu</string>
|
||||
<string name="follow_set_icon_description">Ikona pro seznam %1$s</string>
|
||||
<string name="follow_set_absence_indicator">%1$s není v tomto seznamu</string>
|
||||
<string name="follow_set_man_dialog_title">Vaše sady sledování</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nebyly nalezeny žádné sady sledování, nebo žádné nemáte. Klepněte níže pro obnovení nebo použijte menu pro vytvoření nové.</string>
|
||||
|
||||
@@ -459,9 +459,8 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="follow_set_empty_feed_msg">Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Autor zum Folge-Set hinzufügen</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen.</string>
|
||||
<string name="follow_set_type_description">Symbol für %1$s-Liste</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s ist in dieser Liste"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s ist nicht in dieser Liste"</string>
|
||||
<string name="follow_set_icon_description">Symbol für %1$s-Liste</string>
|
||||
<string name="follow_set_absence_indicator">%1$s ist nicht in dieser Liste</string>
|
||||
<string name="follow_set_man_dialog_title">Deine Folge-Sets</string>
|
||||
<string name="follow_set_empty_dialog_msg">Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen.</string>
|
||||
<string name="follow_set_error_dialog_msg">Beim Abrufen ist ein Problem aufgetreten: %1$s</string>
|
||||
@@ -1069,4 +1068,24 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="crashreport_found_send">Senden</string>
|
||||
<string name="this_message_will_disappear_in_days">Diese Nachricht verschwindet in %1$d Tagen</string>
|
||||
<string name="select_signer">Signierer auswählen</string>
|
||||
|
||||
<string name="follow_set_create_btn_label">Neu</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s ist ein öffentliches Mitglied</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s ist ein privates Mitglied</string>
|
||||
<string name="follow_set_public_member_add_label">Als öffentliches Mitglied hinzufügen</string>
|
||||
<string name="follow_set_private_member_add_label">Als privates Mitglied hinzufügen</string>
|
||||
<string name="follow_set_public_members_header_label">Öffentliche Profile</string>
|
||||
<string name="follow_set_private_members_header_label">Private Profile</string>
|
||||
<string name="follow_set_single_member_label">Mitglied</string>
|
||||
<string name="follow_set_multiple_member_label">Mitglieder</string>
|
||||
<string name="follow_set_empty_label">Keine Mitglieder</string>
|
||||
<string name="follow_set_copy_dialog_title">Follow-Set kopieren/klonen</string>
|
||||
<string name="follow_set_desc_modify_label">Beschreibung bearbeiten</string>
|
||||
<string name="follow_set_empty_desc_label">Diese Liste hat keine Beschreibung</string>
|
||||
<string name="follow_set_current_desc_label">Aktuelle Beschreibung:</string>
|
||||
<string name="follow_set_copy_indicator_description">Unten kannst du einen eigenen Namen/Beschreibung für diesen Klon festlegen.</string>
|
||||
<string name="follow_set_copy_name_label">(Original-Set-Name)</string>
|
||||
<string name="follow_set_copy_desc_label">(Original-Set-Beschreibung)</string>
|
||||
<string name="follow_set_copy_action_btn_label">Set kopieren/klonen</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Bearbeiten</string>
|
||||
</resources>
|
||||
|
||||
@@ -1001,8 +1001,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="follow_set_empty_feed_msg">Es scheint, dass du noch keine Folge-Sets hast.\nTippe unten zum Aktualisieren oder verwende die Plus-Taste, um ein neues zu erstellen.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Autor zum Folge-Set hinzufügen</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Benutzer zu Listen hinzufügen oder entfernen, oder eine neue Liste mit diesem Benutzer erstellen.</string>
|
||||
<string name="follow_set_type_description">Symbol für %1$s-Liste</string>
|
||||
<string name="follow_set_presence_indicator">%1$s ist in dieser Liste</string>
|
||||
<string name="follow_set_icon_description">Symbol für %1$s-Liste</string>
|
||||
<string name="follow_set_absence_indicator">%1$s ist nicht in dieser Liste</string>
|
||||
<string name="follow_set_man_dialog_title">Deine Folge-Sets</string>
|
||||
<string name="follow_set_empty_dialog_msg">Keine Folge-Sets gefunden oder du hast keine. Tippe unten zum Aktualisieren oder verwende das Menü, um eines zu erstellen.</string>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Publicar</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="rename">Cambiar nombre</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="failed_to_upload_the_image">No se pudo cargar la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
@@ -445,6 +446,28 @@
|
||||
<string name="follow_list_aroundme">A mi alrededor</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="follow_sets">Conjuntos de seguimiento</string>
|
||||
<string name="labeled_bookmarks">Marcadores etiquetados</string>
|
||||
<string name="general_bookmarks">Marcadores generales</string>
|
||||
<string name="follow_set_type_public">Público</string>
|
||||
<string name="follow_set_type_private">Privado</string>
|
||||
<string name="follow_set_type_mixed">Mixto</string>
|
||||
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
|
||||
\nPulsa abajo para actualizar o pulsa los botones de añadir para crear uno nuevo.
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">Añadir autor a conjunto de seguimiento</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Añadir o eliminar usuario de las listas, o crear una nueva lista con este usuario.</string>
|
||||
<string name="follow_set_absence_indicator">%1$s no se encuentra en esta lista</string>
|
||||
<string name="follow_set_man_dialog_title">Tus conjuntos de seguimiento</string>
|
||||
<string name="follow_set_empty_dialog_msg">No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Pulsa abajo para actualizar o utiliza el menú para crear uno.</string>
|
||||
<string name="follow_set_error_dialog_msg">Ha habido un problema al recuperar: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Crear nueva lista</string>
|
||||
<string name="follow_set_creation_name_label">Nombre del conjunto</string>
|
||||
<string name="follow_set_creation_desc_label">Descripción del conjunto (opcional)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Crear conjunto</string>
|
||||
<string name="follow_set_rename_btn_label">Cambiar nombre del conjunto</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Estás cambiando el nombre de </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> a..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">El puerto predeterminado es 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@@ -832,6 +855,8 @@
|
||||
<string name="media_compression_quality_medium">Media</string>
|
||||
<string name="media_compression_quality_high">Alta</string>
|
||||
<string name="media_compression_quality_uncompressed">Sin comprimir</string>
|
||||
<string name="video_codec_h265_label">Usa H.265/HEVC Codec</string>
|
||||
<string name="video_codec_h265_description">Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265.</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@@ -1014,6 +1039,8 @@
|
||||
<string name="torrent_download">Descargar</string>
|
||||
<string name="torrent_failure">Error al abrir el archivo</string>
|
||||
<string name="torrent_no_apps">No hay aplicaciones torrent instaladas para abrir y descargar el archivo.</string>
|
||||
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
|
||||
<string name="my_lists_and_sets">Mis listas/conjuntos</string>
|
||||
<string name="select_list_to_filter">Selecciona una lista para filtrar el tablón</string>
|
||||
<string name="temporary_account">Cerrar sesión al bloquear dispositivo</string>
|
||||
<string name="private_message">Mensaje privado</string>
|
||||
@@ -1035,4 +1062,5 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">¿Te gustaría enviar el informe de error reciente a Amethyst en un mensaje directo? No se compartirá información personal</string>
|
||||
<string name="crashreport_found_send">Enviarlo</string>
|
||||
<string name="this_message_will_disappear_in_days">Este mensaje desaparecerá en %1$d días</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
</resources>
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
<string name="post">Publicar</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="rename">Cambiar nombre</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="failed_to_upload_the_image">Error al subir la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
@@ -440,6 +441,28 @@
|
||||
<string name="follow_list_aroundme">A mi alrededor</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="follow_sets">Conjuntos de seguimiento</string>
|
||||
<string name="labeled_bookmarks">Marcadores etiquetados</string>
|
||||
<string name="general_bookmarks">Marcadores generales</string>
|
||||
<string name="follow_set_type_public">Público</string>
|
||||
<string name="follow_set_type_private">Privado</string>
|
||||
<string name="follow_set_type_mixed">Mixto</string>
|
||||
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
|
||||
\nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo.
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">Agregar autor a conjunto de seguimiento</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario.</string>
|
||||
<string name="follow_set_absence_indicator">%1$s no se encuentra en esta lista</string>
|
||||
<string name="follow_set_man_dialog_title">Tus conjuntos de seguimiento</string>
|
||||
<string name="follow_set_empty_dialog_msg">No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno.</string>
|
||||
<string name="follow_set_error_dialog_msg">Hubo un problema al recuperar: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Crear nueva lista</string>
|
||||
<string name="follow_set_creation_name_label">Nombre del conjunto</string>
|
||||
<string name="follow_set_creation_desc_label">Descripción del conjunto (opcional)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Crear conjunto</string>
|
||||
<string name="follow_set_rename_btn_label">Cambiar nombre del conjunto</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Estás cambiando el nombre de </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> a..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">El puerto predeterminado es 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@@ -822,6 +845,8 @@
|
||||
<string name="media_compression_quality_medium">Media</string>
|
||||
<string name="media_compression_quality_high">Alta</string>
|
||||
<string name="media_compression_quality_uncompressed">Sin comprimir</string>
|
||||
<string name="video_codec_h265_label">Usa H.265/HEVC Codec</string>
|
||||
<string name="video_codec_h265_description">Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265.</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@@ -1001,6 +1026,8 @@
|
||||
<string name="torrent_download">Descargar</string>
|
||||
<string name="torrent_failure">Error al abrir el archivo</string>
|
||||
<string name="torrent_no_apps">No hay aplicaciones torrent instaladas para abrir y descargar el archivo.</string>
|
||||
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
|
||||
<string name="my_lists_and_sets">Mis listas/conjuntos</string>
|
||||
<string name="select_list_to_filter">Selecciona una lista para filtrar el feed</string>
|
||||
<string name="temporary_account">Cerrar sesión al bloquear dispositivo</string>
|
||||
<string name="private_message">Mensaje privado</string>
|
||||
@@ -1011,4 +1038,5 @@
|
||||
<string name="search_by_hashtag">Buscar hashtag: #%1$s</string>
|
||||
<string name="dont_translate_from">No traducir del</string>
|
||||
<string name="dont_translate_from_description">Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo.</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
</resources>
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
<string name="post">Publicar</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="rename">Cambiar nombre</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="failed_to_upload_the_image">Error al subir la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
@@ -440,6 +441,28 @@
|
||||
<string name="follow_list_aroundme">A mi alrededor</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="follow_sets">Conjuntos de seguimiento</string>
|
||||
<string name="labeled_bookmarks">Marcadores etiquetados</string>
|
||||
<string name="general_bookmarks">Marcadores generales</string>
|
||||
<string name="follow_set_type_public">Público</string>
|
||||
<string name="follow_set_type_private">Privado</string>
|
||||
<string name="follow_set_type_mixed">Mixto</string>
|
||||
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
|
||||
\nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo.
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">Agregar autor a conjunto de seguimiento</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Agregar o quitar usuario de las listas, o crear una nueva lista con este usuario.</string>
|
||||
<string name="follow_set_absence_indicator">%1$s no se encuentra en esta lista</string>
|
||||
<string name="follow_set_man_dialog_title">Tus conjuntos de seguimiento</string>
|
||||
<string name="follow_set_empty_dialog_msg">No se han encontrado conjuntos de seguimiento, o no tienes ningún conjunto de seguimiento. Toca abajo para actualizar o utiliza el menú para crear uno.</string>
|
||||
<string name="follow_set_error_dialog_msg">Hubo un problema al recuperar: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Crear nueva lista</string>
|
||||
<string name="follow_set_creation_name_label">Nombre del conjunto</string>
|
||||
<string name="follow_set_creation_desc_label">Descripción del conjunto (opcional)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Crear conjunto</string>
|
||||
<string name="follow_set_rename_btn_label">Cambiar nombre del conjunto</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Estás cambiando el nombre de </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> a..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">El puerto predeterminado es 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@@ -822,6 +845,8 @@
|
||||
<string name="media_compression_quality_medium">Media</string>
|
||||
<string name="media_compression_quality_high">Alta</string>
|
||||
<string name="media_compression_quality_uncompressed">Sin comprimir</string>
|
||||
<string name="video_codec_h265_label">Usa H.265/HEVC Codec</string>
|
||||
<string name="video_codec_h265_description">Mejor calidad en tamaños de archivos más pequeños, pero no todos los dispositivos admiten la reproducción H.265.</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@@ -1001,6 +1026,8 @@
|
||||
<string name="torrent_download">Descargar</string>
|
||||
<string name="torrent_failure">Error al abrir el archivo</string>
|
||||
<string name="torrent_no_apps">No hay aplicaciones torrent instaladas para abrir y descargar el archivo.</string>
|
||||
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
|
||||
<string name="my_lists_and_sets">Mis listas/conjuntos</string>
|
||||
<string name="select_list_to_filter">Selecciona una lista para filtrar el feed</string>
|
||||
<string name="temporary_account">Cerrar sesión al bloquear dispositivo</string>
|
||||
<string name="private_message">Mensaje privado</string>
|
||||
@@ -1011,4 +1038,5 @@
|
||||
<string name="search_by_hashtag">Buscar hashtag: #%1$s</string>
|
||||
<string name="dont_translate_from">No traducir del</string>
|
||||
<string name="dont_translate_from_description">Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo.</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
</resources>
|
||||
|
||||
@@ -457,9 +457,8 @@
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">लेखक जोडें अनुगम्य सूची में</string>
|
||||
<string name="follow_set_profile_actions_menu_description">प्रयोक्ता को सूचियों में जोडें अथवा हटाएँ अथवा इस प्रयोक्ता के साथ नई सूची बनाएँ।</string>
|
||||
<string name="follow_set_type_description">सूची %1$s के लिए चिह्न</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s इस सूची में है"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s इस सूची में नहीं है"</string>
|
||||
<string name="follow_set_icon_description">सूची %1$s के लिए चिह्न</string>
|
||||
<string name="follow_set_absence_indicator">%1$s इस सूची में नहीं है</string>
|
||||
<string name="follow_set_man_dialog_title">आपके अनुगम्य सूचियाँ</string>
|
||||
<string name="follow_set_empty_dialog_msg">कोई अनुगम्य सूचियाँ प्राप्त नहीं। अथवा आपका कोई अनुगम्य सूचियाँ हैं नहीं। नवीकरण के लिए नीचे दबाएँ अथवा विकल्पसूची द्वारा एक नया बनाएँ।</string>
|
||||
<string name="follow_set_error_dialog_msg">लाने में अपक्रम : %1$s</string>
|
||||
@@ -860,6 +859,8 @@
|
||||
<string name="media_compression_quality_medium">मध्यम</string>
|
||||
<string name="media_compression_quality_high">उच्च</string>
|
||||
<string name="media_compression_quality_uncompressed">असंकुचित</string>
|
||||
<string name="video_codec_h265_label">उच्च कार्यक्षमता चलचित्र संकुचनविस्तारण अथवा एच॰२६५ क्रमलेख का प्रयोग करें</string>
|
||||
<string name="video_codec_h265_description">अधिक गुणवत्ता के साथ सूक्ष्मतर अभिलेख आकार परन्तु एच॰२६५ चालन सभी यन्त्रों द्वारा अवलम्बित नहीं।</string>
|
||||
<string name="edit_draft">पाण्डुलिपि का सम्पादन करें</string>
|
||||
<string name="login_with_qr_code">क्यूआर॰ क्रमचित्र के साथ प्रवेशांकन करें</string>
|
||||
<string name="route">मार्ग</string>
|
||||
|
||||
@@ -457,9 +457,8 @@
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">Szerző hozzáadása a követési gyűjteményhez</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Felhasználó hozzáadása vagy eltávolítása a listákból, vagy új lista létrehozása ezzel a felhasználóval.</string>
|
||||
<string name="follow_set_type_description">Ikon a(z) %1$s nevű listához</string>
|
||||
<string name="follow_set_presence_indicator">"A(z) %1$s már a létezik a listában"</string>
|
||||
<string name="follow_set_absence_indicator">"A(z) %1$s nincs a listában"</string>
|
||||
<string name="follow_set_icon_description">Ikon a(z) %1$s nevű listához</string>
|
||||
<string name="follow_set_absence_indicator">A(z) %1$s nincs a listában</string>
|
||||
<string name="follow_set_man_dialog_title">Saját követési gyüjtemények</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nem találhatók követési gyüjtemények, vagy nincs követési gyüjteménye. Érintse meg az alábbi gombot a frissítéshez vagy használja a menüt egy gyüjtemény létrehozásához.</string>
|
||||
<string name="follow_set_error_dialog_msg">Probléma történt a következő lekérdezésekor: %1$s</string>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<string name="yes">Jā</string>
|
||||
<string name="no">Nē</string>
|
||||
<string name="follow_list_selection">Sekot saraksts</string>
|
||||
<string name="follow_set_type_description">Ikona %1$s sarakstam</string>
|
||||
<string name="follow_set_icon_description">Ikona %1$s sarakstam</string>
|
||||
<string name="follow_set_creation_menu_title">Izveidot jaunu sarakstu</string>
|
||||
<string name="follow_set_creation_dialog_title">Jaunais %1$s saraksts</string>
|
||||
<string name="follow_set_creation_name_label">Kolekcijas nosaukums</string>
|
||||
|
||||
@@ -454,9 +454,8 @@
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">Dodaj autora do zbioru obserwowanych</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Dodaj lub usuń użytkownika z list, lub utwórz nową listę z tym użytkownikiem.</string>
|
||||
<string name="follow_set_type_description">Ikona dla listy %1$s</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s jest obecny na liście"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s nie jest na liście"</string>
|
||||
<string name="follow_set_icon_description">Ikona dla listy %1$s</string>
|
||||
<string name="follow_set_absence_indicator">%1$s nie jest na liście</string>
|
||||
<string name="follow_set_man_dialog_title">Twój zbiór obserwowanych</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nie znaleziono zbiorów obserwowanych lub nie masz żadnych zbiorów obserwowanych. Dotknij poniżej, aby odświeżyć lub użyj menu, aby go utworzyć.</string>
|
||||
<string name="follow_set_error_dialog_msg">Podczas pobierania wystąpił błąd: %1$s</string>
|
||||
|
||||
@@ -453,9 +453,8 @@
|
||||
<string name="follow_set_empty_feed_msg">Parece que você ainda não tem conjuntos de seguimento.\nToque abaixo para atualizar ou use o botão de adicionar para criar um novo.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Adicionar autor ao conjunto de seguimento</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Adicionar ou remover usuário de listas, ou criar uma nova lista com este usuário.</string>
|
||||
<string name="follow_set_type_description">Ícone da lista %1$s</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s está presente nesta lista"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s não está nesta lista"</string>
|
||||
<string name="follow_set_icon_description">Ícone da lista %1$s</string>
|
||||
<string name="follow_set_absence_indicator">%1$s não está nesta lista</string>
|
||||
<string name="follow_set_man_dialog_title">Seus conjuntos de seguimento</string>
|
||||
<string name="follow_set_empty_dialog_msg">Nenhum conjunto de seguimento foi encontrado ou você não possui nenhum. Toque abaixo para atualizar ou use o menu para criar um.</string>
|
||||
<string name="follow_set_error_dialog_msg">Houve um problema ao buscar: %1$s</string>
|
||||
@@ -1064,4 +1063,24 @@
|
||||
<string name="crashreport_found_send">Enviar</string>
|
||||
<string name="this_message_will_disappear_in_days">Esta mensagem desaparecerá em %1$d dias</string>
|
||||
<string name="select_signer">Selecionar assinador</string>
|
||||
|
||||
<string name="follow_set_create_btn_label">Novo</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s é um membro público</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s é um membro privado</string>
|
||||
<string name="follow_set_public_member_add_label">Adicionar como membro público</string>
|
||||
<string name="follow_set_private_member_add_label">Adicionar como membro privado</string>
|
||||
<string name="follow_set_public_members_header_label">Perfis públicos</string>
|
||||
<string name="follow_set_private_members_header_label">Perfis privados</string>
|
||||
<string name="follow_set_single_member_label">membro</string>
|
||||
<string name="follow_set_multiple_member_label">membros</string>
|
||||
<string name="follow_set_empty_label">Nenhum membro</string>
|
||||
<string name="follow_set_copy_dialog_title">Copiar/Clonar conjunto de seguidores</string>
|
||||
<string name="follow_set_desc_modify_label">Modificar descrição</string>
|
||||
<string name="follow_set_empty_desc_label">Esta lista não tem descrição</string>
|
||||
<string name="follow_set_current_desc_label">Descrição atual:</string>
|
||||
<string name="follow_set_copy_indicator_description">Você pode definir um nome/descrição personalizada para este conjunto clonado abaixo.</string>
|
||||
<string name="follow_set_copy_name_label">(Nome original do conjunto)</string>
|
||||
<string name="follow_set_copy_desc_label">(Descrição original do conjunto)</string>
|
||||
<string name="follow_set_copy_action_btn_label">Copiar/Clonar conjunto</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Modificar</string>
|
||||
</resources>
|
||||
|
||||
@@ -123,6 +123,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="post">Pošlji</string>
|
||||
<string name="save">Shrani</string>
|
||||
<string name="create">Ustvari</string>
|
||||
<string name="rename">Preimenuj</string>
|
||||
<string name="cancel">Prekliči</string>
|
||||
<string name="failed_to_upload_the_image">Neuspelo nalaganje slike</string>
|
||||
<string name="relay_address">Naslov releja</string>
|
||||
@@ -456,8 +457,13 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="follow_list_aroundme">V moji okolici</string>
|
||||
<string name="follow_list_global">Globalno</string>
|
||||
<string name="follow_list_mute_list">Spisek utišanih</string>
|
||||
<string name="labeled_bookmarks">Označeni zaznamki</string>
|
||||
<string name="general_bookmarks">Splošni zaznamki</string>
|
||||
<string name="follow_set_type_public">Javno</string>
|
||||
<string name="follow_set_type_private">Zasebno</string>
|
||||
<string name="follow_set_type_mixed">Mešano</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Dodaj ali odstrani uprabnika iz seznama, ali pa ustvari nov seznam s tem uporabnikom.</string>
|
||||
<string name="follow_set_creation_menu_title">Ustvari nov seznam</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Prevzeta vrata so 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Poveži se preko Tor omrežja z Orbot aplikacijo
|
||||
\n\n1. Namesti [Orbot aplikacijo](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -845,6 +851,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="media_compression_quality_medium">Srednja</string>
|
||||
<string name="media_compression_quality_high">Visoka</string>
|
||||
<string name="media_compression_quality_uncompressed">Nestisnjeno</string>
|
||||
<string name="video_codec_h265_label">Uporabi H.265/HEVC Codec</string>
|
||||
<string name="edit_draft">Uredi osnutek</string>
|
||||
<string name="login_with_qr_code">Vpiši se z QR kodo</string>
|
||||
<string name="route">Pot</string>
|
||||
@@ -1049,4 +1056,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Želite poslati poročilo o napaki Amethyst-u preko zasebnega sporočila? Vaši osebni podatki NE bojo posredovani.</string>
|
||||
<string name="crashreport_found_send">Pošlji</string>
|
||||
<string name="this_message_will_disappear_in_days">To sporočilo bo izginilo čez %1$d dni</string>
|
||||
<string name="select_signer">Izberi podpisnika</string>
|
||||
</resources>
|
||||
|
||||
@@ -453,9 +453,8 @@
|
||||
<string name="follow_set_empty_feed_msg">Det verkar som att du inte har några följ-set ännu.\nTryck nedan för att uppdatera eller använd plusknappen för att skapa ett nytt.</string>
|
||||
<string name="follow_set_add_author_from_note_action">Lägg till författare i följ-set</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Lägg till eller ta bort användare från listor, eller skapa en ny lista med denna användare.</string>
|
||||
<string name="follow_set_type_description">Ikon för %1$s-lista</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s finns i denna lista"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s finns inte i denna lista"</string>
|
||||
<string name="follow_set_icon_description">Ikon för %1$s-lista</string>
|
||||
<string name="follow_set_absence_indicator">%1$s finns inte i denna lista</string>
|
||||
<string name="follow_set_man_dialog_title">Dina följ-set</string>
|
||||
<string name="follow_set_empty_dialog_msg">Inga följ-set hittades, eller så har du inga. Tryck nedan för att uppdatera eller använd menyn för att skapa ett.</string>
|
||||
<string name="follow_set_error_dialog_msg">Ett problem uppstod vid hämtning: %1$s</string>
|
||||
@@ -1063,4 +1062,24 @@
|
||||
<string name="crashreport_found_send">Skicka</string>
|
||||
<string name="this_message_will_disappear_in_days">Detta meddelande försvinner om %1$d dagar</string>
|
||||
<string name="select_signer">Välj signatör</string>
|
||||
<string name="follow_set_create_btn_label">Ny</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s är en offentlig medlem</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s är en privat medlem</string>
|
||||
<string name="follow_set_public_member_add_label">Lägg till som offentlig medlem</string>
|
||||
<string name="follow_set_private_member_add_label">Lägg till som privat medlem</string>
|
||||
<string name="follow_set_public_members_header_label">Offentliga profiler</string>
|
||||
<string name="follow_set_private_members_header_label">Privata profiler</string>
|
||||
<string name="follow_set_single_member_label">medlem</string>
|
||||
<string name="follow_set_multiple_member_label">medlemmar</string>
|
||||
<string name="follow_set_empty_label">Inga medlemmar</string>
|
||||
<string name="follow_set_copy_dialog_title">Kopiera/Klona följlista</string>
|
||||
<string name="follow_set_desc_modify_label">Ändra beskrivning</string>
|
||||
<string name="follow_set_empty_desc_label">Den här listan har ingen beskrivning</string>
|
||||
<string name="follow_set_current_desc_label">Nuvarande beskrivning:</string>
|
||||
<string name="follow_set_copy_indicator_description">Du kan ange ett anpassat namn/beskrivning för denna klonlista nedan.</string>
|
||||
<string name="follow_set_copy_name_label">(Ursprungligt listnamn)</string>
|
||||
<string name="follow_set_copy_desc_label">(Ursprunglig beskrivning)</string>
|
||||
<string name="follow_set_copy_action_btn_label">Kopiera/Klona lista</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Ändra</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -457,9 +457,8 @@
|
||||
</string>
|
||||
<string name="follow_set_add_author_from_note_action">添加作者到关注集</string>
|
||||
<string name="follow_set_profile_actions_menu_description">从列表中添加或删除用户,或用此用户创建一个新列表。</string>
|
||||
<string name="follow_set_type_description">%1$s 列表的图标</string>
|
||||
<string name="follow_set_presence_indicator">"此列表中有 %1$s"</string>
|
||||
<string name="follow_set_absence_indicator">"此列表中没有 %1$s"</string>
|
||||
<string name="follow_set_icon_description">%1$s 列表的图标</string>
|
||||
<string name="follow_set_absence_indicator">此列表中没有 %1$s</string>
|
||||
<string name="follow_set_man_dialog_title">您的关注集</string>
|
||||
<string name="follow_set_empty_dialog_msg">未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。</string>
|
||||
<string name="follow_set_error_dialog_msg">获取时出了问题: %1$s</string>
|
||||
|
||||
@@ -519,22 +519,40 @@
|
||||
It seems you do not have any follow sets yet.
|
||||
\nTap below to refresh, or tap the add buttons to create a new one.
|
||||
</string>
|
||||
<string name="follow_set_create_btn_label">New</string>
|
||||
<string name="follow_set_add_author_from_note_action">Add author to follow set</string>
|
||||
<string name="follow_set_profile_actions_menu_description">Add or remove user from lists, or create a new list with this user.</string>
|
||||
<string name="follow_set_type_description">Icon for %1$s List</string>
|
||||
<string name="follow_set_presence_indicator">"%1$s is present in this list"</string>
|
||||
<string name="follow_set_absence_indicator">"%1$s is not in this list"</string>
|
||||
<string name="follow_set_icon_description">Icon for follow set</string>
|
||||
<string name="follow_set_public_presence_indicator">%1$s is a public member</string>
|
||||
<string name="follow_set_private_presence_indicator">%1$s is a private member</string>
|
||||
<string name="follow_set_public_member_add_label">Add as public member</string>
|
||||
<string name="follow_set_private_member_add_label">Add as private member</string>
|
||||
<string name="follow_set_public_members_header_label">Public Profiles</string>
|
||||
<string name="follow_set_private_members_header_label">Private Profiles</string>
|
||||
<string name="follow_set_single_member_label">member</string>
|
||||
<string name="follow_set_multiple_member_label">members</string>
|
||||
<string name="follow_set_empty_label">No members</string>
|
||||
<string name="follow_set_absence_indicator">%1$s is not in this list</string>
|
||||
<string name="follow_set_man_dialog_title">Your Follow Sets</string>
|
||||
<string name="follow_set_empty_dialog_msg">No follow sets were found, or you don\'t have any follow sets. Tap below to refresh, or use the menu to create one.</string>
|
||||
<string name="follow_set_error_dialog_msg">There was a problem while fetching: %1$s</string>
|
||||
<string name="follow_set_creation_menu_title">Make New List</string>
|
||||
<string name="follow_set_creation_item_label">"Create new %1$s list with user</string>
|
||||
<string name="follow_set_creation_item_description">Creates a %1$s follow set, and adds %2$s to it.</string>
|
||||
<string name="follow_set_creation_dialog_title">New %1$s List</string>
|
||||
<string name="follow_set_creation_item_label">New list with %1$s membership</string>
|
||||
<string name="follow_set_creation_item_description">Creates a new follow set, and adds %1$s as a %2$s member.</string>
|
||||
<string name="follow_set_creation_dialog_title">New Follow Set</string>
|
||||
<string name="follow_set_copy_dialog_title">Copy/Clone Follow Set</string>
|
||||
<string name="follow_set_desc_modify_label">Modify description</string>
|
||||
<string name="follow_set_empty_desc_label">This list doesn\'t have a description</string>
|
||||
<string name="follow_set_current_desc_label">Current description:</string>
|
||||
<string name="follow_set_copy_indicator_description">You can set a custom name/description for this clone set below.</string>
|
||||
<string name="follow_set_creation_name_label">Set name</string>
|
||||
<string name="follow_set_copy_name_label">(Original Set name)</string>
|
||||
<string name="follow_set_creation_desc_label">Set description(optional)</string>
|
||||
<string name="follow_set_copy_desc_label">(Original Set description)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Create set</string>
|
||||
<string name="follow_set_copy_action_btn_label">Copy/Clone set</string>
|
||||
<string name="follow_set_rename_btn_label">Rename set</string>
|
||||
<string name="follow_set_desc_modify_btn_label">Modify</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">You are renaming from </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> to..</string>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ languageId = "17.0.6"
|
||||
lazysodiumAndroid = "5.2.0"
|
||||
lazysodiumJava = "5.2.0"
|
||||
lifecycleRuntimeKtx = "2.9.4"
|
||||
lightcompressor = "1.5.0"
|
||||
lightcompressor-enhanced = "1.6.0"
|
||||
markdown = "f92ef49c9d"
|
||||
media3 = "1.8.0"
|
||||
mockk = "1.14.5"
|
||||
@@ -63,7 +63,7 @@ core = "1.7.0"
|
||||
mavenPublish = "0.34.0"
|
||||
|
||||
[libraries]
|
||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor" }
|
||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" }
|
||||
accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "accompanistAdaptive" }
|
||||
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistAdaptive" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
@@ -102,6 +102,7 @@ androidx-security-crypto-ktx = { group = "androidx.security", name = "security-c
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
audiowaveform = { group = "com.github.lincollincol", name = "compose-audiowaveform", version.ref = "audiowaveform" }
|
||||
@@ -128,6 +129,7 @@ markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", nam
|
||||
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
|
||||
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
|
||||
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" }
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ class BookmarkListEvent(
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
+123
-40
@@ -155,6 +155,31 @@ class PeopleListEvent(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: PeopleListEvent,
|
||||
person: UserTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): PeopleListEvent {
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
publicTags = earlierVersion.tags,
|
||||
privateTags = privateTags.remove(person.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
return resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.remove(person.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
publicTags: TagArray,
|
||||
privateTags: TagArray,
|
||||
@@ -223,51 +248,61 @@ class PeopleListEvent(
|
||||
title: String,
|
||||
description: String? = null,
|
||||
isPrivate: Boolean,
|
||||
firstMemberHex: String? = null,
|
||||
firstPublicMembers: List<String> = emptyList(),
|
||||
firstPrivateMembers: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
if (description == null) {
|
||||
val newList =
|
||||
create(
|
||||
name = title,
|
||||
person = UserTag(pubKey = firstMemberHex ?: signer.pubKey),
|
||||
isPrivate = isPrivate,
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
onReady(newList)
|
||||
} else {
|
||||
if (isPrivate) {
|
||||
val event =
|
||||
build(
|
||||
name = title,
|
||||
privatePeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)),
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
addUnique(arrayOf("description", description))
|
||||
}
|
||||
val list = signer.sign(event)
|
||||
onReady(list)
|
||||
} else {
|
||||
val event =
|
||||
build(
|
||||
name = title,
|
||||
publicPeople = listOf(UserTag(pubKey = firstMemberHex ?: signer.pubKey)),
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
addUnique(arrayOf("description", description))
|
||||
}
|
||||
val list = signer.sign(event)
|
||||
onReady(list)
|
||||
val newListTemplate =
|
||||
build(
|
||||
name = title,
|
||||
publicPeople =
|
||||
if (!isPrivate && firstPublicMembers.isNotEmpty()) {
|
||||
firstPublicMembers.map { UserTag(pubKey = it) }
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
privatePeople =
|
||||
if (isPrivate && firstPrivateMembers.isNotEmpty()) {
|
||||
firstPrivateMembers.map { UserTag(pubKey = it) }
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
if (description != null) addUnique(DescriptionTag.assemble(description))
|
||||
}
|
||||
}
|
||||
val newList = signer.sign(newListTemplate)
|
||||
onReady(newList)
|
||||
}
|
||||
|
||||
suspend fun copy(
|
||||
dTag: String,
|
||||
title: String,
|
||||
description: String? = null,
|
||||
firstPublicMembers: List<String> = emptyList(),
|
||||
firstPrivateMembers: List<String> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
) {
|
||||
val cloneTemplate =
|
||||
build(
|
||||
name = title,
|
||||
publicPeople = firstPublicMembers.map { UserTag(pubKey = it) },
|
||||
privatePeople = firstPrivateMembers.map { UserTag(pubKey = it) },
|
||||
signer = signer,
|
||||
dTag = dTag,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
if (description != null) addUnique(DescriptionTag.assemble(description))
|
||||
}
|
||||
|
||||
val listClone = signer.sign(cloneTemplate)
|
||||
onReady(listClone)
|
||||
}
|
||||
|
||||
suspend fun createListWithUser(
|
||||
@@ -311,6 +346,7 @@ class PeopleListEvent(
|
||||
suspend fun removeUser(
|
||||
earlierVersion: PeopleListEvent,
|
||||
pubKeyHex: String,
|
||||
isUserPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit,
|
||||
@@ -319,6 +355,7 @@ class PeopleListEvent(
|
||||
remove(
|
||||
earlierVersion = earlierVersion,
|
||||
person = UserTag(pubKey = pubKeyHex),
|
||||
isPrivate = isUserPrivate,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
@@ -351,5 +388,51 @@ class PeopleListEvent(
|
||||
)
|
||||
onReady(modified)
|
||||
}
|
||||
|
||||
suspend fun modifyDescription(
|
||||
earlierVersion: PeopleListEvent,
|
||||
newDescription: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PeopleListEvent) -> Unit = {},
|
||||
) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
val currentDescriptionTag = earlierVersion.tags.firstOrNull { it[0] == DescriptionTag.TAG_NAME }
|
||||
val currentDescription = currentDescriptionTag?.get(1)
|
||||
if (currentDescription.equals(newDescription)) {
|
||||
// Do nothing
|
||||
return
|
||||
} else {
|
||||
if (newDescription == null || newDescription.isEmpty()) {
|
||||
val modified =
|
||||
resign(
|
||||
publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME },
|
||||
privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME },
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
onReady(modified)
|
||||
} else {
|
||||
val newDescriptionTag = DescriptionTag.assemble(newDescription)
|
||||
val modified =
|
||||
if (currentDescriptionTag == null) {
|
||||
resign(
|
||||
publicTags = earlierVersion.tags.plusElement(newDescriptionTag),
|
||||
privateTags = privateTags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
publicTags = earlierVersion.tags.replaceAll(currentDescriptionTag, newDescriptionTag),
|
||||
privateTags = privateTags.replaceAll(currentDescriptionTag, newDescriptionTag),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
onReady(modified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.quartz.nip51Lists.bookmarkList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
import com.vitorpamplona.quartz.utils.nsecToKeyPair
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BookmarkListEventTest {
|
||||
private val signer = NostrSignerInternal("nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair())
|
||||
|
||||
@Test
|
||||
fun publicTagsPreservedWhenRemovingFromPrivateBookmarks() =
|
||||
runBlocking {
|
||||
// Create a test event bookmark
|
||||
val testEventId = "a".repeat(64)
|
||||
val testBookmark = EventBookmark(testEventId)
|
||||
|
||||
// Create a bookmark list with event in public bookmarks
|
||||
val initialEvent =
|
||||
BookmarkListEvent.create(
|
||||
publicBookmarks = listOf(testBookmark),
|
||||
privateBookmarks = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = 1740669816,
|
||||
)
|
||||
|
||||
// Count the public tags (should be 1 for the bookmark)
|
||||
val initialPublicTagCount = initialEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId }
|
||||
assertTrue(
|
||||
initialPublicTagCount == 1,
|
||||
"Should have exactly 1 public bookmark tag initially",
|
||||
)
|
||||
|
||||
// Try to remove the bookmark from private bookmarks (even though it's only in public)
|
||||
// This simulates the scenario where a bookmark exists in both lists
|
||||
val updatedEvent =
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = initialEvent,
|
||||
bookmarkIdTag = testBookmark,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
createdAt = 1740669817,
|
||||
)
|
||||
|
||||
// Count the public tags again
|
||||
val finalPublicTagCount = updatedEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId }
|
||||
|
||||
// CRITICAL: The public tag count should be unchanged
|
||||
// Without the fix, this would be 0 because the bug also removed public tags
|
||||
assertTrue(
|
||||
finalPublicTagCount == initialPublicTagCount,
|
||||
"Public tags should be preserved when removing from private bookmarks. Initial: $initialPublicTagCount, Final: $finalPublicTagCount",
|
||||
)
|
||||
|
||||
// Also verify using publicBookmarks() method
|
||||
assertTrue(
|
||||
updatedEvent.publicBookmarks().any { it is EventBookmark && it.eventId == testEventId },
|
||||
"Bookmark should still be accessible via publicBookmarks() method",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publicTagsRemovedWhenRemovingFromPublicBookmarks() =
|
||||
runBlocking {
|
||||
// Create a test event bookmark
|
||||
val testEventId = "b".repeat(64)
|
||||
val testBookmark = EventBookmark(testEventId)
|
||||
|
||||
// Create a bookmark list with event only in public bookmarks
|
||||
val initialEvent =
|
||||
BookmarkListEvent.create(
|
||||
publicBookmarks = listOf(testBookmark),
|
||||
privateBookmarks = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = 1740669816,
|
||||
)
|
||||
|
||||
// Remove from public bookmarks
|
||||
val updatedEvent =
|
||||
BookmarkListEvent.remove(
|
||||
earlierVersion = initialEvent,
|
||||
bookmarkIdTag = testBookmark,
|
||||
isPrivate = false,
|
||||
signer = signer,
|
||||
createdAt = 1740669817,
|
||||
)
|
||||
|
||||
// Verify the bookmark was removed from public bookmarks
|
||||
val finalPublicTagCount = updatedEvent.tags.count { tag -> tag.size >= 2 && tag[0] == "e" && tag[1] == testEventId }
|
||||
assertTrue(
|
||||
finalPublicTagCount == 0,
|
||||
"Public tags should be removed when isPrivate=false",
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
updatedEvent.publicBookmarks().any { it is EventBookmark && it.eventId == testEventId },
|
||||
"Bookmark should be removed from public bookmarks",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user