Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts: # gradle/libs.versions.toml # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt
This commit is contained in:
@@ -242,16 +242,16 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
|
||||
Add the following line to your `commonMain` dependencies:
|
||||
|
||||
```gradle
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz:<Amethyst Version>')
|
||||
```
|
||||
|
||||
Variations to each platform are also available:
|
||||
|
||||
```gradle
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz-android:<Amethyst Version>')
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz-jvm:<Amethyst Version>')
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz-iosarm64:<Amethyst Version>')
|
||||
implementation('com.github.vitorpamplona.amethyst:quartz-iossimulatorarm64:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-android:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-jvm:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-iosarm64:<Amethyst Version>')
|
||||
implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:<Amethyst Version>')
|
||||
```
|
||||
|
||||
### How to use
|
||||
@@ -259,25 +259,25 @@ implementation('com.github.vitorpamplona.amethyst:quartz-iossimulatorarm64:<Amet
|
||||
Manage logged in users with the `KeyPair` class
|
||||
|
||||
```kt
|
||||
val keys = KeyPair() // creates a random key
|
||||
val keys = KeyPair("hex...".hexToByteArray())
|
||||
val keys = KeyPair("nsec1...".bechToBytes())
|
||||
val keys = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
|
||||
val keyPair = KeyPair() // creates a random key
|
||||
val keyPair = KeyPair("hex...".hexToByteArray())
|
||||
val keyPair = KeyPair("nsec1...".bechToBytes())
|
||||
val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
|
||||
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
|
||||
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
|
||||
```
|
||||
|
||||
Create signers that can be internal, when you have the private key or when it is a read-only user
|
||||
or external, when it is controlled by Amber in NIP-55
|
||||
Create signers that can be Internal, when you have the private key or a read-only public key,
|
||||
or External, when it is controlled by Amber in NIP-55.
|
||||
|
||||
the `NostrSignerInternal` and `NostrSignerExternal` classes.
|
||||
Use either the `NostrSignerInternal` or `NostrSignerExternal` class:
|
||||
|
||||
```kt
|
||||
val signer = NostrSignerInternal(keyPair)
|
||||
val amberSigner = NostrSignerExternal(
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
packageName = signerPackageName,
|
||||
contentResolver = appContext.contentResolver,
|
||||
packageName = signerPackageName, // Amber package name
|
||||
contentResolver = appContext.contentResolver,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -299,16 +299,19 @@ val client = NostrClient(socketBuilder, appScope)
|
||||
If you want to auth, given a logged-in `signer`:
|
||||
|
||||
```kt
|
||||
val authCoordinator = RelayAuthenticator(client, applicationIOScope) { challenge, relay ->
|
||||
val authedEvent = RelayAuthEvent.create(relayUrl, challenge, signer)
|
||||
val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay ->
|
||||
val authedEvent = RelayAuthEvent.create(relay.url, challenge, signer)
|
||||
client.sendIfExists(authedEvent, relay.url)
|
||||
}
|
||||
```
|
||||
|
||||
To manage subscriptions, the suggested approach is to use subscriptions in the Application class.
|
||||
To manage subscriptions, the simplest approach is to build mutable subscriptions in
|
||||
the Application class. To use the best of the outbox model, this class allows you to
|
||||
build filters for as many relays as needed. The `NostrClient` will connect to the
|
||||
complete set of relays for all subscriptions.
|
||||
|
||||
```kt
|
||||
val metadataSub = RelayClientSubscription(
|
||||
val metadataSub = NostrClientSubscription(
|
||||
client = client,
|
||||
filter = {
|
||||
val filters = listOf(
|
||||
@@ -330,6 +333,14 @@ val metadataSub = RelayClientSubscription(
|
||||
}
|
||||
```
|
||||
|
||||
In that way, you can simply call `metadataSub.updateFilter()` when you need to update
|
||||
subscriptions to all relays. Or call `metadataSub.closeSubscription()` to stop the sub
|
||||
without deleting it.
|
||||
|
||||
When your app goes to the background, you can use NostrClient's `connect` and `disconnect`
|
||||
methods to stop all communication to relays. Add the `connect` to your `onResume` and `disconnect`
|
||||
to `onPause` methods.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
|
||||
|
||||
@@ -151,6 +151,12 @@ android {
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
// TODO: remove this when lightcompressor uses one MP4 parser only
|
||||
packaging {
|
||||
resources {
|
||||
resources.pickFirsts.add('builddef.lst')
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["channel"]
|
||||
|
||||
@@ -334,9 +340,9 @@ dependencies {
|
||||
implementation libs.audiowaveform
|
||||
|
||||
// Video compression lib
|
||||
implementation libs.abedElazizShe.image.compressor
|
||||
implementation libs.abedElazizShe.video.compressor.fork
|
||||
// Image compression lib
|
||||
implementation libs.zelory.video.compressor
|
||||
implementation libs.zelory.image.compressor
|
||||
|
||||
// Cbor for cashuB format
|
||||
implementation libs.kotlinx.serialization.cbor
|
||||
|
||||
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
@@ -92,7 +93,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
|
||||
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
@@ -214,7 +214,6 @@ import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
@@ -266,6 +265,7 @@ class Account(
|
||||
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
|
||||
|
||||
val kind3FollowList = FollowListState(signer, cache, scope, settings)
|
||||
val followSetsState = FollowSetState(signer, cache, scope)
|
||||
|
||||
val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer)
|
||||
val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings)
|
||||
@@ -317,7 +317,7 @@ class Account(
|
||||
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
|
||||
|
||||
// Merges all follow lists to create a single All Follows feed.
|
||||
val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope)
|
||||
val allFollows = MergedFollowListsState(kind3FollowList, followSetsState, hashtagList, geohashList, communityList, scope)
|
||||
|
||||
val privateDMDecryptionCache = PrivateDMCache(signer)
|
||||
val privateZapsDecryptionCache = PrivateZapCache(signer)
|
||||
@@ -829,20 +829,6 @@ class Account(
|
||||
|
||||
fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic)
|
||||
|
||||
suspend fun getFollowSetNotes() =
|
||||
withContext(Dispatchers.Default) {
|
||||
val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile())
|
||||
Log.d(this@Account.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}")
|
||||
return@withContext followSetNotes
|
||||
}
|
||||
|
||||
fun mapNoteToFollowSet(note: Note): FollowSet =
|
||||
FollowSet
|
||||
.mapEventToSet(
|
||||
event = note.event as PeopleListEvent,
|
||||
signer,
|
||||
)
|
||||
|
||||
suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user))
|
||||
|
||||
suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user))
|
||||
|
||||
+10
-10
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.followSets
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.value
|
||||
@@ -31,9 +31,9 @@ data class FollowSet(
|
||||
val identifierTag: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val visibility: ListVisibility,
|
||||
val profileList: Set<String>,
|
||||
) : NostrList(listVisibility = visibility, content = profileList) {
|
||||
val visibility: SetVisibility,
|
||||
val profiles: Set<String>,
|
||||
) : NostrSet(setVisibility = visibility, content = profiles) {
|
||||
companion object {
|
||||
fun mapEventToSet(
|
||||
event: PeopleListEvent,
|
||||
@@ -53,16 +53,16 @@ data class FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = ListVisibility.Private,
|
||||
profileList = privateFollows.toSet(),
|
||||
visibility = SetVisibility.Private,
|
||||
profiles = privateFollows.toSet(),
|
||||
)
|
||||
} else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) {
|
||||
FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = ListVisibility.Public,
|
||||
profileList = publicFollows.toSet(),
|
||||
visibility = SetVisibility.Public,
|
||||
profiles = publicFollows.toSet(),
|
||||
)
|
||||
} else {
|
||||
// Follow set is empty, so assume public. Why? Nostr limitation.
|
||||
@@ -71,8 +71,8 @@ data class FollowSet(
|
||||
identifierTag = dTag,
|
||||
title = listTitle,
|
||||
description = listDescription,
|
||||
visibility = ListVisibility.Public,
|
||||
profileList = publicFollows.toSet(),
|
||||
visibility = SetVisibility.Public,
|
||||
profiles = publicFollows.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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.model.nip51Lists.followSets
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
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(
|
||||
val signer: NostrSigner,
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
private val isActive = MutableStateFlow(false)
|
||||
|
||||
suspend fun getFollowSetNotes() =
|
||||
withContext(Dispatchers.Default) {
|
||||
val followSetNotes = LocalCache.getFollowSetNotesFor(user)
|
||||
return@withContext followSetNotes
|
||||
}
|
||||
|
||||
private fun getFollowSetNotesFlow() =
|
||||
flow {
|
||||
while (isActive.value) {
|
||||
val followSetNotes = getFollowSetNotes()
|
||||
val followSets = followSetNotes.map { mapNoteToFollowSet(it) }
|
||||
emit(followSets)
|
||||
delay(2000)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
|
||||
val profilesFlow =
|
||||
getFollowSetNotesFlow()
|
||||
.map { it ->
|
||||
it.flatMapTo(mutableSetOf()) { it.profiles }.toSet()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
fun mapNoteToFollowSet(note: Note): FollowSet =
|
||||
FollowSet
|
||||
.mapEventToSet(
|
||||
event = note.event as PeopleListEvent,
|
||||
signer,
|
||||
)
|
||||
|
||||
fun isUserInFollowSets(user: User): Boolean = profilesFlow.value.contains(user.pubkeyHex)
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -18,15 +18,15 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.followSets
|
||||
|
||||
sealed class NostrList(
|
||||
val listVisibility: ListVisibility,
|
||||
sealed class NostrSet(
|
||||
val setVisibility: SetVisibility,
|
||||
val content: Collection<String>,
|
||||
)
|
||||
|
||||
class CuratedBookmarkList(
|
||||
class CuratedBookmarkSet(
|
||||
val name: String,
|
||||
val visibility: ListVisibility,
|
||||
val listItems: List<String>,
|
||||
) : NostrList(visibility, listItems)
|
||||
val visibility: SetVisibility,
|
||||
val setItems: List<String>,
|
||||
) : NostrSet(visibility, setItems)
|
||||
+2
-2
@@ -18,9 +18,9 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.followSets
|
||||
|
||||
enum class ListVisibility {
|
||||
enum class SetVisibility {
|
||||
Public,
|
||||
Private,
|
||||
Mixed,
|
||||
+8
-3
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.model.serverList
|
||||
|
||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
@@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class MergedFollowListsState(
|
||||
val kind3List: FollowListState,
|
||||
val followSetList: FollowSetState,
|
||||
val hashtagList: HashtagListState,
|
||||
val geohashList: GeohashListState,
|
||||
val communityList: CommunityListState,
|
||||
@@ -44,12 +46,13 @@ class MergedFollowListsState(
|
||||
) {
|
||||
fun mergeLists(
|
||||
kind3: FollowListState.Kind3Follows,
|
||||
followSetProfiles: Set<String>,
|
||||
hashtags: Set<String>,
|
||||
geohashes: Set<String>,
|
||||
community: Set<CommunityTag>,
|
||||
): FollowListState.Kind3Follows =
|
||||
FollowListState.Kind3Follows(
|
||||
kind3.authors,
|
||||
kind3.authors + followSetProfiles,
|
||||
kind3.authorsPlusMe,
|
||||
kind3.hashtags + hashtags,
|
||||
kind3.geotags + geohashes,
|
||||
@@ -59,15 +62,17 @@ class MergedFollowListsState(
|
||||
val flow: StateFlow<FollowListState.Kind3Follows> =
|
||||
combine(
|
||||
kind3List.flow,
|
||||
followSetList.profilesFlow,
|
||||
hashtagList.flow,
|
||||
geohashList.flow,
|
||||
communityList.flow,
|
||||
) { kind3, hashtag, geohash, community ->
|
||||
mergeLists(kind3, hashtag, geohash, community)
|
||||
) { kind3, followSet, hashtag, geohash, community ->
|
||||
mergeLists(kind3, followSet, hashtag, geohash, community)
|
||||
}.onStart {
|
||||
emit(
|
||||
mergeLists(
|
||||
kind3List.flow.value,
|
||||
followSetList.profilesFlow.value,
|
||||
hashtagList.flow.value,
|
||||
geohashList.flow.value,
|
||||
communityList.flow.value,
|
||||
|
||||
+8
-2
@@ -411,6 +411,10 @@ fun observeUserIsFollowing(
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user1, accountViewModel)
|
||||
val isUserInFollowSets =
|
||||
remember(accountViewModel.account.followSetsState) {
|
||||
accountViewModel.account.followSetsState.isUserInFollowSets(user2)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
@@ -420,12 +424,14 @@ fun observeUserIsFollowing(
|
||||
.follows.stateFlow
|
||||
.sample(1000)
|
||||
.mapLatest { userState ->
|
||||
userState.user.isFollowing(user2)
|
||||
userState.user.isFollowing(user2) || isUserInFollowSets
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user1.isFollowing(user2))
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user1.isFollowing(user2) || isUserInFollowSets,
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ class MediaCompressor {
|
||||
contentType: String?,
|
||||
mediaQuality: CompressorQuality,
|
||||
applicationContext: Context,
|
||||
useH265: Boolean = false,
|
||||
): MediaCompressorResult {
|
||||
// Skip compression if user selected uncompressed
|
||||
if (mediaQuality == CompressorQuality.UNCOMPRESSED) {
|
||||
@@ -57,7 +58,7 @@ class MediaCompressor {
|
||||
// branch into compression based on content type
|
||||
return when {
|
||||
contentType?.startsWith("video", ignoreCase = true) == true -> {
|
||||
VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality)
|
||||
VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality, useH265)
|
||||
}
|
||||
contentType?.startsWith("image", ignoreCase = true) == true &&
|
||||
!contentType.contains("gif") &&
|
||||
|
||||
@@ -46,6 +46,8 @@ class MultiOrchestrator(
|
||||
|
||||
fun first() = list.first()
|
||||
|
||||
fun hasVideo() = list.any { it.media.mimeType?.startsWith("video", ignoreCase = true) == true }
|
||||
|
||||
suspend fun upload(
|
||||
alt: String?,
|
||||
contentWarningReason: String?,
|
||||
@@ -53,6 +55,7 @@ class MultiOrchestrator(
|
||||
server: ServerName,
|
||||
account: Account,
|
||||
context: Context,
|
||||
useH265: Boolean = false,
|
||||
): Result {
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
@@ -67,6 +70,7 @@ class MultiOrchestrator(
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
useH265,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,6 +89,7 @@ class MultiOrchestrator(
|
||||
server: ServerName,
|
||||
account: Account,
|
||||
context: Context,
|
||||
useH265: Boolean = false,
|
||||
): Result {
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
@@ -100,6 +105,7 @@ class MultiOrchestrator(
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
useH265,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -288,9 +288,10 @@ class UploadOrchestrator {
|
||||
mimeType: String?,
|
||||
compressionQuality: CompressorQuality,
|
||||
context: Context,
|
||||
useH265: Boolean = false,
|
||||
) = if (compressionQuality != CompressorQuality.UNCOMPRESSED) {
|
||||
updateState(0.02, UploadingState.Compressing)
|
||||
MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext)
|
||||
MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265)
|
||||
} else {
|
||||
MediaCompressorResult(uri, mimeType, null)
|
||||
}
|
||||
@@ -304,8 +305,9 @@ class UploadOrchestrator {
|
||||
server: ServerName,
|
||||
account: Account,
|
||||
context: Context,
|
||||
useH265: Boolean = false,
|
||||
): UploadingFinalState {
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context)
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265)
|
||||
|
||||
return when (server.type) {
|
||||
ServerType.NIP95 -> uploadNIP95(compressed.uri, compressed.contentType, null, null, context)
|
||||
@@ -324,8 +326,9 @@ class UploadOrchestrator {
|
||||
server: ServerName,
|
||||
account: Account,
|
||||
context: Context,
|
||||
useH265: Boolean = false,
|
||||
): UploadingFinalState {
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context)
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265)
|
||||
val encrypted = EncryptFiles().encryptFile(context, compressed.uri, encrypt)
|
||||
|
||||
return when (server.type) {
|
||||
|
||||
+32
-32
@@ -29,6 +29,7 @@ import android.text.format.Formatter.formatFileSize
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.abedelazizshe.lightcompressorlibrary.CompressionListener
|
||||
import com.abedelazizshe.lightcompressorlibrary.VideoCodec
|
||||
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
||||
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration
|
||||
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
||||
@@ -38,8 +39,8 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// TODO: add Auto setting. Focus on small fast streams. 4->1080p, 1080p->720p, 720p and below stay the same resolution. Use existing matrix to determine bitrate.
|
||||
data class VideoInfo(
|
||||
val resolution: VideoResolution,
|
||||
val framerate: Float,
|
||||
@@ -80,18 +81,26 @@ enum class VideoStandard(
|
||||
override fun toString(): String = label
|
||||
}
|
||||
|
||||
private const val MBPS_TO_BPS_MULTIPLIER = 1_000_000
|
||||
|
||||
data class CompressionRule(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val bitrateMbps: Float,
|
||||
val description: String,
|
||||
) {
|
||||
fun getBitrateMbpsInt(framerate: Float): Int {
|
||||
// Apply 1.5x multiplier for 60fps+ videos
|
||||
val multiplier = if (framerate >= 60f) 1.5f else 1.0f
|
||||
fun getBitrateBps(
|
||||
framerate: Float,
|
||||
useH265: Boolean,
|
||||
): Int {
|
||||
// Apply 1.3x multiplier for 60fps+ videos, 0.7x multiplier for H265
|
||||
val framerateMultiplier = if (framerate >= 60f) 1.3f else 1.0f
|
||||
val codecMultiplier = if (useH265) 0.7f else 1.0f
|
||||
val finalMultiplier = framerateMultiplier * codecMultiplier
|
||||
|
||||
// Library doesn't support float so we have to convert it to int and use 1 as minimum
|
||||
return (bitrateMbps * multiplier).roundToInt().coerceAtLeast(1)
|
||||
Log.d("VideoCompressionHelper", "framerate: $framerate, useH265: $useH265, Bitrate multiplier: $finalMultiplier")
|
||||
|
||||
return (bitrateMbps * finalMultiplier * MBPS_TO_BPS_MULTIPLIER).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,43 +149,33 @@ object VideoCompressionHelper {
|
||||
contentType: String?,
|
||||
applicationContext: Context,
|
||||
mediaQuality: CompressorQuality,
|
||||
useH265: Boolean,
|
||||
timeoutMs: Long = 60_000L, // configurable, default 60s
|
||||
): MediaCompressorResult {
|
||||
val videoInfo = getVideoInfo(uri, applicationContext)
|
||||
|
||||
val videoBitrateInMbps =
|
||||
if (videoInfo != null) {
|
||||
val bitrateMbpsInt =
|
||||
val (videoBitrateInBps, resizer) =
|
||||
videoInfo?.let { info ->
|
||||
val rule =
|
||||
compressionRules
|
||||
.getValue(mediaQuality)
|
||||
.getValue(videoInfo.resolution.getStandard())
|
||||
.getBitrateMbpsInt(videoInfo.framerate)
|
||||
.getValue(info.resolution.getStandard())
|
||||
|
||||
val bitrateBps = rule.getBitrateBps(info.framerate, useH265)
|
||||
Log.d(LOG_TAG, "Bitrate: ${bitrateBps}bps for ${info.resolution.getStandard()} quality=$mediaQuality framerate=${info.framerate}fps useH265=$useH265.")
|
||||
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"Bitrate: ${bitrateMbpsInt}Mbps for ${videoInfo.resolution.getStandard()} " +
|
||||
"quality=$mediaQuality framerate=${videoInfo.framerate}fps.",
|
||||
"Resizer: ${info.resolution.width}x${info.resolution.height} -> " +
|
||||
"${rule.width}x${rule.height} (${rule.description})",
|
||||
)
|
||||
} else {
|
||||
val resizer = VideoResizer.limitSize(rule.width.toDouble(), rule.height.toDouble())
|
||||
|
||||
Pair(bitrateBps, resizer)
|
||||
} ?: run {
|
||||
Log.w(LOG_TAG, "Video bitrate fallback: 2Mbps (videoInfo unavailable)")
|
||||
2
|
||||
}
|
||||
|
||||
val resizer =
|
||||
if (videoInfo != null) {
|
||||
val rules =
|
||||
compressionRules
|
||||
.getValue(mediaQuality)
|
||||
.getValue(videoInfo.resolution.getStandard())
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"Resizer: ${videoInfo.resolution.width}x${videoInfo.resolution.height} -> " +
|
||||
"${rules.width}x${rules.height} (${rules.description})",
|
||||
)
|
||||
VideoResizer.limitSize(rules.width.toDouble(), rules.height.toDouble())
|
||||
} else {
|
||||
Log.d(LOG_TAG, "Resizer: null (original resolution preserved)")
|
||||
null
|
||||
Pair(2 * MBPS_TO_BPS_MULTIPLIER, null)
|
||||
}
|
||||
|
||||
// Get original file size safely
|
||||
@@ -192,10 +191,11 @@ object VideoCompressionHelper {
|
||||
storageConfiguration = AppSpecificStorageConfiguration(),
|
||||
configureWith =
|
||||
Configuration(
|
||||
videoBitrateInMbps = videoBitrateInMbps,
|
||||
videoBitrateInBps = videoBitrateInBps.toLong(),
|
||||
resizer = resizer,
|
||||
videoNames = listOf(UUID.randomUUID().toString()),
|
||||
isMinBitrateCheckEnabled = false,
|
||||
videoCodec = if (useH265) VideoCodec.H265 else VideoCodec.H264,
|
||||
),
|
||||
listener =
|
||||
object : CompressionListener {
|
||||
|
||||
@@ -54,7 +54,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -255,13 +254,13 @@ fun EditPostView(
|
||||
|
||||
postViewModel.multiOrchestrator?.let {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
ImageVideoDescription(
|
||||
it,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||
postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
@@ -279,7 +278,7 @@ fun EditPostView(
|
||||
|
||||
if (lud16 != null && postViewModel.wantsInvoice) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
InvoiceRequest(
|
||||
|
||||
@@ -87,6 +87,9 @@ open class EditPostViewModel : ViewModel() {
|
||||
// Images and Videos
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
|
||||
// Codec selection: false = H264, true = H265
|
||||
var useH265Codec by mutableStateOf(false)
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
var wantsInvoice by mutableStateOf(false)
|
||||
@@ -201,6 +204,7 @@ open class EditPostViewModel : ViewModel() {
|
||||
server,
|
||||
myAccount,
|
||||
context,
|
||||
useH265Codec,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
|
||||
@@ -62,6 +62,9 @@ open class NewMediaModel : ViewModel() {
|
||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||
var mediaQualitySlider by mutableIntStateOf(1)
|
||||
|
||||
// Codec selection: false = H264, true = H265
|
||||
var useH265Codec by mutableStateOf(false)
|
||||
|
||||
open fun load(
|
||||
account: Account,
|
||||
uris: ImmutableList<SelectedMedia>,
|
||||
@@ -111,6 +114,7 @@ open class NewMediaModel : ViewModel() {
|
||||
serverToUse,
|
||||
myAccount,
|
||||
context,
|
||||
useH265Codec,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
|
||||
@@ -261,4 +261,18 @@ fun ImageVideoPost(
|
||||
steps = 2,
|
||||
)
|
||||
}
|
||||
|
||||
// Only show H.265 codec option if there are videos in the upload
|
||||
if (postViewModel.multiOrchestrator?.hasVideo() == true) {
|
||||
SettingSwitchItem(
|
||||
title = R.string.video_codec_h265_label,
|
||||
description = R.string.video_codec_h265_description,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
checked = postViewModel.useH265Codec,
|
||||
onCheckedChange = { postViewModel.useH265Codec = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,20 +21,20 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class FollowSetFeedFilter(
|
||||
val account: Account,
|
||||
val followSetState: FollowSetState,
|
||||
) : FeedFilter<FollowSet>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-followsets"
|
||||
override fun feedKey(): String = followSetState.user.pubkeyHex + "-followsets"
|
||||
|
||||
override fun feed(): List<FollowSet> =
|
||||
runBlocking(account.scope.coroutineContext) {
|
||||
runBlocking(followSetState.scope.coroutineContext) {
|
||||
try {
|
||||
val fetchedSets = account.getFollowSetNotes()
|
||||
val followSets = fetchedSets.map { account.mapNoteToFollowSet(it) }
|
||||
val fetchedSets = followSetState.getFollowSetNotes()
|
||||
val followSets = fetchedSets.map { followSetState.mapNoteToFollowSet(it) }
|
||||
println("Updated follow set size for feed filter: ${followSets.size}")
|
||||
followSets
|
||||
} catch (e: Exception) {
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ fun SettingSwitchItem(
|
||||
text = stringRes(id = description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
+38
-27
@@ -78,7 +78,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
fun ImageVideoDescription(
|
||||
uris: MultiOrchestrator,
|
||||
defaultServer: ServerName,
|
||||
onAdd: (String, ServerName, Boolean, Int) -> Unit,
|
||||
onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit,
|
||||
onDelete: (SelectedMediaProcessing) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -91,7 +91,7 @@ fun ImageVideoDescription(
|
||||
uris: MultiOrchestrator,
|
||||
defaultServer: ServerName,
|
||||
includeNIP95: Boolean,
|
||||
onAdd: (String, ServerName, Boolean, Int) -> Unit,
|
||||
onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit,
|
||||
onDelete: (SelectedMediaProcessing) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -128,6 +128,9 @@ fun ImageVideoDescription(
|
||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||
var mediaQualitySlider by remember { mutableIntStateOf(1) }
|
||||
|
||||
// Codec selection: false = H264, true = H265
|
||||
var useH265Codec by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -294,32 +297,40 @@ fun ImageVideoDescription(
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text =
|
||||
when (mediaQualitySlider) {
|
||||
0 -> stringRes(R.string.media_compression_quality_low)
|
||||
1 -> stringRes(R.string.media_compression_quality_medium)
|
||||
2 -> stringRes(R.string.media_compression_quality_high)
|
||||
3 -> stringRes(R.string.media_compression_quality_uncompressed)
|
||||
else -> stringRes(R.string.media_compression_quality_medium)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = mediaQualitySlider.toFloat(),
|
||||
onValueChange = { mediaQualitySlider = it.toInt() },
|
||||
valueRange = 0f..3f,
|
||||
steps = 2,
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text =
|
||||
when (mediaQualitySlider) {
|
||||
0 -> stringRes(R.string.media_compression_quality_low)
|
||||
1 -> stringRes(R.string.media_compression_quality_medium)
|
||||
2 -> stringRes(R.string.media_compression_quality_high)
|
||||
3 -> stringRes(R.string.media_compression_quality_uncompressed)
|
||||
else -> stringRes(R.string.media_compression_quality_medium)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = mediaQualitySlider.toFloat(),
|
||||
onValueChange = { mediaQualitySlider = it.toInt() },
|
||||
valueRange = 0f..3f,
|
||||
steps = 2,
|
||||
)
|
||||
}
|
||||
|
||||
if (uris.first().media.isVideo() == true) {
|
||||
SettingSwitchItem(
|
||||
title = R.string.video_codec_h265_label,
|
||||
description = R.string.video_codec_h265_description,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
checked = useH265Codec,
|
||||
onCheckedChange = { useH265Codec = it },
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -327,7 +338,7 @@ fun ImageVideoDescription(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider) },
|
||||
onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) },
|
||||
shape = QuoteBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
|
||||
+1
-1
@@ -285,7 +285,7 @@ private fun GenericCommentPostBody(
|
||||
ImageVideoDescription(
|
||||
it,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ fun GroupDMScreenContent(
|
||||
ImageVideoDescription(
|
||||
selectedFiles,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||
postViewModel.uploadAndHold(
|
||||
accountViewModel.toastManager::toast,
|
||||
context,
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ private fun NewProductBody(
|
||||
uris = it,
|
||||
defaultServer = accountViewModel.account.settings.defaultFileServer,
|
||||
includeNIP95 = false,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
|
||||
+2
-2
@@ -326,8 +326,8 @@ private fun NewPostScreenBody(
|
||||
ImageVideoDescription(
|
||||
it,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
}
|
||||
|
||||
+4
-1
@@ -624,8 +624,9 @@ open class ShortNotePostViewModel :
|
||||
server: ServerName,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
useH265: Boolean,
|
||||
) = try {
|
||||
uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context)
|
||||
uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
@@ -640,6 +641,7 @@ open class ShortNotePostViewModel :
|
||||
server: ServerName,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
useH265: Boolean,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||
@@ -654,6 +656,7 @@ open class ShortNotePostViewModel :
|
||||
server,
|
||||
account,
|
||||
context,
|
||||
useH265,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
|
||||
+14
-12
@@ -59,6 +59,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -76,10 +78,10 @@ fun ListsAndSetsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val followSetsViewModel: NostrUserListFeedViewModel =
|
||||
val followSetsViewModel: FollowSetFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrUserListFeedViewModel",
|
||||
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account),
|
||||
key = "FollowSetFeedViewModel",
|
||||
factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
ListsAndSetsScreen(
|
||||
@@ -91,7 +93,7 @@ fun ListsAndSetsScreen(
|
||||
|
||||
@Composable
|
||||
fun ListsAndSetsScreen(
|
||||
followSetsViewModel: NostrUserListFeedViewModel,
|
||||
followSetsViewModel: FollowSetFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -117,8 +119,8 @@ fun ListsAndSetsScreen(
|
||||
refresh = {
|
||||
followSetsViewModel.invalidateData()
|
||||
},
|
||||
addItem = { title: String, description: String?, listType: ListVisibility ->
|
||||
val isSetPrivate = listType == ListVisibility.Private
|
||||
addItem = { title: String, description: String?, listType: SetVisibility ->
|
||||
val isSetPrivate = listType == SetVisibility.Private
|
||||
followSetsViewModel.addFollowSet(
|
||||
setName = title,
|
||||
setDescription = description,
|
||||
@@ -149,9 +151,9 @@ fun ListsAndSetsScreen(
|
||||
|
||||
@Composable
|
||||
fun CustomListsScreen(
|
||||
followSetState: FollowSetState,
|
||||
followSetFeedState: FollowSetFeedState,
|
||||
refresh: () -> Unit,
|
||||
addItem: (title: String, description: String?, listType: ListVisibility) -> Unit,
|
||||
addItem: (title: String, description: String?, listType: SetVisibility) -> Unit,
|
||||
openItem: (identifier: String) -> Unit,
|
||||
renameItem: (followSet: FollowSet, newName: String) -> Unit,
|
||||
deleteItem: (followSet: FollowSet) -> Unit,
|
||||
@@ -195,10 +197,10 @@ fun CustomListsScreen(
|
||||
// TODO: Show components based on current tab
|
||||
FollowSetFabsAndMenu(
|
||||
onAddPrivateSet = { name: String, description: String? ->
|
||||
addItem(name, description, ListVisibility.Private)
|
||||
addItem(name, description, SetVisibility.Private)
|
||||
},
|
||||
onAddPublicSet = { name: String, description: String? ->
|
||||
addItem(name, description, ListVisibility.Public)
|
||||
addItem(name, description, SetVisibility.Public)
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -216,7 +218,7 @@ fun CustomListsScreen(
|
||||
when (page) {
|
||||
0 ->
|
||||
FollowSetFeedView(
|
||||
followSetState = followSetState,
|
||||
followSetFeedState = followSetFeedState,
|
||||
onRefresh = refresh,
|
||||
onOpenItem = openItem,
|
||||
onRenameItem = renameItem,
|
||||
@@ -410,7 +412,7 @@ private fun SetItemPreview() {
|
||||
identifierTag = "00001-2222",
|
||||
title = "Sample List Title",
|
||||
description = "Sample List Description",
|
||||
visibility = ListVisibility.Mixed,
|
||||
visibility = SetVisibility.Mixed,
|
||||
emptySet(),
|
||||
)
|
||||
ThemeComparisonColumn {
|
||||
|
||||
+9
-7
@@ -55,6 +55,8 @@ import androidx.compose.ui.text.withStyle
|
||||
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
|
||||
@@ -98,7 +100,7 @@ fun CustomSetItem(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = "${followSet.profileList.size}")
|
||||
Text(text = "${followSet.profiles.size}")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
@@ -121,9 +123,9 @@ fun CustomSetItem(
|
||||
followSet.visibility.let {
|
||||
val text by derivedStateOf {
|
||||
when (it) {
|
||||
ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
|
||||
ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
|
||||
ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
|
||||
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(
|
||||
@@ -135,9 +137,9 @@ fun CustomSetItem(
|
||||
painter =
|
||||
painterResource(
|
||||
when (it) {
|
||||
ListVisibility.Public -> R.drawable.ic_public
|
||||
ListVisibility.Private -> R.drawable.lock
|
||||
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
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),
|
||||
|
||||
+7
-5
@@ -20,16 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
|
||||
|
||||
sealed class FollowSetState {
|
||||
data object Loading : FollowSetState()
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
|
||||
sealed class FollowSetFeedState {
|
||||
data object Loading : FollowSetFeedState()
|
||||
|
||||
data class Loaded(
|
||||
val feed: List<FollowSet>,
|
||||
) : FollowSetState()
|
||||
) : FollowSetFeedState()
|
||||
|
||||
data object Empty : FollowSetState()
|
||||
data object Empty : FollowSetFeedState()
|
||||
|
||||
data class FeedError(
|
||||
val errorMessage: String,
|
||||
) : FollowSetState()
|
||||
) : FollowSetFeedState()
|
||||
}
|
||||
+9
-8
@@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
@@ -46,17 +47,17 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
@Composable
|
||||
fun FollowSetFeedView(
|
||||
modifier: Modifier = Modifier,
|
||||
followSetState: FollowSetState,
|
||||
followSetFeedState: FollowSetFeedState,
|
||||
onRefresh: () -> Unit = {},
|
||||
onOpenItem: (String) -> Unit = {},
|
||||
onRenameItem: (targetSet: FollowSet, newName: String) -> Unit,
|
||||
onDeleteItem: (followSet: FollowSet) -> Unit,
|
||||
) {
|
||||
when (followSetState) {
|
||||
FollowSetState.Loading -> LoadingFeed()
|
||||
when (followSetFeedState) {
|
||||
FollowSetFeedState.Loading -> LoadingFeed()
|
||||
|
||||
is FollowSetState.Loaded -> {
|
||||
val followSetFeed = followSetState.feed
|
||||
is FollowSetFeedState.Loaded -> {
|
||||
val followSetFeed = followSetFeedState.feed
|
||||
FollowSetLoaded(
|
||||
loadedFeedState = followSetFeed,
|
||||
onRefresh = onRefresh,
|
||||
@@ -66,7 +67,7 @@ fun FollowSetFeedView(
|
||||
)
|
||||
}
|
||||
|
||||
is FollowSetState.Empty -> {
|
||||
is FollowSetFeedState.Empty -> {
|
||||
FollowSetFeedEmpty(
|
||||
message = stringRes(R.string.follow_set_empty_feed_msg),
|
||||
) {
|
||||
@@ -74,9 +75,9 @@ fun FollowSetFeedView(
|
||||
}
|
||||
}
|
||||
|
||||
is FollowSetState.FeedError ->
|
||||
is FollowSetFeedState.FeedError ->
|
||||
FeedError(
|
||||
followSetState.errorMessage,
|
||||
followSetFeedState.errorMessage,
|
||||
) {
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
+14
-14
@@ -30,6 +30,8 @@ import androidx.lifecycle.viewModelScope
|
||||
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
|
||||
@@ -49,12 +51,11 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.UUID
|
||||
|
||||
// TODO Update: Rename this to be used only for follow sets, and create separate VMs for bookmark sets, etc.
|
||||
class NostrUserListFeedViewModel(
|
||||
class FollowSetFeedViewModel(
|
||||
val dataSource: FeedFilter<FollowSet>,
|
||||
) : ViewModel(),
|
||||
InvalidatableContent {
|
||||
private val _feedContent = MutableStateFlow<FollowSetState>(FollowSetState.Loading)
|
||||
private val _feedContent = MutableStateFlow<FollowSetFeedState>(FollowSetFeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
@@ -67,9 +68,8 @@ class NostrUserListFeedViewModel(
|
||||
noteIdentifier: String,
|
||||
account: Account,
|
||||
): AddressableNote? {
|
||||
// checkNotInMainThread()
|
||||
val potentialNote =
|
||||
runBlocking(Dispatchers.IO) { account.getFollowSetNotes() }
|
||||
runBlocking(Dispatchers.IO) { account.followSetsState.getFollowSetNotes() }
|
||||
.find { it.dTag() == noteIdentifier }
|
||||
return potentialNote
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class NostrUserListFeedViewModel(
|
||||
account: Account,
|
||||
): Boolean {
|
||||
val potentialNote =
|
||||
runBlocking(viewModelScope.coroutineContext) { account.getFollowSetNotes() }
|
||||
runBlocking(viewModelScope.coroutineContext) { account.followSetsState.getFollowSetNotes() }
|
||||
.find { (it.event as PeopleListEvent).nameOrTitle() == setName }
|
||||
return potentialNote != null
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class NostrUserListFeedViewModel(
|
||||
|
||||
val newSets = dataSource.loadTop().toImmutableList()
|
||||
|
||||
if (oldFeedState is FollowSetState.Loaded) {
|
||||
if (oldFeedState is FollowSetFeedState.Loaded) {
|
||||
val oldFeedList = oldFeedState.feed.toImmutableList()
|
||||
// Using size as a proxy for has changed.
|
||||
if (!equalImmutableLists(newSets, oldFeedList)) {
|
||||
@@ -108,7 +108,7 @@ class NostrUserListFeedViewModel(
|
||||
this.javaClass.simpleName,
|
||||
"refreshSuspended: Error loading or refreshing feed -> ${e.message}",
|
||||
)
|
||||
_feedContent.update { FollowSetState.FeedError(e.message.toString()) }
|
||||
_feedContent.update { FollowSetFeedState.FeedError(e.message.toString()) }
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class NostrUserListFeedViewModel(
|
||||
PeopleListEvent.addUser(
|
||||
earlierVersion = followSetEvent,
|
||||
pubKeyHex = userProfileHex,
|
||||
isPrivate = followSet.visibility == ListVisibility.Private,
|
||||
isPrivate = followSet.visibility == SetVisibility.Private,
|
||||
signer = account.signer,
|
||||
) {
|
||||
account.sendMyPublicAndPrivateOutbox(it)
|
||||
@@ -223,9 +223,9 @@ class NostrUserListFeedViewModel(
|
||||
|
||||
private fun updateFeed(sets: ImmutableList<FollowSet>) {
|
||||
if (sets.isNotEmpty()) {
|
||||
_feedContent.update { FollowSetState.Loaded(sets) }
|
||||
_feedContent.update { FollowSetFeedState.Loaded(sets) }
|
||||
} else {
|
||||
_feedContent.update { FollowSetState.Empty }
|
||||
_feedContent.update { FollowSetFeedState.Empty }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ class NostrUserListFeedViewModel(
|
||||
|
||||
init {
|
||||
Log.d("Init", this.javaClass.simpleName)
|
||||
Log.d(this.javaClass.simpleName, " FollowSetState : ${_feedContent.value}")
|
||||
Log.d(this.javaClass.simpleName, " FollowSetFeedState : ${_feedContent.value}")
|
||||
collectorJob =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
@@ -266,8 +266,8 @@ class NostrUserListFeedViewModel(
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
NostrUserListFeedViewModel(
|
||||
FollowSetFeedFilter(account),
|
||||
FollowSetFeedViewModel(
|
||||
FollowSetFeedFilter(account.followSetsState),
|
||||
) as T
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -67,14 +67,14 @@ import androidx.compose.ui.unit.sp
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
@@ -92,10 +92,10 @@ fun FollowSetScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
navigator: INav,
|
||||
) {
|
||||
val followSetViewModel: NostrUserListFeedViewModel =
|
||||
val followSetViewModel: FollowSetFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrUserListFeedViewModel",
|
||||
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account),
|
||||
key = "FollowSetFeedViewModel",
|
||||
factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
FollowSetScreen(selectedSetIdentifier, followSetViewModel, accountViewModel, navigator)
|
||||
@@ -105,7 +105,7 @@ fun FollowSetScreen(
|
||||
@Composable
|
||||
fun FollowSetScreen(
|
||||
selectedSetIdentifier: String,
|
||||
followSetViewModel: NostrUserListFeedViewModel,
|
||||
followSetViewModel: FollowSetFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
navigator: INav,
|
||||
) {
|
||||
@@ -144,7 +144,7 @@ fun FollowSetScreen(
|
||||
when {
|
||||
selectedSetState.value != null -> {
|
||||
val selectedSet = selectedSetState.value
|
||||
val users = selectedSet!!.profileList.mapToUsers(accountViewModel).filterNotNull()
|
||||
val users = selectedSet!!.profiles.mapToUsers(accountViewModel).filterNotNull()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -235,10 +235,10 @@ fun TitleAndDescription(
|
||||
Icon(
|
||||
painter =
|
||||
painterResource(
|
||||
when (followSet.listVisibility) {
|
||||
ListVisibility.Public -> R.drawable.ic_public
|
||||
ListVisibility.Private -> R.drawable.lock
|
||||
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
when (followSet.setVisibility) {
|
||||
SetVisibility.Public -> R.drawable.ic_public
|
||||
SetVisibility.Private -> R.drawable.lock
|
||||
SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
},
|
||||
),
|
||||
contentDescription = null,
|
||||
|
||||
+27
-27
@@ -78,13 +78,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NewSetCreationDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -97,10 +97,10 @@ fun FollowSetsManagementDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
navigator: INav,
|
||||
) {
|
||||
val followSetViewModel: NostrUserListFeedViewModel =
|
||||
val followSetViewModel: FollowSetFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrUserListFeedViewModel",
|
||||
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account),
|
||||
key = "FollowSetFeedViewModel",
|
||||
factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
FollowSetsManagementDialog(userHex, followSetViewModel, accountViewModel.account, navigator)
|
||||
@@ -110,7 +110,7 @@ fun FollowSetsManagementDialog(
|
||||
@Composable
|
||||
fun FollowSetsManagementDialog(
|
||||
userHex: String,
|
||||
followSetsViewModel: NostrUserListFeedViewModel,
|
||||
followSetsViewModel: FollowSetFeedViewModel,
|
||||
account: Account,
|
||||
navigator: INav,
|
||||
) {
|
||||
@@ -164,17 +164,17 @@ fun FollowSetsManagementDialog(
|
||||
.imePadding(),
|
||||
) {
|
||||
when (followSetsState) {
|
||||
is FollowSetState.Loaded -> {
|
||||
val lists = (followSetsState as FollowSetState.Loaded).feed
|
||||
is FollowSetFeedState.Loaded -> {
|
||||
val lists = (followSetsState as FollowSetFeedState.Loaded).feed
|
||||
|
||||
lists.forEachIndexed { index, list ->
|
||||
Spacer(StdVertSpacer)
|
||||
FollowSetItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listHeader = list.title,
|
||||
listVisibility = list.visibility,
|
||||
setVisibility = list.visibility,
|
||||
userName = userInfo.toBestDisplayName(),
|
||||
isUserInList = list.profileList.contains(userHex),
|
||||
isUserInList = list.profiles.contains(userHex),
|
||||
onRemoveUser = {
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
@@ -187,7 +187,7 @@ fun FollowSetsManagementDialog(
|
||||
)
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
"Updated List. New size: ${list.profileList.size}",
|
||||
"Updated List. New size: ${list.profiles.size}",
|
||||
)
|
||||
},
|
||||
onAddUser = {
|
||||
@@ -198,28 +198,28 @@ fun FollowSetsManagementDialog(
|
||||
followSetsViewModel.addUserToSet(userHex, list, account)
|
||||
Log.d(
|
||||
"Amethyst",
|
||||
"Updated List. New size: ${list.profileList.size}",
|
||||
"Updated List. New size: ${list.profiles.size}",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FollowSetState.Empty -> {
|
||||
FollowSetFeedState.Empty -> {
|
||||
EmptyOrNoneFound { followSetsViewModel.refresh() }
|
||||
}
|
||||
|
||||
is FollowSetState.FeedError -> {
|
||||
val errorMsg = (followSetsState as FollowSetState.FeedError).errorMessage
|
||||
is FollowSetFeedState.FeedError -> {
|
||||
val errorMsg = (followSetsState as FollowSetFeedState.FeedError).errorMessage
|
||||
ErrorMessage(errorMsg) { followSetsViewModel.refresh() }
|
||||
}
|
||||
|
||||
FollowSetState.Loading -> {
|
||||
FollowSetFeedState.Loading -> {
|
||||
Loading()
|
||||
}
|
||||
}
|
||||
|
||||
if (followSetsState != FollowSetState.Loading) {
|
||||
if (followSetsState != FollowSetFeedState.Loading) {
|
||||
FollowSetsCreationMenu(
|
||||
userName = userInfo.toBestDisplayName(),
|
||||
onSetCreate = { setName, setIsPrivate, description ->
|
||||
@@ -304,7 +304,7 @@ private fun ErrorMessage(
|
||||
fun FollowSetItem(
|
||||
modifier: Modifier = Modifier,
|
||||
listHeader: String,
|
||||
listVisibility: ListVisibility,
|
||||
setVisibility: SetVisibility,
|
||||
userName: String,
|
||||
isUserInList: Boolean,
|
||||
onAddUser: () -> Unit,
|
||||
@@ -330,21 +330,21 @@ fun FollowSetItem(
|
||||
) {
|
||||
Text(listHeader, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
listVisibility.let {
|
||||
setVisibility.let {
|
||||
val text by derivedStateOf {
|
||||
when (it) {
|
||||
ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
|
||||
ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
|
||||
ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
|
||||
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 (listVisibility) {
|
||||
ListVisibility.Public -> R.drawable.ic_public
|
||||
ListVisibility.Private -> R.drawable.lock
|
||||
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||
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),
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ fun PublicMessageScreenContent(
|
||||
ImageVideoDescription(
|
||||
it,
|
||||
accountViewModel.account.settings.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality ->
|
||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||
if (server.type != ServerType.NIP95) {
|
||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||
|
||||
@@ -952,4 +952,35 @@
|
||||
<string name="guidelines">Pokyny</string>
|
||||
<string name="moderators">Moderátoři</string>
|
||||
<string name="open_dropdown_menu">Otevřít rozbalovací nabídku</string>
|
||||
<string name="follow_sets">Sady sledování</string>
|
||||
<string name="labeled_bookmarks">Označené záložky</string>
|
||||
<string name="general_bookmarks">Obecné záložky</string>
|
||||
<string name="follow_set_type_public">Veřejné</string>
|
||||
<string name="follow_set_type_private">Soukromé</string>
|
||||
<string name="follow_set_type_mixed">Smíšené</string>
|
||||
<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_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>
|
||||
<string name="follow_set_creation_menu_title">Vytvořit nový seznam</string>
|
||||
<string name="follow_set_creation_item_label">Vytvořit nový seznam %1$s s uživatelem</string>
|
||||
<string name="follow_set_creation_item_description">Vytvoří %1$s sadu sledování a přidá do ní %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nový seznam %1$s</string>
|
||||
<string name="follow_set_creation_name_label">Název sady</string>
|
||||
<string name="follow_set_creation_desc_label">Popis sady (volitelné)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Vytvořit sadu</string>
|
||||
<string name="follow_set_rename_btn_label">Přejmenovat sadu</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Přejmenováváte z </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> na..</string>
|
||||
<string name="rename">Přejmenovat</string>
|
||||
<string name="torrent_no_info">Událost nemá dostatek informací pro vytvoření magnet odkazu</string>
|
||||
<string name="my_lists_and_sets">Moje seznamy/sady</string>
|
||||
<string name="select_signer">Vybrat podepisovatele</string>
|
||||
<string name="video_codec_h265_label">Použít kodek H.265/HEVC</string>
|
||||
<string name="video_codec_h265_description">Lepší kvalita při menší velikosti souboru, ale ne všechna zařízení podporují přehrávání H.265.</string>
|
||||
</resources>
|
||||
|
||||
@@ -992,4 +992,35 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="guidelines">Richtlinien</string>
|
||||
<string name="moderators">Moderatoren</string>
|
||||
<string name="open_dropdown_menu">Dropdown-Menü öffnen</string>
|
||||
<string name="follow_sets">Folge-Sets</string>
|
||||
<string name="labeled_bookmarks">Markierte Lesezeichen</string>
|
||||
<string name="general_bookmarks">Allgemeine Lesezeichen</string>
|
||||
<string name="follow_set_type_public">Öffentlich</string>
|
||||
<string name="follow_set_type_private">Privat</string>
|
||||
<string name="follow_set_type_mixed">Gemischt</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_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>
|
||||
<string name="follow_set_creation_menu_title">Neue Liste erstellen</string>
|
||||
<string name="follow_set_creation_item_label">Neue %1$s-Liste mit Benutzer erstellen</string>
|
||||
<string name="follow_set_creation_item_description">Erstellt ein %1$s-Folge-Set und fügt %2$s hinzu.</string>
|
||||
<string name="follow_set_creation_dialog_title">Neue %1$s-Liste</string>
|
||||
<string name="follow_set_creation_name_label">Set-Name</string>
|
||||
<string name="follow_set_creation_desc_label">Set-Beschreibung (optional)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Set erstellen</string>
|
||||
<string name="follow_set_rename_btn_label">Set umbenennen</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Du benennst um von </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> zu..</string>
|
||||
<string name="rename">Umbenennen</string>
|
||||
<string name="torrent_no_info">Das Ereignis enthält nicht genügend Informationen, um einen Magnetlink zu erstellen</string>
|
||||
<string name="my_lists_and_sets">Meine Listen/Sets</string>
|
||||
<string name="select_signer">Signierer auswählen</string>
|
||||
<string name="video_codec_h265_label">H.265/HEVC-Codec verwenden</string>
|
||||
<string name="video_codec_h265_description">Bessere Qualität bei kleinerer Dateigröße, aber nicht alle Geräte unterstützen die H.265-Wiedergabe.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1065,4 +1065,5 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">क्या आप निकटकालिक क्रमदोष सूचनापत्र एक सीधे सन्देश में अमेथिस्ट को भेजना चाहते हैं। कोई व्यक्तिगत जानकारी बाँटी नहीं जाएगी</string>
|
||||
<string name="crashreport_found_send">भेजें</string>
|
||||
<string name="this_message_will_disappear_in_days">यह सन्देश %1$d दिनों में अदृश्य हो जाएगा</string>
|
||||
<string name="select_signer">हस्ताक्षरकर्ता चुनें</string>
|
||||
</resources>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Közzététel</string>
|
||||
<string name="save">Mentés</string>
|
||||
<string name="create">Létrehozás</string>
|
||||
<string name="rename">Átnevezés</string>
|
||||
<string name="cancel">Mégse</string>
|
||||
<string name="failed_to_upload_the_image">Nem sikerült feltölteni a képet</string>
|
||||
<string name="relay_address">Atjátszó címe</string>
|
||||
@@ -140,12 +141,12 @@
|
||||
<string name="ln_address">LN-cím</string>
|
||||
<string name="ln_url_outdated">LN-webcím (elavult)</string>
|
||||
<string name="save_to_gallery">Mentés a galériába</string>
|
||||
<string name="image_saved_to_the_gallery">Kép elmentve a képgalériába</string>
|
||||
<string name="image_saved_to_the_gallery">Kép mentve a képgalériába</string>
|
||||
<string name="video_download_has_started_toast">A videó letöltése megkezdődött…</string>
|
||||
<string name="media_download_has_started_toast">A média letöltése megkezdődött…</string>
|
||||
<string name="failed_to_save_the_image">Nem sikerült menteni a képet</string>
|
||||
<string name="video_saved_to_the_gallery">Videó elmentve a videógalériába</string>
|
||||
<string name="failed_to_save_the_video">Nem sikerült elmenteni a videót</string>
|
||||
<string name="video_saved_to_the_gallery">Videó mentve a videógalériába</string>
|
||||
<string name="failed_to_save_the_video">Nem sikerült menteni a videót</string>
|
||||
<string name="upload_image">Kép feltöltése</string>
|
||||
<string name="take_a_picture">Kép készítése</string>
|
||||
<string name="record_a_video">Videó rögzítése</string>
|
||||
@@ -445,6 +446,33 @@
|
||||
<string name="follow_list_aroundme">Közelben lévők bejegyzései</string>
|
||||
<string name="follow_list_global">Globális</string>
|
||||
<string name="follow_list_mute_list">Némítottak bejegyzései</string>
|
||||
<string name="follow_sets">Követési gyüjtemények</string>
|
||||
<string name="labeled_bookmarks">Címkézett könyvjelzők</string>
|
||||
<string name="general_bookmarks">Általános könyvjelzők</string>
|
||||
<string name="follow_set_type_public">Nyílvános</string>
|
||||
<string name="follow_set_type_private">Privát</string>
|
||||
<string name="follow_set_type_mixed">Kevert</string>
|
||||
<string name="follow_set_empty_feed_msg"> Úgy tűnik, hogy még egyetlen gyüjteményt sem követ.
|
||||
\nKoppintson a frissítéshez vagy érintse meg a hozzáadás gombot a gyüjtemény létrehozásához.
|
||||
</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_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>
|
||||
<string name="follow_set_creation_menu_title">Új lista létrehozása</string>
|
||||
<string name="follow_set_creation_item_label">Új %1$s lista létrehozása felhasználóval</string>
|
||||
<string name="follow_set_creation_item_description">%1$s követési gyüjtemény létrehozása, és hozzáadás a következhöz: %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Új %1$s lista</string>
|
||||
<string name="follow_set_creation_name_label">Név megadása</string>
|
||||
<string name="follow_set_creation_desc_label">Leírás megadása (nem kötelező)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Gyüjtemény létrehozása</string>
|
||||
<string name="follow_set_rename_btn_label">Gyüjtemény átnevezése</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Ön átnevezi a követési gyüjteményt erről: </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> erre:</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Alapértelmezett port: 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Kapcsolódás a TORon keresztül az Orbot segítségével
|
||||
\n\n1. Telepítse az [Orbotot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -1014,6 +1042,8 @@
|
||||
<string name="torrent_download">Letöltés</string>
|
||||
<string name="torrent_failure">Nem sikerült megnyitni a fájlt</string>
|
||||
<string name="torrent_no_apps">A fájl megnyitásához és letöltéséhez nincsenek torrent-alkalmazások telepítve.</string>
|
||||
<string name="torrent_no_info">Az esemény nem tartalmaz elegendő információt a mágneshivatkozás létrehozásához</string>
|
||||
<string name="my_lists_and_sets">Saját lista/gyüjtemény</string>
|
||||
<string name="select_list_to_filter">Lista kiválasztása a hírfolyam szűréséhez</string>
|
||||
<string name="temporary_account">Kijelentkeztetés az eszköz zárolása esetén</string>
|
||||
<string name="private_message">Privát üzenet</string>
|
||||
@@ -1035,4 +1065,5 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Szeretné elküldeni a legutóbbi összeomlási jelentést az Amethystnek egy közvetlen üzenetben? A személyes adatait nem osztja meg</string>
|
||||
<string name="crashreport_found_send">Küldés</string>
|
||||
<string name="this_message_will_disappear_in_days">Ez az üzenet %1$d nap múlva eltűnik</string>
|
||||
<string name="select_signer">Aláíró kiválasztása</string>
|
||||
</resources>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Wyślij</string>
|
||||
<string name="save">Zapisz</string>
|
||||
<string name="create">Utwórz</string>
|
||||
<string name="rename">Zmień nazwę</string>
|
||||
<string name="cancel">Anuluj</string>
|
||||
<string name="failed_to_upload_the_image">Nie udało się przesłać obrazu</string>
|
||||
<string name="relay_address">Adres transmitera</string>
|
||||
@@ -442,6 +443,33 @@
|
||||
<string name="follow_list_aroundme">W pobliżu</string>
|
||||
<string name="follow_list_global">Wszystkie</string>
|
||||
<string name="follow_list_mute_list">Zablokowane</string>
|
||||
<string name="follow_sets">Zbiory obserwowanych</string>
|
||||
<string name="labeled_bookmarks">Oznaczone zakładki</string>
|
||||
<string name="general_bookmarks">Ogólne zakładki</string>
|
||||
<string name="follow_set_type_public">Publiczna</string>
|
||||
<string name="follow_set_type_private">Prywatna</string>
|
||||
<string name="follow_set_type_mixed">Mieszana</string>
|
||||
<string name="follow_set_empty_feed_msg"> Wygląda na to, że nie masz jeszcze żadnych zbiorów obserwowanych.
|
||||
\nDotknij poniżej, aby odświeżyć, lub naciśnij przycisk Dodaj, aby utworzyć nowy.
|
||||
</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_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>
|
||||
<string name="follow_set_creation_menu_title">Utwórz nową listę</string>
|
||||
<string name="follow_set_creation_item_label">\"Utwórz nową listę %1$s z użytkownikiem</string>
|
||||
<string name="follow_set_creation_item_description">Tworzy zbiór obserwowanych %1$s i dodaje do niego %2$s.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nowa lista %1$s</string>
|
||||
<string name="follow_set_creation_name_label">Nazwa zbioru</string>
|
||||
<string name="follow_set_creation_desc_label">Opis zbioru (opcjonalnie)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Utwórz zbiór</string>
|
||||
<string name="follow_set_rename_btn_label">Zmień nazwę zbioru</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Zmieniasz nazwę z </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> do..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Domyślny port to 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Połącz przez Tor z Orbotem
|
||||
\n\n1. Zainstaluj [Orbota](https://play.google. om/store/apps/details?id= org.torproject.android)
|
||||
@@ -1011,6 +1039,8 @@
|
||||
<string name="torrent_download">Pobierz</string>
|
||||
<string name="torrent_failure">Nie udało się otworzyć pliku</string>
|
||||
<string name="torrent_no_apps">Brak zainstalowanych aplikacji torrent do otwarcia i pobrania pliku.</string>
|
||||
<string name="torrent_no_info">Zdarzenie nie ma wystarczającej ilości informacji, aby zbudować link magnetyczny</string>
|
||||
<string name="my_lists_and_sets">Moje Listy/Zbiory</string>
|
||||
<string name="select_list_to_filter">Wybierz listę, aby filtrować aktualności</string>
|
||||
<string name="temporary_account">Wyloguj się przy blokowaniu urządzenia</string>
|
||||
<string name="private_message">Wiadomość prywatna</string>
|
||||
@@ -1018,6 +1048,7 @@
|
||||
<string name="group_relay">Transmiter Czatu</string>
|
||||
<string name="group_relay_explanation">Transmiter, z którym łączą się wszyscy użytkownicy tego czatu</string>
|
||||
<string name="share_image">Udostępnij zdjęcie…</string>
|
||||
<string name="unable_to_share_image">Nie można udostępnić obrazu, spróbuj ponownie później…</string>
|
||||
<string name="search_by_hashtag">Szukaj tagu: #%1$s</string>
|
||||
<string name="dont_translate_from">Nie tłumacz z</string>
|
||||
<string name="dont_translate_from_description">Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych.</string>
|
||||
@@ -1031,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">Czy chcesz wysłać ostatni raport o awarii do Amethyst w DM? Żadne dane osobowe nie będą udostępnione</string>
|
||||
<string name="crashreport_found_send">Prześlij</string>
|
||||
<string name="this_message_will_disappear_in_days">Ta wiadomość zniknie za %1$d dni</string>
|
||||
<string name="select_signer">Wybierz Sygnatariusza</string>
|
||||
</resources>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Salvar</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="create">Criar</string>
|
||||
<string name="rename">Renomear</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="failed_to_upload_the_image">Falha ao enviar imagem</string>
|
||||
<string name="relay_address">Endereço do Relay</string>
|
||||
@@ -443,6 +444,31 @@
|
||||
<string name="follow_list_aroundme">Perto de mim</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista Silenciada</string>
|
||||
<string name="follow_sets">Conjuntos de Seguimento</string>
|
||||
<string name="labeled_bookmarks">Favoritos com etiqueta</string>
|
||||
<string name="general_bookmarks">Favoritos gerais</string>
|
||||
<string name="follow_set_type_public">Público</string>
|
||||
<string name="follow_set_type_private">Privado</string>
|
||||
<string name="follow_set_type_mixed">Misto</string>
|
||||
<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_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>
|
||||
<string name="follow_set_creation_menu_title">Criar nova lista</string>
|
||||
<string name="follow_set_creation_item_label">Criar nova lista %1$s com usuário</string>
|
||||
<string name="follow_set_creation_item_description">Cria um conjunto de seguimento %1$s e adiciona %2$s a ele.</string>
|
||||
<string name="follow_set_creation_dialog_title">Nova lista %1$s</string>
|
||||
<string name="follow_set_creation_name_label">Nome do conjunto</string>
|
||||
<string name="follow_set_creation_desc_label">Descrição do conjunto (opcional)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Criar conjunto</string>
|
||||
<string name="follow_set_rename_btn_label">Renomear conjunto</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Você está renomeando de </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> para..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Porta padrão é 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conecte-se através do Tor com o Orbot
|
||||
\n\n1. Instale o [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -830,6 +856,8 @@
|
||||
<string name="media_compression_quality_medium">Média</string>
|
||||
<string name="media_compression_quality_high">Alta</string>
|
||||
<string name="media_compression_quality_uncompressed">Sem compressão</string>
|
||||
<string name="video_codec_h265_label">Usar codec H.265/HEVC</string>
|
||||
<string name="video_codec_h265_description">Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265.</string>
|
||||
<string name="edit_draft">Editar rascunho</string>
|
||||
<string name="login_with_qr_code">Entrar com Código QR</string>
|
||||
<string name="route">Rota</string>
|
||||
@@ -1012,6 +1040,8 @@
|
||||
<string name="torrent_download">Baixar</string>
|
||||
<string name="torrent_failure">Falha ao abrir o arquivo</string>
|
||||
<string name="torrent_no_apps">Nenhum aplicativo torrent instalado para abrir e baixar o arquivo.</string>
|
||||
<string name="torrent_no_info">O evento não tem informações suficientes para criar um link magnético</string>
|
||||
<string name="my_lists_and_sets">Minhas listas/conjuntos</string>
|
||||
<string name="select_list_to_filter">Selecione uma lista para filtrar o feed</string>
|
||||
<string name="temporary_account">Terminar sessão no bloqueio do dispositivo</string>
|
||||
<string name="private_message">Mensagem Privada</string>
|
||||
@@ -1033,4 +1063,5 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Gostaria de enviar o relatório de falha recente para o Amethyst em uma DM? Nenhuma informação pessoal será compartilhada</string>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -456,6 +456,8 @@ 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="follow_set_type_private">Zasebno</string>
|
||||
<string name="follow_set_type_mixed">Mešano</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)
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
<string name="post">Dela</string>
|
||||
<string name="save">Spara</string>
|
||||
<string name="create">Skapa</string>
|
||||
<string name="rename">Byt namn</string>
|
||||
<string name="cancel">Avbryt</string>
|
||||
<string name="failed_to_upload_the_image">Det gick inte att ladda upp bilden</string>
|
||||
<string name="relay_address">Relä Adress</string>
|
||||
@@ -443,6 +444,31 @@
|
||||
<string name="follow_list_aroundme">Runt mig</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Tyst listan</string>
|
||||
<string name="follow_sets">Följ-set</string>
|
||||
<string name="labeled_bookmarks">Märkta bokmärken</string>
|
||||
<string name="general_bookmarks">Allmänna bokmärken</string>
|
||||
<string name="follow_set_type_public">Offentlig</string>
|
||||
<string name="follow_set_type_private">Privat</string>
|
||||
<string name="follow_set_type_mixed">Blandad</string>
|
||||
<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_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>
|
||||
<string name="follow_set_creation_menu_title">Skapa ny lista</string>
|
||||
<string name="follow_set_creation_item_label">Skapa ny %1$s-lista med användare</string>
|
||||
<string name="follow_set_creation_item_description">Skapar ett %1$s-följ-set och lägger till %2$s i det.</string>
|
||||
<string name="follow_set_creation_dialog_title">Ny %1$s-lista</string>
|
||||
<string name="follow_set_creation_name_label">Set-namn</string>
|
||||
<string name="follow_set_creation_desc_label">Set-beskrivning (valfritt)</string>
|
||||
<string name="follow_set_creation_action_btn_label">Skapa set</string>
|
||||
<string name="follow_set_rename_btn_label">Byt namn på set</string>
|
||||
<string name="follow_set_rename_dialog_indicator_first_part">Du byter namn från </string>
|
||||
<string name="follow_set_rename_dialog_indicator_second_part"> till..</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Standardporten är 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Anslut genom Tor med Orbot
|
||||
\n\n1. Installera [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
@@ -829,6 +855,8 @@
|
||||
<string name="media_compression_quality_medium">Medel</string>
|
||||
<string name="media_compression_quality_high">Hög</string>
|
||||
<string name="media_compression_quality_uncompressed">Okomprimerad</string>
|
||||
<string name="video_codec_h265_label">Använd H.265/HEVC-codec</string>
|
||||
<string name="video_codec_h265_description">Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning.</string>
|
||||
<string name="edit_draft">Redigera utkast</string>
|
||||
<string name="login_with_qr_code">Logga in med QR-kod</string>
|
||||
<string name="route">Rutt</string>
|
||||
@@ -1011,6 +1039,8 @@
|
||||
<string name="torrent_download">Ladda ner</string>
|
||||
<string name="torrent_failure">Det gick inte att öppna filen</string>
|
||||
<string name="torrent_no_apps">Inga torrent-appar installerade för att öppna och ladda ner filen.</string>
|
||||
<string name="torrent_no_info">Händelsen har inte tillräcklig information för att skapa en magnetlänk</string>
|
||||
<string name="my_lists_and_sets">Mina listor/set</string>
|
||||
<string name="select_list_to_filter">Välj en lista för att filtrera flödet</string>
|
||||
<string name="temporary_account">Logga ut när enheten låses</string>
|
||||
<string name="private_message">Privat meddelande</string>
|
||||
@@ -1032,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">Vill du skicka den senaste kraschrapporten till Amethyst i ett DM? Ingen personlig information kommer att delas</string>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -1065,4 +1065,5 @@
|
||||
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">要用私信将最近的崩溃报告发送给 Amethyst 吗?不会分享个人信息</string>
|
||||
<string name="crashreport_found_send">发送它</string>
|
||||
<string name="this_message_will_disappear_in_days">此消息将在 %1$d 天内消失</string>
|
||||
<string name="select_signer">选择签名者</string>
|
||||
</resources>
|
||||
|
||||
@@ -1039,6 +1039,8 @@
|
||||
<string name="media_compression_quality_medium">Medium</string>
|
||||
<string name="media_compression_quality_high">High</string>
|
||||
<string name="media_compression_quality_uncompressed">Uncompressed</string>
|
||||
<string name="video_codec_h265_label">Use H.265/HEVC Codec</string>
|
||||
<string name="video_codec_h265_description">Better quality at smaller file sizes but not all devices support H.265 playback.</string>
|
||||
|
||||
<string name="edit_draft">Edit draft</string>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ languageId = "17.0.6"
|
||||
lazysodiumAndroid = "5.2.0"
|
||||
lazysodiumJava = "5.2.0"
|
||||
lifecycleRuntimeKtx = "2.9.4"
|
||||
lightcompressor = "1.3.3"
|
||||
lightcompressor = "1.5.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-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor" }
|
||||
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" }
|
||||
@@ -145,7 +145,7 @@ vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", v
|
||||
vico-charts-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico-charts" }
|
||||
vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts" }
|
||||
vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts" }
|
||||
zelory-video-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" }
|
||||
zelory-image-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" }
|
||||
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
|
||||
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||
zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
|
||||
Reference in New Issue
Block a user