Merge branch 'main' into claude/kind-keller-itzy63

Main's 97136c22 moved DisappearingScaffold, SafeImeInsets, the emoji text
and the user-name text into commonsUI, and turned CrossfadeIfEnabled's
`accountViewModel` parameter into a LocalAnimationsEnabled composition
local. Cordn's coordinators screen was the last caller still naming the
old package and passing the removed argument; every other cordn screen
already sits on the app-level DisappearingScaffold wrapper, which main
kept.

Both conflicts were in the calendar ViewModel tests, where this branch
had mitigated a cross-test Main-dispatcher leak by yielding on
Dispatchers.Default before resetMain. Main's bc44d870 fixes the same leak
deterministically by joining the model scopes under a timeout, so those
two files take main's version whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
Claude
2026-09-24 16:05:26 +00:00
354 changed files with 2653 additions and 2028 deletions
+2
View File
@@ -28,6 +28,8 @@ TASKS=(
:quartz:jvmTest
:commons:jvmTest
:commonsUI:jvmTest
# Android-only commonsUI code (e.g. SafeImeInsets) is tested here, not in jvmTest.
:commonsUI:testAndroidHostTest
:nestsClient:jvmTest
:quic:jvmTest
:marmotQuic:jvmTest
+1
View File
@@ -379,6 +379,7 @@ jobs:
:quartz:jvmTest \
:commons:jvmTest \
:commonsUI:jvmTest \
:commonsUI:testAndroidHostTest \
:nestsClient:jvmTest \
:amethyst:testFdroidDebugUnitTest \
:amethyst:testPlayDebugUnitTest \
@@ -61,11 +61,11 @@ import org.junit.Test
* [aCancelledImeAnimationMustNotWedgeTheAnimatedInset] FAILS on every version from 1.4.0 on, so it
* is [Ignore]d to keep CI green. It is not a test of Amethyst code — it is the upstream repro we
* attached to the bug. **Re-run it by hand after every Compose upgrade**: when it passes, the
* upstream fix has landed and [com.vitorpamplona.amethyst.ui.insets.SafeImeInsets] can be retired.
* upstream fix has landed and [com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets] can be retired.
*
* [theAnimationTargetSurvivesTheWedge] documents the asymmetry that makes a workaround possible
* and is expected to PASS — `updateImeAnimationTarget` is called outside the guard. It stays
* enabled, because it guards the premise [com.vitorpamplona.amethyst.ui.insets.SafeImeInsets]
* enabled, because it guards the premise [com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets]
* depends on: if a future Compose release stopped keeping `imeAnimationTarget` current, our
* fallback would silently start reading a dead value too.
*/
@@ -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
@@ -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
@@ -2076,7 +2076,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
/**
@@ -45,6 +45,7 @@ import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay
import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState
import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeLevelIndicator
@@ -62,7 +63,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMe
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack
import com.vitorpamplona.amethyst.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
internal const val SKIP_SECONDS = 10
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.model.Account
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv
import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton
@@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
@@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.copy_stack_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg2
import com.vitorpamplona.amethyst.ui.components.util.setText
import kotlinx.coroutines.launch
import java.io.PrintWriter
import java.io.StringWriter
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem
@@ -82,7 +83,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
import kotlinx.collections.immutable.ImmutableList
@@ -75,12 +75,12 @@ import com.vitorpamplona.amethyst.commons.resources.twitter
import com.vitorpamplona.amethyst.commons.resources.twitter_proof_url_template
import com.vitorpamplona.amethyst.commons.resources.username
import com.vitorpamplona.amethyst.commons.resources.website_url
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_section_explai
import com.vitorpamplona.amethyst.commons.resources.delete_bolt12_offer
import com.vitorpamplona.amethyst.commons.resources.invalid_bolt12_offer
import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
@@ -57,12 +57,12 @@ import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_profile_
import com.vitorpamplona.amethyst.commons.resources.media_servers
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache_caption
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
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.commons.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -128,6 +128,7 @@ import com.vitorpamplona.amethyst.commons.resources.manage_stored_files_empty
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
import com.vitorpamplona.amethyst.commons.resources.retry
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
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
@@ -135,7 +136,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip56Reports.ReportType
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_relay_label
import com.vitorpamplona.amethyst.commons.resources.nests_servers_remove
import com.vitorpamplona.amethyst.commons.resources.nests_servers_title
import com.vitorpamplona.amethyst.commons.resources.nests_servers_use_defaults
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -82,7 +83,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.payment_target_type
import com.vitorpamplona.amethyst.commons.resources.payment_targets
import com.vitorpamplona.amethyst.commons.resources.payment_targets_explainer
import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_mint_label
import com.vitorpamplona.amethyst.commons.resources.cashu_no_wallet_found
import com.vitorpamplona.amethyst.commons.resources.cashu_redeem
import com.vitorpamplona.amethyst.commons.resources.sats
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.commons.ui.note.OpenInNewIcon
@@ -68,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip60Cashu.token.CashuToken
import kotlinx.collections.immutable.ImmutableList
@@ -93,7 +93,7 @@ fun CashuPreview(
}
}
CrossfadeIfEnabled(targetState = cashuData, label = "CashuPreview", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = cashuData, label = "CashuPreview") {
when (it) {
is GenericLoadable.Loaded<ImmutableList<CashuToken>> -> {
CashuPreview(it.loaded, accountViewModel)
@@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink
import kotlinx.coroutines.launch
@@ -31,8 +31,8 @@ import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.components.util.setText
import kotlinx.coroutines.launch
/**
@@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink
import kotlinx.coroutines.launch
@@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.components.util.setText
import kotlinx.coroutines.launch
@Composable
@@ -20,12 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -34,34 +28,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.concord_invite_naddr_label
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.ClickableInLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.CustomEmojiChecker
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.njumpLink
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
@@ -84,9 +73,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.toNIP19
import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@Composable
fun ClickableRoute(
@@ -406,208 +392,6 @@ fun CreateClickableText(
)
}
@Composable
fun CustomEmojiChecker(
text: String,
tags: ImmutableListOfLists<String>?,
onRegularText: @Composable (String) -> Unit,
onEmojiText: @Composable (ImmutableList<CustomEmoji.Renderable>) -> Unit,
) {
val mayContainEmoji by remember(text, tags) {
mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags?.lists))
}
if (mayContainEmoji) {
var emojiList by
remember(text, tags) {
mutableStateOf<ImmutableList<CustomEmoji.Renderable>?>(null)
}
LaunchedEffect(text, tags) {
val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags?.lists)
if (newEmojiList != null) {
emojiList = newEmojiList
}
}
emojiList?.let {
onEmojiText(it)
} ?: run {
onRegularText(text)
}
} else {
onRegularText(text)
}
}
@Composable
fun CustomEmojiChecker(
text: String,
emojis: ImmutableMap<String, String>,
onRegularText: @Composable (String) -> Unit,
onEmojiText: @Composable (ImmutableList<CustomEmoji.Renderable>) -> Unit,
) {
val mayContainEmoji by remember(text, emojis) {
mutableStateOf(CustomEmoji.fastMightContainEmoji(text, emojis))
}
if (mayContainEmoji) {
var emojiList by
remember(text, emojis) {
mutableStateOf<ImmutableList<CustomEmoji.Renderable>?>(null)
}
LaunchedEffect(text, emojis) {
val newEmojiList = CustomEmoji.assembleAnnotatedList(text, emojis)
if (newEmojiList != null) {
emojiList = newEmojiList
}
}
emojiList?.let {
onEmojiText(it)
} ?: run {
onRegularText(text)
}
} else {
onRegularText(text)
}
}
@Composable
fun CreateTextWithEmoji(
text: String,
tags: ImmutableListOfLists<String>?,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontWeight: FontWeight? = null,
fontSize: TextUnit = TextUnit.Unspecified,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier,
) {
CustomEmojiChecker(
text,
tags,
onEmojiText = {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
val style =
LocalTextStyle.current
.merge(
TextStyle(
color = textColor,
textAlign = TextAlign.Unspecified,
fontWeight = fontWeight,
fontSize = fontSize,
),
).toSpanStyle()
InLineIconRenderer(it, style, fontSize, maxLines, overflow, modifier)
},
onRegularText = {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
Text(
text = it,
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize,
maxLines = maxLines,
overflow = overflow,
modifier = modifier,
)
},
)
}
@Composable
fun CreateTextWithEmoji(
text: String,
emojis: ImmutableMap<String, String>,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontWeight: FontWeight? = null,
fontSize: TextUnit = TextUnit.Unspecified,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier,
) {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
CustomEmojiChecker(
text,
emojis,
onEmojiText = {
val currentStyle = LocalTextStyle.current
val style =
remember(currentStyle) {
currentStyle
.merge(
TextStyle(
color = textColor,
textAlign = TextAlign.Unspecified,
fontWeight = fontWeight,
fontSize = fontSize,
),
).toSpanStyle()
}
InLineIconRenderer(it, style, fontSize, maxLines, overflow, modifier)
},
onRegularText = {
Text(
text = it,
color = textColor,
textAlign = textAlign,
fontWeight = fontWeight,
fontSize = fontSize,
maxLines = maxLines,
overflow = overflow,
modifier = modifier,
)
},
)
}
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
maxLines: Int = Int.MAX_VALUE,
tags: ImmutableListOfLists<String>?,
style: TextStyle,
onClick: () -> Unit,
) {
CustomEmojiChecker(
text = clickablePart,
tags = tags,
onRegularText = {
Text(
text =
buildAnnotatedString {
withLink(
LinkAnnotation.Clickable(
"me",
TextLinkStyles(style = style.toSpanStyle()),
) {
onClick()
},
) {
append(clickablePart)
}
},
style = style,
maxLines = maxLines,
)
},
onEmojiText = {
ClickableInLineIconRenderer(it, maxLines, style.toSpanStyle(), onClick = onClick)
},
)
}
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
@@ -653,144 +437,3 @@ fun CreateClickableTextWithEmoji(
},
)
}
@Composable
fun ClickableInLineIconRenderer(
wordsInOrder: ImmutableList<CustomEmoji.Renderable>,
maxLines: Int = Int.MAX_VALUE,
style: SpanStyle,
suffix: String? = null,
nonClickableStype: SpanStyle? = null,
onClick: () -> Unit,
) {
val placeholderSize =
remember(style) {
if (style.fontSize == TextUnit.Unspecified) 22.sp else style.fontSize.times(1.1f)
}
val inlineContent =
wordsInOrder
.mapIndexedNotNull { idx, value ->
if (value is CustomEmoji.ImageUrlType) {
Pair(
"inlineContent$idx",
InlineTextContent(
Placeholder(
width = placeholderSize,
height = placeholderSize,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
AsyncImage(
model = value.url,
contentDescription = null,
modifier =
Modifier
.fillMaxSize()
.padding(1.dp),
)
},
)
} else {
null
}
}.associate { it.first to it.second }
val annotatedText =
buildAnnotatedString {
wordsInOrder.forEachIndexed { idx, value ->
withLink(
LinkAnnotation.Clickable("link", TextLinkStyles(style)) {
onClick()
},
) {
if (value is CustomEmoji.TextType) {
append(value.text)
} else if (value is CustomEmoji.ImageUrlType) {
appendInlineContent("inlineContent$idx", "[icon]")
}
}
}
if (suffix != null && nonClickableStype != null) {
withStyle(nonClickableStype) {
append(suffix)
}
}
}
Text(
text = annotatedText,
inlineContent = inlineContent,
maxLines = maxLines,
)
}
@Composable
fun InLineIconRenderer(
wordsInOrder: ImmutableList<CustomEmoji.Renderable>,
style: SpanStyle,
fontSize: TextUnit = TextUnit.Unspecified,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier,
) {
val placeholderSize =
remember(fontSize) {
if (fontSize == TextUnit.Unspecified) 22.sp else fontSize.times(1.1f)
}
val inlineContent =
wordsInOrder
.mapIndexedNotNull { idx, value ->
if (value is CustomEmoji.ImageUrlType) {
Pair(
"inlineContent$idx",
InlineTextContent(
Placeholder(
width = placeholderSize,
height = placeholderSize,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
AsyncImage(
model = value.url,
contentDescription = null,
modifier =
Modifier
.fillMaxSize()
.padding(horizontal = 0.dp),
)
},
)
} else {
null
}
}.associate { it.first to it.second }
val annotatedText =
remember {
buildAnnotatedString {
wordsInOrder.forEachIndexed { idx, value ->
withStyle(
style,
) {
if (value is CustomEmoji.TextType) {
append(value.text)
} else if (value is CustomEmoji.ImageUrlType) {
appendInlineContent("inlineContent$idx", "[icon]")
}
}
}
}
}
Text(
text = annotatedText,
inlineContent = inlineContent,
fontSize = fontSize,
maxLines = maxLines,
overflow = overflow,
modifier = modifier,
)
}
@@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.error_dialog_pay_withdraw_error
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -54,7 +54,7 @@ fun MayBeWithdrawal(
launch(Dispatchers.IO) { lnWithdrawal = LnWithdrawalUtil.findWithdrawal(lnurlWord) }
}
CrossfadeIfEnabled(targetState = lnWithdrawal, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = lnWithdrawal) {
if (it != null) {
ClickableWithdrawal(withdrawalString = it)
} else {
@@ -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
@@ -29,11 +29,11 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
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.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -82,7 +82,6 @@ fun LoadUrlPreviewDirect(
CrossfadeIfEnabled(
targetState = urlPreviewState,
label = "UrlPreview",
accountViewModel = accountViewModel,
) { state ->
when (state) {
is UrlPreviewState.Loaded -> {
@@ -37,13 +37,14 @@ 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.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size6dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size75dp
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -73,7 +74,7 @@ fun MyAsyncImage(
val ratio = MediaAspectRatioCache.get(imageUrl)
val showImage = remember { mutableStateOf(accountViewModel.settings.showImages()) }
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center) {
if (it) {
SubcomposeAsyncImage(
model = imageUrl,
@@ -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)
@@ -50,10 +50,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.components.util.setText
import kotlinx.coroutines.launch
/**
@@ -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
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
import com.vitorpamplona.amethyst.commons.resources.no_zap_amount_setup_long_press_to_change
import com.vitorpamplona.amethyst.commons.resources.thank_you
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
@@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
@@ -254,7 +254,6 @@ fun ReusableZapButton(
CrossfadeIfEnabled(
targetState = wasZappedByLoggedInUser.value,
label = "ZapIcon",
accountViewModel = accountViewModel,
) {
if (it) {
ZappedIcon(config.iconSizeModifier)
@@ -286,7 +285,6 @@ fun ReusableZapButton(
CrossfadeIfEnabled(
targetState = wasZappedByLoggedInUser.value,
label = "ZapIcon",
accountViewModel = accountViewModel,
) {
if (it) {
ZappedIcon(config.iconSizeModifier)
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.richtext.Base64Segment
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment
@@ -102,9 +103,12 @@ import com.vitorpamplona.amethyst.commons.richtext.VideoSegment
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState
@@ -116,7 +120,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
@@ -977,7 +980,7 @@ private fun DisplayUserFromTag(
val nickname by observeUserNickname(baseUser, accountViewModel)
val petName = nickname?.petName
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag") {
Row {
CreateClickableTextWithEmoji(
clickablePart = remember(meta, petName) { petName ?: it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() },
@@ -67,12 +67,12 @@ import com.vitorpamplona.amethyst.commons.resources.content_warning_explanation
import com.vitorpamplona.amethyst.commons.resources.content_warning_with_reason
import com.vitorpamplona.amethyst.commons.resources.show_anyway
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.commons.ui.theme.PaddingHorizontal12Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
@@ -154,7 +154,7 @@ fun ContentWarningGate(
}
}
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = showContentWarningNote) {
if (it) {
if (backdrop != null) {
Box(modifier = modifier.clipToBounds()) {
@@ -180,7 +180,7 @@ fun ObserveSensitivityWarning(
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = showContentWarningNote) {
if (it) {
ContentWarningNote(reason) { showContentWarningNote = false }
} else {
@@ -29,6 +29,7 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.view.WindowCompat
import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
@Composable
@@ -51,13 +51,13 @@ import com.vitorpamplona.amethyst.commons.resources.url_preview_open_in_browser
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.Size14Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.commons.ui.theme.previewCardImageModifier
import com.vitorpamplona.amethyst.ui.components.util.setText
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -20,31 +20,13 @@
*/
package com.vitorpamplona.amethyst.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.Window
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import com.vitorpamplona.amethyst.Amethyst
// Window utils
@Composable
fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
@Composable
fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
private tailrec fun Context.getActivityWindow(): Window? =
when (this) {
is Activity -> window
is ContextWrapper -> baseContext.getActivityWindow()
else -> null
}
tailrec fun Context.getActivity(): ComponentActivity =
when (this) {
is ComponentActivity -> this
@@ -80,6 +80,8 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
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.ui.components.getActivityWindow
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
@@ -104,11 +104,14 @@ 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.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -124,11 +127,9 @@ import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.images.BlossomFetcher
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard
import com.vitorpamplona.amethyst.ui.components.pdf.PdfViewerDialog
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -413,7 +414,7 @@ fun LocalImageView(
} else {
content.localFile
}
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) { imageVisible ->
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center) { imageVisible ->
if (imageVisible) {
SubcomposeAsyncImage(
model = imageModel,
@@ -546,7 +547,7 @@ fun UrlImageView(
content.url
}
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center) {
if (it) {
SubcomposeAsyncImage(
model = imageModel,
@@ -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" ||
@@ -62,6 +62,7 @@ import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.createBitmap
import coil3.disk.DiskCache
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.components.ImmersiveSystemBarsEffect
@@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.ui.components.ViewerBackButton
import com.vitorpamplona.amethyst.ui.components.ViewerControlsRow
import com.vitorpamplona.amethyst.ui.components.ViewerSaveToGalleryButton
import com.vitorpamplona.amethyst.ui.components.ViewerShareButton
import com.vitorpamplona.amethyst.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.ui.components.rememberViewerControlsVisibility
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.utils.Log
@@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import kotlinx.coroutines.launch
@@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
@@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -111,7 +111,6 @@ fun RenderFeedContentState(
CrossfadeIfEnabled(
targetState = feedState,
animationSpec = tween(durationMillis = 100),
accountViewModel = accountViewModel,
) { state ->
when (state) {
is FeedState.Empty -> onEmpty()
@@ -20,57 +20,17 @@
*/
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingBarNestedScroll
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingBarState
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingBarState
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberDisappearingBarState
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.components.getActivityWindow
import com.vitorpamplona.amethyst.ui.insets.SafeImeInsets
import com.vitorpamplona.amethyst.ui.insets.rememberSafeImeInsets
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
private enum class DisappearingSlot { Top, Bottom, Content, Fab }
private val FabEdgePadding = 16.dp
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold as SharedDisappearingScaffold
/**
* The app's [SharedDisappearingScaffold]: bars hide on scroll only when the user's immersive
* scrolling setting is on, and never on large screens (rail / permanent drawer), which pin the
* chrome.
*/
@Composable
fun DisappearingScaffold(
isInvertedLayout: Boolean,
@@ -81,293 +41,13 @@ fun DisappearingScaffold(
isActive: () -> Boolean = { true },
allowBarHide: Boolean = true,
mainContent: @Composable (padding: PaddingValues) -> Unit,
) {
val state = rememberDisappearingBarState()
// Large screens (rail / permanent drawer) pin the chrome: bars never slide away on
// scroll, and the immersive status-bar hiding stays off.
val canHideBars = allowBarHide && !LocalScreenLayout.current.isLargeScreen
// Hold the latest values in state so the NSC's captured lambda stays fresh across
// recompositions without rebuilding the NSC itself.
val latestIsActive by rememberUpdatedState(isActive)
val latestAllowBarHide by rememberUpdatedState(canHideBars)
val latestAccountViewModel by rememberUpdatedState(accountViewModel)
val connection =
remember(state, isInvertedLayout) {
DisappearingBarNestedScroll(
state = state,
canScroll = {
latestAllowBarHide &&
latestIsActive() &&
latestAccountViewModel.settings.isImmersiveScrollingActive()
},
reverseLayout = isInvertedLayout,
)
}
// Only wire the lifecycle observer + system-bar control when the scaffold actually moves its bars.
if (canHideBars) {
ResetBarsOnResume(state)
ImmersiveStatusBarEffect(state)
}
// If the bars were scrolled away when hiding got disabled (e.g. the window grew to a
// large tier mid-scroll), nothing above can bring them back — the nested-scroll
// connection and the resume reset are gone. Snap them visible here instead of
// leaving the chrome stranded off-screen.
LaunchedEffect(canHideBars, state) {
if (!canHideBars) state.resetToVisible()
}
// When bars are pinned, skip attaching the nested-scroll connection entirely.
// The outer Surface provides the Material container color + onBackground as
// LocalContentColor, matching M3 Scaffold's behaviour (without it, default text
// color falls back to Color.Black and is invisible on the dark theme).
// Hoisted above the branch: imePaddingSafe() inside both arms would put the call in two
// different composition groups, so toggling canHideBars (a window-size-class change) would
// dispose and rebuild it. Resolved once here, and handed to ScaffoldLayout below so the value
// this pads with is the same object the nav-bar subtraction reads.
val imeInsets = rememberSafeImeInsets()
val baseModifier =
if (canHideBars) {
Modifier.windowInsetsPadding(imeInsets).nestedScroll(connection)
} else {
Modifier.windowInsetsPadding(imeInsets)
}
val rootModifier =
baseModifier
// systemBars (not just statusBars) so a desktop window's caption bar is respected too.
.let { if (topBar == null) it.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) else it }
.let { if (bottomBar == null) it.navigationBarsPadding() else it }
Surface(
modifier = rootModifier,
color = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
) {
ScaffoldLayout(
state = state,
topBar = topBar,
bottomBar = bottomBar,
floatingButton = floatingButton,
imeInsets = imeInsets,
mainContent = mainContent,
)
}
}
@Composable
private fun ScaffoldLayout(
state: DisappearingBarState,
topBar: (@Composable () -> Unit)?,
bottomBar: (@Composable () -> Unit)?,
floatingButton: (@Composable () -> Unit)?,
imeInsets: SafeImeInsets,
mainContent: @Composable (padding: PaddingValues) -> Unit,
) {
val navBarInsets = WindowInsets.navigationBars
SubcomposeLayout { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val topPlaceable =
topBar?.let { bar ->
subcompose(DisappearingSlot.Top) {
Column(
modifier =
Modifier.graphicsLayer {
translationY = state.topHeightOffset
},
) {
bar()
HorizontalDivider(thickness = DividerThickness)
}
}.firstOrNull()?.measure(looseConstraints)
}
val topHeight = topPlaceable?.height ?: 0
val bottomPlaceable =
bottomBar?.let { bar ->
subcompose(DisappearingSlot.Bottom) {
Column(
modifier =
Modifier.graphicsLayer {
translationY = -state.bottomHeightOffset
},
) {
bar()
}
}.firstOrNull()?.measure(looseConstraints)
}
// When the bar lambda is provided but its content emits nothing (e.g. AppBottomBar
// hides itself on canPop entries, or while the keyboard is up), reserve the
// system-nav-bar inset so the FAB and content stay clear of the navigation bar instead
// of sliding under it. Subtract the IME inset: the root imePadding has already lifted the
// whole scaffold above the keyboard, and the IME inset spans the nav-bar band, so
// reserving the full nav bar on top of that would double-count and strand a gap above the
// keyboard (the `bottomBar == null` branch gets this for free via navigationBarsPadding,
// which excludes the consumed IME inset). The `bottomBar == null` case is on rootModifier.
val bottomHeight =
(bottomPlaceable?.height ?: 0).let { measured ->
if (bottomBar != null && measured == 0) {
(navBarInsets.getBottom(this) - imeInsets.getBottom(this)).coerceAtLeast(0)
} else {
measured
}
}
// Publish the measured limits so the nested-scroll connection can clamp correctly.
state.topHeightLimit = topHeight.toFloat()
state.bottomHeightLimit = bottomHeight.toFloat()
val contentPadding =
PaddingValues(
top = topHeight.toDp(),
bottom = bottomHeight.toDp(),
)
val contentPlaceable =
subcompose(DisappearingSlot.Content) {
CompositionLocalProvider(
LocalDisappearingScaffoldPadding provides contentPadding,
LocalDisappearingBarState provides state,
) {
mainContent(contentPadding)
}
}.firstOrNull()?.measure(
Constraints.fixed(layoutWidth, layoutHeight),
)
val fabPlaceable =
floatingButton?.let { fab ->
subcompose(DisappearingSlot.Fab) {
FloatingButtonHolder(state = state) { fab() }
}.firstOrNull()?.measure(looseConstraints)
}
val fabEdgePx = FabEdgePadding.roundToPx()
layout(layoutWidth, layoutHeight) {
contentPlaceable?.place(0, 0)
topPlaceable?.place(0, 0)
bottomPlaceable?.place(0, layoutHeight - bottomHeight)
if (fabPlaceable != null) {
val x = layoutWidth - fabPlaceable.width - fabEdgePx
val yBase = layoutHeight - bottomHeight - fabPlaceable.height - fabEdgePx
fabPlaceable.place(x, yBase)
}
}
}
}
/**
* Holds the floating button. Scales / fades it with the bottom-bar collapse fraction and
* rides along with the bar via graphicsLayer, so the layout pass is untouched while
* the bar animates.
*/
@Composable
private fun FloatingButtonHolder(
state: DisappearingBarState,
content: @Composable () -> Unit,
) {
Box(
modifier =
Modifier.graphicsLayer {
val visible = (1f - state.bottomCollapsedFraction).coerceAtLeast(0.001f)
scaleX = visible
scaleY = visible
alpha = visible
translationY = -state.bottomHeightOffset
},
) {
content()
}
}
@Composable
private fun ResetBarsOnResume(state: DisappearingBarState) {
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
DisposableEffect(lifecycleOwner, state) {
val observer =
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME &&
(state.topHeightOffset != 0f || state.bottomHeightOffset != 0f)
) {
scope.launch { state.resetToVisible() }
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
}
/** Fraction at which the tracked in-app bar is considered fully settled into the hidden edge. */
private const val STATUS_BAR_HIDE_THRESHOLD = 0.999f
/**
* Fraction the chrome must fall back below before the OS status bar reappears. Lower than
* [STATUS_BAR_HIDE_THRESHOLD] on purpose: this hysteresis gap means the finger that stops a fast
* scroll — which makes a tiny reverse reveal — does not immediately pop the status bar back. The
* user has to deliberately scroll the chrome back by ~30% before the status bar returns.
*/
private const val STATUS_BAR_SHOW_THRESHOLD = 0.7f
/**
* Hides the OS status bar once the tracked in-app bar has settled into the hidden edge, and shows it
* the moment it starts coming back. The OS status bar is binary (cannot slide), so it is driven off
* the collapse fraction with a near-1 threshold, which means it toggles on settle rather than
* mid-drag.
*
* The status bar sits with the top in-app bar, so it tracks the top bar's collapse. The top and
* bottom bars can collapse at different rates (each clamps to its own height), so tracking the top
* fraction keeps the status bar in step with the chrome directly beneath it. On screens with no top
* bar (topHeightLimit == 0) it falls back to the bottom bar, so the status bar still hides in sync
* with the bottom navigation.
*
* Only the top status bar is touched; the bottom OS navigation bar is left alone.
* BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE lets the user swipe to peek the status bar while immersed.
* The status bar is always restored on dispose so leaving an immersive screen can never strand a
* hidden bar.
*/
@Composable
private fun ImmersiveStatusBarEffect(state: DisappearingBarState) {
val view = LocalView.current
if (view.isInEditMode) return
val window = getActivityWindow() ?: return
val controller = remember(window, view) { WindowInsetsControllerCompat(window, view) }
LaunchedEffect(controller, state) {
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
var statusBarHidden = false
snapshotFlow {
if (state.topHeightLimit > 0f) state.topCollapsedFraction else state.bottomCollapsedFraction
}.distinctUntilChanged()
.collect { fraction ->
// Hysteresis: hide once fully settled, but only show again after the chrome has
// been pulled back past STATUS_BAR_SHOW_THRESHOLD. Between the two thresholds the
// status bar keeps its current state, so a stray reverse reveal can't flip it.
val shouldHide =
when {
fraction >= STATUS_BAR_HIDE_THRESHOLD -> true
fraction <= STATUS_BAR_SHOW_THRESHOLD -> false
else -> statusBarHidden
}
if (shouldHide != statusBarHidden) {
statusBarHidden = shouldHide
if (shouldHide) {
controller.hide(WindowInsetsCompat.Type.statusBars())
} else {
controller.show(WindowInsetsCompat.Type.statusBars())
}
}
}
}
DisposableEffect(controller) {
onDispose { controller.show(WindowInsetsCompat.Type.statusBars()) }
}
}
) = SharedDisappearingScaffold(
isInvertedLayout = isInvertedLayout,
topBar = topBar,
bottomBar = bottomBar,
floatingButton = floatingButton,
immersiveScrolling = { accountViewModel.settings.isImmersiveScrollingActive() },
isActive = isActive,
allowBarHide = allowBarHide && !LocalScreenLayout.current.isLargeScreen,
mainContent = mainContent,
)
@@ -29,52 +29,14 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
/** How the app shell presents its top-level navigation for the current window size. */
enum class NavigationStyle {
/** Compact windows (phones): bottom navigation bar + modal drawer. */
BOTTOM_BAR,
/**
* Every non-Compact window that does not dock — portrait tablets and unfolded foldables at
* any width, plus short landscape windows: a left navigation rail replaces the bottom bar
* and the drawer stays modal behind the rail's avatar button.
*/
NAV_RAIL,
/** Wide, landscape, tall windows (landscape tablets, desktop): the drawer docks permanently on the left. */
PERMANENT_DRAWER,
}
/**
* The shell layout decisions for the current window, published once per window size change
* through [LocalScreenLayout] so every screen, bar and panel agrees on the same tier.
*/
@Immutable
data class ScreenLayoutSpec(
val navigationStyle: NavigationStyle,
val hasRoomForNotificationPanel: Boolean,
) {
/**
* True on the rail and permanent-drawer tiers. Large screens hide the bottom bar and pin
* the top/bottom chrome (no disappearing bars on scroll).
*/
val isLargeScreen: Boolean get() = navigationStyle != NavigationStyle.BOTTOM_BAR
companion object {
val Phone = ScreenLayoutSpec(NavigationStyle.BOTTOM_BAR, hasRoomForNotificationPanel = false)
}
}
val LocalScreenLayout = compositionLocalOf { ScreenLayoutSpec.Phone }
import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle
import com.vitorpamplona.amethyst.commons.ui.layouts.ScreenLayoutSpec
/**
* Minimum window width for the docked notification panel: a leading navigation pane, a
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccoun
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri
import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri_description
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthPromptHost
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages
@@ -74,7 +75,6 @@ import com.vitorpamplona.amethyst.ui.broadcast.DisplayBroadcastProgress
import com.vitorpamplona.amethyst.ui.call.CallActivity
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages
import com.vitorpamplona.amethyst.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.ui.layouts.rememberScreenLayoutSpec
import com.vitorpamplona.amethyst.ui.navigation.bottombars.LocalTabReselectCoordinator
import com.vitorpamplona.amethyst.ui.navigation.bottombars.TabReselectCoordinator
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.onSurface65
import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel
import com.vitorpamplona.amethyst.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
/** Content height of the [AppBottomBar] (the 50.dp Column inside [RenderBottomMenu]),
@@ -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
@@ -25,8 +25,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import com.vitorpamplona.amethyst.ui.insets.SafeImeInsets
import com.vitorpamplona.amethyst.ui.insets.rememberSafeImeInsets
import com.vitorpamplona.amethyst.commons.ui.insets.rememberSafeImeInsets
enum class KeyboardState {
Opened,
@@ -36,7 +35,7 @@ enum class KeyboardState {
/**
* Whether the soft keyboard is currently on screen, derived from the IME inset.
*
* This intentionally reads the same [SafeImeInsets] that drives `Modifier.imePaddingSafe()`
* This intentionally reads the same [SafeImeInsets][com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets] that drives `Modifier.imePaddingSafe()`
* everywhere else in the app, so the two can never disagree about whether the keyboard is up.
*
* The original implementation measured `View.getWindowVisibleDisplayFrame` from a
@@ -48,7 +47,7 @@ enum class KeyboardState {
*
* Reading `WindowInsets.ime` fixed that, but has a latch of its own: Compose stops updating the
* inset entirely once its insets listener is left mid-animation, which pins this back at [Opened]
* in exactly the same way. [SafeImeInsets] is what closes that last hole, which is why this reads
* in exactly the same way. [SafeImeInsets][com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets] is what closes that last hole, which is why this reads
* the corrected inset rather than the raw one.
*/
@Composable
@@ -71,15 +71,15 @@ 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.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier
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
@@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.bookmarks
import com.vitorpamplona.amethyst.commons.resources.drafts
@@ -116,8 +117,10 @@ 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.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
@@ -145,8 +148,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount
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
@@ -26,9 +26,8 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import com.vitorpamplona.amethyst.commons.ui.insets.rememberSafeImeInsets
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.insets.SafeImeInsets
import com.vitorpamplona.amethyst.ui.insets.rememberSafeImeInsets
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
@@ -44,7 +43,7 @@ const val IME_SETTLE_TIMEOUT_MS = 700L
* [WindowInsetsAnimationCompat][androidx.core.view.WindowInsetsAnimationCompat] is cancelled before
* its terminal (zero) frame reaches Compose. `WindowInsets.ime` is a single app-wide holder, so it
* stays "animating" and every IME padding in the app — not just the screen being left — freezes at
* the keyboard height. [SafeImeInsets] is the backstop for when that freeze turns out to be
* the keyboard height. [SafeImeInsets][com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets] is the backstop for when that freeze turns out to be
* permanent; this is the prevention, and it is the cheaper of the two because it keeps the padding
* from ever being wrong.
*
@@ -64,7 +63,7 @@ fun interface ImeSettler {
}
/**
* Reads the same [SafeImeInsets] that drives `Modifier.imePaddingSafe()`, so the settler and the
* Reads the same [SafeImeInsets][com.vitorpamplona.amethyst.commons.ui.insets.SafeImeInsets] that drives `Modifier.imePaddingSafe()`, so the settler and the
* padding can never disagree about whether the keyboard is gone. Reading the raw `WindowInsets.ime`
* here would make every navigation burn the full [IME_SETTLE_TIMEOUT_MS] once the inset is stranded,
* waiting on a value that is never going to move again.
@@ -24,17 +24,16 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.minichatRouteFor
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
@@ -79,34 +78,6 @@ import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.nipCCGeocaching.curation.GeocacheCurationListEvent
import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent
/**
* A minichat reply — a kind-1111 [CommentEvent] posted into a chat message's thread — should open
* that message's minichat ([Route.ChatMinichat]), not the whole channel it lives in. Regular chat
* messages are kind 9 / 42, so a [CommentEvent] in a *chat context* is always a thread reply. We
* gate on the chat context (the reply is attached to a Concord / relay-group / public-chat gatherer,
* or its root message is) precisely so a generic NIP-22 comment on an article or note keeps its own
* thread route and isn't mistaken for a minichat. Returns null when it isn't a chat-context comment.
*/
fun minichatRouteFor(note: Note): Route? {
val comment = note.event as? CommentEvent ?: return null
val rootId = comment.rootEventIds().firstOrNull() ?: return null
// Prefer the reply's own Concord channel (the reply arrived over that plane, so its gatherer
// always carries the community/channel), else the root note's if it happens to be loaded. Passing
// these lets the minichat screen resolve the plane + relays even when the parent isn't cached.
val concord =
note.inGatherers?.firstNotNullOfOrNull { it as? ConcordChannel }
?: LocalCache.getNoteIfExists(rootId)?.inGatherers?.firstNotNullOfOrNull { it as? ConcordChannel }
if (concord != null) {
return Route.ChatMinichat(rootId, concord.channelId.communityId, concord.channelId.channelId)
}
val inChatContext = note.isInChatGatherer() || LocalCache.getNoteIfExists(rootId)?.isInChatGatherer() == true
return if (inChatContext) Route.ChatMinichat(rootId) else null
}
private fun Note.isInChatGatherer(): Boolean = inGatherers?.any { it is ConcordChannel || it is RelayGroupChannel || it is PublicChatChannel } == true
/**
* Kinds whose destination is `Route.Note(id)` — the generic thread view — no matter what the
* event body turns out to say. Mirrors the `else ->` branch of [routeForInner]: every kind here
@@ -419,26 +390,6 @@ fun routeToMessage(
accountViewModel: AccountViewModel,
): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, expiresDays, accountViewModel)
fun routeFor(note: EphemeralChatChannel): Route = Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url)
fun routeFor(note: PublicChatChannel): Route = Route.PublicChatChannel(note.idHex)
fun routeFor(note: LiveActivitiesChannel): Route = Route.LiveActivityChannel(note.address.kind, note.address.pubKeyHex, note.address.dTag)
fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.relayUrl.url)
fun routeFor(note: RelayGroupChannel): Route = Route.RelayGroup(note.groupId.id, note.groupId.relayUrl.url)
fun routeFor(groupId: GroupId): Route = Route.RelayGroup(groupId.id, groupId.relayUrl.url)
fun routeFor(channel: ConcordChannel): Route = Route.Concord(channel.channelId.communityId, channel.channelId.channelId)
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
fun routeForUser(userHex: HexKey): Route.Profile = Route.Profile(userHex)
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
fun routeReplyTo(
note: Note,
account: Account,
@@ -36,6 +36,8 @@ 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.layouts.LocalScreenLayout
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,8 +47,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
/**
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.new_badge_award_notif
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import com.vitorpamplona.amethyst.commons.model.Bolt12ZapEntry
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
@@ -35,9 +35,9 @@ import com.vitorpamplona.amethyst.commons.resources.copy_text_translated
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.components.cachedTranslation
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.types.displayedNoteText
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImageBorder
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -45,7 +45,7 @@ fun DisplayAuthorBanner(
)
} else {
val authorState by observeNote(baseNote, accountViewModel)
CrossfadeIfEnabled(authorState.note.author, accountViewModel = accountViewModel) { author ->
CrossfadeIfEnabled(authorState.note.author) { author ->
if (author != null) {
BannerImage(
author,
@@ -36,10 +36,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.navigation.routeForUser
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.collections.immutable.ImmutableList
@@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
@@ -72,16 +72,21 @@ 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.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.authorRouteFor
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
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.richtext.InLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding
@@ -104,10 +109,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
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
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -49,6 +49,8 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.navigation.routeForUser
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05VerifState
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -56,7 +58,9 @@ import com.vitorpamplona.amethyst.commons.resources.nip05_checking
import com.vitorpamplona.amethyst.commons.resources.nip05_failed
import com.vitorpamplona.amethyst.commons.resources.nip05_verified
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP
import com.vitorpamplona.amethyst.commons.ui.theme.NIP05IconSize
@@ -69,11 +73,8 @@ 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.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
@@ -466,7 +467,7 @@ fun RenderNIP05VerifiedSymbol(
modifier: Modifier,
accountViewModel: AccountViewModel,
) {
CrossfadeIfEnabled(targetState = state, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = state) {
when (it) {
is Nip05VerifState.Verifying, is Nip05VerifState.NotStarted -> {
// Treat "not started" and "in flight" as a single loading visual so the
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel
import com.vitorpamplona.amethyst.commons.model.isCommunityDefinition
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo
@@ -85,6 +86,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 +119,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
@@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_unfollow
import com.vitorpamplona.amethyst.commons.resources.quick_action_unmute_thread
import com.vitorpamplona.amethyst.commons.resources.report_dialog_block_hide_user_btn
import com.vitorpamplona.amethyst.commons.resources.report_dialog_blocking_a_user
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SmallestBorder
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
import com.vitorpamplona.amethyst.commons.ui.theme.secondaryButtonBackground
import com.vitorpamplona.amethyst.commons.util.njumpLink
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.Cashu
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.NutzapEntry
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.nutzap
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.nutzap
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.OnchainZapEntry
import com.vitorpamplona.amethyst.commons.model.OnchainZapStatus
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.OnchainZappedIcon
import com.vitorpamplona.amethyst.commons.ui.note.PendingClockBadge
@@ -152,7 +152,9 @@ import com.vitorpamplona.amethyst.commons.resources.reload_mint_title
import com.vitorpamplona.amethyst.commons.resources.sats_to_complete
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ChangeReactionIcon
import com.vitorpamplona.amethyst.commons.ui.note.CommentIcon
@@ -167,6 +169,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ShareIcon
import com.vitorpamplona.amethyst.commons.ui.note.VoiceReplyIcon
import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
@@ -222,13 +225,10 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.uploads.FloatingRecordingIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo
import com.vitorpamplona.amethyst.ui.note.elements.ShareOptionsBottomSheet
import com.vitorpamplona.amethyst.ui.note.types.EditState
@@ -603,7 +603,6 @@ private fun RenderShowIndividualReactionsButton(
CrossfadeIfEnabled(
targetState = wantsToSeeReactions.value,
label = "RenderShowIndividualReactionsButton",
accountViewModel = accountViewModel,
) {
if (it) {
ExpandLessIcon(modifier = Size22Modifier, Res.string.close_all_reactions_to_this_post)
@@ -1112,7 +1111,7 @@ fun LikeReaction(
onLongClick = { nav.nav(Route.UpdateReactionType) },
) {
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
CrossfadeIfEnabled(targetState = reactionType, contentAlignment = Center, label = "LikeIcon", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = reactionType, contentAlignment = Center, label = "LikeIcon") {
if (reactionType != null) {
RenderReactionType(reactionType, heartSizeModifier, iconFontSize)
} else {
@@ -1447,7 +1446,7 @@ fun ZapReaction(
accountViewModel,
zapStartingTime,
) { zapIconState ->
CrossfadeIfEnabled(targetState = zapIconState, label = "ZapIcon", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = zapIconState, label = "ZapIcon") {
if (it.wasZappedByLoggedInUser) {
ZappedIcon(iconSizeModifier)
} else {
@@ -1460,7 +1459,7 @@ fun ZapReaction(
baseNote,
accountViewModel,
) { zapIconState ->
CrossfadeIfEnabled(targetState = zapIconState, label = "ZapIcon", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = zapIconState, label = "ZapIcon") {
if (it.wasZappedByLoggedInUser) {
ZappedIcon(iconSizeModifier)
} else if (it.hasPendingPaymentRequest) {
@@ -50,13 +50,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.expand_relay_list
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ShowMoreRelaysButtonBoxModifer
import com.vitorpamplona.amethyst.commons.ui.theme.Size17Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.FlowPreview
@@ -130,7 +130,7 @@ private fun RenderRelaySlot(
accountViewModel: AccountViewModel,
nav: INav,
) {
CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier) {
if (it != null) {
RenderRelay(it, accountViewModel, nav)
}
@@ -55,6 +55,8 @@ 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.components.util.setText
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,8 +72,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
import kotlinx.coroutines.launch
@@ -35,10 +35,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.and
import com.vitorpamplona.amethyst.commons.resources.replying_to
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.lessImportantLink
@@ -72,13 +72,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.add
import com.vitorpamplona.amethyst.commons.resources.new_reaction_symbol
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
@@ -86,8 +89,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.firstFullChar
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -110,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer
import com.vitorpamplona.amethyst.commons.resources.zap_type_section_explainer
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
@@ -117,7 +118,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifi
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
@@ -34,6 +34,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.profile.ui.FollowButton
import com.vitorpamplona.amethyst.commons.profile.ui.ListButton
import com.vitorpamplona.amethyst.commons.profile.ui.UnfollowButton
@@ -21,47 +21,38 @@
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
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
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 +516,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 +536,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 =
@@ -23,30 +23,26 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.note.PlayIcon
import com.vitorpamplona.amethyst.commons.ui.note.UserNameText
import com.vitorpamplona.amethyst.commons.ui.theme.StdButtonSizeModifier
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.utils.Log
@@ -91,7 +87,7 @@ fun WatchAuthorWithBlank(
inner(noteAuthor)
} else {
val authorState by observeNote(baseNote, accountViewModel)
CrossfadeIfEnabled(targetState = authorState.note.author, modifier = modifier, label = "WatchAuthorWithBlank", accountViewModel = accountViewModel) { newAuthor ->
CrossfadeIfEnabled(targetState = authorState.note.author, modifier = modifier, label = "WatchAuthorWithBlank") { newAuthor ->
inner(newAuthor)
}
}
@@ -109,59 +105,22 @@ fun UsernameDisplay(
val userMetadata by observeUserInfo(baseUser, accountViewModel)
val nickname by observeUserNickname(baseUser, accountViewModel)
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay") {
// the account's own nickname for this user wins over the user's metadata;
// its custom emojis resolve against the contact card's tags, not the profile's
val petName = nickname?.petName
val name = petName ?: it?.info?.bestName()
if (name != null) {
UserDisplay(name, if (petName != null) nickname?.tags else it?.tags, weight, fontWeight, textColor, textAlign)
} else {
NPubDisplay(baseUser, weight, fontWeight, textColor, textAlign)
}
UserNameText(
user = baseUser,
displayName = petName ?: it?.info?.bestName(),
tags = if (petName != null) nickname?.tags else it?.tags,
modifier = weight,
fontWeight = fontWeight,
textColor = textColor,
textAlign = textAlign,
)
}
}
@Composable
private fun NPubDisplay(
user: User,
modifier: Modifier,
fontWeight: FontWeight = FontWeight.Bold,
textColor: Color = Color.Unspecified,
textAlign: TextAlign? = null,
) {
Text(
text = remember { user.pubkeyDisplayHex() },
fontWeight = fontWeight,
modifier = modifier,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = textColor,
textAlign = textAlign,
)
}
@Composable
private fun UserDisplay(
bestDisplayName: String,
tags: ImmutableListOfLists<String>?,
modifier: Modifier,
fontWeight: FontWeight = FontWeight.Bold,
textColor: Color = Color.Unspecified,
textAlign: TextAlign? = null,
) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
fontWeight = fontWeight,
textAlign = textAlign,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier,
color = textColor,
)
}
@Composable
fun DrawPlayName(name: String) {
val context = LocalContext.current
@@ -27,10 +27,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderByParentFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalFoundationApi::class)
@@ -80,7 +80,7 @@ fun WatchNoteEvent(
} else {
// avoid observing costs if already has an event.
val hasEvent by observeNoteHasEvent(baseNote, accountViewModel)
CrossfadeIfEnabled(targetState = hasEvent, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = hasEvent) {
if (it) {
onNoteEventFound()
} else {
@@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_private
import com.vitorpamplona.amethyst.commons.resources.zap_type_private_explainer
import com.vitorpamplona.amethyst.commons.resources.zap_type_public
import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.note.buttons.CloseButton
@@ -113,7 +114,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
@@ -46,6 +46,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.clink_lightning_offer
import com.vitorpamplona.amethyst.commons.resources.clink_offer_amount_range
@@ -47,12 +47,12 @@ import com.vitorpamplona.amethyst.commons.resources.invoice_expired
import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.pay
import com.vitorpamplona.amethyst.commons.resources.sats
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.service.lnurl.CachedLnInvoiceParser
import com.vitorpamplona.amethyst.service.lnurl.InvoiceAmount
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.PaymentCard
import com.vitorpamplona.amethyst.ui.components.PaymentCardAmount
import com.vitorpamplona.amethyst.ui.components.PaymentCardDescription
@@ -85,7 +85,7 @@ fun MayBeInvoicePreview(
accountViewModel: AccountViewModel,
) {
LoadValueFromInvoice(lnbcWord = lnbcWord) { invoiceAmount ->
CrossfadeIfEnabled(targetState = invoiceAmount, label = "MayBeInvoicePreview", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = invoiceAmount, label = "MayBeInvoicePreview") {
if (it != null) {
InvoicePreview(it.invoice, it.amount, it.description, it.expiresAt, accountViewModel)
} else {
@@ -95,10 +95,10 @@ import com.vitorpamplona.amethyst.commons.resources.location_picker_search_empty
import com.vitorpamplona.amethyst.commons.resources.location_picker_search_hint
import com.vitorpamplona.amethyst.commons.resources.location_picker_title
import com.vitorpamplona.amethyst.commons.resources.location_picker_use_mine
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.service.location.ForwardGeolocation
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.label
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
@@ -42,12 +42,12 @@ import androidx.compose.ui.text.style.TextOverflow
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol
import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard
@@ -217,7 +217,6 @@ private fun MyLoadUrlPreviewDirect(
CrossfadeIfEnabled(
targetState = urlPreviewState,
label = "UrlPreview",
accountViewModel = accountViewModel,
) { state ->
when (state) {
is UrlPreviewState.Loaded -> {
@@ -286,7 +285,6 @@ private fun MyLoadUrlPreviewDirectFillWidth(
CrossfadeIfEnabled(
targetState = urlPreviewState,
label = "UrlPreview",
accountViewModel = accountViewModel,
) { state ->
when (state) {
is UrlPreviewState.Loaded -> {
@@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.forked_from
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -70,10 +70,10 @@ import com.vitorpamplona.amethyst.commons.resources.timestamp_it
import com.vitorpamplona.amethyst.commons.resources.timestamp_pending
import com.vitorpamplona.amethyst.commons.resources.unfollow
import com.vitorpamplona.amethyst.commons.resources.unpin_from_profile
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.LightRedColor
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialogOneButton
import com.vitorpamplona.amethyst.ui.note.copyNoteTextAction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.post_anonymously
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton
@@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -80,6 +80,7 @@ import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_definition_available_on
@@ -101,16 +102,16 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner
import com.vitorpamplona.amethyst.commons.resources.profile_image
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LinkIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
@@ -43,6 +43,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.accept_badge
import com.vitorpamplona.amethyst.commons.resources.award_badge
@@ -74,12 +75,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
@@ -32,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
@@ -33,6 +33,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
@@ -46,6 +46,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessGameViewer
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.about_us
@@ -76,6 +77,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 +95,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
@@ -49,11 +49,11 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.components.AddButton
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.RemoveButton
import com.vitorpamplona.amethyst.commons.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.getGradient
@@ -189,7 +189,7 @@ private fun EmojiListOptions(
usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex)
}
CrossfadeIfEnabled(targetState = hasAddedThis, label = "EmojiListOptions", accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = hasAddedThis, label = "EmojiListOptions") {
if (it != true) {
AddButton(modifier = Modifier.padding(start = 3.dp)) { accountViewModel.addEmojiPack(emojiPackNote) }
} else {
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
@@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
@@ -110,7 +110,7 @@ private fun ObserverAndRenderNIP95(
mutableStateOf<BaseMediaContent?>(newContent)
}
CrossfadeIfEnabled(targetState = content, accountViewModel = accountViewModel) {
CrossfadeIfEnabled(targetState = content) {
if (it != null) {
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
ZoomableContentView(
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.follow_list_item_label
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.fundraiser_ends
import com.vitorpamplona.amethyst.commons.resources.fundraiser_onchain_donation
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.CopyIcon
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -55,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags

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