Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness

This commit is contained in:
KotlinGeekDev
2026-01-16 16:11:23 +01:00
276 changed files with 2984 additions and 1190 deletions
+3
View File
@@ -348,6 +348,9 @@ dependencies {
// Image compression lib
implementation libs.zelory.image.compressor
// Voice anonymization DSP
implementation libs.tarsosdsp
// Cbor for cashuB format
implementation libs.kotlinx.serialization.cbor
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.verify
@@ -174,7 +174,7 @@ class ThreadDualAxisChartAssemblerTest {
null,
)
val filter = ThreadFeedFilter(account, naddr.toTag())
val filter = ThreadFeedFilter(account, naddr.toTag(), LocalCache)
val calculatedFeed = filter.feed()
val expectedOrder =
@@ -26,8 +26,11 @@ import android.content.pm.ApplicationInfo
import android.os.Debug
import androidx.core.content.getSystemService
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
@@ -228,3 +231,12 @@ inline fun debug(
Log.d(tag, debugMessage())
}
}
fun Event.countMemory(): Int =
7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
12 + // createdAt + kind
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() +
sig.bytesUsedInMemory()
@@ -24,15 +24,21 @@ import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.model.IAccount
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListState
import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction
import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction
import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
@@ -46,11 +52,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState
import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
@@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListD
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
@@ -571,7 +571,8 @@ class Account(
suspend fun report(
user: User,
type: ReportType,
) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer))
content: String = "",
) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, content, userProfile(), signer))
suspend fun delete(note: Note) = delete(listOf(note))
@@ -1658,11 +1659,11 @@ class Account(
fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
fun isHidden(user: User) = isHidden(user.pubkeyHex)
override fun isHidden(user: User) = isHidden(user.pubkeyHex)
fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex)
fun followingKeySet(): Set<HexKey> = kind3FollowList.flow.value.authors
override fun followingKeySet(): Set<HexKey> = kind3FollowList.flow.value.authors
fun isAcceptable(user: User): Boolean {
if (userProfile().pubkeyHex == user.pubkeyHex) {
@@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
@@ -143,7 +145,8 @@ class AccountSettings(
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
) {
) : EphemeralChatRepository,
PublicChatListRepository {
val saveable = MutableStateFlow(AccountSettingsUpdater(null))
val syncedSettings: AccountSyncedSettings = AccountSyncedSettings(AccountSyncedSettingsInternal())
@@ -389,7 +392,9 @@ class AccountSettings(
}
}
fun updateChannelListTo(newChannelList: ChannelListEvent?) {
override fun channelList() = backupChannelList
override fun updateChannelListTo(newChannelList: ChannelListEvent?) {
if (newChannelList == null || newChannelList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
@@ -429,7 +434,9 @@ class AccountSettings(
}
}
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
override fun ephemeralChatList() = backupEphemeralChatList
override fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
@@ -106,12 +106,11 @@ class AntiSpamFilter {
recentAddressables.put(hash, address)
} else {
// normal event
val existingEvent = recentEventIds[hash]
if (
(recentEventIds[hash] != null && recentEventIds[hash] != event.id) ||
(existingEvent != null && existingEvent != event.id) ||
(spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id))
) {
val existingEvent = recentEventIds[hash]
val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay))
val link2 = njumpLink(NEvent.create(event.id, null, null, relay))
@@ -23,14 +23,16 @@ package com.vitorpamplona.amethyst.model
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
import com.vitorpamplona.amethyst.model.privateChats.ChatroomList
import com.vitorpamplona.amethyst.service.BundledInsert
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.note.dateFormatter
@@ -212,7 +214,7 @@ interface ILocalCache {
) {}
}
object LocalCache : ILocalCache {
object LocalCache : ILocalCache, ICacheProvider {
val antiSpam = AntiSpamFilter()
val users = LargeSoftCache<HexKey, User>()
@@ -317,16 +319,24 @@ object LocalCache : ILocalCache {
}
}
fun getUserIfExists(key: String): User? {
override fun getUserIfExists(key: String): User? {
if (key.isEmpty()) return null
return users.get(key)
}
override fun countUsers(predicate: (String, User) -> Boolean): Int {
var count = 0
users.forEach { key, user ->
if (predicate(key, user)) count++
}
return count
}
fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) }
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
@@ -357,7 +367,7 @@ object LocalCache : ILocalCache {
return null
}
fun checkGetOrCreateNote(key: String): Note? {
override fun checkGetOrCreateNote(key: String): Note? {
if (ATag.isATag(key)) {
return checkGetOrCreateAddressableNote(key)
}
@@ -382,6 +392,19 @@ object LocalCache : ILocalCache {
return null
}
override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream =
object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream {
override val newEventBundles = live.newEventBundles
override val deletedEventBundles = live.deletedEventBundles
}
override fun hasBeenDeleted(event: Any): Boolean =
if (event is Event) {
deletionIndex.hasBeenDeleted(event)
} else {
false
}
fun getOrAddAliasNote(
idHex: String,
note: Note,
@@ -436,7 +459,7 @@ object LocalCache : ILocalCache {
fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) }
fun getOrCreateAddressableNote(key: Address): AddressableNote {
override fun getOrCreateAddressableNote(key: Address): AddressableNote {
val note = getOrCreateAddressableNoteInternal(key)
// Loads the user outside a Syncronized block to avoid blocking
if (note.author == null) {
@@ -497,9 +520,13 @@ object LocalCache : ILocalCache {
// avoids processing empty contact lists.
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty() && (wasVerified || justVerify(event))) {
user.updateContactList(event)
val needsToUpdateFollowers = user.updateContactList(event)
// Log.d("CL", "Consumed contact list ${user.toNostrUri()} ${event.relays()?.size}")
needsToUpdateFollowers.forEach {
getUserIfExists(it)?.flowSet?.followers?.invalidateData()
}
updateObservables(event)
return true
@@ -1371,7 +1398,7 @@ object LocalCache : ILocalCache {
}
}
fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) }
override fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) }
fun getAnyChannel(noteEvent: Event): Channel? =
when (noteEvent) {
@@ -2690,7 +2717,7 @@ object LocalCache : ILocalCache {
}
}
fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true)
override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true)
fun justConsume(
event: Event,
@@ -24,6 +24,4 @@ package com.vitorpamplona.amethyst.model
typealias Note = com.vitorpamplona.amethyst.commons.model.Note
typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer
typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote
typealias NoteFlowSet = com.vitorpamplona.amethyst.commons.model.NoteFlowSet
typealias NoteBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.NoteBundledRefresherFlow
typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState
@@ -21,9 +21,6 @@
package com.vitorpamplona.amethyst.model
// Re-export from commons for backwards compatibility
typealias UserDependencies = com.vitorpamplona.amethyst.commons.model.UserDependencies
typealias User = com.vitorpamplona.amethyst.commons.model.User
typealias UserFlowSet = com.vitorpamplona.amethyst.commons.model.UserFlowSet
typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo
typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.UserBundledRefresherFlow
typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState
@@ -0,0 +1,27 @@
/**
* 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.service
// Re-export from commons for backwards compatibility
typealias BundledUpdate = com.vitorpamplona.amethyst.commons.service.BundledUpdate
typealias BasicBundledUpdate = com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate
typealias BundledInsert<T> = com.vitorpamplona.amethyst.commons.service.BundledInsert<T>
typealias BasicBundledInsert<T> = com.vitorpamplona.amethyst.commons.service.BasicBundledInsert<T>
@@ -30,8 +30,8 @@ import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.key.Keyer
import coil3.request.Options
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image
import com.vitorpamplona.amethyst.commons.base64Image.toBitmap
import com.vitorpamplona.amethyst.commons.richtext.Base64Image
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.sha256.sha256
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -24,11 +24,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ChannelState
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.ChannelState
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
@@ -25,17 +25,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import kotlinx.collections.immutable.ImmutableList
@@ -392,7 +393,9 @@ fun observeUserFollowerCount(
.followers.stateFlow
.sample(200)
.mapLatest { userState ->
userState.user.transientFollowerCount()
LocalCache.countUsers { _, user ->
user.latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
}
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
}
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
/**
* Listens to NostrClient's onNotify messages from the relay
@@ -47,7 +48,7 @@ class RelaySpeedLogger(
msg: Message,
) {
if (msg is EventMessage) {
current.increment(msg.event.kind, msg.subId, relay.url, msg.event.countMemory())
current.increment(msg.event.kind, msg.subId, relay.url, msgStr.bytesUsedInMemory())
}
}
}
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
class VoiceAnonymizationController(
private val scope: CoroutineScope,
private val logTag: String,
private val onError: (Throwable) -> Unit,
) {
var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE)
private set
var processingPreset: VoicePreset? by mutableStateOf(null)
private set
var distortedFiles: Map<VoicePreset, AnonymizedResult> by mutableStateOf(emptyMap())
private set
private var processingJob: Job? = null
fun activeFile(originalFile: File?): File? =
if (selectedPreset == VoicePreset.NONE) {
originalFile
} else {
distortedFiles[selectedPreset]?.file
}
fun activeWaveform(originalWaveform: List<Float>?): List<Float>? =
if (selectedPreset == VoicePreset.NONE) {
originalWaveform
} else {
distortedFiles[selectedPreset]?.waveform
}
fun selectPreset(
preset: VoicePreset,
originalFile: File?,
) {
if (processingPreset != null || preset == selectedPreset) return
if (preset == VoicePreset.NONE) {
selectedPreset = preset
return
}
if (distortedFiles.containsKey(preset)) {
selectedPreset = preset
return
}
val file = originalFile ?: return
processingJob?.cancel()
processingJob =
scope.launch {
processingPreset = preset
try {
val anonymizer = VoiceAnonymizer()
val result = anonymizer.anonymize(file, preset)
result
.onSuccess { anonymizedResult ->
distortedFiles = distortedFiles + (preset to anonymizedResult)
selectedPreset = preset
}.onFailure { error ->
Log.w(logTag, "Failed to anonymize voice", error)
onError(error)
}
} finally {
processingPreset = null
processingJob = null
}
}
}
fun clear() {
cancelProcessing()
deleteDistortedFiles()
selectedPreset = VoicePreset.NONE
}
fun deleteDistortedFiles() {
distortedFiles.values.forEach { result ->
try {
if (result.file.exists()) {
result.file.delete()
Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}")
}
} catch (e: Exception) {
Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e)
}
}
distortedFiles = emptyMap()
}
private fun cancelProcessing() {
processingJob?.cancel()
processingJob = null
processingPreset = null
}
}
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size5dp
@Composable
fun VoiceAnonymizationSection(
selectedPreset: VoicePreset,
processingPreset: VoicePreset?,
onPresetSelected: (VoicePreset) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(thickness = DividerThickness)
Spacer(modifier = Modifier.height(12.dp))
Column(
verticalArrangement = Arrangement.spacedBy(Size5dp),
) {
Text(
text = stringRes(R.string.voice_anonymize_title),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = stringRes(R.string.voice_anonymize_description),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(modifier = Modifier.height(8.dp))
VoicePresetSelector(
selectedPreset = selectedPreset,
processingPreset = processingPreset,
onPresetSelected = onPresetSelected,
)
}
}
@@ -0,0 +1,450 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMuxer
import android.util.Log
import be.tarsos.dsp.AudioDispatcher
import be.tarsos.dsp.AudioEvent
import be.tarsos.dsp.AudioProcessor
import be.tarsos.dsp.WaveformSimilarityBasedOverlapAdd
import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter
import be.tarsos.dsp.io.TarsosDSPAudioFormat
import be.tarsos.dsp.resample.RateTransposer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.ByteOrder
import kotlin.math.abs
data class AnonymizedResult(
val file: File,
val waveform: List<Float>,
val duration: Int,
)
class VoiceAnonymizer {
companion object {
private const val TAG = "VoiceAnonymizer"
private const val CHANNELS = 1
private const val BIT_RATE = 128000
}
suspend fun anonymize(
inputFile: File,
preset: VoicePreset,
onProgress: (Float) -> Unit = {},
): Result<AnonymizedResult> =
withContext(Dispatchers.IO) {
if (preset == VoicePreset.NONE) {
return@withContext Result.failure(
IllegalArgumentException("Cannot anonymize with NONE preset"),
)
}
try {
val outputFile = createOutputFile(inputFile, preset)
val (pcmData, sampleRate, duration) =
decodeAudioToPcm(inputFile) { progress ->
onProgress(progress * 0.3f)
}
val processedPcm =
processPcmWithTarsos(pcmData, preset, sampleRate) { progress ->
onProgress(0.3f + progress * 0.4f)
}
val waveform = extractWaveform(processedPcm, sampleRate)
encodePcmToAac(processedPcm, sampleRate, outputFile) { progress ->
onProgress(0.7f + progress * 0.3f)
}
onProgress(1f)
Result.success(AnonymizedResult(outputFile, waveform, duration))
} catch (e: Exception) {
Log.e(TAG, "Failed to anonymize audio", e)
Result.failure(e)
}
}
private fun createOutputFile(
inputFile: File,
preset: VoicePreset,
): File {
val baseName = inputFile.nameWithoutExtension
val presetSuffix = preset.name.lowercase()
return File(inputFile.parentFile, "${baseName}_$presetSuffix.mp4")
}
private data class DecodedAudio(
val pcmData: FloatArray,
val sampleRate: Int,
val duration: Int,
)
private suspend fun decodeAudioToPcm(
inputFile: File,
onProgress: (Float) -> Unit,
): DecodedAudio {
val extractor = MediaExtractor()
var decoder: MediaCodec? = null
try {
extractor.setDataSource(inputFile.absolutePath)
var audioTrackIndex = -1
var format: MediaFormat? = null
for (i in 0 until extractor.trackCount) {
val trackFormat = extractor.getTrackFormat(i)
val mime = trackFormat.getString(MediaFormat.KEY_MIME)
if (mime?.startsWith("audio/") == true) {
audioTrackIndex = i
format = trackFormat
break
}
}
if (audioTrackIndex == -1 || format == null) {
throw IllegalStateException("No audio track found in file")
}
extractor.selectTrack(audioTrackIndex)
val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm"
val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
val durationUs = format.getLong(MediaFormat.KEY_DURATION)
val duration = (durationUs / 1_000_000).toInt()
decoder = MediaCodec.createDecoderByType(mime)
decoder.configure(format, null, null, 0)
decoder.start()
val pcmSamples = mutableListOf<Float>()
val bufferInfo = MediaCodec.BufferInfo()
var inputDone = false
var outputDone = false
while (!outputDone && currentCoroutineContext().isActive) {
if (!inputDone) {
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
if (inputBufferIndex >= 0) {
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
decoder.queueInputBuffer(
inputBufferIndex,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
)
inputDone = true
} else {
val presentationTimeUs = extractor.sampleTime
decoder.queueInputBuffer(
inputBufferIndex,
0,
sampleSize,
presentationTimeUs,
0,
)
extractor.advance()
if (durationUs > 0) {
onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f))
}
}
}
}
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000)
if (outputBufferIndex >= 0) {
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer()
while (shortBuffer.hasRemaining()) {
pcmSamples.add(shortBuffer.get() / 32768f)
}
decoder.releaseOutputBuffer(outputBufferIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
outputDone = true
}
}
}
return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration)
} finally {
decoder?.stop()
decoder?.release()
extractor.release()
}
}
private fun processPcmWithTarsos(
pcmData: FloatArray,
preset: VoicePreset,
sampleRate: Int,
onProgress: (Float) -> Unit,
): FloatArray {
val baseFactor = preset.pitchFactor
val factor =
when (preset) {
VoicePreset.DEEP, VoicePreset.HIGH -> {
// Add ±10% random variation
val randomShift = 0.9 + (Math.random() * 0.2)
baseFactor * randomShift
}
else -> baseFactor
}
val processedSamples = mutableListOf<Float>()
val totalSamples = pcmData.size
val wsola =
WaveformSimilarityBasedOverlapAdd(
WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults(
factor,
sampleRate.toDouble(),
),
)
val rateTransposer = RateTransposer(factor)
val bufferSize = wsola.inputBufferSize
val overlap = wsola.overlap
val tarsosDspFormat =
TarsosDSPAudioFormat(
sampleRate.toFloat(),
16,
1,
true,
false,
)
val collector =
object : AudioProcessor {
override fun process(audioEvent: AudioEvent): Boolean {
val buffer = audioEvent.floatBuffer
for (i in 0 until audioEvent.bufferSize) {
processedSamples.add(buffer[i])
}
return true
}
override fun processingFinished() {}
}
val dispatcher =
AudioDispatcher(
FloatArrayAudioInputStream(pcmData, tarsosDspFormat, pcmData.size.toLong()),
bufferSize,
overlap,
)
wsola.setDispatcher(dispatcher)
dispatcher.addAudioProcessor(wsola)
dispatcher.addAudioProcessor(rateTransposer)
dispatcher.addAudioProcessor(collector)
var samplesProcessed = 0
val progressProcessor =
object : AudioProcessor {
override fun process(audioEvent: AudioEvent): Boolean {
samplesProcessed += audioEvent.bufferSize
onProgress((samplesProcessed.toFloat() / totalSamples).coerceIn(0f, 1f))
return true
}
override fun processingFinished() {}
}
dispatcher.addAudioProcessor(progressProcessor)
dispatcher.run()
return processedSamples.toFloatArray()
}
private fun extractWaveform(
pcmData: FloatArray,
sampleRate: Int,
): List<Float> {
val waveform = mutableListOf<Float>()
var offset = 0
while (offset < pcmData.size) {
val end = minOf(offset + sampleRate, pcmData.size)
var maxAmplitude = 0f
for (i in offset until end) {
val amplitude = abs(pcmData[i])
if (amplitude > maxAmplitude) {
maxAmplitude = amplitude
}
}
waveform.add(maxAmplitude * 32768f)
offset += sampleRate
}
return waveform
}
private fun encodePcmToAac(
pcmData: FloatArray,
sampleRate: Int,
outputFile: File,
onProgress: (Float) -> Unit,
) {
val format =
MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, CHANNELS)
format.setInteger(
MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC,
)
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
var muxerStarted = false
try {
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
encoder.start()
var audioTrackIndex = -1
val bufferInfo = MediaCodec.BufferInfo()
var inputOffset = 0
var inputDone = false
var outputDone = false
val totalSamples = pcmData.size
while (!outputDone) {
if (!inputDone) {
val inputBufferIndex = encoder.dequeueInputBuffer(10000)
if (inputBufferIndex >= 0) {
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
inputBuffer.clear()
val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset)
if (samplesToWrite <= 0) {
encoder.queueInputBuffer(
inputBufferIndex,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
)
inputDone = true
} else {
for (i in 0 until samplesToWrite) {
val sample =
(pcmData[inputOffset + i] * 32767)
.toInt()
.coerceIn(-32768, 32767)
.toShort()
inputBuffer.putShort(sample)
}
val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate
encoder.queueInputBuffer(
inputBufferIndex,
0,
inputBuffer.position(),
presentationTimeUs,
0,
)
inputOffset += samplesToWrite
onProgress(inputOffset.toFloat() / totalSamples)
}
}
}
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000)
when {
outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
audioTrackIndex = muxer.addTrack(encoder.outputFormat)
muxer.start()
muxerStarted = true
}
outputBufferIndex >= 0 -> {
val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!!
if (muxerStarted && bufferInfo.size > 0) {
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo)
}
encoder.releaseOutputBuffer(outputBufferIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
outputDone = true
}
}
}
}
} finally {
encoder.stop()
encoder.release()
if (muxerStarted) {
muxer.stop()
}
muxer.release()
}
}
}
private class FloatArrayAudioInputStream(
private val floatArray: FloatArray,
private val format: TarsosDSPAudioFormat,
private val frameLength: Long,
) : be.tarsos.dsp.io.TarsosDSPAudioInputStream {
private var position = 0
override fun getFormat(): TarsosDSPAudioFormat = format
override fun getFrameLength(): Long = frameLength
override fun read(
buffer: ByteArray,
offset: Int,
length: Int,
): Int {
val converter = TarsosDSPAudioFloatConverter.getConverter(format)
val floatBuffer = FloatArray(length / 2)
val samplesToRead = minOf(floatBuffer.size, floatArray.size - position)
if (samplesToRead <= 0) return -1
System.arraycopy(floatArray, position, floatBuffer, 0, samplesToRead)
position += samplesToRead
converter.toByteArray(floatBuffer, samplesToRead, buffer, offset)
return samplesToRead * 2
}
override fun skip(bytesToSkip: Long): Long {
val samplesToSkip = (bytesToSkip / 2).toInt()
val actualSkip = minOf(samplesToSkip, floatArray.size - position)
position += actualSkip
return actualSkip.toLong() * 2
}
override fun close() {}
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import com.vitorpamplona.amethyst.R
enum class VoicePreset(
val pitchFactor: Double,
val labelRes: Int,
) {
NONE(1.0, R.string.voice_preset_none),
DEEP(1.4, R.string.voice_preset_deep),
HIGH(0.75, R.string.voice_preset_high),
NEUTRAL(0.9, R.string.voice_preset_neutral),
}
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun VoicePresetSelector(
selectedPreset: VoicePreset,
processingPreset: VoicePreset?,
onPresetSelected: (VoicePreset) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val isProcessing = processingPreset != null
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
) {
VoicePreset.entries.forEach { preset ->
val isSelected = preset == selectedPreset
val isThisProcessing = preset == processingPreset
val isEnabled = !isProcessing || preset == VoicePreset.NONE
FilterChip(
selected = isSelected,
onClick = { if (isEnabled) onPresetSelected(preset) },
enabled = isEnabled,
label = {
if (isThisProcessing) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary,
)
} else {
Text(stringRes(context, preset.labelRes))
}
},
colors =
FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedLabelColor = MaterialTheme.colorScheme.onPrimary,
),
)
}
}
}
@@ -375,7 +375,7 @@ fun CustomEmojiChecker(
onEmojiText: @Composable (ImmutableList<CustomEmoji.Renderable>) -> Unit,
) {
val mayContainEmoji by remember(text, tags) {
mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags))
mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags?.lists))
}
if (mayContainEmoji) {
@@ -385,7 +385,7 @@ fun CustomEmojiChecker(
}
LaunchedEffect(text, tags) {
val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags)
val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags?.lists)
if (newEmojiList != null) {
emojiList = newEmojiList
}
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.ListChange
import com.vitorpamplona.amethyst.commons.model.ListChange
import kotlinx.coroutines.flow.MutableSharedFlow
interface ChangesFlowFilter<T> : IAdditiveFeedFilter<T> {
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.dal
// Re-export from commons for backwards compatibility
typealias IFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.IFeedFilter<T>
typealias IAdditiveFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.IAdditiveFeedFilter<T>
typealias FeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter<T>
typealias AdditiveFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter<T>
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.BundledInsert
import com.vitorpamplona.amethyst.service.BundledUpdate
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.commons.model.Channel
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
@@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.feeds
// Re-export from commons for backwards compatibility - import everything
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState as CommonsFeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState as CommonsFeedState
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent as CommonsInvalidatableContent
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState as CommonsLoadedFeedState
typealias FeedState = CommonsFeedState
typealias LoadedFeedState<T> = CommonsLoadedFeedState<T>
typealias InvalidatableContent = CommonsInvalidatableContent
typealias FeedContentState = CommonsFeedContentState
@@ -20,13 +20,13 @@
*/
package com.vitorpamplona.amethyst.ui.navigation.routes
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@@ -32,13 +32,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
@@ -53,9 +53,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
@@ -20,14 +20,16 @@
*/
package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
@Stable
class EmojiSuggestionState(
val account: Account,
) {
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
@@ -43,10 +42,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -57,7 +55,6 @@ fun ShowEmojiSuggestionList(
emojiSuggestions: EmojiSuggestionState,
onSelect: (EmojiPackState.EmojiMedia) -> Unit,
onFullSize: (EmojiPackState.EmojiMedia) -> Unit,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
) {
val suggestions by emojiSuggestions.results.collectAsStateWithLifecycle(emptyList())
@@ -79,22 +76,23 @@ fun ShowEmojiSuggestionList(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = spacedBy(Size10dp),
) {
Box(Size40Modifier) {
UrlImageView(it.link, accountViewModel)
}
AsyncImage(
it.link,
contentDescription = it.code,
modifier = Size40Modifier,
)
Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Box(Size40Modifier, contentAlignment = Alignment.Center) {
IconButton(
onClick = {
onFullSize(it)
},
) {
Icon(
imageVector = Icons.Outlined.OpenInFull,
contentDescription = stringRes(R.string.use_direct_url),
modifier = Modifier.size(20.dp),
)
}
IconButton(
modifier = Size40Modifier,
onClick = {
onFullSize(it)
},
) {
Icon(
imageVector = Icons.Outlined.OpenInFull,
contentDescription = stringRes(R.string.use_direct_url),
modifier = Modifier.size(20.dp),
)
}
}
HorizontalDivider(
@@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.currentWord
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
@@ -435,7 +435,7 @@ open class CommentPostViewModel :
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let {
EmojiUrlTag(
it.code,
it.link.url,
it.link,
)
}
}
@@ -634,11 +634,11 @@ open class CommentPostViewModel :
}
open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) {
val wordToInsert = item.link.url + " "
val wordToInsert = item.link + " "
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
iMetaAttachments.downloadAndPrepare(item.link) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link)
}
}
@@ -357,7 +357,6 @@ private fun GenericCommentPostBody(
it,
postViewModel::autocompleteWithEmoji,
postViewModel::autocompleteWithEmojiUrl,
accountViewModel,
modifier = Modifier.heightIn(0.dp, 300.dp),
)
}
@@ -100,6 +100,13 @@ fun RenderTextEvent(
}
}
// Check if this is an audio-only event (content is just an audio URL with waveform IMeta)
val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() }
if (isAudioOnly) {
RenderAudioFromIMeta(note, accountViewModel, nav)
return
}
LoadDecryptedContent(
note,
accountViewModel,
@@ -69,8 +69,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.ui.theme.Size75Modifier
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip14Subject.subject
import com.vitorpamplona.quartz.nip92IMeta.imetas
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
@Composable
@@ -107,6 +110,32 @@ fun VoiceHeader(
?.let { WaveformData(it) }
}
RenderAudioWithWaveform(
mediaUrl = media,
title = noteEvent.subject(),
mimeType = null,
waveform = waveform,
note = note,
accountViewModel = accountViewModel,
nav = nav,
)
}
/**
* Shared composable for rendering audio with waveform visualization.
* Used by both VoiceHeader (for BaseVoiceEvent) and RenderAudioFromIMeta (for other events with audio IMeta).
*/
@Composable
fun RenderAudioWithWaveform(
mediaUrl: String,
title: String?,
mimeType: String?,
waveform: WaveformData?,
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event ?: return
val callbackUri = remember(note) { note.toNostrUri() }
Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) {
@@ -114,14 +143,14 @@ fun VoiceHeader(
verticalAlignment = Alignment.CenterVertically,
) {
GetMediaItem(
videoUri = media,
title = noteEvent.subject(),
videoUri = mediaUrl,
title = title,
artworkUri = null,
authorName = note.author?.toBestDisplayName(),
callbackUri = callbackUri,
mimeType = null,
mimeType = mimeType,
aspectRatio = null,
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media),
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(mediaUrl),
keepPlaying = false,
waveformData = waveform,
) { mediaItem ->
@@ -168,7 +197,7 @@ fun RenderVoicePlayer(
factory = { context: Context ->
PlayerView(context).apply {
player = controllerState.controller
// if we alrady know the size of the frame, this forces the player to stay in the size
// if we already know the size of the frame, this forces the player to stay in the size
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
@@ -263,3 +292,54 @@ fun PlayPauseButton(controllerState: MediaControllerState) {
}
}
}
/**
* Extracts AudioMeta from an event's IMeta tags if it has audio content with waveform.
* Returns the first audio IMeta that has a waveform, or null if none found.
*/
fun Event.getAudioMetaWithWaveform(): AudioMeta? {
val audioMetas = imetas().map { AudioMeta.parse(it) }
return audioMetas.firstOrNull { meta ->
meta.waveform != null &&
(meta.mimeType == null || meta.mimeType?.startsWith("audio/") == true)
}
}
/**
* Checks if the event content is primarily an audio attachment (content is just the audio URL).
*/
fun Event.isAudioOnlyContent(): Boolean {
val audioMeta = getAudioMetaWithWaveform() ?: return false
return content.trim() == audioMeta.url
}
/**
* Renders audio with waveform for any event type that has audio IMeta attachment.
* This allows KIND 1 (TextNoteEvent) and KIND 1111 (CommentEvent) with voice
* attachments to display the same waveform UI as VoiceEvent.
*/
@Composable
fun RenderAudioFromIMeta(
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event ?: return
val audioMeta = remember(noteEvent) { noteEvent.getAudioMetaWithWaveform() } ?: return
val waveform =
remember(audioMeta) {
audioMeta.waveform?.let { WaveformData(it) }
}
RenderAudioWithWaveform(
mediaUrl = audioMeta.url,
title = null,
mimeType = audioMeta.mimeType,
waveform = waveform,
note = note,
accountViewModel = accountViewModel,
nav = nav,
)
}
@@ -28,12 +28,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
@@ -20,52 +20,17 @@
*/
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Stable
abstract class FeedViewModel(
// Re-export from commons for backwards compatibility
typealias FeedViewModel = com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel
/**
* Android-specific FeedViewModel base class that provides LocalCache as the cache provider.
* Subclasses can extend this to automatically use LocalCache without passing cacheProvider.
*/
abstract class AndroidFeedViewModel(
localFilter: FeedFilter<Note>,
) : ViewModel(),
InvalidatableContent {
val feedState = FeedContentState(localFilter, viewModelScope)
override val isRefreshing = feedState.isRefreshing
fun sendToTop() = feedState.sendToTop()
suspend fun sentToTop() = feedState.sentToTop()
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
init {
Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}")
viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes ->
Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}")
feedState.updateFeedWith(newNotes)
}
}
viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.deletedEventBundles.collect { newNotes ->
Log.d("Rendering Metrics", "Delete from feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}")
feedState.deleteFromFeed(newNotes)
}
}
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
super.onCleared()
}
}
) : FeedViewModel(localFilter, LocalCache)
@@ -25,12 +25,12 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.BundledUpdate
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableList
@@ -20,11 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter
@@ -50,28 +51,28 @@ class AccountFeedContentStates(
val scope: CoroutineScope,
) {
val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope)
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope)
val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope)
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache)
val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope, LocalCache)
val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope)
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope)
val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope, LocalCache)
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache)
val videoFeed = FeedContentState(VideoFeedFilter(account), scope)
val videoFeed = FeedContentState(VideoFeedFilter(account), scope, LocalCache)
val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope)
val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope)
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope)
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope)
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope)
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope)
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope)
val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope, LocalCache)
val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope, LocalCache)
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope, LocalCache)
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope, LocalCache)
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope, LocalCache)
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope, LocalCache)
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
val notificationSummary = NotificationSummaryState(account)
val feedListOptions = TopNavFilterState(account, scope)
val drafts = FeedContentState(DraftEventsFeedFilter(account), scope)
val drafts = FeedContentState(DraftEventsFeedFilter(account), scope, LocalCache)
suspend fun init() {
notificationSummary.initializeSuspend()
@@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -41,6 +40,10 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
@@ -50,9 +53,6 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.UiSettingsFlow
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator
import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder
import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder
@@ -67,15 +67,10 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.service.uploads.UploadingState
import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
@@ -107,6 +102,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -134,7 +130,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -390,7 +385,7 @@ class AccountViewModel(
note.flow().author(),
note.flow().metadata.stateFlow,
note.flow().reports.stateFlow,
) { hiddenUsers, followingUsers, autor, metadata, reports ->
) { hiddenUsers, followingUsers, _, metadata, _ ->
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
}.onStart {
emit(
@@ -706,9 +701,10 @@ class AccountViewModel(
fun report(
user: User,
type: ReportType,
content: String = "",
) {
launchSigner {
account.report(user, type)
account.report(user, type, content)
account.hideUser(user.pubkeyHex)
}
}
@@ -998,6 +994,32 @@ class AccountViewModel(
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
/**
* Fixes author and relay hints in MarkedETag list by looking up notes from cache.
* This ensures reply tags have proper author pubkeys and relay hints for threading.
*/
fun fixReplyTagHints(tags: List<MarkedETag>) {
tags.forEach { tag ->
val note = getNoteIfExists(tag.eventId)
val cachedAuthor = note?.author?.pubkeyHex
val cachedRelay = note?.relayHintUrl()
// Fix author if missing or different from cached
if (tag.author.isNullOrBlank() && cachedAuthor != null) {
tag.author = cachedAuthor
} else if (cachedAuthor != null && tag.author != cachedAuthor) {
tag.author = cachedAuthor
}
// Fix relay hint if missing or different from cached
if (tag.relay == null && cachedRelay != null) {
tag.relay = cachedRelay
} else if (cachedRelay != null && tag.relay != cachedRelay) {
tag.relay = cachedRelay
}
}
}
override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address)
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
@@ -1011,11 +1033,11 @@ class AccountViewModel(
LocalCache.findLatestModificationForNote(note)
}
fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key)
fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key)
fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key)
fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key)
fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key)
fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel = LocalCache.getOrCreateEphemeralChannel(key)
fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex)
@@ -1148,53 +1170,6 @@ class AccountViewModel(
super.onCleared()
}
fun sendVoiceReply(
note: Note,
recording: RecordingResult,
context: Context,
) {
if (isWriteable()) {
val hint = note.toEventHint<BaseVoiceEvent>() ?: return
launchSigner {
val uploader = UploadOrchestrator()
val result =
uploader.upload(
uri = recording.file.toUri(),
mimeType = recording.mimeType,
alt = null,
contentWarningReason = null,
compressionQuality = CompressorQuality.UNCOMPRESSED,
server = account.settings.defaultFileServer,
account = account,
context = context,
)
if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
account.sendVoiceReplyMessage(
result.result.url,
result.result.fileHeader.mimeType ?: recording.mimeType,
result.result.fileHeader.hash,
recording.duration,
recording.amplitudes,
hint,
)
} else if (result is UploadingState.Error) {
toastManager.toast(
R.string.failed_to_upload_media_no_details,
result.errorResource,
*result.params,
)
}
}
} else {
toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_reply,
)
}
}
fun loadThumb(
context: Context,
thumbUri: String,
@@ -1422,7 +1397,7 @@ class AccountViewModel(
// First check if we have an actual response from the DVM in LocalCache
val response =
LocalCache.notes.maxOrNullOf(
filter = { key, note ->
filter = { _, note ->
val noteEvent = note.event
noteEvent is NIP90ContentDiscoveryResponseEvent &&
noteEvent.pubKey == pubkeyHex &&
@@ -1531,8 +1506,6 @@ class AccountViewModel(
}
}
fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account)
fun convertAccounts(loggedInAccounts: List<AccountInfo>?): Set<HexKey> =
loggedInAccounts
?.mapNotNull {
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.privateChats.ChatroomList
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.IEvent
@@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
@Stable
class BookmarkPrivateFeedViewModel(
val account: Account,
) : FeedViewModel(BookmarkPrivateFeedFilter(account)) {
) : AndroidFeedViewModel(BookmarkPrivateFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
@@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
@Stable
class BookmarkPublicFeedViewModel(
val account: Account,
) : FeedViewModel(BookmarkPublicFeedFilter(account)) {
) : AndroidFeedViewModel(BookmarkPublicFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
@@ -30,12 +30,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
@@ -24,12 +24,13 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.model.ListChange
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.ListChange
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
@@ -53,7 +54,7 @@ abstract class ListChangeFeedViewModel(
localFilter: ChangesFlowFilter<Note>,
) : ViewModel(),
InvalidatableContent {
val feedState = FeedContentState(localFilter, viewModelScope)
val feedState = FeedContentState(localFilter, viewModelScope, LocalCache)
override val isRefreshing = feedState.isRefreshing
@@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.compose.currentWord
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
@@ -519,7 +519,7 @@ class ChatNewMessageViewModel :
): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) }
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) }
}
}
@@ -659,11 +659,11 @@ class ChatNewMessageViewModel :
}
fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) {
val wordToInsert = item.link.url + " "
val wordToInsert = item.link + " "
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
iMetaAttachments.downloadAndPrepare(item.link) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link)
}
}
@@ -311,7 +311,6 @@ fun GroupDMScreenContent(
it,
postViewModel::autocompleteWithEmoji,
postViewModel::autocompleteWithEmojiUrl,
accountViewModel,
Modifier.heightIn(0.dp, 300.dp),
)
}
@@ -127,7 +127,6 @@ fun PrivateMessageEditFieldRow(
it,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
}
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ListChangeFeedViewModel
class ChannelFeedViewModel(
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -30,8 +30,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -35,7 +35,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
@@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -30,8 +30,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
@@ -41,7 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -37,7 +37,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
@@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -31,9 +31,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
@@ -29,8 +29,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
@@ -27,8 +27,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
@@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -36,9 +36,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.currentWord
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
@@ -484,7 +484,7 @@ open class ChannelNewMessageViewModel :
): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) }
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) }
}
}
@@ -575,11 +575,11 @@ open class ChannelNewMessageViewModel :
}
open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) {
val wordToInsert = item.link.url + " "
val wordToInsert = item.link + " "
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
iMetaAttachments.downloadAndPrepare(item.link) {
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link)
}
}
@@ -104,7 +104,6 @@ fun EditFieldRow(
it,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
}
@@ -41,11 +41,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
@@ -31,11 +31,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState

Some files were not shown because too many files have changed in this diff Show More