Commit Graph

11984 Commits

Author SHA1 Message Date
Claude 22a418bea2 feat(cli): make FsEventStore the source of truth for relay events
Every event Amy observes — drained from a relay subscription,
unwrapped from a NIP-59 gift wrap, or generated locally for publish —
now flows through Context.verifyAndStore: NIP-01 id + signature check
via Event.verify(), then store.insert(). Bad events are dropped with
a stderr log and never reach command code; persistence failures are
logged but do not propagate, so a broken store never breaks a relay
subscription.

- Context.drain() now persists every received event before surfacing
  it to the caller. Existing callers (FeedCommand, DmCommands,
  ProfileCommands, Marmot sync) get caching for free.
- Context.publish() persists outbound events too, so the local store
  reflects what Amy has done even when every relay rejects.
- Three cache-first read helpers expose the most common lookups
  without hitting relays: profileOf(pubKey) → kind:0,
  relaysOf(pubKey) → kind:10002, contactsOf(pubKey) → kind:3.
- Class doc on Context spells out the contract; README.md gets a new
  "Local event store — the source of truth" section pointing at the
  on-disk layout and the design plans.
2026-04-25 01:40:05 +00:00
Claude 44470b4821 test(quartz): SQLite parity matrix for FsEventStore (step 10)
Drives both EventStore (SQLite reference) and FsEventStore with
identical event streams and asserts query() result sets match. SQLite
throws on blocked inserts (NIP-09 tombstones, NIP-62 vanish, NIP-40
expiration) while the FS store silently skips — both are observable
"event not persisted", so insertBoth() catches SQLite throws and
parity is judged on the post-insert state.

Coverage: id lookup, kind+author, since/until/limit, single-letter
tag query (OR within key), replaceable winner, replaceable older
rejected, addressable d-tag dedup, deletion by id (incl. block-
reinsert), deletion by address (incl. cutoff semantics), expiration
sweep, NIP-50 single-token search (sticking to ASCII where SQLite's
unicode61 tokenizer agrees with our port), count, multi-filter
union, delete by filter, mixed kitchen-sink scenario.

113 fs tests now green (97 unit + 16 parity).
2026-04-25 00:48:01 +00:00
Claude ea64f7e06d feat(cli): wire FsEventStore into amy Context (step 9)
- DataDir.eventsDir → "<root>/events-store" (avoids colliding with the
  marmot file name conventions).
- Context.store: lazy IEventStore over that directory. Lazy so the
  many existing commands that don't touch persistent event state pay
  zero open cost (no .lock, no seed file). Closed by Context.close()
  only when the lazy delegate has actually been initialised — checked
  via reflection so close() never accidentally forces it.
2026-04-25 00:47:48 +00:00
Claude 47e924ed40 feat(quartz): FsEventStore flock + transactions + scrub/compact (step 8)
- FsLockManager: cross-process exclusive flock(.lock) with per-thread
  re-entry. withWriteLock { body } acquires once on a fresh thread
  and reuses on nested calls — so transaction { insert(); insert() }
  doesn't self-deadlock.
- FsEventStore: insert / delete / delete(filter) / delete(filters) /
  delete(id) / deleteExpiredEvents / transaction now run under
  withWriteLock. The `*Locked` helpers expose the lock-free body for
  re-entrant callers (transaction body, vanish/deletion cascades,
  expiration sweep). close() releases the lock channel.
- scrub(): wipes idx/ and rebuilds every entry from the canonical
  events. Slots, tombstones and seed are left alone — slots can pin
  data the canonical pass doesn't see, and tombstone removal is a
  deliberate "un-forget" per the design plan.
- compact(): drops dangling idx/ entries whose canonical no longer
  exists. Cheap — only touches idx/, never opens a JSON.

Tests: 11 new in FsMaintenanceTest covering lock file presence,
transaction commit / propagated exception with kept-prior-events
semantics, re-entrant lock from inside a transaction, scrub
rebuilding idx + FTS after a manual wipe, scrub preserving the
replaceable slot, compact dropping dangling and leaving valid alone,
close idempotence + reopen, and two-thread concurrent insert
serialisation. 97 fs tests green.
2026-04-24 23:53:16 +00:00
Claude d5a806a5c3 feat(quartz): FsEventStore NIP-50 full-text search (step 7)
Adds idx/fts/<token>/<ts>-<id> hardlinks and an FTS-driven query path.

- FsSearchTokenizer: lowercase + Unicode-aware split on non letter-or-
  digit, matching SQLite FTS5's unicode61 default closely enough that
  the same call indexes content and parses queries (any drift cancels).
  Tokens capped at 100 chars to keep filenames under FS limits.
- FsLayout: idxFts + ftsEntry / ftsTokenDir helpers; skeleton dir.
- FsIndexer.pathsFor: when event implements SearchableEvent, emits one
  hardlink per unique tokenised word — so insert/delete maintenance
  rides the existing link/unlink path. Eviction (replaceable swap),
  NIP-09 cascade and NIP-62 vanish all clean up FTS for free.
- FsQueryPlanner: when filter.search is non-blank, drives by FTS.
  Tokenises the query, walks each idx/fts/<token>/ listing into a
  HashMap<id, ts>, and intersects smallest-first (AND across tokens —
  matching SQLite FTS5 default MATCH semantics). Output sorted by
  createdAt DESC. Other Filter fields (kinds, authors, tags, since /
  until) still apply via Filter.match post-filter.

Tests: 16 new in FsSearchTest covering tokenizer (whitespace, case,
unicode, punctuation, empty), index maintenance (entries created,
non-searchable kinds skipped, delete unlinks), and query semantics
(single token, AND of tokens, ordering, limit, kind/author compose,
no-match, blank string ignored, reopen). 86 fs tests green.
2026-04-24 23:44:43 +00:00
Claude 8c2b2f65a9 feat(quartz): FsEventStore NIP-62 right-to-vanish (step 6)
Adds tombstones/vanish/<owner_hex>.json hardlinks (one per owner,
strongest cutoff wins) plus the cascade and block-future-insert
semantics from SQLite's RightToVanishModule.

- FsLayout: vanishTombstonePath helper + tombstones/vanish/ skeleton.
- FsTombstones: vanishCutoff / installVanish / clearVanish — install
  uses atomic rename-with-REPLACE_EXISTING and only proceeds when the
  new kind-62's createdAt is strictly greater than any existing tomb.
- FsEventStore:
  - constructor now takes optional relay: NormalizedRelayUrl? to scope
    NIP-62 cascades (matches SQLiteEventStore's relay arg).
  - isBlockedByTombstone now also rejects events whose owner has an
    active vanish with createdAt >= event.createdAt — owner is the
    recipient for GiftWrap, matching pubkey_owner_hash semantics.
  - processVanish() runs after canonical write: skips if !shouldVanish-
    From(relay), installs the tombstone, then walks idx/owner/<hex>/
    and deletes every event with ts < vanish.createdAt. The vanish
    event itself survives (its ts equals the cutoff).

Tests: 11 new in FsVanishTest — relay-scoped cascade, different-relay
no-op, vanishFromEverywhere, block re-insert + newer-passes, equal
ts blocked, other authors unaffected, stronger cutoff wins, weaker
ignored, tombstone shares inode with kind-62 canonical, kind-62 stays
queryable. 70 fs tests green.
2026-04-24 23:31:14 +00:00
Claude 53cae2ed09 feat(quartz): FsEventStore NIP-40 expiration (step 5)
Adds idx/expires_at/<padded_exp>-<id> hardlinks for events with an
expiration tag, an injectable clock so tests can drive time
deterministically, and the deleteExpiredEvents() sweep.

- FsLayout: idxExpiresAt + expirationEntry path helper.
- FsIndexer.pathsFor: emits the expiration entry whenever
  event.expiration() > 0, so insert / delete maintain it alongside
  the kind / author / owner / tag indexes.
- FsEventStore: pre-insert guard rejects events with exp <= now
  (SQLite parity: trigger uses inclusive <=). Constructor takes a
  clock function defaulting to TimeUtils.now(). deleteExpiredEvents
  walks idx/expires_at, parses filenames, and deletes anything with
  exp < now (strict <, matching SQLite's sweep query).

Tests: 8 new in FsExpirationTest — future expiration accepted +
indexed, already-expired-on-insert rejected, exp==now rejected on
insert but kept by sweep, non-positive exp ignored, sweep removes
canonical + index entries, plain events untouched. 59 fs tests green.
2026-04-24 22:26:17 +00:00
Claude 721546b140 feat(quartz): FsEventStore NIP-09 deletion + tombstones (step 4)
Tombstone files under tombstones/id/<id>.json and tombstones/addr/
<kind>/<pubkey>/<sha256(d)>.json, each a hardlink to the kind-5
event that authored the deletion. One source of truth: the tombstone
IS the deletion event, just indexed by target.

- FsTombstones — installs id tombstones unconditionally, installs
  addr tombstones with strongest-cutoff-wins semantics (later kind-5
  replaces earlier via atomic rename), and exposes hasIdTombstone /
  addrTombstoneCutoff for pre-insert checks.
- FsEventStore.insert — pre-insert guard: id tombstone always blocks;
  addr tombstone blocks when event.createdAt <= tomb.createdAt, matching
  SQLite's reject_deleted_events trigger. Kind-5 inserts trigger a
  cascade: for each e-tag target owned by the deletion author, unlink
  indexes + slot + canonical; for each a-tag (same pubkey), evict the
  slot winner if its createdAt <= deletion.createdAt. Tombstones are
  installed for every target regardless, so future re-inserts are
  blocked.

Tests: 11 new in FsDeletionTest — delete-by-id, block-reinsert-by-id,
non-author-deletion still installs tombstone but no cascade, cascade
addressable slot, newer-at-deleted-address passes, older blocked,
equal-timestamp blocked, later kind-5 raises cutoff, earlier kind-5
does not lower, deletion event stays queryable, tombstone shares
inode with kind-5 canonical. 51 fs tests green.
2026-04-24 21:38:37 +00:00
Claude 096aa88096 feat(quartz): FsEventStore replaceable + addressable slots (step 3)
Brings the file-backed store up to parity with SQLite's ReplaceableModule
and AddressableModule. A slot is a single hardlink that encodes the
UNIQUE(kind, pubkey[, d-tag]) constraint directly in the directory
layout:

  replaceable/<kind>/<pubkey>.json                      (kinds 0, 3, 10000-19999)
  addressable/<kind>/<pubkey>/<sha256(dTag)>.json       (kinds 30000-39999)

FsSlots handles the full lifecycle: pre-insert guard (reject if newer or
equal exists), atomic rename-with-REPLACE_EXISTING install, and eviction
of the old winner's canonical + index hardlinks. Because the slot is a
hardlink the event data survives external canonical deletion, matching
the "files come and go" contract in the design plan.

delete(id) also clears the slot when the deleted event is the current
winner, so no orphan slot files linger.

Tests: 14 new in FsSlotsTest — newer wins / older rejected / equal
rejected / eviction unlinks old indexes / empty d-tag / canonical-
deletion survives via hardlink / delete-clears-slot / non-replaceable
events never touch the slot dirs. 40 fs tests pass.
2026-04-24 21:29:15 +00:00
Claude e07090d4fa feat(quartz): FsEventStore indexes + query planner (step 2)
Hardlink indexes under idx/ and a minimal query planner bring the
file-backed store up to parity with SQLite on filtered lookups.

- FsLayout — path helpers, .seed file (8 random bytes, salts all hashes),
  entry filename format <zero-padded ts>-<id>.
- FsIndexer — on insert creates hardlinks at idx/kind/<k>/, idx/author/
  <pk>/, idx/owner/<owner_hex>/, idx/tag/<name>/<hash_hex>/. Owner hash
  matches SQLite's pubkey_owner_hash (recipient for GiftWrap). Honours
  DefaultIndexingStrategy: single-letter tag names only. On delete
  unlinks every known path so the inode can be reclaimed.
- FsQueryPlanner — picks a driver (ids / first tag / kinds / authors /
  all kinds) and yields candidates sorted by createdAt DESC. Final
  predicate check runs through Filter.match so any driver is
  correctness-safe.
- FsEventStore — sets mtime to event.createdAt on write, runs queries
  through the planner with post-filter, applies limit, dedupes by id
  across multi-filter unions, and unlinks indexes on delete.

Tests: 16 new in FsQueryTest covering order, limit, author / kind /
tag drivers, tag OR within a key, tagsAll AND across keys, non-
single-letter-tag behaviour, since/until, count, index hardlink
maintenance, and reopen persistence. All 26 fs tests green.
2026-04-24 21:22:55 +00:00
Claude 3550044ae0 feat(quartz): FsEventStore skeleton — insert, query by id, delete (step 1)
First slice of the file-backed IEventStore planned in
cli/plans/2026-04-24-file-event-store-*. Each event is stored as
events/<aa>/<bb>/<id>.json with atomic tmp+rename writes. Ephemeral
kinds are dropped. Duplicate inserts are no-ops (id-level uniqueness).
Staging leftovers are swept on open.

Only id-based query and delete are wired; the query planner, indexes,
replaceable/addressable slots, tombstones, vanish, expiration sweep,
FTS, and transactions land in later steps.

Tests: 10 new tests under quartz jvmTest, all passing.
2026-04-24 21:10:50 +00:00
Claude d230c5e86b docs(cli): plan a file-backed event store for amy
Three-part design doc for an IEventStore backed by a directory tree
instead of SQLite, targeted at the cli/ module.

- overview: goals, directory layout, feature-parity matrix, public API
- pipelines: insert (T1-T8 with crash-safety), query planner, delete,
  transactions, concurrency
- nips: replaceable/addressable slot enforcement via hardlinks +
  atomic rename, NIP-09 tombstones (hardlink to the kind-5), NIP-40
  expirations, NIP-50 FTS via inverted-index hardlinks, NIP-62 vanish
  cascade, tag indexing, seed file, scrub/compact, test strategy,
  10-step rollout
2026-04-24 20:58:18 +00:00
Vitor Pamplona f341bc776e Merge pull request #2552 from vitorpamplona/claude/cli-profile-feed-commands-03lOu
Add profile and notes commands to CLI
2026-04-24 15:16:52 -04:00
Claude 8cbe8c67fe refactor(cli): rename default data-dir to ./amy and nest MLS state under marmot/
Two on-disk layout changes that only affect freshly-created data-dirs (no
migration shim since `amethyst-cli-data` was never tagged or released):

- Default `--data-dir` is now `./amy` instead of `./amethyst-cli-data` —
  shorter, matches the binary name, and still overridable via the flag or
  `$AMETHYST_CLI_DATA`.
- MLS-only files (`groups/` and `keypackages.bundle`) move under a new
  `marmot/` subdirectory so the top-level root stays tidy as more
  non-Marmot state lands (notes, profile caches, etc). Existing
  top-level files (`identity.json`, `relays.json`, `state.json`) are
  unchanged.

README tree diagram and DEVELOPMENT.md test table updated to match.
2026-04-24 19:15:58 +00:00
Vitor Pamplona 3423891506 Merge pull request #2551 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 14:52:20 -04:00
Crowdin Bot c24fba81d1 New Crowdin translations by GitHub Action 2026-04-24 18:41:23 +00:00
Vitor Pamplona 7448641445 Merge pull request #2503 from greenart7c3/main
feat(media): add playback controls and autoplay support for GIFs
2026-04-24 14:39:41 -04:00
Claude 825e2fd911 refactor(cli): group post + feed under notes subcommand
Moves the top-level `post` and `feed` verbs under a single `notes` group
(`amy notes post`, `amy notes feed`). Keeps the per-event-family grouping
consistent with `profile` (kind:0), `dm` (NIP-17), and `marmot` (MLS).

PostCommand and FeedCommand are unchanged — the new NotesCommands object
is a thin dispatcher that routes `notes post` / `notes feed` into them.
2026-04-24 18:28:52 +00:00
Vitor Pamplona 857caa97c9 Merge pull request #2550 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 14:27:34 -04:00
Crowdin Bot a9081923f8 New Crowdin translations by GitHub Action 2026-04-24 18:15:56 +00:00
Vitor Pamplona 0e8767b9cd No need for String.format 2026-04-24 14:13:39 -04:00
Vitor Pamplona e1a133ac9f removes unneeded annotation 2026-04-24 14:11:07 -04:00
Claude 525c4bce0f feat(cli): add profile, post, and feed verbs
Adds three new top-level verbs to amy that mirror the core social-feed
surface of the Android client:

- `amy profile show [USER]` / `amy profile edit ...` reads and patches the
  user's NIP-01 kind:0 metadata. `edit` reuses MetadataEvent.updateFromPast
  semantics so unset flags keep prior values and blank values delete the
  field, falling back to MetadataEvent.createNew when no kind:0 exists yet.
- `amy post TEXT` publishes a NIP-10 kind:1 short text note to the user's
  outbox relays via TextNoteEvent.build.
- `amy feed [--author USER] [--following] [--limit N] [--since|--until TS]`
  reads kind:1 notes — own / single-author / contact-list — using
  Context.drain with author-scoped filters, dedups by id, and returns the
  newest-first window as JSON.

All three keep the thin-assembly-layer rule: no Nostr or business logic in
cli/, every event is built/signed via quartz/ and published through the
existing Context.publish + Context.drain helpers.
2026-04-24 18:01:12 +00:00
Vitor Pamplona d8a8082887 removes the account retainer logic 2026-04-24 13:39:00 -04:00
Vitor Pamplona 711a70cdb4 Merge pull request #2539 from vitorpamplona/claude/fix-giftwrap-unwrap-2659O
Preload all writable accounts for always-on notifications
2026-04-24 13:28:29 -04:00
Vitor Pamplona 141cbf93c3 Merge pull request #2538 from vitorpamplona/claude/add-reply-notifications-GvMOn
Add reply and mention notifications for public notes
2026-04-24 13:27:14 -04:00
Vitor Pamplona 13fc014e66 Merge pull request #2548 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 13:14:55 -04:00
Claude 82e4448b58 refactor(quartz): move lowercase-p notification check into PTag
The default Event.notifies body was hand-rolling a tag scan for lowercase
`p`, duplicating the tag-shape knowledge that already lives in PTag.
Give PTag ownership of the check:

    PTag.isNotifying(tags, userHex)  // iterates tags and uses PTag.isTagged

Event.notifies now delegates to it. Kinds that override (CommentEvent,
WakeUpEvent) still work — they compose or replace the default as before.

https://claude.ai/code/session_01GQDJxiHPogdzCNhUBN7Pjc
2026-04-24 16:36:21 +00:00
Crowdin Bot b5671b7449 New Crowdin translations by GitHub Action 2026-04-24 16:19:07 +00:00
davotoula 999184cced optimise imports 2026-04-24 18:15:58 +02:00
Claude 10bbd0ddb2 refactor(notifications): per-kind Event.notifies(HexKey) routing
The notification pipeline previously hard-coded a lowercase-`p`-tag match
in two places (the observer predicate and consumeFromCache via
taggedUserIds). That's correct for most kinds but wrong for two:

- NIP-22 CommentEvent: a comment several levels deep only tags the root
  author via uppercase `P` (RootAuthorTag). Pure lowercase-p routing
  missed "someone replied deep in your thread" notifications.

- Experimental WakeUpEvent (kind 23903): its `p` tags are the authors of
  the subject events it references — Bob reacting to Alice's post yields
  a WakeUpEvent with p=Bob, even though Alice's device is the one that
  needs to wake up. Transport-layer routing (push/relay subscription)
  already delivered the event to the right device, so the in-event
  routing has to be permissive.

Introduce `open fun Event.notifies(userHex: HexKey): Boolean` with a
lowercase-`p` default that covers NIP-01/04/17/25/28/34/57/68/71/84/AC/
chess/wiki/long-form/poll mentions. Each kind with distinct semantics
overrides:

- CommentEvent.notifies: super.notifies(u) || rootAuthorKeys().contains(u)
  — picks up uppercase P root-author routing on top of lowercase p.
- WakeUpEvent.notifies: true — every logged-in account is a valid wake
  target once the event has reached LocalCache on this device.

NotificationDispatcher's observer predicate and EventNotificationConsumer.
consumeFromCache both now ask `event.notifies(accountHex)` instead of
extracting taggedUserIds themselves. The zap path's redundant
isTaggedUser re-check is gone too (it was duplicating what the outer
routing already enforced).

https://claude.ai/code/session_01GQDJxiHPogdzCNhUBN7Pjc
2026-04-24 16:06:55 +00:00
Vitor Pamplona 6a55c752e8 Merge pull request #2547 from greenart7c3/feat/reorder-payment-targets-icon
feat(reactions): place payment targets icon after zap by default
2026-04-24 11:47:21 -04:00
Claude 86a86e1426 refactor(notifications): observer takes a single predicate
Filter.match is itself just a boolean predicate, so carrying a Filter and
a separate composition predicate on NewEventMatchingFilter duplicated the
abstraction. Collapse to one predicate:

- NewEventMatchingFilter(predicate, onNew): drops the Filter parameter.
- LocalCache.observeNewEvents(predicate): primary API.
- LocalCache.observeNewEvents(filter): kept as a one-liner that forwards
  filter::match, so existing feed/DAL callers are unchanged.

In the dispatcher the freshness check, kind check, p-tag match, and
since cutoff now live in a single lambda, short-circuiting on cheap
kind comparison first. NOTIFICATION_KINDS is now a Set<Int> for O(1)
membership instead of O(n) List.contains — cheap, but on a hot path
that runs for every new cache insertion it pays for itself.

https://claude.ai/code/session_01GQDJxiHPogdzCNhUBN7Pjc
2026-04-24 14:39:42 +00:00
greenart7c3 e30413a081 feat(reactions): hide payment targets icon by default
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:15:00 -03:00
greenart7c3 4c8959dbd4 feat(reactions): place payment targets icon after zap by default
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:34:06 -03:00
Vitor Pamplona f2e7a622f1 Merge pull request #2546 from vitorpamplona/claude/increase-icon-size-x4vc7
Fix Material Symbol icon rendering and adjust bottom bar icon sizes
2026-04-24 09:16:48 -04:00
Claude 15e63c48a2 fix(icons): render Material Symbols with the correct tint
The painter measured the glyph with TextStyle(color = Unspecified) and
tried to override the color at draw time via drawText(layout, color = tint).
That override is unreliable across Compose versions: when the measured
style carries Color.Unspecified, drawText can fall through to Color.Black,
producing black-on-black icons (visible on the back arrow in dark mode).

Include the tint in the measured TextStyle directly. The (symbol, fontFamily,
density, tint, rtl) remember key on rememberMaterialSymbolPainter was already
keyed on tint, so the TextMeasurer cache still hits for every (symbol, color)
pair used by the app.
2026-04-24 13:04:30 +00:00
Claude 7ab23ce30e fix: increase bottom bar icon size to compensate for MaterialSymbols padding
MaterialSymbols glyphs have more internal padding than the previous
MaterialIcons, so at 20dp they looked smaller than before. Bump the
NavigationBar icons to 24dp (Material's default NavigationBar icon size)
and widen the notification-dot box accordingly to preserve its offset.
2026-04-24 12:55:28 +00:00
Vitor Pamplona 9874a44956 Merge pull request #2545 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 08:14:41 -04:00
Crowdin Bot 222a3255d1 New Crowdin translations by GitHub Action 2026-04-24 12:13:39 +00:00
Vitor Pamplona 6a001cd807 Merge pull request #2544 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 08:12:07 -04:00
Vitor Pamplona e0605058da Merge pull request #2543 from davotoula/fix/hand-raised-bug-and-sonar-fixes
Fix: hand raised toggle bug and sonar fixes
2026-04-24 08:11:57 -04:00
Crowdin Bot 5298684892 New Crowdin translations by GitHub Action 2026-04-24 12:08:04 +00:00
Vitor Pamplona 5c2d5dafb7 Merge pull request #2542 from davotoula/docs/security-policy
add SECURITY.md with private vulnerability reporting policy
2026-04-24 08:06:08 -04:00
greenart7c3 bd222e3e9b refactor(media): simplify GIF rendering and wire autoplay setting
Replaces the video-style play/pause controls on GIFs with normal image
rendering. When Auto-play Videos is off, GIFs pause on the first frame
(feed GIFs additionally show a small "gif" label in the bottom-left);
tapping still opens the fullscreen dialog.

Profile-picture GIFs now respect the same setting. The key fix is
bypassing ProfilePictureFetcher (which serves a static JPEG thumbnail
and prevents animation); GIF avatars load via the raw URL so Coil's
animated decoder runs. Autoplay is read reactively from a new
autoPlayVideosFlow StateFlow so toggling the setting immediately
starts/stops animation in the drawer, top bar, and every other avatar.
2026-04-24 08:43:43 -03:00
greenart7c3 e170b65753 feat(media): add playback controls and autoplay support for GIFs
Introduces `GifVideoView` to manage GIF playback with manual play/pause
controls and support for the "Auto-play Videos" setting. Updates
`MyAsyncImage` and `ZoomableContentView` to utilize this specialized
view when GIF content is detected via file extension or MIME type.

Key changes include:
*   **GifVideoView**: A new component that uses Coil's `Animatable` to start and stop GIF animations, featuring a video-like UI with play/pause overlays and a zoom button.
*   **GIF Detection**: Added `isGif()` and `isGifUrl()` helpers to identify GIF content by MIME type or URL patterns.
*   **Integration**: Redirects GIF rendering in `MyAsyncImage` and `ZoomableContentView` from static image views to the new interactive `GifVideoView`.
2026-04-24 08:31:22 -03:00
davotoula 20367af3db fix(cli): warn when FileStores delete() fails
Delete() returning false on a still-existing file meant we silently lost
scratch state for MLS group / key-package / message stores. Route the
four call sites through a `deleteOrWarn` helper that logs via the
KMP-safe quartz Log when the file refuses to disappear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:10:39 +02:00
davotoula 33dacaa260 fix(audio-room): give hand-raise button a visible toggled state
Replace FilledTonalIconButton with FilledTonalIconToggleButton so the
container color reflects raised/lowered state, and drop the tautological
`if (handRaised) PanTool else PanTool` conditional flagged by Sonar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:09:14 +02:00
davotoula 59b36080ea docs: add SECURITY.md with private vulnerability reporting policy
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:54:12 +02:00
Vitor Pamplona 82bbfa3d94 Merge pull request #2541 from vitorpamplona/claude/fix-arrowback-icon-0v6aT
Optimize Material Symbols rendering with shared font and text measurer
2026-04-23 23:11:10 -04:00