Compare commits

...
49 Commits
Author SHA1 Message Date
David KasparandGitHub 513b21e9af Merge pull request #4119 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-09-13 18:27:27 +02:00
vitorpamplonaandgithub-actions[bot] cba56f6304 chore: sync Crowdin translations and seed translator npub placeholders 2026-09-13 16:26:48 +00:00
Vitor PamplonaandGitHub bd1021c6f4 Merge pull request #4111 from vitorpamplona/claude/beautiful-brahmagupta-17clq5
Split commons into headless + commonsUI; fix 34 bugs
2026-09-13 12:12:38 -04:00
Claude 00379719c8 Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5 2026-09-13 15:00:55 +00:00
Vitor PamplonaandGitHub 0c59d0ef2e Merge pull request #4113 from vitorpamplona/claude/app-relay-connection-count-jo13xd
fix(relay): honest connected-relay count by finishing the WebSocket close handshake
2026-09-13 10:59:09 -04:00
Claude 3e9b629b8d refactor(relay): one adapter is one session; drop the lock and the socket identity checks
The adapters compared the socket OkHttp named in each callback against
the one they held, and took a monitor to make the compare-and-null
atomic and to close a window in connect() where a callback could arrive
before the field was assigned. Neither is needed: the relay client builds
a fresh adapter per dial and OkHttp binds exactly one socket to the
listener created in connect(), so anything that reaches that listener is
from this session by construction. The only question a callback has to
ask is whether the session already ended, which is one AtomicBoolean
claimed by whichever of onClosed, onFailure or disconnect() gets there
first. A compare-and-set keeps the "exactly one terminal report"
guarantee without a monitor, and there is no assignment window left to
guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-13 13:02:47 +00:00
Vitor PamplonaandGitHub 032546f70b Merge pull request #4114 from vitorpamplona/claude/image-blurhash-load-delay-y0o5f3
fix(media): stop a no-imeta GIF rendering as an invisible note while it loads
2026-09-13 08:49:07 -04:00
Claude e2bbe41991 Merge remote-tracking branch 'origin/claude/image-blurhash-load-delay-y0o5f3' into claude/image-blurhash-load-delay-y0o5f3 2026-09-13 12:17:39 +00:00
Claude 25e61542ba fix: three defects this branch introduced, found auditing its own diff
**Tor's control flow was being pinned alive for the process lifetime.** The
eviction wiring subscribed to `torManager.activePortOrNull` from a never-cancelled
coroutine on `applicationIOScope`, in both managers. That flow chains to
`TorManager.status`, whose upstream is `WhileSubscribed` and runs
`launch { service.start() }` when collected — so a permanent subscriber starts
Arti at process construction and never lets it unsubscribe on background.
AppModules documents this exact hazard for the battery ledger and deliberately
watches the raw `TorService.status` instead; I wired the poisoned well four lines
away from the warning.

Rewired from signals that are plain StateFlows and therefore free to observe:
`torPrefs.torType`, `torPrefs.externalSocksPort`, and `torService.status`'s socks
port. It moves to AppModules, which is where those live and where the precedent
is; commons had no business knowing Tor's subscription hazards anyway.

**`drop(1)` promised more than it delivered.** In the manager it skipped whatever
was present when the *coroutine started*, not when the call was made, so a route
change landing in that window was swallowed. In its new home the two coincide —
this runs during AppModules construction, before anything is pooled — so the
operator now means what the comment says.

**Animated media in a Crop cell flashed its raw URL.** The loading ladder keyed
its last branch on `ratio != null`, but `mediaSizingModifier` also bounds the
height for `ContentScale.Crop`. MyAsyncImage passes dimensions/blurhash/thumbhash
all null, so every gif in a card slot — DVM covers, long-form headers,
follow-set/calendar/music cards — hit the unbounded branch on first load and drew
URL text where it used to draw nothing. The predicate wanted "is the height
bounded", so it now says so: `contentScale == Crop || ratio != null`.

Drops ProxyRouteChange.kt and its tests with the rewire. The trigger is now three
stdlib flow operators; what needed judgement was which signals are safe to watch,
and that is recorded in the comment rather than in a test of combine().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-13 12:16:56 +00:00
Claude 624e38ce34 Merge remote-tracking branch 'origin/claude/beautiful-brahmagupta-17clq5' into claude/beautiful-brahmagupta-17clq5 2026-09-13 12:09:54 +00:00
Claude b243184026 refactor(relay): count from the connected flow itself; drop the members snapshot
The first commit on this branch added INostrClient.connectedRelays(), a
snapshot read from the pool members, because connectedRelaysFlow could not
be trusted: a relay the pool had dropped could sit in it for minutes. That
has since been fixed at the source -- the pool clears the flow itself when
it lets a relay go, and every transport reports its session end exactly
once -- so the flow's value and the members' isConnected() move on the
same transitions, and re-reading the members on every emission was a
redundant pool walk. The notification takes its count from the emitted
set, the breakdown and the Active Subscriptions screen go back to the
flow, and the snapshot API is removed. The pool test keeps pinning what
the app actually reads: the flow drops a relay on removal and on
disconnect even when the socket layer never confirms the close.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-13 12:07:46 +00:00
Vitor PamplonaandGitHub 41113ca259 Merge pull request #4118 from vitorpamplona/claude/confident-darwin-rbzz0f
Remove unused Guardian Project Maven repository
2026-09-13 08:04:21 -04:00
Claude 2b5ce62c6e build: drop the unused Guardian Project maven repository
Nothing in the version catalog or any module references an
info.guardianproject artifact anymore (the old tor-android/jtorctl
dependencies were replaced by kmp-tor from Maven Central). Resolving
every module's dependency graph with and without the repository yields
an identical result, so the repository is dead weight that only adds
a network lookup to every miss.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmSZEn7Ub9URvxow4etZc
2026-09-13 12:01:59 +00:00
Claude 5dcaa0bf5d Revert "test(commons): make the macOS keychain probe path testable on every host"
This reverts commit 047eb197a4.
2026-09-13 11:59:46 +00:00
Claude 2afe9b9221 Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5 2026-09-13 11:59:00 +00:00
Vitor PamplonaandGitHub 1590fca9b7 Merge pull request #4117 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-09-13 07:52:50 -04:00
davotoulaandgithub-actions[bot] afeb70b15b chore: sync Crowdin translations and seed translator npub placeholders 2026-09-13 11:47:56 +00:00
davotoula 68a9f77337 fix(cli): fail fast when a buzz-agent wrapper can't be made executable
setExecutable's result was ignored (Sonar). The runner launches the wrapper
via sh -c <path>, so a missing exec bit only surfaced later as an opaque
permission-denied; check it at extraction time instead.
2026-09-13 13:41:25 +02:00
David KasparandGitHub 153fd89f8d Merge pull request #4116 from davotoula/fix/sonar-http-literals
fix(amethyst): use https in preview data and image-URL placeholders
2026-09-13 13:36:19 +02:00
davotoula f9b11ef68b fix(amethyst): use https in preview data and image-URL placeholders
Clears Sonar 'Using HTTP protocol is insecure' hits. None were real requests:
six are @Preview dummy strings, four are picture-URL field hints that
suggested http:// to users. The Namecoin RPC onion placeholder stays
http:// on purpose (Tor encrypts; Namecoin Core RPC has no TLS).
2026-09-13 12:19:49 +02:00
David KasparandGitHub c8262d440b Merge pull request #4107 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-09-13 10:58:43 +02:00
davotoulaandgithub-actions[bot] 686975e322 chore: sync Crowdin translations and seed translator npub placeholders 2026-09-13 08:22:05 +00:00
David KasparandGitHub 5943dfe3d7 Merge branch 'main' into claude/beautiful-brahmagupta-17clq5 2026-09-13 10:20:46 +02:00
David KasparandGitHub 8e6cb1f7ac Merge branch 'main' into claude/image-blurhash-load-delay-y0o5f3 2026-09-13 10:19:56 +02:00
Claude 047eb197a4 test(commons): make the macOS keychain probe path testable on every host
SecureKeyStorageVaultTest's "strict lookup keeps the strict contract for
aliases outside the vault" fails on Linux (red on main's own CI run for
38bbfa8c and reproduced in a clean worktree of origin/main): the test wires a
fake mac `security` probe through macSecurityLookup, but the strict lookup
gates that path on the real os.name, so on a Linux runner it takes the
javakeyring branch, where an unknown alias throws instead of answering null.

Make the OS check injectable like the other test hooks (keyringFactory,
macSecurityLookup) and have wireMacProbe force it on, so the suite exercises
the probe path it was written for on any host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-13 02:54:11 +00:00
Claude 795394c454 Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5 2026-09-13 02:43:34 +00:00
Claude a2b0fd9405 fix(http): drop pooled connections once per real Tor route change
Removing the per-rebuild eviction left one sliver: when the user switches Tor ON,
the direct client's idle sockets to real hosts stayed pooled for the 5-minute
keepalive. Nothing could route a request through them — OkHttp keys the pool by
`Address`, which includes the proxy — but they are real connections to real hosts
outliving the moment the user asked for everything to go through Tor.

[evictOnProxyRouteChange] closes that by watching the one signal that means the
route actually moved: `torManager.activePortOrNull`. Tor coming up (null -> 9050),
going away (9050 -> null), or moving (9050 -> 9150) each evict exactly once.
`drop(1)` keeps subscribing from counting as a change.

Deliberately not driven by the two things that misled the old code:

  - Client rebuilds. One factory mints both the proxied and the direct client and
    they share a pool, so a per-rebuild check fired on every isMobileDataProvider
    emission and every resubscribe — constantly, and never specifically on a Tor
    toggle.
  - The per-feature Tor switches (imagesViaTor, videosViaTor, …). Those change
    which of the two existing clients a request picks, not the route either one
    uses, so no pooled connection goes stale.

Wired in both managers' init, so the media and relay pools behave identically.
Six tests cover the trigger, including that re-emitting the same port never
evicts — the regression the old design had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-13 02:42:06 +00:00
Claude 9e380cf9dc perf(http): drop the proxy-change pool eviction in both OkHttp factories
Both factories emptied the whole connection pool whenever the proxy differed
from the last one they were handed. It was defensive, and it was not free.

It is not needed. OkHttp keys the pool by `Address`, and `Address.equalsNonHost`
compares `proxy` — so a call is only ever given a connection opened through the
very same route. A connection left over from an old proxy is already unreachable
by anything using the new one; it just ages out of the pool on its own. There was
never a stale-route connection to protect against.

The cost was real, though. `evictAll()` empties the ENTIRE shared pool, and each
factory mints BOTH clients: `DualHttpClientManager` builds defaultHttpClient
(always SOCKS, since buildLocalSocksProxy falls back to 9050 rather than
returning null) and defaultHttpClientWithoutProxy (always null) from one
instance, and they share one `rootClient.connectionPool`. A single "last proxy"
field therefore alternated forever, and every rebuild read as a route change and
dropped every warm connection the other client was relying on. Both are
stateIn(WhileSubscribed(1000)) flows collected from a composable, so it fired on
each isMobileDataProvider change and each foreground round trip — and the next
image then paid a fresh DNS + TCP + TLS. Plausibly the "first image after a pause
takes forever" stall the pingInterval above it was added for.

DualHttpClientManagerForRelays has the identical shape, so the relay factory is
fixed the same way. Less damaging there — evictAll spares connections with active
calls, so live relay sockets survived — but it was still discarding idle pooled
connections on every network flap.

This replaces the ProxyRouteTracker approach from earlier on this branch, which
kept the eviction and merely made its bookkeeping correct. Deleting the mechanism
is the better answer, and it takes the class and its tests with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-13 01:43:43 +00:00
Claude 0067db3608 refactor(notifications): trigger the count refresh on the connected flow alone
availableRelaysFlow was merged in when connectedRelaysFlow could not be
trusted to move on a removal: a relay the pool had dropped could sit in it
for minutes, so the one flow that did move on membership changes was used
as an extra wake-up and the count re-read from the pool members. The pool
now clears the connected flow itself whenever it lets a relay go, and every
transport reports its session end exactly once, so that flow emits on every
change the count can reflect and the extra trigger only added wake-ups on
membership churn. The count still comes from client.connectedRelays(),
the members' live socket state, which is what it is meant to show.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-13 01:43:11 +00:00
Claude 08529c3a74 refactor(relay): make the socket adapters own the session, and take the guard out of BasicRelayClient
The previous commit taught BasicRelayClient to remember which listener it
had wired and to ignore callbacks from any other. That put the knowledge
"this report is about a socket I already threw away" in the wrong layer:
the transport adapter is the one that owns the socket, and OkHttp names
the socket in every callback, so the adapter can tell for free.

BasicRelayClient goes back to exactly its previous code. The contract it
relies on is now written on the WebSocket interface and kept by every
transport: a session ends with exactly one terminal callback, and
disconnect() reports onClosed synchronously and forwards nothing from that
socket afterwards -- what InProcessWebSocket has always done. Both OkHttp
adapters (quartz's BasicOkHttpWebSocket and the Android app's
OkHttpWebSocket) now check ownership on every callback, claim the terminal
report under a lock so a session cannot be reported twice, and answer
disconnect() themselves instead of waiting for OkHttp, which raises nothing
for a cancel when no reader is left to fail and otherwise raises it later
on its own thread. The reconnect race this closes is the same one as
before: with disconnect()+connect() back to back, the old socket's late
failure used to land on the new connection and cancel it.

The client-level stale-socket test is replaced by adapter-level tests on
both modules: disconnect() reports once and synchronously, OkHttp's own
reaction to the cancel never surfaces, and a relay-initiated close followed
by disconnect() is still one report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-13 01:36:28 +00:00
Claude 5f53568204 Merge remote-tracking branch 'origin/main' into claude/image-blurhash-load-delay-y0o5f3
One conflict, in BlossomReadAuthTokenProvider.signOnce(). Main already carries
the same fix via PR #4108 (davotoula), landed while this branch was open. The two
are functionally identical — same third cache look, in the same place, for the
same reason — so the conflict is resolved by taking main's wholesale and dropping
mine. The file is now byte-identical to main; my add7bfe3 contributes nothing
beyond it.

What this branch still adds over main:
  - GifVideoView loading fallback (the invisible no-imeta note)
  - ProxyRouteTracker + its tests (the shared connection pool being evicted
    whenever the proxied and direct clients alternate)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-13 01:20:04 +00:00
Claude c33256a08e Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5 2026-09-13 01:16:42 +00:00
Claude 64e0ce4b3e revert(http): restore the dispatcher limits, and record why they stay
I lowered these to 32/8 on reasoning that does not survive reading the source.

`maxRequests` is the thread ceiling — Dispatcher's executor really is
corePoolSize=0 / maxPoolSize=MAX_VALUE over a SynchronousQueue, and its own kdoc
notes a pool sized exactly to maxRequests is not even sufficient. That much was
right. The conclusions drawn from it were not:

- "128 concurrent TLS handshakes" is wrong. These hosts are HTTP/2, so concurrent
  calls to one host multiplex over a single connection; handshake count is bounded
  by distinct hosts and the pool, not by maxRequests.
- "A tighter total lets the visible images finish first" is backwards.
  promoteAndExecute walks readyAsyncCalls as a strict FIFO with no priority, and
  PrefetchFeedMedia enqueues notes before the user reaches them — so a lower cap
  makes the on-screen image queue behind those prefetches rather than start
  immediately. That is the very symptom under investigation.
- Halving maxRequestsPerHost also halves HTTP/2 stream concurrency against the
  single Blossom host a feed pulls from, which is what these were tuned for.

What is left is thread memory, and blocked threads commit little. No measurement
justified the change, so the values go back as they were. The comment now carries
the analysis so the next reader does not re-derive the same wrong intuition.

The ProxyRouteTracker fix from the same commit is unaffected and stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-13 00:46:21 +00:00
Claude 6030d22aa9 fix(commons): declare okio so the Apple targets compile after the Compose split
test-quartz-ios failed on :commons:compileKotlinIosSimulatorArm64 with
"Unresolved reference 'okio'" in service/image/DeferredDeleteFileSystem.kt.
The file always used okio, but commons only ever got it transitively:
through Coil on every target, and through OkHttp on JVM. Moving Coil to
:commonsUI removed the only Apple-side provider, and the JVM builds kept
passing, so nothing caught it locally.

Declare okio (3.18.1, the version already resolved everywhere; Apache-2.0
per its POM) in commons commonMain. Reproduced and re-verified on Linux with
:commons:compileCommonMainKotlinMetadata, which resolves commonMain against
the shared-dependency set exactly like the iOS compile. That task, for all
three KMP library modules, is now part of the lint job so this class of gap
fails fast on Linux.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-13 00:36:31 +00:00
Claude d108ba0cc8 fix(relay): answer CLOSE in the Android socket too, and stop trusting socket callbacks for pool bookkeeping
Follow-ups from an audit of the two previous commits on this branch.

The Android app does not use quartz's BasicOkHttpWebSocket: AppModules wires
its own OkHttpWebSocket, a near-twin that decides needsReconnect() from the
OkHttpClient in use. The onClosing answer therefore only reached desktop,
CLI and geode. Mirror it here, with the same loopback RFC 6455 test. The
unit-test android.util.Log stub gets a primitive-signature isLoggable (the
boxed one it had was a different method to the JVM, which is why OkHttp
could never be built in this module's tests) plus println, so the socket can
be exercised for real.

RelayPool now clears its connected flow itself on every path where it lets
a relay go -- removeRelay, removeAllRelays and disconnect -- instead of
waiting for a callback the socket layer may never deliver. The previous
commit added a members-read snapshot and migrated three callers; the other
consumers of connectedRelaysFlow (drawer status, connection-time accounting,
"wait until connected" loops) were still reading the stale set.

BasicRelayClient retires its listener before tearing a socket down and
ignores anything a retired socket reports afterwards. OkHttp delivers
onClosed from its writer thread and a cancelled socket's failure later
still; with the pool rebuilding sessions via disconnect()+connect(), a late
callback from the old socket could null the new one, orphaning a live
connection and dialing a third. disconnect() also reports onDisconnected
itself now, so a client-initiated teardown never depends on the socket
layer confirming it.

KDoc and comments that described the unanswered-close behaviour in the
present tense, or claimed a still-desired relay reads isConnected()=false
on a silent close, are corrected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-12 23:28:47 +00:00
Claude 5142c88629 build: restore upstream's Gradle 10 source-set cleanup in quartz after the merge
The merge of origin/main resolved quartz/build.gradle.kts with the branch
side wholesale, which kept the shared purity-gate apply but dropped the
`by getting` -> `getByName(...)` cleanup from 6e3af61e. Re-applied on top.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 23:20:12 +00:00
Claude ae0b70728e Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5
# Conflicts:
#	commons/build.gradle.kts
#	quartz/build.gradle.kts
2026-09-12 23:11:15 +00:00
Claude add7bfe3ca fix(http): close the read-auth single-flight window left open before putIfAbsent
signOnce() looked at the cache a second time to catch a leader that had already
finished, but it looked too early — before claiming the in-flight slot. The gap
between that look and putIfAbsent still spans signerProvider() and an allocation,
and a leader caches its token and retires its entry inside it. A straggler in
that gap therefore put into a map the leader had just emptied, won the slot, and
signed a duplicate.

Winning the slot is not proof that nobody signed; only a look from inside it is.
Once we hold the entry no one else can be leader, and our successful put observed
the map after that leader's removal, which its cache write is ordered before — so
a token visible at that point is the last word. Hand it over and stand down.

This is pre-existing, not fallout from the dispatcher change: the same test fails
on an unmodified origin/main worktree, and aFastSignerStillSharesOneSignature
already says the window is "microseconds wide, so one round hits it only now and
then" and runs 200 rounds to catch it. It cost a duplicate signature — with a
NIP-55 external signer that is a second IPC round trip, and potentially a second
prompt, for a burst of images from one gated host.

Verified with 8 consecutive runs of the suite (1600 signing rounds), all green,
against a baseline that failed inside the first run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-12 22:52:59 +00:00
Claude b8135e76c4 fix(relay): answer a relay's WebSocket CLOSE frame so OkHttp can finish the handshake
OkHttp fires onClosed only once both peers have sent a CLOSE frame, and
sending ours is the application's job (WebSocketListener KDoc; RealWebSocket
emits onClosed solely from the writer once our Close is dequeued with the
peer's code already set; the bundled WebSocketEcho recipe answers onClosing
with close(1000, null)). BasicOkHttpWebSocket never implemented onClosing,
so a relay-initiated close left the socket half-closed: no onClosed, no
onFailure, send() still accepted and silently discarded, and a later
cancel() silent as well because no reader was left to fail. The relay
client kept believing it was connected, with its REQs live, until the 120s
ping path finally failed up to two intervals later.

Answer onClosing with close(1000, null). Verified against OkHttp 5.5.0:
onClosed then fires at once whether the relay still holds the TCP session
or has already dropped it, and the existing onClosed path in
BasicRelayClient marks the connection closed and lets the pool reconnect
under its normal backoff. Always 1000 rather than echoing the relay's code,
since close() validates the code it writes and relays may send reserved
ones.

The test drives the wrapper against a minimal RFC 6455 server on a loopback
ServerSocket (no new dependency): the relay sends CLOSE, the client must
answer with its own CLOSE frame and report onClosed with the relay's code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-12 22:52:37 +00:00
Claude 4fa619d2a2 fix: audit of commons and commonsUI — 34 verified bug and performance fixes
Three read-only sweeps over commons (feeds, relayClient, model, viewmodels,
services) and commonsUI produced 44 candidates; each was re-read in the
source, 38 held up, 34 are fixed here, 4 are deferred with rationale in
commons/plans/2026-09-12-audit-findings.md.

Correctness highlights:
- SecureKeyStorage (jvm): AES-GCM was initialised with IvParameterSpec, which
  every JDK rejects, so the no-keyring fallback never worked. GCMParameterSpec.
- OnionLocationInterceptor cached any Onion-Location header and re-pointed all
  Tor traffic at it for 24h over plain http; only .onion targets now.
- BasicBundledInsert wedged forever after one exception (no finally).
- CachedRichTextParser keyed parses on a 32-bit hash without checking inputs.
- noProtocolUrlValidator backtracked exponentially per composer keystroke.
- Base83 indexed a 255-entry table with any char code from an imeta tag.
- MetadataRateLimiter never flushed a batch smaller than 20 pubkeys.
- FeedMetadataCoordinator mutated six HashSets from two threads and marked
  pubkeys 101+ as requested without asking for them.
- Note: two discarded boolean/relay expressions, an NPE window in flow(),
  removeReport leaving empty buckets, unlocked read-modify-write on
  replies/boosts/edits/reactions/reports/labels.
- EventCollectionState restarted its flush timer on every insert.
- Chatroom prune outside the lock; top-zappers publish outside the mutex;
  hashCode used as dedup/feed keys; OnlyLatestVersionSet.addAll always true.
- UI: ClickableTexts remembered a stale onClick, ZonedSwipeModifier a stale
  openDrawer, two robohash light-theme predicates thrashed one cache, a chess
  remember key summed two counters.

Performance: user-cache search off the Compose dispatcher, regexes hoisted
in SearchResultSorter, frame-rate animation reads moved out of composition
in Shimmer/LoadingAnimation/BunkerHeartbeat, GlowingCard allocations cached,
emoji inlineContent remembered, itemsIndexed in the search pickers.

Ten regression tests added (rate limiter flush/dedup/rate, GCM round trip,
regex timing, Base83 bounds, CosineCache key). Verified: commons and
commonsUI jvmTest, compiles of cli, desktopApp, nappletHost, amethyst.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 22:03:11 +00:00
Claude fe8580ca56 chore: share the KMP purity gate, fix stale doc paths and dead imports after the split
Follow-ups from a review of the commons/commonsUI split:

- `verifyKmpPurity` was copy-pasted into quartz, commons and commonsUI and the
  three copies had already drifted (checked dirs, hint text). It now lives
  once in gradle/kmp-purity.gradle.kts and each module applies it; the
  checked-dir list is the union, filtered by existence. Verified the shared
  task still fails on a deliberate java.util.UUID reference in commons.
- amethyst/src/main/res/CLAUDE.md and the compose-expert catalog reference
  still pointed at the pre-split composeResources / ui paths.
- Same-package imports left behind in the files moved to commons.feeds and
  commons.model are removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 21:43:20 +00:00
Claude 501c9a5b5a Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5
# Conflicts:
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/http/BlossomReadAuthTokenProvider.kt
2026-09-12 21:38:48 +00:00
Claude 97b13b7c47 refactor: move reply-context logic to commons.model; keep the Compose compiler in commons by measurement
Closes the last two items of documented debt from the commons/commonsUI split.

ParentNote (replyingDirectlyTo, isCommunityDefinition) and ReplyContext are
pure thread logic used by ViewModels, so they move from the misleading
`ui.note` package to `commons.model`, next to ThreadAssembler, together with
their tests and the StubCache fixture that shared the package. No `ui.*`
package is left in commons. The `ui.note` composables in commonsUI gain
explicit imports; consumer imports rewritten.

Whether commons still needs the Compose compiler plugin was an open question;
it is now measured. With compiler reports on the three GUI modules and full,
non-incremental recompiles in both configurations, removing the plugin flips
composable parameters typed with unannotated commons classes (TopFilter,
TorSettings, ProfileBroadcastStatus, ScheduledPost, EmojiPackState, ...) from
runtime-stable to unstable: 20→28 in commonsUI, 33→65 in desktopApp,
90→149 in amethyst. The plugin stays; the numbers are recorded in the build
file, ARCHITECTURE.md and the split plan so the question is not reopened.

Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug
compiles for nappletHost and amethyst; commons/commonsUI/cli/desktopApp JVM
test suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 20:58:28 +00:00
Claude 943e137d4e perf(http): stop wiping the media connection pool, and size the dispatcher for a phone
Two problems in the shared non-relay client, both of which make every HTTP
request cost more than it should.

The connection pool was being emptied constantly. buildHttpClient() evicted the
whole pool whenever the proxy differed from the last one it was handed, tracked
in a single field. But one factory mints BOTH long-lived variants:
DualHttpClientManager builds defaultHttpClient (always SOCKS, since
buildLocalSocksProxy falls back to 9050 rather than returning null) and
defaultHttpClientWithoutProxy (always null) from the same instance, and the two
share one rootClient.connectionPool. So the field alternated between the proxy
and null forever, and every rebuild read as a route change and wiped the pool
they share. Both are stateIn(WhileSubscribed(1000)) flows collected from a
composable, so that happened on every isMobileDataProvider change and every
foreground round trip — and the next image then paid a fresh DNS + TCP + TLS.
This is a plausible source of the "first image after a pause takes forever"
stall the pingInterval above it was added for.

ProxyRouteTracker narrows it to what the eviction was actually for: a direct
build never evicts (null is that variant's permanent route), and a proxied build
evicts only when the Tor port really moved. Nothing is lost by being this narrow
— OkHttp's Address, the pool key, already includes the proxy, so proxied and
direct connections to the same host are distinct entries that can never be
handed to each other's calls.

Second, maxRequests was 128. Dispatcher's executor is an unbounded cached pool,
so that is the thread ceiling: up to 128 threads running 128 concurrent TLS
handshakes on a handset. Nothing upstream bounds the arrival rate either —
Coil's enqueue is unbounded and PrefetchFeedMedia warms ±3 notes on both sides
of the viewport on every visible-range change — so a fast scroll really does
reach it. Past saturation, more concurrency slices the same bandwidth thinner
and pushes every image's completion out together, the on-screen one included.
32 total / 8 per host keeps the per-host lift that feeds need while letting
visible images finish and paint.

The pool fix is covered by tests. The dispatcher numbers are a reasoned choice,
not a measured one — worth a run against benchmark/ before release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-12 20:55:41 +00:00
Claude e076b1eaee fix(media): stop a no-imeta GIF rendering as an invisible note while it loads
GifVideoView's Loading branch emitted DisplayBlurHash unconditionally, and
DisplayBlurHash renders nothing at all when both hashes are absent —
placeholderModel(null, null) returns null and the composable early-returns.

That is exactly the state a post with no imeta lands in. With no `dim` tag and
nothing in MediaAspectRatioCache, `ratio` is null, so mediaSizingModifier falls
to a bare fillMaxWidth() with no height constraint. The container then wraps an
empty loading state and the whole note collapses to zero height: no picture, no
URL, no spinner, just a gap in the feed for however long the fetch takes, and
then the image appearing from nowhere. Seen on a kind-1111 comment from Sidecar
whose content is a single blossom .gif URL.

UrlImageView already has the ladder this needs, so mirror it: blurhash/thumbhash
when there is one, a spinner in the reserved box when only a ratio is known, and
otherwise the URL plus a loading symbol via WaitAndDisplay. Every branch now
emits something with a height.

This is the missing feedback, not the latency — a slow fetch still takes as long
as it takes, it just stops being invisible while it does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdidqyhRni5L3ft5h8qti5
2026-09-12 20:55:22 +00:00
Claude 88ae3d0f5a fix: close the fast-signer single-flight race in BlossomReadAuthTokenProvider
`aFastSignerStillSharesOneSignature` failed on a loaded machine (rounds 11,
18 and 31 across three runs, on the branch head and on the already-pushed
commit alike): a straggler could miss the in-flight map, miss the cache,
get descheduled, and then win `putIfAbsent` only because the fast leader
had already cached *and* retired its entry — and sign a second time.

Take one more look at the cache after winning the in-flight slot. A prior
leader's cache write happens before its `remove`, and winning the slot
after that `remove` goes through the same ConcurrentHashMap bin, so the
just-minted token is visible there; hand it out and retire the slot instead
of launching a duplicate signature. The test class now passes four runs in
a row where it previously failed three in a row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 20:20:30 +00:00
Claude 1093b3ce8a fix(notifications): count connected relays from the pool's live socket state
The always-on notification read its "Connected to N relays" from
RelayPool.connectedRelays, a set that only moves on the onConnected /
onDisconnected callbacks. That set over-reports: a relay that sent a
WebSocket CLOSE frame never produces a callback (the app does not answer
onClosing, so OkHttp fires neither onClosed nor onFailure, and the later
cancel() is silent too), so the URL lingers until the 120s ping path
finally fails. After the feeds tore down in the background this left
hundreds of already-dropped relays in the count for minutes, with no
subscription in the "show details" breakdown to justify any of them.

Expose the pool's ground truth instead: RelayPool.connectedRelayUrls()
reads each member's isConnected(), surfaced as INostrClient.connectedRelays()
(defaulting to the flow's value for pool-less clients). The notification
keeps the flows only as a trigger, merging in availableRelaysFlow because
that is the flow that moves when the pool drops such a relay, and re-reads
the live count on each sample. The "show details" breakdown and the
Active Subscriptions screen read the same source so all three agree.

A pool test pins the drift: with a socket layer that never confirms the
close, removing a relay leaves it in the flow but out of the snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014cq6vrfQkASwxgqXXY4py8
2026-09-12 20:17:29 +00:00
Claude 713b7d41f6 refactor: move feed DAL to commons.feeds, chess UI to nip64Chess.ui, tighten amy budget
Follow-ups to the commons/commonsUI split.

Feed data-access layer: FeedFilter, AdditiveFeedFilter, AdditiveComplexFeedFilter,
ChangesFlowFilter, FeedContentState, FeedState, InvalidatableContent,
DefaultFeedOrder, RepostRenderability and friends move from the misleading
`ui.feeds` package to the root of `commons.feeds`, next to `feeds/custom`.
The `ui.feeds` composables (NewPostsChip, RelayReachMarker, DM history cards)
stay in commonsUI and gain explicit imports. Consumer imports rewritten.

Chess: the eleven composable files in commonsUI move from the flat
`nip64Chess` package to `nip64Chess.ui`; the logic stays in
`commons/…/nip64Chess`. Consumer imports rewritten.

CLI size budget: measured after the split (1.15.2, Linux x64) the JVM
tarball is 55 MB and the jlink image tarball 80 MB, so the release gate
drops from 200 MB to 120 MB per asset. BUILDING.md, the architecture docs,
the feed-patterns skill and the split plan record the new state.

Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug
compiles for nappletHost and amethyst; commons/commonsUI/cli/desktopApp JVM
test suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 20:13:18 +00:00
Claude a87222c51f refactor: split Compose UI out of commons into a new commonsUI module
`:commons` is on the CLI classpath, yet it declared Compose UI, Coil, Compose
resources, markdown and desktop Compose as dependencies, dragging ~40 MB of
UI/Skiko jars into every `amy` distribution. This moves every
Compose-dependent file into a new KMP module, `:commonsUI`, that
`api`-depends on `:commons`; `:commons` keeps only the Compose runtime
(stability annotations + snapshot state) and lifecycle-viewmodel.

Files keep their `com.vitorpamplona.amethyst.commons.*` packages, so the split
is a build-graph boundary and no consumer import changed. 236 files were
`git mv`'d (composables, icons, robohash, theme, Coil fetchers, the
`@Composable` relay-client entry points, `composeResources`, and the tests
that exercise them). Two headless files needed surgery instead of a move:
`GalleryParser` lost a vestigial foundation `@OptIn`, and the
`LocalPrivacyLockState`/`lockStateFor` CompositionLocal accessor moved out of
`PrivacyLockState` into its own commonsUI file. The feed DAL under `ui/feeds`
and `ui/note/ParentNote`+`ReplyContext` stay in `commons` because ViewModels
depend on them.

`amethyst`, `desktopApp`, `nappletHost` (NappletWebContract serves the shell
from composeResources) and `benchmark` now depend on `:commonsUI`; `cli`,
`geode` and `marmotBench` do not. commons' androidMain gains an explicit
androidx.core KTX dep it previously got transitively through Compose UI.

CI, crowdin, the icon-font tools and the escaping hook point at the new
composeResources location; CLAUDE.md, commons/ARCHITECTURE.md, a new
commonsUI/ARCHITECTURE.md, CONTRIBUTING, BUILDING and the affected skills
document the boundary. A plan doc under commons/plans records the
classification method and follow-ups.

Verified: JVM compiles for commons, commonsUI, cli, desktopApp; Android debug
compiles for nappletHost and amethyst; commons/commonsUI/cli JVM test suites;
both verifyKmpPurity gates; the cli runtime classpath no longer resolves
Compose UI, material3, Skiko or Coil.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
2026-09-12 16:26:59 +00:00
607 changed files with 3433 additions and 1572 deletions
+43 -25
View File
@@ -3,7 +3,7 @@
## Project Overview
Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching
over to a Kotlin Multiplatform project. The main modules are: `quartz`, `commons`, `amethyst`,
over to a Kotlin Multiplatform project. The main modules are: `quartz`, `commons`, `commonsUI`, `amethyst`,
`desktopApp`, `cli`, plus the audio-rooms transport stack `quic` + `nestsClient`. Quartz should
contain implementations of Nostr specifications and utilities to help implement them. Commons stores
shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). The Desktop
@@ -48,11 +48,18 @@ amethyst/
│ ├── androidMain/ # Android-specific (crypto, storage)
│ ├── jvmMain/ # Desktop JVM-specific
│ └── iosMain/ # iOS-specific
├── commons/ # Shared UI components (convert to KMP)
├── commons/ # Shared HEADLESS layer (models, state, ViewModels, relay client) — CLI-safe
│ └── src/
│ ├── commonMain/ # Shared composables, icons, state
│ ├── androidMain/ # Android-specific UI utilities
── jvmMain/ # Desktop-specific UI utilities
│ ├── commonMain/ # Domain models, state holders, ViewModels, services
│ ├── jvmAndroid/ # JVM-bound services shared by Android + Desktop
── androidMain/ # Android-specific actuals (Keystore, DataStore)
│ └── jvmMain/ # Desktop-specific actuals (keyring, upload pipeline)
├── commonsUI/ # Shared Compose UI on top of commons (composables, icons, theme, Coil, resources)
│ └── src/
│ ├── commonMain/ # Shared composables, icons, theme, composeResources (strings/fonts)
│ ├── jvmAndroid/ # Markdown renderer, Coil OkHttp fetchers
│ ├── androidMain/ # Android Coil bridge
│ └── jvmMain/ # Desktop Coil bridge (+ skikoMain shared with iOS)
├── quic/ # Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport (audio-rooms transport)
│ └── src/
│ ├── commonMain/ # Protocol, frame/packet codecs, TLS state machine
@@ -70,12 +77,20 @@ amethyst/
**Sharing Philosophy:**
- `quartz/` = Nostr business logic, protocol, data (no UI)
- `commons/` = Shared code for every front end (Android, Desktop, iOS, and the
headless `cli`): domain models, state holders, ViewModels, the relay client,
shared services, **and** the Compose UI that ≥1 GUI front end renders. The
package taxonomy, the CLI-safe / UI boundary, and a "where does my code go?"
guide are documented in **`commons/ARCHITECTURE.md`** — read it before adding
a new package or dropping code into `commons`.
- `commons/` = Shared **headless** code for every front end (Android, Desktop,
iOS, and the headless `cli`): domain models, state holders, ViewModels, the
relay client, shared services. It may use the Compose *runtime*
(`@Stable`/`@Immutable`, snapshot state) but never Compose UI, Coil or
Compose resources — the build enforces this: `commons` has no such deps.
- `commonsUI/` = Shared **Compose UI** that ≥1 GUI front end renders
(composables, `ui/theme`, icons, robohash, Coil fetchers, markdown, the
`composeResources` strings/fonts and the generated `Res` class). Depends on
`commons` (as `api`); `cli` never depends on it. Files keep their
`com.vitorpamplona.amethyst.commons.*` packages — the split is a module
boundary, not a package rename. The package taxonomy, the CLI-safe / UI
boundary, and a "where does my code go?" guide are documented in
**`commons/ARCHITECTURE.md`** (+ `commonsUI/ARCHITECTURE.md`) — read them
before adding a new package or dropping code into either module.
- `quic/` = Transport library (QUIC + HTTP/3 + WebTransport); reusable for any
KMP project that needs MoQ. Has no Android-framework dependencies.
- `nestsClient/` = MoQ + audio-rooms client; takes `:quic` as transport,
@@ -88,7 +103,7 @@ amethyst/
- `amethyst/` & `desktopApp/` = Platform-native layouts and navigation
- `cli/` = Thin assembly layer over `quartz/` + `commons/` (no new logic
allowed). May also depend on `:geode` (for `amy serve`, which embeds the
standalone relay); never on `:amethyst` or `:desktopApp`.
standalone relay); never on `:commonsUI`, `:amethyst` or `:desktopApp`.
**Plans per module:** design docs for new subsystems live in the owning
module's `plans/YYYY-MM-DD-<slug>.md` (e.g. `cli/plans/`, `commons/plans/`).
@@ -180,16 +195,19 @@ etc. instead of re-implementing them.
**Share vs keep platform-native:**
- **Share** → `quartz/commonMain/` (business logic, data models, protocol) and
`commons/commonMain/` (major UI components, **ViewModels** under
`viewmodels/`, icons). ViewModels are platform-agnostic state + logic
(StateFlow/SharedFlow), so they belong in `commons`.
- **Share** → `quartz/commonMain/` (business logic, data models, protocol),
`commons/commonMain/` (**ViewModels** under `viewmodels/`, state holders,
relay client, services — headless) and `commonsUI/commonMain/` (major UI
components, icons, theme). ViewModels are platform-agnostic state + logic
(StateFlow/SharedFlow), so they belong in `commons`; anything that imports
`androidx.compose.ui`/`foundation`/`material3`, Coil, or `Res` belongs in
`commonsUI`.
- **Keep native** → screen composables/scaffolding (Desktop `Window` vs Android
`Activity`), navigation (sidebar vs bottom nav), platform interactions
(gestures, keyboard shortcuts), system integrations (notifications, file
pickers).
When extracting a composable: move it to `commons/commonMain/` (see
When extracting a composable: move it to `commonsUI/commonMain/` (see
`/compose-expert`), add expect/actual for any platform behavior (see
`/kotlin-multiplatform`), then point both Android and Desktop at the shared
version. `quartz/` is protocol-only — no composables.
@@ -216,7 +234,7 @@ version. `quartz/` is protocol-only — no composables.
## Dependency Licensing
**MANDATORY whenever you introduce a new third-party dependency** — in *any*
module (`quartz`, `commons`, `amethyst`, `desktopApp`, `cli`, `quic`,
module (`quartz`, `commons`, `commonsUI`, `amethyst`, `desktopApp`, `cli`, `quic`,
`nestsClient`, …), whether you add it to `gradle/libs.versions.toml` or to a
module's `build.gradle.kts`: determine its license **before** wiring it in.
Amethyst ships under the **MIT** license, so a copyleft dependency linked into a
@@ -253,9 +271,9 @@ JVM). See `/kotlin-multiplatform` for the expect/actual and source-set patterns.
## Icons
The Material Symbols font bundled at
`commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf`
`commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf`
is a **subset** that only contains the glyphs referenced from
`commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt`.
`commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt`.
**MANDATORY:** Whenever you add a new icon — i.e. introduce a
`MaterialSymbol("\uXXXX")` codepoint that wasn't already referenced anywhere in
@@ -274,21 +292,21 @@ regenerating.
### Amethyst's own icons are also a font
The icons in `commons/.../commons/icons/*.kt` (Like, Reply, Reposted, Zap, …) are
The icons in `commonsUI/.../commons/icons/*.kt` (Like, Reply, Reposted, Zap, …) are
**also** compiled into a font, `composeResources/font/amethyst_icons.ttf`, and drawn
as glyphs via `AmethystIconGlyph`. Drawing an `ImageVector` rasterises its paths into
a per-instance cached layer, so a feed re-rasterised the same glyph once per card;
a glyph is a blit from the shared text atlas. Measured: frame P90 **-10.7%**,
overrun P90 **-17.4%** on the feed scroll benchmark.
**MANDATORY:** whenever you add or change an icon under `commons/.../commons/icons/`,
**MANDATORY:** whenever you add or change an icon under `commonsUI/.../commons/icons/`,
regenerate the font *and* its codepoint table together:
```bash
python3 tools/icon-font/build_icon_font.py \
commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons \
commons/src/commonMain/composeResources/font/amethyst_icons.ttf \
commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/AmethystIcons.kt
commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons \
commonsUI/src/commonMain/composeResources/font/amethyst_icons.ttf \
commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/AmethystIcons.kt
```
Both outputs must be committed together: codepoints are assigned in filename order,
+2 -2
View File
@@ -21,7 +21,7 @@ that has to catch it.
Repair with:
python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \\
commons/src/commonMain/composeResources
commonsUI/src/commonMain/composeResources
`--no-unwrap-quotes` is mandatory on already-migrated files: escape conversion is
idempotent, quote-unwrapping is not, and a second unwrap strips the real display
@@ -77,7 +77,7 @@ def main() -> int:
print(
"\nRepair:\n"
" python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \\\n"
" commons/src/commonMain/composeResources\n"
" commonsUI/src/commonMain/composeResources\n"
"(--no-unwrap-quotes is mandatory on already-migrated files.)",
file=out,
)
@@ -47,7 +47,7 @@ Walk the imports. The usual offenders:
| `android.util.Log` | Replace with `quartz` `PlatformLog` (already multiplatform). |
| `android.graphics.Bitmap` | Almost never needed by Amy. Keep in Android and split the function. |
| `android.net.Uri` | Replace with `kotlinx.io` path types or a plain `String`. |
| `androidx.compose.*` | Must stay out of `commons/commonMain` unless you're in a Compose-Multiplatform module. Amy doesn't depend on Compose. |
| `androidx.compose.*` | Compose UI (`ui`/`foundation`/`material3`), Coil and `Res` must stay out of `commons` entirely — they belong in `:commonsUI`, which Amy never depends on. Only the Compose *runtime* (`@Stable`, snapshot state) is allowed in `commons`. |
### Step 3 — Pick a migration strategy per dependency
@@ -66,7 +66,7 @@ Walk the imports. The usual offenders:
# Target location depends on what it is:
# - Protocol → quartz/src/commonMain/kotlin/…
# - Business logic → commons/src/commonMain/kotlin/…
# - UI → commons/src/commonMain/… (needs Compose Multiplatform)
# - UI → commonsUI/src/commonMain/… (needs Compose Multiplatform; never used by amy)
git mv amethyst/src/main/java/com/.../FollowListManager.kt \
commons/src/commonMain/kotlin/com/.../FollowListManager.kt
```
+7 -7
View File
@@ -24,7 +24,7 @@ Visual UI patterns for sharing composables across Android and Desktop.
## Philosophy: Share by Default
**Default to `commons/commonMain`** unless platform experts indicate otherwise.
**Default to `commonsUI/commonMain`** (shared composables live in `:commonsUI`, the Compose half of the shared layer; headless state/ViewModels stay in `:commons`) unless platform experts indicate otherwise.
### Always Share
@@ -416,7 +416,7 @@ fun DataScreen(uiState: UiState) {
}
```
**Components** (all in `commons/commonMain`):
**Components** (all in `commonsUI/commonMain`):
- `LoadingState` - Progress indicator + message
- `EmptyState` - Empty message + optional refresh button
- `ErrorState` - Error message + optional retry button
@@ -527,12 +527,12 @@ fun FeedList(items: List<Item>) {
| Task | Pattern | Location |
|------|---------|----------|
| Reusable UI | State hoisting | commons/commonMain |
| Reusable UI | State hoisting | commonsUI/commonMain |
| Simple state | remember { mutableStateOf() } | Composable scope |
| Derived state | derivedStateOf { } | remember block |
| Async → state | produceState { } | Composable function |
| Custom icons | roboBuilder + PathData | commons/icons |
| Loading/Error | LoadingState, ErrorState | commons/ui/components |
| Custom icons | roboBuilder + PathData | commonsUI/icons |
| Loading/Error | LoadingState, ErrorState | commonsUI/ui/components |
| Theme colors | MaterialTheme.colorScheme | Any @Composable |
| Navigation | Delegate to platform expert | amethyst/, desktopApp/ |
@@ -540,7 +540,7 @@ fun FeedList(items: List<Item>) {
### Creating a Shared Component
1. Start in `commons/src/commonMain/kotlin/.../ui/components/`
1. Start in `commonsUI/src/commonMain/kotlin/.../ui/components/`
2. Use Material3 primitives only
3. Hoist state (parameters for data, callbacks for events)
4. Add modifier parameter
@@ -551,7 +551,7 @@ fun FeedList(items: List<Item>) {
1. Read current implementation in `amethyst/` or `desktopApp/`
2. Identify pure visual logic (no platform APIs)
3. Create in `commons/commonMain` with hoisted state
3. Create in `commonsUI/commonMain` with hoisted state
4. Replace platform implementations with shared component
5. Keep platform-specific wrappers if needed
@@ -1,11 +1,11 @@
# Shared Composables Catalog
This catalog documents shared UI components in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/`.
This catalog documents shared UI components in `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/` (the Compose half of the shared layer; headless state stays in `commons`).
## Directory Structure
```
commons/src/commonMain/kotlin/.../commons/ui/
commonsUI/src/commonMain/kotlin/.../commons/ui/
├── components/ # Reusable UI components
├── screens/ # Screen-level composables
├── theme/ # Theming and styling
@@ -232,7 +232,7 @@ private val pathData1 = PathData {
```
**roboBuilder** - Custom ImageVector.Builder DSL
- Located in: `commons/robohash/`
- Located in: `commonsUI/.../commons/robohash/`
- Pattern: Builder-based, composable paths
- Parts: Face, Eyes, Mouth, Body, Accessory (0-9 variants each)
- Colors: Dynamic (fgColor parameter) + Black constants
+4 -4
View File
@@ -1,6 +1,6 @@
---
name: feed-patterns
description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with the shared `FeedFilter` / `AdditiveFeedFilter` / `ChangesFlowFilter` / `FeedContentState` in `commons/.../ui/feeds/`, the Android-only `AdditiveComplexFeedFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose.
description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with the shared `FeedFilter` / `AdditiveFeedFilter` / `ChangesFlowFilter` / `FeedContentState` in `commons/.../feeds/`, the Android-only `AdditiveComplexFeedFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose.
---
# Feed Patterns
@@ -25,7 +25,7 @@ Amethyst's "feed" abstraction is: a `FeedFilter` that decides which notes belong
│ ◄── MarmotGroupFeedViewModel │
│ │
│ │
│ commons/.../ui/feeds/ (shared, KMP) │
│ commons/.../feeds/ (shared, KMP) │
│ IFeedFilter / FeedFilter<T> (abstract base) │
│ IAdditiveFeedFilter / AdditiveFeedFilter<T> │
│ ChangesFlowFilter │
@@ -68,7 +68,7 @@ Amethyst's "feed" abstraction is: a `FeedFilter` that decides which notes belong
### Shared filter bases (commons)
`commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/`:
`commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/`:
- **`FeedFilter.kt`** — `abstract class FeedFilter<T> : IFeedFilter<T>`. Has `feed(): List<T>` (the sync query against the cache), `feedKey(): String` (identity used to cache), `limit()`, and `loadTop()`.
- **`AdditiveFeedFilter.kt`** — `abstract class AdditiveFeedFilter<T> : FeedFilter<T>(), IAdditiveFeedFilter<T>`. Adds incremental updates (the "additive" part): `updateListWith(oldList, newItems)` runs `applyFilter(newItems)` and grafts accepted items onto the existing list (re-`sort` + `take(limit())`) without recomputing everything.
@@ -100,7 +100,7 @@ Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities,
## Filter Sharing (Android vs Desktop)
- The filter **base classes** (`FeedFilter`, `AdditiveFeedFilter`, `ChangesFlowFilter`) and feed state (`FeedContentState`) are in `commons/.../ui/feeds/`**shared**. ViewModels are in `commons/.../viewmodels/`**shared**.
- The filter **base classes** (`FeedFilter`, `AdditiveFeedFilter`, `ChangesFlowFilter`) and feed state (`FeedContentState`) are in `commons/.../feeds/`**shared**. ViewModels are in `commons/.../viewmodels/`**shared**.
- The **concrete** filters are platform-local: Android's in `amethyst/.../ui/screen/loggedIn/*/dal/`, Desktop's in `desktopApp/.../feeds/`. `amethyst/.../ui/dal/` keeps Android-only helpers (`AdditiveComplexFeedFilter`, `FilterByListParams`, `DefaultFeedOrder`) plus back-compat typealiases.
- When porting a feed, share the concrete filter only if both platforms need identical inclusion rules.
@@ -24,9 +24,9 @@ There are two separate `strings.xml` trees, each with its own default `values/`
| Tree | Default file | Per-locale file |
|------|--------------|-----------------|
| **amethyst** (Android app) | `amethyst/src/main/res/values/strings.xml` | `amethyst/src/main/res/values-<locale>/strings.xml` |
| **commons** (KMP Compose resources, shared by Android + Desktop) | `commons/src/commonMain/composeResources/values/strings.xml` | `commons/src/commonMain/composeResources/values-<locale>/strings.xml` |
| **commonsUI** (KMP Compose resources, shared by Android + Desktop) | `commonsUI/src/commonMain/composeResources/values/strings.xml` | `commonsUI/src/commonMain/composeResources/values-<locale>/strings.xml` |
The `commons` tree appeared when shared event-renderer composables were extracted out of `amethyst/` into `commons/` (Compose Multiplatform `stringResource`). It is **not** a copy of the amethyst tree — the vast majority of its keys are commons-only; only a small handful overlap. Every diff/count/translate command below works on either tree by swapping the base path — **run the whole technique once per tree** and report them separately (each maps to its own Crowdin file, so the counts should reconcile against two different Crowdin UI numbers).
The `commonsUI` tree appeared when shared event-renderer composables were extracted out of `amethyst/` into `commons/` — now `commonsUI/` since the UI split (Compose Multiplatform `stringResource`). It is **not** a copy of the amethyst tree — the vast majority of its keys are commons-only; only a small handful overlap. Every diff/count/translate command below works on either tree by swapping the base path — **run the whole technique once per tree** and report them separately (each maps to its own Crowdin file, so the counts should reconcile against two different Crowdin UI numbers).
**Locale-qualifier caveat:** `commons` uses the same region-qualified locale dirs as amethyst for our four targets (`values-cs`, `values-de-rDE`, `values-sv-rSE`, `values-pt-rBR`), but the *full* set of locale dirs differs between trees. Enumerate `values-*` under each tree's own base rather than assuming they match.
@@ -37,7 +37,7 @@ The `commons` tree appeared when shared event-renderer composables were extracte
Detect name-overlap **and flag value mismatches** in one pass:
```bash
cdef=commons/src/commonMain/composeResources/values/strings.xml
cdef=commonsUI/src/commonMain/composeResources/values/strings.xml
adef=amethyst/src/main/res/values/strings.xml
comm -12 \
<(grep '<string name=' "$cdef" | sed 's/.*name="\([^"]*\)".*/\1/' | sort -u) \
@@ -54,7 +54,7 @@ Only `SAFE-COPY` keys may be copied verbatim. For `VALUE-DIFFERS`, translate the
**Whitespace-quote convention differs between trees.** Android string resources use surrounding double-quotes to preserve leading/trailing whitespace (`"replying to "`). The **commons Compose-resources tree does NOT use this convention** — it authors trailing/leading spaces raw and unquoted (`replying to `). So when copying/translating a commons string with edge whitespace, **match the commons source: raw spaces, no wrapping quotes.** (Mistake we made: we copied amethyst's quoted `"replying to "` into commons, where the quotes would render literally.) A quick check for stray quote-wrapping you introduced:
```bash
grep -nE '<string name="[^"]*">"' commons/src/commonMain/composeResources/values-*/strings.xml
grep -nE '<string name="[^"]*">"' commonsUI/src/commonMain/composeResources/values-*/strings.xml
# The commons English tree has zero quote-wrapped values — any hit in a locale file is almost certainly a bad copy from amethyst.
```
@@ -147,14 +147,14 @@ Default: amethyst/src/main/res/values/strings.xml
Target: amethyst/src/main/res/values-<locale>/strings.xml
# commons tree
Default: commons/src/commonMain/composeResources/values/strings.xml
Target: commons/src/commonMain/composeResources/values-<locale>/strings.xml
Default: commonsUI/src/commonMain/composeResources/values/strings.xml
Target: commonsUI/src/commonMain/composeResources/values-<locale>/strings.xml
```
A convenient way to run the whole technique twice is to loop over the two base dirs:
```bash
for base in amethyst/src/main/res commons/src/commonMain/composeResources; do
for base in amethyst/src/main/res commonsUI/src/commonMain/composeResources; do
echo "########## tree: $base ##########"
# ... run the diff/count/value-extraction commands with $base/values[...] ...
done
@@ -273,8 +273,8 @@ Flag and offer to fix:
# hardcode "1" (or other literal digits) instead of using a placeholder.
# Looks at default + all values-* locales, in BOTH resource trees.
for f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml \
commons/src/commonMain/composeResources/values/strings.xml \
commons/src/commonMain/composeResources/values-*/strings.xml; do
commonsUI/src/commonMain/composeResources/values/strings.xml \
commonsUI/src/commonMain/composeResources/values-*/strings.xml; do
awk -v file="$f" '
/<plurals/ { in_plurals = 1; name = $0; sub(/.*name="/, "", name); sub(/".*/, "", name) }
in_plurals && /quantity="one"/ {
@@ -294,8 +294,8 @@ Then scan for dead `quantity="zero"` entries. CLDR's `zero` category is integer-
```bash
for f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml \
commons/src/commonMain/composeResources/values/strings.xml \
commons/src/commonMain/composeResources/values-*/strings.xml; do
commonsUI/src/commonMain/composeResources/values/strings.xml \
commonsUI/src/commonMain/composeResources/values-*/strings.xml; do
# Skip Arabic, Latvian and Welsh — they natively use the zero category.
# (Latvian's zero covers 0, 10, 11-19, 20, 30, … — stripping it breaks most counts.)
case "$f" in
@@ -328,7 +328,7 @@ itre = re.compile(r'<item quantity="([^"]+)"[^>]*>(.*?)</item>', re.S)
# (?<!\\) is REQUIRED: \%2$d is an escaped literal, not a placeholder.
phre = re.compile(r'(?<!\\)%(?:(\d+)\$)?([sdf])')
sig = lambda t: sorted(m.group(0) for m in phre.finditer(t))
for base in ['amethyst/src/main/res', 'commons/src/commonMain/composeResources']:
for base in ['amethyst/src/main/res', 'commonsUI/src/commonMain/composeResources']:
d = io.open(f'{base}/values/strings.xml', encoding='utf-8').read()
dstr = {m.group(1): sig(m.group(2)) for m in keyre.finditer(d)}
dpl = {}
@@ -354,7 +354,7 @@ PY
# Empty plural items render as nothing at runtime — always a bug.
grep -rn '<item quantity="[a-z]*"></item>' \
amethyst/src/main/res/values*/strings.xml \
commons/src/commonMain/composeResources/values*/strings.xml
commonsUI/src/commonMain/composeResources/values*/strings.xml
```
Three things this scan taught us, all of which it now encodes:
@@ -543,7 +543,7 @@ When adding translated strings to locale files:
## Common Mistakes
- **Scanning only the amethyst tree** — there are now **two** Crowdin-managed `strings.xml` trees (`amethyst/src/main/res` and `commons/src/commonMain/composeResources`). A key extracted into `commons/` will never show up in the amethyst diff. Run the whole technique once per tree (see "Resource trees") and report each separately.
- **Scanning only the amethyst tree** — there are now **two** Crowdin-managed `strings.xml` trees (`amethyst/src/main/res` and `commonsUI/src/commonMain/composeResources`). A key extracted into `commonsUI/` will never show up in the amethyst diff. Run the whole technique once per tree (see "Resource trees") and report each separately.
- **Copying an overlapping `commons` translation by key name alone** — a shared key name does NOT mean shared English. `napplet_card_permissions` is "What it can access" in commons but "Permissions:" in amethyst; copying by name produced the wrong string. Diff the English *values* first; copy verbatim only when they're byte-identical, else translate fresh (see "Overlap" in Resource trees).
- **Applying amethyst's `"…"` whitespace-quote convention to a commons string** — the commons Compose-resources tree authors edge whitespace raw and unquoted; wrapping quotes copied from amethyst render literally there. Match the commons source format.
- **Trying to "dedupe" the amethyst↔commons value-overlap** — it's required architecture (commons can't depend on amethyst, so shared composables need their own `Res.string` catalog), not an error. Don't fold consolidation into a translation pass.
+1 -1
View File
@@ -383,7 +383,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns |
| **ViewModels** | **commons/commonMain/viewmodels/** | **StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible** |
| UI formatters (pure) | commons/commonMain | Reusable, no dependencies |
| UI components (simple) | commons/commonMain | Cards, buttons, dialogs |
| UI components (simple) | commonsUI/commonMain | Cards, buttons, dialogs (Compose UI never goes in `commons`) |
| **Screen layouts** | **Platform-specific** | **Window vs Activity, sidebar vs bottom nav** |
| Navigation | Platform-specific only | Activity vs Window too different |
| Permissions | Platform-specific only | APIs incompatible |
+1 -1
View File
@@ -17,7 +17,7 @@ The layer between `LocalCache`/`Account` and the raw relay connection. Ensures c
## Layout
All under `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/`:
All under `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/` (the `@Composable` entry points — `observeUser*`, `*FilterAssemblerSubscription`, `KeyDataSourceSubscription` — sit in the same package but in `commonsUI/src/commonMain/…`, the Compose half of the shared layer):
```
relayClient/
+18 -3
View File
@@ -45,7 +45,16 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Linter (gradle)
run: ./gradlew spotlessCheck :quartz:verifyKmpPurity :commons:verifyKmpPurity
# The three metadata compiles resolve commonMain against only the
# deps every target shares, which is the Apple classpath — a
# dependency that reaches JVM transitively (okio via OkHttp) but is
# missing for iOS fails here, on Linux, instead of in test-quartz-ios.
run: |
./gradlew spotlessCheck \
:quartz:verifyKmpPurity :commons:verifyKmpPurity :commonsUI:verifyKmpPurity \
:quartz:compileCommonMainKotlinMetadata \
:commons:compileCommonMainKotlinMetadata \
:commonsUI:compileCommonMainKotlinMetadata
build-desktop:
needs: lint
@@ -93,7 +102,7 @@ jobs:
- name: Test + Build Desktop (gradle)
run: |
CMD="./gradlew :quartz:jvmTest :commons:jvmTest :nestsClient:jvmTest :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}"
CMD="./gradlew :quartz:jvmTest :commons:jvmTest :commonsUI:jvmTest :nestsClient:jvmTest :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}"
if [ "${{ runner.os }}" = "Linux" ]; then
xvfb-run --auto-servernum $CMD
else
@@ -129,6 +138,7 @@ jobs:
path: |
quartz/build/reports/tests
commons/build/reports/tests
commonsUI/build/reports/tests
nestsClient/build/reports/tests
cli/build/reports/tests
desktopApp/build/reports/tests
@@ -307,11 +317,15 @@ jobs:
# :commons:jvmTest stays green — this is the job that catches it.
# - compileTestKotlinIosArm64 catches device-only compile drift
# (iosArm64 = aarch64-apple-ios) without needing a physical device.
# :commonsUI (the Compose half split out of :commons) gets the same
# treatment so the shared composables keep compiling on Apple targets.
- name: Test Commons on iOS
run: |
./gradlew \
:commons:iosSimulatorArm64Test \
:commons:compileTestKotlinIosArm64
:commons:compileTestKotlinIosArm64 \
:commonsUI:iosSimulatorArm64Test \
:commonsUI:compileTestKotlinIosArm64
- name: Upload iOS Test Reports
uses: actions/upload-artifact@v7
@@ -363,6 +377,7 @@ jobs:
:amethyst:lintPlayBenchmark \
:quartz:jvmTest \
:commons:jvmTest \
:commonsUI:jvmTest \
:nestsClient:jvmTest \
:amethyst:testFdroidDebugUnitTest \
:amethyst:testPlayDebugUnitTest \
+9 -9
View File
@@ -568,22 +568,22 @@ jobs:
( cd "$SRC" && tar czf "$OLDPWD/dist/amy-${VER}-jvm.tar.gz" bin lib )
echo "Collected: dist/amy-${VER}-jvm.tar.gz"
- name: Enforce CLI size budget (200 MB per asset)
- name: Enforce CLI size budget (120 MB per asset)
run: |
set -euo pipefail
# The plan at cli/plans/2026-04-21-cli-distribution.md §size-budget
# targets < 80 MB, but :commons currently leaks Compose + Skiko as
# transitive deps (~40 MB of unused UI jars). Budget is set to
# 200 MB until commons is split into core + ui modules — track that
# as a follow-up. Until then, this gate just catches pathological
# regressions (e.g. accidental :amethyst dep pulling Android libs).
# Measured after the :commons / :commonsUI split (1.15.2, Linux x64):
# amy-*-jvm.tar.gz 55 MB, amy-*.tar.gz (jlink image) 80 MB, lib/ 60 MB
# on disk. The budget sits 50% above the largest asset so a Compose /
# Skiko / Android leak (+25-40 MB compressed) trips it, while the
# per-OS JRE variance of the jlink image does not. The "Assert no
# Compose UI" step above is the precise check; this is the coarse one.
fail=0
for f in dist/*; do
if [[ -f "$f" ]]; then
size=$(wc -c < "$f")
mb=$(( size / 1048576 ))
if (( size > 209715200 )); then
echo "::error file=$f::asset is ${mb} MB — exceeds 200 MB amy budget"
if (( size > 125829120 )); then
echo "::error file=$f::asset is ${mb} MB — exceeds 120 MB amy budget"
fail=1
else
echo "OK: $f — ${mb} MB"
+4 -4
View File
@@ -61,7 +61,7 @@ jobs:
# Both files in crowdin.yml are declared `type: android`, so Crowdin's Android
# serializer escapes apostrophes on the way down: `l'URL` comes back as `l\'URL`.
# That is correct for amethyst/src/main/res/, which aapt un-escapes at build time,
# and WRONG for commons/.../composeResources/, where Compose resolves only \uXXXX,
# and WRONG for commonsUI/.../composeResources/, where Compose resolves only \uXXXX,
# \n and \t and leaves \' \" \? \@ alone -- so the backslash reaches the screen.
#
# Without this step every sync reopens the same regression and CI's
@@ -76,14 +76,14 @@ jobs:
- name: Convert Android escaping to Compose escaping in the shared catalog
run: |
python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \
commons/src/commonMain/composeResources
commonsUI/src/commonMain/composeResources
# Assert the conversion actually satisfied the check that guards main, so a case
# the converter cannot repair fails the sync loudly here instead of opening a red
# PR. Known gap if this ever trips: fix_escapes.py only rewrites text inside
# <string>/<item> elements, while the check scans the whole file -- an escape in an
# XML comment (comments do propagate into the locale files) has to be fixed at the
# source string in commons/.../composeResources/values/strings.xml by hand.
# source string in commonsUI/.../composeResources/values/strings.xml by hand.
- name: Verify the shared catalog is free of Android-only escaping
run: .claude/hooks/compose_escaping_check.py
@@ -107,7 +107,7 @@ jobs:
branch: l10n_crowdin_translations
add-paths: |
amethyst/src/main/res/**/strings.xml
commons/src/commonMain/composeResources/**/strings.xml
commonsUI/src/commonMain/composeResources/**/strings.xml
docs/changelog/translators.json
commit-message: 'chore: sync Crowdin translations and seed translator npub placeholders'
title: 'New Crowdin Translations'
+8 -6
View File
@@ -96,7 +96,7 @@ and each has its own guide:
| Artifact | Committed at | Regenerate when | Guide |
|---|---|---|---|
| **Material Symbols subset font** | `commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` |
| **Material Symbols subset font** | `commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` |
| **Arti (Tor) native libs** | `amethyst/src/main/jniLibs/*.so` | You update the pinned Arti version, change the JNI wrapper, or want to reproduce the binaries | [`tools/arti-build/README.md`](tools/arti-build/README.md) |
> **Material Symbols is mandatory after icon changes.** The bundled font is a
@@ -466,8 +466,8 @@ Homebrew removes the quarantine attribute on its own downloads.
> with `dry_run=true` — the sign+notarize step runs regardless of `dry_run` and
> now prints the per-file notary log on a non-`Accepted` verdict. If it comes
> back `Invalid`, the fix is to codesign the dylibs *inside* those jars before
> zipping (and/or strip the unused `skiko`/Compose jars from the CLI image the
> `:commons` core/ui split the size budget already flags). The **desktop** app
> zipping (the unused `skiko`/Compose jars left the CLI image with the
> `:commons` / `:commonsUI` split). The **desktop** app
> bundles the same jars through Compose/jpackage notarization, so run a desktop
> dry-run too; its in-jar handling differs and is likewise unverified.
@@ -686,9 +686,11 @@ Caveats that the maintainer must weigh before submitting:
- **Pre-built-jar scrutiny.** homebrew-core prefers source builds; downloading
a jar bundle is an accepted-but-reviewed pattern for JVM tools. Be ready to
justify it (sandboxed Gradle can't fetch Maven deps).
- **Bundle size.** The bundle is ~70 MB today because `:commons` leaks
Compose/Skiko jars onto the CLI classpath. Trimming that (a `:commons`
core/ui split) would shrink it and smooth review — tracked as a follow-up.
- **Bundle size.** The bundle used to be ~70 MB because `:commons` leaked
Compose/Skiko jars onto the CLI classpath. Compose UI now lives in
`:commonsUI`, which `:cli` does not depend on: the JVM tarball is ~55 MB
and the jlink image tarball ~80 MB (1.15.2, Linux x64). The release
workflow caps every amy asset at 120 MB.
After the formula merges, the `livecheck` block lets homebrew-core's BrewTestBot
auto-open version-bump PRs on each stable release — no token or workflow on our
+7 -3
View File
@@ -175,9 +175,13 @@ device. PRs that introduce any of them will be sent back.
### KMP source-set discipline
- **Android-only imports don't belong in `commons/commonMain` or
`quartz/commonMain`.** Use `expect`/`actual` for platform-specific
bits, or move the Android-specific code to `androidMain`.
- **Android-only imports don't belong in `commons/commonMain`,
`commonsUI/commonMain` or `quartz/commonMain`.** Use `expect`/`actual`
for platform-specific bits, or move the Android-specific code to
`androidMain`.
- **Compose UI (`ui`/`foundation`/`material3`), Coil and `Res` don't belong
in `commons` at all** — that module is on the CLI classpath. Put the file
in `commonsUI` (same package) instead.
### Logging
+8 -5
View File
@@ -3,7 +3,7 @@
Thanks for your interest in improving Amethyst. This document captures the
expectations, conventions, and review rules for code, documentation, and
translation contributions across all modules in this repository (`amethyst/`,
`desktopApp/`, `quartz/`, `commons/`, `cli/`, `quic/`, `nestsClient/`).
`desktopApp/`, `quartz/`, `commons/`, `commonsUI/`, `cli/`, `quic/`, `nestsClient/`).
By contributing, you agree to license your work under the MIT license. Any
work contributed where you are not the original author must contain its
@@ -157,7 +157,8 @@ Common Gradle entry points:
Modules:
- `quartz/` — Nostr KMP library (protocol, crypto, models). **No UI.**
- `commons/` — Shared Compose Multiplatform UI, icons, ViewModels, flows.
- `commons/` — Shared headless layer: models, ViewModels, flows, relay client. **No Compose UI** (the CLI depends on it).
- `commonsUI/` — Shared Compose Multiplatform UI, icons, theme, Compose resources, on top of `commons`.
- `quic/` — Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport.
- `nestsClient/` — Audio-rooms client (NIP-53) built on `:quic` and
`:quartz`.
@@ -175,7 +176,8 @@ of PR churn. Place new code by purpose:
| What you're adding | Goes in |
|---|---|
| Nostr event types, NIPs, tags, signing, crypto, Bech32 | `quartz/commonMain/` |
| Shared Composables, icons, ViewModels, StateFlows | `commons/commonMain/viewmodels/` or `commons/commonMain/` |
| Shared ViewModels, StateFlows, relay subscriptions | `commons/commonMain/viewmodels/` or `commons/commonMain/` |
| Shared Composables, icons, theme | `commonsUI/commonMain/` (same packages as `commons`) |
| Android-only screen, navigation, system integration | `amethyst/` |
| Desktop-only window, sidebar, menu bar, shortcut | `desktopApp/` |
| `amy <verb>` subcommand (thin assembly only) | `cli/src/main/kotlin/.../cli/` |
@@ -188,8 +190,9 @@ Hard rules:
- `cli/` has **no Nostr protocol or business logic** — it's a thin assembly
layer over `quartz` + `commons`. If your CLI command needs new behavior,
extract it into `commons/` first.
- ViewModels belong in `commons/commonMain/`. Only screens (the Composable
that wires layout + navigation) stay in the platform module.
- ViewModels belong in `commons/commonMain/`; shared composables in
`commonsUI/commonMain/`. Only screens (the Composable that wires layout +
navigation) stay in the platform module.
- For platform-specific behavior in a shared file, use `expect`/`actual`.
## Workflow
+1
View File
@@ -405,6 +405,7 @@ dependencies {
implementation(project(":quartz"))
implementation(project(":commons"))
implementation(project(":commonsUI"))
implementation(project(":nestsClient"))
// Agent text stream previews: the raw-QUIC binding plus the QUIC
// stack under it (for the certificate validator it requires).
@@ -184,6 +184,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
@@ -647,6 +648,41 @@ class AppModules(
onionCache = onionLocationCache,
)
// Drops pooled connections once per real Tor route change. When the user switches
// Tor on, the direct clients' idle sockets to real hosts would otherwise sit in the
// pool for its 5-minute keepalive after the user has asked for everything to go
// through Tor. No request could use them either way -- OkHttp keys the pool by
// `Address`, which includes the proxy, so a connection on a dead route is already
// unreachable -- which is why this is hygiene and not correctness, and why it is
// fine for it to be a little late.
//
// Every source here is a plain StateFlow, so subscribing costs nothing. Deliberately
// NOT torManager.activePortOrNull: that chains to TorManager.status, whose upstream
// is WhileSubscribed and calls service.start() when collected, so a process-lifetime
// subscription there would hold Arti's control flow open forever -- the same hazard
// the battery ledger above documents and sidesteps the same way.
//
// Also deliberately not the per-feature Tor switches (imagesViaTor, videosViaTor, ...):
// those change which of the two existing clients a request picks, not the route either
// one uses, so no pooled connection goes stale.
init {
applicationIOScope.launch {
combine(
torPrefs.torType,
torPrefs.externalSocksPort,
torService.status.map { it.socksPort },
) { torType, externalPort, artiPort -> Triple(torType, externalPort, artiPort) }
.distinctUntilChanged()
// Only later moves count; the route in force at process construction is the
// status quo, and nothing is pooled yet to evict.
.drop(1)
.collect {
okHttpClients.factory.evictPooledConnections()
okHttpClientForRelays.factory.evictPooledConnections()
}
}
}
// Connects the INostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder(
@@ -320,6 +320,14 @@ class NotificationRelayService : Service() {
}
launch {
// This flow used to over-report: it is fed by socket callbacks, and until
// the OkHttp adapters answered a relay's CLOSE frame a relay-initiated close
// produced none (no onClosed, no onFailure, and a silent cancel()
// afterwards), so after the feeds tore down in the background it carried
// hundreds of already-dropped relays for minutes. The pool now clears it
// itself whenever it lets a relay go, and every transport reports its
// session end exactly once (see WebSocket), so what it emits is the count.
//
// sample() caps how often we touch the notification. During feed
// load/teardown connectedRelaysFlow churns dozens of times per second;
// posting on every delta blows past Android's notification rate limit
@@ -34,6 +34,7 @@ import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.util.concurrent.atomic.AtomicBoolean
class OkHttpWebSocket(
val url: NormalizedRelayUrl,
@@ -41,7 +42,17 @@ class OkHttpWebSocket(
val out: WebSocketListener,
) : WebSocket {
private var usingOkHttp: OkHttpClient? = null
private var socket: okhttp3.WebSocket? = null
@Volatile private var socket: okhttp3.WebSocket? = null
/**
* Set once, by whichever of `onClosed`, `onFailure` or [disconnect] ends the session first.
* One adapter is one session (the relay client builds a fresh one per dial, and OkHttp binds
* exactly one socket to the listener), so a callback only has to ask whether the session
* already ended. See quartz's `BasicOkHttpWebSocket` for the full reasoning; the two adapters
* differ only in how [needsReconnect] is decided.
*/
private val ended = AtomicBoolean(false)
fun buildRequest() = Request.Builder().url(url.url).build()
@@ -68,8 +79,10 @@ class OkHttpWebSocket(
}
override fun connect() {
usingOkHttp = httpClient(url)
socket = usingOkHttp?.newWebSocket(buildRequest(), OkHttpWebsocketListener(out))
if (socket != null || ended.get()) return
val client = httpClient(url)
usingOkHttp = client
socket = client.newWebSocket(buildRequest(), OkHttpWebsocketListener(out))
}
inner class OkHttpWebsocketListener(
@@ -91,35 +104,60 @@ class OkHttpWebSocket(
}
}
/** Claims the session's single terminal report. False if it already ended. */
private fun endSession(): Boolean {
if (!ended.compareAndSet(false, true)) return false
socket = null
incomingMessages.close()
job.cancel()
scope.cancel()
return true
}
override fun onOpen(
webSocket: okhttp3.WebSocket,
response: Response,
) = out.onOpen(
(response.receivedResponseAtMillis - response.sentRequestAtMillis).toInt(),
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
)
) {
if (ended.get()) return
out.onOpen(
(response.receivedResponseAtMillis - response.sentRequestAtMillis).toInt(),
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
)
}
override fun onMessage(
webSocket: okhttp3.WebSocket,
text: String,
) {
// Asynchronously send the received message to the channel.
// `trySendBlocking` is used here for simplicity within the callback,
// but it's important to understand potential thread blocking if the buffer is full.
if (ended.get()) return
// Never blocks (unlimited channel): the OkHttp reader thread must
// stay free to keep draining the socket.
incomingMessages.trySendBlocking(text)
}
override fun onClosing(
webSocket: okhttp3.WebSocket,
code: Int,
reason: String,
) {
// The relay sent a CLOSE frame. OkHttp fires onClosed only once BOTH peers have sent
// one, and sending ours is the application's job (WebSocketListener KDoc; its own
// WebSocketEcho recipe does exactly this). Unanswered, the socket sat half-closed:
// no onClosed, no onFailure, send() still accepted and discarded, a later cancel()
// silent too -- so the relay client believed it was connected until the 120s ping
// path failed up to two intervals later.
//
// Always 1000 rather than echoing `code`: close() validates the code it writes and
// throws on the reserved ones (1005, 1006, 1015), and a relay may send anything.
webSocket.close(1000, null)
}
override fun onClosed(
webSocket: okhttp3.WebSocket,
code: Int,
reason: String,
) {
// Close the channel on failure, and propagate the error.
incomingMessages.close()
job.cancel()
scope.cancel()
socket = null
if (!endSession()) return
out.onClosed(code, reason)
}
@@ -128,12 +166,7 @@ class OkHttpWebSocket(
t: Throwable,
response: Response?,
) {
// Close the channel on failure, and propagate the error.
incomingMessages.close()
job.cancel()
scope.cancel()
socket = null
if (!endSession()) return
out.onFailure(t, response?.code, response?.message)
}
}
@@ -153,9 +186,15 @@ class OkHttpWebSocket(
}
override fun disconnect() {
// uses cancel to kill the SEND stack that might be waiting
socket?.cancel()
// Claim the session ourselves and cancel (which also kills a SEND stack that might be
// waiting): OkHttp's cancel() raises no callback when no reader is left to fail, and when
// it does the failure arrives later on its own thread. The relay client needs the answer
// now, and must not hear from this socket again.
val closing = socket ?: return
if (!ended.compareAndSet(false, true)) return
socket = null
closing.cancel()
out.onClosed(1000, "client disconnect")
}
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
@@ -87,7 +87,7 @@ fun RenderTopButtonsPreview() {
ThemeComparisonColumn {
Box(Modifier.background(BitcoinOrange)) {
RenderTopButtons(
mediaData = MediaItemData("http://test.mp4"),
mediaData = MediaItemData("https://test.mp4"),
hasMultipleQualities = false,
qualityButton = {},
controllerVisible = remember { mutableStateOf(true) },
@@ -47,10 +47,13 @@ import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.gif
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font10SP
import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.Size6dp
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
@@ -72,6 +75,15 @@ fun GifVideoView(
// remember() to avoid the recompute would cost more (slot read + N equality checks)
// than the work it saves; that's why this stays as a plain expression.
val ratio = dimensions?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(videoUri)
// Mirrors [mediaSizingModifier]: Crop gets fillMaxSize() and a known ratio gets
// aspectRatio(), both of which bound the height. Everything else is a bare
// fillMaxWidth() that wraps its content, and only THAT case needs a loading state
// with intrinsic height to keep the note from collapsing to nothing. Keying the
// fallback on `ratio` alone would put raw URL text inside every Crop card cell --
// MyAsyncImage passes dimensions/blurhash/thumbhash all null, so a gif in a card
// slot hits this on first load, before MediaAspectRatioCache knows its size.
val heightIsBounded = contentScale == ContentScale.Crop || ratio != null
val autoPlay = accountViewModel.settings.autoPlayVideos()
val borderModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier
val context = LocalContext.current
@@ -108,13 +120,29 @@ fun GifVideoView(
when (state) {
is AsyncImagePainter.State.Loading -> {
DisplayBlurHash(
blurhash,
contentDescription,
contentScale,
Modifier.fillMaxSize(),
thumbhash = thumbhash,
)
// When the height is unbounded (see [heightIsBounded]) this branch MUST
// emit something with an intrinsic height, or the box wraps nothing and the
// whole note collapses to zero -- no picture, no URL, no spinner, just a gap
// in the feed until the load finishes. DisplayBlurHash renders NOTHING when
// both hashes are absent (placeholderModel returns null), which is exactly
// what a no-imeta post hits. Mirrors UrlImageView's ladder.
if (blurhash != null || thumbhash != null) {
DisplayBlurHash(
blurhash,
contentDescription,
contentScale,
Modifier.fillMaxSize(),
thumbhash = thumbhash,
)
} else if (heightIsBounded) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LoadingAnimation(Size40dp, Size6dp)
}
} else {
WaitAndDisplay {
DisplayUrlWithLoadingSymbol(videoUri)
}
}
}
is AsyncImagePainter.State.Success -> {
@@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.commons.feeds.AdditiveComplexFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.service.BundledInsert
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveComplexFeedFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.utils.Log
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
@@ -28,8 +28,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -34,6 +34,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -36,7 +36,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent
@Composable
fun WatchLifecycleAndUpdateModel(model: InvalidatableContent) {
@@ -29,8 +29,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -61,8 +61,10 @@ 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.concord.ConcordChannel
import com.vitorpamplona.amethyst.commons.model.isCommunityDefinition
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo
import com.vitorpamplona.amethyst.commons.model.textNoteModifications
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.approve
@@ -78,8 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.QuietMark
import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint
import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint
import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation
import com.vitorpamplona.amethyst.commons.ui.note.isCommunityDefinition
import com.vitorpamplona.amethyst.commons.ui.note.replyingDirectlyTo
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
@@ -46,7 +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.nip64Chess.ChessChallenge
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessGameViewer
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessGameViewer
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chess_accept
import com.vitorpamplona.amethyst.commons.resources.chess_decline
@@ -35,10 +35,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.note.ReplyToLabel
import com.vitorpamplona.amethyst.commons.ui.note.replyingDirectlyTo
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@@ -34,8 +34,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.ui.note.replyingDirectlyTo
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel
import com.vitorpamplona.amethyst.model.LocalCache
@@ -28,8 +28,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
@@ -25,10 +25,10 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
@@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.ComponentCallbacks2
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
@@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
import com.vitorpamplona.amethyst.commons.cashu.ops.describeMintError
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Dao
import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers
@@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuild
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
import com.vitorpamplona.amethyst.commons.tor.TorType
import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState
import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache
import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCacheAsync
@@ -21,10 +21,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.topNavFeeds.TopNavFeedQueryState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import kotlinx.coroutines.flow.StateFlow
/**
@@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
class BookmarkPrivateFeedFilter(
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
class BookmarkPublicFeedFilter(
@@ -214,7 +214,7 @@ private fun Picture(
onValueChange = { bookmarkGroupInfoViewModel.picture.value = it },
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
text = "https://mygroup.com/logo.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
class OldBookmarkPrivateFeedFilter(
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
class OldBookmarkPublicFeedFilter(
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.podcasts.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.isPodcastEvent
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.repositories.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
/**
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
@@ -47,6 +47,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Note
@@ -56,8 +58,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_collection_count
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_subtitle
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title
import com.vitorpamplona.amethyst.commons.resources.calendar_export_event
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
@@ -51,6 +51,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.calendarLocalDayKeyRange
@@ -63,8 +65,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_day_subtitle
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_day_title
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_day
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_day
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -38,6 +38,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.calendarEndSeconds
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.calendarStartSeconds
@@ -46,8 +48,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_feed_subtitle
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_feed_title
import com.vitorpamplona.amethyst.commons.resources.calendar_section_past
import com.vitorpamplona.amethyst.commons.resources.calendar_section_upcoming
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
@@ -53,6 +53,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.MONTH_GRID_MAX_LANES
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.MonthGridBarSegment
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.computeMonthGridBars
@@ -62,8 +64,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_day_a11y_selected_s
import com.vitorpamplona.amethyst.commons.resources.calendar_day_a11y_today_suffix
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_month
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_month
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -51,6 +51,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpanded
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -60,8 +62,6 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_week_subtitle
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_week_title
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_week
import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_week
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -31,7 +31,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.upcomingFirstCalendarOrder
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -36,9 +36,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.commons.ui.feeds.MarmotGroupFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.MarmotGroupFeedFilter
import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -38,13 +38,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04
import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip17
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.commons.ui.feeds.ChatroomFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.ChatroomFeedFilter
import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -54,14 +54,14 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.concord.ConcordCommunitySession
import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.concord_dissolved_read_only
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.ChangesFlowFilter
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.ChangesFlowFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.isMinichatReply
@@ -285,7 +285,7 @@ private fun Picture(
onValueChange = { postViewModel.channelPicture.value = it },
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
text = "https://mygroup.com/logo.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -39,6 +39,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
@@ -46,8 +48,6 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.relay_group_invite_only_to_post
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_to_post
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels
@@ -56,6 +56,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Note
@@ -68,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_browse_title
import com.vitorpamplona.amethyst.commons.resources.relay_group_favorite_relay
import com.vitorpamplona.amethyst.commons.resources.relay_group_message_count_short_capped
import com.vitorpamplona.amethyst.commons.resources.select_list_to_filter
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.model.nip11RelayInfo.WarmNip11
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
@@ -20,12 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.GroupDiscoveryConstraint
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel
@@ -28,7 +29,6 @@ import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
import com.vitorpamplona.amethyst.commons.model.publicChatChannelIdOf
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.util.replace
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.util.replace
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -38,6 +38,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus
@@ -45,8 +47,6 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04
import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip17
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
@@ -40,6 +40,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -47,7 +48,6 @@ import com.vitorpamplona.amethyst.commons.resources.mark_all_as_read
import com.vitorpamplona.amethyst.commons.resources.mark_all_known_as_read
import com.vitorpamplona.amethyst.commons.resources.mark_all_new_as_read
import com.vitorpamplona.amethyst.commons.resources.mark_as_read_dialog_title
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
@@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
@@ -27,7 +27,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -35,7 +35,7 @@ import com.google.accompanist.adaptive.FoldAwareConfiguration
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
@@ -35,10 +35,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.share_to_dm_start_new
import com.vitorpamplona.amethyst.commons.resources.share_to_dm_title
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -61,10 +61,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.LiveChessGameScreen
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessBroadcastBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessSyncBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.LiveChessGameScreen
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chess_game_id
import com.vitorpamplona.amethyst.commons.resources.chess_game_not_found
@@ -62,16 +62,16 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.nip64Chess.ActiveGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ChallengeCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.CompletedGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.NewChessGameDialog
import com.vitorpamplona.amethyst.commons.nip64Chess.OutgoingChallengeCard
import com.vitorpamplona.amethyst.commons.nip64Chess.OverlappingAvatars
import com.vitorpamplona.amethyst.commons.nip64Chess.PublicGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.SpectatingGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ActiveGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChallengeCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessSyncBanner
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.CompletedGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.NewChessGameDialog
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.OutgoingChallengeCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.OverlappingAvatars
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.PublicGameCard
import com.vitorpamplona.amethyst.commons.nip64Chess.ui.SpectatingGameCard
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.dismiss
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
@@ -32,8 +32,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
@@ -51,10 +51,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter
@@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.mapNotNullIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
@@ -30,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -42,6 +42,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -49,8 +51,6 @@ import com.vitorpamplona.amethyst.commons.resources.delete_all
import com.vitorpamplona.amethyst.commons.resources.delete_all_drafts_confirmation
import com.vitorpamplona.amethyst.commons.resources.no
import com.vitorpamplona.amethyst.commons.resources.yes
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteWithConfirmation
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -35,9 +35,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,13 +20,13 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -20,13 +20,13 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.ui.feeds.isRenderableRepost
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams

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