Commit Graph

11989 Commits

Author SHA1 Message Date
Claude c74bbb8510 test(marmot-interop): unwrap newer whitenoise-rs groups show shape
Whitenoise-rs ≥ v0.2.x nests the group object one level deeper —
`{"result": {"group": {...}}}` instead of `{"result": {...}}`. The two
metadata-name pollers in tests 07 and 10 still asked for `.result.name`
and silently saw the empty string, so even when wn-side `groups show`
returned the correct new name the polling loops timed out:

    07 metadata rename       fail   B saw name="" not "Interop-02-renamed"
    10 concurrent commits    fail   diverged: A sees "race-from-amethyst", B sees ""

Both queries now also peel a `.group` wrapper when present, leaving the
older bare-`result` shape working too. Re-running the headless harness
flips 07 + 10 from fail → pass; the remaining failures (09, 14, 15) are
unrelated MLS-encryption / Remove-commit issues that need their own fix.

https://claude.ai/code/session_013VYkpz8P1mPh9Ejxy9anhJ
2026-04-24 23:56:47 +00:00
Vitor Pamplona f41e5d278a Merge pull request #2559 from vitorpamplona/claude/secure-local-files-1mC5q
Add secure private-key storage with OS keychain and NIP-49 support
2026-04-24 19:18:06 -04:00
Vitor Pamplona f2ba799a85 Merge pull request #2557 from vitorpamplona/claude/update-swipe-dismiss-state-zJAnD
Simplify SwipeToDelete by using onDismiss callback
2026-04-24 17:29:40 -04:00
Vitor Pamplona 80e36e004a Merge pull request #2556 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 17:28:46 -04:00
Vitor Pamplona 3f57060cde Merge pull request #2554 from vitorpamplona/claude/optimize-ci-workflows-avIOq
Consolidate CI/CD workflow: merge test and build jobs
2026-04-24 17:28:39 -04:00
Claude 04f543b7a5 fix: migrate SwipeToDeleteContainer off deprecated confirmValueChange
The confirmValueChange parameter on rememberSwipeToDismissBoxState is
deprecated — state changes should not be vetoed via callback. Move the
onStartToEnd action to SwipeToDismissBox.onDismiss and rely on
enableDismissFromEndToStart = false to restrict the anchor set.

https://claude.ai/code/session_01CfYsUSGeuBnYsCqa6FDSPk
2026-04-24 21:22:55 +00:00
Claude 158f387bc0 test(cli): wire --secret-backend=plaintext into dm ghost identity init
dm-03 and dm-04 spawn a throwaway "ghost" identity by calling $AMY_BIN
directly (not through amy_a / amy_d), so they missed the plaintext
backend flag added in the previous commit and were failing with
"No TTY and no passphrase source" on headless CI.

https://claude.ai/code/session_01SqdMfLdXvb3GskFLcEj739
2026-04-24 21:19:01 +00:00
Crowdin Bot b3e7808fc1 New Crowdin translations by GitHub Action 2026-04-24 21:11:04 +00:00
Vitor Pamplona 5f5aaf2941 Merge pull request #2553 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-24 17:09:29 -04:00
Vitor Pamplona ad84f2c6cc Merge pull request #2555 from vitorpamplona/claude/cli-github-actions-deploy-HVNK4
Add native distribution packaging for Amy CLI (jlink + jpackage)
2026-04-24 17:09:18 -04:00
Claude b507cb5986 feat(cli): move private key out of identity.json into keychain / NIP-49
identity.json previously stored `privKeyHex` and `nsec` as plaintext fields.
0600 file perms keep other OS users out, but not another app running as the
same user — and that is the threat model the CLI actually cares about.

Introduce a SecretStore indirection:

 * identity.json now persists only the public parts plus a typed
   `secret: IdentitySecret` envelope (keychain | ncryptsec | plaintext).
 * macOS uses `/usr/bin/security` (Keychain ACLs bind the item to the binary
   that stored it, so other same-user apps need user consent).
 * Linux uses `secret-tool` if a Secret Service is running on the session
   D-Bus; the gain there is at-rest encryption while the keyring is locked.
 * On any platform without a keychain, auto falls back to NIP-49
   (scrypt + XChaCha20) with the passphrase read from --passphrase-file,
   then $AMY_PASSPHRASE, then a TTY prompt. Another same-user app can read
   the blob but cannot decrypt it without the passphrase.
 * `--secret-backend=plaintext` is an explicit opt-in for dev scripts and
   the interop test harness.

Legacy identity.json files that still carry top-level privKeyHex/nsec are
read transparently and auto-migrate on the next save.

whoami, `create` / `login` existence checks, and init-re-run now use a
metadata-only load path so they do not trigger a keychain prompt or ask
for a passphrase just to echo the npub.

Tests: cli/tests/ setups wire --secret-backend=plaintext through the amy_a
/ amy_d wrappers so headless CI runs do not stall on a TTY passphrase
prompt.

https://claude.ai/code/session_01SqdMfLdXvb3GskFLcEj739
2026-04-24 21:06:18 +00:00
Claude e68f77b2f1 feat(cli): ship amy on macOS + Linux via GitHub Release
Adds a new build-cli matrix to create-release.yml so every tag push also
produces self-contained amy binaries:

  - amy-<ver>-macos-x64.tar.gz     (macos-13 runner)
  - amy-<ver>-macos-arm64.tar.gz   (macos-14 runner)
  - amy-<ver>-linux-x64.tar.gz     (ubuntu-latest)
  - amy-<ver>-linux-x64.deb        (ubuntu-latest)
  - amy-<ver>-linux-x64.rpm        (ubuntu-latest)

Each tarball is a flat tree (bin/amy + lib/*.jar + runtime/) with a
jlink'd JDK 21 embedded — no system Java required on the user machine.
The .deb / .rpm install the same tree under /opt/amy/.

Windows is intentionally deferred until cli/ is validated on Windows
(data-dir path handling, file locking on groups/<gid>.mls, identity.json
line endings).

Two follow-ups flagged in comments:

  - :commons leaks Compose + Skiko (~40 MB) as transitive deps to :cli.
    Tarball lands at ~98 MB instead of the plan's <80 MB target. Size
    budget in the workflow is set to 200 MB until :commons is split into
    core + ui modules.
  - .deb/.rpm install to /opt/amy/bin/amy with no /usr/local/bin symlink.
    Users must add to PATH or symlink manually after install.

Wire-up:
  - cli/build.gradle.kts gains jlinkRuntime + amyImage (Sync task that
    combines installDist output with the jlink runtime + a shell
    launcher) + jpackageDeb + jpackageRpm.
  - scripts/asset-name.sh gains cli_asset_name() + collect_cli_assets()
    alongside the existing desktop collector, preserving the
    <product>-<version>-<family>-<arch>.<ext> naming contract.

See cli/plans/2026-04-21-cli-distribution.md for the overall plan.

https://claude.ai/code/session_01Tbh6F7TtEeceb4K3stcUWp
2026-04-24 20:57:04 +00:00
Claude ca92c5a87a ci: drop assembleDebug and its APK uploads
The release workflow builds its own signed release APKs/AABs and does
not consume the debug outputs. Testers use the Benchmark APK, which is
still built and uploaded. Dropping assembleDebug removes a redundant
APK packaging pass from every PR/main run.
2026-04-24 20:56:34 +00:00
Claude 6d930ef3cc ci: split assembleDebug and assembleBenchmark into separate gradle calls
Combining them into a single Gradle invocation caused
:amethyst:packagePlayBenchmark to fail inside AGP's
IncrementalSplitterRunnable worker — both variants share packaging
state in one task graph and the benchmark leg trips on it.

The old workflow always ran these as two separate `./gradlew` calls.
Match that pattern. They still run on the same runner with a warm
Gradle cache, so the compilation savings from sharing quartz/commons
with the test+packageDeb step are preserved; we only pay two Gradle
startups, which is trivial.
2026-04-24 20:52:19 +00:00
Claude b77e6aa54f feat(cli): restrict on-disk data to owner-only permissions
identity.json, relays.json, state.json, MLS group state/retained epochs,
keypackage bundles, and the Marmot message archive were written with
default umask — typically world-readable on Unix. On a shared machine
another OS user could read the stored private key or decrypted chat
history.

Introduce SecureFileIO: overwrites go via a sibling tempfile with POSIX
0600 attrs set at creation time and ATOMIC_MOVE into place, directories
are created 0700, and on upgrade DataDir.init tightens any pre-existing
loose files. Non-POSIX filesystems (Windows) fall back to setReadable/
Writable/Executable to strip other-user access.

This is defense against other OS users. Another app running as the same
user is still in scope — the FileStores doc continues to note that real
deployments MUST encrypt at rest.

https://claude.ai/code/session_01SqdMfLdXvb3GskFLcEj739
2026-04-24 20:30:55 +00:00
Claude 2c5f08a16a ci: merge test and build jobs to eliminate duplicate compilation
Previously each OS ran Gradle twice (or three times on Ubuntu): once to
compile+test, then again to build desktop packages, plus a third run on
Ubuntu for Android APKs. Merging these into a single matrix job per OS
lets Gradle build the task graph once and reuse compiled classes across
test, desktop packaging, and Android assembly.

- Combine test + build-desktop into one matrix job (test-and-build)
- Fold build-android into the same Ubuntu matrix leg
- Use --no-daemon consistently; drop the redundant --stop steps
- Raise timeout to 60 min to cover the combined work
2026-04-24 20:10:03 +00:00
Crowdin Bot cef4036cab New Crowdin translations by GitHub Action 2026-04-24 19:18:22 +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