mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4191 from vitorpamplona/claude/sharp-keller-7itny0
refactor: one shared user avatar, shared user and event observers, score tags
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
|
||||
/**
|
||||
* The account a signer request acts as — avatar + display name — so it's clear WHICH logged-in
|
||||
|
||||
+1
-1
@@ -95,11 +95,11 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_select_no
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_signing_as
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_messages_with
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.call.CallSessionBridge
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
@@ -2048,7 +2048,7 @@ class Account(
|
||||
// blessed status. 40002 survives only as a read-compat tail from the
|
||||
// 10002 -> 40001 -> 40002 migration, so we were the last active writer of a kind
|
||||
// their clients no longer thread on. Reading 40002 stays supported (see
|
||||
// [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply]).
|
||||
// [com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply]).
|
||||
//
|
||||
// Attached media rides as URLs appended to the content.
|
||||
val root = rootEvent.tags.buzzThreadRoot() ?: rootEvent.tags.buzzThreadReply() ?: rootEvent.id
|
||||
|
||||
@@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_consent_deny_always
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_hide_event
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_not_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_show_event
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -97,13 +97,13 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_read_venue
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_send_dm
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread_with
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
|
||||
+44
-357
@@ -22,465 +22,152 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.textNoteModifications
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.isForCommunity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeCommunityApprovalNeedStatus as sharedObserveCommunityApprovalNeedStatus
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote as sharedObserveNote
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteAndMap as sharedObserveNoteAndMap
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent as sharedObserveNoteEvent
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEventAndMap as sharedObserveNoteEventAndMap
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEventAndMapNotNull as sharedObserveNoteEventAndMapNotNull
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteHasEvent as sharedObserveNoteHasEvent
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteMinichatReplyCount as sharedObserveNoteMinichatReplyCount
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteModifications as sharedObserveNoteModifications
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteOts as sharedObserveNoteOts
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReactionCount as sharedObserveNoteReactionCount
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReactions as sharedObserveNoteReactions
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReferences as sharedObserveNoteReferences
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReplies as sharedObserveNoteReplies
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReplyCount as sharedObserveNoteReplyCount
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteRepostCount as sharedObserveNoteRepostCount
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReposts as sharedObserveNoteReposts
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteRepostsBy as sharedObserveNoteRepostsBy
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteZaps as sharedObserveNoteZaps
|
||||
|
||||
/*
|
||||
* Android overloads of the shared per-note observers in commons `relayClient/event`: they take the
|
||||
* account and event-finder data source from [AccountViewModel] instead of the composition locals,
|
||||
* so they also work under Activity roots that don't provide those locals.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun observeNote(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<NoteState> = sharedObserveNote(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().metadata.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* [observeNote] without the relay half: watches LocalCache and asks no relay for the note.
|
||||
*
|
||||
* For a NIP-17 rumor, which has no fetchable id — putting one in a REQ would tell relays the
|
||||
* private event's identity, the leak [com.vitorpamplona.amethyst.commons.model.Note.isPrivateRumor]
|
||||
* guards everywhere else. Nothing is lost by not asking: a rumor only ever reaches the cache by
|
||||
* unwrapping the envelope that carried it, and the always-on gift-wrap tail re-fetches a week of
|
||||
* those on every cold start, so this flow fires on its own once the envelope lands.
|
||||
*/
|
||||
@Composable
|
||||
fun observeNoteLocally(note: Note): State<NoteState> {
|
||||
val flow = remember(note) { note.flow().metadata.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
inline fun <reified T : Event> observeNoteEvent(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<T?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<T?> = sharedObserveNoteEvent<T>(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.note.event as? T? }
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.event as? T?)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun <T> observeNoteAndMap(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
map: (Note) -> T,
|
||||
): State<T> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<T> = sharedObserveNoteAndMap<T>(note, accountViewModel.account, accountViewModel.dataSources().eventFinder, map)
|
||||
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { map(it.note) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(map(note))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T, U> observeNoteEventAndMapNotNull(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
map: (T) -> U,
|
||||
): State<U?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<U?> = sharedObserveNoteEventAndMapNotNull<T, U>(note, accountViewModel.account, accountViewModel.dataSources().eventFinder, map)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { (it.note.event as? T)?.let { map(it) } }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle((note.event as? T)?.let { map(it) })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T, U> observeNoteEventAndMap(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
map: (T?) -> U,
|
||||
): State<U> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<U> = sharedObserveNoteEventAndMap<T, U>(note, accountViewModel.account, accountViewModel.dataSources().eventFinder, map)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { map(it.note.event as? T) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(map(note.event as? T))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeNoteHasEvent(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.note.event != null }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.event != null)
|
||||
}
|
||||
): State<Boolean> = sharedObserveNoteHasEvent(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteReplies(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<NoteState?> = sharedObserveNoteReplies(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().replies.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteReplyCount(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<Int> = sharedObserveNoteReplyCount(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.replies.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.replies.size }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.replies.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of a chat message's **minichat** replies — its kind-1111 [CommentEvent]
|
||||
* children only (inline quote-replies are ordinary kind-9/42 messages and are not
|
||||
* counted here). Drives the "N replies" chip that opens the minichat.
|
||||
*
|
||||
* Mounting this registers the message with [EventFinderFilterAssemblerSubscription], which
|
||||
* batches the visible messages' ids into shared REQs for their replies (kind-1111 among
|
||||
* them) — so for public chats (NIP-28/NIP-29) the thread replies load, and the chip appears,
|
||||
* just by rendering the rows. Concord's kind-1111 replies instead arrive over the channel
|
||||
* plane, so that REQ finds nothing there and is a harmless no-op.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteMinichatReplyCount(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Int> {
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.replies.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.replies.count { reply -> isMinichatReply(reply.event) } }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.replies.count { isMinichatReply(it.event) })
|
||||
}
|
||||
): State<Int> = sharedObserveNoteMinichatReplyCount(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteReactions(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<NoteState?> = sharedObserveNoteReactions(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().reactions.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteReactionCount(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.reactions.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.countReactions() }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(note.countReactions())
|
||||
}
|
||||
): State<Int> = sharedObserveNoteReactionCount(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteZaps(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().zaps.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
): State<NoteState?> = sharedObserveNoteZaps(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteReposts(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<NoteState?> = sharedObserveNoteReposts(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().boosts.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeNoteRepostsBy(
|
||||
note: Note,
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<Boolean> = sharedObserveNoteRepostsBy(note, user, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.boosts.stateFlow
|
||||
.mapLatest { it.note.isBoostedBy(user) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.isBoostedBy(user))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteRepostCount(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.boosts.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { note.boosts.size }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.boosts.size)
|
||||
}
|
||||
): State<Int> = sharedObserveNoteRepostCount(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteReferences(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device.
|
||||
// On-chain zaps piggyback `note.flow().zaps.stateFlow` because Note.addOnchainZap
|
||||
// invalidates the same `flowSet.zaps`. If that ever moves to a dedicated flow,
|
||||
// add it here too — otherwise on-chain-only notes will stop pinging the chevron.
|
||||
val flow =
|
||||
remember(note) {
|
||||
combine(
|
||||
note.flow().zaps.stateFlow,
|
||||
note.flow().boosts.stateFlow,
|
||||
note.flow().reactions.stateFlow,
|
||||
) { zapState, _, _ ->
|
||||
zapState.note.hasZapsBoostsOrReactions()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.hasZapsBoostsOrReactions())
|
||||
}
|
||||
): State<Boolean> = sharedObserveNoteReferences(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeNoteOts(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
): State<NoteState?> = sharedObserveNoteOts(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().ots.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
// Resolves the actual modification list off the main thread and filters identical results,
|
||||
// so the caller's LaunchedEffect only fires when the list of edits truly changes.
|
||||
// `sample(500)` collapses bursts — a heavily-edited note can emit hundreds of times during
|
||||
// initial relay sync, and we only need the last state per ~half second.
|
||||
// Returns `null` until the first IO resolution completes — callers should treat that as
|
||||
// "still loading" and not flip their UI to "no edits".
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteModifications(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<List<Note>?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
return produceState<List<Note>?>(initialValue = null, note) {
|
||||
note
|
||||
.flow()
|
||||
.edits
|
||||
.stateFlow
|
||||
.sample(500)
|
||||
.mapLatest { note.textNoteModifications() }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
}
|
||||
): State<List<Note>?> = sharedObserveNoteModifications(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
@Composable
|
||||
fun observeCommunityApprovalNeedStatus(
|
||||
note: Note,
|
||||
community: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<Boolean?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note, community) {
|
||||
combine(
|
||||
community.flow().metadata.stateFlow,
|
||||
note.flow().boosts.stateFlow,
|
||||
) { communityMetadata, boosts ->
|
||||
(communityMetadata.note.event as? CommunityDefinitionEvent)?.let { communityDefEvent ->
|
||||
val moderators = communityDefEvent.moderatorKeys().toSet()
|
||||
|
||||
if (note.author?.pubkeyHex in moderators) {
|
||||
false
|
||||
} else {
|
||||
val isModerator = accountViewModel.account.userProfile().pubkeyHex in moderators
|
||||
|
||||
if (isModerator) {
|
||||
val wasAlreadyApproved =
|
||||
note.boosts.any {
|
||||
val approvalEvent = it.event
|
||||
(approvalEvent is CommunityPostApprovalEvent || approvalEvent is RepostEvent || approvalEvent is GenericRepostEvent) &&
|
||||
approvalEvent.pubKey in moderators &&
|
||||
approvalEvent.isForCommunity(community.idHex)
|
||||
}
|
||||
!wasAlreadyApproved
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(false)
|
||||
}
|
||||
): State<Boolean?> = sharedObserveCommunityApprovalNeedStatus(note, community, accountViewModel.account, accountViewModel.dataSources().eventFinder)
|
||||
|
||||
+2
-19
@@ -26,39 +26,22 @@ import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssemblerSubscription as SharedUserFinderSubscription
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
fun UserFinderFilterAssemblerSubscription(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = UserFinderFilterAssemblerSubscription(
|
||||
) = SharedUserFinderSubscription(
|
||||
user,
|
||||
accountViewModel.account,
|
||||
accountViewModel.dataSources().userFinder,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun UserFinderFilterAssemblerSubscription(
|
||||
user: User,
|
||||
forAccount: Account,
|
||||
dataSource: UserFinderFilterAssembler,
|
||||
) {
|
||||
// different screens get different states
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(user) {
|
||||
UserFinderQueryState(user, forAccount)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches all the users that are cited in the parent post.
|
||||
* So that we load their relay lists and find posts
|
||||
|
||||
+8
-88
@@ -47,10 +47,13 @@ import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserAboutMe as sharedObserveUserAboutMe
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserBanner as sharedObserveUserBanner
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo as sharedObserveUserInfo
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserPicture as sharedObserveUserPicture
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
@@ -86,113 +89,30 @@ fun observeUserNickname(
|
||||
return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedNickname(user) })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserAboutMe(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
): State<String> = sharedObserveUserAboutMe(user, accountViewModel.dataSources().userFinder, accountViewModel.account)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user.metadata().flow.map {
|
||||
it?.info?.about ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.about ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserInfo(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<UserInfo?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
): State<UserInfo?> = sharedObserveUserInfo(user, accountViewModel.dataSources().userFinder, accountViewModel.account)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user.metadata().flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserBanner(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
): State<String?> = sharedObserveUserBanner(user, accountViewModel.dataSources().userFinder, accountViewModel.account)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.metadata()
|
||||
.flow
|
||||
.map {
|
||||
it?.info?.banner
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.banner,
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserPicture(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
subscribe: Boolean = true,
|
||||
): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
// Callers that already hold a single shared subscription for the same user
|
||||
// (e.g. an author avatar that also observes the contact-card score) can pass
|
||||
// subscribe = false to avoid setting up a redundant relay subscription.
|
||||
if (subscribe) {
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.metadata()
|
||||
.flow
|
||||
.map {
|
||||
it?.info?.picture
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.picture,
|
||||
)
|
||||
}
|
||||
): State<String?> = sharedObserveUserPicture(user, accountViewModel.dataSources().userFinder, accountViewModel.account, subscribe)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
|
||||
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_join
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_subtitle
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedMediaUrl
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp
|
||||
@@ -154,11 +155,3 @@ fun MyAsyncImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isAnimatedMediaUrl(url: String): Boolean =
|
||||
url.endsWith(".gif", ignoreCase = true) ||
|
||||
url.contains(".gif?", ignoreCase = true) ||
|
||||
url.contains(".gif#", ignoreCase = true) ||
|
||||
url.endsWith(".avif", ignoreCase = true) ||
|
||||
url.contains(".avif?", ignoreCase = true) ||
|
||||
url.contains(".avif#", ignoreCase = true)
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_invite_onl
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_private
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_open
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
+2
-6
@@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedMediaUrl
|
||||
import com.vitorpamplona.amethyst.commons.service.image.placeholderModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
|
||||
@@ -749,12 +750,7 @@ fun BaseMediaContent.isAnimatedMedia(): Boolean =
|
||||
if (this is MediaUrlContent) {
|
||||
mimeType == "image/gif" ||
|
||||
mimeType == "image/avif" ||
|
||||
url.endsWith(".gif", ignoreCase = true) ||
|
||||
url.contains(".gif?", ignoreCase = true) ||
|
||||
url.contains(".gif#", ignoreCase = true) ||
|
||||
url.endsWith(".avif", ignoreCase = true) ||
|
||||
url.contains(".avif?", ignoreCase = true) ||
|
||||
url.contains(".avif#", ignoreCase = true)
|
||||
isAnimatedMediaUrl(url)
|
||||
} else if (this is MediaPreloadedContent) {
|
||||
mimeType == "image/gif" ||
|
||||
mimeType == "image/avif" ||
|
||||
|
||||
+1
-1
@@ -41,10 +41,10 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
+1
-1
@@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_logout_toast
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_logout_toast_zero
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_logout_warning
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -116,6 +116,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_hls_video
|
||||
import com.vitorpamplona.amethyst.commons.resources.show_npub_as_a_qr_code
|
||||
import com.vitorpamplona.amethyst.commons.resources.status_update
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -146,7 +147,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.PermanentDrawerWidth
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef
|
||||
|
||||
+1
-1
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.your_profile_image
|
||||
import com.vitorpamplona.amethyst.commons.search.SearchQuery
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
|
||||
@@ -45,7 +46,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.LocalScreenLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
|
||||
@@ -77,10 +77,12 @@ import com.vitorpamplona.amethyst.commons.resources.nutzap
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.FollowingIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RepostedIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ScoreTagSmall
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
@@ -105,7 +107,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
||||
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
|
||||
import com.vitorpamplona.amethyst.ui.components.ExpandableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.authorRouteFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
|
||||
@@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.resources.private_rumor_info_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.private_rumor_mark
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.GenericRepostLayout
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.NoteComposeLayout
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
@@ -117,7 +118,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteModifications
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.expand_relay_list
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_info
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.LargeRelayIconModifier
|
||||
@@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.relayIconModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColorOnSecondSurface
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@@ -21,25 +21,18 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -50,18 +43,15 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image_of_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.unknown_author
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.FollowingIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ScoreTag
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font6SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font8SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
@@ -525,84 +515,6 @@ fun ScoreTag55Preview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTag(
|
||||
score: Int,
|
||||
size: Dp,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
if (size > 34.dp) {
|
||||
ScoreTagRegular(score, modifier)
|
||||
} else if (size > 23.dp) {
|
||||
ScoreTagSmall(score, modifier)
|
||||
} else {
|
||||
ScoreTagSmallest(score, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagRegular(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font10SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font10SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 4.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagSmall(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font8SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font8SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 3.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagSmallest(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font6SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font6SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 2.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchProfilePicture(
|
||||
baseUser: User,
|
||||
@@ -623,22 +535,17 @@ fun InnerUserPicture(
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val myImageModifier =
|
||||
remember {
|
||||
modifier.size(size).clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = userHex,
|
||||
model = userPicture,
|
||||
UserAvatar(
|
||||
userHex = userHex,
|
||||
pictureUrl = userPicture,
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
contentDescription =
|
||||
if (userName != null) {
|
||||
stringRes(id = Res.string.profile_image_of_user, userName)
|
||||
} else {
|
||||
stringRes(id = Res.string.profile_image)
|
||||
},
|
||||
modifier = myImageModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
|
||||
@@ -74,12 +74,12 @@ import com.vitorpamplona.amethyst.commons.resources.badge_awardees_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.badge_untitled
|
||||
import com.vitorpamplona.amethyst.commons.resources.reject_badge
|
||||
import com.vitorpamplona.amethyst.commons.resources.unaccept_badge
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
@@ -93,7 +94,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
|
||||
@@ -44,11 +44,11 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_hosts_and_guests
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
|
||||
+1
-1
@@ -48,11 +48,11 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui.RelayNameChip
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants
|
||||
|
||||
+1
-1
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_search
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_app_recommendations_search_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.select_list_to_filter
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
|
||||
@@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
|
||||
import com.vitorpamplona.amethyst.ui.note.types.ByAuthorChip
|
||||
|
||||
+1
-1
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflic
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
@@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
|
||||
+1
-1
@@ -61,12 +61,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.backups
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff
|
||||
@@ -242,8 +242,8 @@ private fun arrow(
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPresentation(diff: EventDiff): DiffPresentation {
|
||||
val context = LocalContext.current
|
||||
return remember(diff, context) { presentationOf(diff) { context.getString(it) } }
|
||||
val resources = LocalResources.current
|
||||
return remember(diff, resources) { presentationOf(diff) { resources.getString(it) } }
|
||||
}
|
||||
|
||||
/** Turns each event's own diff into typed, labelled groups. [str] resolves the labels. */
|
||||
|
||||
+1
-1
@@ -57,12 +57,12 @@ import com.vitorpamplona.amethyst.commons.resources.badge_untitled
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_badges_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_badges_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_badges_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -64,12 +64,12 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.search.SearchSeed
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SearchTopBarAction
|
||||
|
||||
+1
-1
@@ -44,9 +44,9 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_add_photo
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
|
||||
+1
-1
@@ -24,8 +24,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
||||
+1
-1
@@ -67,13 +67,13 @@ import com.vitorpamplona.amethyst.commons.resources.concord_home_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_member_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_show_all_channels
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
|
||||
+1
-1
@@ -74,12 +74,12 @@ import com.vitorpamplona.amethyst.commons.resources.concord_create_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_edit_banner_hint
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.commons.feeds.ChangesFlowFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply
|
||||
|
||||
class ChannelFeedFilter(
|
||||
val channel: Channel,
|
||||
|
||||
+1
-1
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.chats.ui.LeaveChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier
|
||||
@@ -47,7 +48,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.resources.channel_image
|
||||
import com.vitorpamplona.amethyst.commons.resources.created_at
|
||||
import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor
|
||||
import com.vitorpamplona.amethyst.commons.resources.owner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
|
||||
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
|
||||
+1
-1
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.chats.ui.JoinChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier
|
||||
@@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_section_forums
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_action
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
@@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.looksLikeNonNip29Relay
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
|
||||
+1
-1
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_message_count_short_capped
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_relay_group_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.select_list_to_filter
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -83,7 +84,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.WarmNip11
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
|
||||
+1
-1
@@ -75,12 +75,12 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_pick_titl
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_search
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_top_level_option
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_section_structure
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription
|
||||
|
||||
+1
-1
@@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
|
||||
+1
-1
@@ -38,11 +38,11 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog
|
||||
|
||||
+1
-1
@@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_server_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_messages
|
||||
import com.vitorpamplona.amethyst.commons.resources.unmute_notifications
|
||||
import com.vitorpamplona.amethyst.commons.resources.unpin_conversation
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -113,7 +114,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderByParentFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
|
||||
+2
-2
@@ -70,12 +70,12 @@ import com.vitorpamplona.amethyst.commons.resources.badge_award_image_for
|
||||
import com.vitorpamplona.amethyst.commons.resources.badge_untitled
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_badges_header
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_badges_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
import com.vitorpamplona.amethyst.commons.resources.scan_qr
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
@@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.largeProfilePictureModifier
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndDisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteLocally
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.looking_for_event
|
||||
import com.vitorpamplona.amethyst.commons.resources.looking_for_event_title
|
||||
@@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteLocally
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
|
||||
+1
-1
@@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_session_undo_bloc
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_settings_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_undo
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_info
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -116,7 +117,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.relayAuthPurposeLabelRes
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider
|
||||
|
||||
+1
-1
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.resources.active_subs_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.active_subs_unattributed
|
||||
import com.vitorpamplona.amethyst.commons.resources.active_subs_untagged
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
|
||||
@@ -94,7 +95,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.patrykandpatrick.vico.compose.common.VicoTheme
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenDark
|
||||
@@ -189,6 +190,8 @@ fun AmethystTheme(
|
||||
ProvideAppIcons {
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaledDensity,
|
||||
// ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge.
|
||||
LocalProfilePictureCache provides true,
|
||||
LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)),
|
||||
content = content,
|
||||
)
|
||||
|
||||
+10
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.calendar
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
@@ -33,13 +34,16 @@ import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -137,14 +141,20 @@ class CalendarsViewModelFlowTest {
|
||||
) = runBlocking {
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
val store = ViewModelStore()
|
||||
var modelScope: Job? = null
|
||||
try {
|
||||
val feed = FeedContentState(CalendarAppointmentsFeedFilter(seeEverythingAccount()), scope, LocalCache)
|
||||
val model = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java]
|
||||
modelScope = model.viewModelScope.coroutineContext.job
|
||||
model.init(pubKey, feed)
|
||||
block(model)
|
||||
} finally {
|
||||
store.clear()
|
||||
scope.cancel()
|
||||
// Clearing only requests cancellation. The flowOn producers on Dispatchers.Default can
|
||||
// still be finishing and hand their completion back to the Main collectors; wait for
|
||||
// them here, while setMain is still in force, not after tearDown's resetMain.
|
||||
withTimeout(AWAIT_MS) { modelScope?.join() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-1
@@ -22,13 +22,19 @@ package com.vitorpamplona.amethyst.calendar
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -58,13 +64,24 @@ class CalendarsViewModelTest {
|
||||
store = ViewModelStore()
|
||||
}
|
||||
|
||||
private val scopes = mutableListOf<Job>()
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
store.clear()
|
||||
// Clearing only requests cancellation: a producer the model runs on Dispatchers.Default
|
||||
// (its flowOn) can still be finishing, and it hands its completion back to the Main
|
||||
// collector. Waiting for the scopes keeps that inside setMain, instead of throwing onto a
|
||||
// background thread after resetMain and failing whichever runTest starts next.
|
||||
runBlocking { withTimeout(SCOPE_CLOSE_MS) { scopes.joinAll() } }
|
||||
scopes.clear()
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun newModel(): CalendarsViewModel = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java]
|
||||
private fun newModel(): CalendarsViewModel =
|
||||
ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java].also {
|
||||
scopes += it.viewModelScope.coroutineContext.job
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFreshScreenOpensOnToday() {
|
||||
@@ -148,4 +165,8 @@ class CalendarsViewModelTest {
|
||||
|
||||
assertEquals("the lenses must not share one scroll offset", 4, states.distinct().size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCOPE_CLOSE_MS = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats
|
||||
package com.vitorpamplona.amethyst.commons.model.chats
|
||||
|
||||
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
|
||||
import com.vitorpamplona.quartz.buzz.threading.buzzThreadReply
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.commons.richtext
|
||||
|
||||
/** True for GIF urls (path ends in `.gif`, optionally followed by a query or fragment). */
|
||||
fun isAnimatedGifUrl(url: String): Boolean =
|
||||
url.endsWith(".gif", ignoreCase = true) ||
|
||||
url.contains(".gif?", ignoreCase = true) ||
|
||||
url.contains(".gif#", ignoreCase = true)
|
||||
|
||||
/** True for AVIF urls (path ends in `.avif`, optionally followed by a query or fragment). */
|
||||
fun isAvifUrl(url: String): Boolean =
|
||||
url.endsWith(".avif", ignoreCase = true) ||
|
||||
url.contains(".avif?", ignoreCase = true) ||
|
||||
url.contains(".avif#", ignoreCase = true)
|
||||
|
||||
/** True for GIF/AVIF urls, which may animate and so bypass still-image pipelines like the avatar thumbnail cache. */
|
||||
fun isAnimatedMediaUrl(url: String): Boolean = isAnimatedGifUrl(url) || isAvifUrl(url)
|
||||
+4
-4
@@ -18,12 +18,12 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats
|
||||
package com.vitorpamplona.amethyst.commons.model.chats
|
||||
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A kind-9 chat message is a thread reply only when its `e` tag carries a NIP-10 marker.
|
||||
@@ -46,11 +46,11 @@ is specific to the UI module.
|
||||
| Source set | For |
|
||||
|---------------|-----|
|
||||
| `commonMain` | Composables, icons, theme, Coil fetchers, `composeResources`. Gated by `verifyKmpPurity` like `commons`. |
|
||||
| `jvmAndroid` | Markdown renderer (`ui/markdown`), Coil-OkHttp + Blossom read-auth fetcher, the `viewModel()` helper. |
|
||||
| `jvmMain` | Desktop Coil bridge (`CoilImageBridge.jvm.kt`), `compose.desktop.currentOs`. `dependsOn(jvmAndroid)` + `skikoMain`. |
|
||||
| `androidMain` | Android Coil bridge. `dependsOn(jvmAndroid)`. |
|
||||
| `jvmAndroid` | Markdown renderer (`ui/markdown`), Coil-OkHttp + Blossom read-auth fetcher, the `viewModel()` helper, and the avatar/image engine (`ui/components/RobohashAsyncImage.kt`: `RobohashFallbackAsyncImage`, `GifProfilePicture`) behind the `UserAvatar` `AvatarImage` actual. For a user avatar call the commonMain `UserAvatar`; don't add another avatar composable. |
|
||||
| `jvmMain` | Desktop Coil bridge (`CoilImageBridge.jvm.kt`), a no-op `AnimatedImageAutoPlay` (Coil decodes only the first frame), `compose.desktop.currentOs`. `dependsOn(jvmAndroid)` + `skikoMain`. |
|
||||
| `androidMain` | Android Coil bridge, the `AnimatedImageAutoPlay` actual that starts/stops GIF/AVIF drawables. `dependsOn(jvmAndroid)`. |
|
||||
| `skikoMain` | `org.jetbrains.skia` pixel helpers shared by desktop JVM + iOS (`SkiaBitmapConverter`). |
|
||||
| `iosMain` | iOS Coil bridge. Compile-only spike today. |
|
||||
| `iosMain` | iOS Coil bridge, a plain `AsyncImage` `AvatarImage` actual. Compile-only spike today. |
|
||||
|
||||
## Tooling that points here
|
||||
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.commons.ui.components
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.Image
|
||||
import coil3.asDrawable
|
||||
|
||||
@Composable
|
||||
internal actual fun AnimatedImageAutoPlay(
|
||||
image: Image?,
|
||||
autoPlay: Boolean,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
// asDrawable wraps a bitmap in a new BitmapDrawable on every call; remembering it keeps the
|
||||
// effect below from restarting on each recomposition.
|
||||
val drawable = remember(image, resources) { image?.asDrawable(resources) }
|
||||
|
||||
LaunchedEffect(drawable, autoPlay) {
|
||||
if (drawable is Animatable) {
|
||||
if (autoPlay) drawable.start() else drawable.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* 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.commons.relayClient.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply
|
||||
import com.vitorpamplona.amethyst.commons.model.textNoteModifications
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccount
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderAccount
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.isForCommunity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.sample
|
||||
|
||||
/*
|
||||
* Per-note observers: each subscribes [note] with the relay event finder (batched with every other
|
||||
* on-screen note) and exposes a LocalCache flow of the note as Compose state.
|
||||
*
|
||||
* [account] and [dataSource] default to the front end's [LocalUserFinderAccount] and
|
||||
* [LocalEventFinder], so a shared composable needs only the note. Callers outside a composition
|
||||
* that provides them (a separate Activity root) pass both explicitly.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun observeNote(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().metadata.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* [observeNote] without the relay half: watches LocalCache and asks no relay for the note.
|
||||
*
|
||||
* For a NIP-17 rumor, which has no fetchable id — putting one in a REQ would tell relays the
|
||||
* private event's identity, the leak [com.vitorpamplona.amethyst.commons.model.Note.isPrivateRumor]
|
||||
* guards everywhere else. Nothing is lost by not asking: a rumor only ever reaches the cache by
|
||||
* unwrapping the envelope that carried it, and the always-on gift-wrap tail re-fetches a week of
|
||||
* those on every cold start, so this flow fires on its own once the envelope lands.
|
||||
*/
|
||||
@Composable
|
||||
fun observeNoteLocally(note: Note): State<NoteState> {
|
||||
val flow = remember(note) { note.flow().metadata.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
inline fun <reified T : Event> observeNoteEvent(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<T?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.note.event as? T? }
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.event as? T?)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun <T> observeNoteAndMap(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
map: (Note) -> T,
|
||||
): State<T> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { map(it.note) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(map(note))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T, U> observeNoteEventAndMapNotNull(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
map: (T) -> U,
|
||||
): State<U?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { (it.note.event as? T)?.let { map(it) } }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle((note.event as? T)?.let { map(it) })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T, U> observeNoteEventAndMap(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
map: (T?) -> U,
|
||||
): State<U> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { map(it.note.event as? T) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(map(note.event as? T))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeNoteHasEvent(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.note.event != null }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.event != null)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReplies(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().replies.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteReplyCount(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.replies.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.replies.size }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.replies.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of a chat message's **minichat** replies — its kind-1111 [CommentEvent]
|
||||
* children only (inline quote-replies are ordinary kind-9/42 messages and are not
|
||||
* counted here). Drives the "N replies" chip that opens the minichat.
|
||||
*
|
||||
* Mounting this registers the message with [EventFinderFilterAssemblerSubscription], which
|
||||
* batches the visible messages' ids into shared REQs for their replies (kind-1111 among
|
||||
* them) — so for public chats (NIP-28/NIP-29) the thread replies load, and the chip appears,
|
||||
* just by rendering the rows. Concord's kind-1111 replies instead arrive over the channel
|
||||
* plane, so that REQ finds nothing there and is a harmless no-op.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteMinichatReplyCount(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Int> {
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.replies.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.replies.count { reply -> isMinichatReply(reply.event) } }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.replies.count { isMinichatReply(it.event) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReactions(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().reactions.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteReactionCount(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.reactions.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { it.note.countReactions() }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(note.countReactions())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteZaps(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().zaps.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReposts(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().boosts.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeNoteRepostsBy(
|
||||
note: Note,
|
||||
user: User,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.boosts.stateFlow
|
||||
.mapLatest { it.note.isBoostedBy(user) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.isBoostedBy(user))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteRepostCount(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Int> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note) {
|
||||
note
|
||||
.flow()
|
||||
.boosts.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { note.boosts.size }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.boosts.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteReferences(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Boolean> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device.
|
||||
// On-chain zaps piggyback `note.flow().zaps.stateFlow` because Note.addOnchainZap
|
||||
// invalidates the same `flowSet.zaps`. If that ever moves to a dedicated flow,
|
||||
// add it here too — otherwise on-chain-only notes will stop pinging the chevron.
|
||||
val flow =
|
||||
remember(note) {
|
||||
combine(
|
||||
note.flow().zaps.stateFlow,
|
||||
note.flow().boosts.stateFlow,
|
||||
note.flow().reactions.stateFlow,
|
||||
) { zapState, _, _ ->
|
||||
zapState.note.hasZapsBoostsOrReactions()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(note.hasZapsBoostsOrReactions())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeNoteOts(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<NoteState?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().ots.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
// Resolves the actual modification list off the main thread and filters identical results,
|
||||
// so the caller's LaunchedEffect only fires when the list of edits truly changes.
|
||||
// `sample(500)` collapses bursts — a heavily-edited note can emit hundreds of times during
|
||||
// initial relay sync, and we only need the last state per ~half second.
|
||||
// Returns `null` until the first IO resolution completes — callers should treat that as
|
||||
// "still loading" and not flip their UI to "no edits".
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun observeNoteModifications(
|
||||
note: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<List<Note>?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
return produceState<List<Note>?>(initialValue = null, note) {
|
||||
note
|
||||
.flow()
|
||||
.edits
|
||||
.stateFlow
|
||||
.sample(500)
|
||||
.mapLatest { note.textNoteModifications() }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeCommunityApprovalNeedStatus(
|
||||
note: Note,
|
||||
community: Note,
|
||||
account: UserFinderAccount = LocalUserFinderAccount.current,
|
||||
dataSource: EventFinderFilterAssembler = LocalEventFinder.current,
|
||||
): State<Boolean?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, account, dataSource)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(note, community) {
|
||||
combine(
|
||||
community.flow().metadata.stateFlow,
|
||||
note.flow().boosts.stateFlow,
|
||||
) { communityMetadata, boosts ->
|
||||
(communityMetadata.note.event as? CommunityDefinitionEvent)?.let { communityDefEvent ->
|
||||
val moderators = communityDefEvent.moderatorKeys().toSet()
|
||||
|
||||
if (note.author?.pubkeyHex in moderators) {
|
||||
false
|
||||
} else {
|
||||
val isModerator = account.userFinderPubkeyHex in moderators
|
||||
|
||||
if (isModerator) {
|
||||
val wasAlreadyApproved =
|
||||
note.boosts.any {
|
||||
val approvalEvent = it.event
|
||||
(approvalEvent is CommunityPostApprovalEvent || approvalEvent is RepostEvent || approvalEvent is GenericRepostEvent) &&
|
||||
approvalEvent.pubKey in moderators &&
|
||||
approvalEvent.isForCommunity(community.idHex)
|
||||
}
|
||||
!wasAlreadyApproved
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(false)
|
||||
}
|
||||
+6
-1
@@ -62,8 +62,13 @@ fun observeUserPicture(
|
||||
user: User,
|
||||
userFinder: UserFinderFilterAssembler,
|
||||
account: UserFinderAccount,
|
||||
// Callers that already hold a subscription for the same user (e.g. an author avatar that
|
||||
// also observes the contact-card score) pass false to skip a redundant relay subscription.
|
||||
subscribe: Boolean = true,
|
||||
): State<String?> {
|
||||
UserFinderFilterAssemblerSubscription(user, account, userFinder)
|
||||
if (subscribe) {
|
||||
UserFinderFilterAssemblerSubscription(user, account, userFinder)
|
||||
}
|
||||
|
||||
val flow =
|
||||
remember(user) {
|
||||
|
||||
+36
-66
@@ -20,28 +20,16 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.DefaultAlpha
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
|
||||
|
||||
/**
|
||||
* Wrapper class for profile picture URLs that signals Coil to use the thumbnail
|
||||
@@ -54,7 +42,16 @@ data class ProfilePictureUrl(
|
||||
)
|
||||
|
||||
/**
|
||||
* Shared avatar component that displays a user's profile picture with Robohash fallback.
|
||||
* True when the host's Coil pipeline registers a `Fetcher.Factory<ProfilePictureUrl>` (the
|
||||
* avatar thumbnail cache) and the local Blossom cache bridge that strips the profile-picture
|
||||
* request marker. Avatars then load http(s) pictures through that cache. Hosts without them
|
||||
* (desktop, iOS) keep the default `false`, so avatars load the plain URL.
|
||||
*/
|
||||
val LocalProfilePictureCache = staticCompositionLocalOf { false }
|
||||
|
||||
/**
|
||||
* The shared user avatar: the profile picture clipped to a circle, with a Robohash (or a generic
|
||||
* face icon) while it loads, when it fails, or when pictures are turned off.
|
||||
*
|
||||
* @param userHex The user's public key hex (used for Robohash generation)
|
||||
* @param pictureUrl Optional URL to the user's profile picture
|
||||
@@ -63,7 +60,8 @@ data class ProfilePictureUrl(
|
||||
* @param contentDescription Accessibility description
|
||||
* @param loadProfilePicture Whether to load the profile picture (false = show robohash only)
|
||||
* @param loadRobohash Whether to generate robohash (false = show generic icon)
|
||||
* @param useThumbnailCache Whether to use the thumbnail disk cache for faster repeated loads
|
||||
* @param autoPlayGif Whether animated (GIF/AVIF) pictures play. Only Android can animate them;
|
||||
* desktop shows their first frame either way.
|
||||
* @param badge Optional overlay drawn on top of the avatar (bottom-right by
|
||||
* convention). Used by Desktop for the WoT trust-score chip; Android call
|
||||
* sites leave it null. When null the avatar renders as before (no extra
|
||||
@@ -78,7 +76,7 @@ fun UserAvatar(
|
||||
contentDescription: String? = null,
|
||||
loadProfilePicture: Boolean = true,
|
||||
loadRobohash: Boolean = true,
|
||||
useThumbnailCache: Boolean = false,
|
||||
autoPlayGif: Boolean = true,
|
||||
badge: @Composable (BoxScope.() -> Unit)? = null,
|
||||
) {
|
||||
if (badge != null) {
|
||||
@@ -91,7 +89,7 @@ fun UserAvatar(
|
||||
contentDescription = contentDescription,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
useThumbnailCache = useThumbnailCache,
|
||||
autoPlayGif = autoPlayGif,
|
||||
badge = null,
|
||||
)
|
||||
badge()
|
||||
@@ -106,53 +104,25 @@ fun UserAvatar(
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
val imageModel: Any? =
|
||||
if (pictureUrl != null && useThumbnailCache) {
|
||||
ProfilePictureUrl(pictureUrl)
|
||||
} else {
|
||||
pictureUrl
|
||||
}
|
||||
|
||||
if (imageModel != null && loadProfilePicture) {
|
||||
// Show profile picture with robohash/icon as fallback
|
||||
val fallbackPainter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(
|
||||
image = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight),
|
||||
)
|
||||
} else {
|
||||
rememberMaterialSymbolPainter(MaterialSymbols.Face)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = imageModel,
|
||||
contentDescription = contentDescription,
|
||||
modifier = avatarModifier,
|
||||
placeholder = fallbackPainter,
|
||||
fallback = fallbackPainter,
|
||||
error = fallbackPainter,
|
||||
alignment = Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
alpha = DefaultAlpha,
|
||||
colorFilter = null,
|
||||
filterQuality = DrawScope.DefaultFilterQuality,
|
||||
)
|
||||
} else if (loadRobohash) {
|
||||
// Show robohash only
|
||||
Image(
|
||||
imageVector = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight),
|
||||
contentDescription = contentDescription,
|
||||
modifier = avatarModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
// Show generic icon
|
||||
Image(
|
||||
painter = rememberMaterialSymbolPainter(MaterialSymbols.Face),
|
||||
contentDescription = contentDescription,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
|
||||
modifier = avatarModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
AvatarImage(
|
||||
userHex = userHex,
|
||||
pictureUrl = pictureUrl,
|
||||
contentDescription = contentDescription,
|
||||
modifier = avatarModifier,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
autoPlayGif = autoPlayGif,
|
||||
)
|
||||
}
|
||||
|
||||
/** Draws the avatar picture into an already sized and clipped [modifier]. */
|
||||
@Composable
|
||||
internal expect fun AvatarImage(
|
||||
userHex: String,
|
||||
pictureUrl: String?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean,
|
||||
)
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.commons.ui.note
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font6SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font8SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
|
||||
|
||||
/**
|
||||
* Trust-score chip drawn over the bottom of an avatar, sized to the avatar: regular above 34dp,
|
||||
* small above 23dp, smallest below.
|
||||
*/
|
||||
@Composable
|
||||
fun ScoreTag(
|
||||
score: Int,
|
||||
size: Dp,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
if (size > 34.dp) {
|
||||
ScoreTagRegular(score, modifier)
|
||||
} else if (size > 23.dp) {
|
||||
ScoreTagSmall(score, modifier)
|
||||
} else {
|
||||
ScoreTagSmallest(score, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagRegular(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font10SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font10SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 4.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagSmall(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font8SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font8SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 3.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreTagSmallest(
|
||||
score: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font6SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
lineHeight = Font6SP,
|
||||
modifier =
|
||||
modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 2.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.commons.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.onBackgroundColorFilter
|
||||
|
||||
// iOS has no animated-avatar pipeline yet, so autoPlayGif is unused.
|
||||
@Composable
|
||||
internal actual fun AvatarImage(
|
||||
userHex: String,
|
||||
pictureUrl: String?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean,
|
||||
) {
|
||||
if (pictureUrl != null && loadProfilePicture) {
|
||||
val fallbackPainter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(image = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight))
|
||||
} else {
|
||||
rememberMaterialSymbolPainter(MaterialSymbols.Face)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = pictureUrl,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = fallbackPainter,
|
||||
fallback = fallbackPainter,
|
||||
error = fallbackPainter,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else if (loadRobohash) {
|
||||
Image(
|
||||
imageVector = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = rememberMaterialSymbolPainter(MaterialSymbols.Face),
|
||||
contentDescription = contentDescription,
|
||||
colorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.commons.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
||||
@Composable
|
||||
internal actual fun AvatarImage(
|
||||
userHex: String,
|
||||
pictureUrl: String?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean,
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = userHex,
|
||||
model = pictureUrl,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
autoPlayGif = autoPlayGif,
|
||||
)
|
||||
}
|
||||
+41
-40
@@ -18,15 +18,13 @@
|
||||
* 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.components
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -38,9 +36,8 @@ import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.asDrawable
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import coil3.network.NetworkHeaders
|
||||
@@ -48,12 +45,23 @@ import coil3.network.httpHeaders
|
||||
import coil3.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedMediaUrl
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomCacheRedirectInterceptor
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ProfilePictureUrl
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.forwardingPainter
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.onBackgroundColorFilter
|
||||
import coil3.Image as CoilImage
|
||||
|
||||
/**
|
||||
* Plays or pauses an animated avatar (GIF/AVIF) once Coil has decoded it. Android drives the
|
||||
* decoded `Animatable` drawable; desktop decodes only the first frame, so there [autoPlay] has
|
||||
* no effect.
|
||||
*/
|
||||
@Composable
|
||||
internal expect fun AnimatedImageAutoPlay(
|
||||
image: CoilImage?,
|
||||
autoPlay: Boolean,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RobohashAsyncImage(
|
||||
@@ -93,6 +101,7 @@ fun RobohashFallbackAsyncImage(
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean = true,
|
||||
) {
|
||||
val useThumbnailCache = LocalProfilePictureCache.current
|
||||
if (model != null && loadProfilePicture && isAnimatedMediaUrl(model)) {
|
||||
GifProfilePicture(
|
||||
userHex = robot,
|
||||
@@ -115,14 +124,14 @@ fun RobohashFallbackAsyncImage(
|
||||
)
|
||||
}
|
||||
|
||||
val resources = LocalContext.current.resources
|
||||
SubcomposeAsyncImage(
|
||||
// The thumbnail-cache fetcher behind ProfilePictureUrl delegates to Coil's http-only
|
||||
// ProfilePictureUrl only loads where the host registered its thumbnail-cache fetcher
|
||||
// (see LocalProfilePictureCache). That fetcher delegates to Coil's http-only
|
||||
// NetworkFetcher, so a LOCAL model (e.g. a decrypted Concord community icon cached at
|
||||
// file://) would fail there. Route only remote http(s) pictures through the thumbnail
|
||||
// cache; hand local/content URIs to Coil's native fetchers, which load them directly.
|
||||
model =
|
||||
if (model.startsWith("http://", ignoreCase = true) || model.startsWith("https://", ignoreCase = true)) {
|
||||
if (useThumbnailCache && (model.startsWith("http://", ignoreCase = true) || model.startsWith("https://", ignoreCase = true))) {
|
||||
ProfilePictureUrl(model)
|
||||
} else {
|
||||
model
|
||||
@@ -136,14 +145,7 @@ fun RobohashFallbackAsyncImage(
|
||||
filterQuality = filterQuality,
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
val successState = state as? AsyncImagePainter.State.Success
|
||||
val drawable = successState?.result?.image?.asDrawable(resources)
|
||||
|
||||
LaunchedEffect(drawable, autoPlayGif) {
|
||||
if (drawable is Animatable) {
|
||||
if (autoPlayGif) drawable.start() else drawable.stop()
|
||||
}
|
||||
}
|
||||
AnimatedImageAutoPlay((state as? AsyncImagePainter.State.Success)?.result?.image, autoPlayGif)
|
||||
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
|
||||
@@ -194,7 +196,6 @@ fun GifProfilePicture(
|
||||
loadRobohash: Boolean,
|
||||
autoPlay: Boolean,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val fallbackPainter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(image = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight))
|
||||
@@ -205,20 +206,27 @@ fun GifProfilePicture(
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val context = LocalPlatformContext.current
|
||||
val markForLocalBlossom = LocalProfilePictureCache.current
|
||||
// Animated avatars skip ProfilePictureFetcher (its thumbnail cache would flatten them), so
|
||||
// they carry the profile-picture marker themselves for the local Blossom cache bridge.
|
||||
val model =
|
||||
remember(userPicture) {
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(userPicture)
|
||||
.httpHeaders(
|
||||
NetworkHeaders
|
||||
.Builder()
|
||||
.set(LocalBlossomCacheRedirectInterceptor.MEDIA_HEADER, LocalBlossomCacheRedirectInterceptor.PROFILE_PICTURE)
|
||||
.build(),
|
||||
).build()
|
||||
// they carry the profile-picture marker themselves for the local Blossom cache bridge. Only
|
||||
// hosts that install that bridge (LocalProfilePictureCache) strip the marker before the
|
||||
// request leaves; anywhere else it would reach the image server, so it is not added.
|
||||
val model: Any =
|
||||
remember(userPicture, markForLocalBlossom) {
|
||||
if (markForLocalBlossom) {
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(userPicture)
|
||||
.httpHeaders(
|
||||
NetworkHeaders
|
||||
.Builder()
|
||||
.set(LocalBlossomCacheRedirectInterceptor.MEDIA_HEADER, LocalBlossomCacheRedirectInterceptor.PROFILE_PICTURE)
|
||||
.build(),
|
||||
).build()
|
||||
} else {
|
||||
userPicture
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
@@ -229,14 +237,7 @@ fun GifProfilePicture(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
val successState = state as? AsyncImagePainter.State.Success
|
||||
val drawable = successState?.result?.image?.asDrawable(resources)
|
||||
|
||||
LaunchedEffect(drawable, autoPlay) {
|
||||
if (drawable is Animatable) {
|
||||
if (autoPlay) drawable.start() else drawable.stop()
|
||||
}
|
||||
}
|
||||
AnimatedImageAutoPlay((state as? AsyncImagePainter.State.Success)?.result?.image, autoPlay)
|
||||
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
+33
@@ -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.commons.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import coil3.Image
|
||||
|
||||
@Composable
|
||||
internal actual fun AnimatedImageAutoPlay(
|
||||
image: Image?,
|
||||
autoPlay: Boolean,
|
||||
) {
|
||||
// Coil on desktop decodes only the first frame of a GIF/AVIF, so there is no animation to
|
||||
// start or stop and autoPlay has no effect here.
|
||||
}
|
||||
+1
-1
@@ -49,10 +49,10 @@ import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedGifUrl
|
||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.AnimatedGifImage
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.isAnimatedGifUrl
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
|
||||
-7
@@ -51,13 +51,6 @@ private const val MIN_FRAME_DURATION_MS = 20
|
||||
|
||||
private val gifHttpClient get() = DesktopHttpClient.currentClient()
|
||||
|
||||
fun isAnimatedGifUrl(url: String): Boolean {
|
||||
val lower = url.lowercase()
|
||||
return lower.endsWith(".gif") ||
|
||||
lower.contains(".gif?") ||
|
||||
lower.contains(".gif#")
|
||||
}
|
||||
|
||||
private class GifFrames(
|
||||
val frames: List<ImageBitmap>,
|
||||
val durations: List<Int>,
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedGifUrl
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedGifUrl
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssem
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.isAnimatedGifUrl
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ReplyToLabel
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
|
||||
@@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.desktop.ui.media.AnimatedGifImage
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.AudioPlayer
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopVideoPlayer
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.isAnimatedGifUrl
|
||||
import com.vitorpamplona.amethyst.desktop.ui.rememberDisplayData
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
|
||||
-2
@@ -54,7 +54,6 @@ fun WoTBadgedAvatar(
|
||||
contentDescription: String? = null,
|
||||
loadProfilePicture: Boolean = true,
|
||||
loadRobohash: Boolean = true,
|
||||
useThumbnailCache: Boolean = false,
|
||||
) {
|
||||
val service = LocalWoTService.current
|
||||
val ready = LocalWoTReady.current
|
||||
@@ -75,7 +74,6 @@ fun WoTBadgedAvatar(
|
||||
contentDescription = contentDescription,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
useThumbnailCache = useThumbnailCache,
|
||||
badge =
|
||||
if (score > 0) {
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user