mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge main into the preference-layer migration
Resolves the one conflict, in CalendarReminderSettingsScreen's imports. Main (#4189) moved INav, TopBarWithBackButton, stringRes and pluralStringRes into `commons.ui`; this branch had swapped that screen's CalendarReminderPrefs for the DataStore-backed calendarReminderSettings(). The two changes are orthogonal, so the resolution takes main's package moves and keeps this branch's store, dropping the CalendarReminderPrefs import the store replaced. Also repoints a KDoc link in CalendarReminderWorker that main wrote against CalendarReminderStore, which this branch renamed to CalendarReminderLogStore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
@@ -58,10 +58,10 @@ amethyst/ # Android app module
|
||||
│ │ │ │ ├── MainActivity.kt # Entry point
|
||||
│ │ │ │ ├── navigation/
|
||||
│ │ │ │ │ ├── AppNavigation.kt # NavHost
|
||||
│ │ │ │ │ ├── routes/Routes.kt # @Serializable routes
|
||||
│ │ │ │ │ ├── routes/RouteNavController.kt # NavHost-bound route helpers
|
||||
│ │ │ │ │ └── bottombars/AppBottomBar.kt
|
||||
│ │ │ │ ├── screen/ # 80+ screens
|
||||
│ │ │ │ └── theme/Theme.kt # Material3 theme
|
||||
│ │ │ │ └── theme/Theme.kt # AmethystTheme (accent, prefs, window insets)
|
||||
│ │ │ └── Amethyst.kt # Application class
|
||||
│ │ └── AndroidManifest.xml # Permissions, intent filters
|
||||
│ └── androidMain/ # KMP Android source set
|
||||
@@ -69,6 +69,12 @@ amethyst/ # Android app module
|
||||
└── build.gradle # Android config
|
||||
```
|
||||
|
||||
The `@Serializable` `Route` catalog is headless and lives in
|
||||
`commons/.../commons/model/navigation/Routes.kt`; `INav`/`EmptyNav` and the top bars are
|
||||
in `commonsUI/.../commons/ui/navigation/`. The palettes, `ColorScheme.*` tokens, sizes and
|
||||
typography are in `commonsUI/.../commons/ui/theme/` (markdown styles in its `jvmAndroid`).
|
||||
Add a route to the commons `Routes.kt`; wire its destination in the app's `AppNavigation.kt`.
|
||||
|
||||
## 1. Type-Safe Navigation
|
||||
|
||||
### Pattern: @Serializable Routes
|
||||
@@ -953,9 +959,11 @@ fun testPermissionRequest() {
|
||||
|
||||
**Key Android Files:**
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt`
|
||||
- `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt` (route catalog)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/INav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt` (`AmethystTheme`)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystColorScheme.kt` (palettes + `ColorScheme.*` tokens)
|
||||
- `amethyst/src/main/AndroidManifest.xml`
|
||||
- `amethyst/build.gradle`
|
||||
|
||||
|
||||
@@ -608,7 +608,9 @@ fun testNavigationToProfile() {
|
||||
|
||||
## File Locations
|
||||
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt`
|
||||
- `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt` (route catalog)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/INav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteNavController.kt` (NavHost helpers)
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Nav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt`
|
||||
|
||||
@@ -0,0 +1,900 @@
|
||||
# DECK-0003 (SNO — Simple Nostr Objects) — feasibility and implementation plan
|
||||
|
||||
_Status: **built** on `claude/epic-ptolemy-48n37b`. The reader, the rasterizer,
|
||||
the feed/thread rendering and kind-11333 avatars all ship; §6 records what
|
||||
landed and what did not._
|
||||
|
||||
Source: <https://github.com/arkin0x/cyberspace/blob/master/decks/DECK-0003-sno.md>
|
||||
(Draft, last updated 2026-09-16, requires `CYBERSPACE_V2.md` `2026-03-16-h34-corrected`).
|
||||
Two implementations to check against, and they do not fully agree (§2.6):
|
||||
`decks/sno-reference.py` in the same repo — no dependencies, 25 rejection cases, runs
|
||||
clean — and [`arkin0x/sno-core`](https://github.com/arkin0x/sno-core) v0.1.4, the MIT
|
||||
TypeScript library that ONOSENDAI and the snocrash workshop both read objects with.
|
||||
The surrounding specs (`CYBERSPACE_V2.md` §7.6, §8.6, §8.10, §9.4, §11 and the other
|
||||
two DECKs) are covered in §2.
|
||||
|
||||
## 1. What the spec defines
|
||||
|
||||
A **Simple Nostr Object** is a small triangle mesh written as JSON, small enough to
|
||||
live in one event's `content`. Vertices sit on an integer lattice (whole model units
|
||||
plus 120ths), one palette index per vertex, optional triangles, one of three draw
|
||||
modes. No textures, no materials, no normals, no animation, no external file. The
|
||||
object *is* the event.
|
||||
|
||||
| Thing | Value |
|
||||
| ---- | ---- |
|
||||
| Standalone object | `kind 33331`, addressable, one per `(pubkey, d)` |
|
||||
| Object hidden in a Cyberspace bag | `kind 3330`, a `kind 33330` bag item |
|
||||
| Avatar | `kind 11333`, payload in content (CYBERSPACE_V2 §8.10) |
|
||||
| Palette published as its own event | any kind; `kind 3367` (color moments) is what carries them today |
|
||||
| Model space | X right, Y up, +Z toward the viewer — glTF/three.js, right handed |
|
||||
| Lattice | whole units + ticks (1/120 of a unit), exact rationals, never floats |
|
||||
| Hard limits | 512 vertices, 1024 faces |
|
||||
| `v` | `1` (Z negated on read, colors are literal `[r,g,b]` triples) or `2` (Z as written, colors are palette indices) |
|
||||
|
||||
The payload fields: `v`, `name`, `unit`, `mode`, `vertices`, `colors`, `faces`
|
||||
required; `extent`, `ticks`, `facecolors`, `palette`, `up`, `spin` optional; `type`
|
||||
a legacy field that MUST be ignored and MUST NOT be rejected on.
|
||||
|
||||
Three encodings are run-length: `ticks` (a negative `-N` means N vertices at
|
||||
`[0,0,0]`), `facecolors` (a negative `-N` repeats the previous index N more times).
|
||||
Both are "sign separates the two entry kinds", which works because an index is never
|
||||
negative.
|
||||
|
||||
§1.9 is the validation table — ten numbered rules, and `decks/sno-reference.py`
|
||||
carries a rejection case per rule. That table is the test suite; port it verbatim.
|
||||
|
||||
### Confirmed against the live network, not from memory
|
||||
|
||||
Queried `cyberspace.nostr1.com`, `relay.damus.io`, `nos.lol`, `relay.primal.net`
|
||||
on 2026-09-21 (nostr.band reset the connection):
|
||||
|
||||
```
|
||||
kind 33331 : 7 unique events, 6 distinct authors
|
||||
kind 3330 : 2 unique events
|
||||
kind 11333 : 0
|
||||
kind 3367 : 8 sampled palettes
|
||||
```
|
||||
|
||||
Running each 33331 payload through the reference validator:
|
||||
|
||||
| Event id (first 8) | Author | Result | verts/faces | `unit` | `mode` | event bytes | name |
|
||||
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
||||
| `fbe6a776` | `24bd64d4` | **rejected** | 4/4 | 0 | solid | 726 | hello world |
|
||||
| `4b05663f` | `1b71f696` | **rejected** | 4/4 | 0 | solid | 735 | second thought |
|
||||
| `04df8e12` | `e8ed3798` | **rejected** | 18/24 | 0 | lines | 1907 | Ship |
|
||||
| `89f510e1` | `dcf07605` | valid | 35/20 | 0 | solid | 1231 | Shard 4 copy |
|
||||
| `47031e0f` | `7e5dde75` | valid | 5/5 | 0 | solid | 684 | Shard 2 |
|
||||
| `d1299608` | `f7e15ec0` | valid | 118/94 | 0 | solid | 3248 | first object |
|
||||
| `8fe392c2` | `e8ed3798` | valid | 12/18 | 12 | solid | 981 | Triforce |
|
||||
|
||||
**Three of the seven objects on the network fail the spec's own validator.** All
|
||||
three fail on the same rule (8b): they are `v: 2` payloads whose `colors` are still
|
||||
`[r, g, b]` triples (`[1, 0.15, 0.15]`), and all three also carry `type: "shard"`.
|
||||
This is exactly the transition §5 documents — `colors` changed meaning inside `v: 2`
|
||||
on 2026-09-16 and the authors expected to reissue three objects. They have not, and
|
||||
one of the three (`04df8e12`, "Ship") was published by an author who *also* has a
|
||||
correctly-encoded object, so this is drift, not a single stale client.
|
||||
|
||||
§5 gives a reader explicit permission here: "a reader MAY reject one, and a reader
|
||||
that would rather be generous MAY read the triples literally the way §1.3 reads a
|
||||
`v: 1` payload's". **Take the generous branch** — otherwise Amethyst renders 4 of the
|
||||
7 objects that exist and shows an error for the rest. Decision D1 below.
|
||||
|
||||
Real payloads also already use `mode: "lines"` and a non-zero `unit` (12), so neither
|
||||
is a theoretical branch.
|
||||
|
||||
**The two `kind 3330` events on relays are v1 leftovers, and the current ones are
|
||||
unreachable.** Both live shards have **empty `content`** and carry their geometry in
|
||||
*tags*:
|
||||
|
||||
```
|
||||
C 3d5ffa91… (64-hex cyberspace coordinate)
|
||||
Cd 0
|
||||
S 13346173940832169-15655245699645789-31273450162226572
|
||||
ms 273
|
||||
version 1
|
||||
vertices (empty) colors (empty) indices (empty)
|
||||
position 251894327,617397300,844414457
|
||||
display solid nonce 0000000000000000 e cb5a78f7…
|
||||
```
|
||||
|
||||
That tag-encoded form is v1's and the DECK does not describe it. The current client
|
||||
does write the §1 JSON payload in `content` — `src/lib/hidden.ts` in ONOSENDAI is
|
||||
explicit ("a shard, kind 3330 (v1's shard kind), geometry in the content"), so §3.2
|
||||
is accurate about what is being produced today.
|
||||
|
||||
The reason to defer 3330 is better than a mismatch, and it is `CYBERSPACE_V2.md`
|
||||
§7.6 rather than the DECK. A current shard is not a relay event at all: it is an item
|
||||
inside a **bag**, a `kind 33330` event whose payload is AES-256-GCM ciphertext in an
|
||||
`encrypted` tag, keyed by the `location_decryption_key` of a region at a height
|
||||
(§7.2). Opening one means implementing the coordinate system (§2), the region key
|
||||
derivation (§7.2) and discovery scanning (§7.4) — the Cyberspace protocol, not a
|
||||
rendering feature. A reader without the key sees base64 and nothing else, and §7.6
|
||||
says a failed decryption "MUST NOT be treated as an error in the bag". Defer it —
|
||||
decision D2.
|
||||
|
||||
**Palette events match §1.3b exactly.** All sampled `kind 3367` events carry 4–6
|
||||
`c` tags of well-formed `#rrggbb` in document order, plus `layout` / `alt` / `client`
|
||||
/ `name`, with an emoji in `content`. The spec's survey holds; the `c`-tag reader is
|
||||
the only palette-event reader worth writing.
|
||||
|
||||
### Spec warts to handle defensively
|
||||
|
||||
1. **`type` is poison to reject on.** §1.1a is emphatic, and three live events carry
|
||||
it. Ignore it.
|
||||
2. **No hard bound on how far a vertex may lie from the origin** — §8 open question 3
|
||||
says so outright. "512 vertices at 2^50 units is valid under this text and will
|
||||
produce a grid no renderer wants." §1.8 puts the 64-unit bound on publishers and
|
||||
lets a reader "reject such a payload" or "repair it by growing the extent". Both
|
||||
references repair; so do we (see §4.2d). What is left is the lattice's own limit:
|
||||
a position is a total tick count in an Int.
|
||||
3. **`extent` is repaired, never validated** (§1.8). Out of range → `8`, then grow to
|
||||
contain the data. It is the one deliberately forgiving rule; a reader that rejects
|
||||
on it is wrong.
|
||||
4. **Winding order carries no meaning** (§1.4). Draw both sides, never cull.
|
||||
5. **A palette reference is never load-bearing** (§1.3b). Render immediately with the
|
||||
built-in; re-render if and when the referenced event arrives; never block, never
|
||||
reject. A reference is pinned to the event id it names — do not follow the `e`
|
||||
chain forward.
|
||||
6. **Welding/equality must happen on the integer pair**, before any float conversion
|
||||
(§1.2). 1/120 is not representable in binary floating point.
|
||||
7. **`name` is truncated to 64 characters, not rejected** (§1.8).
|
||||
8. **`spin` is validated even when `up` is absent** (§1.7) but ignored for rendering.
|
||||
|
||||
## 2. The surrounding specs, and the software that already reads this format
|
||||
|
||||
DECK-0003 does not stand alone. Reading the repository it lives in, and the two
|
||||
clients that implement it, changes three things in this plan.
|
||||
|
||||
### 2.1 What a DECK is, and which ones matter here
|
||||
|
||||
`decks/README.md` defines a DECK (Design Extension and Compatibility Kit) as a
|
||||
self-contained optional extension over `CYBERSPACE_V2.md`. A DECK MAY define new
|
||||
kinds and tags; it MUST NOT change consensus-critical base rules. Three exist:
|
||||
DECK-0001 (Hyperspace, Bitcoin block transit), DECK-0002 (Virtual Spawn, a game
|
||||
mechanic), DECK-0003 (SNO). **Neither 0001 nor 0002 touches geometry, objects or
|
||||
rendering** — grepped, zero hits. SNO is the only DECK Amethyst would implement.
|
||||
|
||||
DECK-0003 on `master` is the current revision: I fetched every pull-request head in
|
||||
the repo and none carries a newer DECK-0003 than `master` does. The only artifact of
|
||||
note is that SNO was DECK-**0004** in PR #25, renumbered in the same round that
|
||||
dropped the mandatory `type: "shard"` and turned colors into palette indices — which
|
||||
is precisely the change the three legacy events on the network predate.
|
||||
|
||||
### 2.2 `CYBERSPACE_V2.md` §7 — the bag, and what opening one would actually cost
|
||||
|
||||
Summarized in §1 above and it is the strongest argument in this plan: a current shard
|
||||
is an item inside an AES-256-GCM bag keyed to a region, not a fetchable event.
|
||||
|
||||
**How a bag is found is not what §7.4 makes it look like, and this plan said it
|
||||
wrong twice.** §7.4's discovery scanning — walk the aligned cubes around your own
|
||||
coordinate, derive each key, ask the relay — is the *opportunistic* half, and §7.7
|
||||
says outright what it is worth on its own: "a bag is found only by intentionally deep
|
||||
scanning and/or wandering. With no additional information, any given bag is equally
|
||||
likely to be at any point in the full 2^256 coordinate space: an impossibly hardened
|
||||
secret."
|
||||
|
||||
The mechanism that actually finds things is §7.7's **hint and sweep**, and the
|
||||
sentence that matters is this one:
|
||||
|
||||
> The seeker's own position never enters this cost, because §7.1 makes looking and
|
||||
> walking equivalent: a region key can be computed for any coordinate without
|
||||
> traveling there.
|
||||
|
||||
A hider publishes a `hint` tag naming an aligned box as coarse as they like; a seeker
|
||||
sweeps it, deriving a key per candidate region and batching the `lookup_id`s into one
|
||||
relay query. The price is `2^((Hx-h)+(Hy-h)+(Hz-h))` candidates and nothing else. At
|
||||
the degenerate end, "three heights equal to `h` name the region itself: the hint is
|
||||
then a destination the seeker can compute or walk to directly, not a search."
|
||||
|
||||
So **a client with no avatar and no position could open a hinted bag.** The reason
|
||||
this plan defers it is not that Amethyst has nowhere to stand — that was the wrong
|
||||
reason, stated in D2 and in `SnoShardEvent`'s KDoc — it is the cost and the surface.
|
||||
|
||||
**The cost, measured.** A region key is three Cantor axis roots at the bag's height,
|
||||
each an `O(2^h)` fold of BigInts that double in width every level (§4.6). The spec
|
||||
publishes its own figures in §7.7, one desktop core, per key: about 0.05 ms at
|
||||
heights 0–4, 1.3 ms at h8, 30 ms at h12, 816 ms at h16. Measured here against the
|
||||
same fold, per axis: 4 ms at h12, 92 ms at h16, 2.7 s at h20, 13 s at h22 with a
|
||||
GMP-class bignum — which puts the spec's numbers between a GMP build and a
|
||||
Karatsuba-only one, as it should. The growth is **~2.2x per height**, both in the
|
||||
spec's own figures and in the measurement, and it is 2.2 rather than 2 because a
|
||||
pairing doubles the leaves *and* the operand width. Extrapolating that ratio, the
|
||||
spec's human-scale h34 is on the order of days per key and a 174 GB root, against
|
||||
§9.3's stated 185 GB — which is why `cyberspace-core` caps at
|
||||
`DEFAULT_MAX_COMPUTE_HEIGHT = 20` and throws above it, and why ONOSENDAI sells the
|
||||
heights above a machine's own ceiling through a paid service.
|
||||
|
||||
**What that means for us.** Nothing changes about D2, but the reason is now the right
|
||||
one: opening bags means carrying §2's coordinates, §4's Cantor trees and §7.2's
|
||||
derivation, plus a sweep whose budget is somebody else's puzzle difficulty. That is a
|
||||
protocol stack with its own compute economics, and it is not a rendering feature.
|
||||
|
||||
### 2.3 `CYBERSPACE_V2.md` §8.10 — the avatar, and a proof of work that is normative
|
||||
|
||||
This is the one piece of surrounding spec that is directly implementable and that the
|
||||
DECK only gestures at. `kind 11333` is **replaceable** (one avatar per identity), its
|
||||
content is an SNO payload or empty, and the work it owes is computed from the payload:
|
||||
|
||||
```python
|
||||
AVATAR_FLOOR_BITS = 16; AVATAR_SIZE_BITS = 2; AVATAR_DETAIL_BITS = 3; AVATAR_DETAIL_FREE = 32
|
||||
reach = max(1.0, max(abs(v + t/120) for every vertex coordinate) * 2 ** payload.unit)
|
||||
detail = max(AVATAR_DETAIL_FREE, len(payload.vertices) + len(payload.faces))
|
||||
work = ceil(16 + 2*log2(reach) + 3*log2(detail/32))
|
||||
```
|
||||
|
||||
An avatar is **paid** when its NIP-13 `nonce` tag's committed `target` is at least
|
||||
`work`, **and** its id carries at least `target` leading zero bits. Both conditions,
|
||||
because committing the target before mining is what stops a lucky id being claimed
|
||||
against a lower bar. And then, normatively: *"a client MUST NOT draw an avatar event
|
||||
that is not paid, or that carries content it cannot read; it draws its default avatar
|
||||
for that identity instead."*
|
||||
|
||||
For us that is a short, self-contained addition on top of step 1, and Quartz already
|
||||
has `nip13Pow/` for the leading-zero-bit count — but **not** as a one-liner, and the
|
||||
shortcut is wrong. `Event.pow()` is `PoWRankEvaluator.compute(id, tags.commitedPoW())`,
|
||||
which returns `min(actualRank, committed)`, so `event.pow() >= avatarWork(payload)`
|
||||
passes an event whose id is short of its own commitment: work 16, committed 30, id
|
||||
carrying 20 gives `pow() == 20`, which clears 16 while failing §8.10's second
|
||||
condition. The check has to stay two conditions, read separately —
|
||||
`PoWTag.parseCommitment(...) >= avatarWork(payload)` **and**
|
||||
`PoWRankEvaluator.calculatePowRankOf(id) >= committed` — with a missing `nonce` tag
|
||||
meaning unpaid. It is also the only place in this
|
||||
whole surface where a rendering decision is gated on verification rather than taste.
|
||||
Note the kind history: `11333` **was** `kind 33331` with a `d` fixed at `"avatar"`,
|
||||
and 33331 was then handed to standalone SNO objects. A reader should not be surprised
|
||||
by an old 33331 whose `d` is literally `avatar`; none of the seven live ones is.
|
||||
|
||||
### 2.4 §9.4 and §11 — the axis conventions, which confirm the Z story
|
||||
|
||||
§11.1 fixes `+X_cs` screen-right, `+Y_cs` up, `+Z_cs` forward *toward the black sun*,
|
||||
and §11.3 makes "facing the black sun" the canonical camera. That is why SNO `v: 1`
|
||||
has +Z away from the viewer and `v: 2` has it toward: v1 was Cyberspace's own frame,
|
||||
v2 is glTF's. §9.4's `X_cs = X_ecef, Y_cs = Z_ecef, Z_cs = Y_ecef` is the swap that
|
||||
makes DECK §1.7's (east, up, north) frame right-handed.
|
||||
|
||||
**Do not copy the sign convention from the reference client.** `sno-core`'s
|
||||
`fromPayload` flips Z on `v: 2` and leaves `v: 1` alone — the exact opposite of what
|
||||
this plan says — because ONOSENDAI's internal frame *is* Cyberspace's, so it converts
|
||||
the published frame inward. Amethyst has no Cyberspace frame and renders in the glTF
|
||||
convention, so for us `v: 2` is as-written and `v: 1` is negated. Both are correct;
|
||||
the direction depends on the renderer's frame, and getting it backwards mirrors every
|
||||
asymmetric object without any error.
|
||||
|
||||
### 2.5 The implementations that already exist
|
||||
|
||||
| Project | What it is | License |
|
||||
| ---- | ---- | ---- |
|
||||
| [`arkin0x/sno-core`](https://github.com/arkin0x/sno-core) | The TypeScript SNO library, v0.1.4. The reader both clients use | MIT *(per `package.json`; no LICENSE file committed)* |
|
||||
| [`arkin0x/onosendai`](https://github.com/arkin0x/onosendai) | The 3D Cyberspace client (react-three-fiber). Active — last commit the same day I read it | **CC-BY-SA-4.0** |
|
||||
| `arkin0x/snocrash` | The shard workshop, the modeling tool. On sno-core 0.1.4 | not checked |
|
||||
| `arkin0x/cyberspace-cli` / `cyberspace-cli-js` | Reference protocol implementations (Python / TS), where `avatar.py` / `avatar.ts` and the PoW golden vectors live | not checked |
|
||||
|
||||
**Licensing, per the repo's dependency rule.** Nothing here gets linked into an APK —
|
||||
this is a Kotlin port of a specification. But the rule's spirit applies to copied
|
||||
source: `sno-core` declares MIT and is safe to read and to mirror test vectors from,
|
||||
though it commits no LICENSE file and someone should ask upstream for one before we
|
||||
lean on it. **ONOSENDAI is CC-BY-SA-4.0 — share-alike — so do not copy code out of
|
||||
it.** Write from DECK-0003 and check behavior against `sno-reference.py`.
|
||||
|
||||
`sno-core` also contains a great deal the DECK does not specify, all of it the
|
||||
authoring side: `stamps` (seven parametric shapes on the grid), `triangulate` (ear
|
||||
clipping plus the Newell normal), `outline` (each shared edge drawn once for `lines`
|
||||
mode), `orient` (winding faces outward so a lit bench can paint backfaces dark),
|
||||
`clip` (a shard cropped to the region cube it is sealed to — a rendering rule that
|
||||
exists nowhere in the DECK), `pose`, `scale`, `hsv`. None of it is needed to *read* an
|
||||
object. All of it would be needed to build a workshop, which is D3's argument.
|
||||
|
||||
### 2.5a Checked against the reference implementations, not only the spec
|
||||
|
||||
§8.10 says its two reference implementations are "pinned to one set of golden
|
||||
vectors", and the first pass built the avatar ladder from the section's
|
||||
pseudocode instead — which is the same text both implementations read, so
|
||||
agreement with it proved nothing about agreement with them. Closed since:
|
||||
`tests/fixtures/avatar_work.json` from cyberspace-cli is now a test, and **all
|
||||
eight vectors pass**. The two fixture sets (cyberspace-cli and cyberspace-core)
|
||||
carry the same eight names with identical `reach` and `required` throughout, so
|
||||
the "one set" claim holds and checking either checks both.
|
||||
|
||||
The vector that earns its keep is **"half gibson, ticks"**: a vertex of
|
||||
`[-1, -1, -1]` with ticks `[60, 60, 60]` is **-0.5** of a unit, not -1.5,
|
||||
because the whole part is the floor and the ticks count up from it. An
|
||||
implementation that takes the magnitude before adding the ticks prices that
|
||||
avatar at three times its reach and passes every other vector in the set.
|
||||
|
||||
`verify_avatar_work` in the Python reference also confirms the shape of the
|
||||
payment check independently: committed ≥ required **and** zeros ≥ committed, as
|
||||
two separate conditions with distinct failure reasons — which is what §5 of this
|
||||
plan's own note about `Event.pow()` is about.
|
||||
|
||||
**The Python reference is stale on the avatar kind, and we do not follow it.**
|
||||
|
||||
| Implementation | Avatar kind |
|
||||
| ---- | ---- |
|
||||
| `CYBERSPACE_V2.md` §8.10 | **11333** |
|
||||
| cyberspace-core (TS, 2026-09-14) | 11333 |
|
||||
| ONOSENDAI (the client) | 11333 |
|
||||
| cyberspace-cli (Python, 2026-09-07) | **33331** |
|
||||
|
||||
The Python CLI predates the kind move by a week and still calls 33331 the avatar
|
||||
kind, which DECK-0003 has since given to standalone objects. Run against the
|
||||
current network it would refuse a conformant avatar as `not-an-avatar`, and
|
||||
would treat one of the seven published `kind 33331` objects as an avatar and
|
||||
demand proof of work of it. We follow the spec and the two current
|
||||
implementations.
|
||||
|
||||
Nothing else in the Python CLI overlaps this work: it implements no §1.9, no
|
||||
palette and no mode — its only SNO surface is the avatar ladder. It does
|
||||
implement the `kind 33330` bag (encrypt/decrypt), which is the part we
|
||||
deliberately do not.
|
||||
|
||||
### 2.6 The two reference implementations disagree, and I checked which way
|
||||
|
||||
The DECK's conformance note says `sno-reference.py` and ONOSENDAI's TypeScript reader
|
||||
"agree on all twenty cases". That is no longer true. Running both against the same
|
||||
payloads:
|
||||
|
||||
| Payload | `sno-reference.py` | `sno-core` v0.1.4 |
|
||||
| ---- | ---- | ---- |
|
||||
| `v: 2` with literal `[r,g,b]` triples | **rejected** (rule 8b) | **accepted**, deliberately |
|
||||
| `type: "shard"` present | accepted, ignored | accepted, ignored |
|
||||
| `name` missing entirely | **accepted** — no check at all | accepted, defaults to `"shard"` |
|
||||
| `name` a number | **accepted** | accepted, defaults |
|
||||
| a vertex at 1,000 units from origin | accepted, extent grows | accepted, extent grows |
|
||||
|
||||
Run against the seven live objects rather than synthetic payloads, the gap is the
|
||||
whole legacy cohort: **`sno-core` v0.1.4 reads 7 of 7; `sno-reference.py` reads 4 of
|
||||
7.** (Measured — `sno-core` built from source at v0.1.4 and `fromPayload` called on
|
||||
each event's content.)
|
||||
|
||||
Two things follow. **D1 is settled upstream, not a judgement call.** sno-core's
|
||||
commit says it plainly: version 2 shipped as two changes that did not land together —
|
||||
the wire turned right-handed in ONOSENDAI #165 and color became an index two releases
|
||||
later in #167 — so objects written in between declare `v: 2`, carry triples, and are
|
||||
correct in every other respect *including their frame*. Refusing them "orphaned" real
|
||||
work in both clients. The two forms cannot be confused because one is an array and
|
||||
the other an integer. Amethyst should match sno-core: accept, read literally, keep
|
||||
the declared version's frame (a v2-with-triples object is **not** flipped).
|
||||
|
||||
And **`name` is required in §1.1's table and enforced by nobody.** Treat it as
|
||||
optional with a fallback; rejecting on it would be stricter than every implementation
|
||||
that exists, over a field that is decoration.
|
||||
|
||||
## 3. Quartz — new code
|
||||
|
||||
Package: `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/cyberspace/deck0003Sno/`.
|
||||
This follows the `concord/cord05Invites/` precedent for a spec family that is not a
|
||||
NIP; `nipCC…`-style naming would be a lie about where the spec lives.
|
||||
|
||||
```
|
||||
cyberspace/deck0003Sno/
|
||||
├── SnoObjectEvent.kt kind 33331, extends BaseAddressableEvent
|
||||
├── SnoPayload.kt @Immutable parsed object (IntArrays, not Lists)
|
||||
├── SnoParser.kt §1.9, the whole table, rejection returns null/Result
|
||||
├── SnoMode.kt solid | points | lines
|
||||
├── SnoPalette.kt resolution: built-in | name | nevent/naddr | inline
|
||||
├── SnoBuiltInPalette.kt the 256 cyberspace-neon-256 colors as an IntArray
|
||||
├── SnoPaletteEventReader.kt c-tags first, content-array fallback (§1.3b)
|
||||
├── TagArrayExt.kt / TagArrayBuilderExt.kt
|
||||
└── tags/ reuse NameTag (nip01Core), AltTag (nip31Alts), DTag
|
||||
```
|
||||
|
||||
### 3.1 Reuse — do not re-implement
|
||||
|
||||
| Need | Already exists |
|
||||
| ---- | ---- |
|
||||
| Addressable event base, `dTag()`, `address()` | `nip01Core/core/BaseAddressableEvent.kt` |
|
||||
| Lenient third-party JSON field readers | `nip01Core/kotlinSerialization/LenientJson.kt` |
|
||||
| `alt` tag (NIP-31) | `nip31Alts/` |
|
||||
| `nevent` / `naddr` decoding for the palette reference | `nip19Bech32/` |
|
||||
| Fetching the referenced palette event | `nip01Core/relay/client/accessories/` — `fetchFirst`. **Do not hand-roll a REQ loop** (CLAUDE.md) |
|
||||
| Event construction/signing | `eventTemplate` + `TagArrayBuilder` |
|
||||
|
||||
### 3.2 Parse with `JsonElement`, not a `@Serializable` data class
|
||||
|
||||
`ticks` is a heterogeneous array (triples interleaved with negative ints),
|
||||
`facecolors` likewise, and `colors` is `Int` in `v: 2` but `[Double, Double, Double]`
|
||||
in `v: 1`. kotlinx's declarative path cannot express that without four custom
|
||||
serializers. Parse `LenientJson.parseToJsonElement(content)` once and walk it with an
|
||||
explicit validator that mirrors §1.9's ten rules one function each, so a rule id can
|
||||
be quoted in the failure. `experimental/clink/kotlinSerialization/ClinkKSerializers.kt`
|
||||
is the in-repo precedent for hand-written readers over foreign JSON.
|
||||
|
||||
Store the result as primitive arrays, not `List<List<Int>>`: at the ceiling that is
|
||||
512×3 + 1024×3 ints, and the repo's hot-path style is raw arrays plus the `fast*`
|
||||
operators (CLAUDE.md). `SnoPayload` holds `IntArray wholeX/Y/Y…` or a single flat
|
||||
`IntArray(3 * n)` — flat is cheaper and the rasterizer wants it flat anyway.
|
||||
|
||||
Keep positions as the integer pair `(whole, tick)` on the payload. Expose
|
||||
`fun position(i: Int, axis: Int): Pair<Int, Int>` and convert to float **only** at
|
||||
the rasterizer boundary, per §1.2.
|
||||
|
||||
### 3.3 The built-in palette is a vendored artifact
|
||||
|
||||
`cyberspace-neon-256` is normative (Appendix C) and 256 entries. The upstream source
|
||||
of truth is `decks/sno-palette.json` (2,748 bytes) and its generator
|
||||
`decks/sno-palette.mjs`. Vendor the values as a `const val` hex string or a packed
|
||||
`IntArray` in `SnoBuiltInPalette.kt` with a comment naming the upstream file and its
|
||||
`built` date (`2026-09-16`), and a test that checks a handful of documented anchors
|
||||
(238 = `#ff0000`, 235 = `#00ff00`, 239 = `#0000ff`, 225 = `#ffffff`, 224 = `#000000`)
|
||||
so a bad paste cannot ship silently.
|
||||
|
||||
### 3.4 Tests — the table already exists
|
||||
|
||||
Port `_rejections()` from `sno-reference.py` verbatim: 25 cases across rules 1, 2, 3,
|
||||
4, 5, 6, 7, 8, 8a, 8b, 8c and 10, each a one-field mutation of the Appendix A
|
||||
tetrahedron. Plus:
|
||||
|
||||
- Appendix A validates, and `positions()[3] == (1, 2, 1)`.
|
||||
- 40 ticks is exactly one third of a unit — asserted on the integer pair.
|
||||
- `extent` repair: a vertex at 9 units grows the hint 8 → 9; `extent: 0` becomes 8.
|
||||
- Absent `ticks` means every position is whole; `[-4]` means the same thing.
|
||||
- `facecolors: [238, -3]` expands to four faces of 238; absent is **not** color 0.
|
||||
- The seven live events above as committed fixtures — four parse strictly, three
|
||||
parse under D1, zero crash. Under D1 all seven parse, which is the parity target:
|
||||
`sno-core` reads all seven and `sno-reference.py` reads four.
|
||||
|
||||
### 3.5 Registration points (each is a real integration, not a formality)
|
||||
|
||||
| File | Change |
|
||||
| ---- | ---- |
|
||||
| `quartz/utils/EventFactory.kt` | `SnoObjectEvent.KIND -> SnoObjectEvent(...)` in the `when`, plus the import. (`factories[kind]` exists as a runtime map but every first-party kind is compiled in.) |
|
||||
| `amethyst/model/LocalCache.kt` | add `is SnoObjectEvent,` to the addressable branch near `is GitRepositoryEvent,` — without it the addressable cache never holds the object and a quoted `naddr` stays empty |
|
||||
| `EventFactory.isKnownKind` | free once the branch exists; it decides whether a repost of a 33331 renders at all |
|
||||
| `amethyst/model/CacheSearch.kt` | only if objects should be searchable by name |
|
||||
|
||||
## 4. Rendering — the part with a real decision in it
|
||||
|
||||
Amethyst has no 3D anything today. Grepped: no OpenGL, no `drawVertices`, no
|
||||
`Vertices`, no mesh code anywhere in `commonsUI/` or `amethyst/`.
|
||||
|
||||
### 4.1 `Canvas.drawVertices` is common Compose API but unusable below API 29
|
||||
|
||||
Verified by pulling `androidx.compose.ui:ui-graphics-desktop:1.7.0` and running
|
||||
`javap`: `Vertices`, `VertexMode` and
|
||||
`Canvas.drawVertices(Vertices, BlendMode, Paint)` are present in the **desktop/skiko**
|
||||
artifact, so this is `commonMain` API, not an Android extra. It would give
|
||||
Gouraud-shaded triangles for free on Android and Desktop alike.
|
||||
|
||||
The blocker is Android: `Canvas.drawVertices()` is hardware-accelerated **only from
|
||||
API 29**, and `android-minSdk = "26"`. On Android 8.0–9.0 it silently does nothing
|
||||
on a hardware canvas. A `drawVertices`-only renderer ships a blank box to those users.
|
||||
|
||||
### 4.2 Recommended: a CPU rasterizer in `commons/`, on the blurhash rails
|
||||
|
||||
`commons/` already owns exactly this shape of code — `blurhash/BlurHashDecoder.kt`
|
||||
decodes to an `IntArray` of ARGB in `commonMain`, and
|
||||
`blurhash/PlatformImage.kt` is an `expect class` with a
|
||||
`PlatformImage.create(pixels, width, height)` companion and actuals in
|
||||
`androidMain` (Bitmap), `jvmMain` (BufferedImage) and `iosMain`. `commonsUI/`
|
||||
turns one into a Compose-renderable image via `service/image/CoilImageBridge.kt`
|
||||
(`expect fun PlatformImage.toCoilImage(): Image`), and `BlurHashFetcher` +
|
||||
`BKeyer` already wire a synthetic data class into Coil's memory cache.
|
||||
|
||||
An SNO thumbnail is the same pipeline with a different decoder:
|
||||
|
||||
```
|
||||
SnoPayload → SnoRasterizer.render(payload, palette, w, h, rotation): IntArray
|
||||
→ PlatformImage.create(...) (commons, exists)
|
||||
→ .toCoilImage() (commonsUI, exists)
|
||||
→ AsyncImage(model = SnoWrapper(eventId, rotation))
|
||||
```
|
||||
|
||||
Why the CPU path rather than `drawVertices`:
|
||||
|
||||
- **It works on every target** — API 26 Android, Desktop, and iOS — with no
|
||||
per-platform branch and no `expect fun supportsDrawVertices()`.
|
||||
- **It gets a z-buffer.** `drawVertices` has none; painter's algorithm (sort
|
||||
triangles by depth) breaks on interpenetrating triangles, which lattice-built
|
||||
geometry will have the moment two objects are authored to share an edge.
|
||||
- **It is unit-testable headless.** A `jvmTest` can rasterize a fixture and assert on
|
||||
pixels — no device, no screenshot harness.
|
||||
- **`facecolors` is trivial** (fill flat) and Gouraud is trivial (interpolate the
|
||||
three resolved colors barycentrically), which §4 of the spec requires as the
|
||||
default unlit reading.
|
||||
|
||||
Cost: about 150 lines. Project 512 vertices, cull nothing (§1.4), scanline-fill 1024
|
||||
triangles into a 384×384 buffer with a `FloatArray` depth buffer. That is well under
|
||||
a millisecond of real work on the JVM; the Android figure needs measuring, not
|
||||
guessing, before the interactive viewer lands.
|
||||
|
||||
### 4.2a Why not just draw it on a Canvas — measured
|
||||
|
||||
The obvious objection to a CPU rasterizer is that Skia is right there. It is a
|
||||
fair one and it was measured rather than argued, at 512px, on the same meshes:
|
||||
|
||||
| mesh | CPU rasterizer | Skia `drawPath` | Skia `drawVertices` |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| 8v/12f | 10.52 ms | **6.27 ms** | 6.71 ms |
|
||||
| 118v/94f | 6.35 ms | **4.31 ms** | 7.15 ms |
|
||||
| 512v/1004f | 41.31 ms | **15.46 ms** | 30.11 ms |
|
||||
|
||||
(Skia figures include whole-scene render overhead and are *software* skiko — on
|
||||
Android a Canvas is GPU-accelerated and would be far better than this. The depth
|
||||
sort was hoisted out of the timing loop, which flatters them further.)
|
||||
|
||||
**So a Canvas is faster, and it is still the wrong trade here**, for two reasons
|
||||
that cost correctness rather than speed:
|
||||
|
||||
1. **A Canvas has no depth buffer.** Triangles have to be sorted back to front
|
||||
every frame, which is exact for a convex mesh and wrong for interpenetrating
|
||||
ones. Lattice geometry invites exactly that case: §1.2's whole promise is
|
||||
that two objects authored to share an edge do share it.
|
||||
2. **`drawPath` cannot interpolate a face's vertices.** A Path fill is one
|
||||
colour, so every face would flatten to one — and two of the seven objects on
|
||||
the network are smooth three-colour gradients. `drawVertices` keeps them, but
|
||||
it is hardware-accelerated only from API 29 against a `minSdk` of 26, and on
|
||||
skiko it measured no better than the CPU path for small meshes.
|
||||
|
||||
And the speed is not needed. A real object costs **0.25 ms** at thumbnail size
|
||||
and **3 ms** per viewer frame, off the composition thread; the 54 ms figure is an
|
||||
adversarial payload that §1.8 permits and nobody authors. Trading away depth
|
||||
correctness and per-vertex colour for a 2x on numbers that already fit is a bad
|
||||
deal, and §4 is explicit that the same object must not look like two objects —
|
||||
which is what an API-29 split would produce.
|
||||
|
||||
If a real device ever shows this mattering, the move is `drawVertices` behind
|
||||
the existing interface for API >= 29, accepting painter's-algorithm artifacts
|
||||
there, driven by a measurement on hardware rather than one in a container.
|
||||
|
||||
**The other lever, unspent:** fixed per-frame overhead is 0.62 ms at 512px — two
|
||||
buffer allocations plus the projection, about a fifth of a real object's frame.
|
||||
A caller-owned render target would recover most of it, and is only worth doing
|
||||
if the viewer needs to be tighter; the thumbnail is Coil-cached and pays it once.
|
||||
|
||||
### 4.2b The viewer is the point, and the first pass under-built it
|
||||
|
||||
The framing above — thumbnail cached, viewer secondary — had it backwards.
|
||||
These objects exist to be turned and looked at; the viewer *is* the feature, and
|
||||
the first pass shipped it half-built and capped:
|
||||
|
||||
- **It only rotated.** No zoom, no pan, on an object whose whole purpose is
|
||||
being inspected from angles. One finger now turns it, two move and scale it,
|
||||
a double tap resets it.
|
||||
- **It rendered at under half resolution during that very interaction.** The
|
||||
512px cap was justified by a 48ms measurement — of the *adversarial* mesh,
|
||||
taken *before* the rasterizer got 4x faster. Re-measured after: a real object
|
||||
at full 1080px costs **4.86 ms**. The cap was protecting against a number that
|
||||
no longer existed, at the cost of a soft image exactly where sharpness matters.
|
||||
(Re-measured on a busier machine while §4.2d's work landed: 8.04 ms before that
|
||||
commit's hoist and 6.81 ms after. These runs are minutes apart on a shared box, so
|
||||
read the ratios rather than the absolutes; every figure in §4.2c–d comes from one
|
||||
run and is comparable within it.)
|
||||
|
||||
The cap is now adaptive rather than constant: 384px while a finger is down,
|
||||
full resolution — scaled by how far the reader has zoomed — once it lifts. The
|
||||
frames during a turn are the ones that must not stutter and the ones nobody
|
||||
inspects; the frame you stop on is the one that must be sharp. That also handles
|
||||
the adversarial object without a fixed ceiling: coarse while it moves, and it
|
||||
pays its 177ms once when it settles.
|
||||
|
||||
Zoom and pan do not re-raster at all — they are a `graphicsLayer` transform of
|
||||
the frame already drawn, which is free per frame; only rotation changes which
|
||||
faces point at the viewer. The re-raster on settle is what sharpens a deep zoom
|
||||
rather than leaving it an upscaled bitmap.
|
||||
|
||||
### 4.2c Measured against the author's own viewer
|
||||
|
||||
Two of arkin0x's repositories came up as "do we mimic the viewer?" and they are
|
||||
not the same thing:
|
||||
|
||||
- **`arkin0x/cyberspace-viewer`** is *not* the SNO viewer. Its last commit is
|
||||
2023-08-25, three years before DECK-0003; it draws v1 "Constructs" — wireframe
|
||||
lattices from a hard-coded `ConstructLineData.ts` — through
|
||||
`@react-three/drei`'s `OrbitControls`. Grepping it for `shard`, `33331` or
|
||||
`sno` finds nothing. It is not a comparison point.
|
||||
- **`arkin0x/ONOSENDAI`** is. `src/scene/ShardMesh.tsx` is the author's current
|
||||
renderer, on `sno-core` v0.1.4, and it draws a shard in two distinct ways.
|
||||
|
||||
**The world drawing, which we already matched exactly.**
|
||||
`meshBasicMaterial vertexColors side={DoubleSide} toneMapped={false}` — unlit,
|
||||
per-vertex colours, two-sided, no tone curve — is §4's default reading and is
|
||||
what the rasterizer and the feed thumbnail already did. Lines mode builds its
|
||||
edges from the faces with each shared edge once (`sno-core/outline`), with a
|
||||
single polyline through the vertices when there are no faces, which is also
|
||||
what `drawEdges` already did.
|
||||
|
||||
**Two things it does that we did not.**
|
||||
|
||||
1. **`§1.5`'s "draw the vertices as points in every mode" was half-implemented.**
|
||||
We drew them in `points` and `lines` mode and under 3px, but not over a
|
||||
solid — the one case the sentence is actually about. The reference keeps two
|
||||
point profiles (`scene/pointDisc.ts`) and now so do we: where the vertices
|
||||
*are* the shape they are 0.3 units across, clamped to 1.6–10px; under a
|
||||
solid or a wireframe they are 0.07 units, clamped to 0.7–2.4px and laid over
|
||||
at 55% — a hint that they are there, never a second shape competing with the
|
||||
faces. They read the depth buffer and do not write it, as the reference's
|
||||
point material does, so a vertex behind a face stays hidden while two
|
||||
overlapping dots both land. On an object whose faces carry their own colours
|
||||
the dots take the face colour: the reference arrives there by splitting every
|
||||
coloured face into three corners of its own before drawing, and §1.4a has in
|
||||
any case already decided the vertex colours are not what that object shows.
|
||||
|
||||
2. **Their bench is lit and ours was not.** ONOSENDAI's *world* is unlit under
|
||||
its bloom; its *workshop bench* — the mode where you turn one shard over to
|
||||
understand it — uses `meshLambertMaterial vertexColors flatShading
|
||||
side={FrontSide}` plus a second mesh of the same triangles wound the other
|
||||
way in grey, so an open shape shows its inside and a missing face is plain.
|
||||
Our viewer is a bench that was using the world's shading. §4 permits exactly
|
||||
this ("A client MAY light an object instead, and many will") on two
|
||||
conditions — normals per face, flat, from the triangle's own vertices, and
|
||||
never smoothed across a shared vertex — and both are met.
|
||||
|
||||
**What lighting cost, and what it needed first.** Nothing on the wire says which
|
||||
way round a face is: §1.4 makes winding meaningless and §4 draws both sides, so
|
||||
an object whose triangles disagree is indistinguishable from one whose triangles
|
||||
agree — until it is lit, at which point half a stamped block goes black. So
|
||||
`SnoFaceWinding` ports `sno-core`'s `orient.ts` (MIT): faces sharing a clean
|
||||
edge are made to agree, which groups them into patches, and each patch is then
|
||||
turned as a whole by ray-casting its own normals against the rest of the object.
|
||||
A face whose rays cross an odd number of faces *both* ways is buried inside a
|
||||
join and is left undrawn. That pass is quadratic in the face count and is the
|
||||
most expensive thing the feature does — **0.63 ms** for the largest real object
|
||||
(94 faces), **27.6 ms** for an adversarial one at §1.8's ceiling (1004 faces) —
|
||||
so it is computed once per object, off the composition thread, and held across a
|
||||
turn. The per-frame cost is unchanged: a 512px frame of a real object is
|
||||
**1.21 ms** flat and **1.62 ms** lit.
|
||||
|
||||
Hoisting the single-colour case out of the pixel loop while adding the light
|
||||
paid for most of that: a face whose three corners agree — a stamped block, or
|
||||
any face carrying its own colour — now resolves its colour once for the whole
|
||||
triangle instead of testing for equality at every pixel. A real object at 1080px
|
||||
went 8.04 → 6.81 ms in the same run that added the shading.
|
||||
|
||||
**Where we still differ, deliberately.** Their bench is a perspective camera on
|
||||
`OrbitControls` (`fov 50`, dolly 0.6–60, screen-space pan); ours is orthographic
|
||||
with object-rotation and a `graphicsLayer` zoom. That is a thumbnail-first
|
||||
choice — no perspective distortion, no near plane to clip against, and a raster
|
||||
that can be produced headless — and it is not worth revisiting for a reader who
|
||||
is looking at someone else's object rather than building one. The light switch
|
||||
defaults **off**, so the feed and a freshly opened viewer both show §4's unlit
|
||||
reading and an object looks the same everywhere it is quoted; it is offered only
|
||||
on a filled object, since points and wireframes look identical either way.
|
||||
|
||||
### 4.2d Following the comparison through
|
||||
|
||||
§4.2c ended with two gaps and a list of things ONOSENDAI does that we do not.
|
||||
Five of those were ours to close, and closing the last one reversed a decision
|
||||
this document had recorded twice.
|
||||
|
||||
**`unit` was never on screen.** The plan said to surface it and the first pass
|
||||
did not, so a card read "8 vertices · 12 faces" for an object at `unit: 0` and
|
||||
for the same geometry at `unit: 40` — a molecule and a mountain. `unit` is the
|
||||
only field separating them. `CyberspaceScale` turns the exponent into a length
|
||||
using the two facts that make it one: §1.6 says a model unit is `2^unit` base
|
||||
units, and `CYBERSPACE_V2.md` §9.2 fixes the base unit, the gibson, at `2^-33`
|
||||
metres. The measure table and the three-significant-figure rounding are
|
||||
`sno-core/scale.ts`'s, and the test checks every value `unit` may take, 0 to 84,
|
||||
against that file run over the same range — including `unit: 28`, which is
|
||||
3.125 cm exactly and the one entry where a language's default rounding shows
|
||||
(ECMAScript's `toPrecision` takes the larger; `roundToLong` agrees).
|
||||
|
||||
**An object naming an external palette was drawn in the wrong colours.**
|
||||
`SnoPaletteEventReader` and `SnoParser`'s `fetchedPalette` existed from the
|
||||
start and nothing ever called them with anything, so every §1.3a reference
|
||||
resolved to the built-in 256 in silence. ONOSENDAI does not fetch either —
|
||||
`resolvePalette` has zero call sites there — but Amethyst already has the relay
|
||||
plumbing, so `WithSnoPalette` loads the named event by id (§1.3a pins a
|
||||
reference to the event it names and forbids following the `e` chain forward)
|
||||
and re-parses. It matters more than it sounds: every palette on the network
|
||||
today is a `kind 3367` colour moment of three to six colours, so an object built
|
||||
against one and drawn against 256 built-in entries is not slightly off, it is a
|
||||
different object. The re-parse can also *fail* where the first succeeded — index
|
||||
238 is fine against 256 entries and out of range against five — and §1.3 makes
|
||||
that a defect in the payload rather than something to clamp, which is what the
|
||||
reference does too.
|
||||
|
||||
That fetch exposed a real bug in the thumbnail cache. Coil keyed on
|
||||
`sno:<event id>:<size>:<view>`, on the reasoning that an addressable object
|
||||
republished under the same `d` arrives as a new id — true of geometry, false of
|
||||
colour. One event id now legitimately produces two pictures a second apart, so
|
||||
the key carries a hash of the resolved colours; without it the first paint would
|
||||
have been served from memory forever and the fetch would have bought nothing.
|
||||
|
||||
**The default avatar drew nothing.** §8.10 makes an empty `kind 11333` the
|
||||
default avatar — no geometry, no work owed, what everyone is before they adopt a
|
||||
shape — and drawing nothing for it is correct and unhelpful: the note vanishes
|
||||
out of the feed, which reads as a fault. It now gets the wireframe icosahedron
|
||||
the reference puts in its place, built as an ordinary payload so it goes through
|
||||
the same rasterizer and needs no second drawing path. Its face list is derived,
|
||||
not remembered, because a hand-written icosahedron comes out looking almost
|
||||
right; the test asserts twelve corners, thirty edges, twenty faces, every corner
|
||||
on five and every edge on two, and it caught the hand-written one.
|
||||
|
||||
**A shard's `C` tag was parsed and dropped.** §7.6 lets an item carry its exact
|
||||
coordinate so a client can "render it at a point rather than somewhere in the
|
||||
region". Amethyst has no region to render it in, but the tag still carries one
|
||||
thing a reader understands unaided: the plane (§2.4), which says whether the
|
||||
shard was hidden at a place on Earth or at one with no physical counterpart.
|
||||
That is bit 0 of the 256-bit coordinate and it is where the decoding stops, on
|
||||
purpose. Pulling X, Y and Z out is §2.3 plus an 85-bit decimal printer, and what
|
||||
it would put on screen is three twenty-six-digit integers that tell a reader
|
||||
with no world exactly nothing. The reading that *would* mean something — the GPS
|
||||
position under a dataspace coordinate — is §9.7: 96-digit decimal arithmetic
|
||||
with a hand-rolled deterministic trig series, carried so every client agrees on
|
||||
where a place on Earth is. Amethyst has no Earth to agree about.
|
||||
|
||||
**The ±64-unit rejection is gone, and should never have shipped.** §1.8 states
|
||||
the position bound as an obligation on publishers and gives a reader a choice in
|
||||
as many words: "A reader MAY reject such a payload and MAY instead repair it by
|
||||
growing the extent." Both references repair. The argument recorded here for
|
||||
rejecting — that we draw strangers' events in a feed — was a defence that was
|
||||
never load-bearing: a far-off vertex costs the rasterizer nothing, because the
|
||||
projection fits whatever it is handed to the frame, and the cost that does scale
|
||||
is in the vertex and face counts, which §1.8 bounds and rule 3 enforces. What
|
||||
was real was arithmetic: `whole * 120` overflows an Int, and `abs(Int.MIN_VALUE)`
|
||||
is its own negative, so a coordinate could be silently rewritten to the origin.
|
||||
That is now bounded where it actually lives — the whole is read as a Long, the
|
||||
product is checked against `Int.MAX_VALUE` ticks, and past that there is no
|
||||
coordinate to repair, only one that would wrap. An object at 65 units is drawn,
|
||||
and rule 9 grows the extent past `MAX_EXTENT` to hold it, exactly as
|
||||
`neededExtent` does.
|
||||
|
||||
### 4.3 Two surfaces, and only one of them needs to be fast
|
||||
|
||||
| Surface | Renderer | Cache |
|
||||
| ---- | ---- | ---- |
|
||||
| Feed / thread thumbnail — a static 3/4 view, no interaction | CPU raster once at a fixed rotation | Coil memory cache keyed by event id, exactly like `BlurHashFetcher`'s `BKeyer` |
|
||||
| Full-screen viewer — drag to rotate, pinch to zoom | CPU raster per frame at reduced resolution while dragging; `drawVertices` on API ≥ 29 / skiko is a later optimization behind one interface, not a prerequisite | none |
|
||||
|
||||
Shading follows §4: unlit by default, both sides drawn, flat fill for a face carrying
|
||||
`facecolors`, barycentric interpolation otherwise, **never** averaged smooth normals,
|
||||
never invented geometry. A client SHOULD also draw the vertices as points in every
|
||||
mode so a tiny object stays visible — cheap, do it.
|
||||
|
||||
`v: 1` negates Z on read (§2). Do it in the parser, so nothing downstream knows there
|
||||
are two versions.
|
||||
|
||||
`unit`, `up` and `spin` have no meaning in a client with no world frame. Parse and
|
||||
validate them (rule 10 rejects a bad `spin` whether or not `up` is present), surface
|
||||
`unit` as a caption ("1 unit = 2^12 gibsons"), and ignore them for layout. §1.6 says
|
||||
so explicitly: an application with no natural base unit "SHOULD render an object at a
|
||||
size that suits its context rather than refusing it."
|
||||
|
||||
## 5. Security review items
|
||||
|
||||
- **Enforce the limits before allocating** (§6). Count the arrays; never size a
|
||||
buffer from a declared length. The format has no length fields for this reason.
|
||||
- **Face indices are the crash** (§1.4). Every index `0 ≤ i < vertexCount` and the
|
||||
three distinct, validated before a single triangle reaches the rasterizer.
|
||||
- **The position bound the spec leaves open.** ~~Reject beyond ±64 units.~~ Reversed,
|
||||
see §4.2d: §1.8 offers a reader reject *or* repair and both references repair, so
|
||||
rejecting made Amethyst the only client that refused an object the rest of the
|
||||
network drew. The reach a payload can claim is not a denial of service on its own —
|
||||
the projection fits whatever it is given to the frame — and the one genuine hazard,
|
||||
`whole * 120` overflowing an Int, is now checked as what it is: a representation
|
||||
limit at `Int.MAX_VALUE` ticks, computed in a Long so the multiply cannot wrap.
|
||||
- **A palette reference is a network fetch driven by a stranger's event.** Fetch it
|
||||
through the existing relay accessories with the account's relay set, never from a
|
||||
URL, never blocking a frame, and cap it at one fetch per object.
|
||||
- **`kind 3330` items may be unsigned** (§6), in which case `pubkey` is a claim. If
|
||||
3330 is ever implemented, do not attribute.
|
||||
- **Screen real estate is a resource** (§6). An object from a stranger draws in the
|
||||
feed. Gate the thumbnail behind the same content-warning / follow-based rules the
|
||||
feed already applies to images, rather than inventing a new one.
|
||||
|
||||
## 6. Sequencing — what landed
|
||||
|
||||
Built in this order, each step verified before the next:
|
||||
|
||||
1. `quartz/…/cyberspace/deck0003Sno/` — payload, §1.9 validator, built-in
|
||||
palette, palette-event reader, kind 33331, kind 11333 + the §8.10 work gate.
|
||||
**50 tests**: the 25-case rejection table generated from `sno-reference.py`,
|
||||
the avatar ladder computed from §8.10's pseudocode, all seven live objects as
|
||||
fixtures.
|
||||
2. `commons/…/sno/SnoRasterizer` — CPU rasterizer, **15 tests**, one per rule §4
|
||||
decides, plus the depth ordering a painter's algorithm gets wrong. Checked by
|
||||
eye too: the object named Triforce comes out as a Triforce.
|
||||
3. `commonsUI/…/sno/` — Coil fetcher + `SnoThumbnail` for the cached static
|
||||
view, `SnoObjectViewer` drawing straight to a bitmap for the drag (each drag
|
||||
angle would otherwise be a half-megabyte cache entry), `SnoObjectCard` /
|
||||
`SnoAvatarCard`, and a new `PlatformImage.toComposeImageBitmap()` beside the
|
||||
existing Coil bridge.
|
||||
4. `amethyst/…/note/types/SnoObject.kt`, `SnoAvatar.kt` + `SnoShard.kt`, dispatched from
|
||||
`NoteCompose` and `ThreadFeedView`, stored addressably/replaceably in
|
||||
`LocalCache`, fetcher registered in both image loaders.
|
||||
|
||||
**Decisions as taken:** D1 accept (parity with sno-core — all seven objects
|
||||
read), D1b tolerate, D2 deferred, D3 read-only with the writer in quartz, D4
|
||||
inline quote + thread, D5 ~~clamp at ±64 units~~ repair by growing the extent,
|
||||
as both references do (reversed in §4.2d).
|
||||
|
||||
**Deliberately not wired:** kind 3330 bag items (D2), an authoring/modeling
|
||||
tool, and replacing a user's NIP-01 profile picture with their cyberspace
|
||||
avatar — that last one is a product decision about every feed row, not
|
||||
plumbing.
|
||||
|
||||
### The original plan
|
||||
|
||||
1. **Quartz parse + validate + built-in palette + the 25-case table.** No UI. Lands
|
||||
as a self-contained, fully tested module; `amy` can dump a parsed object for
|
||||
interop checks.
|
||||
2. **`SnoRasterizer` in `commons/`** with jvmTest pixel assertions on the four valid
|
||||
live fixtures. Still no UI.
|
||||
3. **`commonsUI` thumbnail** — Coil fetcher + keyer mirroring `BlurHashFetcher`, and
|
||||
a `SnoThumbnail` composable.
|
||||
4. **Amethyst wiring** — `ui/note/types/SnoObject.kt` with `RenderSnoObject`, a
|
||||
branch in `NoteCompose.kt` (next to `is ChessGameEvent ->`) and in
|
||||
`ThreadFeedView.kt` (next to `is Ps1SaveEvent`), plus the `LocalCache` addressable
|
||||
branch. At this point a `nostr:naddr1…` to an object renders inline.
|
||||
5. **Full-screen viewer** with drag-to-rotate, `mode` respected, palette-reference
|
||||
resolution, and the `unit`/vertex-count caption.
|
||||
6. **`kind 11333` avatars** — same payload, replaceable container, plus the §8.10
|
||||
proof-of-work gate on `nip13Pow/`. Small, self-contained, and the only part of
|
||||
this surface where drawing is conditional on verification. Worth doing once
|
||||
steps 1–2 exist, because avatars are the thing a Cyberspace user actually has.
|
||||
7. **Optional later:** NIP-89 handler advertisement, an authoring/publishing path.
|
||||
|
||||
Steps 1–2 are pure logic with no Android surface and no dependency on the rest; they
|
||||
are where the risk is, and they are testable without a device.
|
||||
|
||||
## 7. Decisions to make before step 1
|
||||
|
||||
- **D1 — legacy `v: 2` triples. Settled upstream; take it.** `sno-core` v0.1.4
|
||||
accepts them deliberately (§2.6), so this is parity with the reader both clients
|
||||
run, not leniency we invent. Read the triples literally, keep the declared
|
||||
version's frame — a `v: 2`-with-triples object is **not** Z-flipped — and put the
|
||||
reasoning in the KDoc, because `sno-reference.py` still rejects and a future reader
|
||||
of our code will check the wrong arbiter first.
|
||||
- **D1b — `name`.** Required in §1.1's table, enforced by neither reference
|
||||
implementation (§2.6). Treat as optional with a fallback.
|
||||
- **D2 — `kind 3330` bag items. Settled twice: the container shipped first, the bag
|
||||
followed.** `SnoShardEvent` reads a shard handed over directly — quoted in a note,
|
||||
or fetched by id — through the same parser and the same card as a standalone
|
||||
object, because §3.2 is two containers for one format rather than two formats.
|
||||
|
||||
Opening the `kind 33330` bag around it was deferred here and is now built; see
|
||||
`quartz/plans/2026-09-22-cyberspace-region-bags.md`. The deferral's *reason* was
|
||||
restated correctly before it was lifted, and the restatement is what made the work
|
||||
tractable: "we have no position in cyberspace" was never the obstacle — §7.7 is
|
||||
explicit that a seeker's position never enters the cost of a sweep — the obstacle
|
||||
was the compute and the protocol surface behind it. Both turned out to be
|
||||
affordable in the corner anyone uses: a region key is 1.2 ms at height 8 against
|
||||
the spec's own 1.3 ms, and a bag's own hint prices its search before a tree is
|
||||
built. A bag whose hint prices it out of reach still says so and shows nothing,
|
||||
which is §7.6 working as written: a failed decryption "MUST NOT be treated as an
|
||||
error in the bag".
|
||||
|
||||
**The v1 tag form was built and then removed, deliberately.** Every `kind 3330`
|
||||
publicly reachable on a relay predates the deck: 21 events, one pubkey, all inside
|
||||
one minute on 2025-01-16, empty content, geometry in `vertices`/`colors`/`indices`
|
||||
tags as flat comma-separated decimals. A reader for it took about 200 lines and
|
||||
worked — and then rendering the whole corpus showed what it buys: **one black
|
||||
triangle and one black dot**, two distinct shapes between the 21 events, every
|
||||
colour `0,0,0`. The cyberspace readme settles it: *"Cyberspace v1 drafts are
|
||||
deprecated and archived. They are not a valid basis for new implementations."* A
|
||||
reverse-engineered decoder for a deprecated encoding that no current spec
|
||||
describes is a maintenance liability priced against two black shapes, so it went.
|
||||
|
||||
The consequence, stated plainly: **no publicly reachable 3330 renders today.** The
|
||||
current ones are sealed in bags and the old ones are v1. The code is right for the
|
||||
shards the current client writes, and is exercised by tests rather than by the
|
||||
network.
|
||||
- **D3 — publishing.** Recommend **read-only first**. Appendix B.4 is right that the
|
||||
deliverable is software that emits the format, but a modeling tool is a screen, not
|
||||
a feature, and the reader is what makes objects visible at all. Build the writer
|
||||
(`SnoObjectEvent.build`) in quartz anyway — it is twenty lines and the tests want
|
||||
round-tripping.
|
||||
- **D4 — where the object surfaces.** Inline quote + thread view is the minimum.
|
||||
A dedicated feed of `kind 33331` is not obviously wanted; there are seven objects
|
||||
on the network.
|
||||
- **D5 — the ±64 unit clamp** (§5). **Reversed.** It shipped as a deliberate
|
||||
divergence and it was the wrong call. Neither `sno-reference.py` nor `sno-core`'s
|
||||
`fromPayload` bounds how far a vertex may lie from the origin — both grow the
|
||||
extent, which is what §8's third open question admits and what §1.8 names as one
|
||||
of a reader's two choices. sno-core even has a `validPoint(p, extent)` helper and
|
||||
deliberately does not call it on the read path. "We render strangers' events in a
|
||||
feed" argued for a defence that was never load-bearing: a far-off vertex costs the
|
||||
rasterizer nothing, because the projection fits whatever it is handed to the frame
|
||||
and the cost is in the vertex and face counts, which §1.8 *does* bound and rule 3
|
||||
*does* enforce. The only real hazard was arithmetic, and that is now bounded where
|
||||
it actually lives. See §4.2d.
|
||||
|
||||
## 8. Size, for the record
|
||||
|
||||
Nothing here threatens a relay. The largest live object is 3,248 bytes for the whole
|
||||
event. The spec's own ceiling is 18.9 KB for a typical 512-vertex/1024-face object
|
||||
and 33.5 KB worst case with a color on every face, against strfry's stock 65,536-byte
|
||||
`events.maxEventSize`. The built-in palette adds 3.3 KB only when carried inline,
|
||||
which Amethyst never does as a reader.
|
||||
Vendored
+3
-3
@@ -240,6 +240,6 @@
|
||||
# The enum FIELDS are already pinned by the blanket `-keepclassmembers enum *`
|
||||
# above; that rule deliberately lets the CLASS be renamed, which is exactly what
|
||||
# breaks here. Every enum used as a route argument needs its name too.
|
||||
-keep class com.vitorpamplona.amethyst.ui.navigation.routes.DiscoverTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType { *; }
|
||||
-keep class com.vitorpamplona.amethyst.ui.navigation.routes.GeocacheTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.navigation.DiscoverTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.navigation.GeocacheTab { *; }
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
+4
-2
@@ -30,7 +30,7 @@ import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.ui.actions.DeferredCrossfade
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -57,8 +57,10 @@ class DeferredAnimationTest {
|
||||
rule.mainClock.autoAdvance = false
|
||||
|
||||
rule.setContent {
|
||||
DeferredCrossfade(
|
||||
// enabled = true is the deferred path (DeferredCrossfade, internal to commonsUI).
|
||||
CrossfadeIfEnabled(
|
||||
targetState = state.value,
|
||||
enabled = true,
|
||||
modifier = Modifier,
|
||||
contentAlignment = Alignment.TopStart,
|
||||
animationSpec = tween(DURATION_MS),
|
||||
|
||||
+4
-1
@@ -67,10 +67,13 @@ import com.vitorpamplona.amethyst.commons.resources.push_server_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.push_server_uses_app_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_dont_show_again_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.select_push_server
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+2
-2
@@ -27,9 +27,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
|
||||
@@ -24765,9 +24765,9 @@ Lcom/vitorpamplona/amethyst/ui/StringResourceCacheKt$StringResSetup$lambda$0$0$$
|
||||
SPLcom/vitorpamplona/amethyst/ui/StringResourceCacheKt$StringResSetup$lambda$0$0$$inlined$onPauseOrDispose$1;-><init>(Landroidx/lifecycle/compose/LifecycleResumePauseEffectScope;)V
|
||||
Lcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;->getActivityOrNull(Landroid/content/Context;)Landroidx/activity/ComponentActivity;
|
||||
Lcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
|
||||
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
@@ -25489,14 +25489,14 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$Key
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;->getLambda$-1905508794$amethyst()Lkotlin/jvm/functions/Function3;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;->lambda__1905508794$lambda$0(Landroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;->getLambda$-1905508794$amethyst()Lkotlin/jvm/functions/Function3;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;->lambda__1905508794$lambda$0(Landroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;->$r8$lambda$EB2Pyswic2JGUT6jyNtb4e7r5z8(Landroidx/compose/runtime/MutableState;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;->$r8$lambda$raCAC-FTNT20_jVOcW9Lmx1V5zI(Landroidx/compose/ui/semantics/SemanticsPropertyReceiver;)Lkotlin/Unit;
|
||||
@@ -25529,12 +25529,12 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$External
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;-><init>(Landroidx/compose/runtime/MutableState;)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;->LoginButton$lambda$0(ZLandroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;->LoginButton(ZZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;-><init>(Z)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;->LoginButton$lambda$0(ZLandroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;->LoginButton(ZZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;-><init>(Z)V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><init>()V
|
||||
@@ -25613,26 +25613,28 @@ Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$$Ex
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$$ExternalSyntheticLambda3;-><init>(Lkotlin/jvm/functions/Function2;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$OpenURIIfNotLoggedIn$lambda$5$0$$inlined$onDispose$1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$OpenURIIfNotLoggedIn$lambda$5$0$$inlined$onDispose$1;-><init>(Landroidx/activity/ComponentActivity;Landroidx/core/util/Consumer;)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButtonKt;->SignUpButton(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ColorKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getPurple200()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getPurple500()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getTeal200()J
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ShapeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getQuoteBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getShapes()Landroidx/compose/material3/Shapes;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize10dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize13dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize20dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize35dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize40dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize50dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/SignUpButtonKt;->SignUpButton(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getPurple200()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getPurple500()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getTeal200()J
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getQuoteBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getShapes()Landroidx/compose/material3/Shapes;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize10dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize13dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize20dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize35dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize40dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize50dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->$r8$lambda$7JAdMf4levt4F3mr6nvsppu__xY(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->$r8$lambda$GsT95FSXMVwriWjqAt3KW-oAPpo(Landroid/view/View;ZLandroidx/compose/material3/ColorScheme;)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;-><clinit>()V
|
||||
@@ -25645,16 +25647,16 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9$0(Landro
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lcom/vitorpamplona/amethyst/model/ThemeType;Lcom/vitorpamplona/amethyst/model/AccentColorType;Lcom/vitorpamplona/amethyst/model/FontFamilyType;Lcom/vitorpamplona/amethyst/model/FontSizeType;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->accentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->lightColors(Lcom/vitorpamplona/amethyst/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->onAccent-8_81llA(J)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->onAccentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccent-8_81llA(J)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccentContainer-DxMtmZc(JZ)J
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;-><init>(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
@@ -25676,19 +25678,19 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnLight$1;-><clinit>
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnLight$1;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getDefaultHeadingStyle()Lkotlin/jvm/functions/Function2;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getDefaultParagraphSpacing()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getFont14SP()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getFont17SP()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;->getDefaultHeadingStyle()Lkotlin/jvm/functions/Function2;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpacing()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont14SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont17SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;
|
||||
SPLcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;-><init>()V
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvi
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManagerForRelays
|
||||
import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomMediaCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.http.OnionLocationCache
|
||||
import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlEndpointResolver
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy
|
||||
@@ -458,13 +459,14 @@ class AppModules(
|
||||
scope = applicationIOScope,
|
||||
dns = surgeDns,
|
||||
// Transparently rewrites sha256-keyed HTTP requests to the local
|
||||
// Blossom cache when the master toggle is on, the profile-pictures-only
|
||||
// restriction is off, and the probe sees 127.0.0.1:24242 as available.
|
||||
shouldBridgeBlossomCache = {
|
||||
// Blossom cache when the master toggle is on, the probe sees
|
||||
// 127.0.0.1:24242 as available, and either the profile-pictures-only
|
||||
// restriction is off or the request is a profile picture.
|
||||
shouldBridgeBlossomCache = { profilePicture ->
|
||||
val settings = sessionManager.loggedInAccount()?.settings
|
||||
val master = settings?.useLocalBlossomCache?.value ?: false
|
||||
val profileOnly = settings?.localBlossomCacheProfilePicturesOnly?.value ?: false
|
||||
master && !profileOnly && localBlossomCacheProbe.available.value
|
||||
master && (profilePicture || !profileOnly) && localBlossomCacheProbe.available.value
|
||||
},
|
||||
onionCache = onionLocationCache,
|
||||
usageInterceptor = httpUsageInterceptor,
|
||||
@@ -477,6 +479,7 @@ class AppModules(
|
||||
cachedHeaderProvider = blossomReadAuthTokens::cachedHeader,
|
||||
onAuthRequired = blossomReadAuthTokens::warm,
|
||||
),
|
||||
onLocalBlossomCacheUnreachable = { localBlossomCacheProbe.markUnavailable() },
|
||||
)
|
||||
|
||||
// Offers easy methods to know when connections are happening through Tor or not
|
||||
@@ -1035,14 +1038,16 @@ class AppModules(
|
||||
}
|
||||
},
|
||||
httpClientBuilder = roleBasedHttpClientBuilder,
|
||||
// Same gate as the interceptor: the profile-pictures-only restriction keeps note
|
||||
// media (including native `blossom:` URIs) off the local cache.
|
||||
useLocalBlossomCache = {
|
||||
sessionManager
|
||||
.loggedInAccount()
|
||||
?.settings
|
||||
?.useLocalBlossomCache
|
||||
?.value ?: false
|
||||
val settings = sessionManager.loggedInAccount()?.settings
|
||||
val master = settings?.useLocalBlossomCache?.value ?: false
|
||||
val profileOnly = settings?.localBlossomCacheProfilePicturesOnly?.value ?: false
|
||||
master && !profileOnly
|
||||
},
|
||||
localCacheProbe = localBlossomCacheProbe,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1148,7 +1153,8 @@ class AppModules(
|
||||
blossomServerResolver = { blossomResolver },
|
||||
// Through the role builder (not raw getHttpClient) so Coil's image
|
||||
// traffic carries the "image" ledger tag. Same Tor decision inside.
|
||||
callFactory = { roleBasedHttpClientBuilder.okHttpClientForImage(it) },
|
||||
// Marked as media so the local Blossom cache may serve these downloads.
|
||||
callFactory = { LocalBlossomMediaCallFactory(roleBasedHttpClientBuilder.okHttpClientForImage(it)) },
|
||||
thumbnailCache = thumbnailDiskCache,
|
||||
backgroundScope = applicationIOScope,
|
||||
readAuth = blossomReadAuthTokens,
|
||||
@@ -1341,8 +1347,7 @@ class AppModules(
|
||||
state.account.settings.localBlossomCacheProfilePicturesOnly
|
||||
.drop(1),
|
||||
).collect {
|
||||
blossomResolver.uriToUrlCache.evictAll()
|
||||
blossomResolver.blossomHitCache.cache.evictAll()
|
||||
blossomResolver.clearCaches()
|
||||
localBlossomCacheProbe.invalidate()
|
||||
// Re-probe immediately so enabling the feature activates it
|
||||
// this session. Otherwise `available` only advances when a
|
||||
@@ -1358,8 +1363,14 @@ class AppModules(
|
||||
}
|
||||
applicationIOScope.launch {
|
||||
localBlossomCacheProbe.available.drop(1).collect {
|
||||
blossomResolver.uriToUrlCache.evictAll()
|
||||
blossomResolver.blossomHitCache.cache.evictAll()
|
||||
blossomResolver.clearCaches()
|
||||
}
|
||||
}
|
||||
// A resolution to the local cache made while a media type was not Tor-routed must not
|
||||
// survive the user switching it to Tor: the cache would keep fetching it outside Tor.
|
||||
applicationIOScope.launch {
|
||||
torPrefs.value.propertyWatchFlow.drop(1).collect {
|
||||
blossomResolver.clearCaches()
|
||||
}
|
||||
}
|
||||
// Warm the local-cache probe so the very first image load doesn't pay
|
||||
@@ -1367,6 +1378,22 @@ class AppModules(
|
||||
applicationIOScope.launch {
|
||||
localBlossomCacheProbe.isAvailable()
|
||||
}
|
||||
// Re-checks the local cache about once a minute while the feature is on, so the bridge
|
||||
// comes back when the cache app is restarted and turns off when it is closed, even while
|
||||
// no `blossom:` URI is being resolved. A loopback HEAD never wakes the radio.
|
||||
applicationIOScope.launch {
|
||||
while (true) {
|
||||
delay(LOCAL_BLOSSOM_CACHE_RECHECK_MS)
|
||||
if (sessionManager
|
||||
.loggedInAccount()
|
||||
?.settings
|
||||
?.useLocalBlossomCache
|
||||
?.value == true
|
||||
) {
|
||||
localBlossomCacheProbe.isAvailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warms the video cache off the main thread. SimpleCache's constructor opens a SQLite
|
||||
// index over StandaloneDatabaseProvider and walks every cached span on disk — up to a
|
||||
@@ -1505,5 +1532,8 @@ class AppModules(
|
||||
* check and burn CPU on a heap that has nothing left to give.
|
||||
*/
|
||||
private const val MIN_RECLAIM_INTERVAL_MS = 120_000L
|
||||
|
||||
/** How often the local Blossom cache is re-probed while the feature is enabled. */
|
||||
private const val LOCAL_BLOSSOM_CACHE_RECHECK_MS = 60_000L
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_connect_requests_title
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
class SignerConnectActivity : ComponentActivity() {
|
||||
|
||||
+3
-3
@@ -95,12 +95,12 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_select_no
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_signing_as
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_messages_with
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.call.CallSessionBridge
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
|
||||
@@ -118,9 +118,6 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.AccountMarmotActions
|
||||
import com.vitorpamplona.amethyst.model.AccountRelayGroupActions
|
||||
import com.vitorpamplona.amethyst.model.EventBroadcaster
|
||||
import com.vitorpamplona.amethyst.model.algoFeeds.FavoriteAlgoFeedsOrchestrator
|
||||
import com.vitorpamplona.amethyst.model.bolt12Offers.Bolt12OfferListState
|
||||
import com.vitorpamplona.amethyst.model.buzz.ChannelInvitesState
|
||||
@@ -1431,6 +1428,9 @@ class Account(
|
||||
// wraps still register with the delivery-ticks tracker at publish time.
|
||||
// Null for restart-restored jobs, whose rumor id wasn't persisted.
|
||||
displayedNoteId: HexKey? = null,
|
||||
// Runs once the mined wraps are handed to the relays (e.g. to drop the
|
||||
// composer's draft). Not persisted: a restart-restored job skips it.
|
||||
onPublished: suspend () -> Unit = {},
|
||||
): Boolean {
|
||||
val queue = powQueue() ?: return false
|
||||
if (seals.isEmpty()) return true
|
||||
@@ -1465,7 +1465,10 @@ class Account(
|
||||
}
|
||||
seals.map { NIP17Factory().wrapSeal(it, expirationDelta, templateConversion = mineWrap) }
|
||||
},
|
||||
publish = { wraps -> broadcastPrivately(wraps, displayedNoteId) },
|
||||
publish = { wraps ->
|
||||
broadcastPrivately(wraps, displayedNoteId)
|
||||
onPublished()
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -2947,13 +2950,24 @@ class Account(
|
||||
broadcastPrivately(NIP17Factory().createEncryptedFileNIP17(template, signer))
|
||||
}
|
||||
|
||||
override suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) {
|
||||
override suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) = sendNip17PrivateMessage(template) {}
|
||||
|
||||
/**
|
||||
* [onSent] runs once the wraps are handed to the relays — right away when
|
||||
* the message is not mined, after the nonce search when it is — so the
|
||||
* composer can keep its draft for as long as the message only exists in
|
||||
* the mining queue.
|
||||
*/
|
||||
suspend fun sendNip17PrivateMessage(
|
||||
template: EventTemplate<ChatMessageEvent>,
|
||||
onSent: suspend () -> Unit,
|
||||
) {
|
||||
val powDifficulty = powDifficultyFor(GiftWrapEvent.KIND)
|
||||
if (powDifficulty != null) {
|
||||
// See sendNip17EncryptedFile: sign inline, queue only wrap mining.
|
||||
val senderMessage = signer.sign(template)
|
||||
val seals = NIP17Factory().createSeals(senderMessage, senderMessage.groupMembers(), signer)
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderMessage.id)) {
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderMessage.id, onPublished = onSent)) {
|
||||
// The wraps publish only after mining, but the user has already
|
||||
// replied — advance the read marker now.
|
||||
markDmRoomAsRead(senderMessage)
|
||||
@@ -2962,6 +2976,7 @@ class Account(
|
||||
}
|
||||
|
||||
broadcastPrivately(NIP17Factory().createMessageNIP17(template, signer))
|
||||
onSent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2973,10 +2988,13 @@ class Account(
|
||||
*
|
||||
* [powOverrideDifficulty] is the composer chip's per-post override:
|
||||
* null follows the account's gift-wrap setting, 0 disables mining.
|
||||
* [onSent] runs once the wraps are handed to the relays (after mining,
|
||||
* when mined).
|
||||
*/
|
||||
suspend fun sendPrivateNote(
|
||||
template: EventTemplate<TextNoteEvent>,
|
||||
powOverrideDifficulty: Int? = null,
|
||||
onSent: suspend () -> Unit = {},
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -2986,10 +3004,11 @@ class Account(
|
||||
val senderNote = signer.sign(template)
|
||||
val recipients = senderNote.taggedUserIds().plus(signer.pubKey).toSet()
|
||||
val seals = NIP17Factory().createSeals(senderNote, recipients, signer)
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderNote.id)) return
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderNote.id, onPublished = onSent)) return
|
||||
}
|
||||
|
||||
broadcastPrivately(NIP17Factory().createNoteNIP17(template, signer))
|
||||
onSent()
|
||||
}
|
||||
|
||||
override suspend fun sendGiftWraps(wraps: List<GiftWrapEvent>) {
|
||||
|
||||
@@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_consent_deny_always
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_hide_event
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_not_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_show_event
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ import java.util.concurrent.TimeUnit
|
||||
* to as ACCEPTED.
|
||||
*
|
||||
* The work is bounded — scans LocalCache (which is bounded by the relay subscription) and
|
||||
* consults [CalendarReminderStore] to skip events that have already been notified for. Run as
|
||||
* consults [CalendarReminderLogStore] to skip events that have already been notified for. Run as
|
||||
* a 15-minute periodic worker: that's the WorkManager minimum and matches the resolution of
|
||||
* the reminder UI ("starts in ~15 min" is the smallest interval users perceive as "soon").
|
||||
*
|
||||
|
||||
+4
-4
@@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.resources.crashreport_found
|
||||
import com.vitorpamplona.amethyst.commons.resources.crashreport_found_send
|
||||
import com.vitorpamplona.amethyst.commons.resources.would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared
|
||||
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
+26
-6
@@ -42,6 +42,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,18 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
/** When non-null, re-render the card on this cadence (for clock-driven text like "time left"). */
|
||||
protected open val refreshMs: Long? = null
|
||||
|
||||
/**
|
||||
* How long to keep the service (and whatever it holds up) alive after [isActive] turns
|
||||
* false, before stopping. Lets work that was just handed off asynchronously — e.g. a
|
||||
* broadcast still leaving through the relay pool — finish before the process loses its
|
||||
* foreground protection and gets frozen. A new active emission during the grace keeps
|
||||
* the service running. Asked each time the work drains, so it can depend on how it did.
|
||||
*/
|
||||
protected open fun stopGraceMs(): Long = 0L
|
||||
|
||||
/** The card shown during [stopGraceMs]; null keeps the last one. */
|
||||
protected open fun renderDraining(): Content? = null
|
||||
|
||||
protected abstract fun state(): StateFlow<T>
|
||||
|
||||
/** Keep the service (and notification) alive while this is true; stop once it goes false. */
|
||||
@@ -222,9 +235,15 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
onStarted()
|
||||
watchJob =
|
||||
scope.launch {
|
||||
state().collect { value ->
|
||||
state().collectLatest { value ->
|
||||
onEmission(value)
|
||||
if (!isActive(value)) {
|
||||
// collectLatest: a new active emission cancels this pending stop.
|
||||
val grace = stopGraceMs()
|
||||
if (grace > 0) {
|
||||
renderDraining()?.let { notify(buildNotification(it)) }
|
||||
delay(grace)
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} else {
|
||||
@@ -246,7 +265,7 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
|
||||
private fun startForegroundCompat(value: T) {
|
||||
ensureChannel()
|
||||
val notification = buildNotification(value)
|
||||
val notification = buildNotification(render(value))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, fgsType)
|
||||
} else {
|
||||
@@ -254,18 +273,19 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(value: T) {
|
||||
private fun updateNotification(value: T) = notify(buildNotification(render(value)))
|
||||
|
||||
private fun notify(notification: Notification) {
|
||||
val manager = NotificationManagerCompat.from(this)
|
||||
if (!manager.areNotificationsEnabled()) return
|
||||
try {
|
||||
manager.notify(notificationId, buildNotification(value))
|
||||
manager.notify(notificationId, notification)
|
||||
} catch (_: SecurityException) {
|
||||
// POST_NOTIFICATIONS revoked mid-flight; the FGS keeps running.
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(value: T): Notification {
|
||||
val content = render(value)
|
||||
private fun buildNotification(content: Content): Notification {
|
||||
val style =
|
||||
when (val bar = content.bar) {
|
||||
is Bar.Indeterminate -> NotificationCompat.ProgressStyle().setProgressIndeterminate(true)
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvi
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
|
||||
import com.vitorpamplona.quartz.utils.startsWithIgnoreCase
|
||||
import okhttp3.Call
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
@@ -44,18 +45,49 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
class BlossomFetcher(
|
||||
private val options: Options,
|
||||
private val data: Uri,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val blossomServerResolver: () -> BlossomServerResolver,
|
||||
private val networkFetcher: (url: String) -> Fetcher,
|
||||
private val networkFetcher: (url: String, diskCacheKey: String) -> Fetcher,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? =
|
||||
try {
|
||||
val urlResult = blossomServerResolver().findServers(data.toString())
|
||||
networkFetcher(urlResult?.serverUrl ?: data.toString()).fetch()
|
||||
val uri = data.toString()
|
||||
val key = options.diskCacheKey ?: diskCacheKey(uri)
|
||||
|
||||
// A blob already on disk is served without resolving: resolution may HEAD
|
||||
// servers and wait on relays, and fails outright offline, where the bytes are
|
||||
// still sitting in the cache. NetworkFetcher reads the snapshot under [key]
|
||||
// before it ever looks at the url.
|
||||
val onDisk = options.diskCachePolicy.readEnabled && imageLoader.diskCache?.openSnapshot(key)?.use { true } ?: false
|
||||
val fromDisk =
|
||||
if (onDisk) {
|
||||
// Evicted between the check and the read: the fetcher would try the
|
||||
// network with the unresolvable `blossom:` uri, so resolve after all.
|
||||
try {
|
||||
networkFetcher(uri, key).fetch()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fromDisk ?: networkFetcher(blossomServerResolver().findServers(uri)?.serverUrl ?: uri, key).fetch()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Blossom blobs are content-addressed, so the sha256 names the bytes wherever they
|
||||
* were served from. Keying on it (instead of the resolved server URL) finds the
|
||||
* blob again after a restart, a probe flip or a different server answering.
|
||||
*/
|
||||
fun diskCacheKey(uri: String): String = BlossomUri.parse(uri)?.let { "blossom:${it.sha256}" } ?: uri
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
class Factory(
|
||||
val blossomServerResolver: () -> BlossomServerResolver,
|
||||
@@ -78,20 +110,19 @@ class BlossomFetcher(
|
||||
if (!isApplicable(data)) return null
|
||||
// Wrapped per resolved url (not per Factory) because the server the
|
||||
// blob actually lives on is only known once the resolver has run.
|
||||
return BlossomFetcher(options, data, blossomServerResolver) { url ->
|
||||
return BlossomFetcher(options, data, imageLoader, blossomServerResolver) { url, diskCacheKey ->
|
||||
val keyedOptions = options.copy(diskCacheKey = diskCacheKey)
|
||||
readAuthAware(url, readAuth) { authHeader ->
|
||||
NetworkFetcher(
|
||||
url = url,
|
||||
options = options.withAuthHeader(authHeader),
|
||||
options = keyedOptions.withAuthHeader(authHeader),
|
||||
networkClient = lazy { networkClient(url).asNetworkClient() },
|
||||
diskCache = lazy { imageLoader.diskCache },
|
||||
cacheStrategy = cacheStrategyLazy,
|
||||
connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) },
|
||||
concurrentRequestStrategy = concurrentRequestStrategyLazy,
|
||||
)
|
||||
// Keyed on the resolved server url, which is what the NetworkFetcher above
|
||||
// caches under -- not on the `blossom:` uri the request came in as.
|
||||
}.onSystemFileSystem(options.diskCacheKey ?: url)
|
||||
}.onSystemFileSystem(diskCacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -48,9 +48,8 @@ import com.vitorpamplona.amethyst.commons.service.image.BlurHashFetcher
|
||||
import com.vitorpamplona.amethyst.commons.service.image.ThumbHashFetcher
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.commons.sno.SnoFetcher
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.service.images.BlossomFetcher
|
||||
import com.vitorpamplona.amethyst.service.images.ProfilePictureFetcher
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -87,9 +86,10 @@ class ImageLoaderSetup {
|
||||
// coordinates through a map of in-flight fetches that it owns, so it only
|
||||
// works when every fetcher shares the same instance -- a fresh one per
|
||||
// request can never see anybody else's fetch and the de-dupe silently
|
||||
// no-ops. Shared across all three network-backed factories so a feed
|
||||
// image and the same blob reached through `blossom:` (or a profile
|
||||
// picture) still collapse onto one download.
|
||||
// no-ops. Shared across all three network-backed factories so the same
|
||||
// key requested through any of them (e.g. a feed image and a profile
|
||||
// picture with the same URL) collapses onto one download. `blossom:`
|
||||
// blobs are keyed by their sha256 instead (see BlossomFetcher.diskCacheKey).
|
||||
val concurrentRequests = DeDupeConcurrentRequestStrategy()
|
||||
|
||||
SingletonImageLoader.setUnsafe(
|
||||
@@ -108,11 +108,13 @@ class ImageLoaderSetup {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
add(Base64Fetcher.Factory)
|
||||
add(BlurHashFetcher.Factory)
|
||||
add(SnoFetcher.Factory)
|
||||
add(ThumbHashFetcher.Factory)
|
||||
add(BlossomFetcher.Factory(blossomServerResolver, callFactory, concurrentRequests, readAuth))
|
||||
add(ProfilePictureFetcher.Factory(thumbnailCache, callFactory, backgroundScope, concurrentRequests, readAuth))
|
||||
add(Base64Fetcher.BKeyer)
|
||||
add(BlurHashFetcher.BKeyer)
|
||||
add(SnoFetcher.SKeyer)
|
||||
add(ThumbHashFetcher.TKeyer)
|
||||
add(ProfilePictureFetcher.BKeyer)
|
||||
add(OkHttpFactory(callFactory, concurrentRequests, readAuth))
|
||||
|
||||
+4
-1
@@ -36,6 +36,8 @@ import coil3.network.NetworkFetcher
|
||||
import coil3.network.okhttp.asNetworkClient
|
||||
import coil3.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvider
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomCacheRedirectInterceptor.Companion.PROFILE_PICTURE
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomMediaCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ProfilePictureUrl
|
||||
@@ -121,7 +123,8 @@ class ProfilePictureFetcher(
|
||||
NetworkFetcher(
|
||||
url = data.url,
|
||||
options = options.withAuthHeader(authHeader),
|
||||
networkClient = lazy { networkClient(data.url).asNetworkClient() },
|
||||
// Marked so the local Blossom cache bridge can honour "profile pictures only".
|
||||
networkClient = lazy { LocalBlossomMediaCallFactory(networkClient(data.url), PROFILE_PICTURE).asNetworkClient() },
|
||||
diskCache = diskCacheLazy,
|
||||
cacheStrategy = cacheStrategyLazy,
|
||||
connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) },
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -38,14 +38,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.videoGalleryModifier
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
|
||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_off
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_unnamed_track
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.Locale
|
||||
|
||||
+5
-5
@@ -41,11 +41,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.enter_picture_in_picture
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.state.rememberProgressStateWithTickCount
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+6
-6
@@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.muted_button
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.VolumeBottomIconSize
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Preview
|
||||
|
||||
+9
-9
@@ -50,16 +50,16 @@ import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.playback_actions_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_player_settings_action_fullscreen
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.VolumeBottomIconSize
|
||||
import com.vitorpamplona.amethyst.model.VideoPlayerAction
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
||||
+6
-6
@@ -40,12 +40,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.ui.note.EnablePiP
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.EnablePiP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+5
-5
@@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.pause
|
||||
import com.vitorpamplona.amethyst.commons.resources.play
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PlayIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PlayIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -45,9 +45,9 @@ import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.state.rememberPlaybackSpeedState
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.ui.compose.state.rememberProgressStateWithTickInterval
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Preview
|
||||
|
||||
+6
-6
@@ -64,6 +64,12 @@ import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.model.VideoButtonLocation
|
||||
import com.vitorpamplona.amethyst.model.VideoPlayerAction
|
||||
import com.vitorpamplona.amethyst.service.cast.CastRequest
|
||||
@@ -78,12 +84,6 @@ import com.vitorpamplona.amethyst.ui.components.ShareMediaAction
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
+6
-6
@@ -52,12 +52,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_download_has_started_toast
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Preview
|
||||
|
||||
+6
-6
@@ -42,12 +42,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.skip_back
|
||||
import com.vitorpamplona.amethyst.commons.resources.skip_forward
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.SKIP_SECONDS
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
|
||||
private val FadeIn = fadeIn()
|
||||
private val FadeOut = fadeOut()
|
||||
|
||||
+4
-4
@@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_settings_video_quality
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_quality_auto
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.Locale
|
||||
|
||||
+1
-1
@@ -42,8 +42,8 @@ import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
|
||||
+1
-1
@@ -40,6 +40,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.ContentFrame
|
||||
import androidx.media3.ui.compose.state.rememberMuteButtonState
|
||||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
@@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.constrain
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
|
||||
+6
-1
@@ -39,6 +39,7 @@ import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DynamicCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomCacheRedirectInterceptor
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder
|
||||
@@ -59,7 +60,11 @@ class PlaybackService : MediaSessionService() {
|
||||
okHttpClient: DynamicCallFactory,
|
||||
blossomServerResolver: BlossomServerResolver,
|
||||
): MediaSessionPool {
|
||||
val dataSourceFactory = OkHttpDataSource.Factory(okHttpClient)
|
||||
// Marked as media so the local Blossom cache may serve these downloads.
|
||||
val dataSourceFactory =
|
||||
OkHttpDataSource
|
||||
.Factory(okHttpClient)
|
||||
.setDefaultRequestProperties(mapOf(LocalBlossomCacheRedirectInterceptor.MEDIA_HEADER to LocalBlossomCacheRedirectInterceptor.MEDIA))
|
||||
|
||||
val resolvingDataSourceFactory: DataSource.Factory =
|
||||
ResolvingDataSource.Factory(
|
||||
|
||||
+67
@@ -22,15 +22,20 @@ package com.vitorpamplona.amethyst.service.pow
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.PowerManager
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobPhase
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState
|
||||
import com.vitorpamplona.amethyst.service.foreground.FlowProgressForegroundService
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -44,6 +49,17 @@ import kotlinx.coroutines.launch
|
||||
* unmined resumes on the next app launch. Started on every enqueue (the app
|
||||
* is necessarily in the foreground then), stops itself when the queue drains.
|
||||
*
|
||||
* Staying schedulable is not enough on its own once the user also locks the
|
||||
* screen or leaves the app, so while it runs the service also:
|
||||
* - holds a partial wake lock, or the CPU suspends with the screen off and the
|
||||
* nonce search stalls until the phone is unlocked;
|
||||
* - keeps the relay pool (and Tor) up. The UI stops holding them ~30 s after
|
||||
* the app is backgrounded, so a post mined after that would only reach the
|
||||
* in-memory outbox — while its checkpoint and draft were already deleted —
|
||||
* and die with the process;
|
||||
* - lingers [PUBLISH_GRACE_MS] after the queue drains, so the event that was
|
||||
* just handed to the pool actually leaves before the process is frozen.
|
||||
*
|
||||
* The notification card (a live [androidx.core.app.NotificationCompat.ProgressStyle]) and all the
|
||||
* service lifecycle live in [FlowProgressForegroundService]; this subclass only maps mining state
|
||||
* to that card.
|
||||
@@ -68,17 +84,60 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
private var sessionTotal = 0
|
||||
private var lastQueueSize = 0
|
||||
|
||||
// Whether the last non-empty queue still had a job publishing: a queue that drains by
|
||||
// publishing needs the grace for the event to leave; one drained by cancel does not.
|
||||
private var drainedByPublishing = false
|
||||
|
||||
override fun stopGraceMs() = if (drainedByPublishing) PUBLISH_GRACE_MS else 0L
|
||||
|
||||
override fun renderDraining() = Content(stringRes(this, R.string.pow_notification_sending), null, Bar.Indeterminate)
|
||||
|
||||
// Benchmarked once per service run (~250 ms, cached by the estimator).
|
||||
@Volatile
|
||||
private var hashRate: Double? = null
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
running = true
|
||||
|
||||
wakeLock =
|
||||
runCatching {
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "amethyst:pow-mining")
|
||||
.apply {
|
||||
setReferenceCounted(false)
|
||||
acquire(WAKE_LOCK_TIMEOUT_MS)
|
||||
}
|
||||
}.onFailure { Log.w(TAG, "Could not acquire the mining wake lock", it) }
|
||||
.getOrNull()
|
||||
|
||||
// Re-arm the timeout while the service lives: before Android 14 there is no FGS budget
|
||||
// and a hard nonce can mine for hours, but a lock with no timeout could outlive a
|
||||
// service that died without onDestroy.
|
||||
scope.launch {
|
||||
while (true) {
|
||||
delay(WAKE_LOCK_TIMEOUT_MS / 2)
|
||||
wakeLock?.let { if (it.isHeld) it.acquire(WAKE_LOCK_TIMEOUT_MS) }
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors what the resumed UI collects (AccountScreen's ManageRelayServices +
|
||||
// ManageWebOkHttp): subscribed, they keep the relay pool connected and, through
|
||||
// the connector's combine and the proxy-port provider, Tor up.
|
||||
val app = Amethyst.instance
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch { app.relayProxyClientConnector.relayServices.collect {} }
|
||||
launch { app.okHttpClients.defaultHttpClient.collect {} }
|
||||
launch { app.okHttpClients.defaultHttpClientWithoutProxy.collect {} }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
running = false
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
wakeLock = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -97,6 +156,7 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
}
|
||||
|
||||
override fun onEmission(value: ImmutableList<PoWJobState>) {
|
||||
if (value.isNotEmpty()) drainedByPublishing = value.any { it.phase == PoWJobPhase.PUBLISHING }
|
||||
if (value.size > lastQueueSize) sessionTotal += value.size - lastQueueSize
|
||||
lastQueueSize = value.size
|
||||
}
|
||||
@@ -155,6 +215,13 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
|
||||
private const val PROGRESS_REFRESH_MS = 30_000L
|
||||
|
||||
// Time for a just-mined post to connect, send and get its OK before the service
|
||||
// lets go of the relays and the process becomes freezable.
|
||||
private const val PUBLISH_GRACE_MS = 20_000L
|
||||
|
||||
// Safety net only: onDestroy releases it, and the service re-arms it while alive.
|
||||
private const val WAKE_LOCK_TIMEOUT_MS = 10 * 60 * 1000L
|
||||
|
||||
// Best-effort de-dup for start(): the queue calls it on EVERY enqueue.
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
+3
-3
@@ -97,15 +97,15 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_read_venue
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_send_dm
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread_with
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
+2
-2
@@ -25,9 +25,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.notify.NotifyRequestsCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_required_title
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
|
||||
@Composable
|
||||
|
||||
+4
-4
@@ -42,13 +42,13 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
|
||||
import com.vitorpamplona.amethyst.commons.resources.notify_block_relay
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
|
||||
@Composable
|
||||
fun NotifyRequestDialog(
|
||||
|
||||
+3
-3
@@ -45,11 +45,11 @@ import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_conn_t
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_reconnects
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_wakelock
|
||||
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
+33
-13
@@ -23,12 +23,14 @@ package com.vitorpamplona.amethyst.service.resourceusage
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -97,24 +99,29 @@ class UsageCountingInterceptor(
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
// Loopback traffic (LocalBlossomCacheRedirectInterceptor rewrites cache
|
||||
// hits to 127.0.0.1) never touches the radio: counting it would inflate
|
||||
// the network numbers — and could trip the background-data alert — for
|
||||
// exactly the users who cache aggressively to SAVE data.
|
||||
// Loopback traffic never touches the radio: counting it would inflate the
|
||||
// network numbers — and could trip the background-data alert — for exactly
|
||||
// the users who cache aggressively to SAVE data. This interceptor is the
|
||||
// outermost one, so it sees URLs BEFORE LocalBlossomCacheRedirectInterceptor
|
||||
// rewrites them to 127.0.0.1; the host that was actually contacted is only
|
||||
// known from the response's request.
|
||||
if (isLoopback(request.url.host)) return chain.proceed(request)
|
||||
|
||||
val role = request.tag(UsageRoleTag::class.java)?.role ?: defaultRole
|
||||
|
||||
accountant.add(UsageKeys.netReqs(role, isMobile(), isForeground()), 1)
|
||||
bursts?.onHttpActivity()
|
||||
|
||||
val requestBytes = request.body?.contentLength()?.coerceAtLeast(0L) ?: 0L
|
||||
if (requestBytes > 0) {
|
||||
accountant.add(UsageKeys.net(role, isMobile(), isForeground(), received = false), requestBytes)
|
||||
}
|
||||
|
||||
val startedAtMs = nowMs()
|
||||
val response = chain.proceed(request)
|
||||
val response =
|
||||
try {
|
||||
chain.proceed(request)
|
||||
} catch (e: IOException) {
|
||||
countRequest(request, role)
|
||||
throw e
|
||||
}
|
||||
|
||||
if (isLoopback(response.request.url.host)) return response
|
||||
|
||||
countRequest(request, role)
|
||||
|
||||
return response
|
||||
.newBuilder()
|
||||
.body(
|
||||
@@ -131,6 +138,19 @@ class UsageCountingInterceptor(
|
||||
).build()
|
||||
}
|
||||
|
||||
private fun countRequest(
|
||||
request: Request,
|
||||
role: String,
|
||||
) {
|
||||
accountant.add(UsageKeys.netReqs(role, isMobile(), isForeground()), 1)
|
||||
bursts?.onHttpActivity()
|
||||
|
||||
val requestBytes = request.body?.contentLength()?.coerceAtLeast(0L) ?: 0L
|
||||
if (requestBytes > 0) {
|
||||
accountant.add(UsageKeys.net(role, isMobile(), isForeground(), received = false), requestBytes)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** OkHttp reports IPv6 hosts unbracketed ("::1"); keep the bracketed form defensively. */
|
||||
fun isLoopback(host: String): Boolean = host.startsWith("127.") || host == "localhost" || host == "::1" || host == "[::1]"
|
||||
|
||||
+1
-1
@@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification_sent_title
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPost
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostNotifier
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.extractContentPreview
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.extractContentPreview
|
||||
|
||||
/**
|
||||
* Android [ScheduledPostNotifier]: posts a user-visible system notification when a
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.MimeTypes
|
||||
import com.davotoula.lightcompressor.video.GifToMp4Converter
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.uploads.isAvif
|
||||
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import id.zelory.compressor.Compressor
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||
import com.vitorpamplona.amethyst.commons.service.image.BlurhashWrapper
|
||||
import com.vitorpamplona.amethyst.commons.service.image.ThumbhashWrapper
|
||||
import com.vitorpamplona.amethyst.commons.thumbhash.toThumbhash
|
||||
import com.vitorpamplona.amethyst.service.uploads.isAvif
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
+78
-21
@@ -28,17 +28,23 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isValid
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.firstNotNullOrNullAsync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class BlossomServerResolver(
|
||||
val loggedInUsers: () -> List<HexKey>,
|
||||
@@ -46,10 +52,20 @@ class BlossomServerResolver(
|
||||
val httpClientBuilder: IRoleBasedHttpClientBuilder,
|
||||
val useLocalBlossomCache: () -> Boolean = { false },
|
||||
val localCacheProbe: LocalBlossomCacheProbe? = null,
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
||||
) {
|
||||
val blossomHitCache: ServerHeadCache = ServerHeadCache()
|
||||
val uriToUrlCache = LruCache<String, BlossomUriServer>(200)
|
||||
|
||||
// Unresolvable URIs, with when they failed. Without this every retry — each ExoPlayer
|
||||
// load attempt blocks a loader thread on it — paid the full resolution timeout again.
|
||||
private val missCache = LruCache<String, Long>(200)
|
||||
|
||||
// One resolution per URI at a time: the feed preview, Coil and the player asking for the
|
||||
// same `blossom:` URI together share it instead of each firing its own HEADs and relay
|
||||
// subscriptions. Runs in [scope] so a caller that goes away doesn't cancel it for the rest.
|
||||
private val inFlight = ConcurrentHashMap<String, Deferred<BlossomUriServer?>>()
|
||||
|
||||
class BlossomUriServer(
|
||||
val uri: BlossomUri,
|
||||
val serverUrl: String,
|
||||
@@ -57,41 +73,71 @@ class BlossomServerResolver(
|
||||
|
||||
fun cachedFindServer(uriStr: String): BlossomUriServer? = uriToUrlCache[uriStr]
|
||||
|
||||
// Bumped by [clearCaches]. A resolution that started before a clear (e.g. it picked the
|
||||
// local cache just before the cache went down) must not write its stale answer back.
|
||||
private val generation = AtomicInteger(0)
|
||||
|
||||
/** Forgets every resolution, e.g. when the local cache comes up or goes down. */
|
||||
fun clearCaches() {
|
||||
generation.incrementAndGet()
|
||||
uriToUrlCache.evictAll()
|
||||
missCache.evictAll()
|
||||
blossomHitCache.cache.evictAll()
|
||||
}
|
||||
|
||||
suspend fun findServers(uriStr: String): BlossomUriServer? {
|
||||
uriToUrlCache[uriStr]?.let { return it }
|
||||
missCache[uriStr]?.let { failedAt ->
|
||||
if (TimeUtils.nowMillis() - failedAt < MISS_TTL_MS) return null
|
||||
}
|
||||
|
||||
// Confined to Dispatchers.IO: this is reached from Compose
|
||||
// `produceState`/`LaunchedEffect` (RichTextViewer, MarmotGroupIconDisplay),
|
||||
// which run on the main dispatcher. The pre-suspension work here —
|
||||
// BlossomUri parsing, LruCache lookups, the local-cache probe's client
|
||||
// build, and the server-list flow setup — must stay off the UI thread.
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(10000) {
|
||||
findServersInner(uriStr)
|
||||
val resolution =
|
||||
inFlight.computeIfAbsent(uriStr) {
|
||||
// Confined to Dispatchers.IO: this is reached from Compose
|
||||
// `produceState`/`LaunchedEffect` (RichTextViewer, MarmotGroupIconDisplay),
|
||||
// which run on the main dispatcher. The pre-suspension work here —
|
||||
// BlossomUri parsing, LruCache lookups, the local-cache probe's client
|
||||
// build, and the server-list flow setup — must stay off the UI thread.
|
||||
scope.async(Dispatchers.IO) {
|
||||
val startedAt = generation.get()
|
||||
try {
|
||||
val result = withTimeoutOrNull(RESOLVE_TIMEOUT_MS) { findServersInner(uriStr) }
|
||||
// Cleared while resolving: answer the waiting callers, remember nothing.
|
||||
if (startedAt == generation.get()) {
|
||||
if (result != null) {
|
||||
uriToUrlCache.put(uriStr, result)
|
||||
} else {
|
||||
missCache.put(uriStr, TimeUtils.nowMillis())
|
||||
}
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
inFlight.remove(uriStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
uriToUrlCache.put(uriStr, result)
|
||||
}
|
||||
|
||||
return result
|
||||
return resolution.await()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun findServersInner(uriStr: String): BlossomUriServer? {
|
||||
val uri = BlossomUri.parse(uriStr) ?: return null
|
||||
|
||||
if (useLocalBlossomCache() && localCacheProbe?.isAvailable() == true) {
|
||||
val expectedMimeType = mimeTypeMap[uri.extension]
|
||||
|
||||
// Same rule as the HTTP-level bridge, which Tor-routed clients don't carry: media the
|
||||
// user sends through Tor must not be handed to the local cache, which would fetch it
|
||||
// from the origin outside Tor.
|
||||
if (useLocalBlossomCache() && !isTorRouted(uri, expectedMimeType) && localCacheProbe?.isAvailable() == true) {
|
||||
return BlossomUriServer(uri, uri.toLocalCacheUrl(LocalBlossomCacheProbe.LOCAL_CACHE_BASE))
|
||||
}
|
||||
|
||||
val expectedMimeType = mimeTypeMap[uri.extension]
|
||||
val filename = uri.filename()
|
||||
|
||||
if (uri.servers.isNotEmpty()) {
|
||||
val workingUrl = firstWorkingUrl(uri.servers, filename, expectedMimeType, uri.size)
|
||||
// Bounded well under RESOLVE_TIMEOUT_MS so a hint server that drops packets
|
||||
// leaves time for the author's own server list below.
|
||||
val workingUrl = firstWorkingUrl(uri.servers, filename, expectedMimeType, uri.size, XS_TIMEOUT_MS)
|
||||
if (workingUrl != null) {
|
||||
return BlossomUriServer(uri, workingUrl)
|
||||
}
|
||||
@@ -135,13 +181,20 @@ class BlossomServerResolver(
|
||||
filename: String,
|
||||
expectedMimeType: String?,
|
||||
expectedSize: Long?,
|
||||
timeoutMs: Long = RESOLVE_TIMEOUT_MS,
|
||||
): String? =
|
||||
firstNotNullOrNullAsync(servers, 10000) {
|
||||
firstNotNullOrNullAsync(servers, timeoutMs) {
|
||||
blossomHitCache.urlIfServerHasFile(it, filename, expectedMimeType, expectedSize) { url ->
|
||||
client(url, expectedMimeType)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether this blob's media type would be fetched through Tor from a regular (clearnet) server. */
|
||||
private fun isTorRouted(
|
||||
uri: BlossomUri,
|
||||
mimeType: String?,
|
||||
): Boolean = client(uri.toServerUrl() ?: "https://blossom.invalid/${uri.filename()}", mimeType).proxy != null
|
||||
|
||||
fun client(
|
||||
url: String,
|
||||
mimeType: String?,
|
||||
@@ -153,9 +206,13 @@ class BlossomServerResolver(
|
||||
else -> httpClientBuilder.okHttpClientForPreview(url)
|
||||
}
|
||||
|
||||
fun canResolve(scheme: String) = scheme == SCHEME
|
||||
fun canResolve(scheme: String) = scheme.equals(SCHEME, ignoreCase = true)
|
||||
|
||||
companion object {
|
||||
const val SCHEME = "blossom"
|
||||
|
||||
private const val RESOLVE_TIMEOUT_MS = 10_000L
|
||||
private const val XS_TIMEOUT_MS = 4_000L
|
||||
private const val MISS_TTL_MS = 30_000L
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -32,6 +32,7 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Discovers a local Blossom cache running on `http://127.0.0.1:24242` per
|
||||
@@ -48,6 +49,9 @@ class LocalBlossomCacheProbe(
|
||||
@Volatile
|
||||
private var cachedAtMs: Long = 0L
|
||||
|
||||
// Bumped by [markUnavailable]; lets a probe that raced it discard its stale positive result.
|
||||
private val unavailableMarks = AtomicInteger(0)
|
||||
|
||||
private val _available = MutableStateFlow(false)
|
||||
val available: StateFlow<Boolean> = _available
|
||||
|
||||
@@ -66,7 +70,11 @@ class LocalBlossomCacheProbe(
|
||||
return@withLock _available.value
|
||||
}
|
||||
|
||||
val startedAt = unavailableMarks.get()
|
||||
val newResult = probe()
|
||||
// A refused connection reported while this probe was in flight is newer evidence
|
||||
// than a HEAD that may have succeeded just before the cache went away.
|
||||
if (newResult && startedAt != unavailableMarks.get()) return@withLock false
|
||||
_available.value = newResult
|
||||
cachedAtMs = currentTimeMs()
|
||||
newResult
|
||||
@@ -80,6 +88,17 @@ class LocalBlossomCacheProbe(
|
||||
cachedAtMs = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that the cache just refused a connection. Flipping [available] right away turns
|
||||
* the bridge off everywhere it is read, instead of letting every sha256 URL keep failing
|
||||
* against the dead loopback port until the positive TTL runs out and someone re-probes.
|
||||
*/
|
||||
fun markUnavailable() {
|
||||
unavailableMarks.incrementAndGet()
|
||||
cachedAtMs = currentTimeMs()
|
||||
_available.value = false
|
||||
}
|
||||
|
||||
// Confined to Dispatchers.IO because callers reach this through suspend
|
||||
// resolvers invoked from Compose `LaunchedEffect`/`produceState`, which run
|
||||
// on the main dispatcher: building the OkHttp client and issuing the HEAD
|
||||
|
||||
+11
-5
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads.blossom.bud10
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
@@ -56,13 +56,18 @@ class ServerHeadCache {
|
||||
|
||||
client(url).newCall(request).executeAsync().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
cache.put(url, HasFile.NoFile)
|
||||
// Only a definitive answer is remembered. A 5xx, 429 or auth challenge is
|
||||
// transient: caching it would hide this server for the app's lifetime.
|
||||
if (response.code == 404 || response.code == 410) {
|
||||
cache.put(url, HasFile.NoFile)
|
||||
}
|
||||
return HasFile.NoFile
|
||||
}
|
||||
|
||||
// Retrieve the "Content-Length" header
|
||||
val contentLength = response.header("Content-Length")?.toLongOrNull()
|
||||
val mimeType = response.header("Content-Type")?.toMediaType()?.toString()
|
||||
// type/subtype only: a `; charset=binary` parameter must not fail the match below.
|
||||
val mimeType = response.header("Content-Type")?.toMediaTypeOrNull()?.let { "${it.type}/${it.subtype}" }
|
||||
|
||||
return if (contentLength != null && mimeType != null) {
|
||||
val result = HasFile.TypeAndSize(mimeType, contentLength)
|
||||
@@ -75,7 +80,8 @@ class ServerHeadCache {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
cache.put(url, HasFile.NoFile)
|
||||
// Offline, DNS failure, timeout: says nothing about the server having the file,
|
||||
// so it is not cached — a brief outage must not hide the blob until eviction.
|
||||
return HasFile.NoFile
|
||||
}
|
||||
}
|
||||
@@ -104,7 +110,7 @@ class ServerHeadCache {
|
||||
if (result.size == expectedSize) {
|
||||
return url
|
||||
}
|
||||
if (expectedSize == null && result.size > 0 && result.mimeType == expectedMimeType) {
|
||||
if (expectedSize == null && result.size > 0 && result.mimeType.equals(expectedMimeType, ignoreCase = true)) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.debugState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -39,7 +40,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETT
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForPointer
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NowProvider
|
||||
|
||||
+10
-127
@@ -20,25 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.collection.mutableScatterMapOf
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
@@ -50,116 +37,12 @@ fun <T> CrossfadeIfEnabled(
|
||||
label: String = "Crossfade",
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (accountViewModel.settings.isPerformanceMode()) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
DeferredCrossfade(targetState, modifier, contentAlignment, animationSpec, label, content)
|
||||
}
|
||||
}
|
||||
|
||||
/** Latches the first time a crossfade's target moves off the value it was composed with. */
|
||||
private class ChangeLatch {
|
||||
var changed = false
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MyCrossfade] that does not build its [androidx.compose.animation.core.Transition] until there
|
||||
* is something to animate.
|
||||
*
|
||||
* `updateTransition` allocates a transition, its animation list and its seeking state on *first
|
||||
* composition*, even though first composition has nothing to cross-fade — target and initial state
|
||||
* are the same value. In a feed that is waste: every card scrolled in builds a transition per
|
||||
* animated element, and during a scroll essentially none of them run, because the underlying counts
|
||||
* and icons do not change in the second a card is on screen.
|
||||
*
|
||||
* So the plain content renders until the target actually moves. At that point the transition is
|
||||
* built seeded at the *original* value via [MutableTransitionState] and immediately re-targeted at
|
||||
* the new one, so the first real change still animates exactly as before; every later change
|
||||
* animates through the now-live transition normally.
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
internal fun <T> DeferredCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier,
|
||||
contentAlignment: Alignment,
|
||||
animationSpec: FiniteAnimationSpec<Float>,
|
||||
label: String,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val initial = remember { targetState }
|
||||
val latch = remember { ChangeLatch() }
|
||||
if (targetState != initial) latch.changed = true
|
||||
|
||||
if (!latch.changed) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
val transitionState = remember { MutableTransitionState(initial) }
|
||||
transitionState.targetState = targetState
|
||||
val transition = rememberTransition(transitionState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> MyCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val transition = updateTransition(targetState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun <T> Transition<T>.MyCrossfade(
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
contentKey: (targetState: T) -> Any? = { it },
|
||||
content: @Composable (targetState: T) -> Unit,
|
||||
) {
|
||||
val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }
|
||||
val contentMap = remember { mutableScatterMapOf<T, @Composable () -> Unit>() }
|
||||
// If not animating, just display the current state
|
||||
if (currentState == targetState && (currentlyVisible.size != 1 || currentlyVisible[0] != targetState)) {
|
||||
// Remove all the intermediate items from the list once the animation is finished.
|
||||
currentlyVisible.removeAll { it != targetState }
|
||||
contentMap.clear()
|
||||
}
|
||||
|
||||
if (targetState !in contentMap) {
|
||||
// Replace target with the same key if any
|
||||
val replacementId =
|
||||
currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) }
|
||||
if (replacementId == -1) {
|
||||
currentlyVisible.add(targetState)
|
||||
} else {
|
||||
currentlyVisible[replacementId] = targetState
|
||||
}
|
||||
contentMap.clear()
|
||||
currentlyVisible.fastForEach { stateForContent ->
|
||||
contentMap[stateForContent] = {
|
||||
val alpha by
|
||||
animateFloat(transitionSpec = { animationSpec }) {
|
||||
if (it == stateForContent) 1f else 0f
|
||||
}
|
||||
Box(Modifier.graphicsLayer { this.alpha = alpha }, contentAlignment) { content(stateForContent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment) {
|
||||
currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } }
|
||||
}
|
||||
}
|
||||
) = CrossfadeIfEnabled(
|
||||
targetState = targetState,
|
||||
enabled = !accountViewModel.settings.isPerformanceMode(),
|
||||
modifier = modifier,
|
||||
contentAlignment = contentAlignment,
|
||||
animationSpec = animationSpec,
|
||||
label = label,
|
||||
content = content,
|
||||
)
|
||||
|
||||
@@ -78,6 +78,19 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv
|
||||
import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
@@ -86,25 +99,12 @@ import com.vitorpamplona.amethyst.ui.components.BechLink
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
|
||||
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.copy_stack_to_clipboard
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg2
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@@ -71,20 +71,20 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
||||
+4
-4
@@ -75,13 +75,13 @@ import com.vitorpamplona.amethyst.commons.resources.twitter
|
||||
import com.vitorpamplona.amethyst.commons.resources.twitter_proof_url_template
|
||||
import com.vitorpamplona.amethyst.commons.resources.username
|
||||
import com.vitorpamplona.amethyst.commons.resources.website_url
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -29,9 +29,9 @@ import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.uploads.ConfirmationCallbacks
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun StrippingFailureDialog(confirmation: SuspendableConfirmation) {
|
||||
|
||||
+10
-10
@@ -63,20 +63,20 @@ import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_section_explai
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.invalid_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun Bolt12OffersScreen(
|
||||
|
||||
+14
-13
@@ -61,9 +61,15 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.mediaServers.ui.MediaServerEditField
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.draggableRelayItem
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.relayDragHandle
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_media_server
|
||||
@@ -88,20 +94,15 @@ import com.vitorpamplona.amethyst.commons.resources.media_servers_upload_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_blossom_server_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_default_servers
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.draggableRelayItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relayDragHandle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import com.vitorpamplona.quartz.utils.Rfc3986
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
|
||||
+6
-6
@@ -57,13 +57,13 @@ import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_profile_
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache_caption
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
fun AllMediaServersScreen(
|
||||
@@ -212,7 +212,7 @@ fun MediaCacheSection(accountViewModel: AccountViewModel) {
|
||||
*/
|
||||
@Composable
|
||||
private fun CacheDetectionChip(accountViewModel: AccountViewModel) {
|
||||
val probeAvailable by accountViewModel.useLocalBlossomBridgeForProfilePics
|
||||
val probeAvailable by accountViewModel.localBlossomCacheDetected
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
val color = if (probeAvailable) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.grayText
|
||||
|
||||
+7
-7
@@ -98,6 +98,7 @@ import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.back
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_delete_from_host
|
||||
@@ -127,16 +128,15 @@ import com.vitorpamplona.amethyst.commons.resources.manage_stored_files_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
+8
-7
@@ -62,6 +62,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.mediaServers.ui.MediaServerEditField
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_add_url_label
|
||||
@@ -81,14 +83,13 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_import_sources_secti
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_start_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_media_server
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
/** Vibrant palette for server monograms; picked deterministically from the host name. */
|
||||
private val ImportMonogramColors =
|
||||
|
||||
+3
-3
@@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_done
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_syncing
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomSyncState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/** How long the finished "Sync complete" banner lingers before it auto-dismisses. */
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server_description
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
||||
+12
-12
@@ -69,23 +69,23 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_relay_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_remove
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_use_defaults
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.HttpUrlFormatter
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent
|
||||
|
||||
|
||||
+10
-10
@@ -61,19 +61,19 @@ import com.vitorpamplona.amethyst.commons.resources.payment_target_type
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -38,8 +38,8 @@ import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message_description
|
||||
import com.vitorpamplona.amethyst.ui.components.ToggleableBox
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ToggleableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun RecordVoiceButton(
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_with_time
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Animated expanding circles that pulse outward from the recording button
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.upload_file
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
+1
-1
@@ -42,8 +42,8 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.upload_image
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.uploads.isAvif
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
+5
-5
@@ -72,17 +72,17 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.CloseIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size40Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.no_camera_app_found
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_video
|
||||
import com.vitorpamplona.amethyst.commons.resources.take_a_picture
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
+2
-2
@@ -48,9 +48,9 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing
|
||||
import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun UploadProgressIndicator(
|
||||
|
||||
+3
-3
@@ -35,9 +35,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_anonymize_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_anonymize_title
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
|
||||
@Composable
|
||||
fun VoiceAnonymizationSection(
|
||||
|
||||
+1
-1
@@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.commons.resources.re_record
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun VoicePresetSelector(
|
||||
|
||||
@@ -100,14 +100,14 @@ import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.RelayResult
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond
|
||||
import com.vitorpamplona.amethyst.service.pow.formatTimeLeft
|
||||
import com.vitorpamplona.amethyst.service.pow.powKindLabelRes
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
|
||||
+4
-4
@@ -84,10 +84,10 @@ import com.vitorpamplona.amethyst.commons.resources.timeout
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastEvent
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.RelayResult
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
|
||||
@@ -44,7 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_open_
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_video
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_voice
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
fun hasPermission(
|
||||
context: Context,
|
||||
|
||||
@@ -71,9 +71,9 @@ import com.vitorpamplona.amethyst.commons.resources.call_incoming
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_incoming_video
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_incoming_voice
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_reject
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.call.session.CallSession
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -52,12 +52,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_calling
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_duration_hours_minutes_seconds
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_duration_minutes_seconds
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import org.webrtc.VideoTrack
|
||||
|
||||
@@ -74,12 +74,12 @@ import com.vitorpamplona.amethyst.commons.resources.call_search_users
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_speaker
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_switch_camera
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_unmute
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.call.AudioRoute
|
||||
import com.vitorpamplona.amethyst.ui.call.session.CallSession
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.delay
|
||||
import org.webrtc.VideoTrack
|
||||
|
||||
@@ -33,13 +33,13 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cast_searching_for_devices
|
||||
import com.vitorpamplona.amethyst.commons.resources.cast_to_device_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.cast.CastRegistry
|
||||
import com.vitorpamplona.amethyst.service.cast.CastRequest
|
||||
import com.vitorpamplona.amethyst.service.cast.CastSessionState
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BuzzInviteLinkSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
||||
@@ -51,17 +52,17 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRenderer
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextInteractions
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextSegmentRenderer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextViewer as CommonsRichTextViewer
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,17 +59,17 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_redeem
|
||||
import com.vitorpamplona.amethyst.commons.resources.sats
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.OpenInNewIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuToken
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
+2
-2
@@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -58,17 +58,17 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_naddr_label
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.util.njumpLink
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
+1
-1
@@ -34,11 +34,11 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_pay_withdraw_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.lightning.LnWithdrawalUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
+3
-3
@@ -44,14 +44,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_join
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_subtitle
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.concord.cord05Invites.CommunityInvite
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon
|
||||
|
||||
|
||||
+5
-5
@@ -43,13 +43,13 @@ import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.show_more
|
||||
import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
|
||||
object ShowFullTextCache {
|
||||
val cache = LruCache<String, Boolean>(10)
|
||||
|
||||
+4
-4
@@ -39,13 +39,13 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.commons.util.prettyMime
|
||||
import com.vitorpamplona.amethyst.ui.components.pdf.extractFilename
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
|
||||
/**
|
||||
* The renderer for a declared file that none of the media viewers can display — a webxdc app,
|
||||
|
||||
+5
-5
@@ -76,12 +76,12 @@ import com.vitorpamplona.amethyst.commons.resources.media_download_has_started_t
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@@ -48,14 +48,14 @@ 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.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size6dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -44,10 +44,10 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.richtext.ImageGalleryParagraph
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
+2
-2
@@ -26,11 +26,11 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
|
||||
/**
|
||||
* Strategy for rendering a note quoted inline in another note's text (a
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user