Compare commits

...

1868 Commits

Author SHA1 Message Date
Vitor Pamplona
9587026abc v1.08.0 2026-04-01 15:45:46 -04:00
Vitor Pamplona
6ee19506ad Merge pull request #2065 from vitorpamplona/claude/fix-addstyle-indexing-9t4Ew
Fix mention styling by deferring style application until after text mutations
2026-04-01 15:32:27 -04:00
Claude
09fb10220b fix: apply addStyle after all text mutations in OutputTransformation
TextFieldBuffer.addStyle() positions are not adjusted by subsequent
replace() calls. When multiple mentions had different-length display
names, styles for later mentions became misaligned. Split into two
phases: all replace() calls first, then all addStyle() calls with
cumulative-shift-corrected positions.

https://claude.ai/code/session_01SSsxsfJLbRiesBBhQFEVUd
2026-04-01 19:15:47 +00:00
Vitor Pamplona
b2be345979 Merge pull request #2064 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-01 14:05:21 -04:00
Crowdin Bot
6ed251f7b9 New Crowdin translations by GitHub Action 2026-04-01 18:03:07 +00:00
Vitor Pamplona
391475e75e Merge pull request #2063 from vitorpamplona/claude/migrate-to-arti-tor-voQeh
Replace Android TorService with ArtiProxy for Tor connectivity
2026-04-01 14:01:05 -04:00
Claude
8e6d2164d7 fix: move ArtiNative initialization off main thread
System.loadLibrary("arti_android") runs when ArtiNative is first
accessed. Previously this happened in TorService.init (via
setLogCallback), which ran on main thread during AppModules creation.

Moved setLogCallback into start(), which runs on Dispatchers.IO.
Now all JNI calls — including the native library load — happen off
main thread.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:56:22 +00:00
Vitor Pamplona
45f1d79410 Adding compiled libs and customizing the log message. 2026-04-01 13:50:36 -04:00
Claude
bc24434f1c refactor: remove duplicate android_logger from native Arti wrapper
All log messages go through send_log_to_java() → Kotlin ArtiLogCallback
→ Log.d("TorService"), which already writes to logcat. The android_logger
module was a second FFI call to __android_log_write that duplicated
every line.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:32:38 +00:00
Claude
2429e695ce chore: bump Arti to 2.2.0 (arti-client/tor-rtcompat 0.41)
All features and APIs verified present in 0.41:
- tokio, rustls, compression, onion-service-client, static-sqlite
- TorClient::create_bootstrapped, from_directories, connect()

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:18:25 +00:00
Claude
53ae87aa9d chore: trim unnecessary Arti features to reduce binary size
- Set default-features = false on arti-client and tor-rtcompat
- Removed bridge-client (UI doesn't expose bridge config yet)
- Narrowed tokio features from "full" to only what the SOCKS proxy
  needs: rt-multi-thread, net, io-util, time, macros

Kept: tokio, rustls, compression, onion-service-client, static-sqlite
(all required for Amethyst's .onion relay support)

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:12:35 +00:00
Claude
35bf16b63e docs: add README for Arti Android build tools
Covers prerequisites, build commands, output verification, version
updates, architecture decisions, Cargo features, and troubleshooting.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:03:47 +00:00
Claude
035c570902 chore: bump Arti version to 1.9.0 (arti-client/tor-rtcompat 0.38)
https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 16:55:03 +00:00
Claude
e50ae0fb1e feat: replace arti-mobile-ex with custom-built Arti native library
The Guardian Project's arti-mobile-ex AAR has three problems:
1. No 16KB page-aligned binaries (required for Google Play)
2. ArtiProxy's stop()+start() causes state file lock conflicts
   (lock is tied to TorClient object lifetime, released only on GC)
3. ~140MB AAR size

Replace with a custom JNI bridge built from Arti source, following
BitChat's proven approach:

Build tooling (tools/arti-build/):
- build-arti.sh: Clones official Arti, compiles with cargo-ndk
  for ARM64 + x86_64, NDK 25+ for 16KB page alignment
- Cargo.toml: Minimal deps with size-optimized release profile
- src/lib.rs: Custom SOCKS5 proxy with proper lifecycle:
  - initialize() creates TorClient once (holds state lock forever)
  - startSocksProxy() binds port and accepts connections
  - stopSocksProxy() aborts listener only (TorClient stays alive)
  This cleanly separates "stop routing traffic" from "destroy client"

Kotlin side:
- ArtiNative.kt: JNI declarations + ArtiLogCallback interface
- TorService.kt: Uses ArtiNative directly, start() initializes +
  starts proxy, stop() only stops proxy (no lock issues)
- TorManager.kt: Restored stop() calls for OFF/EXTERNAL modes
  since our native stop is now safe

Removed: arti-mobile-ex dependency from build.gradle and version catalog

Native libraries must be built separately:
  cd tools/arti-build && ./build-arti.sh

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 16:37:44 +00:00
Vitor Pamplona
abe1082121 Fixes the default port. 2026-04-01 11:03:35 -04:00
Claude
4f92878146 fix: never stop ArtiProxy — Arti's lock is tied to object lifetime
Arti's state file lock is released when the TorClient object is
garbage collected, not when stop() is called. Calling stop()+start()
on the same ArtiProxy creates a new internal TorClient that conflicts
with the old lock that hasn't been GC'd yet.

Solution: start ArtiProxy once, let it run for the process lifetime.
When the user turns Tor OFF or EXTERNAL, TorManager simply stops
emitting Active status — OkHttp stops routing through the proxy.
The idle proxy uses negligible resources and drops circuits when no
SOCKS connections are active.

This removes stop(), Mutex, NonCancellable, CompletableDeferred — all
the complexity that was trying to work around a fundamental Arti
design constraint.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:29:22 +00:00
Claude
14f8bf1245 fix: create ArtiProxy once and reuse — never recreate
The root cause of file lock conflicts was creating new ArtiProxy
objects on each start. Even after stop() confirmed, build() could
race with OS-level lock release.

Now ArtiProxy is created once in the TorService constructor and
reused for the app's lifetime. start() and stop() just toggle it
on/off on the same instance. No more file lock conflicts.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:25:28 +00:00
Claude
7cac95726b fix: wait for Arti to confirm stop before allowing restart
The fixed 2s delay was insufficient — Arti's native layer releases
file locks asynchronously. Now stop() waits for the actual
"state changed to Stopped" log confirmation via CompletableDeferred,
with a 10s timeout as safety net. This ensures the file lock is
truly released before start() creates a new ArtiProxy instance.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:21:33 +00:00
Claude
388a02b60c fix: stop Arti when switching to EXTERNAL Tor mode
EXTERNAL means another Tor process (e.g., Orbot) is running on the
device. No reason to keep our internal Arti alive alongside it.

Also removed the fallback that started Arti when the external port
was invalid — if the user chose EXTERNAL with a bad port, Tor should
be off, not silently falling back to internal.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:11:29 +00:00
Claude
1c33dde81a fix: restore service.stop() on explicit Tor OFF for safety
Users in restrictive countries need Tor circuits torn down when they
switch to OFF — an idle proxy still maintains detectable connections.

stop() is now called only on explicit OFF toggle (user action), not on
flow pause/resume (app lifecycle). This avoids file lock races on
resume while ensuring OFF truly disconnects from the Tor network.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:09:22 +00:00
Claude
dd3f90aee9 refactor: remove unused lastLogTime from TorService
Dead code from the earlier callbackFlow version that had a bootstrap
stall monitor. No longer needed with the MutableStateFlow design.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:04:44 +00:00
Claude
d94d8776eb fix: keep ArtiProxy alive across mode switches to avoid file lock conflicts
ArtiProxy holds an exclusive filesystem lock. Destroying it on OFF and
recreating on INTERNAL caused lock conflicts because the native layer
needs time to release the lock.

Instead, create ArtiProxy once and never destroy it. When Tor is OFF
or EXTERNAL, the proxy sits idle with no SOCKS connections — negligible
resource usage. This eliminates all file lock race conditions.

Also wrap start/stop in NonCancellable to prevent transformLatest
cancellation from leaking half-initialized proxy instances.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 13:01:31 +00:00
Claude
c83256bac2 fix: singleton ArtiProxy lifecycle to prevent state file lock conflicts
The callbackFlow-based TorService created a new ArtiProxy on every flow
collection. When the app paused and resumed, the old instance's file lock
wasn't released before the new one started, causing "Another process has
the lock on our state files" errors.

Redesigned TorService to own a single ArtiProxy with explicit start/stop:
- ArtiProxy is created once and reused across flow re-collections
- State exposed via MutableStateFlow instead of callbackFlow
- Mutex guards start/stop to prevent races
- TorManager calls service.start() and service.stop() explicitly when
  switching between INTERNAL/OFF/EXTERNAL modes
- stop() includes a 2s settle delay for native file lock release

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 12:44:24 +00:00
Claude
293fd39a68 fix: harden TorService with bootstrap timeout, thread safety, and error recovery
Address gaps found in code review:
- Use AtomicBoolean/AtomicLong for state accessed from native Arti
  log callback thread (was a data race)
- Add bootstrap timeout monitor (120s) that restarts Arti once if
  bootstrapping stalls, following BitChat's inactivity pattern
- Clean up failed ArtiProxy instances before port retry
- Detect "Another process has the lock" fatal error from Arti logs

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 00:23:52 +00:00
Vitor Pamplona
cd11503b1e v1.07.5 2026-03-31 20:18:34 -04:00
Vitor Pamplona
9b2fbcbf56 Fixes image uploading crash 2026-03-31 20:17:14 -04:00
Vitor Pamplona
5cd83bbaa4 v1.07.4 2026-03-31 19:19:24 -04:00
Vitor Pamplona
91dfda3480 Merge pull request #2061 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 19:13:59 -04:00
Crowdin Bot
b1b2825dd6 New Crowdin translations by GitHub Action 2026-03-31 23:12:49 +00:00
Vitor Pamplona
48d90a8252 Fixes Wallet import encoding bug 2026-03-31 19:10:15 -04:00
Claude
11af50e0ad feat: migrate Tor implementation from tor-android to Arti (Rust)
Replace Guardian Project's tor-android (C Tor) and jtorctl with
arti-mobile-ex, a Rust-based Tor implementation. This eliminates
the Android Service binding complexity in favor of an in-process
ArtiProxy object.

Key changes:
- TorService: Replace ServiceConnection to org.torproject.jni.TorService
  with direct ArtiProxy.Builder/start/stop API. Bootstrap state detected
  via log parsing (following BitChat's pattern).
- TorServiceStatus: Remove TorControlConnection field (Arti doesn't
  support jtorctl control protocol).
- RelayProxyClientConnector: Remove DORMANT/ACTIVE/NEWNYM control
  signals. Arti manages its own circuit lifecycle internally.
- Dependencies: Replace tor-android + jtorctl with arti-mobile-ex 1.2.3.

TorManager's external API (status/activePortOrNull StateFlows) and all
downstream consumers (DualHttpClientManager, TorSettings, UI) are
unchanged.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-03-31 22:00:48 +00:00
Vitor Pamplona
69099728b2 v1.07.3 2026-03-31 17:32:23 -04:00
Vitor Pamplona
190b99d55a Merge pull request #2060 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 17:31:30 -04:00
Crowdin Bot
95adb53640 New Crowdin translations by GitHub Action 2026-03-31 21:29:37 +00:00
davotoula
25a4b68958 update translations: CZ, DE, PT, SE 2026-03-31 23:26:48 +02:00
David Kaspar
e91c793f73 Merge pull request #2059 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 23:21:41 +02:00
David Kaspar
5b477c64cf Merge branch 'main' into l10n_crowdin_translations 2026-03-31 23:21:32 +02:00
Crowdin Bot
11ca2c14be New Crowdin translations by GitHub Action 2026-03-31 21:07:39 +00:00
Vitor Pamplona
1b39bf1d52 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-31 17:05:26 -04:00
Vitor Pamplona
a4e3625f26 Migrates to the new 10008 profile badges kind 2026-03-31 16:52:24 -04:00
Crowdin Bot
e75b5d173b New Crowdin translations by GitHub Action 2026-03-31 20:25:02 +00:00
Vitor Pamplona
027cc660e2 Merge pull request #2058 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 16:24:28 -04:00
Vitor Pamplona
49850e773f Adds a space after inserting the username to avoid issues in some keyboards. 2026-03-31 16:22:57 -04:00
Vitor Pamplona
44d100f60a Fixes file name 2026-03-31 16:11:45 -04:00
Crowdin Bot
f110436853 New Crowdin translations by GitHub Action 2026-03-31 19:47:41 +00:00
Vitor Pamplona
aebbe0974a Fixes wrong filter 2026-03-31 15:45:34 -04:00
Vitor Pamplona
a79ceb527f Adding filters by community and location to the new side feeds 2026-03-31 15:23:50 -04:00
Vitor Pamplona
29bb0d2682 Dynamic filters for Longs, Shorts, Polls and Pictures 2026-03-31 14:44:56 -04:00
Vitor Pamplona
794af6eb2b Adds Long Video Menu item 2026-03-31 13:24:55 -04:00
Vitor Pamplona
3e83762761 Adds new TopNav Filter settings 2026-03-31 13:24:37 -04:00
Vitor Pamplona
c6e1dc9d79 Fixes dal filters for Shorts 2026-03-31 12:58:53 -04:00
Vitor Pamplona
6354b32205 Adds a Shorts Feed 2026-03-31 12:22:33 -04:00
Vitor Pamplona
6618644ba3 No more pages in the video tab 2026-03-31 11:45:08 -04:00
Vitor Pamplona
329d133291 Merge pull request #2057 from vitorpamplona/claude/instagram-picture-feed-9sBzo
Add Pictures feed screen with NIP-68 picture event support
2026-03-31 10:45:00 -04:00
Vitor Pamplona
3fec18b9a8 Improvements to the old bookmarks screen 2026-03-31 10:40:24 -04:00
Vitor Pamplona
de395f5629 Merge pull request #2056 from vitorpamplona/claude/migrate-bookmark-event-86Biv
Add support for legacy NIP-51 bookmark list (kind 30001)
2026-03-31 10:22:24 -04:00
Claude
55a1626159 fix: merge old bookmarks into existing new bookmarks during migration
Instead of replacing the new bookmark list, the migration now checks for
an existing kind 10003 event and merges old bookmarks into it, skipping
duplicates that already exist.

https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
2026-03-31 13:31:29 +00:00
Vitor Pamplona
581f13eea8 This code is already running on IO 2026-03-31 09:25:41 -04:00
Claude
b960a4692a feat: migrate BookmarkListEvent from kind 30001 to 10003
Rename the existing BookmarkListEvent (kind 30001) to OldBookmarkListEvent
and create a new BookmarkListEvent with kind 10003 as per the updated spec.

- OldBookmarkListEvent (kind 30001): Kept as-is for backward compatibility,
  displayed as "Old Bookmarks" in the Bookmark Lists screen
- BookmarkListEvent (kind 10003): New replaceable event, all new bookmark
  operations (save/check/remove) use this kind
- Both kinds are displayed as separate items in the Bookmark Lists screen
- Old Bookmarks screen includes a "Move All to New Bookmarks" button that
  migrates public bookmarks to public and private to private
- Created PrivateReplaceableTagArrayEvent base class for replaceable events
  with encrypted private tags (kind 10003 is in the 10000-19999 range)
- Updated all relay filters, search queries, and profile views to fetch
  both kinds
- Updated Android and Desktop modules

https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
2026-03-31 13:24:27 +00:00
Vitor Pamplona
4256e3f3cf Merge pull request #2055 from vitorpamplona/claude/migrate-quartz-nip58-XQ8bw
Refactor NIP-58 Badge events with improved tag handling
2026-03-31 09:17:19 -04:00
Vitor Pamplona
f291bb126a Merge branch 'main' into claude/migrate-quartz-nip58-XQ8bw 2026-03-31 09:17:11 -04:00
Vitor Pamplona
e5c8e6dd7f Merge pull request #2054 from vitorpamplona/claude/edit-field-visual-transform-GDY9G
Refactor user mention parsing to support nprofile and nostr: URIs
2026-03-31 09:09:59 -04:00
Vitor Pamplona
bbeb2a66ee Fixes bad merge 2026-03-31 09:06:12 -04:00
Vitor Pamplona
7ec5357653 Lint 2026-03-31 09:04:42 -04:00
Vitor Pamplona
96a2645070 Merge pull request #2053 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 09:02:17 -04:00
Crowdin Bot
bc8b73e458 New Crowdin translations by GitHub Action 2026-03-31 13:01:46 +00:00
Vitor Pamplona
da96180a10 Merge pull request #2046 from vitorpamplona/claude/add-reaction-notifications-NzyLO
Add push notifications for post reactions
2026-03-31 09:01:43 -04:00
Vitor Pamplona
4dbd833c0d Merge pull request #2048 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 09:01:32 -04:00
Crowdin Bot
d5f27c7994 New Crowdin translations by GitHub Action 2026-03-31 13:01:19 +00:00
Claude
20425a7678 feat: add @Preview composables for picture feed UI components
Adds 6 preview functions covering:
- PictureCardCompose with title, without title, and multi-image
- PictureCardCaption with title, without title, and long content
- Uses ThemeComparisonColumn for light/dark theme comparison
- Uses mock PictureEvent JSON data consumed via LocalCache

https://claude.ai/code/session_01KtuzFmDZXk67tW8QxPqmMy
2026-03-31 13:00:29 +00:00
Vitor Pamplona
26798e2b57 Fixes assertion 2026-03-31 08:59:38 -04:00
Vitor Pamplona
9cb7f9cc21 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-31 08:57:49 -04:00
Vitor Pamplona
094adcd6f6 Merge branch 'main' into claude/add-reaction-notifications-NzyLO 2026-03-31 08:47:21 -04:00
Vitor Pamplona
3dbd79d879 Merge pull request #2052 from vitorpamplona/claude/fix-sorting-order-p23ZY
Improve sorting stability by adding secondary sort keys
2026-03-31 08:42:46 -04:00
Vitor Pamplona
126d58e2d1 Merge pull request #2051 from vitorpamplona/claude/fix-quote-cursor-position-QOw9L
Fix quote placement to begin at start of message
2026-03-31 08:41:54 -04:00
Vitor Pamplona
87d0d2e768 Merge pull request #2049 from vitorpamplona/claude/tor-exception-handling-2Qy1u
Add error handling to Tor service initialization and control
2026-03-31 08:29:03 -04:00
Vitor Pamplona
50470771b0 Merge pull request #2050 from vitorpamplona/claude/simplify-localcache-redirects-9vbb7
Refactor event consumption to use base methods directly
2026-03-31 08:28:30 -04:00
Claude
acd0a13abf fix: catch exceptions from Tor library to prevent app crashes
Wrap all Tor library interactions in try-catch blocks so that failures
in the Tor service (JNI, control connection, bind/unbind) are logged
and gracefully degrade to TorServiceStatus.Off instead of crashing
the entire app.

https://claude.ai/code/session_019JDZMTvVdJyc9z2WmoAimv
2026-03-31 12:07:09 +00:00
David Kaspar
bb6bd3f0ae Merge pull request #1910 from davotoula/chess-enhancements-and-bug-fixes
Chess enhancements and bug fixes
2026-03-31 09:06:36 +02:00
davotoula
4d1e9cb8d5 Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-31 07:26:39 +02:00
Claude
abeccb23a2 feat: support npub and nprofile with nostr: prefix in edit field visual transformation
The visual transformation for message edit fields now processes nostr:npub1...,
nostr:nprofile1..., and @nprofile1... mentions in addition to the existing @npub1...
pattern. Both UrlUserTagOutputTransformation (new API) and UrlUserTagTransformation
(old API) are updated to resolve these to user display names.

https://claude.ai/code/session_012ijrLmft5PjcQ64zDwXJWj
2026-03-31 04:23:50 +00:00
Claude
d57c03a01a feat: add Instagram-style picture feed for kind 20 events
Implements a dedicated picture feed screen with a custom card layout:
- Full-width image display with user avatar and name header
- Title and 3-line content preview below the image
- Reaction row (reply, boost, like, zap, share) at the bottom
- Top bar with follow list filter (same pattern as polls feed)
- Complete relay subscription pipeline for picture events
- Accessible from the navigation drawer

https://claude.ai/code/session_01KtuzFmDZXk67tW8QxPqmMy
2026-03-31 04:10:27 +00:00
Claude
be1ad9cdb8 refactor: remove ~94 redirect consume functions from LocalCache
Replace typed consume() overloads that simply forwarded to
consumeRegularEvent() or consumeBaseReplaceable() with direct calls
in the when dispatch block. This removes ~600 lines of boilerplate
without changing behavior.

https://claude.ai/code/session_017dM6dt1on3g3yhWSi69Pwq
2026-03-31 04:06:02 +00:00
Claude
3a850ba26c fix: place cursor at beginning of text field when quoting a note
When a user hits Quote, the new post screen opens with the nostr URI
pre-filled but the cursor was at the end (after the URI). This moves
the cursor to the beginning so the user can immediately start typing
their commentary.

https://claude.ai/code/session_01RPseKC9GJ8hs3GGBR85ezw
2026-03-31 04:01:05 +00:00
Claude
96c4092138 fix: ensure secondary sort by id ascending when sorting by createdAt descending
When events share the same createdAt timestamp, the tiebreaker sort by id
must be ascending to produce a stable, deterministic order. Fixed several
locations that either had no secondary sort, used descending id sort, or
used .reversed() which incorrectly flipped both sort directions.

https://claude.ai/code/session_01RvuyPf1x9wLf2DCgsSXLGz
2026-03-31 03:45:24 +00:00
Claude
a4bf093cb3 refactor: migrate NIP-58 badges to nip88Polls-style sub-package structure
Restructures the flat nip58Badges package into sub-packages following
the established nip88Polls pattern with proper tag classes, TagArrayExt,
and TagArrayBuilderExt for each event type:

- definition/ - BadgeDefinitionEvent (kind 30009) with NameTag, ImageTag,
  DescriptionTag, ThumbTag supporting dimensions per spec
- award/ - BadgeAwardEvent (kind 8) with build() method
- profiles/ - BadgeProfilesEvent (kind 30008) with AcceptedBadge tag
  for proper paired a+e tag parsing per NIP-58 spec

Adds build() companion methods to all three event types and full
spec compliance including image/thumb dimension support.

https://claude.ai/code/session_019Uvxfa7jJbjeprLxECoJ8s
2026-03-31 03:08:25 +00:00
Vitor Pamplona
c5edcab051 Fixes the loading of old labeled bookmarks 2026-03-30 22:03:51 -04:00
Claude
a7f60082f0 feat: add push notifications for reactions
Add a new notification channel for reactions (kind 7 events) so users
get notified when someone reacts to their posts. Follows the same
pattern as zap notifications with deduplication, time-window filtering,
and grouped summaries.

https://claude.ai/code/session_01J9rAA4oeFEfNcYD6GoJqqt
2026-03-30 21:51:03 +00:00
Vitor Pamplona
91f5dffc6d Merge pull request #2045 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 16:52:24 -04:00
Vitor Pamplona
8e4ee7a618 v1.07.2 2026-03-30 16:46:01 -04:00
Crowdin Bot
332bfcd88a New Crowdin translations by GitHub Action 2026-03-30 20:39:46 +00:00
Vitor Pamplona
1872a5879e Merge pull request #2042 from greenart7c3/main
Assign `rejected` based on the presence of the "rejected" key in intent extras rather than retrieving its specific boolean value
2026-03-30 16:39:13 -04:00
Vitor Pamplona
a293976501 Merge pull request #2043 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 16:38:44 -04:00
Vitor Pamplona
a1095b4c17 Merge pull request #2044 from vitorpamplona/claude/add-gif-keyboard-support-ahCJX
Add image paste support to message input field
2026-03-30 16:38:14 -04:00
Claude
95d678f62f feat: Add GIF keyboard support to short note post screen
Enable the message field to receive images/GIFs from Android's keyboard
using Compose Foundation's contentReceiver API. When content is received,
it's routed through the existing selectImage flow for upload.

https://claude.ai/code/session_019GfY3MtCPcYn3wqTHCMSNu
2026-03-30 20:33:55 +00:00
Crowdin Bot
b625ac6d2d New Crowdin translations by GitHub Action 2026-03-30 20:26:29 +00:00
Vitor Pamplona
f848ad20d8 Merge pull request #2041 from greenart7c3/fix_external_signer
fix: support external signers wrapped in NostrSignerWithClientTag
2026-03-30 16:24:56 -04:00
greenart7c3
198e049873 Assign rejected based on the presence of the "rejected" key in intent extras rather than retrieving its specific boolean value 2026-03-30 17:18:02 -03:00
greenart7c3
f2d007f9ba fix: support external signers wrapped in NostrSignerWithClientTag
The external signer registration logic was failing when the signer was
wrapped in a `NostrSignerWithClientTag`. Fix by:

1. Updating `ListenToExternalSignerIfNeeded` to unwrap the signer if it's
   a `NostrSignerWithClientTag` containing an `IActivityLauncher`
2. Using the unwrapped launcher for intent registration and response handling
3. Renaming the intent callback to `intentLauncher` to avoid shadowing the `ActivityResultLauncher` variable
2026-03-30 17:09:59 -03:00
Vitor Pamplona
4b9c3f239c Fixes version mismatch 2026-03-30 16:02:55 -04:00
Vitor Pamplona
3ac04cdc63 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 15:59:14 -04:00
Vitor Pamplona
f9d231063d Merge pull request #2040 from vitorpamplona/claude/migrate-textfield-state-6ezEZ
Migrate text fields from TextFieldValue to TextFieldState
2026-03-30 15:58:19 -04:00
Vitor Pamplona
4bca74888d Merge pull request #2039 from vitorpamplona/claude/relay-manager-user-search-jivZ2
Replace hex input with user search dialog in relay management
2026-03-30 15:46:03 -04:00
Vitor Pamplona
17ff27bac9 Merge pull request #2038 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 15:00:48 -04:00
Vitor Pamplona
3913384fcf Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 15:00:32 -04:00
Vitor Pamplona
b02b8c4ed9 new change logs 2026-03-30 14:57:13 -04:00
Crowdin Bot
ed31b12e3f New Crowdin translations by GitHub Action 2026-03-30 18:50:03 +00:00
Vitor Pamplona
3b63264927 Merge pull request #2036 from Nam0101/contribai/docs/undocumented-identity-based-comparison-i
Docs: Undocumented identity-based comparison in `equalImmutableLists`
2026-03-30 14:48:27 -04:00
Vitor Pamplona
640c47e29f v1.07.1 2026-03-30 13:53:25 -04:00
Vitor Pamplona
3dd1b949eb Merge pull request #2037 from vitorpamplona/claude/threadsafe-without-synchronized-Zw8Tx
Replace synchronized queue with Channel and AtomicBoolean
2026-03-30 13:51:20 -04:00
Claude
13bd492e32 refactor: replace synchronized with lock-free CAS in BleChunkAssembler
Use AtomicReference holding an immutable list with a compare-and-set loop,
eliminating the synchronized block while maintaining thread safety.

https://claude.ai/code/session_01KLRz7FJgWRtgCcZe3qD1VX
2026-03-30 17:48:58 +00:00
Claude
ef8b9be0e8 refactor: replace synchronized with Channel + AtomicBoolean in BLE relay classes
Use Channel(UNLIMITED) as a lock-free send queue and AtomicBoolean with
atomic exchange for the isSending flag, eliminating all synchronized blocks
while maintaining thread safety via a double-check pattern.

https://claude.ai/code/session_01KLRz7FJgWRtgCcZe3qD1VX
2026-03-30 17:37:23 +00:00
Vitor Pamplona
c44f59b1ce Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 12:30:54 -04:00
Vitor Pamplona
0c4e5be1ea v1.07.0 2026-03-30 12:29:41 -04:00
Nguyen Van Nam
bc491b154b docs: undocumented identity-based comparison in equalimmutablelists
`equalImmutableLists` is public and uses referential equality (`===`) per element rather than structural equality (`==`). Without documentation, callers may assume normal list equality and get false negatives for value-equal but distinct instances. This is a non-obvious contract and should be documented explicitly to prevent subtle logic bugs.


Affected files: ListUtils.kt

Signed-off-by: Nguyen Van Nam <nam.nv205106@gmail.com>
2026-03-30 22:48:41 +07:00
davotoula
a7259b21ee Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-30 17:40:40 +02:00
davotoula
06fae3ba31 remove unused imports 2026-03-30 17:26:21 +02:00
Vitor Pamplona
ea7a839568 Merge pull request #2035 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 11:13:44 -04:00
Crowdin Bot
6912a120f8 New Crowdin translations by GitHub Action 2026-03-30 15:12:36 +00:00
davotoula
3d0364ec5e update translations: CZ, DE, PT, SE 2026-03-30 17:09:39 +02:00
davotoula
f76fc23840 update logging to new style 2026-03-30 16:42:38 +02:00
davotoula
43914d655c chore: add .worktrees/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 15:52:02 +02:00
David Kaspar
e9a33a88ca Merge pull request #2032 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 15:11:05 +02:00
davotoula
40ce80786e update logging to new style 2026-03-30 14:50:42 +02:00
Crowdin Bot
4581233c10 New Crowdin translations by GitHub Action 2026-03-30 12:50:10 +00:00
Vitor Pamplona
f81631d95c Merge pull request #2034 from vitorpamplona/claude/simplify-relay-md-docs-GXKoW
Simplify RELAY.md documentation for clarity and brevity
2026-03-30 08:48:28 -04:00
Vitor Pamplona
5214470209 Merge pull request #2033 from vitorpamplona/claude/create-nostr-readme-BUScG
Add comprehensive Nostr client documentation guide
2026-03-30 08:48:14 -04:00
Claude
f65fd30728 docs: simplify RELAY.md by removing verbose sections
Remove architecture diagram, NIP support table, module table,
Live Subscriptions explanation, serve method details, production-ready
example, and excessive code comments. Keep the readable Quick Start,
Store options, and Policy documentation in a more concise form.

https://claude.ai/code/session_01U3iW3eRD7bfLwrM3L9gkDc
2026-03-30 12:46:38 +00:00
David Kaspar
4eb03f66f9 Merge pull request #2029 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 14:42:13 +02:00
Crowdin Bot
7ea9f72a0a New Crowdin translations by GitHub Action 2026-03-30 12:38:18 +00:00
Vitor Pamplona
a468f3b6d6 Merge pull request #2031 from vitorpamplona/claude/simplify-relay-api-Zq3o8
Add AutoCloseable support and simplify relay connection lifecycle
2026-03-30 08:36:44 -04:00
Claude
df88cab92d refactor: simplify relay API with AutoCloseable and serve() helper
- NostrServer and IEventStore implement AutoCloseable for .use {} support
- Add NostrServer.serve() to handle session lifecycle automatically
- IRelayPolicy + operator now returns PolicyStack instead of List
- Deprecate shutdown() in favor of close()
- Update RELAY.md guide to use simplified API patterns

https://claude.ai/code/session_013oL9PkQaFyNQHKVg2vw9qs
2026-03-30 12:35:27 +00:00
Claude
3d400d4197 docs: add CLIENT.md guide for building Nostr clients with Quartz
Companion to the existing RELAY.md, this guide covers the client side:
Ktor WebSocket transport implementation, subscribing to events (Flow
and callback APIs), publishing signed events, filters, key management,
and multi-relay patterns.

https://claude.ai/code/session_01KFos33kA9VoMhKdBPNLquF
2026-03-30 12:34:51 +00:00
Vitor Pamplona
b808f5a1f6 Merge pull request #2030 from vitorpamplona/claude/nip89-compliance-modernize-BWso3
Add PlatformLinkTag parsing and NIP-89 app handler extensions
2026-03-30 08:22:36 -04:00
Vitor Pamplona
5d7f37fb41 Merge pull request #2027 from nrobi144/fix/general-bugfixes
fix: desktop bugfixes — flaky test, repost rendering, reads feed
2026-03-30 08:21:52 -04:00
Claude
5f0fd52f26 refactor: remove nip88PollApps discovery package (out of scope)
https://claude.ai/code/session_013r2LXj11SieWa5PaLesKfC
2026-03-30 12:17:03 +00:00
Vitor Pamplona
e556f7716c Merge pull request #2028 from vitorpamplona/claude/create-relay-readme-6VEdI
Add comprehensive Nostr relay implementation guide (RELAY.md)
2026-03-30 08:11:25 -04:00
davotoula
17f071b0f4 Merge branch 'main' into chess-enhancements-and-bug-fixes
# Conflicts:
#	commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessEventBroadcaster.kt
2026-03-30 13:14:33 +02:00
nrobi144
0a41806d7e fix(desktop): feed loading issues — missing events, broken profile navigation
- Profile feed now includes reposts (kind 6/16), not just text notes
- Metadata consume returns false to avoid wasteful null note lookups
- Feed subscription limits raised from 50 to 200 for global/following
- Thread replies subscription fetches NIP-22 comments (kind 1111)
- Cache now handles CommentEvent for thread display
- Wire onNavigateToThread through UserProfileScreen so tapping notes opens threads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:33:18 +03:00
nrobi144
882c39fd0e fix(cache): use getOrCreateNote for reply linking to fix flaky thread test
Fixes #2001 — ThreadFilter test failed intermittently because
consumeTextNote used getNoteIfExists for reply-to linking. If the
reply event arrived before the root, the link was lost. Now uses
getOrCreateNote to create placeholders, same pattern as reposts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:44:24 +03:00
nrobi144
2b3005797e feat(desktop): render reposts and quoted notes in feed
- Extract GenericRepostLayout + BoostedMark to commons for cross-platform use
- Feed filters accept kind 6/16 reposts with deduplication by original note
- Relay subscriptions request kinds 1, 6, 16
- Cache consumes GenericRepostEvent (kind 16) and uses getOrCreateNote for
  repost originals to handle out-of-order arrival
- FeedNoteCard renders reposts with overlapping avatars + "Boosted" label
- QuotedNoteEmbed renders nostr:nevent/note references as embedded NoteCards
  with reactive metadata observation
- Direct relay subscriptions fetch missing referenced notes and author metadata
- FeedMetadataCoordinator routes all events (not just metadata) back to cache
- consumeMetadata invalidates note flows when author metadata arrives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:44:24 +03:00
nrobi144
0e8c6fa434 fix(cache): route ReadsScreen following-mode events through cache
Following-mode long-form feed was not calling consumeEvent(),
so LongTextNoteEvents never reached the cache. Back-navigation
showed empty reads because cache had nothing to seed from.
2026-03-30 11:44:23 +03:00
Claude
4ba92931f6 docs: add relay README for building with Ktor, NostrServer, and SQLite EventStore
Comprehensive guide covering the full relay stack: NostrServer setup,
SQLite EventStore configuration, Ktor WebSocket integration, policy
system (VerifyPolicy, FullAuthPolicy, PolicyStack), indexing strategies,
testing patterns, and NIP support matrix.

https://claude.ai/code/session_01CTyiKDNhgXdfCFBBBXf1NF
2026-03-30 04:13:53 +00:00
Claude
940e59b323 feat: NIP-89 compliance fixes, modernize package structure, link NIP-88 polls
Fix PlatformLinkTag.parse() off-by-one bug where tag indices were shifted
(platform/uri/entityType read from wrong positions). Add missing TagArrayExt
and EventExt files for definition and recommendation packages to align with
the modern pattern used by nip88Polls. Parameterize DiscoverNIP89FeedFilter
by targetKind and create NIP-89 poll app discovery infrastructure linking
NIP-88 polls (kind 1068) to the NIP-89 app handler discovery system.

https://claude.ai/code/session_013r2LXj11SieWa5PaLesKfC
2026-03-30 03:49:21 +00:00
Claude
4d8583eed9 feat: replace hex input with user search in relay manager allow/ban dialogs
Users can now search by name, npub, or NIP-05 identifier when adding
users to relay allow/ban lists, using the same UserSuggestionState
mechanism as the My List screens. The hex pubkey input was not
user-friendly since most users don't know their hex representation.

https://claude.ai/code/session_01SWReoziLKJKxrbwDWprQYs
2026-03-30 03:43:51 +00:00
Vitor Pamplona
35d677f531 Merge pull request #2025 from vitorpamplona/claude/nip90-dvm-kinds-itxXb
feat: add all NIP-90 DVM kind event classes from data-vending-machines spec
2026-03-29 23:28:15 -04:00
Claude
273af03c9f feat: add reader methods to all DVM request events for server processing
Each request event now exposes typed reader methods so DVM servers can
extract inputs, parameters, and configuration from incoming requests:
- inputs() returns List<InputTag> with value, type, relay, marker
- Kind-specific readers like language(), model(), searchQuery(), pow(), etc.
- Shared TagArrayExt with dvmParam(), dvmParamAll(), dvmParamValues()

Also adds readers to existing 5300/5301 events (dvmPubKey, user, relays).

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 03:21:19 +00:00
Claude
cd4b352bdf feat: add typed build parameters and shared DVM tag infrastructure
Add shared tag classes (InputTag, OutputTag) and TagArrayBuilder
extensions (inputUrl, inputText, inputEvent, inputJob, inputPrompt,
output, param) for all DVM kinds to reuse.

Update all 17 request event build() methods with typed parameters
matching their spec:
- 5000: inputUrl, outputMimeType, range, alignment
- 5001: eventIds, length, outputMimeType
- 5002: eventIds, language
- 5050: prompt, model, maxTokens, temperature, topK, topP, frequencyPenalty
- 5100: prompt, sourceImageUrl, model, lora, ratio, size, negativePrompt
- 5200: videoUrl, outputMimeType, range
- 5201: videoUrl, language, subtitle, range, outputFormat
- 5202: imageUrl
- 5250: text, language
- 5302: searchQuery, users, since, until, maxResults
- 5303: searchQuery, maxResults
- 5400: inputs, relays, groups, filterJson
- 5500: fileUrl
- 5900: eventId
- 5901: text
- 5905: eventJson, relays
- 5970: eventJson, pow

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 03:07:07 +00:00
Vitor Pamplona
ea2054fa56 Merge pull request #2024 from vitorpamplona/claude/update-nip-support-tUrxB
feat: update NIP support list to reflect current implementation status
2026-03-29 22:55:53 -04:00
Claude
fe9f2e71a0 feat: update NIP support list to reflect current implementation status
Cross-referenced the official NIP list from nostr-protocol/nips with
quartz package implementations. All NIPs are now supported except NIP-EE
(MLS Protocol). Added NIP-22, NIP-5A, NIP-B0. Removed merged NIPs
(NIP-12, NIP-16, NIP-20). Updated names to match official titles.

https://claude.ai/code/session_01TyGpa2VEryZ9aqmZvmE4v1
2026-03-30 02:47:10 +00:00
Claude
4f32776b24 feat: add all NIP-90 DVM kind event classes from data-vending-machines spec
Add request/response event classes for all 19 DVM kinds defined in
nostr-protocol/data-vending-machines: text extraction (5000/6000),
summarization (5001/6001), translation (5002/6002), text generation
(5050/6050), image generation (5100/6100), video conversion (5200/6200),
video translation (5201/6201), image-to-video (5202/6202), text-to-speech
(5250/6250), content search (5302/6302), people search (5303/6303),
event count (5400/6400), malware scanning (5500/6500), event timestamping
(5900/6900), OP_RETURN creation (5901/6901), event publish schedule
(5905/6905), and event PoW delegation (5970/6970).

Also fixes existing events:
- Content discovery response (6300) now parses both "a" and "e" tags
- User discovery response (6301) now has innerTags() to parse "p" tags

All new events registered in EventFactory.

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 02:45:54 +00:00
Vitor Pamplona
21488561e0 Merge pull request #2023 from vitorpamplona/claude/refactor-nip90-structure-ugeNj
Refactor NIP90 DVMs: reorganize packages and extract tag classes
2026-03-29 22:26:02 -04:00
Vitor Pamplona
570f7d787d Merge pull request #2022 from vitorpamplona/claude/nostr-ble-messaging-i2rrJ
Add NIP-BE Bluetooth Low Energy mesh networking support
2026-03-29 22:24:43 -04:00
Claude
6e7f6eccf1 refactor: reorganize NIP-BE into subpackages and add README
Restructured nipBEBle/ into logical subpackages:
- Root: public API surface (BleNostrMesh, BleConfig, BlePeer, BleRole)
- protocol/: wire format (BleMessageChunker, BleChunkAssembler)
- transport/: platform BLE abstraction (BleTransport, AndroidBleTransport)
- relay/: Nostr protocol over BLE (BleNostrClient, BleNostrServer, BleMeshManager)

Added README.md with quick start guide, package structure overview,
advanced usage examples, and wire protocol reference.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:23:49 +00:00
Claude
ce3de5caf7 refactor: restructure NIP90 DVMs to match NIP88 Polls pattern
Each event kind now gets its own subpackage with dedicated tag classes,
TagArrayExt for parsing, and TagArrayBuilderExt for building. Events use
the eventTemplate pattern instead of manual tag construction.

New structure:
- status/ (kind 7000) with StatusTag, AmountTag
- contentDiscoveryRequest/ (kind 5300) with RelaysTag, ParamTag
- contentDiscoveryResponse/ (kind 6300)
- userDiscoveryRequest/ (kind 5301)
- userDiscoveryResponse/ (kind 6301)

https://claude.ai/code/session_01JeB9nAK4SKuwKZBEd6EAPd
2026-03-30 02:22:54 +00:00
Claude
28f68382dd feat: add BleNostrMesh facade for simpler developer API
Before (4 steps, manual wiring):
  val transport = AndroidBleTransport(context)
  val mesh = BleMeshManager(transport, object : BleMeshListener { ... })
  transport.setListener(mesh)
  mesh.start()

After (2 steps, auto-wired):
  val mesh = BleNostrMesh(AndroidBleTransport(context))
  mesh.onEvent { event, peer -> saveEvent(event) }
  mesh.start()

The facade uses lambda callbacks instead of interface implementation,
and auto-wires the transport listener via AndroidBleTransportContract.
BleMeshManager remains available for advanced use cases.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:18:24 +00:00
Claude
57aec95e8d fix: align NIP-BE chunk format with KoalaSat/samiz reference implementation
Reviewed the samiz BLE implementation and fixed compatibility issues:

- Chunk index: changed from 2 bytes to 1 byte (matching samiz/NIP-BE example)
- Chunk overhead: 2 bytes total (1 index + 1 total count), not 3
- chunkSize parameter now means payload size (500), not total chunk size
- Android: TX power HIGH, advertise timeout indefinite (matching samiz)
- Android: request MTU 512 and CONNECTION_PRIORITY_HIGH on connect
- Android: discover services after MTU negotiation (samiz flow)
- Android: set WRITE_TYPE_DEFAULT on write characteristic
- MTU-to-chunkSize: properly accounts for ATT overhead (3) + chunk overhead (2)

These changes ensure wire-level compatibility with samiz devices.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:12:52 +00:00
Vitor Pamplona
2d21529176 Merge pull request #2021 from vitorpamplona/claude/implement-nip-29-MEtLk
Add NIP-29 Relay Groups event types and tag support
2026-03-29 21:58:40 -04:00
Vitor Pamplona
bb7cdc973a Merge pull request #2019 from vitorpamplona/claude/implement-nip69-InF2y
Add NIP-69 P2P Order Events support
2026-03-29 21:58:33 -04:00
Claude
fb78c1a4c4 feat: implement NIP-BE Nostr BLE Communications Protocol in Quartz
Adds full NIP-BE implementation for BLE-based Nostr peer-to-peer
messaging and synchronization with KMP support across all targets.

Protocol layer (commonMain):
- BleConfig: NIP-BE constants (service UUID, characteristic UUIDs)
- BleMessageChunker: DEFLATE compression + chunk splitting/joining
- BleRole/assignRole: Role assignment based on UUID comparison
- BleTransport: Platform-agnostic BLE transport interface
- BleNostrClient: IRelayClient implementation over BLE
- BleNostrServer: Relay server over BLE with notification support
- BleMeshManager: High-level mesh manager with auto-discovery,
  role assignment, and event broadcasting
- BleChunkAssembler: Thread-safe chunk reassembly

Platform support:
- Deflate expect/actual for jvmAndroid, apple, and linux targets
- AndroidBleTransport: Full Android BLE implementation using GATT
  server/client APIs with scanning, advertising, and MTU negotiation

Tests: Deflate compression, message chunking, role assignment,
and chunk assembly (25 tests passing).

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 01:58:26 +00:00
Vitor Pamplona
abd53c87a0 Merge pull request #2020 from vitorpamplona/claude/implement-nip-15-kMewH
Add NIP-15 Marketplace event types and data models
2026-03-29 21:58:26 -04:00
Claude
c5bd3ebcd3 feat: implement NIP-15 Nostr Marketplace protocol in Quartz
Adds support for the NIP-15 decentralized marketplace protocol with
all event kinds: StallEvent (30017), ProductEvent (30018),
MarketplaceEvent (30019), AuctionEvent (30020), BidEvent (1021),
and BidConfirmationEvent (1022). Follows NIP-88 Polls structure
with JSON content data models and tag builder extensions.

https://claude.ai/code/session_014tf8Fn7J5CNpEgRsyMnhAy
2026-03-30 01:56:01 +00:00
Vitor Pamplona
974b4fcfb8 Removing a hex scheme-less urldetector test to leave it for later. 2026-03-29 21:55:33 -04:00
Claude
f1547a020c feat: implement NIP-29 relay-based groups on Quartz
Add protocol-level support for NIP-29 relay-based groups with all
event kinds following the nip88Polls structure pattern.

Event kinds implemented:
- 9000 (put-user), 9001 (remove-user), 9002 (edit-metadata)
- 9005 (delete-event), 9007 (create-group), 9008 (delete-group)
- 9009 (create-invite), 9021 (join-request), 9022 (leave-request)
- 39000 (group-metadata), 39001 (group-admins)
- 39002 (group-members), 39003 (supported-roles)

Shared tags: GroupIdTag (h), PreviousTag, RoleTag, CodeTag, GroupAdminTag

https://claude.ai/code/session_016mcHkA5DUr4M2K5CddAGSq
2026-03-30 01:52:58 +00:00
Claude
0a25018f20 feat: implement NIP-69 P2P Order events (kind 38383) in Quartz
Add support for peer-to-peer order events as defined in NIP-69,
enabling unified liquidity pools across P2P trading platforms.

Implements P2POrderEvent as a BaseAddressableEvent with all required
tags (k, f, s, amt, fa, pm, premium, expires_at, expiration, y, z)
and optional tags (source, rating, network, layer, name, g, bond).
Follows the nip88Polls structure with per-tag classes, TagArrayExt,
and TagArrayBuilderExt. Reuses existing GeoHashTag and ExpirationTag.

https://claude.ai/code/session_01VpvrrRMLdjpB5C9Pq4VupK
2026-03-30 01:52:15 +00:00
Vitor Pamplona
59ea94c248 Fixes PlatformLog's missing file on linux 2026-03-29 21:48:56 -04:00
Vitor Pamplona
9e21fcd2e8 Fixes lack of proxy access to download VLC files in claude's web environment. 2026-03-29 21:43:54 -04:00
Vitor Pamplona
6371f2e0d0 Fixes Url 2026-03-29 21:41:57 -04:00
Vitor Pamplona
207fc6beba removes Volatile (missing import) 2026-03-29 21:30:12 -04:00
Vitor Pamplona
7068263c00 Merge pull request #2018 from vitorpamplona/claude/implement-nip88-rendering-Ps1Ea
Implement NIP-C7 chat messages (kind 9)
2026-03-29 21:24:59 -04:00
Claude
092c057e6f feat: implement NIP-C7 chat messages (kind 9)
Add ChatEvent in quartz/nipC7Chats with support for kind 9 chat
messages and q-tag based replies per the NIP-C7 specification.
Wire up rendering in NoteCompose and NoteMaster (ThreadFeedView).

https://claude.ai/code/session_01BmAMHBCRKXG612i6zrhhP4
2026-03-30 01:17:28 +00:00
Vitor Pamplona
15c4fd61f1 Merge pull request #2017 from vitorpamplona/claude/implement-nip5a-rendering-5ioVq
feat: implement NIP-5A static website event rendering
2026-03-29 20:43:09 -04:00
Vitor Pamplona
c89a02eb0d Merge branch 'main' into claude/implement-nip5a-rendering-5ioVq 2026-03-29 20:43:02 -04:00
Claude
104b836bbe refactor: remove path listing from NIP-5A rendering
Users should click through to view the website in the browser
rather than seeing a list of file paths in the note card.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-30 00:41:00 +00:00
Claude
869628ae16 Revert "refactor: redesign NIP-5A rendering as website preview card"
This reverts commit 07f16c2efe.
2026-03-30 00:37:59 +00:00
Claude
07f16c2efe refactor: redesign NIP-5A rendering as website preview card
Replace the file-listing style with a UrlPreviewCard-like layout
showing nsite host URL, title, description, and optional favicon.
Remove path rendering and unused string resources.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-30 00:25:09 +00:00
Vitor Pamplona
365a526b12 Merge pull request #2016 from vitorpamplona/claude/implement-nip7d-rendering-d4zgy
feat: implement NIP-7D thread events (kind 11)
2026-03-29 20:19:18 -04:00
Vitor Pamplona
7f4e1f153a Merge pull request #2014 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 20:17:43 -04:00
Crowdin Bot
d9471f9531 New Crowdin translations by GitHub Action 2026-03-30 00:14:57 +00:00
Vitor Pamplona
5e20806229 Merge pull request #2015 from vitorpamplona/claude/implement-nip87-rendering-PBlES
Add NIP-87 Ecash support with Cashu and Fedimint event types
2026-03-29 20:13:32 -04:00
Vitor Pamplona
f91f68a74e Merge pull request #2013 from vitorpamplona/claude/refactor-url-detector-FCLCA
Refactor URL detection and path normalization for clarity
2026-03-29 19:16:05 -04:00
Claude
c4044858a9 feat: implement NIP-87 ecash mint discoverability
Add protocol support and UI rendering for NIP-87 which defines ecash
mint discovery via three event kinds: MintRecommendationEvent (38000),
CashuMintEvent (38172), and FedimintEvent (38173).

https://claude.ai/code/session_01JR2nFVPjPGG9jTV4Qq2PUW
2026-03-29 22:25:46 +00:00
Claude
776bc6d36f feat: implement NIP-5A static website event rendering
Add Quartz protocol support for NIP-5A Pubkey Static Websites with
RootSiteEvent (kind 15128) and NamedSiteEvent (kind 35128), including
tag parsers for path, server, title, description, and source. Wire up
rendering in NoteCompose and ThreadFeedView to display site metadata.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-29 22:18:02 +00:00
Claude
8e66776ee1 feat: implement NIP-7D thread events (kind 11)
Add support for NIP-7D thread events with title rendering in both
NoteCompose (feed view) and ThreadFeedView (detail view). Replies
use existing NIP-22 kind 1111 comments.

https://claude.ai/code/session_01MR2hLpmq3pkzzT11t86dAt
2026-03-29 22:15:21 +00:00
David Kaspar
a170335db8 Merge pull request #2012 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 21:36:21 +02:00
Crowdin Bot
59c18f2c75 New Crowdin translations by GitHub Action 2026-03-29 18:39:41 +00:00
David Kaspar
6613d1cacd Merge pull request #2011 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 20:38:25 +02:00
Crowdin Bot
bed5f96f20 New Crowdin translations by GitHub Action 2026-03-29 14:53:06 +00:00
Vitor Pamplona
c624293661 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-29 10:51:11 -04:00
Vitor Pamplona
9dc208deba Removing internal runBlockings 2026-03-29 10:43:38 -04:00
Vitor Pamplona
decf7df2bb Merge pull request #2007 from davotoula/ci-build-stop-daemons-after-build
Added ./gradlew --stop (with if: always()) after the Gradle steps in both test and build-desktop jobs
2026-03-29 10:21:39 -04:00
Vitor Pamplona
ebbcb603fb Fixes lazy on trimming service 2026-03-29 10:20:06 -04:00
Vitor Pamplona
8613ae4f31 Speeding up start by parallelizing access to the preference files before they are needed. 2026-03-29 10:17:51 -04:00
Vitor Pamplona
56510e4213 Merge pull request #2010 from vitorpamplona/claude/nip43-relay-members-dWO19
Add NIP-43 relay members support with UI screens
2026-03-29 10:17:29 -04:00
Vitor Pamplona
cd17badaf4 Merge pull request #2008 from vitorpamplona/claude/add-nip-61-support-KTkuk
Add NIP-61 Nutzaps support with event types and tag handling
2026-03-29 10:17:18 -04:00
Vitor Pamplona
a4ce8439de Merge branch 'main' into claude/add-nip-61-support-KTkuk 2026-03-29 10:17:11 -04:00
Vitor Pamplona
2c5f80f613 Merge pull request #2009 from vitorpamplona/claude/add-nip60-support-DOf8B
Add NIP-60 Cashu wallet and spending history event support
2026-03-29 10:16:46 -04:00
Claude
cad0ff3d80 fix: Make add/remove member events support multiple p tags and add NIP-43 to NoteMaster
Changes memberPubKey() to memberPubKeys() returning a list of all p tags
in RelayAddMemberEvent and RelayRemoveMemberEvent. Updates renderers to
display multiple members. Adds NIP-43 event rendering to ThreadFeedView
(NoteMaster) alongside NoteCompose.

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 14:13:41 +00:00
Vitor Pamplona
ae99700cef Updates media 2026-03-29 10:02:52 -04:00
Vitor Pamplona
03c692a8cf Merge pull request #2005 from davotoula/Introduce-log-level-filtering
Introduce log level filtering
2026-03-29 09:56:21 -04:00
David Kaspar
efe0bde005 Merge pull request #2006 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 15:42:25 +02:00
davotoula
3f09150d80 Added ./gradlew --stop (with if: always()) after the Gradle steps in both test and build-desktop jobs 2026-03-29 15:41:40 +02:00
Crowdin Bot
bc367b2a7b New Crowdin translations by GitHub Action 2026-03-29 13:40:34 +00:00
davotoula
380026c7be optimise imports 2026-03-29 15:38:51 +02:00
davotoula
0b3bce5bc2 fixes for sonar 2026-03-29 15:13:43 +02:00
davotoula
c40c1ed383 perf: convert remaining 12 interpolated log calls to lambda overloads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 13:07:34 +02:00
davotoula
c17fb4641a feat: add find-non-lambda-logs skill for auditing log call hygiene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 13:00:21 +02:00
davotoula
6d89de9444 refactor: simplify PlatformLog actuals and convert remaining interpolated log calls to lambdas
- Extract private log() helper in JVM and iOS PlatformLog to reduce
  copy-paste branching
- Fix JVM formatter to be private and non-nullable
- Convert 60+ interpolated Log.w/e/i calls to lambda overloads,
  including hot-path Filter.kt toJson() and LocalCache event processing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:58:11 +02:00
davotoula
3b81731771 perf: convert 194 interpolated Log.d() calls to lambda overloads
Defers string construction until after the level check, avoiding
allocation when debug logging is filtered in release/benchmark builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:58:10 +02:00
davotoula
541b197a64 feat: add throwable support to Log.d/i and inline lambda overloads
- Add optional throwable parameter to d() and i() across all platforms
- Add inline lambda overloads for all log levels to defer message
  construction, avoiding string allocation when level is filtered
- Restore VoiceMessagePreview to pass throwable directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
250d89ca0a feat: set Log.minLevel in app entry points and migrate android.util.Log usages
- Set Log.minLevel based on BuildConfig.DEBUG in Amethyst.kt init
- Set Log.minLevel = DEBUG in Desktop Main.kt
- Migrate VideoCompressionHelper to quartz Log + LogLevel enum
- Migrate VoiceAnonymizer, VoiceMessagePreview, LiveStatusIndicator to quartz Log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
25b2cb42d0 feat: convert Log from expect/actual to filtered wrapper over PlatformLog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
65bc617222 feat: add PlatformLog actual implementations for all platforms
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:55:18 +02:00
davotoula
067938657f feat: add LogLevel enum and PlatformLog expect declaration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:55:18 +02:00
Claude
72106ea478 feat: add NIP-61 Nutzaps protocol support in quartz
Implements NIP-61 (Nutzaps) event types following the NIP-88 structure pattern:
- NutzapEvent (kind 9321): P2PK-locked Cashu token payments with proof, mint URL, unit tags
- NutzapInfoEvent (kind 10019): Replaceable event for recipient preferences (mints, relays, P2PK pubkey)
- NutzapRedemptionEvent (kind 7376): Token redemption history with NIP-44 encrypted content
- TokenEvent (kind 7375): Unspent proof storage with NIP-44 encrypted content

https://claude.ai/code/session_013tQRvZXdyh5VD11xENpNE5
2026-03-29 04:37:54 +00:00
Claude
32324e5bbe feat: Add NIP-60 Cashu wallet support in quartz
Implements the NIP-60 specification for cashu-based wallets with four event types:
- CashuWalletEvent (kind:17375) - replaceable wallet definition with encrypted mints/privkey
- CashuTokenEvent (kind:7375) - unspent cashu proofs with NIP-44 encrypted JSON content
- CashuSpendingHistoryEvent (kind:7376) - transaction history with encrypted tag array
- CashuMintQuoteEvent (kind:7374) - optional mint quote state with NIP-40 expiration

Follows the same structural patterns as the NIP-88 polls implementation.

https://claude.ai/code/session_018UfHPzrQCAFMwB1zB1ftDM
2026-03-29 04:34:41 +00:00
Claude
c3447e2a05 feat: Add @Preview annotations to NIP-43 UI components
Adds preview functions for RelayMemberEventCard variants (membership
list, add/remove member, join/leave request) and MembershipActions
states (not member, is member, join sent, leave sent).

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 04:22:33 +00:00
Claude
a462221ca6 refactor: improve URL detector performance and readability
- Replace 6 regex operations in Url.removeDotSegments() with simple
  string operations (startsWith/indexOf) using a when-expression
- Remove regex-based dropLastSegment() in favor of StringBuilder with
  lastIndexOf
- Extract duplicated hex/octal/decimal parsing in DomainNameReader
  into shared parseNumericLiteral() helper
- Deduplicate scheme matching by unifying findValidScheme* methods
  into findSchemeSuffix() using regionMatches (avoids lowercase allocation)
- Extract readPath() validity check into isPathValid() to remove
  duplicated 5-line condition
- Extract trySchemeNoSlashesOrUserPass() from readScheme() to
  eliminate duplicated branch logic
- Restructure readCurrent() with when-expression and extract
  resetDomainCounters() helper

https://claude.ai/code/session_017oGieyaUiLCxehNJ5aMFDK
2026-03-29 04:08:58 +00:00
Claude
b1b9707c5f feat: Implement NIP-43 relay access metadata and membership management
Adds full NIP-43 support with Quartz event classes (kinds 13534, 8000,
8001, 28934, 28935, 28936) following the nip88Polls structure pattern.
Includes relay membership list screen with join/leave actions and
NoteCompose rendering for NIP-43 events in the feed.

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 03:13:56 +00:00
Vitor Pamplona
48da76c6a3 Activates the decryption for all types of files if it matches the decryption url with the cipher info 2026-03-28 22:24:54 -04:00
Vitor Pamplona
a096d62cfb Adds relay discovery to the node master rendering list 2026-03-28 22:19:56 -04:00
Vitor Pamplona
8702ec4d96 Adds a scroll to the settings page. 2026-03-28 20:22:16 -04:00
Vitor Pamplona
4b5405c6b7 Dynamically adjust the preferred server to the first one when the list changes and the current default is not in the list of servers. 2026-03-28 20:21:57 -04:00
Vitor Pamplona
589bb041b2 Fixes refactoring issues by Claude. 2026-03-28 19:58:04 -04:00
Vitor Pamplona
ff9b619d26 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  fix: keep screen on during PiP playback and survive screen lock
  feat: add RelayDiscoveryEvent renderer for feed display
  feat: add OpenGraph preview to WebBookmark cards
  revert: keep INostrClient/NostrClient naming and restore onEose/isLive
  refactor: simplify NostrClient API for beginner-friendliness
2026-03-28 19:25:29 -04:00
Vitor Pamplona
14ff2fbf8c Sends Accept and Content-Type to the NIP86 request 2026-03-28 19:23:48 -04:00
Vitor Pamplona
7290f16d0d sorts pubkeys in the relay management and fixes issues on duplicated keys for the lazy column 2026-03-28 19:23:23 -04:00
Vitor Pamplona
567cfa33e8 Merge pull request #2000 from vitorpamplona/claude/add-relay-discovery-events-NqmlS
Add relay discovery event rendering UI component
2026-03-28 19:12:41 -04:00
Vitor Pamplona
31adfe80ff Merge pull request #1999 from vitorpamplona/claude/pip-wake-lock-investigation-XhK3D
Fix PiP video playback: keep screen on and improve lifecycle
2026-03-28 19:12:05 -04:00
Vitor Pamplona
dbd0fe03fa Adds another test case for NIP-11 2026-03-28 18:42:04 -04:00
Claude
1514de53c7 fix: keep screen on during PiP playback and survive screen lock
PiP video now keeps the screen on while playing (matching YouTube behavior)
and no longer kills the activity when the screen locks, allowing audio to
continue through the PlaybackService.

https://claude.ai/code/session_01QJzmFaabSA9YQ3oCQwEHTJ
2026-03-28 22:40:03 +00:00
Claude
0adea4f40a feat: add RelayDiscoveryEvent renderer for feed display
Add a composable to render NIP-66 RelayDiscoveryEvent (kind 30166)
in the note feed, displaying relay URL, RTT metrics with color-coded
chips, network type, relay type, requirements, supported NIPs,
accepted kinds, topics, and geohash location.

https://claude.ai/code/session_01EsPmtRFQgknkDV6zmKkeS2
2026-03-28 22:36:42 +00:00
Vitor Pamplona
23dcda55d3 Solves crashing when multiple relays with the same url are included in the resulting list. 2026-03-28 18:30:21 -04:00
Vitor Pamplona
d991af72ff Merge pull request #1986 from vitorpamplona/claude/simplify-nostrclient-api-ZpOTB
Refactor Nostr client API with clearer naming conventions
2026-03-28 18:09:12 -04:00
Vitor Pamplona
f96e2c02de Merge pull request #1997 from vitorpamplona/claude/add-opengraph-preview-uaOzk
Enhance web bookmarks with URL preview metadata
2026-03-28 18:08:59 -04:00
Vitor Pamplona
c596e16182 Improves layout of the Relay Monitor events. 2026-03-28 18:04:42 -04:00
Claude
9c361dbe1e feat: add OpenGraph preview to WebBookmark cards
Fetches URL metadata via the existing UrlCachedPreviewer to display
OG images, fallback titles, and descriptions in the bookmark list.

https://claude.ai/code/session_016igt51hduwQ7bXF9kNNHGD
2026-03-28 21:29:29 +00:00
Vitor Pamplona
485ed871d1 Finishing up the Poll Screen 2026-03-28 17:12:51 -04:00
Vitor Pamplona
20eb95ae57 Removing more warnings 2026-03-28 16:30:00 -04:00
Vitor Pamplona
3c01c17d59 Clearing more warnings 2026-03-28 15:46:59 -04:00
Vitor Pamplona
f3cefe0e13 resolving some more warnings 2026-03-28 15:35:45 -04:00
Vitor Pamplona
4db617aeef fixes the need for coroutine scopes 2026-03-28 15:26:29 -04:00
Claude
72bc7e0ed5 style: apply linter formatting to TextFieldState migration
https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:13:18 +00:00
Claude
f997586e4b feat: migrate remaining IMessageField implementors to TextFieldState
Completes the TextFieldState migration for ShortNotePostViewModel,
LongFormPostViewModel, and their associated post screens (GeoHash,
Hashtag, ShortNote, LongForm).

https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:11:09 +00:00
Claude
1d208edd33 feat: migrate ThinPaddingTextField to BasicTextField(TextFieldState) for GIF keyboard support
Migrates all new post screens from the old BasicTextField(TextFieldValue)
to the new BasicTextField(TextFieldState) API, which properly sets
EditorInfo.contentMimeTypes to enable GIF keyboard input.

Key changes:
- Add TextFieldState-based ThinPaddingTextField overload (keep old for readonly dropdowns)
- Create UrlUserTagOutputTransformation for the new OutputTransformation API
- Create TextFieldState extension functions (insertUrlAtCursor, replaceCurrentWord, currentWord)
- Update IMessageField interface to use TextFieldState + onMessageChanged()
- Migrate all IMessageField implementors: ChannelNewMessageViewModel,
  ChatNewMessageViewModel, NewProductViewModel, NewPublicMessageViewModel,
  CommentPostViewModel, ShortNotePostViewModel, LongFormPostViewModel
- Update PreviewState to accept String instead of TextFieldValue
- Add TextFieldState overload to UserSuggestionState.replaceCurrentWord()
- Update all post screen call sites

https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:09:38 +00:00
Vitor Pamplona
c0212c09e5 Merge pull request #1996 from vitorpamplona/claude/migrate-autofill-apis-kQZDc
Replace deprecated autofill API with semantics-based approach
2026-03-28 14:58:16 -04:00
Vitor Pamplona
8c1ca8dc8b Merge branch 'main' into claude/migrate-autofill-apis-kQZDc 2026-03-28 14:58:01 -04:00
Vitor Pamplona
c44f155a82 Merge pull request #1995 from vitorpamplona/claude/migrate-clipboard-manager-QEupN
Update Compose clipboard API from LocalClipboardManager to LocalClipboard
2026-03-28 14:50:19 -04:00
Claude
0eb32ae4fb refactor: migrate LocalClipboardManager to LocalClipboard
Replace deprecated LocalClipboardManager with LocalClipboard across
all 21 files in both amethyst and desktopApp modules.

https://claude.ai/code/session_01FE2NjKxhUYUFRvhyqbYpUH
2026-03-28 18:46:02 +00:00
Vitor Pamplona
9b3e77e2d5 Merge pull request #1994 from vitorpamplona/claude/migrate-tabrow-material3-FlqJl
Replace TabRow and ScrollableTabRow with Secondary variants
2026-03-28 14:43:54 -04:00
Claude
61cf21a875 feat: migrate from deprecated LocalAutofillTree to semantics-based Autofill API
Replace manual AutofillNode/LocalAutofillTree/LocalAutofill usage with
the new Modifier.semantics { contentType = ContentType.Password } API
from androidx.compose.ui.autofill in KeyTextField, PasswordField, and
AccountBackupScreen.

https://claude.ai/code/session_018e5E6dFATZTEnGtTZbkKhP
2026-03-28 18:40:29 +00:00
Claude
6a13b2b7b7 feat: migrate TabRow/ScrollableTabRow to Material 3 Secondary variants
Replace deprecated material3 TabRow with SecondaryTabRow and
ScrollableTabRow with SecondaryScrollableTabRow across all 12 files.

https://claude.ai/code/session_01VBDx3qQ8n9AzQJV3dGWYff
2026-03-28 18:40:10 +00:00
Vitor Pamplona
72f6e47478 Merge pull request #1993 from vitorpamplona/claude/migrate-context-compat-activity-iAHrz
Replace ContextCompat.startActivity with Context.startActivity
2026-03-28 14:39:21 -04:00
Claude
8f1b10efa6 refactor: replace ContextCompat.startActivity with Context.startActivity
ContextCompat.startActivity is an unnecessary wrapper that just delegates
to Context.startActivity. Call the method directly on the context instead.

https://claude.ai/code/session_019gvXxtttDRZTnSE1x8F2EE
2026-03-28 18:36:14 +00:00
Vitor Pamplona
9231fa5457 Merge pull request #1990 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 14:35:53 -04:00
Crowdin Bot
496ee8a58c New Crowdin translations by GitHub Action 2026-03-28 18:27:56 +00:00
Vitor Pamplona
7b1c28c4b7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  increase android CI build to 45 minutes
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
2026-03-28 14:25:44 -04:00
Vitor Pamplona
cfa59dab01 removing more warnings 2026-03-28 14:24:13 -04:00
Crowdin Bot
61000e9662 New Crowdin translations by GitHub Action 2026-03-28 17:47:14 +00:00
David Kaspar
7c02dc3d2d Merge pull request #1989 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 18:46:43 +01:00
davotoula
d707900aa0 increase android CI build to 45 minutes 2026-03-28 18:45:54 +01:00
Crowdin Bot
9c5e5381a4 New Crowdin translations by GitHub Action 2026-03-28 17:43:09 +00:00
davotoula
b8b754a698 update translations: CZ, DE, PT, SE 2026-03-28 18:39:09 +01:00
Vitor Pamplona
aa7f480f14 Merge branch 'main' into claude/simplify-nostrclient-api-ZpOTB 2026-03-28 13:09:52 -04:00
Vitor Pamplona
e8ba9a2d2f Merge pull request #1987 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 12:48:43 -04:00
Crowdin Bot
b6184ad075 New Crowdin translations by GitHub Action 2026-03-28 16:46:57 +00:00
Vitor Pamplona
352a7f12a2 Merge pull request #1985 from vitorpamplona/claude/fix-quartz-deprecations-Y8BYY
Suppress deprecation warnings and improve code quality
2026-03-28 12:45:25 -04:00
Claude
4be617e64a revert: keep INostrClient/NostrClient naming and restore onEose/isLive
Reverts three naming changes from the previous refactor:
- Interface stays INostrClient (not NostrClient)
- Class stays NostrClient (not DefaultNostrClient)
- onEose stays onEose (not onCaughtUp)
- isLive stays isLive (not isRealTime)

All other renames from the API simplification are preserved:
subscribe, unsubscribe, publish, count, fetchAll, fetchFirst, etc.

https://claude.ai/code/session_01JPcYCcRx5eZN4GvgGxGwbf
2026-03-28 16:43:30 +00:00
Claude
4e2717c5c5 refactor: simplify NostrClient API for beginner-friendliness
Comprehensive rename of the NostrClient relay infrastructure to use
intuitive, self-documenting names instead of protocol-level jargon.

Interface/class renames:
- INostrClient → NostrClient (interface)
- NostrClient → DefaultNostrClient (implementation)
- IRequestListener → SubscriptionListener
- IRelayClientListener → RelayConnectionListener
- IOpenNostrRequest → SubscriptionHandle
- NostrClientStaticReq → StaticSubscription
- NostrClientDynamicReq → DynamicSubscription

Method renames:
- openReqSubscription() → subscribe()
- close(subId) → unsubscribe(subId)
- send() → publish()
- queryCount() → count()
- subscribe/unsubscribe(listener) → addConnectionListener/removeConnectionListener
- renewFilters() → syncFilters()

Callback renames:
- onEose → onCaughtUp
- isLive → isRealTime
- onStartReq → onSubscriptionStarted
- onCloseReq → onSubscriptionClosed

Extension function renames:
- query() → fetchAll()
- downloadFirstEvent() → fetchFirst()
- req() → subscribe()
- reqAsFlow() → subscribeAsFlow()
- reqUntilEoseAsFlow() → fetchAsFlow()
- sendAndWaitForResponse() → publishAndConfirm()
- queryCountSuspend() → count()
- queryCountMergedHll() → countMerged()
- reqBypassingRelayLimits() → fetchAllPages()
- updateFilter() → refresh() on SubscriptionHandle

https://claude.ai/code/session_01JPcYCcRx5eZN4GvgGxGwbf
2026-03-28 16:43:30 +00:00
Claude
610fcd7e0c fix: suppress deprecation and unchecked cast warnings in Quartz internal code
Deprecated APIs in Quartz are kept for library consumers but internal usages
now carry @Suppress annotations so the module builds warning-free. Also
replaces redundant .toInt() on hex Int literals in ChaCha20Core and migrates
deprecated java.net.URL to URI.resolve() in ServerInfoParser.

The spotless license header delimiter is updated to recognize @file: annotations.

https://claude.ai/code/session_01EwS56YAGGnnac5EuwhaUSs
2026-03-28 16:39:51 +00:00
Vitor Pamplona
0054c01921 Merge pull request #1984 from vitorpamplona/claude/parallelize-db-tests-I6D3g
Refactor forEachDB to use coroutines with parallel execution
2026-03-28 12:22:02 -04:00
Claude
d299b65c8a feat: make forEachDB action suspending, remove redundant runBlocking
Change the action lambda to `suspend (EventStore) -> Unit` so callers
can use suspend functions directly. Remove the now-unnecessary
runBlocking wrapper around delay() in ExpirationTest.

https://claude.ai/code/session_01TQRzVTAXBVWRGstcsapA5F
2026-03-28 16:15:23 +00:00
Claude
0889473c15 feat: parallelize BaseDBTest forEachDB using coroutines
Each EventStore uses an independent in-memory SQLite database, so tests
can safely run concurrently. This launches all 16 DB configurations in
parallel via coroutines on Dispatchers.Default instead of running them
sequentially.

https://claude.ai/code/session_01TQRzVTAXBVWRGstcsapA5F
2026-03-28 16:12:44 +00:00
Vitor Pamplona
009326ae0b Merge pull request #1982 from davotoula/fix-for-githooks-in-git-worktree
Handle git worktree where .git is a file, not a directory
2026-03-28 12:08:28 -04:00
davotoula
60686d0fef fix: resolves the .git file in worktrees by reading the gitdir: pointer and using the actual git directory for hooks installation 2026-03-28 16:59:01 +01:00
Vitor Pamplona
4c5606cd8d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: complete NIP-85 Trusted Assertions implementation in quartz
2026-03-28 11:51:50 -04:00
Vitor Pamplona
dd2ec3f05f moves the initialization of the Secp lib to the setup of the test to make sure it doesn't affect the expiration timing tests. 2026-03-28 11:50:43 -04:00
Vitor Pamplona
b147e6d4c3 Fixes missing actual 2026-03-28 11:39:25 -04:00
Vitor Pamplona
2745b29bf7 removing some warnings 2026-03-28 11:39:15 -04:00
Vitor Pamplona
be62ca5374 Merge pull request #1981 from vitorpamplona/claude/implement-nip-85-hBlbp
Add NIP-85 trusted assertions support for events and addressables
2026-03-28 11:36:29 -04:00
Vitor Pamplona
b51efb5caf Merge branch 'main' into claude/implement-nip-85-hBlbp 2026-03-28 11:35:19 -04:00
Vitor Pamplona
5776e74bb5 Fixing some warnings 2026-03-28 11:33:03 -04:00
Vitor Pamplona
eaa45aee05 Fixes constructor issue in this BigDecimal Implementation 2026-03-28 11:31:14 -04:00
Vitor Pamplona
c11e41529e Merge pull request #1980 from davotoula/toast-for-downloads
Show toast instead of dialog on media download success
2026-03-28 10:36:06 -04:00
davotoula
cbf9b31a3f feat: show toast instead of dialog on media download success
Replace the interruptive AlertDialog with a brief Android Toast when
media downloads complete. Errors still show the full dialog for visibility.
2026-03-28 15:04:27 +01:00
Claude
d40dccde11 feat: complete NIP-85 Trusted Assertions implementation in quartz
Add full compliance with NIP-85 by implementing all four assertion event
kinds and their associated tag types in the nip85TrustedAssertions package:

- Kind 30382 (User Assertions): Add all 15 missing tag parsers and
  builders (first_created_at, post_cnt, reply_cnt, reactions_cnt, all
  zap/report/topic/active_hours tags) to ContactCardEvent
- Kind 30383 (Event Assertions): New EventAssertionEvent with rank,
  comment_cnt, quote_cnt, repost_cnt, reaction_cnt, zap_cnt, zap_amount
- Kind 30384 (Addressable Event Assertions): New
  AddressableAssertionEvent with same tags as 30383
- Kind 30385 (External ID Assertions): New ExternalIdAssertionEvent
  with rank, comment_cnt, reaction_cnt
- Add ProviderTypes for all new assertion kinds (30383/30384/30385)
- Register all new event kinds in EventFactory
- Add comprehensive tests for all assertion event types

https://claude.ai/code/session_01XZQsSnkHJ6tFiPS7fRLzPJ
2026-03-28 13:59:12 +00:00
Vitor Pamplona
e3c6c35688 Merge pull request #1973 from vitorpamplona/claude/review-quartz-multiplatform-00Oi0
feat: add linuxX64 KMP target and restructure native source sets
2026-03-28 09:54:11 -04:00
Vitor Pamplona
05e7b305a7 Merge pull request #1977 from vitorpamplona/claude/polls-feed-screen-DQ78j
Add Polls feed screen with filtering and discovery support
2026-03-28 09:53:50 -04:00
Vitor Pamplona
3b15242a02 Merge pull request #1978 from vitorpamplona/claude/nip-a4-compliance-mftpU
Add NIP-A4 support: k tag in zap requests and e tag filtering
2026-03-28 09:53:15 -04:00
Claude
5db9330306 fix: update test packages after nipA4PublicMessages rename
Move test files from experimental.publicMessages to nipA4PublicMessages
to match the package rename on main.

https://claude.ai/code/session_019JsSsTTXivnNWiAtE5DE1R
2026-03-28 13:52:02 +00:00
Claude
559ad40223 feat: add Polls feed screen with Global/Follows filter
Add a dedicated Polls-only feed screen (NIP-88 kind 1068, non-zap polls)
accessible from the left navigation drawer. The screen includes the
standard top nav filter bar for Global/Follows/etc filtering, reusing
the Discovery follow list settings.

New files:
- PollsFeedFilter: filters LocalCache for PollEvent only
- PollsFilterAssembler + PollsSubAssembler: relay subscriptions
- PollsScreen: main screen with pull-to-refresh feed
- PollsTopBar: top bar with Global/Follows filter spinner
- Relay filter subassemblies for authors, follows, global, hashtag

Wiring:
- Route.Polls added to navigation
- Polls entry in left drawer with ic_poll icon
- pollsFeed added to AccountFeedContentStates
- PollsFilterAssembler added to RelaySubscriptionsCoordinator

https://claude.ai/code/session_013JWrsYpQczmZtpW3WwRRfK
2026-03-28 13:51:51 +00:00
Vitor Pamplona
3ad42d2d8b Merge pull request #1979 from vitorpamplona/claude/nip24-compliance-check-WvjhO
Add birthday field support to UserMetadata
2026-03-28 09:48:35 -04:00
Claude
b465785c27 feat: NIP-A4 compliance - add k tag to zap requests and enforce e tag prohibition
- Add KindTag to LnZapRequestEvent.create() when zapping events, as required
  by NIP-A4 ("NIP-57 zaps MUST include the k tag to 24")
- Enforce e tag prohibition in all PublicMessageEvent.build() methods by
  stripping any e tags from the tag builder output (NIP-A4: "e tags must not
  be used")
- Add comprehensive tests for both changes

https://claude.ai/code/session_019JsSsTTXivnNWiAtE5DE1R
2026-03-28 13:48:29 +00:00
Vitor Pamplona
9b483f903d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  feat: implement NIP-32 Labeling protocol in quartz
2026-03-28 09:41:26 -04:00
Vitor Pamplona
0c7e5fddea Fixes test cases 2026-03-28 09:41:09 -04:00
Claude
46fd8c651c feat: add NIP-24 birthday field to UserMetadata
NIP-24 defines an optional birthday object with year, month, and day
fields for kind 0 metadata events. This was the only NIP-24 field
missing from Quartz's UserMetadata model.

https://claude.ai/code/session_01Sbj2DF5XDtEuDCeJ7yR6oS
2026-03-28 13:30:51 +00:00
Vitor Pamplona
7e1ff20e6d Merge pull request #1976 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 09:30:27 -04:00
Crowdin Bot
89b0064a11 New Crowdin translations by GitHub Action 2026-03-28 13:27:22 +00:00
Vitor Pamplona
b5a0c8b6d5 Merge pull request #1975 from vitorpamplona/claude/implement-nip-32-gWs2E
Add NIP-32 Labeling support with LabelEvent and tag parsing
2026-03-28 09:25:48 -04:00
Vitor Pamplona
943d46e01a Moves public messages out of experimental 2026-03-28 09:18:34 -04:00
Vitor Pamplona
5d281c34d5 Moves trusted assertions out of experimental 2026-03-28 09:16:44 -04:00
Vitor Pamplona
bfad08a4a7 Merge pull request #1974 from greenart7c3/claude/add-workflow-permissions-OO87O
Add explicit permissions to GitHub Actions workflows
2026-03-28 08:45:08 -04:00
Claude
8cbd11816e ci: add explicit permissions to workflows 2026-03-28 09:32:48 +00:00
Claude
aa15085f40 feat: implement NIP-32 Labeling protocol in quartz
Add kind 1985 LabelEvent with L (namespace) and l (label) tag support
for distributed moderation, content classification, and license assignment.
Includes tag parsers, builder extensions, self-reporting helpers, and tests.

https://claude.ai/code/session_011t5ZoP1BdgZTT5Cen9GH5z
2026-03-28 03:57:46 +00:00
Claude
5fc7526076 feat: add linuxX64 KMP target and restructure native source sets
Restructure quartz module's Kotlin/Native source set hierarchy for
proper multiplatform coverage. Adds linuxX64 target with complete
actual implementations for all expect declarations.

Source set hierarchy:
  commonMain
  ├── jvmAndroid → jvmMain, androidMain
  └── nativeMain (shared pure Kotlin)
      ├── appleMain (Apple APIs) → iosMain
      └── linuxMain (POSIX/OpenSSL) → linuxX64Main

nativeMain: Address, OptimizedJsonMapper, EventHasherSerializer,
  ChessEngine, Secp256k1Instance, BitSet, StringExt, io/ utilities

appleMain: Log (NSLog), SecureRandom (SecRandomCopyBytes),
  UriParser (NSURLComponents), BigDecimal (NSDecimalNumber),
  GZip (zlib), Sha256 (CC_SHA256), crypto (Apple provider),
  LargeCache (CacheMap), UrlEncoder, UnicodeNormalizer

linuxMain: Log (println), SecureRandom (/dev/urandom),
  UriParser (pure Kotlin), BigDecimal (pure Kotlin),
  GZip (zlib cinterop), Sha256 (OpenSSL), crypto (OpenSSL),
  LargeCache (AtomicReference<LinkedHashMap>), UrlEncoder

macOS targets deferred pending negentropy-kmp macOS artifacts.
macosMain/Platform.kt retained as scaffold for future activation.

https://claude.ai/code/session_01M9CAuPUV3TfPB2xShnsBhw
2026-03-28 02:55:20 +00:00
Vitor Pamplona
007a0b0d43 More descriptive counters 2026-03-27 21:42:35 -04:00
Vitor Pamplona
391a0bfab2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  sonar fixes
  update translations: CZ, DE, PT, SE
  New Crowdin translations by GitHub Action
  update video compression library to latest
  update to use new limit short size bit more aggressive compression defaults
  add local maven repo for easier library development
2026-03-27 19:40:04 -04:00
Vitor Pamplona
ea8fbd4af9 Making a localhost:3030 string work correctly in the URL Detector 2026-03-27 19:34:55 -04:00
Vitor Pamplona
16dd14f403 Merge pull request #1972 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 19:30:35 -04:00
Crowdin Bot
8f3c9d2d93 New Crowdin translations by GitHub Action 2026-03-27 23:25:02 +00:00
davotoula
44b8ef5d8f sonar fixes 2026-03-28 00:23:31 +01:00
davotoula
c5a88bba97 update translations: CZ, DE, PT, SE 2026-03-28 00:19:38 +01:00
David Kaspar
6a6fadb493 Merge pull request #1971 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 00:06:55 +01:00
Crowdin Bot
4d38c1dfb0 New Crowdin translations by GitHub Action 2026-03-27 22:58:31 +00:00
Vitor Pamplona
da060a27fd Merge pull request #1970 from davotoula/update-video-compression-library
Update video compression library
2026-03-27 18:57:20 -04:00
Vitor Pamplona
49f0ca30b2 Fixes test case 2026-03-27 18:49:12 -04:00
Vitor Pamplona
e8c98f31e6 Reverting merge issues 2026-03-27 18:48:29 -04:00
Vitor Pamplona
ff8e5dd4e4 Fixes missing strings during merge 2026-03-27 18:43:06 -04:00
Vitor Pamplona
e444e59f66 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  feat: add NIP-66 monitor reports UI and string resources
  feat: register NIP-66 filter assembler in coordinator
  feat: add NIP-66 relay info subscription composable
  feat: add NIP-66 relay discovery filter sub-assembler
  feat: display NIP-66 relay monitor reports in relay information screen
2026-03-27 18:27:40 -04:00
davotoula
bb469700b7 update video compression library to latest 2026-03-27 23:25:55 +01:00
davotoula
3423065b94 update to use new limit short size
bit more aggressive compression defaults
2026-03-27 23:25:55 +01:00
Vitor Pamplona
549baa2ceb Ignore vlc temp files 2026-03-27 17:04:04 -04:00
Vitor Pamplona
c068fe38be Fixes tests 2026-03-27 16:36:42 -04:00
davotoula
acfb596647 add local maven repo for easier library development 2026-03-27 21:32:32 +01:00
Vitor Pamplona
323694761e Updating dependencies 2026-03-27 16:21:16 -04:00
Vitor Pamplona
540194e0ca Merge pull request #1969 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 16:18:57 -04:00
Crowdin Bot
6a99210633 New Crowdin translations by GitHub Action 2026-03-27 20:15:23 +00:00
Vitor Pamplona
d0a5e7d44b Merge pull request #1968 from vitorpamplona/claude/relay-info-nip66-GpWzf
Add relay monitor discovery events display to relay info screen
2026-03-27 16:13:49 -04:00
Vitor Pamplona
e0c569ea9b Merge pull request #1966 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 16:13:04 -04:00
Crowdin Bot
1696ad1248 New Crowdin translations by GitHub Action 2026-03-27 20:04:06 +00:00
Claude
77656ffc84 feat: add NIP-66 monitor reports UI and string resources
Add relay monitor report section to RelayInformationScreen showing
discovery events from multiple NIP-66 monitors. Add string resources
for the monitor reports UI.

https://claude.ai/code/session_01KWuXTp3Z7HST9cu6RWQ4G5
2026-03-27 19:58:26 +00:00
Vitor Pamplona
aa3202de4b feat: register NIP-66 filter assembler in coordinator 2026-03-27 15:56:19 -04:00
Vitor Pamplona
3387e119d5 feat: add NIP-66 relay info subscription composable 2026-03-27 15:55:25 -04:00
Vitor Pamplona
0a44bb4904 feat: add NIP-66 relay discovery filter sub-assembler 2026-03-27 15:55:19 -04:00
Vitor Pamplona
d65ffd1978 feat: display NIP-66 relay monitor reports in relay information screen
Subscribe to kind 30166 (RelayDiscoveryEvent) events for the relay being
viewed and display monitoring data from multiple providers. Shows RTT
metrics (open/read/write), network type, relay type, supported NIPs,
and access requirements reported by each monitor.

https://claude.ai/code/session_01KWuXTp3Z7HST9cu6RWQ4G5
2026-03-27 15:55:02 -04:00
Vitor Pamplona
4d426a011a Fixes typo 2026-03-27 15:50:15 -04:00
Vitor Pamplona
7ec1ec1a3f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: (26 commits)
  feat(cache): seed NotificationsScreen from cache on compose
  feat(cache): wire State classes on DesktopIAccount for GC retention
  feat(cache): extract Kind3FollowListState + Nip65RelayListState to commons
  feat(cache): extract BookmarkListState to commons/commonMain
  feat(cache): add periodic memory cleanup for Desktop
  feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
  refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
  feat(cache): seed Reads and Bookmarks screens from cache on compose
  fix(cache): don't reset followersCount to 0 on subscription restart
  feat(cache): cache profile metadata and follower/following counts
  fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions
  fix(cache): add missing relay subscriptions for FeedScreen and UserProfileScreen
  refactor(cache): P3 simplifications — remove dead code, reduce abstractions
  fix(cache): address review findings — subscription churn, O(n) size, atomicity
  feat(cache): ThreadScreen → DesktopFeedViewModel + DesktopThreadFilter
  feat(ui): relay health indicator in NavigationRail
  feat(cache): UserProfileScreen notes feed → DesktopFeedViewModel
  feat(cache): FeedScreen → DesktopFeedViewModel + Note-based FeedNoteCard
  feat(cache): Coordinator error handling + interaction subscriptions
  feat(cache): kind-based consumer registry + new event types
  ...
2026-03-27 15:47:56 -04:00
Vitor Pamplona
787b9ab9db Merge pull request #1905 from nrobi144/feat/desktop-cache-v2
feat(desktop): cache-centric architecture for desktop feeds
2026-03-27 15:45:34 -04:00
Vitor Pamplona
20f8d14d28 Final touches on rendering Zap Goals 2026-03-27 15:43:20 -04:00
Vitor Pamplona
58628dbc12 spotless apply 2026-03-27 15:36:14 -04:00
Vitor Pamplona
3a7402a62b Merge pull request #1964 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 15:34:28 -04:00
Vitor Pamplona
fe92deeb53 Merge pull request #1965 from vitorpamplona/claude/nip75-goals-feature-332LM
Add NIP-75 Zap Goals support with creation and rendering
2026-03-27 15:33:57 -04:00
Vitor Pamplona
25fee78296 Merge branch 'main' into claude/nip75-goals-feature-332LM 2026-03-27 15:33:50 -04:00
Vitor Pamplona
787eb01c2c Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  Get rid of libsodium related files.
2026-03-27 15:29:10 -04:00
Vitor Pamplona
1a2e34bdcf Adds a Pin icon to the post instead of making a separate section in user profiles 2026-03-27 15:27:50 -04:00
Crowdin Bot
a0bea7602e New Crowdin translations by GitHub Action 2026-03-27 19:19:32 +00:00
Vitor Pamplona
ddb23118a2 Merge pull request #1963 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 15:18:10 -04:00
Vitor Pamplona
0a62b898be Merge pull request #1962 from KotlinGeekDev/kmp-completeness
Remove libsodium files.
2026-03-27 15:17:59 -04:00
Crowdin Bot
37dad897cf New Crowdin translations by GitHub Action 2026-03-27 18:59:52 +00:00
Vitor Pamplona
b79cd53d34 Finishes vanish final touches. 2026-03-27 14:57:20 -04:00
KotlinGeekDev
d7930d4527 Get rid of libsodium related files. 2026-03-27 19:26:17 +01:00
Vitor Pamplona
face5ba6e6 Fixes long lists of vanish request relays 2026-03-27 14:18:32 -04:00
Vitor Pamplona
427423edda Adds the user's outbox relays to keep a copy of vanish requests. 2026-03-27 14:18:07 -04:00
Vitor Pamplona
153666b9b4 Improves vanishing tests 2026-03-27 13:59:55 -04:00
Vitor Pamplona
1ece456f41 Uses the new query method from quartz 2026-03-27 12:36:45 -04:00
Vitor Pamplona
a456517fbf Offers a new query method from quartz 2026-03-27 12:36:35 -04:00
Vitor Pamplona
d3ddb86dbb Allows users to pick relays on the request to vanish screen 2026-03-27 12:13:19 -04:00
Vitor Pamplona
0862be2761 Improvements to the layout of the kinds 2026-03-27 11:21:46 -04:00
Vitor Pamplona
cf28e799b7 Better wording 2026-03-27 11:00:47 -04:00
Vitor Pamplona
94c900e98f Moves manage action to the Relay Info header 2026-03-27 10:59:59 -04:00
Vitor Pamplona
e144eabf96 Merge pull request #1961 from vitorpamplona/claude/pinned-notes-in-tab-8y3TR
Integrate pinned notes into new threads tab
2026-03-27 10:25:26 -04:00
Vitor Pamplona
e209b9e0cc Merge pull request #1960 from vitorpamplona/claude/pubkey-user-mapping-vWcSL
Display user profiles in relay management pubkey lists
2026-03-27 10:16:12 -04:00
Claude
7618a5039d feat: show pinned notes at top of Notes tab instead of separate tab
Remove the dedicated Pinned Notes tab from the profile and display
pinned notes at the beginning of the Notes tab with a pin icon marker.

https://claude.ai/code/session_01ChamaudsEwwQ8mNtqnx4ze
2026-03-27 14:12:06 +00:00
Claude
9e871c5719 refactor: use flow.map for pubkey-to-user resolution, filter invalid pubkeys
Replace MutableStateFlow caches with Flow.map derived from bannedPubkeys
and allowedPubkeys. Use LocalCache.checkGetOrCreateUser to safely resolve
pubkeys, filtering out invalid ones (null). Remove raw pubkey display
from PubkeyUserCard since invalid pubkeys are already excluded.

https://claude.ai/code/session_018x2PcJX6VGyJmVuphkbK54
2026-03-27 14:08:31 +00:00
Vitor Pamplona
9be81ec1a5 Merge pull request #1959 from vitorpamplona/claude/auto-load-opengraph-bookmarks-XGj82
Add Open Graph preview fetching to web bookmarks dialog
2026-03-27 09:57:24 -04:00
Claude
438d7c9122 feat: map pubkeys to users with profile info in relay management
Resolve pubkeys to User objects via LocalCache in RelayManagementViewModel,
caching the mapping. Render banned/allowed pubkeys using SlimListItem with
user picture, name, and NIP-05 verification, with the remove action button
in trailingContent. Falls back to HexEntryCard for unknown pubkeys.

https://claude.ai/code/session_018x2PcJX6VGyJmVuphkbK54
2026-03-27 13:40:29 +00:00
Claude
c0a8da0e0c feat: auto-load OpenGraph data when adding web bookmarks
When creating a new web bookmark, automatically fetches OpenGraph
metadata (title, description) after the user leaves the URL field.
Only populates fields that are still empty, preserving any user input.

https://claude.ai/code/session_01RteQotL6WHqJtDr38fUHan
2026-03-27 13:28:41 +00:00
Vitor Pamplona
51c68bfaf1 use Users instead of Pubkeys as name of the tab 2026-03-27 09:22:20 -04:00
Vitor Pamplona
511fcb4c2a Fixes Web Bookmarks floating action button to circle 2026-03-27 08:37:21 -04:00
Vitor Pamplona
09e47d4174 Merge pull request #1958 from vitorpamplona/claude/nip62-relay-deletion-EN01M
Add NIP-62 Request to Vanish feature for data deletion
2026-03-27 08:29:51 -04:00
Vitor Pamplona
33b91050e1 Merge branch 'main' into claude/nip62-relay-deletion-EN01M 2026-03-27 08:29:43 -04:00
Vitor Pamplona
35e1c0b9c0 Merge pull request #1957 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 08:27:25 -04:00
Crowdin Bot
74b943336c New Crowdin translations by GitHub Action 2026-03-27 12:22:02 +00:00
Vitor Pamplona
d121d73bb7 Merge pull request #1954 from vitorpamplona/claude/nip86-relay-management-KuTru
Add NIP-86 relay management UI and client implementation
2026-03-27 08:20:33 -04:00
Vitor Pamplona
cbb8a345e2 Add NavigateIfIntentRequested to AppNavigation 2026-03-27 08:18:16 -04:00
Vitor Pamplona
143d8c30aa Merge branch 'main' into claude/nip86-relay-management-KuTru 2026-03-27 08:17:23 -04:00
Vitor Pamplona
515659791b Merge pull request #1955 from vitorpamplona/claude/add-negentropy-support-PcTpR
Add NIP-77 Negentropy set reconciliation protocol support
2026-03-27 08:15:31 -04:00
Vitor Pamplona
009b5655f9 Merge branch 'main' into claude/add-negentropy-support-PcTpR 2026-03-27 08:15:24 -04:00
Vitor Pamplona
bc7a7effb8 Merge pull request #1956 from vitorpamplona/claude/add-pinned-notes-njyQF
Add pinned notes feature with NIP-10001 support
2026-03-27 08:11:50 -04:00
Claude
c66d63dbb1 feat: add pinned notes support (NIP-51 kind 10001)
Implement pinned notes using NIP-51 kind 10001 event. This allows users
to pin notes to their profile and view pinned notes on other profiles.

- Rewrite PinListEvent from non-standard kind 33888 to NIP-51 kind 10001
  using e tags and BaseReplaceableEvent
- Add PinListState for reactive pin state management
- Add pin/unpin actions to Account, AccountViewModel, and 3-dot menu
- Add Pinned Notes tab to user profile page
- Add Pinned Notes tab to My Bookmarks screen
- Subscribe to kind 10001 in account metadata and profile relay filters

https://claude.ai/code/session_012GQzb2qcGfAcizC6jqZArD
2026-03-27 03:00:49 +00:00
Claude
be297b89b3 feat: Add NIP-75 Zap Goal rendering and creation screen
- Add GoalEvent renderer (Goal.kt) with progress bar showing zap
  funding status, goal image, summary, and closed status
- Add NoteCompose integration so GoalEvents render properly in feeds
- Add NewGoalScreen with form for description, amount, summary,
  image URL, website URL, and optional deadline
- Add NewGoalViewModel handling event creation via GoalEvent.build()
- Add Route.NewGoal and navigation registration
- Add string resources for goal UI

https://claude.ai/code/session_012dS1srrK9LjA4vEZ8WKDEk
2026-03-27 00:16:18 +00:00
Claude
2ed3de8d80 feat: add Vanish History screen with relay compliance testing
Add a new screen that fetches and displays all existing NIP-62 Request
to Vanish events from connected relays. Each entry shows the target
relays and event date. Per-relay "Test" buttons query the relay for
events older than the vanish date to check NIP-62 compliance - if
events are found, the relay is flagged as non-compliant.

https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
2026-03-26 23:58:10 +00:00
Vitor Pamplona
12a51f6e6d Merge pull request #1953 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 19:45:10 -04:00
Crowdin Bot
dfd959be4e New Crowdin translations by GitHub Action 2026-03-26 23:11:00 +00:00
Vitor Pamplona
75a8e461ad 20x Faster Rfc3986Normalizer and way less objects being created. 2026-03-26 19:04:18 -04:00
Vitor Pamplona
7c8d44559e Benchmark for the new Rfc3986 Normalizer 2026-03-26 17:32:07 -04:00
Vitor Pamplona
395b16050b Try catch to avoid crashes 2026-03-26 17:31:32 -04:00
Vitor Pamplona
4cb5b63631 Speeding up DrawerContent 2026-03-26 15:56:01 -04:00
Vitor Pamplona
e92d5c8a5c Simplifies navigation row 2026-03-26 15:48:47 -04:00
Vitor Pamplona
9cbcb3b39d Speedup rendering of the left bar by avoiding a textfield 2026-03-26 15:37:06 -04:00
Vitor Pamplona
85ebb7918b AsyncImage is faster than Image for resources 2026-03-26 15:22:03 -04:00
Vitor Pamplona
5d256bab2c stable subs 2026-03-26 15:21:08 -04:00
Vitor Pamplona
9171209d44 Kickstart LRUCache to void constructor blocking in main thread 2026-03-26 15:20:49 -04:00
Vitor Pamplona
3ecf761d68 Removes logs from animation 2026-03-26 14:37:43 -04:00
Vitor Pamplona
39e89834ba Moves gesture enable to inside the composable.
Creates a function just for navhost
2026-03-26 14:37:16 -04:00
Vitor Pamplona
bba4d28e4b stable toast manager 2026-03-26 14:35:31 -04:00
Vitor Pamplona
d0a3679778 Minimizes the size of the banner 2026-03-26 14:35:18 -04:00
Vitor Pamplona
423bc04dba Fixes logging 2026-03-26 13:54:46 -04:00
Vitor Pamplona
9f7672249b Otherwise, it is created by the main thread when the AccountViewModel Factory is run 2026-03-26 13:54:33 -04:00
Vitor Pamplona
79d66cca8f Merge pull request #1952 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 11:41:15 -04:00
Crowdin Bot
7cb9cc6cd5 New Crowdin translations by GitHub Action 2026-03-26 15:37:20 +00:00
Vitor Pamplona
7d6f1a539f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
  fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
  refactor: remove Namecoin label from search results
  feat: show Namecoin label on .bit search results without ViewModel state
  refactor: remove Namecoin label from search results per review
  refactor: simplify .bit search resolution per review feedback
  feat: resolve bare .bit domains and show Namecoin resolution status in UI
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  refactor: rename trustAllCerts → usePinnedTrustStore
  fix: harden TLS for GrapheneOS and security-conscious Android ROMs
  doc: verify .onion ElectrumX cert via Tor, document pinning
  fix: bypass cert pinning for .onion ElectrumX servers
  feat: TOFU cert pinning for custom ElectrumX servers
  feat: add Test Connection diagnostics to Namecoin settings
  fix: pin ElectrumX server certs instead of trust-all TrustManager

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
2026-03-26 11:34:54 -04:00
Vitor Pamplona
155e57ab11 Defers most module creations to when they are needed via lambdas 2026-03-26 11:11:44 -04:00
Vitor Pamplona
7952f558eb Parallelizes the JSON parsing from stored account. 2026-03-26 11:11:21 -04:00
Vitor Pamplona
958a460ea9 Adds a try/catch to make sure this procedure unsubscribes from the req 2026-03-26 11:10:45 -04:00
Vitor Pamplona
3776f65179 Defers the creation of the geohashStateFlow until needed 2026-03-26 11:08:27 -04:00
Vitor Pamplona
44c39b01ea Defers NIP05 Resolver/NamecoinResolver Builder until needed 2026-03-26 11:07:56 -04:00
Vitor Pamplona
d73dd662c6 Reduces calculations and state creations in the NavHost builder 2026-03-26 11:03:07 -04:00
Vitor Pamplona
4ef1ba1ac6 Fixes the bug of not sending NIP-17 reactions in the complete UI mode 2026-03-26 09:25:09 -04:00
Vitor Pamplona
276dc6589c Merge pull request #1937 from mstrofnone/fix/samsung-oneui7-namecoin-tls
fix: pin ElectrumX server certs for Samsung One UI 7 (Android 16) compatibility
2026-03-26 09:14:30 -04:00
Vitor Pamplona
f9c381a113 Merge pull request #1951 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 08:38:12 -04:00
Crowdin Bot
b77e6c1b6c New Crowdin translations by GitHub Action 2026-03-26 10:57:57 +00:00
davotoula
17eea1dca9 update translations: CZ, DE, PT, SE 2026-03-26 11:53:59 +01:00
M
7293b0985b fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
The local private WatchAndDisplayNip05Row uses NonClickableObserveAndDisplayNIP05
(plain text domain), while the shared one in ShowQRScreen uses the clickable
ObserveAndDisplayNIP05. They are not the same function — restore the local copy
to preserve the correct non-clickable behavior in user suggestions.
2026-03-26 17:36:53 +11:00
nrobi144
9c9c7d3f24 feat(cache): seed NotificationsScreen from cache on compose
- Add localCache param to NotificationsScreen
- Seed reactions and zaps from cache on initial compose
- Notifications now persist across tab navigation
- All screens now have cache seeding (Feed, Thread, Profile,
  Bookmarks, Reads, Notifications)
2026-03-26 07:20:22 +02:00
nrobi144
072e437277 feat(cache): wire State classes on DesktopIAccount for GC retention
- Add BookmarkListState, Kind3FollowListState, Nip65RelayListState
  to DesktopIAccount — pins important AddressableNotes via strong refs
- Create stub Kind3FollowListRepository + Nip65RelayListRepository
  (no persistence yet, null backups)
- Update followingKeySet() to read from Kind3FollowListState.flow
- Revert accidental long-form branch changes in DeckColumnContainer
2026-03-26 07:17:39 +02:00
nrobi144
2b95cc013a feat(cache): extract Kind3FollowListState + Nip65RelayListState to commons
- Add Kind3FollowListState to commons/commonMain with ICacheProvider
  + Kind3FollowListRepository interface for settings needs
- Add Nip65RelayListState to commons/commonMain with ICacheProvider
  + Nip65RelayListRepository interface with default relay sets
- Per-feature repository interfaces match existing EphemeralChatRepository
  / PublicChatListRepository pattern in commons
- Android originals unchanged — Desktop will use commons versions
- Static LocalCache.justConsumeMyOwnEvent calls replaced with cache param
2026-03-26 07:14:16 +02:00
nrobi144
7151d3052a feat(cache): extract BookmarkListState to commons/commonMain
- Move BookmarkListState from amethyst/ to commons/commonMain/
- Change cache param from LocalCache to ICacheProvider
- Android file becomes typealias to commons version
- No settings or decryptionCache dependencies (simplest State class)
- Pins bookmarkList AddressableNote via strong ref for GC retention
2026-03-26 07:14:16 +02:00
nrobi144
cf6a74b162 feat(cache): add periodic memory cleanup for Desktop
- Add cleanMemory() to DesktopLocalCache (sweeps stale WeakRef entries)
- Add startCleanupLoop() to Coordinator with heap monitoring
  (MemoryMXBean, checks every 30s, cleans at >75% heap or every 5min)
- cleanObservers() clears unused NoteFlowSets on notes + addressables
- Try-catch per operation so one failure doesn't skip the rest
- 2-minute startup grace period before first cleanup
- Cleanup job cancelled on clear() (account switch / logout)
2026-03-26 07:14:15 +02:00
nrobi144
3dbbc039b3 feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
- DesktopLocalCache now uses LargeSoftCache (WeakReference-based, GC-driven)
  instead of BoundedLargeCache (strong refs, 50k cap, arbitrary eviction)
- Delete BoundedLargeCache.kt — no longer needed
- Rewrite findUsersStartingWith to use forEach instead of values()
- Remove BoundedLargeCache eviction tests (no longer applicable)
- Notes now only disappear when nothing references them, not arbitrarily

Per Vitor's feedback: "this maximum size approach might not work well,
as things will just disappear"
2026-03-26 07:13:27 +02:00
nrobi144
00b06a0e2a refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
- Move LargeSoftCache from amethyst/model to commons/jvmAndroid/model/cache
  so both Android and Desktop share the same WeakReference cache implementation
- Change ICacheProvider return types from Any? to proper types (User?, Note?)
  since these model classes already live in commons
- Remove unnecessary casts in ThreadAssembler, ChatNewMessageState, SearchBarState
- Improve LargeSoftCache.cleanUp() to use single-pass iterator (zero allocation)
- Update Android imports in LocalCache, LargeSoftCacheAddressExt, and test

Per Vitor's feedback on PR #1905: BoundedLargeCache evicts arbitrarily.
LargeSoftCache uses WeakReferences so GC respects the reference graph.
2026-03-26 07:13:27 +02:00
nrobi144
3b489fb3cb feat(cache): seed Reads and Bookmarks screens from cache on compose
Both screens now show cached data instantly on navigation instead of
showing loading states while re-fetching from relays.

- BookmarksScreen: reads cached BookmarkListEvent from addressableNotes,
  seeds public bookmark events from notes cache
- ReadsScreen: seeds long-form notes (kind 30023) from notes cache

Relay subscriptions still run to fetch fresh data in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
f27c8811d5 fix(cache): don't reset followersCount to 0 on subscription restart
Cached value now stays visible until relay data exceeds it, preventing
the count jumping from cached→0→ticking back up on each navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
f2169e0236 feat(cache): cache profile metadata and follower/following counts
Profile screen now reads initial values from cache so navigating back
shows data instantly instead of re-fetching everything from scratch.

- Seed displayName/about/picture from User.metadataOrNull() on compose
- Add followerCounts/followingCounts maps to DesktopLocalCache
- Write counts on each update, read on profile open
- Follower count still ticks up live (good UX) but starts from cached value
- 4 new tests for profile count caching + metadata cache reads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
3e41bab5d5 fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions
Root cause of "0 relays, 0 notes, 0 follows": all screens used
connectedRelays as the relay set for subscriptions, but NostrClient
uses relay-on-demand — relays only connect when openReqSubscription
is called with their URL. This created a deadlock: screens waited for
connected relays, but relays only connect when screens subscribe.

Fix: use relayStatuses.keys (registered/available relays) which are
populated by addDefaultRelays() at startup. openReqSubscription
triggers connection on-demand.

Affected screens: FeedScreen, UserProfileScreen, ThreadScreen,
NotificationsScreen, BookmarksScreen, ReadsScreen, NewDmDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
1e1bc1b172 fix(cache): add missing relay subscriptions for FeedScreen and UserProfileScreen
FeedScreen and UserProfileScreen used DesktopFeedViewModel (cache-backed)
but never subscribed to relays for kind 1/kind 3 events, causing 0 notes,
0 followed, and 0 relays on the home feed.

Adds rememberSubscription calls for:
- Contact list (kind 3) in FeedScreen → populates followedUsers
- Global/following feed (kind 1) in FeedScreen → populates cache
- Profile notes (kind 1) in UserProfileScreen → populates cache

Also adds 33 integration tests covering the full cache pipeline:
- DesktopCachePipelineTest: cache → filter → ViewModel (25 tests)
- CoordinatorPipelineTest: coordinator → cache → ViewModel (8 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
0621e8f7c2 refactor(cache): P3 simplifications — remove dead code, reduce abstractions
- Replace SubscriptionHealth map + data class with single lastEventAt
  Long? timestamp (only consumer was RelayHealthIndicator taking max())
- Replace consumer HashMap registry with when block (safe casts,
  compiler-checked, 9 kinds doesn't benefit from O(1) lookup)
- Remove dead loadReactionsForNotes() (superseded by requestInteractions)
- Remove unused BoundedLargeCache methods: containsKey, isEmpty,
  mapNotNull, forEach (zero external callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
4f5665a2bd fix(cache): address review findings — subscription churn, O(n) size, atomicity
P1 fixes from compound engineering review:
- Fix subscription churn flooding relays: DisposableEffect keyed on
  feedMode/noteId (stable) instead of feedState (changes every 250ms)
- BoundedLargeCache: AtomicInteger counter replaces O(n)
  ConcurrentSkipListMap.size() — eliminates 50K traversal per event
- Non-atomic updateHealth(): use MutableStateFlow.update{} for
  atomic compare-and-set instead of read-modify-write race

P2 fixes:
- DesktopThreadFilter: LinkedHashSet for O(1) containment checks,
  down from O(R²) with MutableList
- consumeContactList: createdAt guard ensures newer events always win
- consumeEvent error logging: include event ID and relay URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
c598c6135c feat(cache): ThreadScreen → DesktopFeedViewModel + DesktopThreadFilter
Replace EventCollectionState + 7 rememberSubscription blocks + inline
count maps with DesktopFeedViewModel + DesktopThreadFilter (graph walk).
Keep root note + replies relay subscriptions to populate cache. Use
FeedNoteCard for both root and reply notes (reads counts from Note.flowSet).
Remove ~200 lines of inline state management (zapsByEvent, reactionIdsByEvent,
replyIdsByEvent, repostIdsByEvent, bookmarkList, bookmark subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
8c1e54e147 feat(ui): relay health indicator in NavigationRail
Add RelayHealthIndicator component — shows elapsed time since last
relay event. Hidden when <30s (healthy), shows "45s ago" / "3m ago"
when stale. Placed above BunkerHeartbeatIndicator in NavigationRail.
Reads from Coordinator's subscriptionHealth StateFlow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
50eb03bc3e feat(cache): UserProfileScreen notes feed → DesktopFeedViewModel
Replace EventCollectionState + createUserPostsSubscription + manual cache
hydration with DesktopFeedViewModel + DesktopProfileFeedFilter. Profile
header subscriptions (metadata, contact list, followers) kept as-is since
they're not feed concerns. DisposableEffect cleanup on profile switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
c63ace1d9c feat(cache): FeedScreen → DesktopFeedViewModel + Note-based FeedNoteCard
Rewrite FeedScreen from inline EventCollectionState + 8 rememberSubscription()
calls to cache-centric DesktopFeedViewModel pattern:

- ViewModel keyed on feedMode with DisposableEffect cleanup (destroy())
- FeedState pattern matching (Loading/Loaded/Empty/Error)
- FeedNoteCard takes Note instead of Event, reads counts from Note.flowSet
- DisposableEffect for note.clearFlow() cleanup in LazyColumn
- DisposableEffect for Coordinator interaction subscription lifecycle
- LaunchedEffect for rate-limited metadata loading via Coordinator
- Extract FeedHeader into separate composable
- Add destroy() to DesktopFeedViewModel (ViewModel.clear() is internal in KMP)
- Update UserProfileScreen to look up Note from cache for FeedNoteCard

Removes ~350 lines of inline state management (EventCollectionState,
zapsByEvent, reactionIdsByEvent, replyIdsByEvent, repostIdsByEvent,
followedUsers, bookmarkList, 8 rememberSubscription blocks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
e8ede21c17 feat(cache): Coordinator error handling + interaction subscriptions
Add try-catch in consumeEvent() so one bad event doesn't kill the
pipeline. Add requestInteractions()/releaseInteractions() with Job
tracking via ConcurrentHashMap for screen-scoped subscription lifecycle.
Add SubscriptionHealth tracking (lastEventReceivedAt, eoseReceived)
exposed as StateFlow for UI consumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
684ba158ab feat(cache): kind-based consumer registry + new event types
Replace when-dispatch with HashMap<Int, Handler> for O(1) event routing.
Add consume methods for: RepostEvent (kind 6), ContactListEvent (kind 3),
LongTextNoteEvent (kind 30023), BookmarkListEvent (kind 30001).
Replace @Volatile followedUsers with MutableStateFlow for thread safety
and Compose observability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
e834dbd1e5 fix(ui): show display names instead of npubs in feed
toNoteDisplayData() now reads User.toBestDisplayName() from cache
instead of always showing npub. Falls back to npub if no metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
e80473a7d9 feat(cache): Phase 3 — route all screen events through cache
All screens now pipe relay events into DesktopLocalCache via
coordinator.consumeEvent(). Existing per-screen EventCollectionState
kept for rendering (incremental migration), but data also enters
cache for persistence across navigation.

- FeedScreen: route global + following feed events
- ThreadScreen: route root note + reply events
- UserProfileScreen: route posts + add cache hydration on mount
  (LaunchedEffect queries cache for existing posts → instant display)
- BookmarksScreen: route public + private bookmark events
- ReadsScreen: route long-form feed events (add coordinator param)
- NotificationsScreen: route notification events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
c198df57fd feat(cache): Phase 2+3 foundation — FeedFilters + DesktopFeedViewModel
Phase 2: 8 FeedFilter implementations:
- DesktopGlobalFeedFilter (kind 1, limit 2500)
- DesktopFollowingFeedFilter (kind 1 + followed pubkeys)
- DesktopThreadFilter (root + reply graph walk)
- DesktopProfileFeedFilter (notes by pubkey, limit 1000)
- DesktopBookmarkFeedFilter (notes by ID set)
- DesktopReadsFeedFilter (kind 30023, limit 500)
- DesktopNotificationFeedFilter (events tagging user)
- DesktopSearchFeedFilter (content text search)

All use AdditiveFeedFilter for incremental updates (O(batch)
after initial load). Limits are 5x Android's.

Phase 3 foundation: DesktopFeedViewModel subclass that calls
refreshSuspended() on init to load existing cache data —
fixes navigation persistence (back shows instant data).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
543f7a329c feat(cache): Phase 1 — cache-centric architecture foundation
- Add BoundedLargeCache: LargeCache wrapper with size enforcement
  (50k notes, 25k users, 10k addressable). Lock-free reads via
  ConcurrentSkipListMap, evicts 10% when cap exceeded.

- Switch DesktopLocalCache from ConcurrentHashMap to BoundedLargeCache.
  Exposes filterIntoSet, values, etc. matching Android's query patterns.

- Add consume methods for kinds 1 (TextNote), 7 (Reaction),
  9734 (ZapRequest), 9735 (Zap). Uses tagsWithoutCitations() for
  reply parsing, originalPost()+taggedAddresses() for reactions,
  zappedPost() for zaps. Follows Android LocalCache pattern.

- Add consumeEvent() bridge in coordinator with BasicBundledInsert
  (250ms batching). Routes relay onEvent callbacks to cache.

- Fix SharedFlow: extraBufferCapacity=64, DROP_OLDEST. Prevents
  blocking emitters and dropped events.

- Set -Xmx2g JVM arg for long desktop sessions.

- Clear cache on logout (coordinator first, then cache).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
61119e6916 docs: deepen cache plan — BoundedLargeCache, FeedNoteCard rewrite details
- Resolve LruCache vs LargeCache: use LargeCache (lock-free ConcurrentSkipListMap)
  with BoundedLargeCache wrapper for size enforcement on put()
- Confirm LargeCache available on desktop via quartz jvmAndroid source set
- Detail FeedNoteCard rewrite: Note model field mapping, 5 subscription removals
- Fix filter examples to use filterIntoSet (LargeCache API)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
9c160006fa docs: desktop cache architecture brainstorm and plan
Cache-centric architecture for navigation persistence, mirroring
Android Amethyst's pattern. Three-phase incremental migration:
Phase 1 - Store events in DesktopLocalCache with LRU eviction
Phase 2 - Create FeedFilter implementations
Phase 3 - Migrate screens to FeedViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:45 +02:00
Claude
2f8e62fda5 feat: implement NIP-86 Relay Management API
Add quartz protocol package for NIP-86 JSON-RPC relay management with
NIP-98 HTTP authorization, and a management UI accessible from the
relay information screen when the relay advertises NIP-86 support.

Quartz (nip86RelayManagement/):
- Nip86Method: all 21 method constants from the spec
- Nip86Request: serializable JSON-RPC request with factory methods
- Nip86Response: response model with typed result parsing helpers
- Nip86Client: builds NIP-98 auth headers, serializes requests,
  parses responses for each result type (pubkeys, events, kinds, IPs)

Amethyst:
- Nip86Retriever: OkHttp-based HTTP executor for NIP-86 calls
- RelayManagementViewModel: state management for all NIP-86 operations
- RelayManagementScreen: tabbed UI (Pubkeys, Events, Kinds, IPs,
  Settings) with add/remove dialogs, moderation queue, and relay
  settings fields
- Route.RelayManagement added to navigation
- "Manage Relay" button shown in RelayInformationScreen top bar when
  relay's supported_nips includes "86"

https://claude.ai/code/session_01QckCAm1T8pJqqmURBiNtaQ
2026-03-26 04:32:43 +00:00
Claude
70baf55e1c feat: add NIP-77 negentropy sync support
Add support for NIP-77 (Negentropy Syncing) protocol messages using
the negentropy-kmp library for efficient set reconciliation between
client and relay.

New files:
- NegOpenCmd, NegMsgCmd, NegCloseCmd: Client-to-relay commands
- NegMsgMessage, NegErrMessage: Relay-to-client messages
- NegentropySession: Client-side reconciliation orchestrator
- NegentropyServerSession: Server-side reconciliation handler
- NegentropyManager: High-level sync manager with listener callbacks

Updated serializers:
- Jackson MessageSerializer/Deserializer for NEG-MSG, NEG-ERR
- Jackson CommandSerializer/Deserializer for NEG-OPEN, NEG-MSG, NEG-CLOSE
- Kotlin MessageKSerializer/CommandKSerializer for all NIP-77 types

Compatible with strfry relay's negentropy implementation via the
negentropy-kmp library which implements the same Protocol V1 spec.

https://claude.ai/code/session_01Dc6W1G1jURAAR9kzyVvk6g
2026-03-26 04:23:32 +00:00
Claude
463938768e feat: add NIP-62 Request to Vanish screen
Add a new screen allowing users to send NIP-62 relay deletion requests.
Users can select a specific relay or ALL RELAYS, pick a date (data
created before that date will be requested for deletion), and provide
an optional reason. Includes confirmation dialog with clear warnings
about the irreversible nature of vanish requests.

https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
2026-03-26 04:18:39 +00:00
M
ebbcfa26fb refactor: remove Namecoin label from search results
Per review: .bit resolution is just NIP-05, no special labeling needed.
2026-03-26 10:25:31 +11:00
M
e7cde18371 feat: show Namecoin label on .bit search results without ViewModel state
Derive the label from the existing searchTerm — if it ends with .bit,
show a chain-emoji label above each user result. No new StateFlow or
ViewModel state management needed.
2026-03-26 09:49:34 +11:00
M
1ca904b415 refactor: remove Namecoin label from search results per review
Remove namecoinResolvedUser StateFlow and the chain-emoji label
from search results. .bit resolution still works through the
existing NIP-05 path — just no special UI decoration.
2026-03-26 09:37:56 +11:00
Vitor Pamplona
e43047c027 removes warnings 2026-03-25 18:33:03 -04:00
M
67fd48c5b8 refactor: simplify .bit search resolution per review feedback
Address @vitorpamplona's review comments on the last commit:

1. Remove NamecoinResolutionState sealed class and status banners
   — the existing NIP-05 resolver already handles .bit, no need for
   separate Namecoin-specific UI state (spinner, chain emoji, error)

2. Remove separate toNip05IdOrNull() resolution path
   — widen the existing `if (term.contains('@'))` gate in both
   SearchBarViewModel and UserSuggestionState to also accept bare
   .bit domains (synthesize _@domain.bit → existing NIP-05 flow)

3. Remove d/ and id/ identifier support from search
   — per feedback, only NIP-05 style resolution (.bit) in search

4. Remove special Namecoin result rendering in ImportFollowList
   — just show UserLine for all results, no separation needed

5. Keep a lightweight 'Namecoin' label on resolved users in search
   — namecoinResolvedUser StateFlow tracks the .bit-resolved user,
   shown as a small label above the UserCompose row

Net: -405 lines, +50 lines across 6 files.
2026-03-26 08:43:39 +11:00
M
0d83af8912 feat: resolve bare .bit domains and show Namecoin resolution status in UI
Widen the existing NIP-05 resolution gate in UserSuggestionState and
SearchBarViewModel to also accept bare .bit domains and d//id/ Namecoin
identifiers. A synthesised Nip05Id is passed to the existing
nip05Client.get() path — which already routes .bit to the
NamecoinNameResolver — so no separate resolution logic is needed.

Add NamecoinResolutionState (Idle/Resolving/Resolved/Error) as a
StateFlow so the UI can show progress. ImportFollowListSelectUserScreen
and SearchScreen display an animated status banner (spinner while
resolving, chain emoji on success, warning on error) and label
Namecoin-resolved profiles in the results list.
2026-03-26 07:40:25 +11:00
Vitor Pamplona
46b75abc81 Merge pull request #1950 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 16:20:00 -04:00
Crowdin Bot
618edf9dd5 New Crowdin translations by GitHub Action 2026-03-25 20:17:28 +00:00
Vitor Pamplona
0de01ca295 Merge pull request #1949 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 16:16:54 -04:00
Vitor Pamplona
2e9beacb13 no message 2026-03-25 16:14:20 -04:00
Crowdin Bot
9db9283952 New Crowdin translations by GitHub Action 2026-03-25 20:07:46 +00:00
Vitor Pamplona
452d2accc9 Merge pull request #1948 from vitorpamplona/claude/add-web-bookmarks-qYwpU
feat: implement NIP-B0 Web Bookmarking (kind 39701)
2026-03-25 16:06:09 -04:00
Vitor Pamplona
bf8791088c Trying to resolve the build issue between spotless and vlcSetup without running vlcSetup when applying spotless from Claude. 2026-03-25 16:05:29 -04:00
Vitor Pamplona
15ccdf0c73 Merge pull request #1938 from vitorpamplona/claude/organize-favorite-relay-feeds-DL3o2
feat: separate favorite relay feeds into own category in nav popup
2026-03-25 15:44:59 -04:00
Vitor Pamplona
c903996cc2 Merge pull request #1939 from vitorpamplona/claude/nip89-client-tag-LZYhd
feat: NIP-89 client tag on all signed events
2026-03-25 15:42:31 -04:00
Vitor Pamplona
052fa22d59 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-25 15:38:16 -04:00
Vitor Pamplona
e90945d201 Removing the solution to a warning about spotless dependencies because claude can't run the vlc step 2026-03-25 15:34:32 -04:00
M
2e02edc333 refactor: rename trustAllCerts → usePinnedTrustStore
The field no longer means 'trust all certificates' — since the Samsung
One UI 7 fix, it means 'use the pinned trust store (hardcoded +
TOFU-pinned certs + system CAs) instead of system-only trust.'

The old name was actively misleading and could cause a future
contributor to interpret the flag literally, potentially reintroducing
the trust-all pattern that Samsung Knox and GrapheneOS reject.

Renamed across all 4 files:
- ElectrumxServer.kt: field definition + updated KDoc
- ElectrumXClient.kt: createSocket() usage + comments
- NamecoinSettings.kt: parseServerString() usage + comments
- NamecoinSettingsTest.kt: test assertions
2026-03-26 06:33:43 +11:00
Claude
cbee188ca5 feat: implement NIP-B0 Web Bookmarking (kind 39701)
Add support for NIP-B0 web bookmarks with a complete protocol
implementation in Quartz and a full UI in Amethyst for adding,
editing, browsing, and deleting web bookmarks.

Quartz:
- WebBookmarkEvent (kind 39701) as an addressable event
- Tag builder/parser extensions for title, published_at, hashtags
- Registered in EventFactory

Amethyst:
- WebBookmarksScreen with FAB to add, and per-card edit/delete/open
- WebBookmarkEditDialog for add/edit with URL, title, description, tags
- WebBookmarkFeedFilter querying addressable notes by kind + pubkey
- Account.sendWebBookmark() and Account.deleteWebBookmark()
- LocalCache.consume() for WebBookmarkEvent
- Drawer navigation entry with Language icon
- Route.WebBookmarks and navigation registration

https://claude.ai/code/session_01UzfLJttwuJzovtb8HX5F9n
2026-03-25 19:29:55 +00:00
M
7f451ee7c7 fix: harden TLS for GrapheneOS and security-conscious Android ROMs
Address compatibility and security issues identified by reviewing
GrapheneOS source (hardened Conscrypt, strict TLS enforcement):

1. .onion: prefer pinned factory, fall back to trust-all

   GrapheneOS patches Conscrypt with stricter TLS enforcement that may
   reject no-op X509TrustManagers even for proxied sockets. The .onion
   server's cert is already in PINNED_ELECTRUMX_CERTS (same operator as
   electrumx.testls.space), so we now use cachedPinnedSslFactory() as
   the primary path. onionSslFactory() (trust-all) is kept as a fallback
   on SSLHandshakeException only — this handles cert rotation or unknown
   .onion servers gracefully.

2. TOFU: require explicit user confirmation before pinning

   Previously, Test Connection auto-pinned every cert on success. An
   attacker performing MITM during first test would get their cert
   permanently trusted. Now each new cert triggers an AlertDialog showing
   the full SHA-256 fingerprint. Users must explicitly accept ('Trust')
   or reject each cert — aligning with GrapheneOS's philosophy of
   explicit trust decisions.

3. Clarify trustAllCerts semantics

   The field name is misleading post-refactor: it now means 'use pinned
   trust store' not 'trust all certificates'. Added TODO to rename to
   usePinnedTrustStore and updated inline comments to prevent future
   misinterpretation.

Note: hostname verification on raw SSLSocket is a pre-existing gap
(not introduced by the Samsung fix PR) — SSLSocket.createSocket() uses
the host parameter for SNI only, not hostname verification. A follow-up
should add endpointIdentificationAlgorithm='HTTPS' or fingerprint-based
verification for pinned certs.
2026-03-26 06:26:18 +11:00
Vitor Pamplona
138428a233 Merge pull request #1947 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 15:24:15 -04:00
Crowdin Bot
fd86e3c045 New Crowdin translations by GitHub Action 2026-03-25 19:23:05 +00:00
Vitor Pamplona
283fb96bb7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  reuse hex methods.
  spotless
  delete voice files on failuer
  added progress and test plan to doc
  fix: delete abandoned compressed video when larger than original fix: eagerly delete intermediate temp files in upload pipeline fix: delete temp file from MediaCompressorFileUtils after image compression refactor: simplify temp file cleanup logic - Remove TOCTOU anti-pattern (file.exists() before file.delete()) - Consolidate double deleteTempUri calls into single conditional - Remove restating comments
  analysis for temporary files
  fix: clean up temp files after upload completes
  perf: optimize ChaCha20 and XChaCha20-Poly1305 for speed
  style: apply spotless formatting to ChaCha20/Poly1305 sources
  feat: replace libsodium with pure Kotlin ChaCha20/Poly1305 implementation
2026-03-25 15:20:28 -04:00
Vitor Pamplona
6db28dca0b Merge pull request #1908 from vitorpamplona/claude/remove-libsodium-dependency-DjtWa
Replace libsodium with pure Kotlin XChaCha20-Poly1305 implementation
2026-03-25 13:53:48 -04:00
Vitor Pamplona
2474ba1713 Fixes anon accounts becoming empty bubbles in the notifiy section of new posts. 2026-03-25 13:53:06 -04:00
Vitor Pamplona
fc560d2752 reuse hex methods. 2026-03-25 13:22:02 -04:00
Vitor Pamplona
2ead6645c0 Merge pull request #1925 from vitorpamplona/claude/review-cache-cleanup-Yr2P6
Add cleanup of temporary files in upload pipeline
2026-03-25 13:20:52 -04:00
Vitor Pamplona
138bb144ee Merge branch 'main' into claude/review-cache-cleanup-Yr2P6 2026-03-25 13:20:30 -04:00
Vitor Pamplona
e2ac534be0 spotless 2026-03-25 12:55:31 -04:00
davotoula
aa5dad9fac delete voice files on failuer 2026-03-25 17:42:05 +01:00
Vitor Pamplona
0d92c86459 Merge branch 'main' into claude/remove-libsodium-dependency-DjtWa 2026-03-25 11:50:07 -04:00
Vitor Pamplona
9e9f6c3164 Completing the transition from NIP-44 architecture specific tests to commontTest 2026-03-25 11:48:37 -04:00
Vitor Pamplona
d84d146981 Merge branch 'main' into claude/remove-libsodium-dependency-DjtWa 2026-03-25 11:24:55 -04:00
Vitor Pamplona
617db3613f Re-enables benchmark with native android ChaCha functions 2026-03-25 11:17:17 -04:00
Vitor Pamplona
60a44d25eb Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  feat: implement NIP-45 HyperLogLog for probabilistic event counting
2026-03-25 10:55:12 -04:00
Vitor Pamplona
2ffb920f75 Fixes NIP-46 decoding test 2026-03-25 10:51:31 -04:00
Vitor Pamplona
f8baf83146 Merge pull request #1941 from vitorpamplona/claude/implement-hyperloglog-nip45-Jai8I
feat: implement NIP-45 HyperLogLog support
2026-03-25 09:34:35 -04:00
Vitor Pamplona
08090f57b5 Merge pull request #1945 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 09:34:18 -04:00
Crowdin Bot
19ef2ca041 New Crowdin translations by GitHub Action 2026-03-25 13:30:02 +00:00
Vitor Pamplona
3a21396838 Merge pull request #1936 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 09:28:32 -04:00
Crowdin Bot
0705d070bd New Crowdin translations by GitHub Action 2026-03-25 12:40:37 +00:00
Vitor Pamplona
d6541e37c3 merge 2026-03-25 08:37:18 -04:00
Vitor Pamplona
7583f7f8e1 Moves some of the nip-44 based tests to commonTest 2026-03-25 08:33:04 -04:00
Vitor Pamplona
c654d2267c Protects against crashes when the signer sends an unverifiable payload back to Amethyst 2026-03-25 08:28:31 -04:00
Vitor Pamplona
0a0b6aefa0 Merge pull request #1943 from dadofsambonzuki/attestation-fix
Align attestation events with latest NIP format
2026-03-25 07:49:07 -04:00
Vitor Pamplona
a1ad460b90 Merge pull request #1944 from KotlinGeekDev/kmp-completeness
Kmp completeness: update feature parity table.
2026-03-25 07:47:38 -04:00
Vitor Pamplona
9f6684b044 Merge pull request #1942 from nrobi144/features/long-form-content
feat(desktop): highlight UX, shared stores, profile tabs, editor toolbar
2026-03-25 07:47:21 -04:00
KotlinGeekDev
ac60cbeb02 Update Quartz feature parity table. 2026-03-25 12:42:41 +01:00
Nathan Day
92ae288720 Align attestation tags with latest NIP format 2026-03-25 11:30:29 +00:00
Nathan Day
0b049c656c Merge branch 'vitorpamplona:main' into main 2026-03-25 11:01:43 +00:00
davotoula
42db03790a added progress and test plan to doc 2026-03-25 10:23:38 +01:00
davotoula
60b7c6bd0d fix: delete abandoned compressed video when larger than original
fix: eagerly delete intermediate temp files in upload pipeline
fix: delete temp file from MediaCompressorFileUtils after image compression
refactor: simplify temp file cleanup logic
- Remove TOCTOU anti-pattern (file.exists() before file.delete())
- Consolidate double deleteTempUri calls into single conditional
- Remove restating comments
2026-03-25 10:19:16 +01:00
davotoula
390edf311d analysis for temporary files 2026-03-25 09:29:48 +01:00
davotoula
26fb6fead4 Merge branch 'main-upstream' into claude/review-cache-cleanup-Yr2P6 2026-03-25 09:18:53 +01:00
davotoula
7871a7672b Merge branch 'main' into chess-enhancements-and-bug-fixes 2026-03-25 09:10:46 +01:00
nrobi144
e037254b39 fix(highlights): shared store, context menu, link rendering, publish, profile tabs
- Share DesktopHighlightStore and DesktopDraftStore at app level instead
  of per-column to fix cross-deck reactivity and draft persistence
- Replace broken clipboard-based highlight creation with
  LocalContextMenuRepresentation that piggybacks on Copy to get selected text
- Render highlights as highlight:// links instead of bold/italic markers
- Add collapsible highlights panel in article reader with edit/delete/publish
- Publish highlights to relays as NIP-84 events via HighlightPublishAction
- Add Reads tab (kind 30023) and Highlights tab (kind 9802) to profile
- Rewrite MarkdownToolbar with MarkdownEditorState for selection-aware
  toggle, Material icons, and keyboard shortcuts (Cmd+B/I/E/K)
- Wire DraftsScreen "New Draft" button to ArticleEditorScreen navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 06:59:41 +02:00
Claude
403351610b feat: implement NIP-45 HyperLogLog for probabilistic event counting
Add client-side HyperLogLog support as specified by NIP-45:
- HyperLogLog object with merge, estimate, encode/decode, and
  computeOffset for deterministic filter-based offset calculation
- Add hll ByteArray field to CountResult for relay HLL data
- Update CountResultKSerializer to serialize/deserialize hll hex field
- Add queryCountMergedHll extension for multi-relay HLL merging
- 20 unit tests covering HLL operations and serialization round-trips

https://claude.ai/code/session_01CQzygEkwckLCiXzSRPMpBa
2026-03-25 03:43:47 +00:00
Claude
36af9e2346 feat: add NIP-89 client tag to all signed events via NostrSignerWithClientTag decorator
Introduces a signer decorator that automatically appends the NIP-89
client tag to every event before signing, ensuring all events carry
client identification without modifying individual event creation sites.
Works transparently with internal, external (NIP-55), and remote (NIP-46) signers.

https://claude.ai/code/session_01WRtuk1ySCfqXzfFrieKsDs
2026-03-25 03:24:49 +00:00
Claude
c3c9bd016e feat: separate favorite relay feeds into their own category in nav popup
Relay feeds were previously grouped under the generic "Feeds" category
in the top nav bar filter dialog. This adds a dedicated "Relays" group
so users can more easily find their favorite relay feeds.

https://claude.ai/code/session_016gjbPgYRXrQCoY1zKPz5PX
2026-03-25 02:59:40 +00:00
M
a4953f628d doc: verify .onion ElectrumX cert via Tor, document pinning
Connected to the .onion hidden service via Tor SOCKS proxy and
confirmed it serves the SAME certificate as electrumx.testls.space:
  SHA-256: 53:65:D5:BB:26:19:F5:40:1C:D8:8E:FC:AF:FB:A5:B2:...

The .onion cert is already covered by the first pinned cert entry.
Added comments documenting this relationship and instructions for
fetching .onion certs via Tor for future updates.

The .onion still uses onionSslFactory() (trust-all) as the primary
path — this is correct since Tor provides its own authentication.
The pinned cert serves as defense-in-depth.
2026-03-25 12:13:49 +11:00
M
3afe0c9978 fix: bypass cert pinning for .onion ElectrumX servers
Tor hidden services are authenticated by their onion address (the
public key hash), making TLS certificate verification redundant.

The .onion server in TOR_ELECTRUMX_SERVERS was going through
pinnedSslFactory() but its cert isn't in the pinned list (and can't
be fetched without Tor). This would cause .onion connections to fail
with SSLHandshakeException when Tor mode is active.

Fix: .onion addresses now use a dedicated onionSslFactory() with
trust-all, which is safe because:
1. Tor provides end-to-end encryption
2. The onion address IS the server identity proof
3. Samsung Knox trust-all rejection doesn't apply to proxied sockets
2026-03-25 12:10:45 +11:00
M
b8b51db0b5 feat: TOFU cert pinning for custom ElectrumX servers
When a user adds a custom ElectrumX server and runs Test Connection,
the server's TLS certificate is automatically captured and pinned
(Trust On First Use). This allows custom servers with self-signed
certificates to work on Samsung/Xiaomi/OnePlus devices.

Changes:
- ElectrumXClient: addPinnedCert(), setDynamicCerts() for runtime
  cert management; testServer() now captures server cert PEM and
  SHA-256 fingerprint from SSL session
- NamecoinSharedPreferences: persist pinned certs to DataStore
- AppModules: load pinned certs on startup, sync to ElectrumXClient
- NamecoinSettings: custom servers always set trustAllCerts=true
  (ElectrumX servers almost universally use self-signed certs)
- UI: shows cert fingerprint in test results, auto-pins on success
- ServerTestResult: new serverCertPem + certFingerprint fields

Flow: Add server → Test Connection → cert auto-captured and pinned →
future connections trust that cert even on Samsung Knox devices.
2026-03-25 12:05:05 +11:00
Vitor Pamplona
f42881a48e v1.06.3 2026-03-24 20:55:04 -04:00
Vitor Pamplona
6fc72f47bb Highlights when arriving at the reply message 2026-03-24 20:53:22 -04:00
M
49698de99c feat: add Test Connection diagnostics to Namecoin settings
Add per-server connection testing with detailed error reporting:
- Test Connection button tests each ElectrumX server individually
- Shows streaming results:  success with response time,  failure
  with human-readable error (TLS handshake failed, connection refused,
  timeout, DNS failed, invalid response, etc.)
- Diagnostic card shows: last test timestamp + success count, device
  info (manufacturer/model/Android/API), TLS version negotiated
- ElectrumXClient.testServer() method for single-server diagnostics
  with TLS version capture from SSL session

This gives users (and bug reporters) immediate visibility into why
Namecoin resolution may be failing silently on their device.
2026-03-25 11:52:05 +11:00
Vitor Pamplona
041c788e21 Improvements to anonymous replies 2026-03-24 20:42:06 -04:00
M
15a2d77a0f fix: pin ElectrumX server certs instead of trust-all TrustManager
Samsung One UI 7 (Android 16) silently rejects TLS connections that use
a no-op X509TrustManager which accepts all certificates. This breaks
Namecoin resolution on all Samsung devices running One UI 7, including
Galaxy A15 (SM-A156E) and Galaxy S24 Ultra (SM-S938B).

Replace trustAllSslFactory() with pinnedSslFactory() that:
- Pins the actual self-signed certificates of known ElectrumX servers
- Also includes system CA certificates for servers with real certs
- Uses a proper TrustManagerFactory chain that Samsung Knox accepts

Additional OEM compatibility hardening:
- Cache the SSLSocketFactory (avoid expensive rebuild per connection)
- Request TLSv1.2 explicitly (Xiaomi MIUI/HyperOS and OnePlus ColorOS
  Conscrypt forks may default to TLS 1.0 for raw socket upgrades)
- Enforce TLSv1.2+ enabled protocols on the SSLSocket
- KeyStore fallback to PKCS12 if default type fails (Xiaomi)
- Defensive try/catch on system CA cert re-insertion (some OEMs return
  certs that cannot be added to a new KeyStore)

Tested on Android 16 (API 36) emulator — all ElectrumX servers connect
(hostname, IP-address, factory reuse) and .bit resolution works E2E.

Pinned certs:
- electrumx.testls.space:50002 (expires 2027-05-04)
- nmc2.bitcoins.sk:57002 (expires 2030-10-22)
2026-03-25 11:41:12 +11:00
Vitor Pamplona
1f2e24bc1f v1.06.2 2026-03-24 19:36:30 -04:00
Vitor Pamplona
cfd6577029 no message 2026-03-24 19:36:19 -04:00
Vitor Pamplona
976de28ece Fixes routes for AppDefinition events. 2026-03-24 19:25:01 -04:00
Vitor Pamplona
bc9b6dc912 fixes client tags 2026-03-24 19:24:33 -04:00
Vitor Pamplona
c6fd65dbac Merge pull request #1935 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-24 18:35:47 -04:00
Crowdin Bot
b3f190410f New Crowdin translations by GitHub Action 2026-03-24 22:33:27 +00:00
Vitor Pamplona
6f7c550034 Clears some inconsistencies in translations 2026-03-24 18:31:10 -04:00
Vitor Pamplona
fb976d168a Simplifies report feed in the user profile 2026-03-24 18:30:52 -04:00
Vitor Pamplona
be4a6d4fc4 Merge pull request #1932 from vitorpamplona/claude/add-anonymous-reply-dkdSJ
feat: activate anonymous reply by tapping user picture
2026-03-24 17:55:46 -04:00
Claude
d90c868f32 feat: activate anonymous reply by tapping user picture instead of bottom button
Replace the bottom bar AnonymousPostButton with a tap-on-avatar activation scheme.
Tapping the user picture enables anonymous mode and shows the incognito icon in its
place. Tapping the incognito icon toggles back to normal mode.

https://claude.ai/code/session_013MaeT2T9Bg4HaVe2ACNADy
2026-03-24 21:49:15 +00:00
Vitor Pamplona
e4f55c11f1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  perf: move cache lookups from NavHost route lambdas into screen composables
2026-03-24 17:45:46 -04:00
Vitor Pamplona
71242e50d2 Merge pull request #1933 from vitorpamplona/claude/improve-navhost-performance-AE9WZ
perf: move cache lookups from NavHost route lambdas into screen composables
2026-03-24 17:42:34 -04:00
Vitor Pamplona
5d0f9ac866 Cleaning up residue from removing the need for rfc 3986 and smpForKmp 2026-03-24 17:41:20 -04:00
Vitor Pamplona
ad9596ab92 Fixes flow value and improves tor binding lifecycle. 2026-03-24 17:32:24 -04:00
Vitor Pamplona
d847c06979 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-24 17:23:31 -04:00
Vitor Pamplona
0d8b4e8baa merge 2026-03-24 17:21:52 -04:00
Vitor Pamplona
70d034e50c Merge pull request #1930 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-24 17:20:31 -04:00
Vitor Pamplona
f11172216d Improvements to the status of attestations. Validity first, then Processing status. 2026-03-24 17:20:11 -04:00
Claude
2ef8be8fdc perf: move cache lookups from NavHost route lambdas into screen composables
Route lambdas in NavHost run during navigation composition and should be
kept lightweight. Cache lookups (getNoteIfExists, checkGetOrCreateNote,
getOrCreateAddressableNote) and object construction (RoomId,
RelayUrlNormalizer) are now deferred to the screen composables themselves,
either via remember {} or inside LaunchedEffect blocks.

Affected screens: PublicChatChannelScreen, LiveActivityChannelScreen,
EphemeralChatScreen, GeoHashPostScreen, HashtagPostScreen,
ReplyCommentPostScreen, NewProductScreen, LongFormPostScreen,
ShortNotePostScreen, NewPublicMessageScreen,
ImportFollowListPickFollowsScreen.

https://claude.ai/code/session_01NrVHL4zdCghQvqE8xmi4Gf
2026-03-24 21:18:40 +00:00
Crowdin Bot
cc850c5b7f New Crowdin translations by GitHub Action 2026-03-24 21:17:34 +00:00
Vitor Pamplona
728ff9a4cd Merge pull request #1926 from vitorpamplona/claude/reply-click-navigation-7lKuO
feat: scroll to replied message when clicking reply preview in chat
2026-03-24 17:16:16 -04:00
Vitor Pamplona
6f9fc23a64 Merge pull request #1929 from vitorpamplona/claude/relay-info-dialog-uuE8W
feat: make profile relay rows open relay info dialog
2026-03-24 17:16:01 -04:00
Claude
bcdaf8630c feat: make relay rows clickable to open relay info dialog on profile page
Adds onClick parameter to RelayCompose and navigates to RelayInformationScreen
when a relay row is tapped in the profile relay tab.

https://claude.ai/code/session_01PBFQz53xzXoFsEHD5Ghyqr
2026-03-24 20:34:01 +00:00
davotoula
86fd92e505 feat(chess): add dismiss/clear-all for finished games in lobby
Persist dismissed game IDs locally (SharedPreferences on Android,
java.util.prefs on Desktop) so completed games can be permanently
hidden from the chess lobby. Adds per-game dismiss button and
"Clear all" action when 2+ finished games exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 21:30:46 +01:00
davotoula
1f34a79676 fix: prevent double-fetch and stop re-polling finished chess games
- Mark recentlyLoadedGames timestamp eagerly in refreshGame (before
  async fetch) to prevent concurrent fetches for the same game
- Clear focused game in removeGameId when the removed game matches,
  so finished games stop being re-polled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 19:41:20 +01:00
davotoula
5bdc684f21 fix: add event-level dedup and handleGameAccepted guard in chess lobby
- Dedup incoming events by ID (bounded LRU set of 500) to prevent
  redundant processing when multiple relays deliver the same event
- Filter non-chess kind 30 events early (isStart=false, isMove=false)
- Guard handleGameAccepted with recentlyLoadedGames check to prevent
  N concurrent relay fetches when N move events arrive for same game
2026-03-24 19:29:06 +01:00
Vitor Pamplona
2d25bb2592 Sorts followers into a set to avoid LazyColumn key conflicts. 2026-03-24 14:23:21 -04:00
Vitor Pamplona
405d1d0519 Merge pull request #1928 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-24 13:42:23 -04:00
Crowdin Bot
88042f06d4 New Crowdin translations by GitHub Action 2026-03-24 17:40:05 +00:00
Vitor Pamplona
8437a819f8 Merge pull request #1909 from KotlinGeekDev/kmp-completeness
Kmp completeness, part 3 - adios, Swift bridge.
2026-03-24 13:38:44 -04:00
Vitor Pamplona
cef07a2460 Fixes single label domains with non ascii chars around them 2026-03-24 13:33:14 -04:00
Vitor Pamplona
2a2c61973e Avoids crashing the app in weird urls. 2026-03-24 13:30:36 -04:00
KotlinGeekDev
349781d9be Use uri-reference-kmp in commonMain. Remove platform-specific implementations. 2026-03-24 16:34:13 +01:00
KotlinGeekDev
334aabbf02 Merge branch 'upstream-main' into kmp-completeness 2026-03-24 16:12:14 +01:00
KotlinGeekDev
2933454fb6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main
# Conflicts:
#	quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt
2026-03-24 16:11:04 +01:00
Vitor Pamplona
09054d6ac5 Fixes pool rendering when one label is large and the other is small 2026-03-24 10:58:59 -04:00
Claude
4d8be5e272 feat: scroll to replied message when clicking reply preview in chat
When a user clicks on the reply preview (inner quote) in a chat message,
the chat feed now scrolls to the original replied-to message in the same
screen instead of doing nothing.

https://claude.ai/code/session_01UF4VxoWGvvvwtLYBLBYw9w
2026-03-24 14:51:39 +00:00
davotoula
944fbcd000 add extensive [chessdebug] logging across all chess layers
Uses Log.d("chessdebug", ...) via quartz's multiplatform Log utility
(android.util.Log on Android, println on Desktop) for proper logcat
integration.

Layers instrumented:
- [Reconstructor] state reconstruction, move replay, desync detection
- [Collector/CollectorMgr] event ingestion, dedup, routing
- [GameLoader] game loading, live state conversion
- [Lobby] challenges, moves, resign, spectating, polling refresh
- [Polling] polling cycles, focused mode
- [Broadcaster] relay connections, event send/confirm
- [LiveGame] move validation, opponent moves, head event tracking
- [AndroidVM] incoming event parsing from subscriptions

Filter with: adb logcat | grep chessdebug
2026-03-24 15:43:17 +01:00
Claude
0ad3180fc5 feat: add anonymous reply with throwaway keypair
Allow users to reply to notes using a fresh, randomly generated keypair
that is not linked to their account. When the "Anonymous" toggle is
activated in the compose screen, the reply is signed with a new
throwaway identity and broadcast to the user's outbox relays without
being cached as the user's own event.

- Add AnonymousPostButton composable (PersonOff icon toggle)
- Add wantsAnonymousPost state to ShortNotePostViewModel
- Add signAnonymouslyAndBroadcast to Account (creates temp KeyPair)
- Show warning banner and hide user avatar when anonymous mode is on
- Add string resources for anonymous post UI

https://claude.ai/code/session_013MaeT2T9Bg4HaVe2ACNADy
2026-03-24 14:25:25 +00:00
Vitor Pamplona
c913c0e1ab Merge pull request #1921 from vitorpamplona/claude/hide-video-controls-k09Ga
Hide video controls on playback start
2026-03-24 10:20:55 -04:00
Vitor Pamplona
30ff9ab09b Merge pull request #1918 from vitorpamplona/claude/draggable-video-progress-CQODi
feat: add drag-to-seek on video progress indicator
2026-03-24 10:12:31 -04:00
Vitor Pamplona
159f1a121b Merge pull request #1916 from vitorpamplona/claude/add-multiple-choice-polls-CQUAv
feat: add multiple choice poll type selector
2026-03-24 10:07:33 -04:00
Vitor Pamplona
9b5b50f237 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  code review: - Removed redundant equality guards in SideEffect — MutableState already suppresses no-op writes for value types - Changed mutableStateOf({}) to mutableStateOf(null) with explicit nullable type — clearer intent, no accidental no-op invocation -  stopRecording() now early-returns with ?: return when not recording, so the Toast only shows for genuinely failed recordings (too short)
  entire solid recording indicator bar stops recording when tapped, not just the small stop icon.
  New Crowdin translations by GitHub Action
  update skill
  update translations: CZ, DE, PT, SE
2026-03-24 10:00:10 -04:00
Vitor Pamplona
898e6ac677 Added credits 2026-03-24 09:41:23 -04:00
Vitor Pamplona
d6d4bdc5e3 avoids duplicating quote tags by key + value 2026-03-24 09:40:02 -04:00
Vitor Pamplona
c39eca5172 Add new utility methods 2026-03-24 09:39:09 -04:00
Vitor Pamplona
3d9efa9938 Tries to solve a duplicate user in a lazy column bug 2026-03-24 09:33:50 -04:00
Vitor Pamplona
11dabd46ba Checks if p tags are user pubkeys when loading drafts. 2026-03-24 09:33:37 -04:00
Vitor Pamplona
94e708b5fc Reduces the need to compute naddr to index articles in bookmarks 2026-03-24 09:32:38 -04:00
Vitor Pamplona
8d81d6d57c Removes the clickable NIP-05 url in the @ tagging of users 2026-03-24 09:30:40 -04:00
Vitor Pamplona
8c4b2fe62b Makes sure quotes only happen once in events. 2026-03-24 09:30:07 -04:00
Vitor Pamplona
514e90ff17 Fixes non http uris in the references tag 2026-03-24 09:13:48 -04:00
KotlinGeekDev
4c14554263 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-24 13:27:40 +01:00
davotoula
da53001e65 Merge branch 'main' into chess-enhancements-and-bug-fixes 2026-03-24 13:21:41 +01:00
nrobi144
efa294ea72 feat(highlights): add article highlights and note-taking system
Phase 1: HighlightData model + DesktopHighlightStore (Preferences-based)
Phase 2: Text selection highlight via Cmd+H/Cmd+Shift+H, inline bold/italic
         rendering, HighlightAnnotationDialog, SelectionContainer wrapping
Phase 3: MyHighlightsScreen with grouped view, HighlightPublishAction for
         NIP-84 (kind 9802), deck/sidebar/menu wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:16:54 +02:00
David Kaspar
22142d17f9 Merge pull request #1924 from davotoula/larger-stop-area-for-voice-recording
Larger stop area for voice recording
2026-03-24 10:59:41 +01:00
nrobi144
c75176891f feat(reads): integrate long-form into deck layout with zoom and reactions
- Wire Article, Editor, Drafts into DeckColumnType + DeckColumnContainer
- Add Drafts to sidebar nav, Add Column dialog, and Window menu
- Add article navigation (onNavigateToArticle) in SinglePaneLayout
- Add NoteActionsRow to ReadsScreen LongFormCards (zaps, reactions, replies)
- Add Cmd+/Cmd- zoom in ArticleReaderScreen via fontScale on RenderMarkdown
- Add kind 30023 to ProfileSubscription for long-form in user profiles
- Add fontScale param to RenderMarkdown using scaled LocalDensity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:18:37 +02:00
nrobi144
328fcf86d7 refactor(reads): address all review findings — security, performance, simplicity
P1 Critical:
- Add 300ms debounce on editor preview to prevent AST re-parse per keystroke
- Hoist Regex constants in ReadingTimeCalculator and TableOfContents
- Add URI scheme allowlist to editor onLinkClick (was missing vs reader)
- Add MAX_CONTENT_BYTES (100KB) validation before publish

P2 Important:
- Delete MarkdownSpikeScreen (278 LOC spike artifact)
- Delete HighlightCreator (95 LOC, zero callers — YAGNI)
- Delete DraftLongTextNoteEvent (162 LOC, no consumers — YAGNI)
- Replace ArticleMediaRenderer interface with onLinkClick lambda
- Extract inline subscription to FilterBuilders.longFormByAddress()
- Replace SimpleDateFormat with thread-safe DateTimeFormatter
- Remove dead placeholders (bookmark button, reactions row, unused param)
- Cache draft index in memory to avoid redundant disk reads
- Add TODO for publish-before-relay-ack issue

P3 Nice-to-have:
- Remove timestamp from subscription subId for stability
- Remove redundant slug ".." removal (regex handles it)
- Add metadata field length limits (title 256, summary 1024)
- Fix identical isMacOS branches in editor
- Validate image URLs (http/https only) in ArticleHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:03:29 +02:00
nrobi144
4fcbffdb3b feat(reads): Phase 2 — editor, drafts, publish, highlights
- Create DraftLongTextNoteEvent (kind 30024) in quartz with EventFactory registration
- Create LongFormPublishAction for signing + publishing kind 30023 events
- Create split-pane ArticleEditorScreen with live markdown preview
- Create MarkdownToolbar (bold, italic, heading, link, image, code, quote)
- Create MetadataPanel (title, summary, banner, tags, slug)
- Create DesktopDraftStore with JSON index + .md files, atomic writes, slug sanitization
- Create DraftsScreen with draft list, delete, new draft button
- Create HighlightCreator dialog for kind 9802 highlight creation
- Add Drafts nav rail entry and Editor/Drafts screen routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:59:52 +02:00
nrobi144
5ef36363cd feat(reads): Phase 1 — article reader with markdown, ToC, reading time
- Add richtext-commonmark deps to commons for shared markdown rendering
- Create RenderMarkdown composable with URL scheme allowlist (security)
- Create ArticleMediaRenderer interface for platform-specific media
- Create ArticleHeader with banner, title, author, reading time metadata
- Create TableOfContents with heading extraction and scroll-spy
- Create ReadingTimeCalculator (238 WPM prose, 80 WPM code, image decay)
- Create ArticleReaderScreen with Medium-style typography (680dp, 1.58x)
- Add DesktopScreen.Article navigation with address tag routing
- Update ReadsScreen to navigate via address tags instead of event IDs
- Responsive ToC sidebar (shown when window > 1100dp)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:59:52 +02:00
davotoula
e976bf9e2e code review:
- Removed redundant equality guards in SideEffect — MutableState already suppresses no-op writes for value types
- Changed mutableStateOf({}) to mutableStateOf(null) with explicit nullable type — clearer intent, no accidental no-op invocation
-  stopRecording() now early-returns with ?: return when not recording, so the Toast only shows for genuinely failed recordings (too short)
2026-03-24 09:25:07 +01:00
davotoula
4e43b14b09 entire solid recording indicator bar stops recording when tapped, not just the small stop icon. 2026-03-24 09:25:07 +01:00
David Kaspar
6f3308e45c Merge pull request #1923 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-24 09:00:06 +01:00
Crowdin Bot
1dbe552821 New Crowdin translations by GitHub Action 2026-03-24 07:35:59 +00:00
davotoula
6436668808 update skill 2026-03-24 08:32:56 +01:00
davotoula
346a7ecac2 update translations: CZ, DE, PT, SE 2026-03-24 08:27:32 +01:00
Claude
c67d66981c feat: hide video controls on playback start
Video controls are now hidden by default when playback begins.
Users can tap to reveal them. Previously, controls were shown for
2 seconds before auto-hiding (TwoSecondController) or shown by
default (VideoViewInner).

https://claude.ai/code/session_01WpAqbodB5w8oDJpN6MLBs6
2026-03-24 06:15:11 +00:00
Claude
cba65344b9 fix: clean up temp files after upload completes
MetadataStripper, MediaCompressor, and EncryptFiles create intermediate
temp files in cacheDir during the upload pipeline, but these were never
deleted after the upload finished. Over time this causes unbounded disk
growth. Wrap upload calls in try/finally to ensure all intermediate temp
files (compressed, stripped, encrypted) are deleted regardless of
success or failure.

https://claude.ai/code/session_01YS3dbZ1k84N2EHS3AruYRV
2026-03-24 04:37:41 +00:00
Claude
6e0e8cf00f feat: add drag-to-seek support on video progress indicator
The progress scrubber can now be dragged to seek through the video,
in addition to the existing tap-to-seek. During drag, the scrubber
enlarges for visual feedback and tracks the finger position in
real-time. Seeking is applied on drag end.

https://claude.ai/code/session_01QvYAvNdC35zGgkdpnujFn2
2026-03-24 04:01:27 +00:00
Claude
8525f2b930 feat: add poll type selector for single and multiple choice polls
The poll creation UI now shows filter chips to choose between
single choice and multiple choice poll types. Previously it was
hardcoded to single choice only.

https://claude.ai/code/session_01PEsEyGiNvNcNPSjREN4WzW
2026-03-24 02:50:46 +00:00
Claude
425fc24bc2 perf: optimize ChaCha20 and XChaCha20-Poly1305 for speed
ChaCha20Core.chaCha20Xor:
- Parse key/nonce once into initial state, reuse across blocks
- XOR at word level (4 bytes at a time) instead of byte-by-byte
- Reuse working IntArray across blocks instead of allocating per block

XChaCha20Poly1305:
- Add hChaCha20FromNonce24 to avoid nonce copyOfRange(0,16) allocation
- Add chaCha20PolyKey to generate only 32 bytes instead of full 64-byte block
- Use single result array instead of ciphertext + tag concatenation

Benchmark (98-byte padded message, JVM):
  Before: encrypt 2286 ns/op, decrypt 2062 ns/op
  After:  encrypt 1544 ns/op, decrypt 341 ns/op

https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-24 02:35:30 +00:00
Vitor Pamplona
7874893938 v1.06.1 2026-03-23 18:19:42 -04:00
Vitor Pamplona
51fa28f22a no message 2026-03-23 18:16:24 -04:00
Vitor Pamplona
ec4f949782 Leaves some space to click when their is no space or new lines on the poll. 2026-03-23 18:15:36 -04:00
Vitor Pamplona
4c9f2de6f7 - Improves rendering of the completed polls
- Doesn't render quoted polls
2026-03-23 18:01:41 -04:00
Vitor Pamplona
e5aeb49a4b Solves some of the crashes of concurrent modification exception 2026-03-23 17:59:14 -04:00
Vitor Pamplona
da84848df7 Merge pull request #1911 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-23 17:46:06 -04:00
Crowdin Bot
3d488165c3 New Crowdin translations by GitHub Action 2026-03-23 21:40:20 +00:00
Vitor Pamplona
f99543dc3b Merge pull request #1913 from greenart7c3/claude/add-url-crash-tests-J8Omf
Claude/add url crash tests j8 omf
2026-03-23 17:38:51 -04:00
Claude
7ea5557828 test: fix UrlTest to use 今北産業 as the crash input (PR #1907)
https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2
2026-03-23 21:02:41 +00:00
Claude
3a93c4b075 test: add UrlTest for getPart() boundary conditions fixed in PR #1907
Tests the StringIndexOutOfBoundsException fix in Url.getPart() by directly
constructing Url objects with UrlMarker indices that exceed the trimmed
originalUrl length — the exact scenario that occurs when readEnd() strips
a trailing character (e.g. ':') while the PORT/QUERY marker still points
to the original buffer position.

https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2
2026-03-23 20:57:49 +00:00
Claude
9ebc4a1e67 style: remove trailing blank line in UrlMarkerTest
https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2
2026-03-23 20:56:58 +00:00
Claude
9202b60dcf test: add regression tests for 今北産業 URL crash (PR #1907)
Verifies that the Japanese phrase "今北産業" does not cause a
StringIndexOutOfBoundsException in Url.getPart() and is not
detected as a URL.

https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2
2026-03-23 20:46:59 +00:00
Claude
e31e3829a6 test: add regression tests for PR #1907 URL crash fix
Tests cover the StringIndexOutOfBoundsException in Url.getPart() that
occurred when readEnd() trimmed trailing characters (e.g. ':') from a
detected URL but urlMarker indices remained pointing past the trimmed
string's end.

- UrlMarkerTest: three cases testing PORT/QUERY index at or beyond
  string length (the boundary conditions fixed by minOf clamping and
  the startIndex >= length guard)
- UrlsDetectorTest: regression for "今北産業" (no crash, 0 URLs) and
  for a bare "example.com:" host
- UrlParserTest: end-to-end regression for "今北産業" and "example.com:"

https://claude.ai/code/session_013rJ9iYndYVJgpYK2VazxL2
2026-03-23 20:31:28 +00:00
Vitor Pamplona
f553489ddf Improves the wording of the Last Seen 2026-03-23 15:51:35 -04:00
Vitor Pamplona
2b920fc47a Merge pull request #1907 from greenart7c3/claude/fix-android-runtime-exception-auFbS
Fix URL parsing bounds checking in Url.kt
2026-03-23 15:51:07 -04:00
davotoula
6c8021f219 code review fixes:
Deduplicate chess notify() into shared notifyChessEvent() helper using BaseChessEvent
Remove addCompletedGameDirectly, extend moveToCompleted with optional liveState param
Hoist currentUser lookup to top of ChessLobbyContent, remove 4 duplicate remembers
Add missing imports in desktop ChessScreen, replace all FQN usages with short names
Remove redundant distinctBy in completed games UI (state already prevents duplicates)
2026-03-23 20:39:54 +01:00
KotlinGeekDev
ac38a73b5e spotless apply. 2026-03-23 20:27:04 +01:00
KotlinGeekDev
4672e6b587 Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-03-23 19:55:49 +01:00
KotlinGeekDev
bddff36e6b androidLibrary {} -> android {} 2026-03-23 19:55:35 +01:00
KotlinGeekDev
03c00b9fba Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-23 19:52:22 +01:00
KotlinGeekDev
b495f3f409 NIP19 tests for iOS. 2026-03-23 19:50:26 +01:00
KotlinGeekDev
cfecfe2130 Move uri-reference-kmp to version catalog. Remove swift bridge(was a nice run). 2026-03-23 19:49:24 +01:00
KotlinGeekDev
f2bdb24ee5 Removal of references to APIs using the Swift bridge. 2026-03-23 19:38:10 +01:00
KotlinGeekDev
a9c41e1d55 Introduce uri-reference-kmp and replace uriBridge usages with it. 2026-03-23 19:31:58 +01:00
davotoula
a6205a29c4 fix(chess): add chess kinds to notification relay subscription
Chess event kinds 30065 (accept) and 30066 (move) were missing from the
notification relay subscription filters, so they were never fetched from
relays for the in-app notification feed. Also bypass the follow-list
filter for chess events since opponents may not be followed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
c86daa6950 fix(chess): filter own games from live list, dismissable errors, clickable avatars and completed games
- Filter user's own games from public "Live Games" list in lobby
- Fix broken addSpectatingGame filter (playerPubkey is always viewerPubkey)
- Add removeGame() to clean up stale entries when "Game not found"
- Make player avatars clickable to open profile on game screen
- Make completed game cards clickable to view final board state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
08d5f2ebb5 fix(chess): resolve TimeUtils compilation error in ChessLobbyState
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
478c4cc356 feat(chess): notifications for challenge accepted and opponent moves
Add Android push notifications for chess events:
- LiveChessGameAcceptEvent (kind 30065): notifies when opponent accepts challenge
- LiveChessMoveEvent (kind 30066): notifies when opponent makes a move
- New Chess notification channel alongside existing DM and Zap channels
- Chess kinds added to NotificationFeedFilter for in-app notification feed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
292c6de08a feat(chess): exit spectator mode with Leave Game button
Adds ability to leave a spectated game on both Android and Desktop:
- Added stopSpectating() to ChessLobbyLogic (removes from state + stops polling)
- Both ViewModels now delegate to logic.stopSpectating() instead of bypassing it
- LiveChessGameScreen accepts onLeaveSpectating callback shown in spectator info area
- Android: Leave Game button pops back stack and stops spectating
- Desktop: Leave Game button clears selectedGameId (returns to lobby) and stops spectating

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
ed9d942ce0 feat(chess): finished games section below active games in lobby
Adds a "Finished Games" section to the Android chess lobby screen,
showing up to 10 completed games with opponent avatars, result
(Won/Lost/Draw), and move count. The section only appears when
completedGames is non-empty, matching the Desktop implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
012e97494e feat(chess): add rank numbers (1-8) to board coordinates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
006a2229fe feat(chess): avatar vs avatar display on board and game list
Add ChessPlayerChip.kt with three shared composables:
- ChessPlayerChip: single player avatar + name with active border
- ChessPlayerVsHeader: mirrored White vs Black header above the board
- OverlappingAvatars: compact overlapping circular avatars for list cards

Wire avatars into:
- LiveChessGameScreen: vs header with active player green border
- DesktopChessGameLayout: vs header above the chess board
- Android & Desktop lobby cards: overlapping avatars in ActiveGameCard,
  ChallengeCard, and OutgoingChallengeCard avatar slots

Uses existing UserAvatar (coil3 AsyncImage + Robohash fallback) from
commons for KMP compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
8c69105fdb fix(chess): exclude own games from spectator list
Skip adding games to spectatingGames when the user is a participant
(playerPubkey or opponentPubkey matches userPubkey) or when the game
is already finished.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
638bdb7b2d fix(chess): finished games transition from active to completed
Wire onGameEndDismiss callback so the "Continue" button on game end
overlay moves the game to the completed list. Add auto-detection of
finished games during polling refresh so games transition even if the
user navigates away without clicking the button. Discovered games that
are already finished now go straight to completed instead of appearing
in the active list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
2004caa53c fix(chess): board interactive on mobile at game start
The board was non-interactive for participants during pending challenges
on Android because canMakeMoves factored in isPendingChallenge. Desktop
passed isSpectator directly without the pending check, so it worked.

Remove isPendingChallenge from the board interactivity gate to match
Desktop behavior. The header still shows "Challenge Pending" status,
and this also fixes a secondary issue where the isPendingChallenge flag
could get stuck when replaceGameState preserved the old state via its
open-challenge workaround.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
Claude
2e1bcf3e52 style: apply spotless formatting to ChaCha20/Poly1305 sources
https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-23 17:57:31 +00:00
Vitor Pamplona
306d76f793 Tries to fix
IllegalArgumentException: 1.06.0-PLAY

| Prop | Value |
|------|-------|
| Manuf |Google |
| Model |Pixel 8a |
| Prod |akita |
| Android |16 |
| SDK Int |36 |
| Brand |google |
| Hardware |akita |
| Device | akita |
| Host | r-0123456789abcdef-0123 |
| User | android-user |

```
java.lang.IllegalArgumentException: Cannot disable reuse from root if it was caused by other groups
    androidx.compose.runtime.PreconditionsKt.throwIllegalArgumentException(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:3)
    androidx.compose.runtime.ComposerImpl.endReuseFromRoot(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:19)
    androidx.compose.runtime.PausedCompositionImpl.resume(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:369)
    androidx.compose.ui.layout.LayoutNodeSubcompositionsState$precomposePaused$2.resume(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:50)
    androidx.compose.foundation.lazy.layout.PrefetchHandleProvider$HandleAndRequestImpl.performPausableComposition(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:49)
    androidx.compose.foundation.lazy.layout.PrefetchHandleProvider$HandleAndRequestImpl.executeRequest(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:128)
    androidx.compose.foundation.lazy.layout.PrefetchHandleProvider$HandleAndRequestImpl.execute(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:33)
    androidx.compose.foundation.lazy.layout.AndroidPrefetchScheduler.runRequest(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:36)
    androidx.compose.foundation.lazy.layout.AndroidPrefetchScheduler.run(r8-map-id-eb237b6d0af50dd04fa25285a72672c5149c195876ad0d708b61d757d31f3d90:113)
    android.os.Handler.handleCallback(Handler.java:1070)
    android.os.Handler.dispatchMessage(Handler.java:125)
    android.os.Looper.dispatchMessage(Looper.java:333)
    android.os.Looper.loopOnce(Looper.java:263)
    android.os.Looper.loop(Looper.java:367)
    android.app.ActivityThread.main(ActivityThread.java:9331)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:566)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:837)
```
2026-03-23 13:47:00 -04:00
Claude
d586a0dc25 feat: replace libsodium with pure Kotlin ChaCha20/Poly1305 implementation
Remove LazySodium/JNA/libsodium native dependencies and replace with
pure Kotlin implementations of ChaCha20, HChaCha20, XChaCha20,
Poly1305 MAC, and XChaCha20-Poly1305 AEAD. This eliminates platform-
specific native binaries while maintaining full NIP-44 compatibility.

- Add ChaCha20Core with RFC 8439 block function, IETF stream XOR,
  HChaCha20, and XChaCha20
- Add Poly1305 MAC (RFC 8439 §2.5)
- Add XChaCha20Poly1305 AEAD encrypt/decrypt
- Convert LibSodiumInstance from expect/actual to commonMain object
- Delete platform-specific LibSodiumInstance actuals (android/jvm/ios)
- Remove iOS native interop (cinterop, linker opts, .a libraries)
- Remove lazysodium-java, lazysodium-android, JNA from build deps
- Clean up ProGuard rules (remove JNA/LazySodium keep rules)
- Add RFC 8439 + libsodium test vectors for ChaCha20 and XChaCha20
- Update benchmark to use simplified ChaCha20 API

https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-23 17:40:29 +00:00
Vitor Pamplona
eeca86e542 Fixes comparator to avoid Comparison method violates its general contract! 2026-03-23 13:16:32 -04:00
Claude
951610d9ca fix: prevent StringIndexOutOfBoundsException in Url.getPart()
When UrlDetector.readEnd() trims the last character from URLs ending
with characters like '.', ',', etc., the urlMarker indices remain
based on the original buffer length. This caused getPart() to call
substring() with an end index beyond the trimmed URL's length.

Fix by clamping the end index to originalUrl.length and guarding
against an out-of-bounds start index.

https://claude.ai/code/session_014Q2SvTE5vKgUm1w8dPVjYo
2026-03-23 17:10:56 +00:00
Vitor Pamplona
590bd21f62 UriParser might crash, wraps in a try catch 2026-03-23 13:03:43 -04:00
Vitor Pamplona
81380a32ae Fixes POCO phone's removal of the + sign in NWC 2026-03-23 13:03:18 -04:00
Vitor Pamplona
943f1256a7 Improves zap-store settings 2026-03-23 12:41:30 -04:00
Vitor Pamplona
7af1be7363 v1.06.0 2026-03-23 11:23:44 -04:00
Vitor Pamplona
a39688dabb remove warnings 2026-03-23 10:46:55 -04:00
Vitor Pamplona
33ec43a5c3 Removing warnings 2026-03-23 10:39:56 -04:00
Vitor Pamplona
1f5841d090 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
  New Crowdin translations by GitHub Action
2026-03-23 10:31:22 -04:00
Vitor Pamplona
9d086df37a Updating all dependencies 2026-03-23 10:28:33 -04:00
Vitor Pamplona
ed3390f64a Fixes weird import 2026-03-23 10:16:59 -04:00
Vitor Pamplona
e659651dd4 Merge pull request #1906 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-23 09:26:59 -04:00
Crowdin Bot
0cc06f424e New Crowdin translations by GitHub Action 2026-03-23 13:12:22 +00:00
davotoula
11176a4679 update translations: CZ, DE, PT, SE 2026-03-23 14:08:14 +01:00
Vitor Pamplona
d964816fd3 Fixes build dependency bug: Spotless needs vlcSetup for... something... 2026-03-23 09:06:52 -04:00
Vitor Pamplona
bbb75afed9 Merge pull request #1904 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-23 08:27:58 -04:00
Crowdin Bot
2eb3ebd365 New Crowdin translations by GitHub Action 2026-03-23 12:25:24 +00:00
Vitor Pamplona
c75881d928 Merge pull request #1903 from nrobi144/fix/chess-interop
fix(chess): normalize SAN for cross-client Jester interop
2026-03-23 08:23:51 -04:00
nrobi144
9d8d0e249d fix(chess): encode required fields in Jester content JSON
JesterContent fields version, fen, and history were omitted from
serialized JSON because kotlinx.serialization skips default values.
Jester's isStartGameEvent checks arrayEquals(json.history, []) which
fails when the field is absent (undefined !== []).

Added @EncodeDefault to version, fen, and history so they are always
included in the JSON output regardless of value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:08:28 +02:00
nrobi144
677bc6c34a fix(chess): add offchain.pub relay for jester interop
jester.nyo.dev uses offchain.pub as one of its relays. Adding it gives
us 2 shared relays (relay.damus.io + offchain.pub) instead of 1,
improving challenge visibility between Amethyst and Jester clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:26:09 +02:00
nrobi144
3eaf3d38a5 fix(chess): normalize SAN for cross-client interop
ChessEngine.makeMove() now normalizes SAN notation before passing to
chesslib, fixing games against jester.nyo.dev and other clients that
use different notation:

- Castling: 0-0 → O-O, 0-0-0 → O-O-O (zeros to letters)
- Annotations: strips !, ?, !!, ??, !?, ?! suffixes

Previously, chesslib rejected 0-0/0-0-0 causing ChessStateReconstructor
to mark games as desynced and skip remaining moves. Games appeared
permanently stuck, even after app restart.

Also fixes discoverUserGames() silently dropping games classified as
spectator — now adds them to spectatingGames instead.

23 new interop tests covering SAN variants, reconstruction, spectator
detection, and missing start event handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 07:21:17 +02:00
Vitor Pamplona
d4fd8088f1 lint 2 2026-03-22 21:04:11 -04:00
Vitor Pamplona
3e7633ea6b lint 2026-03-22 20:02:13 -04:00
Vitor Pamplona
11ed878111 Removing unnecessary XML loading. 2026-03-22 19:43:00 -04:00
Vitor Pamplona
76df43b031 Removes logs from blossom fetcher 2026-03-22 19:07:18 -04:00
Vitor Pamplona
41b0abc8e9 Moves expiration process to IO 2026-03-22 19:06:37 -04:00
Vitor Pamplona
f70fd9ad2b more change log improvements. 2026-03-22 15:47:00 -04:00
Vitor Pamplona
9eba27e9b2 Additional updates to the change log 2026-03-22 15:28:08 -04:00
Vitor Pamplona
37f6a7b133 Makes sure subscriptions are closed on the NostrClient utility methods 2026-03-22 15:18:48 -04:00
Vitor Pamplona
439b392aa0 Moves EmptyNostrClient to a class to avoid auto-import issues 2026-03-22 15:14:49 -04:00
Vitor Pamplona
2e1066a609 Merge pull request #1902 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-22 14:55:48 -04:00
Crowdin Bot
0905b05388 New Crowdin translations by GitHub Action 2026-03-22 18:50:38 +00:00
Vitor Pamplona
b2f7c3296f Activate edits for Articles. 2026-03-22 14:47:27 -04:00
Vitor Pamplona
9c30e2d0f6 Adds draft editing to long form posting 2026-03-22 14:38:44 -04:00
Vitor Pamplona
570639c1df Fixes build 2026-03-22 12:50:26 -04:00
Vitor Pamplona
65ae09e201 Adding file uploads and metadata stripping to the Longform screen 2026-03-22 12:46:25 -04:00
Vitor Pamplona
93b1392080 Merge pull request #1901 from vitorpamplona/claude/add-markdown-post-screen-Xma8x
Add NIP-23 Long-Form Content (Markdown Articles) Support
2026-03-22 12:38:14 -04:00
Vitor Pamplona
3e93472dce Merge branch 'main' into claude/add-markdown-post-screen-Xma8x 2026-03-22 12:38:04 -04:00
Vitor Pamplona
26a3dcbd85 Adds upload cover url 2026-03-22 12:35:19 -04:00
Vitor Pamplona
ffa60c5fd3 Final touches on new Markdown Screen 2026-03-22 12:25:49 -04:00
Vitor Pamplona
cd3282b342 Merge pull request #1900 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-22 11:25:05 -04:00
Crowdin Bot
41539c1b7a New Crowdin translations by GitHub Action 2026-03-22 15:21:49 +00:00
Vitor Pamplona
40c45fb3a1 Adds a RelaySync filter by since/until dates. 2026-03-22 11:18:28 -04:00
Vitor Pamplona
b6b4856fe4 Fixes a closing bug when the coroutine crashes
Fixes an index bug on matching subscriptions
Requires the relay to send events that match the sub in order to move forward with the paging
2026-03-22 11:18:12 -04:00
Vitor Pamplona
adb3a0e0fc Better transitions to a Nostrclient that is autocloseable, destroying all links to everything else when closing 2026-03-22 11:16:54 -04:00
Vitor Pamplona
459f69ff2c Moves open polls view to a state flow in thew viewmodel 2026-03-21 16:20:15 -04:00
Vitor Pamplona
90cced23d2 Fixes search by noteID returning a version of the note instead of the address 2026-03-21 15:49:19 -04:00
David Kaspar
0dec0164ad Merge pull request #1899 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-21 18:47:46 +01:00
Crowdin Bot
5af0cc7eac New Crowdin translations by GitHub Action 2026-03-21 17:38:51 +00:00
Vitor Pamplona
ef5508b60b Merge pull request #1896 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-21 13:38:47 -04:00
Vitor Pamplona
fd79da0b09 Merge pull request #1898 from vitorpamplona/claude/nip42-relay-auth-P878m
Implement pluggable relay policies for NIP-42 authentication
2026-03-21 13:37:36 -04:00
Vitor Pamplona
15ff3040b5 Merge branch 'main' into claude/nip42-relay-auth-P878m 2026-03-21 13:37:21 -04:00
Crowdin Bot
a86b9ab2e1 New Crowdin translations by GitHub Action 2026-03-21 17:35:12 +00:00
Vitor Pamplona
862aa0cb78 Update CHANGELOG for release v1.06.0
Updated CHANGELOG for release v1.06.0, detailing new features, improvements, and removals including support for NIP-85 Polls, custom NIP-40 Expirations, and various UI enhancements.
2026-03-21 13:33:51 -04:00
Vitor Pamplona
cfead47d71 Improves design of the policy 2026-03-21 13:21:21 -04:00
davotoula
32c0efb291 update translations: CZ, DE, PT, SE 2026-03-21 16:04:16 +01:00
Vitor Pamplona
b2ad97bb6b merge 2026-03-20 21:19:49 -04:00
Vitor Pamplona
df6dc17047 Final touches on the Attestation cards 2026-03-20 21:16:59 -04:00
David Kaspar
81308a2368 Merge pull request #1894 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-21 00:16:29 +01:00
Crowdin Bot
24d2928881 New Crowdin translations by GitHub Action 2026-03-20 23:08:05 +00:00
Vitor Pamplona
2ef634ca01 Fixes showing attestations in the NoteMaster 2026-03-20 19:05:07 -04:00
Vitor Pamplona
aca62ec81f Improves layout of the open poll cards 2026-03-20 18:41:11 -04:00
Claude
b0a8cae194 refactor: extract AuthPolicy interface from requireAuth boolean
Replace the requireAuth: Boolean parameter in NostrServer with a
pluggable AuthPolicy interface. Each policy has trigger points for
EVENT (acceptEvent), REQ (acceptReq with filter rewriting), COUNT
(acceptCount), and live event delivery (canSendToSession).

Built-in policies: OpenPolicy (allow all) and RequireAuthPolicy
(require auth for all commands, matching the previous behavior).

https://claude.ai/code/session_017vdjbdxdYK1oJMH66koVZE
2026-03-20 22:40:18 +00:00
Vitor Pamplona
f2e60986d6 Merge pull request #1895 from vitorpamplona/claude/show-open-polls-notifications-1ZDVi
Add Open Polls section to notifications feed
2026-03-20 18:30:07 -04:00
Vitor Pamplona
b93ad35029 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE add: find missing translations skill
2026-03-20 18:18:24 -04:00
Vitor Pamplona
d54bc91c20 Updates changelog for next release 2026-03-20 18:12:48 -04:00
David Kaspar
b1ca425c46 Merge pull request #1893 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-20 23:11:31 +01:00
Claude
8fd08aa56d feat: implement NIP-42 server-side relay authentication
Add server-side AUTH challenge/response handling per NIP-42 spec:
- RelaySession generates a unique challenge per connection and tracks
  authenticated pubkeys (multiple users can auth on one session)
- NostrServer gains relayUrl and requireAuth parameters; when requireAuth
  is true, EVENT/REQ/COUNT are rejected with auth-required: prefix
- AUTH handler validates kind 22242, created_at within 10 min, challenge
  and relay tag matching
- AuthCmd now accepts Event (not just RelayAuthEvent) so the server can
  gracefully reject wrong-kind auth attempts
- 11 new tests covering auth success, wrong challenge/relay/kind/timestamp,
  multi-user auth, and requireAuth gating

https://claude.ai/code/session_017vdjbdxdYK1oJMH66koVZE
2026-03-20 22:05:53 +00:00
Crowdin Bot
6bdf65da56 New Crowdin translations by GitHub Action 2026-03-20 21:59:53 +00:00
davotoula
64316f7d73 update translations: CZ, DE, PT, SE
add: find missing translations skill
2026-03-20 22:55:31 +01:00
Vitor Pamplona
73536dd421 merge 2026-03-20 17:49:31 -04:00
Vitor Pamplona
817c4c7723 Removes some Attestation events from the feeds, adding on others 2026-03-20 17:45:45 -04:00
Claude
98cda3ba71 style: apply spotless formatting to OpenPollsSection
https://claude.ai/code/session_01LLLN5MDA5nJFYx38MVo81Q
2026-03-20 21:41:13 +00:00
Claude
b6e54db81b refactor: use LocalCache.observeNotes flow for open polls
Replace LocalCache.notes.forEach iteration with observeNotes using a
Filter for PollEvent.KIND and ZapPollEvent.KIND by the current user.
This makes the open polls list reactive — it updates automatically
when new polls arrive instead of only computing once on composition.

https://claude.ai/code/session_01LLLN5MDA5nJFYx38MVo81Q
2026-03-20 21:38:48 +00:00
David Kaspar
ed624a0a36 Merge pull request #1892 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-20 22:33:58 +01:00
Crowdin Bot
57c046261e New Crowdin translations by GitHub Action 2026-03-20 21:32:27 +00:00
Vitor Pamplona
6cb686f5cb Merge pull request #1889 from davotoula/convert-dropdown-menus-to-M3-action-sheet-dialogs
Convert dropdown menus to m3 action sheet dialogs
2026-03-20 17:30:58 -04:00
Vitor Pamplona
bbac00ccb3 Merge branch 'main' into convert-dropdown-menus-to-M3-action-sheet-dialogs 2026-03-20 17:30:50 -04:00
Vitor Pamplona
0f921989b9 Merge pull request #1891 from vitorpamplona/claude/add-attestations-home-feed-lQCT8
Add support for rendering attestation events
2026-03-20 17:28:38 -04:00
Vitor Pamplona
a21da546ab Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-20 17:26:22 -04:00
Vitor Pamplona
9bfb170ffa simple documentation 2026-03-20 17:15:05 -04:00
David Kaspar
ccffeb1e49 Merge pull request #1890 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-20 22:14:17 +01:00
Crowdin Bot
752f460bc0 New Crowdin translations by GitHub Action 2026-03-20 21:08:30 +00:00
Vitor Pamplona
b247310ba7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  code review fixes:   1. options.isNotEmpty().also → if guard   2. Hardcoded English strings in FeedGroup enum   3. Hardcoded accessibility label   4. Raw 14.sp literals (×6) → replaced with Font14SP theme constant   5. Raw 12.sp literal → replaced with Font12SP theme constant   6. Modifier.size(20.dp) → replaced with existing Size20Modifier theme constant
  update gitignore
  add grouped feed filter dialog with Material 3 styling add icons, reduce text size, center group headers in filter dialog
  modernize SpinnerSelectionDialog with Material 3 styling
  feature switch chess icon on android: visible in debug/benchmark client only
  New Crowdin translations by GitHub Action
  Intentionality: check the return value and log a warning via Log.w() when deletion fails
  Update CS, DE, SV, PT
2026-03-20 17:05:29 -04:00
Vitor Pamplona
fc1e3e6b83 Adds a simple relay to quartz 2026-03-20 16:54:04 -04:00
Vitor Pamplona
d431b12f94 Migrates EventStore from Android's SQLLite to KMP
Fixes testing of libsodium between java and android
2026-03-20 16:00:17 -04:00
Vitor Pamplona
c5066d89c3 Fixes serialization issues when the code falls back to Kotlin Serialization 2026-03-20 15:58:17 -04:00
davotoula
26a97aa141 Code review fixes:
- Replaced manual .semantics with the simpler .clickable - avoids conditional Modifier allocation and a redundant semantics block
 - Moved LocalClipboardManager.current inside the if
 - Merged two consecutive postNostrUri?.let blocks into a single one
 - Wrapped the "Copy & Gallery" M3ActionSection in a condition
 - Removed duplicate string resource
2026-03-20 20:03:54 +01:00
davotoula
c9afec47cd style: run spotlessApply after M3 dialog migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:24:49 +01:00
davotoula
bd10f74b2a feat: convert bookmark item options menu to M3 dialog
Replace DropdownMenu with M3ActionDialog, M3ActionSection, and M3ActionRow
components. Move dialog outside ClickableBox. Preserve wantsToEditPost and
reportDialogShowing state holders unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:22:31 +01:00
davotoula
6007e22871 feat: convert note actions menu to M3 dialog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:19:53 +01:00
davotoula
4e5d2e7d2a feat: convert media context menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:17:16 +01:00
davotoula
b3a61ba288 feat: convert user profile menu to M3 dialog
Replace DropdownMenu with M3ActionDialog in UserProfileDropDownMenu,
structured into share, moderation, and report sections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:14:50 +01:00
davotoula
6632d3f515 feat: convert media playback menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:12:13 +01:00
davotoula
2fc9b294ae feat: convert follow pack actions menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:08:50 +01:00
davotoula
28dbb3948b feat: convert bookmark group management menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:05:47 +01:00
davotoula
5bde93d58f feat: convert people list management menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:33:26 +01:00
davotoula
2c6d18607a feat: convert bookmark list actions menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:30:58 +01:00
davotoula
cbe13f6819 feat: convert add member menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:10:45 +01:00
davotoula
dce1484dcc feat: convert add bookmark menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:19:27 +01:00
davotoula
aaed4bf697 feat: convert relay export menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:11:46 +01:00
davotoula
289c882d59 feat: convert URL preview copy menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:05:03 +01:00
davotoula
29f0b78fdd feat: convert Messages mark-as-read menu to M3 dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:02:46 +01:00
davotoula
19ff300f65 feat: add string resources for M3 action dialog titles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:59:55 +01:00
davotoula
fdb5b58513 feat: add reusable M3ActionDialog, M3ActionSection, M3ActionRow components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:59:51 +01:00
Vitor Pamplona
864581ecaa Merge pull request #1887 from davotoula/ui-polish-feed-filter
UI polish: feed filter
2026-03-20 10:00:38 -04:00
davotoula
620f8dbf10 code review fixes:
1. options.isNotEmpty().also → if guard
  2. Hardcoded English strings in FeedGroup enum
  3. Hardcoded accessibility label
  4. Raw 14.sp literals (×6) → replaced with Font14SP theme constant
  5. Raw 12.sp literal → replaced with Font12SP theme constant
  6. Modifier.size(20.dp) → replaced with existing Size20Modifier theme constant
2026-03-20 14:30:07 +01:00
davotoula
f15b7620df update gitignore 2026-03-20 14:01:45 +01:00
davotoula
3a195c4648 add grouped feed filter dialog with Material 3 styling
add icons, reduce text size, center group headers in filter dialog
2026-03-20 14:01:45 +01:00
davotoula
460ccc2d02 modernize SpinnerSelectionDialog with Material 3 styling 2026-03-20 14:00:19 +01:00
Vitor Pamplona
4f80fbfae4 Merge pull request #1886 from davotoula/feature-switch-for-chess
feature switch out chess icon on android
2026-03-20 07:41:38 -04:00
Claude
525b54e3c6 feat: add attestation events to LocalCache, Home feed, profile feed, and NoteCompose UI
- Load AttestationEvent (31871), AttestationRequestEvent (31872),
  AttestorRecommendationEvent (31873), and AttestorProficiencyEvent (11871)
  into LocalCache via consumeBaseReplaceable
- Create beautiful card-based UI renderers for all four attestation types
  with color-coded status indicators, validity badges, and kind chips
- Add attestation kinds to Home feed data source (FilterHomePostsByAuthors)
  and DAL filter (HomeNewThreadFeedFilter)
- Add attestation kinds to user profile feed data source
  (FilterUserProfilePosts) and DAL filter (UserProfileNewThreadFeedFilter)
- Add string resources for attestation status labels

https://claude.ai/code/session_01BEMFoHZENBwzmzS6TMPxVE
2026-03-20 10:39:32 +00:00
davotoula
05eb94915b feature switch chess icon on android: visible in debug/benchmark client only 2026-03-20 10:14:59 +01:00
David Kaspar
1159edad15 Merge pull request #1885 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-20 10:06:54 +01:00
Crowdin Bot
450a352c98 New Crowdin translations by GitHub Action 2026-03-20 08:36:42 +00:00
davotoula
94cf31eb29 Intentionality: check the return value and log a warning via Log.w() when deletion fails 2026-03-20 09:32:11 +01:00
davotoula
123d9eea0c Update CS, DE, SV, PT 2026-03-20 09:23:01 +01:00
Vitor Pamplona
c976f24a50 Fixes the list of users in the connected relays UI 2026-03-19 17:41:25 -04:00
Vitor Pamplona
583b9e56ba Merge pull request #1883 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 17:13:00 -04:00
Crowdin Bot
e183d5efd2 New Crowdin translations by GitHub Action 2026-03-19 21:10:44 +00:00
Vitor Pamplona
f9369b0cf1 Merge pull request #1884 from vitorpamplona/claude/improve-android-push-notifications-9DzMr
Add direct reply and mark read actions to DM notifications
2026-03-19 17:08:50 -04:00
Vitor Pamplona
11b5ebe108 Final touches. 2026-03-19 17:07:03 -04:00
Vitor Pamplona
379cbc7e69 Merge pull request #1882 from vitorpamplona/claude/add-attestations-package-ZnoES
Add attestation event types and tag support for NIP-31871
2026-03-19 16:42:44 -04:00
Vitor Pamplona
a3941e3b01 Merge pull request #1837 from vitorpamplona/claude/strip-file-metadata-sVIEd
Add metadata stripping for images, videos, and audio files
2026-03-19 16:41:10 -04:00
Claude
afb3221446 feat: add attestations package to quartz experimental
Implement the Attestations NIP with four event types:
- AttestationEvent (31871, addressable) - validity claims about other events
- AttestationRequestEvent (31872, addressable) - request attestation from attestors
- AttestorRecommendationEvent (31873, addressable) - recommend attestors for kinds
- AttestorProficiencyEvent (11871, replaceable) - declare verification proficiency

Each event follows Quartz's tag-per-file pattern with TagArrayExt and
TagArrayBuilderExt for reading/building tags.

https://claude.ai/code/session_01UswHRVTNqn1ToU4P4XDh2V
2026-03-19 20:40:19 +00:00
David Kaspar
32fdb63a89 Merge pull request #1881 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 21:32:11 +01:00
Claude
638bad3672 feat: show open polls at top of notification screen
Adds an "Open Polls" section at the top of the notification feed that
displays the user's PollEvent (NIP-88) and ZapPollEvent (kind 6969)
that are still active. Polls without a closing time are assumed to
expire 1 day after creation.

https://claude.ai/code/session_01LLLN5MDA5nJFYx38MVo81Q
2026-03-19 20:24:41 +00:00
Crowdin Bot
a1694bd84d New Crowdin translations by GitHub Action 2026-03-19 20:16:12 +00:00
David Kaspar
892d6f2b7c Merge pull request #1880 from davotoula/update-translations
Update CS, DE, SV, PT
2026-03-19 21:14:26 +01:00
davotoula
d1e70879ee Update CS, DE, SV, PT 2026-03-19 21:09:36 +01:00
davotoula
adc2e15078 Merge branch 'main-upstream' into claude/strip-file-metadata-sVIEd 2026-03-19 20:49:15 +01:00
Vitor Pamplona
4df78340b6 Merge pull request #1879 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 15:41:16 -04:00
davotoula
ae5f76e018 Merge branch 'main-upstream' into claude/strip-file-metadata-sVIEd 2026-03-19 20:38:57 +01:00
Crowdin Bot
f8d6ac7900 New Crowdin translations by GitHub Action 2026-03-19 19:38:07 +00:00
Vitor Pamplona
cee4291ba9 Merge pull request #1878 from vitorpamplona/claude/event-sync-screen-sYGtN
Claude/event sync screen
2026-03-19 15:36:38 -04:00
Vitor Pamplona
fd5e8dcca0 Merge pull request #1877 from vitorpamplona/claude/nip17-dm-migration-qxDbT
Deprecate NIP-04 and always use NIP-17 for DM sending
2026-03-19 15:25:38 -04:00
Vitor Pamplona
ad53bff8e3 Final touches to remove nip04 dms 2026-03-19 15:23:48 -04:00
Vitor Pamplona
f75971fc6a Merge pull request #1876 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 13:31:51 -04:00
Crowdin Bot
2f60697b28 New Crowdin translations by GitHub Action 2026-03-19 16:43:50 +00:00
Vitor Pamplona
3f4e265dc0 Brings the creation of Zap Polls back. 2026-03-19 12:40:14 -04:00
Claude
695d3c7a23 feat: deprecate NIP-04 DM sending, always use NIP-17
NIP-04 encryption for sending DMs is now deprecated. All new messages
are sent using NIP-17 (gift-wrapped sealed messages). Reading NIP-04
messages remains supported for backward compatibility.

When a recipient lacks both a DM relay list (kind 10050) and NIP-65
inbox relays, the send button is disabled and a warning is shown
explaining that messages cannot be delivered.

Changes:
- ChatNewMessageState: Remove nip17/requiresNip17 toggles, add
  recipientsMissingDmRelays state, always send via NIP-17
- ChatNewMessageViewModel: Always use NIP-17, remove NIP-04 send
  paths, add recipient relay status checking
- ToggleNip17Button -> Nip17Indicator: Replace toggle with static
  NIP-17 indicator (always on)
- PrivateMessageEditFieldRow: Show warning when recipients lack
  DM relay lists, hide message input
- ChatFileSender: sendAll() always uses NIP-17
- Desktop ChatPane: Remove NIP-17 toggle, show relay warning
- ChatroomView: Reactively check recipient DM relay availability

https://claude.ai/code/session_01T7QhUW9cZogk4DxDXbbbJJ
2026-03-19 15:22:13 +00:00
Vitor Pamplona
185fd96802 Merge branch 'main' into claude/strip-file-metadata-sVIEd 2026-03-19 10:06:20 -04:00
Vitor Pamplona
0f4555cfaa Merge pull request #1875 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 09:58:46 -04:00
Crowdin Bot
5903af2229 New Crowdin translations by GitHub Action 2026-03-19 13:56:12 +00:00
Vitor Pamplona
29db4ca0e3 Merge pull request #1825 from vitorpamplona/claude/event-sync-screen-sYGtN
Add Event Sync feature to redistribute events across relays
2026-03-19 09:54:26 -04:00
Vitor Pamplona
78ffb9fce4 Merge branch 'main' into claude/event-sync-screen-sYGtN 2026-03-19 09:54:20 -04:00
Vitor Pamplona
30f862cf26 Increase max parallel to 50 2026-03-19 09:50:50 -04:00
Vitor Pamplona
ab81b2a71d Improving calendar debug 2026-03-19 09:50:31 -04:00
Vitor Pamplona
ec29a47dc4 More debugging information 2026-03-19 09:34:52 -04:00
Vitor Pamplona
98578de840 Merge pull request #1874 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-19 08:51:23 -04:00
Crowdin Bot
311bbe19cf New Crowdin translations by GitHub Action 2026-03-19 12:45:49 +00:00
Vitor Pamplona
3f841c94ec Merge pull request #1873 from nrobi144/feat/desktop-media
feat(desktop): Full media parity — images, video, audio, encrypted DMs, upload, lightbox
2026-03-19 08:44:21 -04:00
nrobi144
5b2f2ca42b feat(chats): per-message encryption badge — lock for NIP-17, lock-open for NIP-04
Show a small lock icon next to the timestamp on each DM message:
- NIP-17 (ChatMessageEvent, ChatMessageEncryptedFileHeaderEvent): filled
  lock in primary color — relay can't see sender/recipient
- NIP-04 (PrivateDmEvent): open lock in muted gray — legacy encryption,
  metadata visible to relays

Matches Android's IncognitoBadge pattern. Both NIP-04 and NIP-17 messages
in the same 1-on-1 conversation produce identical ChatroomKeys, so they
merge into one conversation — the badge is the only way to tell them apart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:23:06 +02:00
nrobi144
f3e55f9958 fix(chats): handle ChatMessageEncryptedFileHeaderEvent in GiftWrap unwrap
The GiftWrap inner event handler only processed ChatMessageEvent (kind 14)
and ReactionEvent. ChatMessageEncryptedFileHeaderEvent (kind 15) was
silently dropped, so encrypted file attachments never appeared in DMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:11:46 +02:00
nrobi144
35b58e489b feat(chats): right-click context menu on encrypted files — download + forward
Right-click on encrypted file attachments in DMs now shows a context menu:
- "Save to disk" — decrypts and saves via native file dialog
- "Forward" — placeholder callback for forwarding to another conversation

Also caches decrypted bytes (not just image bitmap) so save doesn't
re-download. Non-image files also get decrypted on load for save support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:06:26 +02:00
nrobi144
d9848b868b feat(chats): always show attach button, auto-force NIP-17 with snackbar
- Attach button always visible in DM input (not gated on NIP-17 mode)
- Drag-and-drop always enabled
- When files are attached in NIP-04 mode, auto-switches to NIP-17
  with snackbar: "Switched to NIP-17 — file attachments require
  encrypted messaging"
- NIP-17 toggle remains for manual control

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:02:05 +02:00
nrobi144
9187bf448f fix(chats): pass compactMode through RootContent — single-pane keeps split
compactMode was hardcoded in the Messages branch of RootContent, which is
shared between deck and single-pane. Now compactMode is a RootContent
parameter: deck passes true, single-pane defaults to false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:53:03 +02:00
nrobi144
0184ebad11 fix(test): disambiguate mock upload call after ByteArray overload
The new ByteArray upload overload on DesktopBlossomClient made the
positional any() mock call ambiguous. Specify explicit type parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:51:22 +02:00
nrobi144
e06287acb3 feat(chats): stacked messages layout in deck columns
In multi-deck mode, Messages column now uses stacked navigation instead
of side-by-side split pane. Full-width contact list OR full-width chat —
clicking a conversation navigates to chat, back arrow returns to list.

Single-pane mode keeps the existing split layout (280dp list + flex chat).

Changes:
- Add compactMode param to DesktopMessagesScreen (default false)
- Extract SplitMessagesContent and CompactMessagesContent composables
- Add onBack callback to ChatPane with back arrow in header
- Remove hardcoded 280dp from ConversationListPane (caller controls width)
- Pass compactMode=true from deck RootContent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:49:36 +02:00
nrobi144
598256639b fix(chats): share single DesktopIAccount across deck — DMs now visible
DeckColumnContainer was creating its own DesktopIAccount (and ChatroomList)
for the Messages column, while Main.kt created a separate one for DM relay
subscriptions. Events were added to one ChatroomList but the UI read from
the other, so DMs never appeared.

Fix: thread the single iAccount from Main.kt through DeckLayout →
DeckColumnContainer → RootContent → DesktopMessagesScreen. Removed the
duplicate DesktopIAccount and DmSendTracker creation from RootContent.

Audited all other deck column types — no similar instance duplication found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:31:27 +02:00
nrobi144
24d4073b9b feat(media): encrypted file sharing in desktop DMs (NIP-17 Phase 6)
Add full send + receive encrypted media support in desktop DM chat:
- Paperclip attach button and drag-and-drop in ChatPane (NIP-17 mode only)
- AES-GCM encryption before upload to Blossom server
- ChatMessageEncryptedFileHeaderEvent (kind 15) wrapped in GiftWrap
- sendNip17EncryptedFile() added to IAccount interface and implementations
- DesktopUploadOrchestrator.uploadEncrypted() with proper encrypted hash
- DesktopBlossomClient ByteArray upload overload for encrypted blobs
- LRU cache in EncryptedMediaService to avoid re-downloading
- Error handling: retry on failure, disable send during upload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:53:36 +02:00
Vitor Pamplona
3e3adaff10 Finalizes the RelaySync utility 2026-03-18 17:35:05 -04:00
Vitor Pamplona
e57023cad9 Fix: Filters onCannotConnect to the relays that sent a req 2026-03-18 17:34:50 -04:00
davotoula
7bfc35e04d Fixes Applied
Code reuse
  - Extracted remuxTracks() helper to deduplicate video/audio remux loop (~40 lines)
  - Extracted stripAfterCompression() to deduplicate identical blocks in upload()/uploadEncrypted()
  - Replaced 8 identical showStrippingFailureDialog() copy-pastes with shared SuspendableConfirmation utility
  Privacy
  - 4 metadata ViewModels now error on strip failure instead of silently uploading unstripped media
  Efficiency
  - Eliminated temp input file copy for video/audio — MediaExtractor reads URIs directly
  Bug fixes
  - Fixed Long.toInt() overflow for MP3 files >2GB
  - Fixed MP3 temp file leak on exception and null InputStream paths
  Cleanup
  - Mutex field and wrapping the suspendCancellableCoroutine in mutex.withLock. Concurrent callers now queued
  - Converted MetadataStripper from class to object (stateless)
  - Moved StrippingFailureState from service layer to UI as generic ConfirmationCallbacks
2026-03-18 20:59:46 +01:00
davotoula
0cf256e5b2 Merge branch 'main-upstream' into claude/strip-file-metadata-sVIEd 2026-03-18 19:32:43 +01:00
davotoula
bfc04497d6 Profile picture upload fails with an error instead of silently uploading unstripped media when stripping fails 2026-03-18 19:25:57 +01:00
davotoula
eb13bab738 fix grammar 2026-03-18 18:31:34 +01:00
davotoula
97e6206244 fix persisted account preference 2026-03-18 18:31:07 +01:00
davotoula
a7ac30493c Extension mismatch fix
Temp file leak fix
2026-03-18 18:25:31 +01:00
davotoula
49c8423be5 moving the stripping decision after compressIfNeeded 2026-03-18 18:20:01 +01:00
davotoula
127079ede3 - Hide privacy toggle when video compression is selected (compression
already handles metadata stripping)
- Pass stripMetadata=false when toggle is hidden for compressed video
- Hide compression quality for audio and other non-compressible media types
- refactor
2026-03-18 13:54:15 +01:00
nrobi144
c90bf8f610 feat(media): add Note/Picture post type selector; fix gallery crash
- Post type toggle (Note vs Picture/kind 20) in compose dialog, only
  shown when image files are attached. Text input disabled in picture mode.
- Fix LazyVerticalGrid crash in GalleryTab: bounded height via
  fillParentMaxHeight() when nested inside LazyColumn.
- Phase 1 testing plan: all pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:42:44 +02:00
nrobi144
cf17dea53f feat(media): add Note/Picture post type selector in compose dialog
When images are attached, a Note/Picture toggle appears letting the user
publish as kind 20 (PictureEvent) instead of kind 1. Text input is
disabled in picture mode. Selector only shows for image file types.
Updates testing plan: Phase 1 all pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:37:48 +02:00
nrobi144
1d1d32da0e feat(media): add server selector to compose dialog; update testing plan
Server selector dropdown appears when files are attached, letting the
user choose which Blossom server to upload to. Updates testing plan:
Phase 2 & 3 all pass, Phase 9 audio tested with volume bug tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:26:47 +02:00
nrobi144
836e024bd7 feat(media): use VLC :start-volume for initial audio; document volume bug
Passes :start-volume media option to VLC on play. Removes polling/delay
volume hacks. Documents 9.7 volume bug in testing plan — VLC ignores
initial volume on macOS, needs further investigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:26:52 +02:00
nrobi144
f0c80f3c7c fix(media): delay volume enforcement — VLC audio output not ready at playing event
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:34:36 +02:00
nrobi144
11b9fdad80 fix(media): enforce volume on playback start — VLC resets volume on new media
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:30:57 +02:00
nrobi144
2a6087af27 feat(media): global media player — persistent playback across navigation
Media playback now survives navigation. A GlobalMediaPlayer singleton owns
VLC players and exposes StateFlows. Composables are thin viewports.
NowPlayingBar has full controls (volume, mute, save, fullscreen).
GlobalFullscreenOverlay renders video fullscreen above all screens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:27:13 +02:00
davotoula
20ab105e44 feat: add metadata stripping failure dialog to all upload ViewModels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 11:12:36 +01:00
davotoula
c0632e17bd feat: add onStrippingFailed callback to upload orchestrators
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:51:30 +01:00
davotoula
9ce1ff3e63 feat: plumb stripMetadata from UI through ViewModels to upload orchestrators
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:50:16 +01:00
davotoula
3b1b15911f feat: rename stripLocationMetadata to stripMetadata in UI state and dialogs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:42:59 +01:00
davotoula
c76459859d feat: update all Screen call-sites for new stripMetadata onAdd parameter
Add 6th Boolean parameter to onAdd lambdas in all 6 files that call
ImageVideoDescription. The parameter is unused (_) for now in 5 sites
and named in ShortNotePostScreen for future plumbing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:38:04 +01:00
davotoula
0d514a46d7 feat: add privacy toggle to ImageVideoDescription
Add stripMetadata state and SettingSwitchItem toggle. Extend onAdd
callback signature with 6th Boolean parameter for stripMetadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:36:56 +01:00
davotoula
f7a9389a66 feat: add string resources for privacy toggle and metadata dialog
Rename strip_location_metadata_label/description to strip_metadata_label/description
to reflect broader scope. Add new strings for the metadata stripping failure dialog.
Update references in NewMediaView.kt and ChatFileUploadDialog.kt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:36:03 +01:00
davotoula
24e8487db6 fix: hide codec toggle when video compression is set to uncompressed
When the compression quality slider is set to UNCOMPRESSED (value 3),
no encoding happens so the H264/H265 codec choice is irrelevant. Hide
the codec toggle in this case to avoid confusion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:33:35 +01:00
davotoula
0e62904883 fix: hide compression slider for audio uploads
Audio has no compression support in MediaCompressor (falls through to
no-op), so showing the compression quality slider for audio files is
misleading. This removes audio from the slider visibility condition in
ImageVideoDescription and wraps the unconditionally-shown slider in
ChatFileUploadDialog with a media type check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:33:28 +01:00
davotoula
489e731eea feat: add MP3 ID3 tag stripping and early-exit for unsupported audio
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:29:09 +01:00
davotoula
b9d62edaad feat: support multiple tracks in audio metadata stripping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:28:13 +01:00
davotoula
7219c787a6 fix: update direct MetadataStripper.strip() call sites for StrippingResult
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:26:02 +01:00
davotoula
4fc5166815 feat: propagate StrippingResult through UploadOrchestrator
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:25:55 +01:00
davotoula
f9176fc3fb feat: add StrippingResult data class to MetadataStripper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:25:50 +01:00
nrobi144
6e1315a4fa feat(media): improve video player pool, thumbnail extraction, and NoteCard layout
Harden VLC player pool with dedicated thumbnail acquisition, better error
logging, and defensive buffer checks. Simplify lightbox/video controls,
fix NoteCard media sizing across feed, thread, bookmarks, and search screens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:24:00 +02:00
Vitor Pamplona
f25be5e7bd removes the benchmark action... it's not that useful. 2026-03-17 16:00:46 -04:00
Vitor Pamplona
bc5b254d01 merge 2026-03-17 15:57:44 -04:00
Vitor Pamplona
62811282a1 Merge pull request #1871 from vitorpamplona/claude/reorganize-nip47-wallet-mbVxR
Refactor NIP47 WalletConnect into organized subpackages
2026-03-17 15:47:51 -04:00
Vitor Pamplona
3e19195e4a Renaming and making a test case that considers limits.
Fixes active filter index bug
2026-03-17 15:45:52 -04:00
davotoula
72158e8fc2 Add rotation to stripped video 2026-03-17 20:27:15 +01:00
davotoula
e5e51c9c42 REMUX_BUFFER_SIZE = 8MB — extracted as a companion constant (was 1024 * 1024 inline in both video and audio). 8MB prevents silent frame loss on 4K keyframes.
stripAudioMetadata try/finally — now mirrors stripVideoMetadata exactly: muxer/extractor always released, incomplete output file deleted on failure.
2026-03-17 20:09:33 +01:00
Claude
5029db2302 refactor: reorganize nip47 wallet connect package into subpackages
Split the flat nip47WalletConnect package into logical subpackages:
- rpc/ - JSON-RPC protocol models (Request, Response, Notification, NwcMethod, NwcErrorCode, NwcTransaction, etc.)
- events/ - Nostr event types (LnZapPaymentRequestEvent, LnZapPaymentResponseEvent, NwcInfoEvent, NwcNotificationEvent)
- cache/ - Decryption caches (NostrWalletConnectRequestCache, NostrWalletConnectResponseCache)

Root level keeps the entry points: Nip47WalletConnect (URI parsing), Nip47Client, Nip47Server.
Existing subpackages (tags/, kotlinSerialization/, jackson/) remain unchanged.

https://claude.ai/code/session_018YcwMeTHPXmeqpbZHcBEUg
2026-03-17 19:04:24 +00:00
davotoula
d87bf37138 added comment about limitations 2026-03-17 19:53:57 +01:00
davotoula
e7d11fdc84 add Track interleaving 2026-03-17 19:51:58 +01:00
davotoula
a2f29d5e2e Early return with the original uri if there are no tracks 2026-03-17 19:43:16 +01:00
davotoula
4cf3a84190 delete orphaned output file on failure 2026-03-17 19:42:18 +01:00
davotoula
b61e1d5779 fix resource leak(s) on exception 2026-03-17 19:40:12 +01:00
Vitor Pamplona
b8351b8e29 Merge branch 'main' into claude/event-sync-screen-sYGtN 2026-03-17 14:17:13 -04:00
Vitor Pamplona
18fdfcc9d8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  adaptability: use constant instead of repeated string literals
2026-03-17 14:10:45 -04:00
Vitor Pamplona
b22aa1ad0f Shows default banner if the user's image is not available 2026-03-17 14:02:09 -04:00
Vitor Pamplona
7f5baab5df Adds last seen to the user profile 2026-03-17 13:58:07 -04:00
davotoula
76f8130d40 Merge branch 'main-upstream' into claude/strip-file-metadata-sVIEd 2026-03-17 18:47:35 +01:00
Vitor Pamplona
aaa3bcfb2a Merge pull request #1870 from vitorpamplona/claude/fix-event-counter-per-filter-Aps6R
Add per-filter event counting for relay pagination
2026-03-17 13:46:10 -04:00
Claude
308e9fcb9e chore: remove unused coroutineContext import in EventSync
https://claude.ai/code/session_01Baaira4xoEHEsaSQCX52Sy
2026-03-17 17:43:04 +00:00
davotoula
a6c153a154 adaptability: use constant instead of repeated string literals 2026-03-17 18:42:39 +01:00
Claude
76c00dea3c fix: track event counts per filter in downloadFromRelay
Change the event counter from a single global counter to per-filter
tracking using Filter.match(). Each filter's limit is now respected
individually - fulfilled filters are excluded from subsequent pages,
and pagination stops when all filters with limits are satisfied.

https://claude.ai/code/session_01Baaira4xoEHEsaSQCX52Sy
2026-03-17 17:41:40 +00:00
Vitor Pamplona
504a218eee Merge branch 'main' into claude/event-sync-screen-sYGtN 2026-03-17 13:41:34 -04:00
Vitor Pamplona
97758a5163 fixes build 2026-03-17 13:35:50 -04:00
Vitor Pamplona
dd9e57542f Merge branch 'main' into claude/event-sync-screen-sYGtN 2026-03-17 13:28:36 -04:00
Vitor Pamplona
985cab73cd Merge branch 'main' into claude/strip-file-metadata-sVIEd 2026-03-17 13:27:00 -04:00
Vitor Pamplona
843058ce52 Merge pull request #1869 from vitorpamplona/claude/move-delete-button-topbar-SlZVz
Refactor DraftListScreen UI with top app bar improvements
2026-03-17 13:26:23 -04:00
Vitor Pamplona
91994a0b74 Merge branch 'main' into claude/strip-file-metadata-sVIEd 2026-03-17 13:25:54 -04:00
Vitor Pamplona
acb8068606 Merge pull request #1866 from vitorpamplona/claude/auto-load-transactions-scroll-KC8of
Add transaction filtering and pagination to wallet screen
2026-03-17 12:06:31 -04:00
Vitor Pamplona
cc98ac72c1 Revert to 20 since Alby only replies if limit is 20. 2026-03-17 12:04:10 -04:00
Vitor Pamplona
3764cd6d7f missing imports 2026-03-17 11:51:02 -04:00
Vitor Pamplona
e66e5e28c1 Merge branch 'main' into claude/auto-load-transactions-scroll-KC8of 2026-03-17 11:48:21 -04:00
Vitor Pamplona
c027e70c8b Merge pull request #1849 from vitorpamplona/claude/file-encryption-toggle-PwA8W
Add encrypted file upload with fallback option for NIP17 chats
2026-03-17 11:45:25 -04:00
Vitor Pamplona
30b785eec9 Merge branch 'main' into claude/file-encryption-toggle-PwA8W 2026-03-17 11:45:17 -04:00
Vitor Pamplona
4770f1eb90 Fixes sending url without cipher. 2026-03-17 11:43:24 -04:00
Claude
034cc0ab6d feat: move downloadFromRelay pagination logic to Quartz library
Extract the paginated relay download into a reusable INostrClient
extension (NostrClientDownloadFromRelayExt.kt) in the Quartz
commonMain accessories package. Uses Channel-based event collection
for KMP compatibility (no JVM-only atomics).

EventSyncViewModel.downloadFromRelay now delegates to the Quartz
extension, keeping the ViewModel thin.

Add NostrClientDownloadFromRelayTest integration test against
wss://nos.lol with Filter(kinds = [MetadataEvent.KIND], limit = 10)
to verify paginated downloads return correctly-typed events.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-17 15:34:17 +00:00
Claude
bb32962f7d fix: prevent infinite loading spinner on wallet transactions screen
- Convert filteredTransactions from cold Flow to StateFlow via stateIn()
  to ensure reliable state updates with collectAsState()
- Add 30s timeout to NWC requests (fetchBalance, fetchTransactions,
  loadMoreTransactions) so loading state resets if wallet doesn't respond

https://claude.ai/code/session_01CdNY7qYDRHJr1jhAFvVgzj
2026-03-17 15:29:37 +00:00
Vitor Pamplona
e0dc4edf7a Merge branch 'main' into claude/event-sync-screen-sYGtN 2026-03-17 11:21:54 -04:00
nrobi144
d70b13ef33 fix(media): properly constrain video height within NoteCard
Replace aspectRatio modifier with BoxWithConstraints that manually
calculates height from aspect ratio and clamps it to the max height
constraint. Video now stays within its allocated space in the card,
so controls are accessible and content below is not overlapped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:09:40 +02:00
Vitor Pamplona
45e0dc9bd5 renames class and other small changes 2026-03-17 11:02:30 -04:00
nrobi144
fbde0052a8 fix(media): video respects max height, stays within NoteCard bounds
Remove fillMaxWidth before aspectRatio so the caller's heightIn(max)
constraint is respected. Video now sizes to fit within the NoteCard
without overlapping the author header or overflowing the card.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:56:42 +02:00
nrobi144
9a19148da8 fix(media): stable hover controls, fix text cutoff
- Single hover zone on entire video — no more flickering from competing
  zones. Play button always visible when paused, bottom controls appear
  on hover (works in fullscreen too)
- Media height capped to 50% of window (min 200dp) so post text is
  never pushed off-screen by large media

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:51:44 +02:00
nrobi144
248f38cc47 fix(media): zone-based video controls — play always visible, controls on hover
- Center play button always visible when paused
- Pause button appears on center hover when playing
- Bottom controls bar only appears when hovering the bottom area
- Controls also show when paused for discoverability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:32:08 +02:00
nrobi144
e9249c924c fix(media): only show video controls on hover
Controls (play button, seek bar, volume) are hidden by default and
appear when the mouse enters the video area. They auto-hide 2s after
the mouse leaves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:27:57 +02:00
nrobi144
bce6c12ab3 feat(media): video thumbnails in feed via VLC first-frame extraction
Add VideoThumbnailCache that grabs the first rendered frame from VLC
and caches it in memory. Videos in the feed now show a thumbnail
preview instead of a blank background before the user clicks play.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:12:16 +02:00
Vitor Pamplona
baa55ab7f9 reverts to amber's session start 2026-03-17 09:55:56 -04:00
nrobi144
89f39fa77e feat(search): show images and media in search results
Replace plain-text NotePreviewCard with full NoteCard in search results
so images, videos, and author avatars are visible inline without having
to open the note.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:40:48 +02:00
nrobi144
d091084064 fix(media): resize media to fit visible window, never truncate text
- Remove maxLines/TextOverflow from RichTextContent so post text is
  never truncated
- Media height = window height minus 200dp chrome, so each image/video
  fits in the visible area without scrolling
- Use ContentScale.Fit for images to scale down while keeping aspect
  ratio instead of filling width and overflowing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:33:54 +02:00
nrobi144
bdd1feae3e fix(ui): show full post text, scrollable profile header with floating bar
- Remove maxLines=10 on post text so content is never truncated
- Profile header/card/tabs now scroll with content instead of staying
  sticky — scrolls away naturally as you browse posts
- Floating compact header (back + avatar + name) slides in when
  scrolling up and the full header is out of view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:26:20 +02:00
Vitor Pamplona
9bf3086230 Make the session-start executable by default 2026-03-17 09:25:33 -04:00
Vitor Pamplona
345a569115 Merge pull request #1867 from vitorpamplona/claude/add-apk-pr-comments-gR7m7
Add direct APK download link and PR comments to benchmark workflow
2026-03-17 09:01:12 -04:00
nrobi144
2aa84c98ad fix(media): cap feed media height to 70% of window height
Replace hardcoded 400.dp max with window-relative height so media
(images and videos) never overflow the visible area and controls
are always accessible without scrolling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:53:58 +02:00
nrobi144
dffb996335 fix(media): immersive fullscreen via GraphicsDevice, separate view mode buttons
Replace WindowPlacement.Fullscreen (macOS maximize-only) with
GraphicsDevice.setFullScreenWindow() for true exclusive fullscreen that
hides menu bar and dock. Split single cycling view mode button into two
distinct toggles (theater + fullscreen). Hide all navigation chrome
(sidebar, nav rail, dividers, arrows, overlay menus) during fullscreen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:49:39 +02:00
davotoula
f10387025d spotlessApply 2026-03-17 07:27:56 +01:00
nrobi144
7d3d633e03 feat(media): bundle VLC, lazy video player, fullscreen with seek continuity
- Bundle VLC via ir.mahozad.vlc-setup plugin (all plugins for network playback)
- BundledVlcDiscoverer (Win/Linux) and MacOsVlcDiscoverer for bundled VLC discovery
- Lazy player activation — no VLC calls during composition, only on play click
- Pre-init VLC on background thread at startup
- Video controls: seek slider, volume, mute, fullscreen (two-row layout, no clipping)
- Buffering indicator (CircularProgressIndicator) while loading
- ActiveMediaManager enforces single-player — pauses others when new video plays
- Fullscreen passes seek position so lightbox resumes from current playback point
- LightboxOverlay renders videos (DesktopVideoPlayer) or images (ZoomableImage)
- Gitignore downloaded VLC binaries (appResources/{linux,macos,windows}/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 07:23:42 +02:00
Claude
dbf5d6954c fix: revert to artifact zip download, PR comment only
Use upload-artifact with artifact-id for direct zip download URL.
Remove prerelease approach and commit comments.

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 04:08:30 +00:00
Claude
706d282681 fix: direct APK download via prerelease asset, PR comment only
- Upload APK as a GitHub prerelease asset for direct .apk download
  (no zip wrapping like artifacts)
- Remove commit comment, only comment on open PRs
- Clean up existing release with same tag before creating

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 04:05:33 +00:00
Claude
61dc903e9a fix: use artifact ID for direct download URL instead of nightly.link
Use the artifact-id output from upload-artifact to construct a direct
GitHub artifact download URL. No third-party service needed — the URL
points to the artifact on GitHub which triggers a download when clicked.

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 04:03:18 +00:00
Vitor Pamplona
13da04e9f1 Merge pull request #1868 from vitorpamplona/claude/fix-missing-plugin-adHgr
Fix proxy bypass and improve code formatting
2026-03-16 23:51:49 -04:00
Claude
6bc927147c fix: use nightly.link for direct APK download without auth
GitHub artifact URLs require authentication and don't provide direct
file downloads. nightly.link proxies artifact downloads, giving a
direct download link that works for anyone without GitHub login.

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 03:47:47 +00:00
nrobi144
cd7ee69f50 feat(media): add imeta tags on upload and drag-and-drop
- Build IMetaTag (NIP-92) from upload metadata (mime, sha256, dim, blurhash, size)
- Use TextNoteEvent.build directly with tag initializer for imeta injection
- Add drag-and-drop file support via Compose DragAndDropTarget
- Filter dropped files by media extension whitelist
- Visual border highlight on drag-over state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
adecc35721 feat(media): wire profile gallery tab and lightbox
- Add Notes/Gallery PrimaryTabRow to UserProfileScreen
- Subscribe to PictureEvent (kind 20) for gallery grid
- Wire GalleryTab composable with image click callbacks
- Wire LightboxOverlay for full-screen image viewing
- Pass onImageClick to FeedNoteCard in notes tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
7773844d2f feat(media): wire encrypted media, server settings, and server preferences
- ChatPane: display ChatFileAttachment for encrypted file header events
- DesktopPreferences: add blossom server list persistence
- Settings: add MediaServerSettings section above relay settings
- ComposeNoteDialog: use preferred blossom server from preferences

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
e654d48cdf fix(media): code review — dead code removal, perf fixes, bug fixes
- Remove unused: MediaAspectRatioCache, VideoPlayerState, resetZoom,
  onDoubleClick, delete/headUpload/createDeleteAuth, encryptAndUpload,
  EncryptedUploadResult, unused options params, formatAudioTime dupe
- Fix VlcjPlayerPool.release() accumulating stale event listeners
- Fix SaveMediaAction FileDialog running on IO instead of EDT
- Pre-allocate video frame ByteArray to avoid ~240MB/s GC pressure
- Pool audio players in VlcjPlayerPool instead of factory-per-instance
- Remove listener in onDispose before returning player to pool
- Fix acquire() race condition by moving poll inside synchronized
- Reduce memory cache to 15%/256MB with weak references
- Parallelize server health checks
- Remove redundant Content-Type/Content-Length headers
- Deduplicate BlurHashFetcher bitmap conversion via bufferedImageToSkiaBitmap
- Hoist audioExtensions to top-level constant, rename allMediaUrls
- Remove nonfunctional Save button and dead decryptedBytes state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
d5facd90df test(media): add 49 unit tests for media upload, encryption, and server services
Coverage for DesktopMediaMetadata, DesktopUploadTracker, DesktopMediaCompressor,
DesktopBlossomClient (MockK), DesktopUploadOrchestrator (MockK + mockkObject),
ServerHealthCheck, and AESGCM encrypt/decrypt round-trip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
dfaca39528 feat(media): inline audio playback with VLCJ no-video mode
Phase 9: AudioPlayer composable using VLCJ --no-video factory,
play/pause/seek controls, NoteCard splits audio from video URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
4450aacdf9 feat(media): Blossom server management settings with health checks
Phase 8: MediaServerSettings UI for add/remove/reorder Blossom servers,
ServerHealthCheck with 5s timeout HEAD requests, status indicators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
ca4e51afa9 feat(media): NIP-68 picture display and profile gallery grid
Phase 7: PictureDisplay composable for kind 20 events with image-first
layout, GalleryTab with adaptive grid of thumbnails, lightbox support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
f0acbb1934 feat(media): encrypted media service for NIP-17 DM file sharing
Phase 6: AESGCM encrypt/decrypt for DM file attachments, Blossom
upload of encrypted files, ChatFileAttachment composable with
auto-decrypt for images and file type display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
1a2dc9efac feat(media): lightbox overlay with zoom, gallery navigation, and save
Phase 5: Full-screen lightbox overlay with mouse wheel zoom + pan,
left/right gallery carousel, keyboard shortcuts (Esc/arrows/Ctrl+S),
save to disk via FileDialog. Images in NoteCard are clickable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
405601463e feat(media): VLCJ video playback with DirectRendering and player pool
Phase 4: VLCJ player pool (max 3 instances), DirectRendering via
CallbackVideoSurface → Skia Bitmap → Compose Image. Video controls
with auto-hide, seek, play/pause. Graceful fallback when VLC missing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
2181407647 feat(media): desktop upload UX with file picker, clipboard paste, and attachment row
Phase 3: Native file dialog, AWT clipboard paste handler, media
attachment UI with thumbnails, and ComposeNoteDialog media integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
443f7c6a09 feat(media): Phase 2 — Desktop Blossom upload client
- DesktopBlossomClient: PUT /upload, DELETE, HEAD /upload (BUD-06)
- DesktopBlossomAuth: kind 24242 auth with base64 Nostr header
- DesktopMediaMetadata: SHA-256, dimensions, blurhash via ImageIO
- DesktopMediaCompressor: lossless EXIF stripping (Commons Imaging)
- DesktopUploadOrchestrator: strip EXIF → metadata → auth → upload
- DesktopUploadTracker: StateFlow-based upload state tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
nrobi144
212dda40ca feat(media): Phase 0+1 — Coil3 image loading + inline images in notes
- DesktopImageLoaderSetup: Coil3 with explicit memory cache (25% heap,
  max 512MB), persistent disk cache (OS-appropriate paths), SVG decoder
- DesktopBlurHashFetcher: blurhash placeholder images via Skia Bitmap
- DesktopBase64Fetcher: base64 data: URI image decoding
- SkiaGifDecoder: GIF first-frame rendering via Skia Codec
- MediaAspectRatioCache: thread-safe LruCache for aspect ratios
- NoteCard: inline AsyncImage for image URLs, stripped from text
- Added coil-compose, coil-okhttp, coil-svg, vlcj, commons-imaging,
  androidx-collection deps to desktopApp

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:44:33 +02:00
Claude
883a2f995e style: apply ktlint formatting fixes
https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1
2026-03-17 03:43:19 +00:00
Claude
1c4f9769b1 fix: session-start hook fails silently when dl.google.com is blocked
The Claude Code web egress proxy does not include dl.google.com in its
allowlist, causing the session-start hook to abort entirely (due to
set -euo pipefail) when Android SDK downloads fail. This cascading
failure also prevented local.properties creation and ANDROID_HOME export.

Additionally, JAVA_TOOL_OPTIONS contains nonProxyHosts entries for
*.google.com and *.googleapis.com, which causes the JVM to bypass the
proxy for Google Maven, resulting in connection failures.

Changes:
- Remove set -euo pipefail; use per-section error handling instead
- Fix JAVA_TOOL_OPTIONS: strip *.google.com from nonProxyHosts
- Fix no_proxy: remove *.google.com and *.googleapis.com entries
- Move local.properties and env exports before SDK download section
- Make SDK downloads non-fatal with clear warning messages
- Install standalone ktlint as fallback for spotlessApply
- Create spotless-apply wrapper script for proxy-blocked environments
- Update Stop hook to fall back to spotless-apply when Gradle fails

https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1
2026-03-17 03:43:10 +00:00
Claude
af12d13d67 fix: use artifact-url output for direct APK download link
The manually constructed URL was invalid. Use the artifact-url output
from upload-artifact which provides the correct download URL.

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 03:34:53 +00:00
Claude
39e15a8929 feat: comment benchmark APK link on open PRs
Add PR commenting to the build-benchmark-apk workflow. When a build
completes, it now finds any open PR for the branch and posts a comment
with a direct download link to the benchmark APK artifact.

https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf
2026-03-17 03:23:00 +00:00
Claude
866dfca710 fix: rename _transactions to allTransactions to fix ktlint backing-property-naming
The backing property _transactions no longer has a matching public
property since it was replaced by filteredTransactions.

https://claude.ai/code/session_01CdNY7qYDRHJr1jhAFvVgzj
2026-03-17 03:07:57 +00:00
Vitor Pamplona
73d1e39587 Merge branch 'main' into claude/auto-load-transactions-scroll-KC8of 2026-03-16 23:03:09 -04:00
Claude
856e85988a feat: improve Android push notifications with modern notification APIs
- Use MessagingStyle for DM notifications with sender avatars and
  conversation formatting (integrates with Android Bubbles/conversation space)
- Add Direct Reply action so users can respond to DMs from the
  notification shade without opening the app
- Add Mark as Read action with SEMANTIC_ACTION_MARK_AS_READ for
  Android Auto/Wear OS integration
- Apply notification grouping with InboxStyle summary notifications
  for both DMs and Zaps (group keys were defined but never used)
- Use BigTextStyle for zap notifications so long messages expand
- Set proper notification categories (CATEGORY_MESSAGE for DMs,
  CATEGORY_SOCIAL for zaps) for OS-level prioritization and DND bypass
- Fix PendingIntent flag from FLAG_MUTABLE to FLAG_IMMUTABLE where
  mutability is not needed (security improvement)
- Replace blocking image load (executeBlocking) with suspend-friendly
  Coil execute() on IO dispatcher
- Upgrade DM channel importance to IMPORTANCE_HIGH for heads-up display

https://claude.ai/code/session_012w6K8H4nvZYxeRryW2USAp
2026-03-17 03:00:08 +00:00
Claude
11950dc4f4 feat: add transaction filter for zaps vs non-zaps on wallet screen
Adds filter chips (All / Zaps / Non-Zaps) to the transactions list.
Zap transactions are identified by having nostr metadata. The filter
is applied client-side via a combined Flow in WalletViewModel.

https://claude.ai/code/session_01CdNY7qYDRHJr1jhAFvVgzj
2026-03-17 02:55:16 +00:00
Claude
c3f7e491a6 feat: increase wallet transaction page size to 100
https://claude.ai/code/session_01CdNY7qYDRHJr1jhAFvVgzj
2026-03-17 02:34:15 +00:00
Vitor Pamplona
7a3b1afacc Merge pull request #1865 from vitorpamplona/claude/add-apk-benchmark-action-5c4UX
Add GitHub Actions workflow for building benchmark APK
2026-03-16 22:33:15 -04:00
Claude
61e85df8db feat: post commit comment when benchmark APK is ready
Adds a GitHub Script step that comments on the commit with a direct
link to the workflow artifacts, making it visible in PRs and branch
pages.

https://claude.ai/code/session_013hcKzBgJgfsrArSgTTGNEk
2026-03-17 02:27:53 +00:00
Claude
146f3f91b5 feat: add GitHub Action to build benchmark APK on claude branches
Adds a lightweight workflow that triggers on pushes to claude/* branches,
building only the Play Benchmark APK for quick testing after Claude
completes a task.

https://claude.ai/code/session_013hcKzBgJgfsrArSgTTGNEk
2026-03-17 02:20:47 +00:00
Claude
0459633d82 feat: move delete all button to top bar as icon in drafts screen
Replace the ElevatedButton in a sticky header with a delete icon
(Icons.Default.Delete) in the top bar actions area. The confirmation
dialog is preserved and now lives at the screen level, reading the
current feed state directly when confirmed.

https://claude.ai/code/session_01AbiBeDnT4KPKD16sKKaLD8
2026-03-17 02:20:13 +00:00
Claude
4a430e6663 feat: auto-load wallet transactions on scroll
Add infinite scroll pagination to the wallet transactions screen.
When the user scrolls within 5 items of the bottom, the next page of
transactions is automatically fetched and appended. Uses the existing
NWC list_transactions limit/offset support and total_count to know
when all transactions have been loaded.

https://claude.ai/code/session_01CdNY7qYDRHJr1jhAFvVgzj
2026-03-17 02:09:44 +00:00
Vitor Pamplona
d0297b7e34 Fixes stability plugin for AGP9 2026-03-16 19:44:51 -04:00
Vitor Pamplona
56cce46858 Solving all problems with Quartz tests 2026-03-16 19:22:28 -04:00
Vitor Pamplona
962da3280b Moves the N44 test to Jvmtest 2026-03-16 18:57:00 -04:00
Vitor Pamplona
b0c714176a Updates versions for github actions 2026-03-16 18:46:39 -04:00
Vitor Pamplona
6ffc902598 Unifies UriParser 2026-03-16 18:15:43 -04:00
Vitor Pamplona
790fd45470 Removes warnings 2026-03-16 18:10:55 -04:00
Vitor Pamplona
f708f6c265 Specify which .def file is used between simulators and ios Devices 2026-03-16 17:46:20 -04:00
Vitor Pamplona
7f61b4819b new log fuctions mocked 2026-03-16 17:45:05 -04:00
Vitor Pamplona
e7760dc7c7 adds bundleId to the binary file 2026-03-16 17:16:18 -04:00
Vitor Pamplona
12c27af813 cancel other running builds in github actions when a new one comes up 2026-03-16 17:16:05 -04:00
Vitor Pamplona
7e2912ffd8 removes more warnings 2026-03-16 17:15:33 -04:00
Vitor Pamplona
874d750bf0 removing unused import 2026-03-16 17:01:59 -04:00
Vitor Pamplona
95c7adc90b Less warnings 2026-03-16 16:53:17 -04:00
Vitor Pamplona
b96c1c2c3a Fixes iosGzip 2026-03-16 16:38:31 -04:00
Vitor Pamplona
b916817247 Finishes serialization features for NIP-47 2026-03-16 16:35:05 -04:00
Vitor Pamplona
2d959b0108 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  drawerGesturesEnabled is true whenever the drawer is in transition (target != current)
  Split the logic between onPreScroll and onPostScroll
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  Unnecessary ?. / ?: / !! on non-null Missing @OptIn annotations Parameter name mismatches Icons.Filled.*  / Icons.AutoMirrored.* No cast needed / remove inline LocalLifecycleOwner import fix NIP-51 name() → title() Chess gameId → startEventId Nullable DecimalFormat safe calls DelicateCoroutinesApi opt-in
  use 50/50 split for pager vs dock zones
  fix: allow swipe-to-close when drawer is open on pager screens
2026-03-16 16:28:55 -04:00
Vitor Pamplona
abf2151bac Damus' relay is way too restrictive 2026-03-16 16:28:42 -04:00
Vitor Pamplona
56f7d83b5b Merge pull request #1860 from davotoula/swipe-on-main-screen
Swipe to switch tabs. Main screen and messages
2026-03-16 16:14:53 -04:00
Vitor Pamplona
dd55827486 Merge pull request #1863 from davotoula/reduce-compilation-warnings
Reduce compilation warnings
2026-03-16 16:14:38 -04:00
David Kaspar
645affd511 Merge pull request #1864 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-16 18:34:37 +00:00
Crowdin Bot
1a671fd0b4 New Crowdin translations by GitHub Action 2026-03-16 18:27:11 +00:00
David Kaspar
0c8a462ddf Merge pull request #1862 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-16 18:25:26 +00:00
davotoula
9d95817b4d drawerGesturesEnabled is true whenever the drawer is in transition (target != current) 2026-03-16 19:04:01 +01:00
davotoula
bf6ee6dc87 Split the logic between onPreScroll and onPostScroll 2026-03-16 19:01:51 +01:00
Crowdin Bot
21b6f9c67e New Crowdin translations by GitHub Action 2026-03-16 17:50:19 +00:00
David Kaspar
d9c95de736 Merge pull request #1861 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-16 17:48:10 +00:00
Vitor Pamplona
7c22c4e6c5 Migrates to collections 1.6, which deprecates iOS Intel targets. 2026-03-16 13:45:01 -04:00
Crowdin Bot
1af1383428 New Crowdin translations by GitHub Action 2026-03-16 17:09:41 +00:00
Vitor Pamplona
ea6116cd5c Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  Fix swipe-back triggering unwanted draft deletion signing
  fix: revert ReactionsSettingsScreen to upstream version
  fix(nav): preserve screen state across overlay navigation
  feat(search): collapsible section headers + sorting + search improvements
  fix(ui): remove duplicate back button from SinglePaneLayout
  refactor(search): extract thread-safe EventDeduplicator, unify dedup
  fix(search): loading indicator, result delivery, and history race
  fix(search): cleanup review findings — dead code, FQN refs, naming
  refactor(search): extract shared code to commons, remove dead code
  feat(search): add keyboard shortcuts, search history UI, and operator hints
  feat(search): add SearchHistoryStore with recent queries and saved searches
  feat(search): add advanced search UI with panel, results list, and SearchScreen rewrite
  feat(search): add AdvancedSearchBarState with bidirectional sync
  feat(search): add SearchFilterFactory and SearchResultFilter
  feat(search): add query engine — SearchQuery, QueryParser, QuerySerializer, KindRegistry
2026-03-16 13:02:52 -04:00
Vitor Pamplona
81ecaa82ff Migrates to vico charts 3.0 2026-03-16 12:53:54 -04:00
davotoula
588cfe4c96 Unnecessary ?. / ?: / !! on non-null
Missing @OptIn annotations
Parameter name mismatches
Icons.Filled.*  / Icons.AutoMirrored.*
No cast needed / remove inline
LocalLifecycleOwner import fix
NIP-51 name() → title()
Chess gameId → startEventId
Nullable DecimalFormat safe calls
DelicateCoroutinesApi opt-in
2026-03-16 16:23:08 +01:00
davotoula
844847d871 use 50/50 split for pager vs dock zones 2026-03-16 15:40:59 +01:00
davotoula
2a9c9e4510 fix: allow swipe-to-close when drawer is open on pager screens
Keep drawer gestures enabled when the drawer is open so users can
swipe left to close it. Only disable gestures when the drawer is
closed (to let the pager and ZonedSwipeModifier handle gestures).
2026-03-16 15:40:40 +01:00
Vitor Pamplona
714afdc2a8 Update dependencies 2026-03-16 09:35:41 -04:00
Nathan Day
4491f3aedb Merge pull request #1 from dadofsambonzuki/fix/dm-swipe-back-signing
Fix swipe-back triggering unwanted draft deletion signing
2026-03-16 12:25:52 +00:00
Vitor Pamplona
7f6219c846 Merge pull request #1859 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-16 08:19:13 -04:00
Crowdin Bot
be09535cd6 New Crowdin translations by GitHub Action 2026-03-16 12:17:53 +00:00
Vitor Pamplona
2acf02945b Merge pull request #1840 from nrobi144/feat/desktop-advanced-search
feat(desktop): advanced search with NIP-50, collapsible sections, and nav state preservation
2026-03-16 08:14:18 -04:00
Vitor Pamplona
f5d4d967eb Merge pull request #1858 from dadofsambonzuki/fix/dm-swipe-back-signing
Fix swipe-back triggering unwanted draft deletion signing
2026-03-16 08:12:56 -04:00
Nathan Day
1c663cb26b Fix swipe-back triggering unwanted draft deletion signing
When swiping back on a DM screen with an empty message, the BackHandler
was calling sendDraftSync() which checks if the message is blank and then
calls deleteDraftIgnoreErrors(). This creates and signs a NIP-09 DeletionEvent,
prompting the external signer to sign a deletion event.

The fix adds a check to only call sendDraftSync() when there's actual
content in the message, avoiding the unnecessary signing prompt.
2026-03-16 10:32:18 +00:00
davotoula
c56665d4dd spotlessApply 2026-03-16 10:51:24 +01:00
nrobi144
e4b4606ab9 fix: revert ReactionsSettingsScreen to upstream version
Spotless reformatted this file during merge. Restoring upstream's
version to avoid unintended changes to the mobile app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 07:35:09 +02:00
Vitor Pamplona
78ffd5ec6b Moves nip06 tests to the right package. 2026-03-15 21:32:02 -04:00
Vitor Pamplona
d426dbd4ff adds icons for desktop releases 2026-03-15 21:10:59 -04:00
Vitor Pamplona
04b4429809 removes another deprecation 2026-03-15 20:57:46 -04:00
Vitor Pamplona
d10b43c098 Migrates old Preview annotation 2026-03-15 20:55:55 -04:00
Vitor Pamplona
8b4a45cb5b Removing deprecated library addresses and deprecated apis 2026-03-15 20:52:01 -04:00
Vitor Pamplona
bda7e48a84 removes more warnings 2026-03-15 20:33:31 -04:00
Vitor Pamplona
e87ac7468d Removing Locale deprecations 2026-03-15 20:29:05 -04:00
Vitor Pamplona
49f5d9518e Fixes warnings 2026-03-15 20:26:31 -04:00
Vitor Pamplona
19ca7afcb8 fixes warnings 2026-03-15 20:06:20 -04:00
Vitor Pamplona
1cdd061b06 removing warnings 2026-03-15 20:03:39 -04:00
Vitor Pamplona
929b5d6175 Merge pull request #1853 from vitorpamplona/claude/add-relay-event-counters-c7stK
Add NIP-45 COUNT query support to relay management UI
2026-03-15 19:58:19 -04:00
Vitor Pamplona
45b947ec68 Merge branch 'main' into claude/add-relay-event-counters-c7stK 2026-03-15 19:58:12 -04:00
Vitor Pamplona
88565d02ad Merge pull request #1856 from vitorpamplona/claude/cross-platform-github-actions-xeNlt
Refactor CI/CD workflows for multi-platform builds
2026-03-15 19:57:59 -04:00
Vitor Pamplona
0e476c8988 Adds test and some refinements. 2026-03-15 19:51:39 -04:00
Claude
d996911cda feat: add cross-platform CI for Linux, macOS, and Windows
- Split build.yml into lint, test (matrix: 3 OS), build-android, and
  build-desktop (matrix: 3 OS with native packaging) jobs
- Tests now run on ubuntu, macos, and windows to validate cross-platform
  compatibility
- Desktop distributions (DEB, DMG, MSI) built on their native OS and
  uploaded as artifacts
- Split create-release.yml into create-release, deploy-android, and
  deploy-desktop jobs so desktop distributions are included in releases
- Android APK/AAB builds and Quartz publishing remain on Linux

https://claude.ai/code/session_016dxFUErY4P6WRir4xxhCEr
2026-03-15 21:34:18 +00:00
Vitor Pamplona
61089fa866 Merge branch 'main' into claude/add-relay-event-counters-c7stK 2026-03-15 17:30:23 -04:00
Vitor Pamplona
91a9dcb9a1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  Rename build step for Benchmark APK in workflow
  compilation error fix: Changed map to mapNotNull
  spotlessApply
2026-03-15 17:16:00 -04:00
Vitor Pamplona
3624d584db Merge pull request #1855 from vitorpamplona/davotoula-patch-1
Rename build step for Benchmark APK in workflow
2026-03-15 17:15:39 -04:00
Vitor Pamplona
2fd29ab365 uses quiet mode to avoid lots of warnings in the commit screen 2026-03-15 17:15:16 -04:00
Vitor Pamplona
cef8f5acde Fixes test case 2026-03-15 17:14:57 -04:00
David Kaspar
76c6f50fa8 Rename build step for Benchmark APK in workflow 2026-03-15 20:57:02 +00:00
Vitor Pamplona
6216f7a618 no casts needed 2026-03-15 16:55:00 -04:00
Vitor Pamplona
0c939bd447 converted from Java arraylist to kotlin arraylist 2026-03-15 16:50:34 -04:00
davotoula
3cfa9e1619 compilation error fix: Changed map to mapNotNull 2026-03-15 21:37:21 +01:00
davotoula
cd9d345a0e spotlessApply 2026-03-15 21:35:23 +01:00
Vitor Pamplona
30b76347c5 Spotless 2026-03-15 16:15:44 -04:00
Vitor Pamplona
04d04ebe69 Merge pull request #1854 from vitorpamplona/claude/kotlin-serialization-mapper-4WYOP
Add Kotlin Serialization support for Nostr protocol types
2026-03-15 16:04:21 -04:00
Vitor Pamplona
ea48c2a5ec Merge branch 'main' into claude/kotlin-serialization-mapper-4WYOP 2026-03-15 16:03:55 -04:00
Vitor Pamplona
badc681511 Merge pull request #1851 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-15 16:01:44 -04:00
Claude
d10687d6fe feat: Implement iOS OptimizedJsonMapper using KotlinSerializationMapper
Replace all TODO stubs in the iOS actual implementation with delegation
to KotlinSerializationMapper. Wraps SerializationException into
IllegalArgumentException to match the jvmAndroid error handling pattern.

https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw
2026-03-15 19:58:40 +00:00
Claude
3bea2f795c refactor: Move KSerializers to their respective NIP packages
- Bunker serializers → nip46RemoteSigner/kotlinSerialization/
- NIP-47 serializers → nip47WalletConnect/kotlinSerialization/
- Rumor serializer → nip59Giftwrap/rumors/kotlinSerialization/

Mirrors the existing Jackson serializer directory structure which uses
a jackson/ subpackage within each NIP package.

https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw
2026-03-15 19:55:19 +00:00
Claude
ffac682238 feat: Add kotlinx.serialization KSerializers mirroring all Jackson custom serializers
Add 15 KSerializer implementations in commonMain that replicate the exact
behavior of all 21 Jackson serializer/deserializer registrations:

- EventKSerializer, TagArrayKSerializer, FilterKSerializer
- EventTemplateKSerializer, RumorKSerializer, CountResultKSerializer
- MessageKSerializer (8 message types), CommandKSerializer (5 command types)
- BunkerRequest/Response/MessageKSerializer (NIP-46)
- Nip47Request/Response/NotificationKSerializer (NIP-47)
- KotlinSerializationMapper as the main entry point

Uses JsonElement-based approach for performance. All serializers produce
identical JSON output to Jackson. Includes comprehensive test suite (35+
tests) verifying exact output parity and cross-deserialization between
Jackson and kotlinx.serialization.

https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw
2026-03-15 16:37:22 +00:00
Claude
22945b7faa feat: add queryCountSuspend utility to Quartz and simplify ViewModels
Add INostrClient.queryCountSuspend() extension functions in Quartz that
wrap NIP-45 COUNT queries as suspend functions, managing subscription
lifecycle internally. Two overloads: single-relay and multi-relay.

Simplify BasicRelaySetupInfoModel and Nip65RelayListViewModel to use the
new utility — removes IRelayClientListener implementation, subId tracking
maps, onIncomingMessage handlers, and cleanup logic from both ViewModels.

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
2026-03-15 16:14:24 +00:00
Crowdin Bot
d8de0a9b84 New Crowdin translations by GitHub Action 2026-03-15 15:47:20 +00:00
Claude
17c3611f50 feat: redesign RelayEventCountRow with pill chip style
Replace the plain icon+text count display with rounded pill chips that
have a subtle green border and tinted background. Each count entry gets
its own pill with an icon and bold text, making the event counts more
visually distinct and scannable.

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
2026-03-15 15:46:56 +00:00
Vitor Pamplona
3bd6ef5f6c changes from damus to nos.lol for more stable relay behavior in tests 2026-03-15 11:44:20 -04:00
Vitor Pamplona
2b157cc383 Fixes throw vs null test cases and nsec hallucinations 2026-03-15 11:18:39 -04:00
Vitor Pamplona
a793ff149b Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-15 11:18:10 -04:00
Vitor Pamplona
c0a81ef9a2 Merge pull request #1850 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-15 11:07:30 -04:00
Crowdin Bot
68f58c3b8d New Crowdin translations by GitHub Action 2026-03-15 15:06:39 +00:00
Vitor Pamplona
0380927f7e fromJsonTo calls throw, they should always be wrapped if we want null results. 2026-03-15 11:05:40 -04:00
Vitor Pamplona
2c91fdea15 Avoids using format to write hex-encoded strings. 2026-03-15 11:03:50 -04:00
Claude
764c2241ca refactor: integrate relay event counts into each ViewModel
Move count query logic from separate RelayEventCountViewModel into each
relay list's own ViewModel. BasicRelaySetupInfoModel now manages counts
via countFilters() override. Nip65RelayListViewModel gets its own count
infrastructure for home (outbox) and notification (inbox) relay lists.
Simplify AllRelayListScreen by removing 7 extra count ViewModels and
their LaunchedEffects.

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
2026-03-15 15:02:09 +00:00
Claude
b1723a5dfb feat: add relay event count stats to AllRelay settings screen
Use NostrClient's NIP-45 COUNT queries to show how many events each
relay stores, with filters specific to each relay role:
- Outbox: events authored by the user
- Inbox: events where the user is p-tagged
- DM Inbox: DM events (kind 4, 1059) tagging the user
- Private Home: events authored by the user
- Proxy: total event count
- Search: total event count
- Indexer: kind 0 and kind 10002 counts separately
- Broadcast: no count (as specified)

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
2026-03-15 14:41:19 +00:00
Claude
997a59dc23 feat: add file encryption toggle for encrypted DMs with retry on failure
Add an "Encrypt files" toggle (default: on) to the file upload dialog in
NIP-17 encrypted DMs. When encrypted upload fails, show an informative
dialog explaining that many servers don't accept encrypted files on free
accounts, with an option to retry without encryption. The retry dialog
warns that without encryption anyone with the link can see the content.

https://claude.ai/code/session_012xvKzHrZPq3ZTAzBN2LvrR
2026-03-15 14:30:26 +00:00
Claude
aecbcb68a5 feat: per-relay recv/new counts and OK-true acceptance tracking
ViewModel:
- MAX_ACTIVITY_LOG raised to 5000
- Remove activeRelays from LiveSyncActivity (active relay chips removed)
- Add eventsAccepted: Int to CompletedRelayInfo
- Add totalEventsAccepted: Int to SyncState.Done
- Register IRelayClientListener on account.client for the duration of
  the sync to intercept OkMessage(success=true) from destination relays
- sourceRelayOfEvent map attributes each forwarded event to the relay
  it was first seen on; ConcurrentHashMap.remove() ensures the first
  OK true is counted exactly once per event (dedup across dest relays)
- acceptedCountPerRelay accumulates per-source-relay accepted counts,
  read at onRelayComplete time
- downloadFromPool.onEvent changed to (Event, NormalizedRelayUrl) to
  carry the source relay into runSync routing logic

Screen:
- Remove ActiveRelaysCard and ActiveRelayChip composables + preview
- Remove animation imports (no longer needed)
- ActivityLogRow now shows two right-hand columns per relay:
    recv N  — events received from that source relay
    new N   — events accepted as new by destination relays (highlighted
               in primary color when > 0)
- DoneCard now shows three lines:
    Forwarded N events to destination relays.
    M events accepted as new by destination relays.  ← key headline
    Completed in N seconds.

Strings: event_sync_done_body replaced by done_sent / done_accepted /
done_duration; event_sync_reading_from and events_found_log removed;
log_recv and log_new added.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-15 14:19:08 +00:00
David Kaspar
a21e38f428 Merge pull request #1847 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-15 11:53:03 +00:00
Crowdin Bot
7999010390 New Crowdin translations by GitHub Action 2026-03-15 11:51:55 +00:00
David Kaspar
00fb924dbb Update README with build instructions and requirements
Added instructions for full build and requirements.
2026-03-15 11:50:24 +00:00
davotoula
f28fdd8404 Convert between flag types
spotlessApply
2026-03-14 21:39:27 +01:00
David Kaspar
f3aafd3884 Merge pull request #1845 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-14 20:26:58 +00:00
Crowdin Bot
2df98c456a New Crowdin translations by GitHub Action 2026-03-14 20:04:41 +00:00
David Kaspar
21bf8a343a Merge pull request #1844 from davotoula/update-translations
update cz, pt, de, sv
2026-03-14 20:03:08 +00:00
David Kaspar
caee58da55 Add migrations.xml to .gitignore 2026-03-14 20:02:48 +00:00
davotoula
64bf1d5090 update cz, pt, de, sv 2026-03-14 21:00:31 +01:00
Vitor Pamplona
ee449f78f4 Merge pull request #1843 from davotoula/bugfix-multiple-spinners-on-upload
Bugfix: multiple spinners on upload
2026-03-14 14:10:22 -04:00
David Kaspar
9d14a2d840 Merge pull request #1842 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-14 17:30:32 +00:00
davotoula
795da8cf1b Block Add to message button while uploading 2026-03-14 18:00:36 +01:00
davotoula
1fc0ba8628 refactor:
extract MediaUploadTracker with common methods

bugfixes:
 ChatFileUploadState.canPost() was missing isUploadingFile check
 ChatNewMessageViewModel.cancel() was never resetting upload flags
 Double-reset in ChatFileUploader and ChannelNewMessageViewModel
2026-03-14 18:00:36 +01:00
davotoula
42542159d4 bugfix: remove dead code
bugfix: ChannelNewMessageViewModel canPost checks wrong state
2026-03-14 18:00:36 +01:00
davotoula
c290a1c779 - SelectFromGallery and SelectFromFiles now accept an enabled param separate from isUploading
- All ViewModels track isUploadingFile alongside isUploadingImage, set via hasNonMedia()
  - All screen call sites pass source-specific flags so only the initiating button spins
  - canPost() methods block posting during file uploads too
  - ChatNewMessageViewModel delegates to ChatFileUploadState (fixes pre-existing dead state)
  - cancel() methods reset both flags in all ViewModels
2026-03-14 17:59:35 +01:00
Claude
a49a0ac9ae feat: add @Preview composables to EventSyncScreen
One ThemeComparisonColumn preview per card — renders both dark and light
themes stacked — covering all independently previewable composables:

  SyncProgressCardPreview  — mid-sync progress (312/1024 relays, 4821 sent)
  PausedCardPreview        — paused state with relay/event counts
  DoneCardPreview          — completed run (18K events, 187s)
  ErrorCardPreview         — configuration error message
  ActiveRelaysCardPreview  — 5-relay FlowRow with pulsing chips
  DestinationRelaysCardPreview — all three sections (outbox/inbox/DM)
  ActivityLogCardPreview   — mixed log: found events + empty/unreachable

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 16:16:36 +00:00
Claude
6aacaa7d41 fix: remove event sync section item from AllRelayListScreen
https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 15:51:41 +00:00
Crowdin Bot
0dce9542fb New Crowdin translations by GitHub Action 2026-03-14 15:41:38 +00:00
Vitor Pamplona
42574f302c updates change log 2026-03-14 11:39:02 -04:00
Vitor Pamplona
d4ae3411c7 spotless 2026-03-14 11:36:58 -04:00
Vitor Pamplona
6739e66bd3 Merge pull request #1828 from vitorpamplona/claude/implement-nip47-wallet-Fjcds
Implement NIP-47 Wallet Connect protocol with full method support
2026-03-14 11:32:51 -04:00
Claude
78c66b00eb fix: align arrow icons to 40dp to match user picture size
https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 15:29:28 +00:00
Claude
b7dd460734 feat: live relay activity UI for event sync screen
ViewModel now emits a LiveSyncActivity StateFlow alongside the existing
SyncState, carrying:
  - activeRelays: which relays are currently being queried
  - recentCompletions: last 100 completed relays with event counts
  - outboxTargets / inboxTargets / dmTargets: destination relay sets

downloadFromPool gains onRelayStart/onRelayComplete(relay, eventsFound)
callbacks; downloadFromRelay returns Int (total events across all pages).
Tracking uses ConcurrentHashMap.newKeySet + synchronized ArrayDeque for
thread safety across 50 concurrent workers.

Screen adds three new live cards:
  ActiveRelaysCard  — FlowRow of chips with a shared pulsing dot
                      animation (one InfiniteTransition for all chips)
  DestinationRelaysCard — Outbox / Inbox / DMs sections, color-coded
                          using primary / secondary / tertiary
  ActivityLogCard   — fixed-height (260dp) inner-scrollable list;
                      filled dot + count for active relays, muted for
                      empty/unreachable ones; K/M formatting for counts

String resources updated: event_sync_batch_of → event_sync_relays_progress,
paused_body copy updated to relay language; 7 new strings added.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 15:26:12 +00:00
Vitor Pamplona
03e1c32757 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 11:20:33 -04:00
Claude
84035f887a feat: parse NIP-47 transaction metadata for sender/recipient display
Add NwcTransactionMetadata parser that extracts comment, payer_data,
recipient_data, and nostr zap data from the untyped metadata field.
The transaction list UI now shows the Nostr user profile picture and
name for zap senders/recipients, falls back to payer name/email or
lightning address, and displays comment when it differs from description.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 15:18:53 +00:00
Claude
666c795c93 fix: use correct cancellation checks in suspend functions
isActive is a CoroutineScope extension and has no CoroutineScope
receiver in plain suspend fun bodies (runSync, downloadFromRelay).

- runSync catch block: split into CancellationException (rethrow) +
  Exception, removing the invalid isActive guard entirely
- downloadFromRelay while loop: while(isActive) -> while(true) with
  coroutineContext.ensureActive() as the first statement, which works
  in any suspend fun and throws CancellationException when cancelled

The isActive import is kept for the supervisorScope lambda in
downloadFromPool, where a CoroutineScope receiver is in scope.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 15:03:45 +00:00
Claude
2fd623a97d fix: wallet screen showing "No wallet connected" on first open
The WalletViewModel.init() was called inside LaunchedEffect (async),
but hasWalletSetup() was checked synchronously during the first
composition frame—before init had run. Moved init to run synchronously
during composition and made hasWalletSetup a reactive StateFlow so the
UI updates when wallet state changes.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 14:30:54 +00:00
Vitor Pamplona
120981f8c4 spotless apply 2026-03-14 10:18:16 -04:00
Vitor Pamplona
493fe05ac4 Fixes missing parameter 2026-03-14 10:18:08 -04:00
Claude
5905926d4a feat: sliding window relay pool replaces fixed batch chunks
Instead of waiting for all 50 relays in a batch to finish before
starting the next 50, a Semaphore(MAX_CONCURRENT_RELAYS) keeps the
concurrency window full at all times: the moment one relay is fully
exhausted it releases the semaphore and the next relay starts.

Each relay is handled by downloadFromRelay(), which paginates
independently (until cursor) until the relay returns no events.
supervisorScope ensures a single relay failure does not cancel
the rest of the pool.

Dedup sets and event counter are now thread-safe (ConcurrentHashMap
key sets, AtomicLong) since workers fire concurrently.

SyncState.Running/Paused fields renamed:
  chunkIndex/totalChunks -> relaysCompleted/totalRelays
  nextChunkIndex         -> nextRelayIndex

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 13:44:48 +00:00
Claude
1841e47c5c feat: add high-level Nip47Client and Nip47Server APIs for NIP-47
Simplifies the NWC developer experience by providing Nip47Client (for
wallet client apps) and Nip47Server (for wallet service backends) that
handle URI parsing, signer creation, event building, filter construction,
and response decryption. Updates README with quick-start examples.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 11:39:17 +00:00
David Kaspar
43914c0f09 Merge pull request #1841 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-14 11:19:39 +00:00
Crowdin Bot
4fc97f21c5 New Crowdin translations by GitHub Action 2026-03-14 11:10:01 +00:00
David Kaspar
dcf1e4e39b Add Claude Code local settings to .gitignore 2026-03-14 11:08:34 +00:00
David Kaspar
a6d5944bf3 Fix JSON formatting in settings.json 2026-03-14 09:01:55 +00:00
David Kaspar
f2391e698a Merge pull request #1839 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-14 05:06:48 +00:00
Crowdin Bot
f9f4e7716d New Crowdin translations by GitHub Action 2026-03-14 05:06:26 +00:00
David Kaspar
ce3f9bec5c Merge pull request #1838 from davotoula/update-translations
update cz, pt, de, sv
2026-03-14 05:04:49 +00:00
davotoula
3f1e826a10 update cz, pt, de, sv 2026-03-14 06:00:21 +01:00
Claude
580678eed8 feat: add server-side event builders and NIP-47 README for quartz
Adds missing server-side capabilities to the NIP-47 quartz module:

- LnZapPaymentResponseEvent.createResponse(): builds encrypted response
  events (kind 23195) for wallet services to reply to client requests
- NwcNotificationEvent.createNotification(): builds encrypted notification
  events (kind 23197) for wallet services to push payment notifications

Creates comprehensive README.md documenting the full NIP-47 API with
code examples for both wallet client and wallet service implementations,
covering URI parsing, request/response building, encryption, notifications,
error handling, transaction states, and caching.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 04:25:27 +00:00
Claude
4b76c70c29 feat: add NIP-47 wallet interface with balance, send, receive, and transactions
Adds an Alby Go-inspired wallet interface accessible from the left drawer menu.
Uses existing NWC connection from zap settings to communicate with the wallet
via NIP-47 methods (get_balance, get_info, pay_invoice, make_invoice, list_transactions).

New files:
- WalletViewModel: manages wallet state and NWC requests
- WalletScreen: balance display with send/receive action buttons
- WalletSendScreen: paste BOLT-11 invoice and pay
- WalletReceiveScreen: create invoice with QR code display
- WalletTransactionsScreen: scrollable transaction history

Also adds generic sendNwcRequest() to NwcSignerState and Account for
sending arbitrary NIP-47 requests beyond just pay_invoice.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 03:52:41 +00:00
Claude
cf922a064b feat: Strip sensitive metadata from media files before upload
Adds a MetadataStripper that removes GPS location, camera info, timestamps,
and other sensitive EXIF/metadata from images, videos, and audio files before
uploading. This protects user privacy by default.

- Creates MetadataStripper using AndroidX ExifInterface for images and
  MediaMuxer for video/audio remuxing
- Adds "Strip location metadata" toggle to all upload dialogs (NewMediaView,
  ChatFileUploadDialog) so users can disable stripping per upload
- Persists the setting in AccountSettings (stripLocationOnUpload, default true)
- Integrates stripping into UploadOrchestrator before compression
- Covers all upload paths: media posts, DM attachments, channel uploads,
  profile pictures, list/channel metadata images

https://claude.ai/code/session_01974zpAMSRD9GeqBt9ax6XD
2026-03-14 03:50:34 +00:00
Claude
0fe721f426 feat: Add markdown long-form post screen (NIP-23)
Add a new post screen specialized for writing and publishing long-form
content events (kind 30023) with markdown support. Includes:

- MarkdownPostViewModel with title, summary, cover image, and markdown
  body fields, draft support, and LongTextNoteEvent creation
- MarkdownPostScreen with Edit/Preview tabs, monospace editor, markdown
  preview using existing RenderContentAsMarkdown, and media upload that
  inserts markdown image syntax
- Navigation route (Route.NewMarkdownPost) and AppNavigation entry
- String resources for the new screen

https://claude.ai/code/session_012pBNvnWtVtQ6aKSheWJUDt
2026-03-14 03:44:46 +00:00
Claude
83a6feeca4 feat: add Alby JS SDK client interop improvements
- Add NwcBudgetRenewal constants (daily/weekly/monthly/yearly/never)
- Add case-insensitive NwcTransactionState helpers (isSettled, isPending,
  isFailed, isAccepted) for JS SDK lowercase state interop
- Add URI test using Alby JS SDK test vector (69effe7b... pubkey)
- Add JS SDK interop tests: lowercase transaction states in responses,
  notifications, and list_transactions; empty get_budget response;
  all budget renewal periods; structured transaction metadata;
  full 13-method get_info response

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 03:28:17 +00:00
Claude
4ee937ff0c test: add Alby Hub real test vectors to interop tests
Add tests using exact JSON payloads from Alby Hub's test suite:
- Real bolt11 invoice strings from Alby's mock data
- pay_keysend with TLV records and preimage
- make_invoice with nested metadata objects
- make_hold_invoice with 64-char payment hash
- settle_hold_invoice with preimage
- list_transactions with unpaid_outgoing filter
- create_connection with isolated=true

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 03:00:56 +00:00
Claude
9e4cb446ef feat: add NIP-47 get_budget, sign_message, create_connection methods and Alby Hub interop
- Add get_budget, sign_message, create_connection request/response types
- Add NwcTransactionType (incoming/outgoing) and NwcTransactionState
  (PENDING/SETTLED/FAILED/ACCEPTED) constants
- Add missing error codes: BAD_REQUEST, NOT_FOUND, EXPIRED
- Add settle_deadline field to NwcTransaction
- Add total_count to ListTransactionsResult
- Add metadata and lud16 to GetInfoResult
- Add unpaid_outgoing and unpaid_incoming to ListTransactionsParams
- Update Jackson deserializers for new method types
- Add AlbyInteropTest with 25+ tests verifying compatibility with
  Alby Hub's JSON formats for all request/response types
- Update existing tests for new constants and error codes

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-14 02:58:40 +00:00
Claude
50282f32f4 feat: paginate relay queries with per-relay until cursor
Relays cap event responses at ~500 per request. After each EOSE, the
oldest createdAt seen on a relay becomes the next 'until' cursor and
the relay is re-queried for the next page. This repeats until a relay
returns no events (exhausted) or cannot be reached.

All active relays in a batch are queried in parallel each round; only
the relays that still have pages remaining participate in the next round.
Dead relays (onCannotConnect) are dropped immediately.

ConcurrentHashMap is used for per-relay counters and cursors since
IRequestListener callbacks can arrive from concurrent relay threads.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 02:48:40 +00:00
Claude
0bbbadd494 feat: chunk relays, parallel filters, and pausable sync
- Process relays in batches of 50 instead of one at a time; each batch
  opens a single subscription covering all relays simultaneously, so
  5 000 relays become 100 batches rather than 5 000 sequential round-trips.

- Collapse the three sequential phases into one combined query per batch:
  send both the 'authored by me' filter and the 'p-tagged me' filter in
  the same subscription, then route events to outbox/inbox/DM relays by
  their properties.  This halves the total number of relay connections.

- Make the procedure pausable: cancel() now transitions to SyncState.Paused
  (carrying the next chunk index and events-sent count) instead of Idle.
  The screen shows a Resume button that continues from the saved checkpoint
  and a Start Over button to restart from scratch.

- The mobile-data dialog correctly routes to resume() vs start() depending
  on whether a checkpoint exists.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-14 02:40:59 +00:00
Vitor Pamplona
da38555035 Merge pull request #1835 from vitorpamplona/claude/add-swipe-delete-cancel-QjI5b
Add cancel button to swipe-to-delete confirmation UI
2026-03-13 20:42:43 -04:00
Claude
bcb5be76c7 feat: Add cancel button to swipe-to-delete confirmation
After swiping a draft to delete, the red confirmation background now
shows both a "Delete" button (left) and a "Cancel" button (right).
Tapping Cancel resets the swipe state, allowing users to dismiss
the delete action without deleting the draft.

https://claude.ai/code/session_01ULVR1fKwka5TgfgSucPE4r
2026-03-13 23:19:47 +00:00
Vitor Pamplona
64118058f2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: replace dialog with swipe-to-reveal delete button for drafts
  feat: add confirmation dialog when swiping to delete a draft
2026-03-13 19:11:25 -04:00
Vitor Pamplona
474a875167 Fixes padding of the expiration date on NoteCompose 2026-03-13 19:11:10 -04:00
Claude
9d49b2da4f feat: confirm dialog before starting sync on mobile data
Replace the disabled-Start + secondary-Start-Anyway button pattern
with a single always-enabled Start button that shows an AlertDialog
when the connection is metered/mobile, requiring explicit confirmation
before running a potentially gigabyte-scale operation.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-13 22:49:40 +00:00
Vitor Pamplona
ae0ab0c153 Merge pull request #1826 from vitorpamplona/claude/draft-delete-confirmation-y28xT
Add confirmation dialog to draft deletion via swipe gesture
2026-03-13 18:48:05 -04:00
Claude
90d7b0c678 feat: run EventSync in background by scoping it to AccountViewModel
Move EventSyncViewModel from a screen-scoped ViewModel to a plain class
held by AccountViewModel (activity-scoped). The sync job now runs on
AccountViewModel's viewModelScope, so it continues when the user
navigates away. The back button no longer cancels the sync.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-13 22:42:09 +00:00
Claude
441dae8e25 feat: move EventSync to left drawer, remove button from AllRelayListScreen
https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-13 22:37:16 +00:00
Vitor Pamplona
1b5d7fdc13 Merge pull request #1834 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-13 18:14:40 -04:00
Claude
b3adc56adf feat: replace dialog with swipe-to-reveal delete button for drafts
Instead of showing a confirmation dialog, the swipe now stays open
revealing a red background with a clickable "Request Deletion" button.
Supports swiping from both left-to-right and right-to-left directions.
The user taps the revealed button to confirm deletion.

https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU
2026-03-13 22:11:54 +00:00
Crowdin Bot
bb88228019 New Crowdin translations by GitHub Action 2026-03-13 22:10:50 +00:00
Vitor Pamplona
1468a41737 Adds expirations to change log 2026-03-13 18:07:33 -04:00
Vitor Pamplona
c4c73a2bb7 Adds expirations in all new post screens.
Adds expirations in the feed and chats.
2026-03-13 18:05:59 -04:00
Vitor Pamplona
42fdedf23b fixes spotless 2026-03-13 17:03:24 -04:00
Vitor Pamplona
9cd779b60c Merge pull request #1817 from vitorpamplona/claude/add-post-expiration-date-Nmedg
Add NIP-40 expiration date support to note creation
2026-03-13 17:01:27 -04:00
Vitor Pamplona
d81609165e Small refactoring of the relay list screen 2026-03-13 16:28:50 -04:00
Vitor Pamplona
d281c2366c Removes the need to pass context down to a composable 2026-03-13 16:04:46 -04:00
Vitor Pamplona
4a5ffb3169 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add ZIP/JSON relay export and format picker dropdown
  refactor: extract relay export logic into RelayExporter class
  feat: add export button to relay settings screen
2026-03-13 15:31:22 -04:00
Vitor Pamplona
e51967923b adds today's work to change log 2026-03-13 15:20:11 -04:00
Vitor Pamplona
8339413262 Fixes copilot recommendation 2026-03-13 15:20:01 -04:00
Vitor Pamplona
b3bfa8950d Merge pull request #1823 from vitorpamplona/claude/export-relay-settings-v8uQg
Add relay settings export functionality
2026-03-13 15:09:25 -04:00
Vitor Pamplona
9b58a1be11 Fixing IO Dispatchers and scopes of choice. 2026-03-13 15:06:03 -04:00
Claude
0ad63bbfc1 feat: add ZIP/JSON relay export and format picker dropdown
- Create RelayListCollection to bundle all relay lists with section metadata
- Create RelayZipExporter that exports each relay category as a separate
  JSON file (array of relay URL strings) inside a zip archive, shared
  via FileProvider
- Refactor RelayExporter to use RelayListCollection
- Replace single export button with a dropdown menu letting users pick
  between text export and ZIP (JSON) export

https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM
2026-03-13 18:24:05 +00:00
Vitor Pamplona
f3eb609eee Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-13 13:45:44 -04:00
Vitor Pamplona
00f2d46b53 Refines user status flow and isExpired running 2026-03-13 13:45:22 -04:00
David Kaspar
b9f6017590 Merge pull request #1832 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-13 17:41:23 +00:00
Crowdin Bot
9ec8dc167c New Crowdin translations by GitHub Action 2026-03-13 17:36:41 +00:00
Vitor Pamplona
99eaa37d4c Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: complete NIP-38 implementation with p tag, emoji, and expiration support
2026-03-13 13:33:11 -04:00
Vitor Pamplona
6f57652455 Merge pull request #1827 from vitorpamplona/claude/implement-nip-38-q8DOF
Enhance user status display with emoji support and metadata tags
2026-03-13 13:32:41 -04:00
Vitor Pamplona
1966cc2034 Bitcoin first then namecoin 2026-03-13 13:29:55 -04:00
Claude
e83ec94461 refactor: extract relay export logic into RelayExporter class
Moves the text formatting and share intent logic from AllRelayListScreen
into a dedicated RelayExporter class in the common package.

https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM
2026-03-13 17:28:39 +00:00
Vitor Pamplona
8bc14b52b2 Spotless update 2026-03-13 13:25:23 -04:00
Vitor Pamplona
34f3692d9a Merge pull request #1815 from vitorpamplona/claude/add-explorer-settings-page-zXosk
feat: add blockchain explorer settings page for OTS verification
2026-03-13 13:22:57 -04:00
Vitor Pamplona
f4d6e3cb3e Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 13:22:44 -04:00
Vitor Pamplona
bcdc8386a0 Merge pull request #1830 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-13 13:18:50 -04:00
Crowdin Bot
178e47ebf5 New Crowdin translations by GitHub Action 2026-03-13 13:28:43 +00:00
Vitor Pamplona
4bf7f982ba Makes the new translation settings work 2026-03-13 09:24:12 -04:00
Vitor Pamplona
3e0b8a5f8f Fixes gradle build on MacOS and adds a Clibsodium.def file 2026-03-13 08:40:51 -04:00
Vitor Pamplona
c778c9f109 Adds a hook to get claude web to compile and check android code 2026-03-13 08:40:28 -04:00
Vitor Pamplona
ae41a956ac Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-13 08:22:28 -04:00
Vitor Pamplona
e8a031251a Merge pull request #1829 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-13 08:22:12 -04:00
Vitor Pamplona
9ca79acbe4 lint 2026-03-13 08:21:55 -04:00
Crowdin Bot
c0bd88278c New Crowdin translations by GitHub Action 2026-03-13 12:20:55 +00:00
Vitor Pamplona
c33ba239d0 Merge pull request #1824 from vitorpamplona/claude/improve-translation-settings-dDwmL
Redesign language settings UI with searchable language pickers
2026-03-13 08:16:58 -04:00
Vitor Pamplona
3a6d199962 Merge pull request #1821 from KotlinGeekDev/kmp-completeness
Kmp completeness, Part 2 - Libsodium
2026-03-13 08:01:33 -04:00
KotlinGeekDev
10f5799014 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-13 09:20:58 +01:00
Claude
8c73dd8f14 test: add comprehensive test suite for NIP-47 Wallet Connect implementation
Covers all 10 request methods (create, serialize, deserialize), all success
and error response types, notification deserialization, EncryptionTag and
NotificationsTag parse/assemble/isTag, NwcInfoEvent build and capabilities,
NwcNotificationEvent structure, LnZapPaymentRequestEvent create/decrypt
with NIP-04 and NIP-44, and Nip47WalletConnect URI parsing and serialization.

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-13 03:45:50 +00:00
Claude
da26ab14ab feat: complete NIP-47 Nostr Wallet Connect implementation in Quartz
Implements the full NIP-47 specification with all request methods,
response types, notification events, and JSON serialization.

New request methods:
- pay_invoice (updated with amount, metadata params)
- pay_keysend (amount, pubkey, preimage, TLV records)
- make_invoice, lookup_invoice, list_transactions
- get_balance, get_info
- make_hold_invoice, cancel_hold_invoice, settle_hold_invoice

New response types:
- Success responses for all methods above
- NwcErrorResponse (generic error for any method)
- NwcErrorCode enum with UNSUPPORTED_ENCRYPTION

New event kinds:
- NwcInfoEvent (kind 13194) - wallet service capabilities
- NwcNotificationEvent (kind 23197) - NIP-44 encrypted notifications
- Legacy kind 23196 support for NIP-04 notifications

New data models:
- NwcTransaction (shared invoice/payment object)
- TlvRecord (for keysend TLV records)
- Notification types: payment_received, payment_sent, hold_invoice_accepted
- HoldInvoiceAcceptedData with settle_deadline

Tags (following NIP-88 pattern):
- EncryptionTag for encryption scheme negotiation
- NotificationsTag for supported notification types

Updated:
- LnZapPaymentRequestEvent: generic createRequest() with NIP-44 support
- URI parser: lud16 parameter support
- Jackson deserializers: all methods routed correctly
- EventFactory: new kinds registered

https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ
2026-03-13 03:15:21 +00:00
Claude
82e500eee1 feat: complete NIP-38 implementation with p tag, emoji, and expiration support
- Add optional r/p/e/a/emoji tags to StatusEvent.create() for full spec compliance
- Add GENERAL and MUSIC type constants to StatusEvent
- Filter expired statuses in UserStatusCache.addStatus() and add removeExpired()
- Add p tag (profile reference) display in DisplayStatusInner
- Add NIP-30 custom emoji rendering in status display via CreateTextWithEmoji
- Allow status type parameter in UserStatusAction.create()

https://claude.ai/code/session_0117asp4mDiR65dxbAGKi2hg
2026-03-13 03:06:22 +00:00
Claude
48485ce933 feat: add confirmation dialog when swiping to delete a draft
The swipe-to-delete gesture on the drafts screen was too sensitive,
causing accidental deletions. Added a confirmation dialog that appears
after the swipe gesture completes, requiring the user to confirm before
the draft is actually deleted.

https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU
2026-03-13 02:49:07 +00:00
Claude
0da3e562fb style: spotless apply on EventSync files
https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-13 02:40:08 +00:00
Claude
997a270a84 feat: add export button to relay settings screen
Adds a share icon button in the top bar of the relay settings screen
that exports all relay configurations (except Connected Relays) as
human-readable text via Android's share intent. Each section includes
a header and description, with one relay URL per line.

https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM
2026-03-13 02:30:33 +00:00
Claude
7746561360 feat: refresh translation settings UI with modern components
- Translate To: shows all available languages with searchable list instead
  of only device locales
- Don't Translate From: uses InputChip FlowRow with an Add button and
  searchable language picker to add new languages
- Language Preferences: displays as OutlinedCards with radio buttons for
  choosing display language, includes delete and add-new-pair functionality
  with guided source/target selection
- Added new string resources for the refreshed UI

https://claude.ai/code/session_011iTJ1gg8weaxaALiUQNH4C
2026-03-13 02:29:35 +00:00
Claude
83e79cf857 feat: add EventSync screen for relay event synchronization
- Add EventSyncScreen composable with UI for event sync configuration
- Add EventSyncViewModel for managing sync state and business logic
- Add route and navigation entry for EventSync screen
- Add entry point in AllRelayListScreen
- Add string resources for EventSync UI

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
2026-03-13 02:15:16 +00:00
Vitor Pamplona
198aa0971c adds ./gradlew spotlessApply request to claude
improves description of the .md file
2026-03-12 22:13:33 -04:00
KotlinGeekDev
5cdf8bdd86 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-13 01:12:49 +01:00
Vitor Pamplona
d9eb44df1f Improving the spacing in the new translation settings screen 2026-03-12 19:52:36 -04:00
Vitor Pamplona
0d9f390cd9 missing tests fix 2026-03-12 19:44:51 -04:00
Vitor Pamplona
e6eb421e79 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  feat: add missing translation settings to Translation settings menu
2026-03-12 19:43:11 -04:00
Vitor Pamplona
21e29bbb3c Fixes test cases. 2026-03-12 19:42:46 -04:00
Vitor Pamplona
deeac97082 Merge pull request #1814 from vitorpamplona/claude/add-translation-settings-MIoUT
feat: add missing translation settings to Translation settings menu
2026-03-12 19:41:40 -04:00
Vitor Pamplona
ebbd7e1a77 adds today's work to the change log 2026-03-12 19:06:17 -04:00
Vitor Pamplona
fa67030b47 Adds support for BUD-10 Blossom URIs 2026-03-12 18:57:46 -04:00
Vitor Pamplona
d4dd5a65e6 anySync was returning either on true or false. it was not doing what it was supposed to do which is returning after the first true. 2026-03-12 18:56:17 -04:00
KotlinGeekDev
aa172dbfbc Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-12 22:17:46 +01:00
KotlinGeekDev
3838b6b815 Some fixes. 2026-03-12 22:16:34 +01:00
KotlinGeekDev
42f48e11fc Make CI happy(attempt 2). 2026-03-12 21:49:08 +01:00
David Kaspar
4b304ce8d5 Merge pull request #1822 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-12 20:46:11 +00:00
Crowdin Bot
bfd595ea00 New Crowdin translations by GitHub Action 2026-03-12 20:45:10 +00:00
David Kaspar
c477745fbd Merge pull request #1820 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-12 20:43:26 +00:00
KotlinGeekDev
a463a9802f Slightly modify defFile generation, to stop CI cries. 2026-03-12 21:36:25 +01:00
KotlinGeekDev
750bedbabd Foundations(iOS Sourceset): Reduce the header files only to those needed, plus utilities. 2026-03-12 21:06:02 +01:00
KotlinGeekDev
0b928d29a6 Make the libsodium cinterop file generation more reliable. Add in(and comment out) some test code for extracting the libsodium headers from zip file. 2026-03-12 20:28:02 +01:00
KotlinGeekDev
527130a53c Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-03-12 20:22:55 +01:00
KotlinGeekDev
d957efb1b8 Foundations(iOS Sourceset): Streamline libsodium bindings configuration. 2026-03-12 16:55:19 +01:00
Crowdin Bot
f31e73f182 New Crowdin translations by GitHub Action 2026-03-12 14:53:15 +00:00
Vitor Pamplona
ad17924622 Making sure UrlDetector returns a schema even if the parser points to some user:password scheme. 2026-03-12 10:48:11 -04:00
Vitor Pamplona
3eb2fcbcb8 Solves the sorting contract crash by precaching all values before sorting. 2026-03-12 10:45:54 -04:00
KotlinGeekDev
2b61f4a3d6 Function rename for digest provider. 2026-03-12 15:33:07 +01:00
KotlinGeekDev
759b0c8b96 Foundations(iOS Sourceset): Use alternative implementation for MacInstance, due to complications with mac length under previous implementation, which makes NIP49 tests(with all other relevant tests) pass. Some spotless fixes. 2026-03-12 15:31:33 +01:00
KotlinGeekDev
8f02ea0cf6 Foundations(iOS Sourceset): Provide implementation for LibSodiumInstance.ios.kt, using the native library(shipped in project). Generate cinterop definition file with custom Gradle task. Defines bindings for use in actual code(may be streamlined later). Adds NIP44 tests and makes sure they pass. 2026-03-12 15:13:32 +01:00
nrobi144
327bb1efc8 fix(nav): preserve screen state across overlay navigation
RootContent now stays composed when overlays are shown, preventing
search results and other state from being destroyed on navigation.
Overlay wrapped in opaque Surface to cover root content visually.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:32:05 +02:00
nrobi144
5a437ed5ac feat(search): collapsible section headers + sorting + search improvements
- Collapsible sticky section headers with animated chevron
- Full header row clickable to toggle collapse
- SearchResultSorter, SearchSortOrder, pseudo-kind filtering
- TextFieldValue cursor preservation, relay timeout improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:23:27 +02:00
nrobi144
7180edf162 Merge upstream/main into feat/desktop-advanced-search
Resolve conflicts:
- RelayStatus.kt: keep both relay URLs
- SearchScreen.kt: keep advanced search implementation
- SinglePaneLayout.kt: add missing Spacer import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:22:15 +02:00
Claude
e07d42e3da feat: add NIP-40 expiration date to short note post screen
Allows users to add an expiration date to posts via NIP-40. The feature
follows the same pattern as content warning (toggle button in the bottom
action bar, expandable section in the post body) and uses the same
calendar/time picker UI as poll deadlines.

- ExpirationDateButton: timer icon toggles orange when active
- ExpirationDatePicker: date+time picker with "Expires in X" display
- ViewModel: wantsExpirationDate + expirationDate state, wired into
  both TextNoteEvent and PollEvent builders, loaded from drafts
- Strings: add_expiration_date, expiration_date_label, etc.

https://claude.ai/code/session_016QjmArr6zfM1Qx58iNrntA
2026-03-12 10:38:23 +00:00
Claude
117c903e7c feat: add missing translation settings to Translation settings menu
Expose the translateTo and languagePreferences fields from
AccountLanguagePreferences in the Translation settings screen
(UserSettingsScreen), which previously only showed the
dontTranslateFrom setting.

New settings added:
- Translate To: dropdown to select the target translation language
  from the device's configured locales, with a checkmark on the
  currently selected language.
- Language Display Preferences: for each saved language-pair
  preference (source → target), a dropdown to choose which language
  to show first; only visible when at least one preference exists.

https://claude.ai/code/session_01BheCJbDDZDAAYo6GAJBtFf
2026-03-12 09:46:07 +00:00
Claude
3e16d317fc feat: add blockchain explorer settings page for OTS verification
Adds a new "Bitcoin Explorer (OTS)" settings page in App Settings,
modeled after the existing Namecoin settings page.

Users can configure a custom Mempool-compatible blockchain explorer
API URL for OpenTimestamps proof verification. When a custom URL is
set it takes priority over the automatic Tor-aware selection (mempool.space
when Tor is active, blockstream.info otherwise).

Changes:
- OtsSettings: immutable data class for explorer config
- OtsSharedPreferences: DataStore-backed persistence (global/app-level)
- TorAwareOkHttpOtsResolverBuilder: accepts customExplorerUrl lambda
- OtsSettingsSection: UI section with active explorer display, URL input,
  known-explorers reference list, and reset button
- OtsSettingsScreen: full screen wrapping the section
- Routes.OtsSettings: new navigation route
- AllSettingsScreen: new entry under App Settings
- AppModules: wires otsPrefs and passes custom URL to resolver builder

https://claude.ai/code/session_01GX7k36uepGxU3U1fBKYFrx
2026-03-12 09:35:16 +00:00
Vitor Pamplona
a94d5bfd07 Fixes BUD 10 tests 2026-03-11 15:29:16 -04:00
Vitor Pamplona
548ce0a69d spotless apply 2026-03-11 15:24:02 -04:00
Vitor Pamplona
3712448c3f Merge pull request #1812 from vitorpamplona/claude/kotlin-blossom-uri-parser-lrQXn
Add BUD-10 Blossom URI parser and serializer
2026-03-11 15:19:22 -04:00
David Kaspar
853cdddc99 Merge pull request #1811 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-11 19:19:15 +00:00
Claude
ac69a0fa74 feat(nipB7Blossom): add BUD-10 Blossom URI parser
Implements a pure-Kotlin (commonMain) parser for the BUD-10 blossom: URI
scheme, which allows sharing blob references with discovery hints.

Format: blossom:<sha256>.<ext>[?xs=<server>&as=<pubkey>&sz=<bytes>]

- BlossomUri data class holds sha256, extension, servers (xs), authors (as), size (sz)
- BlossomUri.parse() returns null for non-blossom URIs or invalid SHA-256
- Supports repeated xs/as params (multiple servers and authors)
- Percent-encodes server URLs on serialisation; decodes on parse
- BlossomUri.toUriString() round-trips back to a canonical URI
- Includes commonTest coverage for all parsing cases

https://claude.ai/code/session_01T4jyLbkNWd3f7i6MwsBcD4
2026-03-11 19:16:45 +00:00
Crowdin Bot
6aef5df6c8 New Crowdin translations by GitHub Action 2026-03-11 19:11:37 +00:00
David Kaspar
3bfd3f4314 Merge pull request #1810 from davotoula/update-translations
update cz, pt, de, sv
2026-03-11 19:09:52 +00:00
David Kaspar
61421f1393 Merge pull request #1809 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-11 19:05:03 +00:00
davotoula
d7f9d584d6 update cz, pt, de, sv 2026-03-11 20:04:05 +01:00
Crowdin Bot
a81b98a63a New Crowdin translations by GitHub Action 2026-03-11 19:03:42 +00:00
Vitor Pamplona
0c040d8816 addresses long domain names for blossom and nostr profiles 2026-03-11 14:59:52 -04:00
Vitor Pamplona
6a2cbb3a3a Using a real hex key in preview 2026-03-11 14:59:22 -04:00
Vitor Pamplona
1385feb066 fixes non-lowercase schemas on posts 2026-03-11 14:38:19 -04:00
Vitor Pamplona
d521e29824 Shows all users cited in subscriptions for each relay. 2026-03-11 14:27:51 -04:00
Vitor Pamplona
1c044c9072 If warning is blank, revert to defaults 2026-03-11 14:12:02 -04:00
Vitor Pamplona
ffe5e14b2b Display content warning reason 2026-03-11 14:10:55 -04:00
Vitor Pamplona
a4010189cd Adds content warning reason to all new post screens 2026-03-11 13:52:18 -04:00
Vitor Pamplona
7ce13b5ba5 Adds content sensitivity descriptors to all screens 2026-03-11 13:47:10 -04:00
Vitor Pamplona
9bb87b38f5 Merge pull request #1808 from vitorpamplona/claude/add-content-warning-description-5BF4B
Add optional description field for sensitive content warnings
2026-03-11 13:43:47 -04:00
Vitor Pamplona
eedfc61fad Fixes lingering relays on loading follows outbox relays. 2026-03-11 13:17:06 -04:00
Claude
2b0518a675 feat: allow users to add a description to the sensitive content warning
- Add contentWarningReason() helper to TagArrayExt and EventExt for reading the reason from existing events
- Add contentWarningDescription state to ShortNotePostViewModel, wired into event building and draft load/save/reset
- Update ContentSensitivityExplainer to accept description state and render an OutlinedTextField for user input
- Pass description state from ShortNotePostScreen to ContentSensitivityExplainer
- Add "Reason (optional)" placeholder string resource

https://claude.ai/code/session_01P2nkYdNiWXiAcfMPTRTVEW
2026-03-11 16:51:37 +00:00
Vitor Pamplona
84da81b62f Adds calendar events and snippets to the search query 2026-03-11 12:44:29 -04:00
Vitor Pamplona
1bd96707e5 Improving the looks of namecoin settings 2026-03-11 12:33:52 -04:00
Vitor Pamplona
3bd2f4251b Increasing sha256 pool size 2026-03-11 12:33:11 -04:00
Vitor Pamplona
2312bea10d Merge pull request #1807 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-11 12:30:59 -04:00
Vitor Pamplona
5437ee98b4 updates readme 2026-03-11 12:29:00 -04:00
Crowdin Bot
b6829523a7 New Crowdin translations by GitHub Action 2026-03-11 16:28:06 +00:00
Vitor Pamplona
bba11b1813 Merge pull request #1805 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-11 12:26:36 -04:00
Vitor Pamplona
2649d05e25 merge with spotless apply 2026-03-11 12:24:25 -04:00
Crowdin Bot
60db83ecad New Crowdin translations by GitHub Action 2026-03-11 16:24:10 +00:00
Vitor Pamplona
0f934ee461 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add CodeSnippetEvent (NIP-C0) rendering to NoteCompose and ThreadFeedView
2026-03-11 12:22:44 -04:00
Vitor Pamplona
8d968c2fc0 Merge pull request #1806 from vitorpamplona/claude/namecoin-settings-route-qFW0S
Extract Namecoin settings to dedicated screen
2026-03-11 12:21:57 -04:00
Vitor Pamplona
c1705507a8 Adds 1.06 change log 2026-03-11 12:21:03 -04:00
Claude
3b3482e753 feat: move NamecoinSettingsSection to its own route and screen
Extract the Namecoin settings from PrivacyOptionsScreen into a
dedicated NamecoinSettingsScreen with its own Route.NamecoinSettings
route. Add a "Namecoin Settings" entry to AllSettingsScreen so users
can navigate to it directly from the settings list.

https://claude.ai/code/session_013gEw6fJYFiFoe4zETo8dpC
2026-03-11 16:19:07 +00:00
Vitor Pamplona
dccc644a9e Merge pull request #1804 from vitorpamplona/claude/add-code-snippet-rendering-0G1Mv
Add support for rendering NIP-23 Code Snippet events
2026-03-11 12:12:21 -04:00
Claude
74e44f1256 feat: add CodeSnippetEvent (NIP-C0) rendering to NoteCompose and ThreadFeedView
- Create CodeSnippet.kt with RenderCodeSnippetEvent (feed card view) and
  RenderCodeSnippetHeaderForThread (full thread view)
- Feed card shows: filename/language badge, description, 5-line code preview
  in a monospace box with rounded border
- Thread header shows: full code in scrollable monospace block plus metadata
  (license, runtime, deps)
- Add is CodeSnippetEvent branch to NoteCompose.kt RenderNoteRow when-statement
- Add is CodeSnippetEvent branch to ThreadFeedView.kt header when-statement

https://claude.ai/code/session_01X6jjsMLGBUmcaWXHJKmrEn
2026-03-11 15:46:18 +00:00
Vitor Pamplona
76090077b6 Displays % of uptime in relay settings 2026-03-11 10:59:43 -04:00
Vitor Pamplona
76e86ee5f7 adds calendar rendering to the thread view 2026-03-11 10:59:21 -04:00
Vitor Pamplona
3fdde60a37 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  fix: resolve conflicts with upstream PR #1789 merge
  fix(desktop): remove noisy debug logs and silence SLF4J warnings
  feat(desktop): add bunker heartbeat indicator to sidebar navigation
  fix(desktop): verify user pubkey via get_public_key after nostrconnect login
  fix(desktop): address code review findings for NIP-46 bunker login
  feat(desktop): add nostrconnect:// login flow for QR-based signer pairing
  revert: remove QR code scanning from login
  feat(desktop): webcam QR scanning via ffmpeg for bunker login
  feat(desktop): add webcam QR code scanning for bunker login
  feat(desktop): add QR code scanning for bunker login
  fix(desktop): clear stored credentials on logout
  feat(desktop): update login hints to mention bunker:// option
  fix(desktop): simplify settings logout to just a button
  feat(desktop): add logout option to menu bar and settings
  test: add NIP-46 test suite for desktop, quartz, and fix pre-existing chess test errors
  feat(desktop): add NIP-46 remote signer (bunker://) login
2026-03-11 08:42:44 -04:00
Vitor Pamplona
71574a5ce0 tracks and displays connection tentatives on relay settings 2026-03-11 08:42:32 -04:00
Vitor Pamplona
ca9074e6f5 better relay parsing messaging 2026-03-11 08:17:14 -04:00
Vitor Pamplona
4576204c1d Fixes relay list backup 2026-03-11 08:10:50 -04:00
Vitor Pamplona
190bdb0795 Merge pull request #1803 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-11 07:51:20 -04:00
Crowdin Bot
c67781f87d New Crowdin translations by GitHub Action 2026-03-11 11:49:38 +00:00
Vitor Pamplona
d01b5aba4e Merge pull request #1791 from nrobi144/feat/nip46-bunker-login
feat(desktop): NIP-46 bunker login with heartbeat indicator
2026-03-11 07:47:38 -04:00
nrobi144
f0ffe3cbe3 fix(ui): remove duplicate back button from SinglePaneLayout
Overlay screens (UserProfileScreen, ThreadScreen) already render their
own back+title header. Remove the redundant SinglePaneHeader bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:36:46 +02:00
nrobi144
3c6c368582 refactor(search): extract thread-safe EventDeduplicator, unify dedup
Replace raw MutableSet with synchronized EventDeduplicator class.
Remove dual dedup in addNoteResults — trackRelayEvent now gates all
event processing. SearchScreen callbacks skip dupes early via return
value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:36:39 +02:00
nrobi144
edf764709d fix: resolve conflicts with upstream PR #1789 merge
- Add missing commons domain files (BunkerLoginUseCase, NostrConnectLoginUseCase)
- Restore CoroutineScope in NostrSignerRemote for suspend decrypt call
- Update isolation test: upstream removed `since` filter (PR #1789)
- Add new INostrClient interface methods to TrackingNostrClient mock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:35:56 +02:00
nrobi144
36efbd1544 fix(desktop): remove noisy debug logs and silence SLF4J warnings
Remove feedSub/contactList debug prints from FeedScreen, suppress
GiftWrapEvent decryption failure log (expected for others' wraps),
and add slf4j-nop to silence "No SLF4J providers" console warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:20:19 +02:00
nrobi144
4d1d92db39 feat(desktop): add bunker heartbeat indicator to sidebar navigation
Pulsating green heart icon shows NIP-46 signer connection status in both
DeckSidebar and SinglePaneLayout NavigationRail. Tooltip displays last
ping time. SignerConnectionState moved to commons for KMP sharing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:20:18 +02:00
nrobi144
5475d6c2bc fix(desktop): verify user pubkey via get_public_key after nostrconnect login
The nostrconnect flow was trusting params[0] from the signer's connect
message as the user's pubkey. Some signers (e.g. nsec.app) don't put
the actual user pubkey there, causing all relay subscriptions to query
the wrong identity — contact lists, profiles, and DMs all returned empty.

Now calls remoteSigner.getPublicKey() after the handshake to get the
verified pubkey from the signer. Also stabilizes FeedScreen relay
subscriptions with distinctUntilChanged() and adds relay.primal.net
to defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:20:18 +02:00
nrobi144
3a1e38a2f2 fix(desktop): address code review findings for NIP-46 bunker login
- Add bounds check in fromBunkerUri for missing query params (P2-1)
- Preserve bunker keys on transient force-logout (P2-3)
- Replace debug printlns with DebugConfig.log() (P2-4)
- Replace unused Unstable with Disconnected state (P2-5)
- Move validateBunkerUri to account package (P3-9)
- Fix Compose state update from IO thread (P3-11)
- Add disconnectNip46Client() to onDispose cleanup (P3-12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:10 +02:00
nrobi144
12c91aae3d feat(desktop): add nostrconnect:// login flow for QR-based signer pairing
Adds client-initiated NIP-46 (nostrconnect://) flow so users can pair
with a remote signer (e.g. Amber) by scanning a QR code instead of
manually copying a bunker:// URI.

Changes:
- Fix RemoteSignerManager to decrypt NIP-44 content before parsing
- Add CoroutineScope to NostrSignerRemote for async event callbacks
- Add loginWithNostrConnect() to AccountManager with 120s timeout
- Add QrCodeCanvas composable using ZXing for QR generation
- Add "Connect" tab to LoginCard showing QR + copyable URI + waiting state
- Wire nostrconnect flow through LoginScreen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:10 +02:00
nrobi144
bff6723ba6 revert: remove QR code scanning from login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:10 +02:00
nrobi144
d030843522 feat(desktop): webcam QR scanning via ffmpeg for bunker login
Replaces webcam-capture (broken on Apple Silicon) with ffmpeg
avfoundation capture. Opens a live preview dialog that scans
for QR codes from the FaceTime camera. Works natively on arm64.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:10 +02:00
nrobi144
6e04482981 feat(desktop): add webcam QR code scanning for bunker login
Uses webcam-capture + ZXing to scan QR codes from the camera.
Click the QR icon on the login screen to open a webcam scanner
dialog that auto-detects bunker:// URIs from Amber's QR display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:10 +02:00
nrobi144
fd1b435020 feat(desktop): add QR code scanning for bunker login
Adds a QR scan button to the login card that decodes QR codes from
clipboard images or image files using ZXing. Useful for scanning
bunker:// URIs from Amber's QR display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
5661c6df27 fix(desktop): clear stored credentials on logout
All user-initiated logouts now pass deleteKey=true to remove
nsec from secure storage, clear last_npub, and delete bunker state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
eafedda99e feat(desktop): update login hints to mention bunker:// option
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
26ea0be4c7 fix(desktop): simplify settings logout to just a button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
2346d92f16 feat(desktop): add logout option to menu bar and settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
1821b9ff71 test: add NIP-46 test suite for desktop, quartz, and fix pre-existing chess test errors
Add comprehensive test coverage for NIP-46 bunker login across quartz and desktopApp:

Quartz (4 files, 48 tests):
- ResponseParserTest: all 7 response parsers (success/error/unexpected)
- FromBunkerUriTest: URI parsing, validation, edge cases
- ConvertExceptionsTest: SignerResult→Exception mapping
- NostrConnectEventTest: canDecrypt, talkingWith, verifiedRecipientPubKey

Desktop (6 files, 45 tests):
- BunkerUriUtilsTest: validateBunkerUri + stripBunkerSecret
- AccountManagerKeyLoginTest: nsec/npub/invalid login, save, generate
- AccountManagerLogoutTest: logout, forceLogout, state transitions
- AccountManagerLoadAccountTest: internal/bunker/missing-key scenarios
- AccountManagerBunkerLoginTest: hasBunkerAccount, setConnectingRelays
- AccountManagerHeartbeatTest: start/stop, no-crash with internal signer

Production changes:
- AccountManager: constructor private→internal, add homeDir param for test injection, extract stripBunkerSecret to internal top-level, constants internal
- desktopApp/build.gradle.kts: add mockk test dependency

Fix pre-existing chess test compilation errors:
- ChessStateReconstructorTest: add missing jester subpackage imports
- ChessGameEventTest: altText()→alt() + add nip31Alts import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:16:09 +02:00
nrobi144
6c24c52104 feat(desktop): add NIP-46 remote signer (bunker://) login
Support bunker:// URI login for desktop, enabling private key delegation
to remote signers (nsec.app, Amber). Includes heartbeat monitoring,
force-logout on revocation, and ConnectingRelays startup state.

- AccountManager: bunker login/save/load, heartbeat ping, force logout
- LoginCard: auto-detect bunker:// URI, validation, connecting state
- LoginScreen: wire bunker callback, ConnectingRelays screen
- Main.kt: relay timeout, error recovery, scope cleanup, data objects
- ForceLogoutDialog: alert on signer revocation/disconnect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:15:24 +02:00
nrobi144
979d07679c fix(search): loading indicator, result delivery, and history race
- Replace boolean isSearching with counter-based activeSubscriptionCount
  so loading state stays true until both subs (people + notes) complete
- Add indeterminate LinearProgressIndicator overlaid on search bar bottom
  border with matching 12dp rounding, replacing "Searching relays..." text
- Send search REQs to all configured relays (relayStatuses) instead of
  only connected ones — fixes NIP-50 relays never receiving search queries
- Move clearResults/startSearching out of remember() into LaunchedEffect
  to avoid side effects during composition
- Fix history saving partial queries by replacing LaunchedEffect with
  snapshotFlow to properly observe isSearching transitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:29:27 +02:00
nrobi144
a702dcc0ee fix(search): cleanup review findings — dead code, FQN refs, naming
- Remove unused `key` param from ExpandableSection
- Replace FQN references with imports in AdvancedSearchPanel and SearchScreen
- Use MetadataEvent.KIND instead of magic `0`
- Remove dead `relayStatuses` collection (only `connectedRelays` used)
- Remove CachedUserResult sealed variant (never constructed)
- Rename SearchHint `identifier` param to `example`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 07:28:04 +02:00
nrobi144
e8e2650fa9 refactor(search): extract shared code to commons, remove dead code
- Extract DateUtils (isLeapYear, dateToUnix, timestampToDate) to deduplicate
  identical implementations in QueryParser and QuerySerializer
- Move SavedSearch data class from desktopApp to commons/search
- Replace local formatTimestamp with existing toTimeAgo from commons/util
- Remove dead code: resolveAuthorName, _authorSuggestions (never called)
- Remove dead code: createOrFilters (never wired into SearchScreen)
- Remove unused ICacheProvider dependency from AdvancedSearchBarState

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 07:21:17 +02:00
Vitor Pamplona
9f903709e7 Move location permission watcher to the outside of the screens to avoid recreating them 2026-03-10 20:01:55 -04:00
Vitor Pamplona
8cd9449ba6 Improves upload of music and documents. 2026-03-10 19:49:15 -04:00
Vitor Pamplona
8a03f0b501 Replaces Aboutme for NIP-05 in the user search 2026-03-10 19:12:27 -04:00
Vitor Pamplona
2567a7c022 Better alignment and options in the UI. 2026-03-10 18:56:00 -04:00
Vitor Pamplona
178fcbb4c1 removes unused imports 2026-03-10 18:47:00 -04:00
Vitor Pamplona
cc4e81146f Saves the position of the screen on import follows dialog 2026-03-10 18:46:18 -04:00
Vitor Pamplona
7a3794eabb New string resource 2026-03-10 18:19:10 -04:00
Vitor Pamplona
a8d4705fa6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-10 16:58:24 -04:00
Vitor Pamplona
a1e90b8c9f save the lazy list state and make it scroll in the search view model to avoid coming back on another feed position 2026-03-10 16:56:31 -04:00
Vitor Pamplona
26b7efafe8 enforces specific keys when using nip05/namecoin 2026-03-10 16:41:59 -04:00
Vitor Pamplona
ef1d7e1249 Merge pull request #1800 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-10 16:29:14 -04:00
Vitor Pamplona
9a3b11e522 Adds the ability to upload any file form any new post screen 2026-03-10 16:23:02 -04:00
Crowdin Bot
ffff1399b9 New Crowdin translations by GitHub Action 2026-03-10 20:18:49 +00:00
Vitor Pamplona
087e5878a6 Fixes some compilation issues 2026-03-10 16:14:38 -04:00
Vitor Pamplona
c0e97c19c0 Improves wording to download to the phone as opposed to the profile gallery 2026-03-10 16:07:45 -04:00
Vitor Pamplona
b4184d2235 Moves the upload file button closer to the image upload 2026-03-10 15:54:03 -04:00
Vitor Pamplona
b2a8a422b2 - Breaks the new Import Follow interface into two screens
- Improves user suggestion search by evaluating specific nip-05s and npubs
2026-03-10 15:48:04 -04:00
Vitor Pamplona
dbaa02cdb0 merge 2026-03-10 15:29:28 -04:00
Vitor Pamplona
cdc9ff783d Accepts INIP05Client on the check and update function 2026-03-10 14:39:33 -04:00
David Kaspar
8f5e69e5ba Merge pull request #1799 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-10 18:38:29 +00:00
Vitor Pamplona
558e8a2dd2 Adds constructor to DualCase class 2026-03-10 14:37:55 -04:00
Vitor Pamplona
1de38ad9ec Creates an interface for Nip05Client 2026-03-10 14:37:40 -04:00
Crowdin Bot
132959a5cf New Crowdin translations by GitHub Action 2026-03-10 18:23:38 +00:00
Vitor Pamplona
9b233c8679 no message 2026-03-10 14:23:31 -04:00
Vitor Pamplona
835001bd6c Making nip05 client stable 2026-03-10 14:22:06 -04:00
Vitor Pamplona
ccb3ca69dc Merge pull request #1798 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-10 14:21:39 -04:00
Vitor Pamplona
d69a9fdde1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  fix: preserve search screen scroll position across navigation
  feat: add audio and PDF file upload support to ShortNotePostScreen
  New Crowdin translations by GitHub Action
2026-03-10 13:50:13 -04:00
Vitor Pamplona
9e105e017d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  update cz, pt, de, sv
  New Crowdin translations by GitHub Action
2026-03-10 13:49:26 -04:00
Crowdin Bot
98a09db9b0 New Crowdin translations by GitHub Action 2026-03-10 17:47:16 +00:00
Vitor Pamplona
7b8352afe2 Merge pull request #1795 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-10 13:45:34 -04:00
Vitor Pamplona
cdecc2087c Merge pull request #1796 from vitorpamplona/claude/add-audio-pdf-upload-dy1dy
Add file upload support for audio and PDF documents
2026-03-10 13:45:23 -04:00
Vitor Pamplona
4ad9e435e0 Merge pull request #1797 from vitorpamplona/claude/fix-search-scroll-position-U10Lv
Persist search screen scroll position across navigation
2026-03-10 13:44:24 -04:00
Claude
a64e9bb3cf fix: preserve search screen scroll position across navigation
Replace ephemeral rememberLazyListState() with rememberForeverLazyListState()
so the scroll position is persisted when navigating to a search result and back.

https://claude.ai/code/session_01PmSFQqSMzP51mtqPLwFEtp
2026-03-10 15:15:27 +00:00
Vitor Pamplona
ab9ecdc939 Adds methods to follow a bunch of users at the same time 2026-03-10 11:12:23 -04:00
Claude
a7cec6f5fd feat: add audio and PDF file upload support to ShortNotePostScreen
Add a new "attach file" button that opens a document picker filtered to
audio/* and application/pdf MIME types. This uses OpenMultipleDocuments
contract since PickVisualMedia only supports images/videos.

- Add SelectFromFiles composable with AttachFile icon button
- Add isAudio() and isDocument() helpers to SelectedMedia
- Add FilePreviewPlaceholder for audio/PDF thumbnail display
- Add upload_file string resource

https://claude.ai/code/session_018naEzfHjLRQLgaWAtY4aSz
2026-03-10 14:48:47 +00:00
Vitor Pamplona
b8f9b18506 Moves the account import idea to a route after login 2026-03-10 10:19:18 -04:00
Crowdin Bot
bc7d0fd758 New Crowdin translations by GitHub Action 2026-03-10 14:07:56 +00:00
David Kaspar
f42939c3b0 Merge pull request #1794 from davotoula/update-translations
update cz, pt, de, sv
2026-03-10 14:04:37 +00:00
davotoula
1e65228488 update cz, pt, de, sv 2026-03-10 14:56:33 +01:00
David Kaspar
b749fbdb88 Merge pull request #1793 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-10 13:37:53 +00:00
Crowdin Bot
1aa368221a New Crowdin translations by GitHub Action 2026-03-10 13:30:51 +00:00
Vitor Pamplona
ff4356ce31 Moves away from calling relay feeds "favorites" 2026-03-10 09:26:09 -04:00
Vitor Pamplona
3146b91c59 Make user import stable 2026-03-10 09:13:16 -04:00
Vitor Pamplona
55477429be removes old null mute lists from top nav bar 2026-03-10 09:13:05 -04:00
Vitor Pamplona
c4b4e1b5c8 adds a button to see the feed in the RelayInformationScreen 2026-03-10 08:51:10 -04:00
Vitor Pamplona
50a578cfdc Fixes preview of relay information screen 2026-03-10 08:50:51 -04:00
Vitor Pamplona
741a2e13b5 Fixes imports in NoteCompose 2026-03-10 08:44:53 -04:00
Vitor Pamplona
9ca5fc0496 Fixed rendering of calendar event previews 2026-03-10 08:43:59 -04:00
Vitor Pamplona
9c8bac7b0b Merge pull request #1792 from vitorpamplona/claude/add-calendar-event-rendering-HsY4W
Add support for rendering NIP-52 calendar events
2026-03-10 08:28:30 -04:00
Claude
fec2f2dfc8 feat: add @Preview functions for RenderCalendarTimeSlotEvent and RenderCalendarDateSlotEvent 2026-03-10 12:26:42 +00:00
Claude
2a2bdc8026 feat: add calendar event rendering for NIP-52 CalendarTimeSlotEvent and CalendarDateSlotEvent
Adds UI rendering for kind 31922 (CalendarDateSlotEvent) and kind 31923
(CalendarTimeSlotEvent) in the note feed. Each event renders with a header
image, title, date/time range, location, and summary.
2026-03-10 12:20:07 +00:00
Vitor Pamplona
376e34bde6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  fix(quartz): connect() returns Unit, caller calls getPublicKey separately
  fix(quartz): fix NIP-44 key mutation and NIP-46 connect response handling
  feat: add custom ElectrumX server settings for Namecoin resolution
  feat: import follow list with Namecoin resolution during signup
2026-03-10 08:20:03 -04:00
Vitor Pamplona
70af50b74f Adds default Relay Feeds 2026-03-10 08:19:49 -04:00
Vitor Pamplona
21d1734365 Merge pull request #1786 from mstrofnone/feat/custom-electrumx-settings
feat: Custom ElectrumX server settings for Namecoin resolution
2026-03-10 08:14:36 -04:00
Vitor Pamplona
97a3fcc9df Merge pull request #1785 from mstrofnone/feat/import-follow-list-namecoin
feat: import follow list with Namecoin resolution during signup
2026-03-10 08:14:24 -04:00
Vitor Pamplona
88e6825028 Merge pull request #1789 from nrobi144/fix/nip46-quartz-bugs
fix(quartz): fix NIP-44 key mutation and NIP-46 connect response handling
2026-03-10 08:08:08 -04:00
nrobi144
b3952ce9db feat(search): add keyboard shortcuts, search history UI, and operator hints
Phase 6: Integration and polish.
- Escape key closes advanced panel or clears search
- Search history shown in empty state (recent + saved searches)
- Auto-save to history when search completes
- Updated empty state with operator hint examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:15:14 +02:00
nrobi144
e1abb037be feat(search): add SearchHistoryStore with recent queries and saved searches
Phase 5: Persistent search history using Java Preferences API.
- Last 20 queries stored, deduped by serialized text
- Saved searches with labels and timestamps
- Uses QuerySerializer/QueryParser for round-trip serialization (no new deps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:13:34 +02:00
nrobi144
55154dddb2 feat(search): add advanced search UI with panel, results list, and SearchScreen rewrite
Phase 4: Desktop UI composables for advanced search.
- Rewrite SearchScreen.kt to use AdvancedSearchBarState with TextFieldValue
- Add AdvancedSearchPanel with kind presets, author chips, date range, hashtags, language
- Add SearchResultsList with sticky headers for People/Notes/Articles/Other sections
- Make QueryParser.parseDateToTimestamp and QuerySerializer.timestampToDate public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:12:17 +02:00
nrobi144
8265d3f70a feat(search): add AdvancedSearchBarState with bidirectional sync
Phase 3: State holder with MutableStateFlow<SearchQuery> as SSOT,
sourceOfChange discriminator to prevent parse/serialize loops,
debounced query for subscriptions, results management, author
name resolution via cache autocomplete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:44:57 +02:00
nrobi144
c3a45822d5 feat(search): add SearchFilterFactory and SearchResultFilter
Phase 2: SearchFilterFactory converts SearchQuery → List<Filter> with
default 3-group kind splitting (Android parity), NIP-50 inline
extensions, OR query support. SearchResultFilter handles client-side
exclusions, reply/media pseudo-kind detection, dedup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:43:48 +02:00
nrobi144
ae76b86ab6 feat(search): add query engine — SearchQuery, QueryParser, QuerySerializer, KindRegistry
Phase 1 of advanced search. Recursive descent parser for operators
(from:, kind:, since:, until:, lang:, domain:, #tag, -exclude, OR),
serializer for bidirectional sync, kind alias registry with pseudo-kind
support. 68 unit tests all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:42:13 +02:00
nrobi144
e56b0d2b83 fix(quartz): connect() returns Unit, caller calls getPublicKey separately
Per Amber maintainer: connect never returns a pubkey — response is
"ack" or the secret string. Rewrote ConnectResponse.parse() to check
result/error fields directly on base BunkerResponse (matching Jackson
deserialization). connect() is now a pure protocol handshake; callers
call getPublicKey() separately as a domain concern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:24:57 +02:00
Vitor Pamplona
e47299cd6b Rendering a fake waveform when playing music without any video frames. 2026-03-09 16:32:50 -04:00
Vitor Pamplona
328fd5221c Reducing the distance from the center to the side seek buttons on video playback 2026-03-09 13:02:15 -04:00
Vitor Pamplona
eafbd450ea changes player's min height to match the gradients on controllers. 2026-03-09 13:00:02 -04:00
Vitor Pamplona
68997b8033 Always render video playback center buttons on the top 2026-03-09 12:59:10 -04:00
Vitor Pamplona
05b3a122f5 Positions search relays after users in the search result list 2026-03-09 12:48:48 -04:00
Vitor Pamplona
7b3a745548 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-09 12:41:16 -04:00
Vitor Pamplona
1aa61a156d If tying a relay in the search screen, adds it to the results. 2026-03-09 12:37:58 -04:00
Vitor Pamplona
bdf4add64f Adds a list of users of a relay to the relay information dialog. 2026-03-09 12:24:10 -04:00
Vitor Pamplona
a34ba7cea8 Adds relay search to the search page 2026-03-09 11:46:21 -04:00
Vitor Pamplona
2387a1bb91 Improves the order of search results 2026-03-09 11:25:00 -04:00
Vitor Pamplona
76598c304e Improvements to profile page 2026-03-09 10:44:33 -04:00
David Kaspar
8e10bd1911 Merge pull request #1790 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-09 13:12:51 +00:00
Crowdin Bot
40bbd62725 New Crowdin translations by GitHub Action 2026-03-09 12:47:21 +00:00
Vitor Pamplona
961b8cf19e Fixes user search/list not resetting
Fixes modifier bug
2026-03-09 08:43:29 -04:00
nrobi144
39393d328b fix(quartz): fix NIP-44 key mutation and NIP-46 connect response handling
Two bugs in quartz's NIP-46 remote signer:

1. FixedKey.getEncoded() returned the raw byte array reference. javax.crypto.Mac
   zeroes the key via destroy() after HMAC computation, corrupting the NIP-44
   saltPrefix ("nip44-v2") after the first computeConversationKey call. All
   subsequent NIP-44 decryptions with different key pairs fail with Invalid Mac.
   Fix: return key.copyOf() instead of key.

2. NostrSignerRemote.connect() only handled pubkey responses. Amber (and other
   signers) may respond with "ack" or "already connected" error. Added
   ConnectResponse parser that maps these to success and falls back to
   getPublicKey() to retrieve the actual pubkey.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:49:58 +02:00
Vitor Pamplona
af4404c86e Improves display of the drop down when adding relays. 2026-03-08 13:47:11 -04:00
Vitor Pamplona
7fa9fdff9e When adding new relays, show full information dialog 2026-03-08 13:07:29 -04:00
Vitor Pamplona
693931ed15 Removes the need to start user searches with @ in the user fields across the app. 2026-03-08 12:43:59 -04:00
Vitor Pamplona
785ef8f2f3 Final touches 2026-03-08 12:31:10 -04:00
Vitor Pamplona
92da66059b Opens the amount dialog when pressing the zap button without any zap setup 2026-03-08 12:31:03 -04:00
Vitor Pamplona
db27b876ff Refines new Zap dialog screen 2026-03-08 12:17:41 -04:00
Vitor Pamplona
8912e55269 text improvements 2026-03-08 12:12:08 -04:00
Vitor Pamplona
c1e8231ed9 Merge pull request #1784 from vitorpamplona/claude/improve-zap-settings-ui-D36RD
feat: improve Zap Settings UI with sections, chips, and animations
2026-03-08 11:59:13 -04:00
Vitor Pamplona
f39de5a078 Merge pull request #1788 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-08 10:04:24 -04:00
Crowdin Bot
0b59a6233e New Crowdin translations by GitHub Action 2026-03-08 14:00:37 +00:00
Vitor Pamplona
44a2f11963 Merge pull request #1787 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-08 09:59:38 -04:00
Crowdin Bot
5cf661dd8b New Crowdin translations by GitHub Action 2026-03-08 13:59:11 +00:00
Vitor Pamplona
991f1dfca5 spotless apply 2026-03-08 09:58:41 -04:00
Vitor Pamplona
be09cc8aa3 Merge pull request #1783 from vitorpamplona/claude/improve-urldetector-readability-lqyci
refactor: improve URLDetector readability
2026-03-08 09:57:56 -04:00
M
55c45480b4 feat: add custom ElectrumX server settings for Namecoin resolution
Add a 'Namecoin Resolution' section that lets users configure custom
ElectrumX servers for .bit / d/ / id/ name resolution. When custom
servers are configured, they are used EXCLUSIVELY — the hardcoded
public defaults are completely ignored. This gives privacy-conscious
users full control over which servers observe their name lookups.

Server format: host:port (TLS default) or host:port:tcp (plaintext).
Supports .onion addresses for Tor-routed lookups.

New files:
  NamecoinSettings.kt
    Immutable settings data class with server string parsing/formatting.

  NamecoinSharedPreferences.kt
    DataStore-backed persistence following the TorSharedPreferences
    pattern. Provides a synchronous snapshot for the serverListProvider
    lambda and observable StateFlow for the UI.

  NamecoinSettingsSection.kt
    Compose UI: enable/disable toggle, active server display with
    DEFAULT/CUSTOM badge, add/remove servers with validation, reset.

  NamecoinSettingsTest.kt
    Unit tests for parsing, formatting, round-trips, and edge cases.

Integration:
  AppModules.kt — added namecoinPrefs (NamecoinSharedPreferences) and
  wired customServersOrNull into the existing serverListProvider so
  user-configured servers take priority over the Tor/default logic.

Refactored from an earlier patch to avoid duplicating code already in
main. The quartz-layer ElectrumXClient, NamecoinNameResolver, and
supporting types are reused unchanged.
2026-03-08 16:16:22 +11:00
M
8456b38dea feat: import follow list with Namecoin resolution during signup
Add a new signup step that lets users bootstrap their feed by importing
another profile's follow list. Supports all identifier types:

  - npub1... (NIP-19 bech32)
  - 64-char hex pubkey
  - alice@example.com (NIP-05 HTTP)
  - alice@example.bit / d/name / id/name (Namecoin blockchain)

Flow: Enter identifier → Resolve → Fetch kind 3 → Preview with
select/deselect → Apply follows → Done. Users can search multiple
profiles before continuing.

Changes to existing files:
  - NamecoinNameResolver: add resolveDetailed() + NamecoinResolveOutcome
  - NamecoinNameService: add resolveDetailed(), resolvePubkey()
  - AccountSessionManager: add isNewAccount flag, finishNewAccountSetup()
  - AccountScreen: route new accounts to ImportFollowListSection

New files:
  - FollowListImporter: identifier resolution + kind 3 fetch + parsing
  - ImportFollowListViewModel: state machine for the import flow
  - ImportFollowListScreen: UI with preview, select/deselect, search again
  - FollowListImporterTest: 14 unit tests
2026-03-08 15:38:54 +11:00
Claude
233d0bf089 feat: improve Zap Settings UI with sections, chips, and animations
- Organize zap settings into three clear sections with primary-colored
  headers: Quick Zap Amounts, Zap Privacy, and Nostr Wallet Connect
- Replace plain Buttons with InputChip for amount selection — each chip
  shows a lightning bolt, the amount, and a close icon to remove it
- Add animateContentSize() to the chips FlowRow so adding/removing
  amounts animates smoothly
- Add descriptive sub-text under each section header explaining what
  the control does
- NWC section: show an animated "Connected / Not Connected" status badge
  (green CheckCircle vs grey RadioButtonUnchecked) that cross-fades via
  AnimatedContent when the connection state changes
- NWC section: add animateColorAsState for the status color transition
- Replace the bare icon-only Add button with an OutlinedButton labelled
  "Connect Wallet" for better discoverability
- Move the manual pubkey/relay/secret fields into a collapsible
  "Advanced" row animated with AnimatedVisibility (expandVertically +
  fadeIn/fadeOut). The section auto-expands when a connection already
  exists or when a QR/clipboard import populates the pubkey field
- Add 9 new string resources for the new UI copy

https://claude.ai/code/session_01QzBq319fsxhrT91bGxws6N
2026-03-08 02:09:28 +00:00
Claude
812f465608 refactor: improve URLDetector readability
- UrlDetector: simplify space handler in readDefault() — both branches
  of if/else were calling the same readEnd(InvalidUrl), making the
  conditional dead code; now we always call readEnd after attempting
  to read the domain
- UrlDetector: remove redundant inner buffer.isNotEmpty() check in
  readEnd(), already guarded by the outer condition
- UrlDetector: rename processColon() parameter from length to
  startLength to eliminate the var length = length self-shadowing
- UrlDetector: replace ArrayList<Url>() with mutableListOf<Url>()
  for idiomatic Kotlin
- DomainNameReader: replace lastSection.deleteRange(0, lastSection.length)
  with lastSection.clear() which expresses the intent directly
- DomainNameReader: move isDotPercent() and isXn() extension functions
  from inside the class body to private file-level functions — extension
  functions defined on a class instance are unusual and confusing

https://claude.ai/code/session_01Pci2AC45yQxQw6F6g7jWdd
2026-03-08 00:54:38 +00:00
David Kaspar
46d54d2826 Merge pull request #1782 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-07 22:09:58 +00:00
Crowdin Bot
e94985c614 New Crowdin translations by GitHub Action 2026-03-07 22:03:53 +00:00
Vitor Pamplona
9054e45310 Fixes test case 2026-03-07 16:55:12 -05:00
Vitor Pamplona
2059f46c42 Make Urls Stable 2026-03-07 16:55:04 -05:00
Vitor Pamplona
e6e697a5f8 Adds the full list of URLs to the Desktop app 2026-03-07 16:54:47 -05:00
Vitor Pamplona
0e3ca7ac35 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: (35 commits)
  New Crowdin translations by GitHub Action
  fix chess tests
  Replace subsequent checks with '!isNullOrEmpty()' call
  use forEach: no intermediate list created.
  Use filterIsInstance to avoid dual iteration
  Lambda argument should be moved out of parentheses
  replace null checks with Elvis
  merge call chain with flatMap (slight performance improvement)
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
  was this meant to be withContext?
  add names to boolean params
  remove unused imports
  Explicit type arguments can be inferred
  remove redundant modifiers
  correct package name
  If-Null return/break/... foldable to '?:'
  Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
  Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
  ...

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt
2026-03-07 16:47:16 -05:00
Vitor Pamplona
966d02d89e Removes top level domain number of char limitations to accept nostr: uris 2026-03-07 16:41:11 -05:00
Vitor Pamplona
e3f99051db Applies new UrlDetector to the RichTextViewer 2026-03-07 16:38:25 -05:00
Vitor Pamplona
030921c963 Adds treatment for the transition between ASCII and non-ASCII in the middle of urls 2026-03-07 15:19:24 -05:00
Vitor Pamplona
5e1a193f78 only http for the url previews 2026-03-07 11:08:38 -05:00
Vitor Pamplona
109a5e7cd7 Adds single label domains to parse nostr: urls too 2026-03-07 10:51:19 -05:00
David Kaspar
864555bc0f Merge pull request #1781 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-07 07:31:54 +00:00
Crowdin Bot
ec0fca24c6 New Crowdin translations by GitHub Action 2026-03-07 01:23:23 +00:00
Vitor Pamplona
feeba9bf90 Merge pull request #1780 from davotoula/lint-fixes-pt2
Lint fixes pt2
2026-03-06 20:21:54 -05:00
Vitor Pamplona
6318c54214 Merge pull request #1779 from davotoula/quartz-fix-jvm-chess-tests
fix chess tests
2026-03-06 20:21:12 -05:00
davotoula
5658b14ad4 fix chess tests 2026-03-06 23:26:40 +01:00
davotoula
b713b01af1 Replace subsequent checks with '!isNullOrEmpty()' call 2026-03-06 22:47:29 +01:00
davotoula
f8187bad14 use forEach: no intermediate list created. 2026-03-06 22:38:05 +01:00
davotoula
03171f97a5 Use filterIsInstance to avoid dual iteration 2026-03-06 22:21:03 +01:00
davotoula
6bfb508ddc Lambda argument should be moved out of parentheses 2026-03-06 22:17:32 +01:00
davotoula
d7d9352a90 replace null checks with Elvis 2026-03-06 22:09:19 +01:00
davotoula
4e614a3179 merge call chain with flatMap (slight performance improvement) 2026-03-06 21:51:26 +01:00
David Kaspar
a89bfd0b05 Merge pull request #1778 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-06 20:47:40 +00:00
Crowdin Bot
3bf79355b8 New Crowdin translations by GitHub Action 2026-03-06 20:46:29 +00:00
David Kaspar
b61ba78eb3 Merge pull request #1777 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-06 20:45:03 +00:00
Crowdin Bot
82fd15272d New Crowdin translations by GitHub Action 2026-03-06 20:43:36 +00:00
David Kaspar
069a19b3f5 Merge pull request #1776 from davotoula/update-translations
update cz, pt, de, sv
2026-03-06 20:42:07 +00:00
Vitor Pamplona
5d9687fb47 Merge pull request #1771 from mstrofnone/namecoin-nip05-improvements
fix: address race conditions and improve Namecoin NIP-05 resolution
2026-03-06 15:13:20 -05:00
Vitor Pamplona
976a9d5c77 Merge pull request #1774 from davotoula/lint-fixes
Lint fixes
2026-03-06 15:12:43 -05:00
Vitor Pamplona
1729a55add Merge pull request #1775 from davotoula/potential-bug-note-compose
Typo / potential bug?
2026-03-06 15:12:19 -05:00
davotoula
8d212b8f03 update cz, pt, de, sv 2026-03-06 21:04:26 +01:00
davotoula
aec0101769 was this meant to be withContext? 2026-03-06 20:10:35 +01:00
davotoula
7f0b2c500f add names to boolean params 2026-03-06 19:59:20 +01:00
davotoula
582153b193 remove unused imports 2026-03-06 19:59:20 +01:00
davotoula
8868b1b0e6 Explicit type arguments can be inferred 2026-03-06 19:59:20 +01:00
davotoula
bd71e9ae8f remove redundant modifiers 2026-03-06 19:59:20 +01:00
davotoula
8329184d99 correct package name 2026-03-06 19:46:48 +01:00
davotoula
c8c42b5d70 If-Null return/break/... foldable to '?:' 2026-03-06 19:46:47 +01:00
davotoula
34e1ea92b2 Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable. 2026-03-06 19:46:47 +01:00
davotoula
99e8ffcdeb Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable. 2026-03-06 19:46:47 +01:00
davotoula
7de098063f Specifying : ExoPlayer makes the contract explicit 2026-03-06 19:46:47 +01:00
davotoula
953e8c1749 supress false positive 2026-03-06 19:46:47 +01:00
davotoula
5ac8b9ad62 Prefer mutableLongStateOf instead of mutableStateOf 2026-03-06 19:46:47 +01:00
davotoula
80fb171e0a Unnecessary; SDK_INT is always >= 26 2026-03-06 19:46:47 +01:00
davotoula
8876c3c1d7 use multi-dollar string interpolation
avoid default locale
2026-03-06 19:46:47 +01:00
davotoula
ccb5738826 Replace the API-branched calls with ContextCompat.registerReceiver() which handles the flags transparently across all API levels 2026-03-06 19:46:47 +01:00
davotoula
4ee4a00b58 Add a nested comment explaining why this function is empty 2026-03-06 19:46:47 +01:00
Vitor Pamplona
f4d401bc56 Switches to our own version of the Url Detector 2026-03-06 10:58:59 -05:00
mstrofnone
a817a2cc79 remove dead namecoinNameService declaration from AppModules
The NamecoinNameService.init() singleton was the only consumer of
the companion object removed in the previous commit. The
namecoinNameService val itself was never referenced — namecoinResolver
is what's wired into Nip05Client.
2026-03-07 00:49:05 +11:00
mstrofnone
24a03b5d20 remove unused singleton companion object from NamecoinNameService
The companion object (instance, getInstance, init) was dead code —
nothing outside the class referenced it. Koin handles instantiation
via AppModules.
2026-03-07 00:38:58 +11:00
Vitor Pamplona
ddc4daaa28 Merge pull request #1773 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-06 08:12:46 -05:00
Crowdin Bot
998e6c690b New Crowdin translations by GitHub Action 2026-03-06 13:12:29 +00:00
Vitor Pamplona
dd062f6e38 Merge pull request #1772 from greenart7c3/main
perf: optimize state management and clean up styling in ReactionsSettingsScreen
2026-03-06 08:10:13 -05:00
greenart7c3
0320ccf4a0 perf: optimize state management and clean up styling in ReactionsSettingsScreen
- Change `items` state initialization to use `toList()` instead of `toMutableList()` to ensure a fresh immutable snapshot from the flow.
- Reformat chained modifiers in `DraggableItem` for better readability.
2026-03-06 08:15:33 -03:00
M
4ecfbbb844 fix: add missing StateFlow import, merge duplicate KDoc blocks, and fix rebase conflicts
- Add missing 'import kotlinx.coroutines.flow.StateFlow' in SearchBarViewModel.kt
- Merge duplicate KDoc blocks on nameShowWithFallback in ElectrumXClient.kt
- Move NamecoinLookupException to ElectrumXServer.kt (avoid redeclaration)
- Remove duplicate NameShowResult/ElectrumxServer from ElectrumXClient.kt
- Fix ElectrumxClient -> ElectrumXClient renames in NamecoinNameService
- Extract namecoinElectrumxClient in AppModules.kt for NamecoinNameService.init()
- Update package paths from nip05.namecoin to nip05DnsIdentifiers.namecoin
2026-03-06 14:22:49 +11:00
M
050fd3a412 feat: propagate distinct error types from ElectrumX to search UI
Add NamecoinLookupException sealed class to distinguish:
- NameNotFound: name queried successfully but doesn't exist on blockchain
- NameExpired: name exists but expired (>36000 blocks since last update)
- ServersUnreachable: all ElectrumX servers failed with connection errors

Previously, all three cases returned null from nameShowWithFallback,
making it impossible for the UI to show meaningful feedback.

Changes:
- ElectrumxClient.connectAndQuery: throws NameNotFound/NameExpired
  instead of returning null for definitive blockchain answers
- ElectrumxClient.nameShow: lets NamecoinLookupException propagate
  (only catches connection/IO errors as null)
- ElectrumxClient.nameShowWithFallback: short-circuits on NameNotFound/
  NameExpired (no point trying other servers for definitive answers),
  throws ServersUnreachable when all servers fail with connection errors
- SearchBarViewModel: new namecoinSearchState flow with typed states
  (Idle/Loading/Resolved/NotFound/Expired/ServersUnreachable) for UI
  to show loading spinners, resolved profiles, and specific error
  messages. Derived namecoinResolvedUser from this flow.

Discovered while porting to notedeck (damus-io/notedeck#1314).
2026-03-06 14:15:11 +11:00
M
ee418bdd0a fix: address race conditions and improve Namecoin NIP-05 resolution
Fixes discovered while porting Namecoin NIP-05 to Primal Android
(PrimalHQ/primal-android-app#934):

1. Race condition: stale intermediate lookups overwrite final result
   - Changed map to mapLatest in SearchBarViewModel so that previous
     in-flight ElectrumX lookups are cancelled when the query changes
   - Without this, typing 'd/testls' character by character causes
     stale results from 'd/test', 'd/testl' etc. to arrive after
     the correct 'd/testls' result and overwrite it with null

2. CancellationException swallowed in catch block
   - mapLatest cancels previous coroutines via CancellationException
   - The generic catch was catching it and emitting null, wiping the
     valid result. Now re-throws CancellationException

3. Root lookup fails when names object has no underscore key
   - extractFromDomainValue tried names[localPart] then names[underscore]
     which is the same thing for root lookups
   - Now falls back to first available entry for root lookups when
     no underscore key exists
   - Non-root lookups still fail correctly (no false matches)

4. Global Mutex serializes all lookups
   - Single Mutex in ElectrumxClient blocked unrelated lookups to
     different servers. A 25s timeout on one server stalled everything
   - Now uses per-server mutexes via ConcurrentHashMap

5. customServers set but never read
   - NamecoinNameService.setCustomServers() stored the list but the
     resolver was constructed with the default serverListProvider
   - Now wires customServers through to the resolver

6. resolveLive uses orphaned CoroutineScope
   - Now accepts an optional external scope parameter so callers can
     tie resolution to their own lifecycle

Adds 2 new tests: root fallback to first entry, non-root no-fallback.
2026-03-06 14:14:29 +11:00
Vitor Pamplona
13f401df8c Moves test cases to commonTest 2026-03-05 08:34:18 -05:00
Vitor Pamplona
73c5418114 Fixes bug on Show More calculations for very long texts. 2026-03-05 08:31:01 -05:00
Vitor Pamplona
cb6391085a Improving the ui for reaction row settings 2026-03-04 17:37:52 -05:00
Vitor Pamplona
f4ec5735d3 Better description of the settings 2026-03-04 17:21:32 -05:00
Vitor Pamplona
0e124f3017 Merge pull request #1769 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 17:18:51 -05:00
Crowdin Bot
52ec6eded8 New Crowdin translations by GitHub Action 2026-03-04 22:09:54 +00:00
Vitor Pamplona
6be2c7cbdf Merge pull request #1768 from vitorpamplona/claude/add-drag-drop-reactions-43YJ2
Replace move buttons with drag-to-reorder in reactions settings
2026-03-04 17:08:14 -05:00
Claude
c2384d2082 feat: add drag-and-drop reordering for reaction row items
Replace up/down arrow buttons with a drag handle icon that supports
long-press drag gestures for reordering reaction items in settings.
Items visually lift with elevation and scale during drag, and swap
positions when dragged past neighboring item thresholds.

https://claude.ai/code/session_01RYsPFgnoeJ3BmwjFPMvTuU
2026-03-04 21:32:09 +00:00
David Kaspar
13dadeda37 Merge pull request #1767 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 20:13:57 +00:00
Crowdin Bot
9f772b0e13 New Crowdin translations by GitHub Action 2026-03-04 20:12:55 +00:00
Vitor Pamplona
9b9c2a722a Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-04 15:09:40 -05:00
Vitor Pamplona
c95a9842a4 Adds the latin1 characters to the ascii block to avoid splitting words 2026-03-04 15:09:26 -05:00
David Kaspar
82ff8010c5 Merge pull request #1766 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 19:57:11 +00:00
Crowdin Bot
3a97f89199 New Crowdin translations by GitHub Action 2026-03-04 19:42:16 +00:00
Vitor Pamplona
2f94b3cb15 Adds the latin1 characters to the ascii block to avoid splitting words 2026-03-04 14:38:48 -05:00
Vitor Pamplona
be5c1f4832 Removes encrypted data from search 2026-03-04 13:56:06 -05:00
Vitor Pamplona
a536cc6f82 Merge pull request #1764 from greenart7c3/claude/reactions-settings-HhrO0
Add customizable reactions row settings screen
2026-03-04 13:35:03 -05:00
Vitor Pamplona
4839535355 Fixes test case for new class names 2026-03-04 13:29:09 -05:00
greenart7c3
4bd0ed97aa Explicit imports 2026-03-04 14:46:44 -03:00
Claude
d2d887a782 feat: Add reactions row settings (enable/disable, order, show/hide counters)
Add a new Reactions Settings screen that allows users to:
- Enable or disable each reaction button (Reply, Boost, Like, Zap, Share)
- Reorder reaction buttons with up/down arrows
- Show or hide the counter for each reaction type

Changes:
- AccountSyncedSettingsInternal: Add ReactionRowAction enum, ReactionRowItem
  data class, and reactionRowItems field to AccountReactionPreferencesInternal
- AccountSyncedSettings: Wrap reactionRowItems in MutableStateFlow
- AccountSettings/Account: Add changeReactionRowItems methods
- AccountViewModel: Add reactionRowItemsFlow() and changeReactionRowItems()
- ReactionsRow: Add showCounter param to BoostReaction, LikeReaction,
  ZapReaction, ReplyReaction variants; refactor GenericInnerReactionRow to
  render dynamically based on settings
- New ReactionsSettingsScreen with per-button enable/counter toggles and
  move-up/move-down ordering
- Routes, AppNavigation, AllSettingsScreen: wire up new settings screen

https://claude.ai/code/session_01QizUFfWDeKjLK2SuhanipJ
2026-03-04 17:24:25 +00:00
Vitor Pamplona
fbbc506e84 updates all dependencies 2026-03-04 12:19:57 -05:00
Vitor Pamplona
1fa93788db android target seems to be creating some issues between plugin dependencies after adding spm 2026-03-04 12:19:47 -05:00
Vitor Pamplona
da01c6ccd1 Moves smp plugin to version catalog 2026-03-04 12:18:55 -05:00
Vitor Pamplona
67390bb8ec Updating Coil 2026-03-04 12:18:11 -05:00
Vitor Pamplona
d662cd6b64 Merge pull request #1757 from KotlinGeekDev/kmp-completeness
KMP Completeness, Part 1.
2026-03-04 10:27:24 -05:00
Vitor Pamplona
089600d7f0 Fixes test case 2026-03-04 10:26:21 -05:00
Vitor Pamplona
8501c86663 Further organizes Calendar events 2026-03-04 10:25:51 -05:00
Vitor Pamplona
d3bab2f8cb Separate generics functions 2026-03-04 10:19:10 -05:00
Vitor Pamplona
b563ab44b0 Merge pull request #1762 from vitorpamplona/claude/nip52-calendar-compliance-yXoXR
Add NIP-52 Calendar event builders and tag helpers
2026-03-04 10:06:03 -05:00
Vitor Pamplona
ac936dc673 minor adjustment 2026-03-04 10:02:23 -05:00
Vitor Pamplona
470b1680d0 Moves electrum client to the jvmAndroid scope 2026-03-04 10:00:33 -05:00
Vitor Pamplona
193561c336 Migrates to LruCache 2026-03-04 09:32:31 -05:00
Vitor Pamplona
c10f7a0ef2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-04 09:32:20 -05:00
Vitor Pamplona
9abd61e2de Improves package layout for nip64 chess 2026-03-04 09:25:38 -05:00
Vitor Pamplona
46dcd105c8 Organizes Chess class and common tags 2026-03-04 09:19:39 -05:00
Vitor Pamplona
4060bef2be Lint 2026-03-04 09:19:21 -05:00
KotlinGeekDev
ff7aa3df79 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-04 14:40:12 +01:00
David Kaspar
aab0785626 Merge pull request #1763 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 13:37:57 +00:00
Crowdin Bot
af589afa4d New Crowdin translations by GitHub Action 2026-03-04 13:34:05 +00:00
Vitor Pamplona
01a45d3b65 Merge pull request #1761 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 08:30:46 -05:00
Vitor Pamplona
ad578b394d Merge pull request #1759 from vitorpamplona/claude/refactor-nip64chess-structure-3iFIB
refactor: restructure nip64Chess to match nip88Polls pattern
2026-03-04 08:30:35 -05:00
Vitor Pamplona
0ebbd54498 Merge pull request #1734 from mstrofnone/feature/namecoin-nip05-resolution
feat: Namecoin NIP-05 identity verification via ElectrumX
2026-03-04 08:30:21 -05:00
Vitor Pamplona
e065cbc63b Merge branch 'main' into feature/namecoin-nip05-resolution 2026-03-04 08:29:48 -05:00
Claude
c00295f6f2 feat: implement full NIP-52 calendar spec compliance
- CalendarDateSlotEvent (31922): Add title, summary, image, geohash,
  hashtags, participants, references, multiple locations accessors and
  build() method with DSL builder pattern
- CalendarTimeSlotEvent (31923): Fix timezone bug (startTzId/endTzId
  were parsed as Long instead of String), add all accessors like
  DateSlot, rename startTmz/endTmz to startTzId/endTzId, add build()
- CalendarRSVPEvent (31925): Remove incorrect location/start/end
  accessors (not in spec), add status/freebusy/calendarEventAddress/
  calendarEventId/calendarEventAuthor accessors, update from outdated
  L/l label format to current status/fb tag format, add build()
- CalendarEvent (31924): Add title and calendarEventAddresses accessors,
  add build() method
- Create NIP-52 tag classes: LocationTag, RSVPStatusTag (with enum),
  FreeBusyTag (with enum)
- Create TagArrayBuilderExt.kt with DSL builder extensions for all
  four calendar event types

https://claude.ai/code/session_01UMXgE3tNftmqNZqcYwF2kL
2026-03-04 13:29:04 +00:00
Crowdin Bot
a5fe5836e5 New Crowdin translations by GitHub Action 2026-03-04 13:28:27 +00:00
Vitor Pamplona
755df5e60e Merge pull request #1760 from nrobi144/feat/desktop-deck-layout
feat(desktop): multi-column deck layout with single-pane toggle
2026-03-04 08:26:32 -05:00
KotlinGeekDev
4cea4e4f5d Fix issues needing a fix caught by review. 2026-03-04 13:51:46 +01:00
KotlinGeekDev
7b6db9404c Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-04 13:21:27 +01:00
nrobi144
db530a6820 fix(desktop): audit fixes for deck layout
Critical:
- GlobalFeed now renders global feed (was identical to HomeFeed)
- DeckState uses atomic _columns.update{} instead of non-atomic .value=
- lastKnownWidth marked @Volatile for thread safety

Medium:
- save() debounced 500ms via coroutine (was sync disk I/O per drag frame)
- Width redistribution fills gaps after clamping on column remove
- Hashtag column pre-fills search query
- ColumnNavigationState.stack exposed as StateFlow not MutableStateFlow
- Settings column deduped (focuses existing instead of adding duplicate)
- Exceptions logged in save/load instead of silently swallowed

Low:
- Thread column icon fixed (was Home, now Article)
- Drag divider hit target widened to 12dp (visual stays 4dp)
- Horizontal scroll added for column overflow
- Auto-fit columns on first composition / window resize
- Column shortcuts limited to actual column count
- Deterministic default column IDs
- Overlay fallback shows message instead of blank

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:02:22 +02:00
nrobi144
73b8059b55 feat(desktop): collapsible info panel in chess deck column
Hide GameInfo/MoveHistory/Actions panel by default in deck view
with a chevron toggle to expand. Add Chess to AddColumnDialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:45:01 +02:00
nrobi144
991ac3b31b merge: integrate upstream main with chess PR #1702
Resolves merge conflicts in Main.kt, adds Chess column type
to deck layout (DeckColumnType, ColumnHeader, DeckColumnContainer,
DeckState, SinglePaneLayout, View menu). Updates DesktopMessagesScreen
calls for new relayManager/localCache params.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:33:44 +02:00
David Kaspar
931ea33694 Merge pull request #1758 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-04 05:21:06 +00:00
KotlinGeekDev
b145a14ea3 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-03-04 02:02:12 +01:00
Claude
2df8a106d4 refactor: restructure nip64Chess to match nip88Polls pattern
Reorganize nip64Chess event classes into individual packages with
dedicated tag classes, TagArrayBuilderExt, and TagArrayExt files,
following the same structure used by nip88Polls.

New package structure:
- challenge/ - LiveChessGameChallengeEvent with PlayerColorTag, TimeControlTag
- accept/ - LiveChessGameAcceptEvent with ChallengeEventTag
- move/ - LiveChessMoveEvent with GameIdTag, MoveNumberTag, SanTag, FenTag
- end/ - LiveChessGameEndEvent with ResultTag, TerminationTag, WinnerTag
- draw/ - LiveChessDrawOfferEvent
- game/ - ChessGameEvent (Kind 64)
- jester/ - JesterEvent, JesterProtocol, JesterContent, JesterGameEvents

Each tag class follows the companion object pattern with isTag(),
parse(), and assemble() methods. Each event package includes
TagArrayBuilderExt for building and TagArrayExt for parsing.

https://claude.ai/code/session_01Qtzhka3p5N3Hbns4xrRbsv
2026-03-04 00:51:43 +00:00
Crowdin Bot
a37c6b5e6f New Crowdin translations by GitHub Action 2026-03-04 00:32:36 +00:00
Vitor Pamplona
546d1e9e73 Protects against invalid URLs 2026-03-03 19:30:39 -05:00
KotlinGeekDev
a820082a6a Fix linter issues. 2026-03-04 01:30:38 +01:00
Vitor Pamplona
5cf306683a Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
  New Crowdin translations by GitHub Action
2026-03-03 19:26:25 -05:00
Vitor Pamplona
96967b9e30 Clearing AccountViewModels more precisely 2026-03-03 19:17:15 -05:00
KotlinGeekDev
bbd17e4e64 Fix build issues. 2026-03-04 01:06:33 +01:00
KotlinGeekDev
a23850f002 Merge branch 'upstream-main' into kmp-completeness 2026-03-04 00:44:47 +01:00
KotlinGeekDev
68a262fa88 Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main
# Conflicts:
#	gradle/libs.versions.toml
#	quartz/build.gradle.kts
#	quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt
#	quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt
#	quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt
2026-03-04 00:37:10 +01:00
KotlinGeekDev
aaf3be98b7 Foundations(iOS Sourceset): Update SpmForKmp. Fix UnicodeNormalizer code. 2026-03-04 00:27:00 +01:00
David Kaspar
ea8ed31ba4 Merge pull request #1756 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-03 22:43:07 +00:00
Crowdin Bot
b1504da012 New Crowdin translations by GitHub Action 2026-03-03 22:40:22 +00:00
David Kaspar
9b6d2e9c3a Merge pull request #1755 from davotoula/update-translations
update cz, pt, de, sv
2026-03-03 22:39:09 +00:00
davotoula
8ad1cddb42 update cz, pt, de, sv 2026-03-03 22:36:06 +00:00
David Kaspar
c04d93fcea Merge pull request #1754 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-03 22:24:51 +00:00
Crowdin Bot
8c8fdbc27c New Crowdin translations by GitHub Action 2026-03-03 22:23:46 +00:00
David Kaspar
43133b70f4 Merge pull request #1753 from davotoula/update-translations
update cz, pt, de, sv
2026-03-03 22:22:21 +00:00
davotoula
382273647f update cz, pt, de, sv 2026-03-03 22:18:20 +00:00
Vitor Pamplona
14e6f1bdb1 Merge pull request #1752 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-03 17:07:27 -05:00
Crowdin Bot
dc817abe2c New Crowdin translations by GitHub Action 2026-03-03 21:44:54 +00:00
Vitor Pamplona
d8911ef69c Account State is more than just a view model. 2026-03-03 16:42:45 -05:00
Vitor Pamplona
2e71401342 Deletes the default scope form App Modules 2026-03-03 15:17:01 -05:00
M
cb1031705b Remove dead ElectrumX servers ulrichard.ch and nmc2.lelux.fi
ulrichard.ch:50006 — connects but never responds (timeout)
nmc2.lelux.fi:50006 — DNS resolution fails (ENOTFOUND)
2026-03-04 06:34:39 +11:00
M
c649f2163b Add nmc2.bitcoins.sk and 46.229.238.187 as backup ElectrumX servers
Both verified to resolve Namecoin names via scripthash lookups.
Port 57002, TLS with self-signed certs.
2026-03-04 06:30:50 +11:00
Vitor Pamplona
a417eb46f7 Fixes the need to have tags and kinds for inbox.nostr.wine to work 2026-03-03 13:40:26 -05:00
Vitor Pamplona
264b988b41 Fixing sending inside this function 2026-03-03 13:37:55 -05:00
Vitor Pamplona
7b80ee8bf0 Fixes the need to specify route in the check if navigation is going to the same route or not 2026-03-03 12:29:17 -05:00
Vitor Pamplona
cdbee03e93 Fixes animation for settings pages 2026-03-03 12:10:57 -05:00
Vitor Pamplona
cd6cdf7add Switch Surface use on KeyBackup to Scaffod 2026-03-03 12:10:45 -05:00
Vitor Pamplona
edb73b3556 Updates AGP 2026-03-03 11:52:03 -05:00
Vitor Pamplona
5d8452af06 lint 2026-03-03 10:54:44 -05:00
Vitor Pamplona
386afdbe8d Merge pull request #1751 from vitorpamplona/claude/improve-zapcustomdialog-ui-2Fzic
Redesign Zap Custom Dialog with improved UI/UX
2026-03-03 10:54:05 -05:00
Vitor Pamplona
7695416ca0 Merge pull request #1750 from vitorpamplona/claude/convert-dialog-to-route-9lrlG
Convert UpdateZapAmountDialog to full-screen navigation route
2026-03-03 10:51:07 -05:00
Vitor Pamplona
583891a145 Merge branch 'main' into claude/convert-dialog-to-route-9lrlG 2026-03-03 10:51:00 -05:00
Vitor Pamplona
be0e53fa08 Merge pull request #1748 from vitorpamplona/claude/convert-backup-dialog-route-ngbkf
Convert AccountBackupDialog to full-screen navigation route
2026-03-03 10:48:20 -05:00
Vitor Pamplona
1fb4860d68 Merge pull request #1749 from vitorpamplona/claude/convert-dialog-to-route-7Co1n
Convert UpdateReactionTypeDialog to screen-based navigation
2026-03-03 10:48:05 -05:00
Claude
6f80232c32 feat: improve ZapCustomDialog UI with chips and cleaner layout
- Add dialog title "Send Zap" with close button in header
- Add preset amount SuggestionChips from user's saved zap amounts
- Replace TextSpinner dropdown with FilterChip row for zap type selection
- Make amount field full-width with "sats" suffix
- Move ZapButton to bottom as full-width call-to-action
- Add imePadding for keyboard avoidance
- Add RoundedCornerShape(16dp) to dialog Surface
- Add HorizontalDivider separating header from content
- Change message keyboard capitalization to Sentences
- Add send_zap string resource

https://claude.ai/code/session_017gXfNSaqGshRth2x5tdTKz
2026-03-03 15:46:19 +00:00
Claude
9910bd0dfa feat: convert AccountBackupDialog to a navigation Route
Replaces the local dialog state in AllSettingsScreen with a proper
Route.AccountBackup, presented as a bottom-slide composable.

- Add `Route.AccountBackup` to Routes.kt (sealed class + getRouteWithArguments)
- Rename `AccountBackupDialog` → `AccountBackupScreen` accepting `INav` instead of `onClose`; remove Dialog wrapper
- Register `composableFromBottom<Route.AccountBackup>` in AppNavigation
- Update AllSettingsScreen to navigate to Route.AccountBackup instead of managing local dialog state

https://claude.ai/code/session_01KRRANB1b7wV7mpxEvpfjc7
2026-03-03 15:43:18 +00:00
Claude
fb066b3b6f feat: convert UpdateReactionTypeDialog to a route
Replace the dialog-based UpdateReactionTypeDialog with a
navigation route (Route.UpdateReactionType) that slides up
from the bottom. UpdateReactionTypeScreen replaces the Dialog
wrapper with a plain Scaffold. Call sites in AllSettingsScreen
and ReactionsRow now navigate to the route instead of managing
local dialog state.

https://claude.ai/code/session_01TSjuYdBADTRXwT4w8Yx1vU
2026-03-03 15:42:51 +00:00
Claude
c3a0141a47 feat: convert UpdateZapAmountDialog to Route
- Add Route.UpdateZapAmount to Routes.kt with optional nip47 parameter
- Create UpdateZapAmountScreen.kt as a Scaffold-based screen using SavingTopBar
- Register composableFromBottomArgs<Route.UpdateZapAmount> in AppNavigation.kt
- Replace dialog state in AllSettingsScreen with nav.nav(Route.UpdateZapAmount())
- Replace dialog state in ReactionsRow with nav.nav(Route.UpdateZapAmount())
- Remove UpdateZapAmountDialog composables, keeping UpdateZapAmountContent and authenticate helpers

https://claude.ai/code/session_01N95sLM14khkJupTfvJcoKq
2026-03-03 15:42:27 +00:00
nrobi144
afe126f393 feat(desktop): add single-pane/deck toggle with smart column resizing
- Add LayoutMode enum (SINGLE_PANE/DECK) persisted in DesktopPreferences
- SinglePaneLayout: NavigationRail + single content area reusing deck routing
- View menu "Deck Layout" toggle with Cmd+Shift+D shortcut
- Column resize: redistribute width on add/remove, double-click header to expand
- Constrain drag resize to window bounds via BoxWithConstraints
- FlowRow headers in FeedScreen/ReadsScreen to wrap on narrow columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:38:32 +02:00
Vitor Pamplona
3c32dbc701 lint 2026-03-03 10:23:27 -05:00
Vitor Pamplona
2b80ab52b7 Merge pull request #1747 from vitorpamplona/claude/nip39-kind-10011-support-EACQk
Implement NIP-39 External Identities (kind 10011) event support
2026-03-03 10:01:52 -05:00
Claude
79b06d35b9 feat: add NIP-39 kind 10011 (ExternalIdentitiesEvent) support
- Add ExternalIdentitiesEvent (kind 10011) to quartz/nip39ExtIdentities
  with createNew/updateFromPast factory methods and identityClaims()
- Generalize TagArrayBuilderExt to work with any Event type (not just
  MetadataEvent) so claims/twitterClaim etc. work with both kinds
- Extract replaceClaims logic to List<IdentityClaimTag>.replaceClaims()
  shared extension used by both MetadataEvent and ExternalIdentitiesEvent
- Register ExternalIdentitiesEvent in EventFactory (kind 10011)
- Add consume(ExternalIdentitiesEvent) to LocalCache and dispatch in
  justConsumeInnerInner
- Add ExternalIdentitiesEvent.KIND to UserMetadataForKeyKinds relay filter
  so kind 10011 is fetched alongside kind 0 when loading user profiles
- Add UserExternalIdentitiesViewModel that observes the kind 10011
  AddressableNote for a user and exposes List<IdentityClaimTag> as a Flow
- Thread UserExternalIdentitiesViewModel through ProfileScreen →
  RenderScreen → ProfileHeader → DrawAdditionalInfo
- DrawAdditionalInfo now shows kind 10011 identities, falling back to
  kind 0 for backwards compatibility
- UserMetadataState: add sendNewUserIdentities() for kind 10011 and
  remove identity claim params from sendNewUserMetadata() (kind 0)
- NewUserMetadataViewModel: load identities from kind 10011 first
  (fallback to kind 0), save identities to kind 10011 separately

https://claude.ai/code/session_017iU6ZdRu5qRXPCeDztVeeV
2026-03-03 13:47:27 +00:00
Vitor Pamplona
a005ec5354 Merge pull request #1746 from davotoula/cleaner-code
Cleaner code
2026-03-03 06:46:34 -05:00
Vitor Pamplona
d2ddb9ded5 Merge pull request #1745 from davotoula/defensive-backup-rules
defensive backup rules
2026-03-03 06:45:28 -05:00
davotoula
169416b548 Remove unnecessary suspend modifier from sentToTop() and simplify getNoteIfExists call 2026-03-03 10:06:32 +00:00
davotoula
2d42556b03 Functions shouldn't be empty 2026-03-03 10:03:01 +00:00
davotoula
b1a1ccc145 Define a constants instead of duplicating String literals 2026-03-03 09:54:54 +00:00
davotoula
b3911fedd9 defensive backup rules 2026-03-03 09:42:43 +00:00
Vitor Pamplona
0370e51e57 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add NIP-C0 code snippet support (kind:1337)
2026-03-02 19:27:27 -05:00
Vitor Pamplona
88f5843e08 A bit more of a refactoring 2026-03-02 19:25:49 -05:00
Vitor Pamplona
1c3e58303d Merge pull request #1744 from vitorpamplona/claude/add-c0-protocol-support-GAos6
feat: add NIP-C0 code snippet support (kind:1337)
2026-03-02 19:17:53 -05:00
Claude
8c12d4d692 feat: add NIP-C0 code snippet support (kind:1337)
Adds full protocol support for NIP-C0 code snippet events following the
same structure used for NIP-88 polls.

Quartz (quartz/nipC0CodeSnippets/):
- CodeSnippetEvent (kind:1337) with accessors for all optional metadata
- TagArrayExt: parse language, extension, name, description, runtime,
  license, deps (repeatable), and repo from tag arrays
- TagArrayBuilderExt: typed builder functions for CodeSnippetEvent
- Tag classes: LanguageTag (l), ExtensionTag, SnippetNameTag (name),
  SnippetDescriptionTag (description), RuntimeTag, LicenseTag, DepTag,
  RepoTag

EventFactory: register kind 1337 → CodeSnippetEvent
LocalCache: add consume(CodeSnippetEvent) and dispatch in when block

https://claude.ai/code/session_013ykLNfJNdwWpXh8ZhZaSXY
2026-03-02 23:48:43 +00:00
Vitor Pamplona
27d1a00196 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-02 18:35:34 -05:00
Vitor Pamplona
624cf26c29 Switches the public message API to use quoted posts on replies 2026-03-02 18:22:45 -05:00
Vitor Pamplona
934b1fd5ed First chess refactoring 2026-03-02 18:14:08 -05:00
David Kaspar
fb6d582560 Merge pull request #1743 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-02 22:44:41 +00:00
Vitor Pamplona
df72a6e094 Fix floating action 2026-03-02 17:43:52 -05:00
Vitor Pamplona
d3343c2747 Fix wrong to string usage 2026-03-02 17:43:34 -05:00
Crowdin Bot
f72838139c New Crowdin translations by GitHub Action 2026-03-02 22:35:03 +00:00
Vitor Pamplona
254ef7081b Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add NIP-66 relay monitor and discovery support to quartz
2026-03-02 17:32:37 -05:00
Vitor Pamplona
c5cbc1887f Moves all event kind names to strings.xml 2026-03-02 17:29:42 -05:00
Vitor Pamplona
15c961670e Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  always show playback speed and position/duration text in white regardless of theme
  New Crowdin translations by GitHub Action
  extract rememberSaveMediaAction to eliminate toast/permission duplication
  restore download toast and permission check fix overflow button background to solid
  Code review fixes: reduce duplicate code
  Code review fixes: Skip-forward clamps against unknown duration Share dropdown is no longer anchored to the overflow button simplify: keep skip amount fixed at 10
  Code review fixes: Redundant Box in SkipButton Unused string resources remember { fadeIn() } pattern
  Code review fixes: skipSeconds duplication + stale duration read pointerInput(Unit) stale capture ShareMediaAction composed unconditionally Overflow menu background opaque in dark theme
  simplify top buttons, move to overflow add skip buttons add gradient overlays and double-tap gesture support
  add skip button component, overflow menu component, gradient overlay
2026-03-02 17:18:40 -05:00
Vitor Pamplona
85330f932c Merge pull request #1742 from vitorpamplona/claude/add-nip66-support-GmCWu
feat: add NIP-66 relay monitor and discovery support to quartz
2026-03-02 17:17:33 -05:00
Vitor Pamplona
7ed0909169 Blocks the size of RelayAuthStatus arrays from growing forever with auth messages 2026-03-02 17:09:52 -05:00
Vitor Pamplona
c3de179124 Merge pull request #1714 from davotoula/media3-updated-player-controls
Media3 updated player controls
2026-03-02 17:08:05 -05:00
davotoula
c335d3acd4 always show playback speed and position/duration text in white regardless of theme 2026-03-02 21:35:32 +00:00
Claude
49e2f2c7c9 feat: add NIP-66 relay monitor and discovery support to quartz
Implements both event kinds from NIP-66 (Relay Monitor Protocol):

- Kind 10166 (RelayMonitorEvent) - replaceable event published by
  monitoring services, declaring check frequency, timeout values, and
  supported check types (open, read, write, auth, nip11, dns, geo)

- Kind 30166 (RelayDiscoveryEvent) - addressable event (keyed by relay
  URL in d-tag) published per monitored relay, carrying RTT metrics,
  network type, relay type, supported NIPs, accepted kinds, access
  requirements, topics, and geohash

Package: nip66RelayMonitor/{monitor,discovery} following existing NIP-88
patterns with tag classes, TagArrayBuilderExt, and TagArrayExt. Both
events registered in EventFactory.

https://claude.ai/code/session_01Wtg56Vudthgfyz8ASTb4p7
2026-03-02 21:20:43 +00:00
Vitor Pamplona
399c9463bf - Removing unnecessary feed definitions
- Adds favorite relays to the top filter
2026-03-02 16:16:23 -05:00
David Kaspar
858d06380e Merge pull request #1741 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-02 21:09:46 +00:00
Crowdin Bot
db380cfe24 New Crowdin translations by GitHub Action 2026-03-02 20:58:37 +00:00
Vitor Pamplona
bb6f052f32 Remove unnecessary params for FeedDefinitions 2026-03-02 15:56:18 -05:00
Vitor Pamplona
333480c29b Moves TopFilter markers from Strings to full objects. 2026-03-02 15:41:44 -05:00
Vitor Pamplona
7fabb6b554 Makes addresses serializable 2026-03-02 15:39:58 -05:00
Vitor Pamplona
88294de509 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  localize hardcoded Navigate string in UserSearchCard
  add user avatar content descriptions in shared components
  add content description to poll deadline date icon
  correct pause button accessibility label
  unrelated code cleanup
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
2026-03-02 15:39:44 -05:00
davotoula
3220b4a1a7 extract rememberSaveMediaAction to eliminate toast/permission duplication 2026-03-02 20:07:53 +00:00
davotoula
08f389230e restore download toast and permission check
fix overflow button background to solid
2026-03-02 20:07:43 +00:00
M
69e95159e9 fix: check Namecoin name expiry before resolving
Query current block height via blockchain.headers.subscribe and reject
names that have expired (>= 36000 blocks since last update). Populate
expiresIn field in NameShowResult for downstream use.

Unconfirmed transactions (height <= 0) are treated as active.
2026-03-03 06:56:59 +11:00
M
3f39f96e81 fix: route ElectrumX through Tor proxy, add onion server
- ElectrumxClient accepts injected SocketFactory (lambda) so connections
  respect the user's Tor/proxy settings instead of leaking their IP
  through raw sockets
- Add ProxiedSocketFactory for SOCKS5 proxy routing
- Add .onion ElectrumX server as primary when Tor is enabled, with
  electrumx.testls.space as clearnet fallback
- NamecoinNameResolver accepts serverListProvider lambda for dynamic
  server selection based on current Tor settings
- RoleBasedHttpClientBuilder.socketFactoryForNip05() bridges Amethyst's
  Tor settings to the socket factory
- NamecoinNameService now requires explicit init with proxy-aware client
- Remove dead code: Nip05NamecoinAdapter (never referenced),
  NamecoinVerificationDisplay (never called)
- Update design documentation
2026-03-03 06:28:01 +11:00
David Kaspar
5984292ea5 Merge pull request #1740 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-02 18:03:49 +00:00
Crowdin Bot
408bc86e1d New Crowdin translations by GitHub Action 2026-03-02 18:02:41 +00:00
David Kaspar
95353b5077 Merge pull request #1739 from davotoula/acessibility-issues
Fix minor accessibility issues
2026-03-02 17:59:17 +00:00
davotoula
49ea2a9a5a localize hardcoded Navigate string in UserSearchCard 2026-03-02 16:57:12 +00:00
davotoula
daebea262a add user avatar content descriptions in shared components 2026-03-02 16:56:28 +00:00
davotoula
937f30b48d add content description to poll deadline date icon 2026-03-02 16:54:24 +00:00
davotoula
434a7dcf52 correct pause button accessibility label 2026-03-02 16:53:40 +00:00
davotoula
79e39e7e4f unrelated code cleanup 2026-03-02 16:52:50 +00:00
David Kaspar
d87f62c330 Merge pull request #1738 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-02 16:28:55 +00:00
Crowdin Bot
7b5a7b8e0f New Crowdin translations by GitHub Action 2026-03-02 16:27:51 +00:00
David Kaspar
f207bf14d7 Merge pull request #1737 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-02 16:26:19 +00:00
David Kaspar
381c3144b6 Merge branch 'main' into l10n_crowdin_translations 2026-03-02 16:26:10 +00:00
Vitor Pamplona
5b0f7b5a02 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  update cz, pt, de, sv
  remove unused imports
  remove unused imports
  remove unused imports
2026-03-02 11:25:18 -05:00
Vitor Pamplona
e6000cd3e9 Fixes spacing for replying to text 2026-03-02 11:20:09 -05:00
Vitor Pamplona
379dfedf05 Fixes link colors 2026-03-02 11:19:59 -05:00
Crowdin Bot
35090fcdfb New Crowdin translations by GitHub Action 2026-03-02 16:16:16 +00:00
Vitor Pamplona
f18b87db5d Merge pull request #1736 from davotoula/update-translations
update cz, pt, de, sv
2026-03-02 11:12:23 -05:00
Vitor Pamplona
c9b8cb677b Merge pull request #1735 from davotoula/remove-unused-imports
Remove unused imports
2026-03-02 11:12:09 -05:00
davotoula
0d513dd63e update cz, pt, de, sv 2026-03-02 16:02:40 +00:00
davotoula
59309c2b1d remove unused imports 2026-03-02 15:53:21 +00:00
davotoula
51ba076f26 remove unused imports 2026-03-02 15:52:03 +00:00
davotoula
8846dd7edb remove unused imports 2026-03-02 15:47:30 +00:00
Vitor Pamplona
89579aa35e Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add RelayFeedScreen for browsing and following relay-specific feeds
2026-03-02 09:31:39 -05:00
Vitor Pamplona
6b1e6ef267 Fixing the "replying to" line appearing in every reply in the threadview 2026-03-02 09:29:11 -05:00
Vitor Pamplona
2c2102f6e6 refactoring 2026-03-02 08:47:28 -05:00
Vitor Pamplona
0072dd3481 Merge pull request #1733 from vitorpamplona/claude/add-relay-filter-screen-aXgh6
Add relay feed screen with filtering and follow/unfollow functionality
2026-03-02 08:34:08 -05:00
M
f6447d2020 feat: wire Namecoin search into SearchBarViewModel
Resolve .bit, d/, and id/ identifiers from the search bar via
ElectrumX blockchain lookups. Typing any Namecoin identifier format
(m@testls.bit, testls.bit, d/testls, id/alice) now queries the
Namecoin blockchain and shows the resolved user at the top of results.

The namecoinResolvedUser flow in SearchBarViewModel detects Namecoin
identifiers, resolves them through NamecoinNameService, and prepends
the result to the standard local cache search results.

Updated docs with search integration details and manual testing guide.
2026-03-02 19:43:24 +11:00
M
d82ace2f63 feat: Namecoin NIP-05 identity verification via ElectrumX
Add censorship-resistant NIP-05 verification using the Namecoin blockchain.
Users can set their nip05 field to a .bit domain (e.g. alice@example.bit)
or direct Namecoin name (d/example, id/alice) and Amethyst will resolve
the pubkey mapping via ElectrumX instead of HTTP.

Resolution uses the standard Electrum protocol (scripthash-based lookups):
- Build canonical name index script matching ElectrumX-NMC indexing
- Query blockchain.scripthash.get_history for the name's tx history
- Parse NAME_UPDATE script from the latest transaction output
- Extract Nostr pubkey from the name's JSON value

Supports both d/ (domain) and id/ (identity) Namecoin namespaces,
simple and extended NIP-05-like value formats with relay hints,
LRU caching with 1h TTL, and self-signed TLS certificates.

New files:
- quartz: ElectrumxClient, NamecoinNameResolver, NamecoinLookupCache
- amethyst: NamecoinNameService, Nip05NamecoinAdapter, NamecoinVerificationDisplay
- docs: namecoin-nip05-design.md
- tests: NamecoinNameResolverTest

Modified:
- Nip05Client: optional namecoinResolver routes .bit to blockchain
- AppModules: wire up resolver

See docs/namecoin-nip05-design.md for full architecture and protocol details.
2026-03-02 13:07:46 +11:00
Claude
3a9622fb1e feat: add RelayFeedScreen for browsing and following relay-specific feeds
- Add RelayFeedScreen similar to HashtagScreen but filtered by relay URL
- Follow/Unfollow buttons add/remove the relay from Favorite Relay list
- New post FAB opens the standard note composer (Route.NewShortNote)
- Add RelayFeedFilter (DAL) to show notes seen from a specific relay
- Add SingleRelayFeedViewModel backed by RelayFeedFilter
- Add datasource layer: RelayFeedFilterAssembler, SubAssembler, and
  FilterPostsByRelay for subscribing to relay-specific note streams
- Add Route.RelayFeed(url) and register in AppNavigation
- Add relayFeed to RelaySubscriptionsCoordinator
- Add FavoriteRelayListState.addRelay/removeRelay for single-relay ops
- Add Account.followFavoriteRelay/unfollowFavoriteRelay
- Add AccountViewModel.followFavoriteRelay/unfollowFavoriteRelay
- Add observeUserIsFollowingRelay composable observer

https://claude.ai/code/session_013gNQjBzeSSmesYow7HfAjW
2026-03-02 01:09:52 +00:00
Vitor Pamplona
050ef73530 adds support for NIP-51 favorite relay lists. 2026-03-01 19:50:07 -05:00
Vitor Pamplona
7fd163d6d4 - Adds reactions and zap settings to the settings screen
- Creates a better distinction between settings for the user and for the whole app.
2026-03-01 19:25:13 -05:00
Vitor Pamplona
69e061e4c4 Fixes Stability of the Update Reaction view model 2026-03-01 19:15:36 -05:00
Vitor Pamplona
7889e3a624 Minimizes subs by merging PayTo with the Metadata sub 2026-03-01 18:59:46 -05:00
Vitor Pamplona
4b82ec4022 Removes unecessary subId names 2026-03-01 17:02:12 -05:00
Vitor Pamplona
0cba056932 formatting 2026-03-01 16:50:17 -05:00
Vitor Pamplona
bb5e09fe8f Fixes duplicated strings 2026-03-01 16:50:09 -05:00
Vitor Pamplona
7f3d01ce0e Merge pull request #1731 from vitorpamplona/claude/move-strings-to-xml-qav0M
Externalize hardcoded strings to string resources
2026-03-01 16:42:27 -05:00
Claude
ba1d61d8d9 feat: move hardcoded UI strings to strings.xml
Replaces hardcoded string literals in composables with string resource
references for proper i18n support. Adds 20 new strings to strings.xml.

Files updated:
- NewUserMetadataScreen: "Social proof" section title
- NoteCompose: "Approve" community post button
- Badge: " and N others" badge awardees text
- Chess: "Decline"/"Accept" challenge buttons
- InteractiveStory: "Restart" story button
- Poll: "Submit" poll button
- ChessGameScreen: "Back", "Relay Settings", "Loading game...",
  "Game Not Found", "This game may have ended...", "Game ID: ...",
  "Go Back"
- ChessLobbyScreen: "Back", "Relay Settings", "New Game",
  "Dismiss", "Connected"
- chess/NewChessGameButton: "New Chess Game" content description
- home/NewChessGameButton: "New Chess Game" content description
- UserSettingsScreen: "Remove <language>" content description

https://claude.ai/code/session_0151CDmy5k2pEWnxhRxet41h
2026-03-01 20:44:43 +00:00
Vitor Pamplona
16088be597 Correct zap colors 2026-03-01 14:40:47 -05:00
Vitor Pamplona
1a2940fba2 Fixes bug that was not recording these EOSEs 2026-03-01 14:40:25 -05:00
Vitor Pamplona
918108118a Adding more event kind names to the relay settings 2026-03-01 14:26:08 -05:00
Vitor Pamplona
f8a0f17384 Making filter stable 2026-03-01 14:22:54 -05:00
Vitor Pamplona
fbdc8791cd Fixes missing empty paragraphs 2026-03-01 12:59:29 -05:00
Vitor Pamplona
cd18fdf79a Merge pull request #1728 from vitorpamplona/claude/add-relay-subscriptions-display-WHbsT
Add active subscriptions and outbox display to relay info screen
2026-03-01 12:48:25 -05:00
Vitor Pamplona
566bdb8a74 Merge pull request #1729 from vitorpamplona/claude/relay-search-suggestions-T1gpP
Add relay URL suggestions to relay input field
2026-03-01 12:47:04 -05:00
Vitor Pamplona
7212e9a879 Adds Relay Url to the Rich Text viewer 2026-03-01 12:45:47 -05:00
Vitor Pamplona
ca6af92411 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: add RelayUrlSegment for ws:// and wss:// relay URIs
  fix: treat multibyte characters as URL terminators in RichTextParser
  Simplify profile edit screen layout
  feat: show "Replying to" label in quoted notes
  fix: suppress parent thread in quoted notes
2026-03-01 11:48:46 -05:00
Vitor Pamplona
2ddcf41201 Merge pull request #1730 from vitorpamplona/claude/add-relay-uri-detection-vOdNR
Add RelayUrlSegment for WebSocket relay URL parsing
2026-03-01 11:48:09 -05:00
Claude
b472be3a1a feat: add relay search suggestions to all Add a Relay fields
When the user types in any "Add a Relay" field in AllRelayListScreen,
the app now searches HintIndexer's relayDB for matching relay URLs
and displays them as clickable suggestions below the text field.

Clicking a suggestion fills the field with the relay URL, matching
the design pattern of ShowUserSuggestionList in ShortNotePostScreen.

New files:
- RelaySuggestionState: debounced Flow-based state filtering relayDB
- ShowRelaySuggestionList: Column of clickable RelayUrlLine rows

https://claude.ai/code/session_0115sYZGVQBZLX7s9oCDcKoi
2026-03-01 16:39:14 +00:00
Claude
304c348736 feat: add RelayUrlSegment for ws:// and wss:// relay URIs
Adds a new RelayUrlSegment type to RichTextParser that detects and
labels relay URIs (ws:// and wss:// schemes) as a distinct segment
type, separate from generic LinkSegments.

https://claude.ai/code/session_01U21sGdEEMLo4hY8dwxNzPr
2026-03-01 16:33:58 +00:00
Claude
ce56f49369 feat: add active relay subscriptions and outbox display to RelayInformationScreen
Adds a new section before Errors and Notices that shows live relay state:
- REQ subscriptions: each subscription card shows its ID and a visual
  breakdown of all active filters using colored chips by kind category
  (notes/social/DM/economic), author counts, tag values, and time bounds
- COUNT subscriptions: same display for count queries
- Pending outbox events: chip list of event IDs awaiting delivery

Filter chips use Material3 color roles to indicate kind categories:
primaryContainer for text events (note/repost/reaction), secondaryContainer
for social/list events, tertiaryContainer for DMs/gift wraps, and
errorContainer for economic events (zap requests/receipts/NWC).

https://claude.ai/code/session_012zzv2j63ibraPNrQx8fGeu
2026-03-01 16:32:41 +00:00
Vitor Pamplona
f5a7461fed Adds an emoji name to the preview to see if we can replicate the alignment when loading the username dynamically 2026-03-01 11:02:14 -05:00
Vitor Pamplona
678a97d01b Merge pull request #1727 from kojira/fix/multibyte-url-terminators
fix: treat multibyte characters as URL terminators in RichTextParser
2026-03-01 11:00:21 -05:00
kojira
fd28d354e7 fix: treat multibyte characters as URL terminators in RichTextParser 2026-03-01 23:54:12 +09:00
Vitor Pamplona
e7a9241833 Merge pull request #1724 from dmnyc/feat/quote-repost-reply-to-label
feat: show "Replying to" label in quoted notes
2026-02-28 20:39:37 -05:00
Vitor Pamplona
e7c5727f75 Merge pull request #1725 from dmnyc/feat/profile-edit-cleanup
Simplify profile edit screen layout
2026-02-28 20:37:39 -05:00
The Daniel
2b36858c12 Simplify profile edit screen layout
Reduce visual clutter by reordering fields, improving labels, and
collapsing rarely-used social proof fields into an expandable section.

- Rename "Name" field to "Username" with @ prefix
- Reorder: Lightning Address, Nostr Address (NIP-05), Website, Pronouns
- Remove deprecated LN URL (lud06) field
- Move social proof fields (Twitter, GitHub, Mastodon) into a
  collapsible "Social proof" section (auto-expands if data exists)
- Clearer labels: "Lightning Address", "Nostr Address (NIP-05)"
2026-02-28 20:10:17 -05:00
The Daniel
eb3b7077fd feat: show "Replying to" label in quoted notes
Follow-up to #1723. When a quoted note is itself a reply, display a
compact "Replying to [username]" label above the content instead of
showing nothing. Uses the event's p-tags to resolve reply targets
directly, matching the Damus-style display.
2026-02-28 18:53:32 -05:00
Vitor Pamplona
5fc7bab885 Merge pull request #1723 from dmnyc/fix/quote-repost-without-parent-display
fix: Suppress parent thread in quoted notes
2026-02-28 17:52:56 -05:00
The Daniel
ace1401a27 fix: suppress parent thread in quoted notes
Adds unPackReply = false to both DisplayFullNote (nostr: bech links)
and DisplayNoteFromTag (tag references) so that quoted notes only
show the quoted post itself without its parent thread context.
2026-02-28 17:44:04 -05:00
Vitor Pamplona
3b0d7c7f09 Reverses claude new rendering scheme because we need to break into new paragraphs before coming to the rendering stage. 2026-02-28 16:34:53 -05:00
Vitor Pamplona
80921b8367 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  refactor: replace FlowRow-based RenderTextParagraph with single Text() composable
2026-02-28 15:41:59 -05:00
Vitor Pamplona
de3f790e0d Merge pull request #1722 from vitorpamplona/claude/refactor-render-text-paragraph-yKHu8
Refactor RichTextViewer to use inline content instead of FlowRow
2026-02-28 15:38:47 -05:00
Claude
956ae21926 refactor: replace FlowRow-based RenderTextParagraph with single Text() composable
RenderTextParagraph previously assembled each word/segment as a separate
composable inside a FlowRow. It now outputs a single Text() composable
with a built AnnotatedString and an InlineTextContent map so images,
videos, and custom emojis are shown inline within the text flow:

- Images and videos (with preview): InlineTextContent with a fixed-size
  media placeholder (300×200 sp) containing ZoomableContentView
- Custom emojis: InlineTextContent with emoji-sized placeholder per
  image glyph via CustomEmoji.assembleAnnotatedList
- Hashtags: LinkAnnotation.Clickable span + optional InlineTextContent
  icon for decorated hashtags (#bitcoin, #nostr, etc.)
- Clickable URLs, emails (mailto:), phones (tel:): LinkAnnotation.Url
  spans with primary-colour styling
- Block-level widgets (URL previews, invoices, LNURL withdraw, Cashu,
  secret emojis, Nostr note/profile references): InlineTextContent with
  a 300×300 sp block placeholder
- User/event tag references without quote preview: InlineTextContent
  with a narrow text-height reference placeholder (150 sp × emojiSize)

The spaceWidth / measureSpaceWidth plumbing is removed from
RenderTextParagraph and its callers (RenderRegular callback no longer
receives Dp). measureSpaceWidth is kept because Highlight.kt still uses
it for its own FlowRow layout.

RenderWordWithPreview, RenderWordWithoutPreview, the private
ZoomableContentView string-URL wrapper, RenderCustomEmoji, and
NoProtocolUrlRenderer are all inlined into RenderTextParagraph and
removed. HashtagIcon.kt @Preview is updated to the new API.

https://claude.ai/code/session_01TtfLvFoR5Qpr2bHBWr5z3f
2026-02-28 20:29:14 +00:00
Vitor Pamplona
8f73a37a53 Fixes wrapping a non-observable value 2026-02-28 12:29:33 -05:00
Vitor Pamplona
e58cbc9c8a Modifier.weight(1f) is stateless. It doesn't need to be keyed on noteEvent 2026-02-28 12:28:23 -05:00
Vitor Pamplona
21ff7c8502 avoids recreating modifiers 2026-02-28 12:21:46 -05:00
Vitor Pamplona
a5edef2db8 No need to recreate the modifier on background changes 2026-02-28 12:20:47 -05:00
Vitor Pamplona
1bff239183 Fixes need for suspending function on uploading 2026-02-28 11:43:31 -05:00
Vitor Pamplona
30ffeaed34 Merge pull request #1720 from vitorpamplona/claude/add-profile-upload-button-RzGz4
feat: add profile picture upload button when user has no picture
2026-02-28 10:53:48 -05:00
Vitor Pamplona
f08297c36c Merge pull request #1718 from vitorpamplona/claude/review-amethyst-issues-VFf0H
fix: render audio MIME types in imeta tags as playable media
2026-02-28 10:52:05 -05:00
Claude
11e86b5805 feat: add profile picture upload button when user has no picture
When a user visits their own profile and has no profile picture set,
show a semi-transparent upload button overlay directly on the profile
picture area. Tapping it opens the gallery picker, uploads the image
to the configured server (NIP-96 or Blossom), and immediately saves
the metadata — without requiring the user to navigate to the edit screen.

- Add uploadPictureAndSave() to NewUserMetadataViewModel: loads current
  metadata, uploads the image, then saves all metadata with the new picture
- Add ProfilePictureWithUploadOverlay composable that wraps ZoomableUserPicture
  and conditionally shows the upload overlay when isMe && no picture
- Add ProfilePictureUploadButton composable for the overlay UI with
  loading state support
- Use ProfilePictureWithUploadOverlay in ProfileHeader instead of
  ZoomableUserPicture directly

https://claude.ai/code/session_01XemJ8Hx9q4mMd1StZNY9CE
2026-02-28 02:06:43 +00:00
Claude
1efcaefe94 fix: render audio MIME types in imeta tags as playable media
Audio URLs with `audio/*` MIME types in NIP-92 imeta tags were silently
dropped (createMediaContent returned null) because only `image/` and
`video/` prefixes were checked. They now map to MediaUrlVideo, which
Media3/ExoPlayer already handles correctly for audio playback.

Also adds common audio extensions (ogg, wav, flac, aac, opus, m4a) to
the extension-based fallback so audio files without imeta tags are also
detected.

Fixes https://github.com/vitorpamplona/amethyst/issues/1701

https://claude.ai/code/session_01YSi64eXRokSy2GzYHoNFES
2026-02-28 00:34:32 +00:00
Vitor Pamplona
3762c8d6c3 Fixes the activity cast 2026-02-27 08:54:30 -05:00
Vitor Pamplona
9675f59040 Fixes lack of import and new kotlin rules 2026-02-27 08:53:49 -05:00
Vitor Pamplona
972c1ac71d Merge pull request #1717 from vitorpamplona/claude/ios-gzip-compression-iHfKC
Implement GZip compression/decompression for iOS using zlib
2026-02-27 07:55:37 -05:00
Claude
b8cda402cb test(quartz/utils): add GZipTest to commonTest
Covers the full expect/actual contract across all platforms (Android,
JVM, iOS):

- round-trip for empty, single-char, simple, unicode, and JSON strings
- repetitive input compresses to a smaller size
- gzip magic number (0x1F 0x8B) present in compressed output
- compressed bytes differ from raw input bytes
- decompress of invalid data throws an exception
- special/control characters survive a round-trip

https://claude.ai/code/session_0125CGfu6aMAnSv6ZzAxFHvV
2026-02-27 01:57:10 +00:00
Claude
5cd05ab008 feat(quartz/ios): implement GZip compress/decompress for iOS
Uses platform.zlib (built-in on all Apple targets) via Kotlin/Native
cinterop to provide the actual GZip implementation:

- compress: deflateInit2 with windowBits=31 (MAX_WBITS+16) for gzip
  format; deflateBound gives a tight upper-bound so a single Z_FINISH
  call is always enough – no output loop required.
- decompress: inflateInit2 with windowBits=47 (MAX_WBITS+32) for
  automatic gzip/zlib format detection; output is collected in
  fixed-size chunks to handle arbitrary decompressed sizes.

https://claude.ai/code/session_0125CGfu6aMAnSv6ZzAxFHvV
2026-02-27 01:18:47 +00:00
Vitor Pamplona
c2868d4100 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: consolidate drawer settings into a single Settings hub screen
  New Crowdin translations by GitHub Action
2026-02-26 20:11:11 -05:00
Vitor Pamplona
027d932b63 Merge pull request #1716 from vitorpamplona/claude/consolidate-drawer-settings-CBYo3
Consolidate settings screens into unified AllSettingsScreen
2026-02-26 20:08:25 -05:00
Claude
e5eb21168b feat: consolidate drawer settings into a single Settings hub screen
Replace six individual drawer items (Relays, Media Servers, Security
Filters, Privacy Options, App Preferences, User Preferences) with a
single "Settings" entry that navigates to a new AllSettingsScreen hub.
The hub lists all six options as clickable rows, each navigating to
their existing screens unchanged.

https://claude.ai/code/session_017Z9x4PNJuMtMQgfYSxTVSU
2026-02-27 01:06:02 +00:00
Vitor Pamplona
fe8dd3fb51 Fixing incorrect cast 2026-02-26 20:05:30 -05:00
Vitor Pamplona
18270f8020 Adds a reqUntilEoseAsFlow extension to the Nostr Client 2026-02-26 19:56:39 -05:00
Vitor Pamplona
1b4044814f Merge pull request #1715 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-26 19:49:09 -05:00
Vitor Pamplona
0e659b5b07 Fixes instruction to use event database 2026-02-26 19:42:45 -05:00
Vitor Pamplona
abf977b791 Removes common tags 2026-02-26 19:39:53 -05:00
Crowdin Bot
6197cc6a7a New Crowdin translations by GitHub Action 2026-02-27 00:39:49 +00:00
Vitor Pamplona
23c3edbec8 docs: add quartz-integration skill for using Quartz in external KMP projects
Adds a comprehensive Claude skill covering Gradle setup, key management,
event creation/signing, relay client, subscriptions, NIP builders, and
platform-specific notes. Also updates the legacy quartz-kmp.md to redirect
to the new skill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:37:44 -05:00
Vitor Pamplona
5b156c4530 Basic (ugly) rendering of Zap events when quoted inside of posts. 2026-02-26 19:26:33 -05:00
davotoula
87bf52b59e Code review fixes:
reduce duplicate code
2026-02-26 18:22:52 +00:00
davotoula
3822197830 Code review fixes:
Skip-forward clamps against unknown duration
Share dropdown is no longer anchored to the overflow button
simplify: keep skip amount fixed at 10
2026-02-26 18:22:28 +00:00
davotoula
ce76f8fb27 Code review fixes:
Redundant Box in SkipButton
Unused string resources
remember { fadeIn() } pattern
2026-02-26 18:22:11 +00:00
davotoula
ebbc7b5526 Code review fixes:
skipSeconds duplication + stale duration read
pointerInput(Unit) stale capture
ShareMediaAction composed unconditionally
Overflow menu background opaque in dark theme
2026-02-26 18:21:43 +00:00
davotoula
3e9475087e simplify top buttons, move to overflow
add skip buttons
add gradient overlays and double-tap gesture support
2026-02-26 18:17:13 +00:00
davotoula
eb57811654 add skip button component, overflow menu component, gradient overlay 2026-02-26 18:13:34 +00:00
Vitor Pamplona
591c32ceb3 Solving some more stability issues 2026-02-25 16:53:50 -05:00
Vitor Pamplona
105dfa21fb Resolving some stability issues 2026-02-25 16:45:03 -05:00
Vitor Pamplona
b523505c05 fixes spotless check 2026-02-24 18:00:04 -05:00
Vitor Pamplona
89cf71d555 Merge pull request #1710 from nrobi144/feat/desktop-private-dms
feat(desktop): encrypted private DMs (NIP-04/NIP-17)
2026-02-24 17:50:55 -05:00
Vitor Pamplona
b1fd450c91 Merge branch 'main' into feat/desktop-private-dms 2026-02-24 17:50:48 -05:00
Vitor Pamplona
f0d62a8ea2 Merge pull request #1713 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-24 17:40:36 -05:00
Crowdin Bot
80ca7d05ea New Crowdin translations by GitHub Action 2026-02-24 22:31:46 +00:00
Vitor Pamplona
f2410a6921 - Turn video controller creation into a flow, removing most of the little hacks to get the controller to work in the lifecycle
- Uses new Media3 content view frames
- Redesigns video playback interface to remove unused buttons and modernize it.
- Adds our own buttons and indicators for the video playback
- Checks if PiP is supported and active before showing the button.
- Improves click interactions with the image dialog
- Fixes Surface release bugs
- Creates new interface for the Picture in Picture view
- Fixes muting and play buttons on the Picture in Picture
2026-02-24 17:28:41 -05:00
Vitor Pamplona
3942a38f0e Fixes missing Video Decoder for the Gallery 2026-02-24 17:22:18 -05:00
Vitor Pamplona
6c63ef60f3 Adds a filter for multiple addressable kinds but one key 2026-02-24 17:21:57 -05:00
Vitor Pamplona
4e39df2707 Initializes the disk cache on an io thread after 3 seconds. 2026-02-24 17:21:37 -05:00
Vitor Pamplona
4c81ef19df Fixes build.gradle to add dependencies for the Gallery 2026-02-24 17:02:03 -05:00
Vitor Pamplona
bdc708e658 Fixes stability of the feedstate 2026-02-24 17:01:25 -05:00
Vitor Pamplona
5e2a4b2cef - Migrates Gallery to not play videos and simply display a tumbnail when it is a video
- Fixes incorrect aspect ratios of the AutoNonLazyGrid when multiple images exist for a single entry
- Adds Play buttons when the image fails to load so that people click and the post appears.
2026-02-23 19:28:53 -05:00
Vitor Pamplona
6c12476902 Fixes bug when showing multiple versions of replaceable videos in the gallery. 2026-02-23 19:21:58 -05:00
KotlinGeekDev
3817fb04f0 Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-02-23 20:46:09 +01:00
KotlinGeekDev
d48dfce3a5 Foundations(iOS Sourceset): Provide implementation for DigestInstance. Make note about KECCAK256(not supported, until other solution is found). 2026-02-23 19:44:55 +01:00
KotlinGeekDev
fd1b1ae635 Foundations(iOS Sourceset): Duplicate Nip-04 tests for iOS, in order to test the AESCBC implementation used underneath. 2026-02-23 18:05:20 +01:00
KotlinGeekDev
265fef0a93 Foundations(iOS Sourceset): Provide implementation for AESGCM. Duplicate AESGCM tests for iOS. Move dependency to 'cryptography-optimal' make use of all iOS crypto providers. 2026-02-23 17:57:54 +01:00
nrobi144
e7239980c6 feat(desktop): add multi-column deck layout replacing NavigationRail
Replace the 80dp NavigationRail + single content area with a TweetDeck-style
multi-column deck. Default layout: Home Feed + Notifications + Messages.

New deck system:
- DeckColumnType sealed class with 12 column types
- DeckState with StateFlow-based column management
- Per-column navigation stack for in-column drill-down
- Draggable dividers for column resize (300-800dp)
- Column config persisted via Jackson JSON in DesktopPreferences
- 48dp thin sidebar with add-column and settings buttons
- AddColumnDialog for picking column types

Keyboard shortcuts:
- Cmd+T: Add column
- Cmd+W: Close focused column
- Cmd+1-9: Focus column by index
- Cmd+Shift+Left/Right: Move column left/right

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:53:31 +02:00
nrobi144
78aa7962e9 feat(desktop): add DM broadcast status banner and audit fixes
- Show relay confirmation URLs in broadcast banner
- Add relayUrls field to DmBroadcastStatus.Sent
- Wire send progress tracking with per-relay confirmations
- Fix empty chatroom Loading state transition
- Improve NewDmDialog npub metadata resolution
- Harden sendAndWaitForResponse timeout handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 06:53:53 +02:00
nrobi144
35a15f0462 feat(desktop): add DM broadcast banner and fix empty room loading
- Fix ListChangeFeedViewModel stuck on Loading for empty chatrooms by
  adding invalidateData() call in init block
- Add DmBroadcastStatus sealed class and DmBroadcastBanner composable
  in commons for relay send progress display
- Add DmSendTracker using sendAndWaitForResponse for confirmed delivery
- Wire send tracking through DesktopIAccount into ChatPane banner
- Fix audit risks: unsubscribe DMs on account change, wait for relay
  connect before subscribing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:20:04 +02:00
nrobi144
5d983c5d27 fix(desktop): wire gift wrap broadcast for DM reactions
The sendWrappedReaction function was creating NIP-17 gift wraps via
NIP17Factory but never broadcasting them. Added sendGiftWraps() to
IAccount interface with implementations in both Desktop and Android.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:34:33 +02:00
nrobi144
3f5ad8f7be feat(desktop): wire NIP-17 emoji reactions in DM chat
Replace TODO stub with sendWrappedReaction using NIP17Factory.
createReactionWithinGroup creates gift-wrapped reactions for group privacy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 07:34:53 +02:00
nrobi144
9a30b935d2 feat(desktop): wire DM sending, subscriptions, and new DM dialog
- DesktopIAccount: sign + broadcast NIP-04/NIP-17 messages via relay layer
- Main.kt: activate DM subscriptions on login, consume events into cache
- NewDmDialog: user search dialog with SearchBarState + UserSearchCard
- DesktopMessagesScreen: wire Cmd+Shift+N and "+" button to NewDmDialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 07:33:04 +02:00
Vitor Pamplona
2f1eeea77b Merge pull request #1708 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-18 15:00:31 -05:00
Crowdin Bot
bae16f0b52 New Crowdin translations by GitHub Action 2026-02-18 19:58:57 +00:00
Vitor Pamplona
fc502d81d4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
2026-02-18 14:56:21 -05:00
Vitor Pamplona
bcd2bc066b - Removes nip96 and updates Blossom recommendations 2026-02-18 14:56:07 -05:00
Vitor Pamplona
9000a11377 Merge pull request #1707 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-18 13:08:20 -05:00
Crowdin Bot
b1dad24393 New Crowdin translations by GitHub Action 2026-02-18 18:05:59 +00:00
David Kaspar
69a7ceef6c Merge pull request #1706 from davotoula/update-translations
update cz, pt, de, sv
2026-02-18 18:03:38 +00:00
Vitor Pamplona
086e2046db Fixes test file loader for iOS 2026-02-18 12:34:55 -05:00
Vitor Pamplona
ee7d20e939 Minor adjustment in class names 2026-02-18 12:25:48 -05:00
Vitor Pamplona
64c8c0edfd Improves the name of the NIP01 Crypto object 2026-02-18 11:29:56 -05:00
Vitor Pamplona
907ff6e844 Moves deterministic signer to the test package since it's only used there. 2026-02-18 11:24:33 -05:00
Vitor Pamplona
cf7b4ed4e3 Fixes warning in chess test cases 2026-02-18 10:49:59 -05:00
Vitor Pamplona
ab6560d47d Fixes Chess's need for clone functions that are not available on iOS 2026-02-18 10:46:06 -05:00
Vitor Pamplona
2c3ca7f906 - Finishes the transition to EventHint objects for building events.
- Removes the "a-tag" dependency on Addressable interface because the `a` can have different names in different objects.
2026-02-18 10:45:50 -05:00
davotoula
2af1dfbcf4 update cz, pt, de, sv 2026-02-18 11:36:20 +00:00
nrobi144
5d81da2486 feat(desktop): add encrypted DMs with split-pane layout (NIP-04/NIP-17)
Full desktop DM implementation with shared components extracted to commons:
- Relay layer: DM relay state, filter construction, subscription coordinator
- Shared: ChatroomFeedFilter, ChatroomFeedViewModel, ChatNewMessageState
- Shared UI: ChatBubbleLayout, ChatMessageCompose, ChatroomHeader, ChatTheme
- Desktop: split-pane (conversation list + chat), keyboard shortcuts, hover reactions
- IAccount extended with signer, sendNip04/17, chatroomList, isAcceptable

Refs: vitorpamplona/amethyst#1704

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:19:46 +02:00
Vitor Pamplona
46b17628ac Adds ditto as search relay 2026-02-17 19:30:04 -05:00
Vitor Pamplona
7bd4698073 Updates Tor 2026-02-17 19:28:53 -05:00
Vitor Pamplona
fa4180e4ac Updates readme with the current list of supported NIPs 2026-02-17 19:19:29 -05:00
Vitor Pamplona
c755908db5 Removes string-concat compiler argument since it is not supported anymore 2026-02-17 19:02:53 -05:00
Vitor Pamplona
862ce9e2c2 Move to AGP 9.0.1 2026-02-17 19:01:25 -05:00
Vitor Pamplona
cf7bdef028 Adds missing actual implementations on iOS 2026-02-17 19:00:24 -05:00
Vitor Pamplona
c4e3d99392 moves chess lib to version catalog 2026-02-17 18:45:35 -05:00
Vitor Pamplona
bb3937edb4 Makes 24h the default end time for polls 2026-02-17 18:41:25 -05:00
Vitor Pamplona
a6ff66b8ae Rounds time formatting 2026-02-17 18:39:17 -05:00
Vitor Pamplona
d6c9462b46 Removing unnecessary class. 2026-02-17 17:09:45 -05:00
Vitor Pamplona
277a311fdc In Compose, a UI element with a height of 0 generally cannot receive pointer events (clicks), even if it visually scales back up later via the graphicsLayer. This commit blocks the progress from going to zero 2026-02-17 16:50:43 -05:00
Vitor Pamplona
46bc0dd750 Fixes poll rendering with images are included in the options 2026-02-17 16:40:46 -05:00
Vitor Pamplona
713e91185b Merge pull request #1702 from nrobi144/feat/chess-pgn-display-878
chess: live chess game implementation with Jester protocol
2026-02-17 16:12:26 -05:00
Vitor Pamplona
b16fd2c4d1 Merge pull request #1705 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-17 16:05:45 -05:00
Crowdin Bot
24e4b86065 New Crowdin translations by GitHub Action 2026-02-17 21:04:02 +00:00
Vitor Pamplona
f665bec63d adds basic support for NIP85 Polls 2026-02-17 16:00:54 -05:00
KotlinGeekDev
460be8cbf3 Foundations(iOS Sourceset): Provide implementation for AESCBC. 2026-02-17 01:03:19 +01:00
KotlinGeekDev
2d2ddda2c5 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-02-13 04:20:01 +01:00
Vitor Pamplona
893a318fbd Correctly refreshes the Live bubbles when switching top filters. 2026-02-12 16:20:03 -05:00
Vitor Pamplona
e84e748959 updating libraries 2026-02-12 15:13:07 -05:00
KotlinGeekDev
bb5d785cb3 Enable running tests with resources for the androidHostTest sourceset, specifically HintIndexerTest. Replace non-null assertion with exception throw, to precisely identify test resource errors. 2026-02-12 15:02:02 +01:00
KotlinGeekDev
68c7f5e884 Linter fixes, plus a lint suppression for Buffer.kt. 2026-02-12 14:28:59 +01:00
Vitor Pamplona
e0db24e9d7 Fixes the blinking and multi-broadcasting issues of the track broadcast snackbar 2026-02-11 17:43:55 -05:00
KotlinGeekDev
9caba9dcd4 Merge upstream changes. 2026-02-11 14:26:36 +01:00
KotlinGeekDev
3f9614dd8f Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main
# Conflicts:
#	gradle/libs.versions.toml
2026-02-11 13:57:01 +01:00
nrobi144
5f15f0c3b1 fix: address Copilot review comments
- ChessPosition: include all fields in equals/hashCode
- AcceptedGamesRegistry: add thread safety with synchronized
- ChessEventBroadcaster: use valid filter for connection trigger
- ChessEventBroadcaster: fix misleading relayResults
- Remove println debug logging from ChessSubscription/Broadcaster
- UserProfileScreen: use proper JSON parsing via MetadataEvent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 16:39:24 +02:00
nrobi144
8d098cf86d feat(chess): focused polling, promotion fix, and game end celebration
- Add focused game mode to ChessPollingDelegate: when viewing a game,
  only that game is polled instead of all active games
- Fix pawn promotion by parsing promotion suffix in publishMove
  (e.g., "e8q" -> "e8" + QUEEN)
- Add game end overlay with victory/defeat/draw celebration
- Remove all debug println statements for production readiness

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:14:34 +02:00
nrobi144
3c89c442bc feat(chess): add slim ViewModels and shared broadcast banner (Steps 7-10)
Step 7: ChessViewModelNew.kt - Slim Android ViewModel (~130 lines)
- Delegates all business logic to ChessLobbyLogic
- Exposes StateFlows from logic.state
- Platform adapter creation only

Step 8: DesktopChessViewModelNew.kt - Slim Desktop ViewModel (~120 lines)
- Same pattern as Android, delegates to ChessLobbyLogic
- UserMetadataCache for profile display
- External CoroutineScope injection

Step 9: ChessBroadcastBanner.kt - Shared Compose banner in commons
- Uses ChessBroadcastStatus from ChessLobbyState
- Works on both Android and Desktop via Compose Multiplatform
- Shows broadcasting progress, sync status, errors

Step 10: UI consumers ready for migration
- New ViewModels use ChessChallenge (enriched display data)
- Shared banner ready to replace Android-only ChessStatusBanner
- Existing screens continue to work with old ViewModel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:14:34 +02:00
nrobi144
42b33ca04c feat(chess): add platform adapters and enhance lobby state (steps 5-6)
- Add ChessLobbyState with completedGames, replaceGameState(), moveToCompleted()
- Add challengerAvatarUrl to ChessChallenge
- Add CompletedGame data class for game history

Platform Adapters:
- AndroidChessAdapter: AndroidChessPublisher, AndroidRelayFetcher, AndroidMetadataProvider
- DesktopChessAdapter: DesktopChessPublisher, DesktopRelayFetcher, DesktopMetadataProvider

Shared Infrastructure:
- ChessRelayFetchHelper for one-shot relay queries
- IUserMetadataProvider interface for platform-specific metadata
- ChessFilterBuilder with simple filter methods for fetchers
- ChessSubscriptionController interface for subscription management

Fix ChessSubscription.kt to remove broken dataSources().chess reference
(subscriptions now managed by ChessLobbyLogic)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:14:34 +02:00
nrobi144
e0d0e33408 refactor impl 2026-02-10 12:14:05 +02:00
nrobi144
c2f035acc0 docs: update chess implementation status
- Mark ChessViewModel, navigation, relay subscriptions as complete
- Update file structure to reflect KMP commons migration
- Update checklist with completed items
- Revise next steps to focus on FAB and badge display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:10:39 +02:00
nrobi144
de2ebb2f7b feat(chess): add relay subscriptions for live game events
- Subscribe to LocalCache.live.newEventBundles for chess events
- Handle incoming opponent moves (LiveChessMoveEvent)
- Handle game acceptance (LiveChessGameAcceptEvent)
- Handle game endings (LiveChessGameEndEvent)
- Track new challenges directed at user (LiveChessGameChallengeEvent)
- Auto-update badge count on relevant events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:10:38 +02:00
nrobi144
4a02c12acd feat(chess): implement ChessViewModel with event publishing
- Add state management for active games, challenges, badges
- Implement createChallenge for new games
- Add acceptChallenge for joining games
- Add publishMove with both simple (from/to) and full (ChessMoveEvent) APIs
- Add resign and offerDraw for game termination
- Wire up to existing ChessGameScreen and Chess.kt UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:10:38 +02:00
nrobi144
607cdf0c17 refactor(commons): migrate chess UI to KMP commonMain
- Move chess UI components from src/main/java to src/commonMain/kotlin
- Add consume functions for chess events in LocalCache
- Resolve merge conflicts with main branch KMP migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:10:38 +02:00
nrobi144
63fc5e86f6 fixes 2026-02-10 12:10:38 +02:00
nrobi144
06accf5831 full chess implementation 2026-02-10 11:58:00 +02:00
nrobi144
18c8156ab2 integrate chess events 2026-02-10 11:56:43 +02:00
nrobi144
19c36c2979 initial nip64 implementation 2026-02-10 11:54:36 +02:00
Vitor Pamplona
a70101f5e5 cleans up broadcasting tracker 2026-02-09 20:28:35 -05:00
KotlinGeekDev
295f980584 Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-02-10 00:23:37 +01:00
Vitor Pamplona
89b058fff3 Adds yabu and nostr1 profile relays 2026-02-09 17:54:04 -05:00
Vitor Pamplona
97251b345a Correcting relay url 2026-02-09 17:41:27 -05:00
Vitor Pamplona
9d32c314d1 Switching nostr.band to antiprimal on app defaults. 2026-02-09 16:27:19 -05:00
Vitor Pamplona
bc9adc8352 Breaks the search filter down into two subscriptions to prioritize Metadata without punishing content. 2026-02-09 16:12:11 -05:00
KotlinGeekDev
dcb10b249a Foundations(iOS Sourceset): Provide implementation for MacInstance(using external library). Bring some tests into commonTest to make sure it works. 2026-02-09 21:57:54 +01:00
KotlinGeekDev
aa4d586025 Linter fixes for GZip.ios.kt 2026-02-09 20:42:18 +01:00
KotlinGeekDev
656af17b1c Implement fix for URLs with absolute paths. 2026-02-09 20:41:18 +01:00
Vitor Pamplona
ae84a64b71 Fixes reformatting bug 2026-02-09 14:09:07 -05:00
Vitor Pamplona
eb66182211 - Updates several libraries
- Reformats code to the newest Klint
- Fixes isEmpty bug on Filters
2026-02-09 14:06:07 -05:00
Vitor Pamplona
710f15f790 - Fixes copyright using KDoc patterns instead of simpler comments
- Moves spotless and git-hooks to hidden folders
2026-02-09 13:06:41 -05:00
Vitor Pamplona
6125394354 Migrates to AGP 9 2026-02-08 16:33:39 -05:00
Vitor Pamplona
f8a232385d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-02-08 12:05:02 -05:00
Vitor Pamplona
9d8c1f3ffe Avoid dependency of AccountSettings for NwcSignerState 2026-02-08 12:04:51 -05:00
Vitor Pamplona
c319b25077 Merge pull request #1700 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-08 11:52:00 -05:00
Crowdin Bot
234ca70c4e New Crowdin translations by GitHub Action 2026-02-08 16:43:29 +00:00
Vitor Pamplona
913b694f78 Clean up on unnecessary methods on Note 2026-02-08 11:35:26 -05:00
Vitor Pamplona
aa4cfdc237 Small refactoring 2026-02-07 18:54:48 -05:00
Vitor Pamplona
393d3b33f1 deletes old test 2026-02-07 18:52:17 -05:00
Vitor Pamplona
f37ab8e656 Moves to LruCache from androidx 2026-02-07 18:25:23 -05:00
Vitor Pamplona
2e64d36f39 Moves Blossom server list to use the ICacheProvider 2026-02-07 18:23:42 -05:00
Vitor Pamplona
571fb6af4b Moves the new observables to commons 2026-02-07 18:20:50 -05:00
Vitor Pamplona
7813d20f8e Increases the limit of Zap downloads for profiles to 1000 2026-02-07 18:20:23 -05:00
Vitor Pamplona
838fb3ed7e Groups zaps in profile by user
Moves author's zap cache to the Profile viewmodel
Redesigns composables to match.
Updates Notifications to not use the old zap cache.
2026-02-07 17:52:26 -05:00
Vitor Pamplona
452d549e43 Fixes the fact that SortedSets can have duplicated objects in parallel adds.
Moves to concurrent sets with a Comparator that checks the object's reference to remove duplications in the set when events from addressables differ
2026-02-07 17:50:39 -05:00
Vitor Pamplona
2e85d1356e Fixes Addressable vs Replaceable bases 2026-02-07 17:48:37 -05:00
Vitor Pamplona
93a17366a7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  Add SKILL.md for AI agent customization
2026-02-07 14:29:35 -05:00
Vitor Pamplona
ea34125fad Moves relays used by each user cache to the new architecture 2026-02-07 14:29:03 -05:00
Vitor Pamplona
03e8316db4 Moves lud06 to lud16 mapping to Quartz 2026-02-07 12:29:13 -05:00
Vitor Pamplona
44b635c9e4 Merge pull request #1699 from CentauriAgent/add-agent-skill
Add SKILL.md for AI agent customization
2026-02-07 12:00:18 -05:00
Centauri
9c9a4f2e7d Add SKILL.md for AI agent customization
This file provides instructions for AI agents to fork, customize,
and build branded versions of Amethyst.

Includes:
- Prerequisites and environment setup
- Step-by-step build workflow
- Common customizations (app name, package ID, icons, client tags)
- Troubleshooting guide
- Distribution options
2026-02-07 11:17:57 -05:00
Vitor Pamplona
4e39a531b7 Moves metadata methods from User to UserCache object 2026-02-07 10:18:13 -05:00
Vitor Pamplona
823624f8f2 adds a longer crop for npubs so that we can see vanity keys better when they don't have nip05s or statuses 2026-02-07 10:17:37 -05:00
Vitor Pamplona
bab881e75d Moves status to a user property to avoid searching on scrolling 2026-02-06 18:15:12 -05:00
Vitor Pamplona
ec629ff081 Migrates contact list management to addressable notes
Creates new observable flows for LocalCache.
2026-02-06 16:12:11 -05:00
Vitor Pamplona
130a83f0b9 Fixes stability of Profile viewmodels 2026-02-06 15:54:08 -05:00
Vitor Pamplona
e97a30fcb1 Migrates to use "title" instead of "name" tags for NIP-51 lists. 2026-02-05 18:20:04 -05:00
Vitor Pamplona
4ced055035 Switches the metadata relay for the relay being used with the most amount of posts to be the relayhint 2026-02-04 17:38:01 -05:00
Vitor Pamplona
a67fe859e8 Merge pull request #1697 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-02-04 16:45:42 -05:00
Crowdin Bot
5de551078f New Crowdin translations by GitHub Action 2026-02-04 21:41:27 +00:00
Vitor Pamplona
f89833cc72 Fix fdroid build 2026-02-04 16:37:29 -05:00
Vitor Pamplona
0225fde2a5 Fixes NIP05 username on Profiles 2026-02-04 16:25:40 -05:00
Vitor Pamplona
e6f7a543cd Migrates Desktop App to the new metadata cache. 2026-02-04 15:38:16 -05:00
Vitor Pamplona
d2f77ed521 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: (48 commits)
  Use accountViewModel.viewModelScope instead of rememberCoroutineScope() to allow download/share to finish even when controls auto-close.
  Add content parameter to allow sharing of video from video player
  New Crowdin translations by GitHub Action
  - use a valid hex key - check the value of the tags
  Fixed NullPointerException when filter contain tags
  Fix assertEquals order
  fix Jackson deserialization for empty Filters and add regression test
  New Crowdin translations by GitHub Action
  Consume File.delete() return values Extract duplicated "https://" literal into a private const val HTTPS_PREFIX
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  Load and save payment targets
  refactor(commons): migrate from Moko to Compose Multiplatform Resources
  feat(broadcast): Enhance retry, multi-broadcast UI, and post tracking (#1682)
  wip: Add BroadcastTracker to AccountViewModel (#1682)
  feat(broadcast): Add transparent event broadcasting feedback (#1682)
  Enabled the reactions expand control for zapraisers even when there are no zaps
  New Crowdin translations by GitHub Action
  refactor to reduce complexity encodePcmToAac
  ...

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt
2026-02-04 14:35:35 -05:00
Vitor Pamplona
7bc7265757 Refactors the old NIP-05 code on Quartz
New Caching system for User metadata
New Caching system for NIP-05 verifications
2026-02-04 14:31:30 -05:00
KotlinGeekDev
1f94602404 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-02-02 16:43:30 +01:00
Vitor Pamplona
8085d3ead6 Merge pull request #1695 from davotoula/share-videos-from-video-view
Share videos from video view
2026-01-30 17:38:55 -05:00
davotoula
f7b815bedb Use accountViewModel.viewModelScope instead of rememberCoroutineScope() to allow download/share to finish even when controls auto-close. 2026-01-30 22:30:56 +01:00
davotoula
1cd8eeda4e Add content parameter to allow sharing of video from video player 2026-01-30 21:49:30 +01:00
David Kaspar
96ff0e59f6 Merge pull request #1694 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-30 14:45:52 +00:00
Crowdin Bot
863f93a621 New Crowdin translations by GitHub Action 2026-01-30 13:48:47 +00:00
David Kaspar
d38450424b Merge pull request #1693 from greenart7c3/main
fix crash in Jackson deserialization for empty Filters
2026-01-30 13:46:52 +00:00
greenart7c3
577b4c0e0a - use a valid hex key
- check the value of the tags
2026-01-30 09:38:09 -03:00
greenart7c3
f2d7b115d9 Fixed NullPointerException when filter contain tags 2026-01-30 09:00:52 -03:00
greenart7c3
8bfd670f8b Fix assertEquals order 2026-01-30 08:50:09 -03:00
greenart7c3
247e30beed fix Jackson deserialization for empty Filters and add regression test 2026-01-30 07:31:38 -03:00
David Kaspar
85e9984153 Merge pull request #1692 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-29 07:46:29 +00:00
Crowdin Bot
781da12475 New Crowdin translations by GitHub Action 2026-01-29 07:23:27 +00:00
David Kaspar
0f286f68c8 Merge pull request #1691 from davotoula/sonar-fixes
chore: sonar fixes
2026-01-29 07:21:57 +00:00
davotoula
7793d2e2a3 Consume File.delete() return values
Extract duplicated "https://" literal into a private const val HTTPS_PREFIX
2026-01-28 23:02:38 +01:00
KotlinGeekDev
b965ff1cc6 Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-01-28 15:09:57 +01:00
KotlinGeekDev
3ee600ea28 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-01-28 00:09:19 +01:00
KotlinGeekDev
506996b382 Foundations(iOS Sourceset): Provide implementation for GZip compression/decompression. Some small fixes in URLs.ios.kt . 2026-01-27 20:52:43 +01:00
KotlinGeekDev
3f9f496c8b Foundations(iOS Sourceset): Provide implementation for UnicodeNormalizer. 2026-01-27 17:14:32 +01:00
KotlinGeekDev
4e2f779b54 Foundations(iOS Sourceset): Provide implementation for UrlEncoder. 2026-01-27 16:31:04 +01:00
KotlinGeekDev
fb5288d5f1 Foundations(iOS Sourceset): Provide implementation for makeAbsoluteIfRelativeUrl() in ServerInfoParser.ios.kt 2026-01-26 20:08:44 +01:00
KotlinGeekDev
94a5237eaa Foundations(iOS Sourceset): Provide implementation for fastFindURLs(). Make a test for it. 2026-01-26 16:56:11 +01:00
Vitor Pamplona
53d0aa8e42 Merge pull request #1689 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-26 10:02:02 -05:00
Crowdin Bot
f2c27dbdc8 New Crowdin translations by GitHub Action 2026-01-26 15:00:22 +00:00
Vitor Pamplona
e52817f8e9 Merge pull request #1688 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-26 09:58:01 -05:00
Vitor Pamplona
596b8aafba Merge pull request #1683 from nrobi144/feat/1682-transparent-broadcast-feedback
add transparent event broadcasting feedback
2026-01-26 09:57:41 -05:00
Crowdin Bot
6fff7dc17b New Crowdin translations by GitHub Action 2026-01-26 14:55:12 +00:00
Vitor Pamplona
c3f09987aa Merge pull request #1676 from nrobi144/nrobi144/phase-2A
Desktop: Basic Search + Bookmarks + Zaps
2026-01-26 09:53:35 -05:00
Vitor Pamplona
671a102c7c Merge pull request #1673 from davotoula/share-videos-to-other-apps
Share videos to other apps (via intent)
2026-01-26 09:47:14 -05:00
Vitor Pamplona
c91bacaa91 Merge pull request #1687 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-26 09:46:11 -05:00
Vitor Pamplona
e8cf5fb60d Merge pull request #1686 from greenart7c3/payto_part2
Payment targets part two
2026-01-26 09:45:54 -05:00
Crowdin Bot
f618741211 New Crowdin translations by GitHub Action 2026-01-26 14:44:00 +00:00
Vitor Pamplona
7a4386da81 Merge pull request #1684 from nrobi144/feat/1675-migrate-moko-to-cmp-resources
refactor(commons): migrate from Moko to Compose Multiplatform Resources
2026-01-26 09:42:12 -05:00
greenart7c3
058cdc782a Load and save payment targets 2026-01-26 11:18:48 -03:00
nrobi144
71e68dc35b refactor(commons): migrate from Moko to Compose Multiplatform Resources
Remove Moko Resources dependency that blocks AGP 9.0 migration.
Migrate to built-in Compose Multiplatform Resources which is compatible
with the new KMP Android plugin.

- Remove moko-resources plugin and libraries from commons module
- Move strings.xml from moko-resources/ to composeResources/values/
- Update imports from SharedRes.strings.* to Res.string.*
- Add compose.components.resources dependency to desktopApp
- Configure resource package as com.vitorpamplona.amethyst.commons.resources

Closes #1675

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 13:29:55 +02:00
nrobi144
dedfd59d0b feat(broadcast): Enhance retry, multi-broadcast UI, and post tracking (#1682)
- Add Retrying state to RelayResult for in-flight retry tracking
- Implement synced 360° rotating animation for pending/retrying icons
- Refactor BroadcastDetailsSheet for multi-broadcast sections (max 2 expanded)
- Add overflow summary for additional broadcasts beyond 2
- Fix sheet text colors using theme colors for better visibility
- Make sheet fully expandable (skipPartiallyExpanded)
- Add event cache to BroadcastTracker for retry support
- Implement retry that updates existing broadcast in-place
- Add tracked broadcasting for posts and quotes via createPostEvent/consumePostEvent
- Fix relay list computation for new posts (use event-based, not empty note)
- Make post editor close immediately, broadcast continues in background
- Style CompletedBroadcastIndicator consistently with BroadcastBanner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 16:27:29 +02:00
David Kaspar
8ebba994e8 Merge pull request #1681 from davotoula/feature/display-zapraise
Enabled the reactions expand control for zapraisers even when there are no zaps
2026-01-23 06:35:13 +00:00
nrobi144
c5d9fc3c15 wip: Add BroadcastTracker to AccountViewModel (#1682)
Phase 2 progress - added BroadcastTracker instance to AccountViewModel.
Integration with reaction buttons pending.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 06:55:13 +02:00
nrobi144
3ac8ba669b feat(broadcast): Add transparent event broadcasting feedback (#1682)
Phase 1 - Core infrastructure:
- BroadcastModels: BroadcastEvent, RelayResult, BroadcastStatus data classes
- BroadcastTracker: State management with live progress updates
- BroadcastBanner: Global progress indicator (animated)
- BroadcastSnackbar: Result notifications with actions
- BroadcastDetailsSheet: Relay status detail bottom sheet

Refs: #1682

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 06:50:25 +02:00
davotoula
1e611a3fb5 Enabled the reactions expand control for zapraisers even when there are no zaps 2026-01-22 18:32:37 +01:00
David Kaspar
8bbeb9aadb Merge pull request #1679 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-22 09:28:08 +00:00
Crowdin Bot
5f75d5a48b New Crowdin translations by GitHub Action 2026-01-22 09:25:17 +00:00
David Kaspar
b39a3d1911 Merge pull request #1678 from davotoula/sonar-fixes
Sonar fixes - reduce Cognitive Complexity in VoiceAnonymizer.kt
2026-01-22 09:23:22 +00:00
davotoula
1a736f1c8a refactor to reduce complexity encodePcmToAac 2026-01-21 17:41:54 +01:00
davotoula
b8ecb03ddd refactor to reduce complexity decodeAudioToPcm 2026-01-21 17:41:53 +01:00
nrobi144
ff47080cf1 fix: Resolve API mismatches after merge with upstream
- Fix import paths to use desktop.subscriptions instead of commons.subscriptions
- Update DesktopLocalCache to match ICacheProvider interface changes
- Use correct commons.relayClient paths for ComposeSubscriptionManager
- Add missing justConsumeMyOwnEvent and getOrCreateAddressableNote methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 06:23:58 +02:00
nrobi144
d08aed47e8 Merge upstream/main into nrobi144/phase-2A
Resolved import conflicts caused by package reorganization:
- upstream moved classes to amethyst/service/relayClient/*
- upstream moved models to commons/model/*
- kept our desktop additions (zaps, bookmarks, search)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 06:04:13 +02:00
davotoula
80c946be44 rename method to reflect new functionality 2026-01-20 22:42:48 +01:00
davotoula
ac4a1de0cd Block share menu from closing while downloading video 2026-01-20 22:31:14 +01:00
davotoula
d542e4d6a0 change to 2 minutes delay before cleaning up temp shared file 2026-01-20 19:55:19 +01:00
davotoula
6257b6dd80 Merge branch 'main' into share-videos-to-other-apps 2026-01-20 19:35:41 +01:00
David Kaspar
29faa7882d Increase max voice record duration to 600 seconds 2026-01-20 18:27:20 +00:00
David Kaspar
5401a89356 Merge pull request #1677 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-20 18:21:28 +00:00
Crowdin Bot
c801e9ce86 New Crowdin translations by GitHub Action 2026-01-20 13:34:58 +00:00
Vitor Pamplona
f134f815d0 Merge pull request #1672 from davotoula/voice-notes-code-review-fixes
Voice notes enhancements and code review fixes
2026-01-20 08:33:11 -05:00
nrobi144
8155cfee9d update metadata loading 2026-01-20 15:18:11 +02:00
nrobi144
8f6de1d304 zap updates 2026-01-19 16:32:01 +02:00
davotoula
4897aa4d09 Use coroutine instead of Handler for temp file cleanup 2026-01-18 22:51:00 +01:00
davotoula
5450c7670f update translations 2026-01-18 22:31:31 +01:00
davotoula
1a04f16d1a Safer temp video handling
Cleanup deletes the actual shared file (and on error) instead of the old temp path in
Guard against empty response.body
Gate media-type detection so image sharing only checks image headers
Added unit test for rename-failure fallback
2026-01-18 22:28:55 +01:00
davotoula
f2e484b18e URL videos: Downloads video to temp file using streaming (avoids OOM), shows progress indicator, shares via Intent, cleans up after 60 seconds
Local videos: Shares directly without downloading
Video format detection: Supports MP4, WebM, MKV, AVI, MOV based on file magic numbers
2026-01-18 20:09:00 +01:00
KotlinGeekDev
893d363a03 Merge remote-tracking branch 'origin/kmp-completeness' into kmp-completeness 2026-01-16 16:11:23 +01:00
KotlinGeekDev
db536289c4 Make linter happy, I guess. 2026-01-16 16:10:58 +01:00
KotlinGeekDev
f2ac68ccf8 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-01-15 20:53:38 +01:00
davotoula
505a6583ee remove unused imports 2026-01-15 14:20:05 +01:00
davotoula
40d6943439 sonar fixes 2026-01-15 14:12:09 +01:00
davotoula
835fbb90b7 Do something with the "Boolean" value returned by "delete". 2026-01-15 14:00:13 +01:00
davotoula
76a05f7d71 moved re-record button inline and added to VoiceMessagePreview.kt 2026-01-15 13:58:14 +01:00
davotoula
5be1633843 limit voice recording to 180s 2026-01-15 13:58:14 +01:00
davotoula
db75a7c910 preallocate list capacity 2026-01-15 13:58:14 +01:00
davotoula
50b0e8bd17 Moved processingPreset = preset to execute synchronously before launching the coroutine 2026-01-15 13:58:14 +01:00
davotoula
0b233f3a3f use colorScheme.onSurfaceVariant instead of Gray 2026-01-15 13:58:14 +01:00
davotoula
001dce8fef guard for null inputFile.parentFile 2026-01-15 13:58:14 +01:00
davotoula
708c6da9fe add docs 2026-01-15 13:58:14 +01:00
davotoula
c6d437b98e confirmed: <1 results in Higher pitch. Adjusted NEUTRAL to slightly lower pitch 2026-01-15 13:58:14 +01:00
davotoula
5ce4391db4 guard encoder / decoder .stop 2026-01-15 13:58:13 +01:00
Vitor Pamplona
6f62e2c7b1 Merge pull request #1671 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-14 17:58:41 -05:00
Crowdin Bot
208f57830f New Crowdin translations by GitHub Action 2026-01-14 22:56:07 +00:00
Vitor Pamplona
92d4654b20 Removes the memory counter methods because they are not the best way to measure the size in memory of these objects, since we intern strings all the time. 2026-01-14 16:49:11 -05:00
Vitor Pamplona
3d0b0c01b9 Finishes editing quartz event store readme 2026-01-14 15:36:32 -05:00
Vitor Pamplona
adb2f48fb5 Updates event store readme 2026-01-14 15:29:13 -05:00
Vitor Pamplona
54ab70beb7 Fixes https://github.com/vitorpamplona/amethyst/issues/1670 2026-01-14 14:24:57 -05:00
Vitor Pamplona
c41040848c Fixes NPE on null existing event. 2026-01-14 09:30:53 -05:00
Vitor Pamplona
6097a0b5b7 Removes the need for an extra object for the tag array in the custom emoji nip 2026-01-13 17:32:38 -05:00
Vitor Pamplona
288fccd414 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-01-13 17:13:17 -05:00
Vitor Pamplona
be7865d458 updates keyring and vico 2026-01-13 17:13:03 -05:00
Vitor Pamplona
e0e0990278 Merge pull request #1669 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-13 16:16:43 -05:00
Crowdin Bot
b0210e98f1 New Crowdin translations by GitHub Action 2026-01-13 21:08:51 +00:00
Vitor Pamplona
04ac40fa1a moves test as well 2026-01-13 16:07:03 -05:00
Vitor Pamplona
2ce1e81072 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-01-13 16:04:15 -05:00
Vitor Pamplona
09d0f4425c Moves some of the stuff that was saved in Commons back to the desktop app because it is only used there 2026-01-13 16:04:02 -05:00
Vitor Pamplona
73fe985a65 Fixes tests 2026-01-13 15:49:08 -05:00
Vitor Pamplona
ba5c86fbba Migrates rich text parser from jvm to commons 2026-01-13 15:31:26 -05:00
Vitor Pamplona
bc55749998 Merge pull request #1668 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-13 15:01:19 -05:00
Crowdin Bot
33c51b9a10 New Crowdin translations by GitHub Action 2026-01-13 20:00:04 +00:00
Vitor Pamplona
42bdd1c831 Emoji State to commons 2026-01-13 14:57:29 -05:00
Vitor Pamplona
7c1053df2d Moves reports to common
Adds support for additional information
2026-01-13 14:37:32 -05:00
Vitor Pamplona
9b7a83796b Moves private chatroom models to commons 2026-01-13 14:33:24 -05:00
Vitor Pamplona
afe66cd55f Moves user actions to commons 2026-01-13 14:31:04 -05:00
Vitor Pamplona
a8dd229fb2 Moves Channels (public chats, ephemeral channels and live streams) Account modules to commons 2026-01-13 14:30:51 -05:00
Vitor Pamplona
6d15e4861c Moves channels to commons and removes the dependency in the cache from Note 2026-01-13 12:22:40 -05:00
Vitor Pamplona
5241a6dfa7 Fixes missing cache after AI PR for the desktop app
Removes dependency on the Cache from User.
Updates interface methods to match.
2026-01-13 11:27:45 -05:00
Vitor Pamplona
2fe0157b2e Merge pull request #1665 from davotoula/voice-distortion-poc
Voice distortion (basic)
2026-01-13 08:13:02 -05:00
davotoula
ec159ff85f add translations 2026-01-12 23:03:52 +01:00
davotoula
f13a9164c2 code review: resource leaks and init guard 2026-01-12 23:03:52 +01:00
davotoula
3287e3c031 add a ±10% random variation to HIGH and DEEP pitch 2026-01-12 23:03:51 +01:00
davotoula
666015b8eb reduce duplication:
New shared controller for state + processing + distorted file cleanup
New shared UI section for anonymization header + preset selector
2026-01-12 23:03:51 +01:00
davotoula
a4af706ebd correct the item where progress spinner happens on 2026-01-12 23:03:51 +01:00
davotoula
6f8533f5e1 added explainer to UI 2026-01-12 23:03:51 +01:00
davotoula
9c0fbbf8a4 correct pitch selection
Add to voice note screen (new note)
2026-01-12 23:03:51 +01:00
davotoula
63d7ba07b9 add voice preset selector UI to VoiceReplyScreen 2026-01-12 23:03:51 +01:00
davotoula
8182268a00 add voice anonymization state to VoiceReplyViewModel 2026-01-12 23:03:51 +01:00
davotoula
8de148e562 add VoiceAnonymizer for audio pitch/formant shifting 2026-01-12 23:03:51 +01:00
davotoula
83d4d3e756 add VoicePreset enum for voice anonymization
Define preset options (None, Deep, High, Neutral)
2026-01-12 23:03:51 +01:00
davotoula
bfd0fc01b9 add TarsosDSP dependency for voice anonymization 2026-01-12 23:03:51 +01:00
David Kaspar
8f1ff084d0 Merge pull request #1664 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-12 21:17:22 +00:00
Crowdin Bot
c585b05879 New Crowdin translations by GitHub Action 2026-01-12 20:32:01 +00:00
Vitor Pamplona
e9210872bd Improves performance by precaching signatures before sorting them
Fixes out of order crash when the same signature came back.
2026-01-12 15:29:33 -05:00
Vitor Pamplona
e561492705 Removes unused aliases 2026-01-12 15:28:33 -05:00
Vitor Pamplona
096f59ffc1 Merge pull request #1663 from nrobi144/phase1-final
Desktop - Threads and Notifications + Empty state updates
2026-01-12 10:00:20 -05:00
Vitor Pamplona
2adde38a9c Merge pull request #1660 from davotoula/1654-voice-reply-to-text-note-as-kind-1
voice reply to text note as kind 1
2026-01-12 09:39:03 -05:00
KotlinGeekDev
a9f6ebde83 Merge branch 'vitorpamplona:main' into kmp-completeness 2026-01-12 15:25:11 +01:00
KotlinGeekDev
f5bb0dbc1d Move cachemap dependency to version catalog. 2026-01-12 15:06:57 +01:00
KotlinGeekDev
7c936cfcac Foundations(iOS Sourceset): Provide implementation for LargeCache, using a CacheMap. 2026-01-12 15:05:50 +01:00
KotlinGeekDev
d90f3f4f98 Foundations(iOS Sourceset): Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge. 2026-01-12 14:55:24 +01:00
KotlinGeekDev
b94f8c9083 Foundations(iOS Sourceset): - Fix test resource loading for iOS targets(using Gradle env. variables).
- Bring in the SwiftPackageManager for KMP plugin, to use Swift libraries for iOS implementations(and configure accordingly).
  - Bring in cachemap dependency(for iOS LargeCache implementation).
2026-01-12 14:52:12 +01:00
Róbert Nagy
30f7077ea3 Merge branch 'main' into phase1-final 2026-01-12 07:02:49 +02:00
nrobi144
e1412c1f97 fix: Add proper empty states with EOSE tracking
- NotificationsScreen: Show "No notifications yet" after EOSE instead of infinite loading
- FeedScreen: Show "No notes found" after EOSE for both Global and Following feeds
- ThreadScreen: Show "Note not found" if root note missing after EOSE, "No replies yet" for empty threads

Uses existing EmptyState component from commons. Tracks EOSE (End of Stored Events) from relays to distinguish between "still loading" and "no data".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:52:33 +02:00
nrobi144
ec629ac3fe feat: Extract Card interface and CardFeedState to commons (B1)
- Create commons/ui/notifications/CardFeedState.kt with:
  - Card interface (createdAt, id)
  - CardFeedState sealed class (Loading, Loaded, Empty, FeedError)
  - DefaultCardComparator for sorting

- Update Android imports to use commons Card/CardFeedState:
  - DefaultFeedOrder.kt
  - AccountViewModel.kt
  - CardFeedContentState.kt
  - CardFeedState.kt (now only contains concrete card types)
  - CardFeedView.kt

Part of Phase B (Notifications Feed Extraction) for #1648

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:34:14 +02:00
nrobi144
45207857c6 finish ThreadScreen 2026-01-12 06:23:57 +02:00
David Kaspar
dea0c336ef Merge pull request #1661 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-11 17:54:16 +00:00
davotoula
1e898d57b2 code review fixes:
textHint variable was retrieved but never used
extracted logic for fixing relay hints and author information in tags
made mimeType check logic clearer
2026-01-11 18:34:59 +01:00
Crowdin Bot
b71bb8cd2b New Crowdin translations by GitHub Action 2026-01-11 17:20:41 +00:00
Vitor Pamplona
d9a9cdd374 Fixes lack of update in follow count on the UserProfile page 2026-01-11 12:18:21 -05:00
davotoula
f51d0fb56e code review:
added p-tag for voice reply to kind 1
allow waveform IMeta with a missing mimeType to render as audio
2026-01-11 17:42:05 +01:00
davotoula
70c638ad90 refactor to a shared composable to eliminate waveform code duplication 2026-01-11 10:37:25 +01:00
davotoula
d8093f64b1 Display kind 1 voice replies as audio waveform 2026-01-11 10:37:24 +01:00
davotoula
58b97e0695 Voice reply to VoiceEvent/VoiceReplyEvent (KIND 1222/1244) → Creates VoiceReplyEvent (KIND 1244)
Voice reply to TextNoteEvent (KIND 1) → Creates TextNoteEvent (KIND 1) with audio IMeta attachment and proper reply tags
2026-01-11 10:12:43 +01:00
Vitor Pamplona
d24fe03bdd Avoids crashing when the k tag is a text 2026-01-10 18:34:01 -05:00
Vitor Pamplona
1cf2beb92e Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  remove unused imports
  guard for NaN crash with short recordings
  fix re-record button
  change icon to stop icon
  change from "hold to record" to "click to start, click to stop"
2026-01-10 12:10:40 -05:00
Vitor Pamplona
3e88eed640 Merge pull request #1657 from davotoula/record-voice-on-off-button
Record voice on off button instead of hold to record
2026-01-10 12:09:59 -05:00
Vitor Pamplona
30e5b3d35e removes bringing back all versions of an Addressable on the search screen. 2026-01-10 12:08:42 -05:00
David Kaspar
1882792a9d Merge pull request #1658 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-10 17:07:09 +00:00
Crowdin Bot
a935c7cecf New Crowdin translations by GitHub Action 2026-01-10 17:06:00 +00:00
Vitor Pamplona
cf0f71d04f Fixes: https://github.com/vitorpamplona/amethyst/issues/1621
Renders the fork information at the second line for the Master Note as well.
2026-01-10 12:02:26 -05:00
davotoula
d52906fda0 remove unused imports 2026-01-10 17:59:01 +01:00
davotoula
7b6b0f66a5 guard for NaN crash with short recordings 2026-01-10 17:59:01 +01:00
davotoula
5faa439a8b fix re-record button 2026-01-10 17:37:50 +01:00
davotoula
524180bec9 change icon to stop icon 2026-01-10 17:37:50 +01:00
davotoula
3c12be354e change from "hold to record" to "click to start, click to stop" 2026-01-10 17:37:50 +01:00
Vitor Pamplona
045f2aced9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
2026-01-10 11:36:22 -05:00
Vitor Pamplona
1ead2983ac Moves to a fork model via interfaces to start creating specific screens for each event type 2026-01-10 11:36:11 -05:00
Vitor Pamplona
1c26ccecc0 Adds support for NipTexts on Amethyst 2026-01-10 11:15:03 -05:00
nrobi144
6aacb8e654 move ThreadFilter to commons 2026-01-09 14:40:30 +02:00
David Kaspar
d8b8650048 Merge pull request #1652 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-09 11:30:39 +00:00
Crowdin Bot
c7b408f79d New Crowdin translations by GitHub Action 2026-01-09 11:29:27 +00:00
David Kaspar
761addb92e Merge pull request #1651 from davotoula/update-translations
update cz, pt, de, sv
2026-01-09 11:28:09 +00:00
davotoula
e874e302e0 update cz, pt, de, sv 2026-01-09 11:17:56 +01:00
nrobi144
a6f49665a7 feat: Extract drawReplyLevel modifier to commons
Move the thread reply level visualization modifier to shared commons
module for use by both Android and Desktop platforms.

- Add ThreadModifiers.kt to commons/ui/thread/
- Update Android ThreadFeedView to use shared modifier
- Add overload for non-State level parameter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 07:14:52 +02:00
nrobi144
9836a47f65 update Android imports 2026-01-09 07:01:36 +02:00
nrobi144
a6d90c319e feat: Add lifecycle-viewmodel KMP dependencies to commons
Adds androidx.lifecycle.viewmodel.compose and lifecycle.runtime.compose
to commons/commonMain as preparation for extracting ViewModels to shared code.

These dependencies have KMP support since 2.8.0 (currently on 2.10.0).

Preparation for Phase A2: Extract Thread ViewModels to commons.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 07:01:36 +02:00
nrobi144
211305c572 A1 extract to commons 2026-01-09 07:01:35 +02:00
Vitor Pamplona
54c1605de7 v1.05.1 2026-01-08 19:01:01 -05:00
Vitor Pamplona
dca9a1da47 Clicking on Drafts now edits the post. 2026-01-08 18:29:00 -05:00
Vitor Pamplona
466ae37b85 Fixes bug of mixing DMs between the logged in users. 2026-01-08 17:30:46 -05:00
Vitor Pamplona
b586971f00 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-01-08 16:23:16 -05:00
Vitor Pamplona
e12d924c59 Merge pull request #1650 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-01-08 10:57:57 -05:00
Crowdin Bot
a972891059 New Crowdin translations by GitHub Action 2026-01-08 15:03:34 +00:00
KotlinGeekDev
086e929e32 Make some modifications to CustomBitSet. 2026-01-07 22:27:01 +01:00
KotlinGeekDev
bd030e2e96 Merge branch 'vitorpamplona:main' into kmp-completeness 2025-12-30 00:39:11 +01:00
KotlinGeekDev
5db9384e7f Foundations(iOS Sourceset): Linter fixes for EventHasherSerializer and Secp256k1Instance. 2025-12-29 19:01:22 +01:00
KotlinGeekDev
4feaec5e00 Foundations(iOS Sourceset): Implement functions(some, basic) for BitSet(with passing tests that use it). 2025-12-29 19:00:00 +01:00
KotlinGeekDev
356b49dba9 Foundations(iOS Sourceset): Implement functions(some, basic) for EventHasherSerializer. 2025-12-26 16:17:58 +01:00
KotlinGeekDev
59c18f6b23 Foundations(iOS Sourceset): Implement secp256k1 instance functions. Small fixes in TestResourceLoader. 2025-12-26 15:27:50 +01:00
3626 changed files with 209649 additions and 23011 deletions

View File

@@ -1,8 +1,14 @@
# Amethyst Desktop Fork
# Amethyst
## Project Overview
Fork of [Amethyst](https://github.com/vitorpamplona/amethyst) adding Compose Multiplatform Desktop support. Quartz library converted to full KMP for code sharing between Android and Desktop JVM.
Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching
over to a Kotlin Multiplatform project. This project has 4 main modules: `quartz`, `commons`,
`amethyst` and `desktopApp`. Quartz should contain implementations of Nostr specifications and
utilities to help implement them. Commons stores shared code between Amethyst Android (`amethyst`)
and Amethyst Desktop (`desktopApp`). The Desktop App is designed to be mouse first and so uses a
completely different screen and navigation architecture while sharing the back end components with
the android counterpart.
## Architecture
@@ -12,7 +18,8 @@ amethyst/
│ └── src/
│ ├── commonMain/ # Shared Nostr protocol, data models
│ ├── androidMain/ # Android-specific (crypto, storage)
── jvmMain/ # Desktop JVM-specific
── jvmMain/ # Desktop JVM-specific
│ └── iosMain/ # iOS-specific
├── commons/ # Shared UI components (convert to KMP)
│ └── src/
│ ├── commonMain/ # Shared composables, icons, state
@@ -20,12 +27,12 @@ amethyst/
│ └── jvmMain/ # Desktop-specific UI utilities
├── desktopApp/ # Desktop JVM application (layouts, navigation)
├── amethyst/ # Android app (layouts, navigation)
└── ammolite/ # Support module
└── ammolite/ # Support module (unused)
```
**Sharing Philosophy:**
- `quartz/` = Business logic, protocol, data (no UI)
- `commons/` = Shared UI components, icons, composables, **ViewModels**
- `quartz/` = Nostr business logic, protocol, data (no UI)
- `commons/` = Shared UI components, icons, composables, flows and ViewModels
- `amethyst/` & `desktopApp/` = Platform-native layouts and navigation
## Tech Stack
@@ -241,6 +248,13 @@ actual fun openExternalUrl(url: String) {
}
```
## Code Formatting
After completing any task that modifies Kotlin files, always run:
```
./gradlew spotlessApply
```
Do this before considering the task complete.
### Navigation Shell
- **Desktop**: Sidebar + main content area
- **Android**: Bottom navigation

View File

@@ -119,7 +119,7 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat
**Focus:** iosMain patterns, Swift/KMP interop, XCFramework generation
**SKILL.md sections:**
- iOS source sets: iosMain, iosX64Main, iosArm64Main
- iOS source sets: iosMain, iosArm64Main
- Swift interop: type mapping, nullability
- expect/actual iOS: 10+ examples from quartz/iosMain
- XCFramework setup: baseName = "quartz-kmpKit"

180
.claude/hooks/session-start.sh Executable file
View File

@@ -0,0 +1,180 @@
#!/bin/bash
# Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web
set -euo pipefail
# Only run in remote (web) environments
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
# --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set ---
proxy="${https_proxy:-${HTTPS_PROXY:-}}"
if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then
rest="${proxy#*://}"
userpass="${rest%@*}"
hostport="${rest##*@}"
user="${userpass%%:*}"
pass="${userpass#*:}"
host="${hostport%%:*}"
port="${hostport##*:}"
port="${port%/}"
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<proxies>
<proxy>
<id>ccw</id><active>true</active><protocol>https</protocol>
<host>$host</host><port>$port</port>
<username>$user</username>
<password><![CDATA[$pass]]></password>
</proxy>
</proxies>
</settings>
EOF
# Force wagon transport for Maven 3.9+ proxy auth compatibility
cat > ~/.mavenrc << 'MAVENRC'
MAVEN_OPTS="$MAVEN_OPTS -Dmaven.resolver.transport=wagon"
MAVENRC
mkdir -p ~/.gradle
cat > ~/.gradle/gradle.properties << EOF
systemProp.https.proxyHost=$host
systemProp.https.proxyPort=$port
systemProp.https.proxyUser=$user
systemProp.https.proxyPassword=$pass
systemProp.http.proxyHost=$host
systemProp.http.proxyPort=$port
systemProp.http.proxyUser=$user
systemProp.http.proxyPassword=$pass
# Override nonProxyHosts: route all external traffic (incl. *.google.com) through proxy
systemProp.http.nonProxyHosts=localhost|127.0.0.1
systemProp.https.nonProxyHosts=localhost|127.0.0.1
# Use Ubuntu's Java trust store (includes Anthropic TLS inspection CA) for all Gradle JVMs.
# This is needed because Gradle may download a custom JDK (e.g. JetBrains) whose bundled
# trust store doesn't include the Anthropic CA, causing TLS inspection failures.
systemProp.javax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts
systemProp.javax.net.ssl.trustStoreType=JKS
systemProp.javax.net.ssl.trustStorePassword=changeit
systemProp.jdk.http.auth.tunneling.disabledSchemes=
systemProp.jdk.http.auth.proxying.disabledSchemes=
EOF
echo "Configured Maven/Gradle proxy from HTTPS_PROXY" >&2
fi
# --- SSL trust: import Anthropic TLS inspection CA into JVM trust stores ---
ANTHROPIC_CA_PEM=$(python3 -c "
import re, ssl, sys
try:
with open('/etc/ssl/certs/ca-certificates.crt') as f:
certs = re.findall(r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', f.read(), re.DOTALL)
for cert in certs:
der = ssl.PEM_cert_to_DER_cert(cert)
if b'Anthropic' in der and b'sandbox-egress-production' in der:
print(cert)
break
except Exception as e:
sys.stderr.write(f'CA extraction failed: {e}\n')
" 2>/dev/null)
if [ -n "$ANTHROPIC_CA_PEM" ]; then
TMPCA=$(mktemp /tmp/anthropic-ca.XXXXXX.pem)
echo "$ANTHROPIC_CA_PEM" > "$TMPCA"
for cacerts in \
/usr/lib/jvm/java-21-openjdk-amd64/lib/security/cacerts \
/root/.gradle/jdks/*/lib/security/cacerts; do
[ -f "$cacerts" ] || continue
keytool -list -keystore "$cacerts" -storepass changeit \
-alias anthropic-egress-production-ca >/dev/null 2>&1 && continue
keytool -import \
-alias anthropic-egress-production-ca \
-file "$TMPCA" \
-keystore "$cacerts" \
-storepass changeit \
-noprompt >/dev/null 2>&1 && \
echo "Imported Anthropic CA into $cacerts" >&2
done
rm -f "$TMPCA"
fi
ANDROID_SDK_DIR="/root/android-sdk"
SDK_REPO_BASE="https://dl.google.com/android/repository"
# Install Android SDK packages by downloading directly with curl
# (sdkmanager cannot reach the SDK repository through the proxy)
install_sdk_package() {
local zip_url="$1"
local dest_dir="$2"
local inner_dir="$3" # top-level dir inside the zip
if [ -d "$dest_dir" ]; then
return 0
fi
echo "Downloading $zip_url..."
local TMP_ZIP
TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip)
curl -fsSL "$zip_url" -o "$TMP_ZIP"
local TMP_DIR
TMP_DIR=$(mktemp -d)
unzip -q "$TMP_ZIP" -d "$TMP_DIR"
rm -f "$TMP_ZIP"
mkdir -p "$(dirname "$dest_dir")"
mv "$TMP_DIR/$inner_dir" "$dest_dir"
rm -rf "$TMP_DIR"
echo "Installed to $dest_dir"
}
# Install Android platform 36
install_sdk_package \
"$SDK_REPO_BASE/platform-36_r02.zip" \
"$ANDROID_SDK_DIR/platforms/android-36" \
"android-36"
# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name)
install_sdk_package \
"$SDK_REPO_BASE/build-tools_r36_linux.zip" \
"$ANDROID_SDK_DIR/build-tools/36.0.0" \
"android-16"
# Install platform-tools
install_sdk_package \
"$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \
"$ANDROID_SDK_DIR/platform-tools" \
"platform-tools"
# Accept SDK licenses (create license files manually)
echo "Writing SDK license files..."
mkdir -p "$ANDROID_SDK_DIR/licenses"
# android-sdk-license
echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license"
echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license"
# android-sdk-preview-license
echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license"
echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license"
# intel-android-extra-license
echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license"
# Create local.properties if missing
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")"
LOCAL_PROPS="$REPO_ROOT/local.properties"
if [ ! -f "$LOCAL_PROPS" ]; then
echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS"
echo "Created local.properties with sdk.dir=$ANDROID_SDK_DIR"
fi
# Export ANDROID_HOME for the session
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export ANDROID_HOME=$ANDROID_SDK_DIR" >> "$CLAUDE_ENV_FILE"
echo "export ANDROID_SDK_ROOT=$ANDROID_SDK_DIR" >> "$CLAUDE_ENV_FILE"
echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE"
fi
cd "$CLAUDE_PROJECT_DIR"
./gradlew --version > /dev/null 2>&1
echo "Android SDK setup complete."

26
.claude/settings.json Normal file
View File

@@ -0,0 +1,26 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./gradlew spotlessApply 2>/dev/null || spotless-apply",
"timeout": 120
}
]
}
]
}
}

View File

@@ -745,8 +745,8 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = 26 // Android 8.0 (Oreo)
targetSdk = 36 // Android 15
versionCode = 430
versionName = "1.04.2"
versionCode = 442
versionName = "1.08.0"
vectorDrawables {
useSupportLibrary = true

View File

@@ -0,0 +1,96 @@
---
name: find-missing-translations
description: Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language
---
# Find Missing Translations
## Overview
Extract string resource keys from the default `values/strings.xml` that are absent in a target locale's `strings.xml`, excluding non-translatable entries. Outputs missing keys and offers to translate them.
## When to Use
- Need to find untranslated strings for a specific locale
- Preparing a batch of strings for a translator
- Checking translation coverage after adding new features
## Target Locales
The default set of locales (unless the user specifies otherwise):
| Locale | Language | Directory |
|--------|----------|-----------|
| `cs-rCZ` | Czech | `values-cs-rCZ` |
| `pt-rBR` | Brazilian Portuguese | `values-pt-rBR` |
| `sv-rSE` | Swedish | `values-sv-rSE` |
| `de-rDE` | German | `values-de-rDE` |
## Technique
### 1. Identify files
```
Default: amethyst/src/main/res/values/strings.xml
Target: amethyst/src/main/res/values-<locale>/strings.xml
```
### 2. Find missing keys using cs-rCZ as reference
Always diff against `cs-rCZ` first — it is the most complete locale and serves as the reference. Any keys missing in `cs-rCZ` will also be missing in the other target locales.
```bash
# Extract translatable keys from default (exclude translatable="false")
comm -23 \
<(grep '<string name=' amethyst/src/main/res/values/strings.xml \
| grep -v 'translatable="false"' \
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
<(grep '<string name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \
| sed 's/.*name="\([^"]*\)".*/\1/' | sort)
```
This gives the list of missing key names. Do NOT diff each locale separately — assume the same keys are missing in all target locales.
### 3. Get English values for missing keys
For each missing key, extract its English value:
```bash
# For each missing key, extract the full line from default strings.xml
while IFS= read -r key; do
grep "name=\"$key\"" amethyst/src/main/res/values/strings.xml
done < <(comm -23 \
<(grep '<string name=' amethyst/src/main/res/values/strings.xml \
| grep -v 'translatable="false"' \
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
<(grep '<string name=' amethyst/src/main/res/values-cs-rCZ/strings.xml \
| sed 's/.*name="\([^"]*\)".*/\1/' | sort))
```
### 4. Present results and ask to translate
Output the missing entries as raw XML resource lines (copy-paste ready):
```xml
<string name="attestation_valid">Valid</string>
<string name="attestation_valid_from">Valid from %1$s</string>
<string name="feed_group_lists">Lists</string>
```
Also check `<string-array>` and `<plurals>` tags using the same approach if the project uses them.
**Then ask the user:** "Would you like me to translate these missing strings into [list of target locales]?"
### 5. Adding translations (if approved)
When adding translated strings to locale files:
- **Append new strings at the bottom** of the file, just before the closing `</resources>` tag.
- Do NOT try to insert them in alphabetical or matching order — a separate process handles ordering.
## Common Mistakes
- **Forgetting `translatable="false"`** — these should never appear in locale files
- **Not checking string-arrays/plurals** — only checking `<string>` misses other resource types
- **Diffing each locale separately** — only diff against `cs-rCZ`; assume the same keys are missing everywhere
- **Inserting strings in a specific position** — always append at the bottom; ordering is handled separately

View File

@@ -0,0 +1,86 @@
---
name: find-non-lambda-logs
description: Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
---
# Find Non-Lambda Log Calls
## Overview
Locates `Log.d/i/w/e` calls that use string interpolation without the lambda overload, wasting string allocation when the log level is filtered out in release builds.
## When to Use
- After merging branches that add new logging
- Periodic audit of logging hygiene
- After migrating `android.util.Log` usages to the shared `Log` wrapper
## What to Flag
Calls with **string interpolation** (`$` in message) that do **not** pass a throwable:
```kotlin
// FLAG - interpolation without lambda, no throwable
Log.d("Tag", "Processing ${event.id}")
Log.w("Tag", "Failed for $url")
// IGNORE - passes throwable (lambda overload doesn't accept throwable)
Log.w("Tag", "Error: ${e.message}", e)
Log.e("Tag", "Failed for $url", throwable)
// IGNORE - no interpolation (no allocation benefit from lambda)
Log.d("Tag", "Initialization complete")
```
## Search Commands
**Important:** Tags can be string literals (`"Tag"`) or variables (`tag`, `LOG_TAG`). Run both patterns for each step.
### Step 1: Find interpolated Log.d/Log.i (highest priority — filtered in release)
```
pattern: Log\.(d|i)\("[^"]+",\s*"[^"]*\$
type: kotlin
```
```
pattern: Log\.(d|i)\(\w+,\s*"[^"]*\$
type: kotlin
```
### Step 2: Find interpolated Log.w/Log.e without throwable
```
pattern: Log\.(w|e)\("[^"]+",\s*"[^"]*\$
type: kotlin
```
```
pattern: Log\.(w|e)\(\w+,\s*"[^"]*\$
type: kotlin
```
Then **manually exclude** lines where a throwable is passed as third argument (ending with `, e)`, `, throwable)`, etc.). Check the actual line — a catch block catching `e` doesn't mean `e` is passed to the Log call.
### Step 3: Verify no android.util.Log leakage
```
pattern: android\.util\.Log\.(d|i|w|e|v)\(
type: kotlin
```
These bypass the `Log.minLevel` filter entirely. Exclude `PlatformLog.android.kt` which is the wrapper implementation.
## Fix Pattern
```kotlin
// Before
Log.d("Tag", "Processing event ${event.id} from ${relay.url}")
// After
Log.d("Tag") { "Processing event ${event.id} from ${relay.url}" }
```
## Do NOT Convert
- Calls passing a `Throwable` parameter - the lambda overload `(tag) { message }` has no throwable parameter
- Static string calls with no `$` interpolation - no allocation benefit
- Commented-out log calls

View File

@@ -47,7 +47,7 @@
### :quartz (KMP Nostr Library)
**Type:** Kotlin Multiplatform Library
**Targets:** JVM, Android, iOS (iosX64, iosArm64, iosSimulatorArm64)
**Targets:** JVM, Android, iOS (iosArm64, iosSimulatorArm64)
**Dependencies:**
- External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable
- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain
@@ -127,7 +127,6 @@ commonMain (base)
│ ├─ androidMain (Android platform)
│ └─ jvmMain (Desktop platform)
└─ iosMain (iOS platform)
├─ iosX64Main
├─ iosArm64Main
└─ iosSimulatorArm64Main
```

View File

@@ -110,8 +110,8 @@ Think of source sets as a dependency graph, not folders.
│ - Jackson │ │ │
│ - OkHttp │ └────┬─────────────┘
└───┬───────────┬───┘ │
│ │ ├─→ iosX64Main
▼ ▼ ├─→ iosArm64Main
│ │
▼ ▼ ├─→ iosArm64Main
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
│android │ │jvmMain │
│Main │ │(Desktop) │
@@ -252,7 +252,7 @@ expect fun currentTimeSeconds(): Long
**iOS (iosMain):**
- Active development, framework configured
- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main
- Architecture targets: macosArm64Main, iosArm64Main, iosSimulatorArm64Main
- Platform APIs via platform.posix, Security framework
### Web, wasm - Future Targets

View File

@@ -29,7 +29,7 @@ Visual guide to source set organization with concrete examples from the codebase
│ - Jackson │ │ - Platform libs │
│ - OkHttp │ └───────┬───────────┘
└────┬─────────┬───┘ │
│ │ ├─→ iosX64Main (simulator Intel)
│ │
│ │ ├─→ iosArm64Main (device ARM64)
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
▼ ▼
@@ -238,7 +238,6 @@ iosMain {
}
}
val iosX64Main by getting { dependsOn(iosMain.get()) }
val iosArm64Main by getting { dependsOn(iosMain.get()) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
```
@@ -249,7 +248,6 @@ val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
- Different from Android/Desktop
**Architecture targets:**
- iosX64Main: Intel simulator
- iosArm64Main: Device (iPhone, iPad)
- iosSimulatorArm64Main: Apple Silicon simulator
@@ -326,7 +324,7 @@ commonMain
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
| iosMain | commonMain | iOS platform | Security framework |
| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific |
| iosMain | Simulator (Intel) | Architecture-specific |
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
| jsMain | commonMain | JS/DOM | Web (future) |
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |

View File

@@ -76,7 +76,6 @@ fun main() = application {
**Source sets:**
- iosMain (common iOS code)
- iosX64Main (Intel simulator)
- iosArm64Main (device - iPhone/iPad)
- iosSimulatorArm64Main (Apple Silicon simulator)
@@ -110,7 +109,7 @@ actual object Secp256k1Instance {
```kotlin
// quartz/build.gradle.kts
kotlin {
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
listOf(macosArm64(), iosArm64(), iosSimulatorArm64())
.forEach { target ->
target.binaries.framework {
baseName = "quartz-kmpKit"
@@ -310,7 +309,7 @@ fun parseJson(json: String): Event {
- Manual desktop app testing
**iOS:**
- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.)
- Unit tests: iosTest (iosArm64Test, etc.)
- Simulator/device testing
**Web (future):**

View File

@@ -0,0 +1,671 @@
---
name: quartz-integration
description: Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSignerInternal, (6) using Bech32 encoding/decoding (NIP-19), (7) platform-specific setup (Android vs JVM/Desktop), (8) NIP-57 zaps, NIP-17 DMs, NIP-44 encryption in external projects.
---
# Quartz Integration Guide
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.08.0` (Maven Central)
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
**License**: MIT
---
## 1. Gradle Setup
### Version Catalog (`libs.versions.toml`)
```toml
[versions]
quartz = "1.08.0"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
```
### `build.gradle.kts` (KMP project)
```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.quartz)
}
}
}
```
### Android-only project
```kotlin
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.08.0")
}
```
### Transitive dependencies pulled in automatically
Quartz exposes these as `api` (you get them transitively):
| Dependency | Used for |
|-----------|----------|
| `fr.acinq.secp256k1:secp256k1-kmp-*` | Schnorr signing |
| `com.github.anthonynsimon:rfc3986-normalizer` | Relay URL normalization |
| `com.fasterxml.jackson.module:jackson-module-kotlin` | Event JSON parsing |
For Android, add to `build.gradle.kts`:
```kotlin
android {
packaging {
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
```
---
## 2. Key Concepts
### Core Types
```kotlin
typealias HexKey = String // 64-char hex string (pubkey, event id, sig)
typealias Kind = Int // Event kind number
typealias TagArray = Array<Array<String>>
```
### Event Anatomy
```kotlin
@Immutable
open class Event(
val id: HexKey, // SHA-256 of canonical JSON (64 hex chars)
val pubKey: HexKey, // Author public key (64 hex chars)
val createdAt: Long, // Unix timestamp (seconds)
val kind: Kind, // Event type
val tags: TagArray, // [["e","eventid"], ["p","pubkey"], ...]
val content: String,
val sig: HexKey, // Schnorr signature (128 hex chars)
)
```
### Kind Classification
```kotlin
// Regular events — stored by relays forever
val isRegular = kind in 1..9999
// Replaceable events — relay keeps only latest per (pubkey, kind)
val isReplaceable = kind == 0 || kind == 3 || kind in 10000..19999
// Addressable events — relay keeps latest per (pubkey, kind, d-tag)
val isAddressable = kind in 30000..39999
// Ephemeral events — relays don't persist
val isEphemeral = kind in 20000..29999
```
---
## 3. Key Management
### Generate a new keypair
```kotlin
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
// Generate fresh random keys
val keyPair = KeyPair()
// From existing private key bytes
val keyPair = KeyPair(privKey = myPrivKeyBytes)
// Read-only (public key only, cannot sign)
val keyPair = KeyPair(pubKey = myPubKeyBytes)
// Access
val pubKeyHex: String = keyPair.pubKey.toHexKey()
val privKeyHex: String? = keyPair.privKey?.toHexKey()
```
### Convert between formats
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
// ByteArray → hex
val hex = byteArray.toHexKey()
// hex → ByteArray
val bytes = HexKey.decodeHex(hex)
// Bech32 import (npub, nsec)
val parsed = Nip19Parser.uriToRoute("npub1abc...")
// or
val parsed = Nip19Parser.uriToRoute("nsec1abc...")
```
---
## 4. Signing Events
### `NostrSignerInternal` (local key, JVM + Android)
```kotlin
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
val keyPair = KeyPair()
val signer = NostrSignerInternal(keyPair)
// Sign any EventTemplate
val template = TextNoteEvent.build("Hello Nostr!")
val signedEvent: TextNoteEvent = signer.sign(template)
```
### `NostrSignerSync` (synchronous, for testing)
```kotlin
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
val signerSync = NostrSignerSync(keyPair)
val event = signerSync.sign<TextNoteEvent>(
createdAt = TimeUtils.now(),
kind = 1,
tags = emptyArray(),
content = "Hello!"
)
```
### NostrSigner interface (for custom signers)
```kotlin
abstract class NostrSigner(val pubKey: HexKey) {
abstract fun isWriteable(): Boolean
abstract suspend fun <T : Event> sign(createdAt: Long, kind: Int, tags: Array<Array<String>>, content: String): T
abstract suspend fun nip04Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip04Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun nip44Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip44Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun deriveKey(nonce: HexKey): HexKey
abstract fun hasForegroundSupport(): Boolean
// Convenience: auto-detects NIP-04 vs NIP-44 by ciphertext format
suspend fun decrypt(encryptedContent: String, fromPublicKey: HexKey): String
}
```
---
## 5. Creating Events
### Using typed event builders (recommended)
```kotlin
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
// Kind 1 — Text note
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Kind 1 — Reply
val replyTemplate = TextNoteEvent.build(
note = "Interesting thread!",
replyingTo = originalEventHintBundle
)
// Kind 7 — Reaction
val reactionTemplate = ReactionEvent.build(
content = "+", // "+" = like, "-" = dislike, emoji = custom
originalNote = targetEvent
)
```
### Using low-level `Event.build` DSL
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.Event
val template = Event.build(
kind = 1,
content = "Hello world",
createdAt = TimeUtils.now()
) {
// TagArrayBuilder DSL
add(arrayOf("p", mentionedPubKey))
add(arrayOf("t", "nostr"))
add(arrayOf("subject", "Greeting"))
}
val event: Event = signer.sign(template)
```
### TagArrayBuilder DSL methods
```kotlin
// In the DSL lambda:
add(arrayOf("tagname", "value")) // append
addFirst(arrayOf("tagname", "value")) // prepend
addUnique(arrayOf("d", "my-slug")) // replace all tags with same name
addAll(listOf(arrayOf("t", "tag1"), ...)) // bulk add
remove("tagname") // remove all with this name
```
## 6. Relay Client Setup (JVM / Android)
The relay client requires an OkHttp WebSocket builder (available on JVM + Android).
### Minimal setup
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
import okhttp3.OkHttpClient
// Build the WebSocket factory
val okHttpClient = OkHttpClient.Builder().build()
val wsBuilder = BasicOkHttpWebSocket.Builder { _ -> okHttpClient }
// Create client (manages its own CoroutineScope internally)
val nostrClient = NostrClient(websocketBuilder = wsBuilder)
nostrClient.connect()
```
### With custom OkHttpClient per relay
```kotlin
val wsBuilder = BasicOkHttpWebSocket.Builder { normalizedUrl ->
if (normalizedUrl.url.contains(".onion")) {
torEnabledOkHttpClient // Tor proxy for .onion relays
} else {
regularOkHttpClient
}
}
```
### With custom CoroutineScope
```kotlin
val appScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val nostrClient = NostrClient(wsBuilder, scope = appScope)
```
---
## 7. Subscribing to Events
### Normalize relay URLs first
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
// Returns NormalizedRelayUrl (wrapper with validated wss:// URL)
val relayUrl = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val relayUrlOrNull = RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io")
// Handles common fixes: https:// → wss://, strips whitespace, etc.
```
### Build a Filter
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
// Fetch a user's notes
val filter = Filter(
authors = listOf(pubKeyHex),
kinds = listOf(1),
limit = 50
)
// Since a timestamp
val filter = Filter(
kinds = listOf(1, 6),
since = System.currentTimeMillis() / 1000 - 3600 // last hour
)
// By event tags
val filter = Filter(
kinds = listOf(7),
tags = mapOf("e" to listOf(eventId)) // reactions to an event
)
// AND tag filter (NIP-91)
val filter = Filter(
kinds = listOf(1),
tagsAll = mapOf(
"t" to listOf("nostr"),
"p" to listOf(specificPubKey)
)
)
// Full-text search (NIP-50)
val filter = Filter(
kinds = listOf(1),
search = "bitcoin lightning"
)
```
### Open a subscription
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
val relay = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val subId = "my-sub-${System.currentTimeMillis()}"
val filtersMap = mapOf(relay to listOf(filter))
nostrClient.openReqSubscription(
subId = subId,
filters = filtersMap,
listener = object : IRequestListener {
override fun onEvent(subId: String, event: Event, relay: IRelayClient) {
println("Got event: ${event.id}")
}
override fun onEOSE(subId: String, relay: IRelayClient) {
println("End of stored events from ${relay.url}")
}
}
)
// Close when done
nostrClient.close(subId)
```
### Global relay listener
```kotlin
nostrClient.subscribe(object : IRelayClientListener {
override fun onIncomingMessage(relay: IRelayClient, msgStr: String, msg: Message) {
when (msg) {
is EventMessage -> handleEvent(msg.subscriptionId, msg.event)
is EoseMessage -> handleEose(msg.subscriptionId)
else -> {}
}
}
override fun onConnected(relay: IRelayClient, pingMillis: Int, compressed: Boolean) {
println("Connected to ${relay.url} in ${pingMillis}ms")
}
override fun onDisconnected(relay: IRelayClient) {
println("Disconnected from ${relay.url}")
}
// other callbacks: onConnecting, onSent, onCannotConnect
})
```
---
## 8. Publishing Events
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
val relaySet = setOf(
RelayUrlNormalizer.normalize("wss://relay.damus.io"),
RelayUrlNormalizer.normalize("wss://nos.lol"),
)
// Sign the event
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Send to relays (handles retry + reconnect automatically)
nostrClient.send(event, relaySet)
```
---
## 9. Event Serialization
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.Event
// Serialize to JSON string
val json: String = event.toJson()
// Parse from JSON string
val event: Event = Event.fromJson(json)
// Null-safe parse
val event: Event? = Event.fromJsonOrNull(json)
// Specific typed parse (returns base Event, cast if needed)
val textNote = Event.fromJson(json) as? TextNoteEvent
```
---
## 10. Bech32 Encoding / Decoding (NIP-19)
```kotlin
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
// Decode any bech32 entity
val result = Nip19Parser.uriToRoute("npub1abc...")
// Returns: NPub | NSec | Note | NEvent | NProfile | NAddr | null
when (val r = Nip19Parser.uriToRoute(input)) {
is Nip19Parser.Return.NPub -> println("pubkey: ${r.hex}")
is Nip19Parser.Return.Note -> println("event id: ${r.hex}")
is Nip19Parser.Return.NEvent -> println("event: ${r.hex}, relays: ${r.relays}")
is Nip19Parser.Return.NProfile -> println("profile: ${r.hex}")
is Nip19Parser.Return.NAddr -> println("address: ${r.kind}:${r.pubKey}:${r.dTag}")
null -> println("not a valid bech32 entity")
else -> {}
}
// The parser also handles nostr: URI scheme
val result = Nip19Parser.uriToRoute("nostr:npub1abc...")
```
---
## 11. Encryption
### NIP-44 (modern, recommended)
```kotlin
// Via signer (preferred)
val encrypted = signer.nip44Encrypt(
plaintext = "Secret message",
toPublicKey = recipientPubKeyHex
)
val decrypted = signer.nip44Decrypt(
ciphertext = encrypted,
fromPublicKey = senderPubKeyHex
)
// Auto-detect format (NIP-04 or NIP-44)
val plaintext = signer.decrypt(encryptedContent, fromPublicKeyHex)
```
### NIP-04 (legacy, avoid for new code)
```kotlin
val encrypted = signer.nip04Encrypt(plaintext, recipientPubKeyHex)
val decrypted = signer.nip04Decrypt(ciphertext, senderPubKeyHex)
```
---
## 12. Common NIP Event Builders
### NIP-02 — Follow list (kind 3)
```kotlin
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
val template = ContactListEvent.build(
follows = listOf(
ContactListEvent.Contact(pubKey = alicePubKey, relayUrl = "wss://relay.damus.io", petname = "alice"),
ContactListEvent.Contact(pubKey = bobPubKey)
)
)
```
### NIP-25 — Reaction (kind 7)
```kotlin
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
val like = ReactionEvent.build("+", targetEvent)
val dislike = ReactionEvent.build("-", targetEvent)
val custom = ReactionEvent.build("🤙", targetEvent)
```
### NIP-57 — Zap request (kind 9734)
```kotlin
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
val template = LnZapRequestEvent.build(
message = "Great post!",
relays = listOf("wss://relay.damus.io"),
target = targetEvent,
zapType = LnZapRequestEvent.ZapType.PUBLIC
)
val zapRequest: LnZapRequestEvent = signer.sign(template)
```
### NIP-59 — Gift wrap / sealed DM (kind 1059 + 14)
```kotlin
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
// Creates sealed rumor + gift wrap pair
val (dmEvent, giftWrap) = NIP17Factory.create(
msg = "Private message",
fromSigner = senderSigner,
toUsers = listOf(recipientPubKey),
relayList = listOf("wss://relay.damus.io")
)
```
### NIP-23 — Long-form article (kind 30023)
```kotlin
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
val template = LongTextNoteEvent.build(
body = markdownContent,
title = "My Article",
image = "https://example.com/cover.jpg",
summary = "A brief summary",
slug = "my-article" // d-tag
)
```
---
## 13. Platform-Specific Notes
### JVM / Desktop
```kotlin
// jvmMain dependencies needed in consuming project:
// secp256k1-kmp-jni-jvm and lazysodium-java are transitive from quartz
// But you need JNA on the classpath for libsodium:
implementation("net.java.dev.jna:jna:5.18.1")
```
### Android
```kotlin
// androidMain dependencies (transitive from quartz):
// secp256k1-kmp-jni-android, lazysodium-android, jna (aar)
// No extra setup needed beyond the maven dependency.
// For NIP-55 (Android external signer apps):
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
```
### iOS
The library produces an XCFramework named `quartz-kmpKit`.
```bash
# Build XCFramework
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
# Output: quartz/build/XCFrameworks/release/quartz-kmpKit.xcframework
```
In Xcode: drag & drop the `.xcframework` into your project, then use from Swift via Kotlin/Native interop.
---
## 14. Event Store (Android only)
SQLite-based storage with full NIP support (NIP-09, NIP-40, NIP-45, NIP-50, NIP-62):
```kotlin
import com.vitorpamplona.quartz.nip01Core.store.EventStore
import android.content.Context
val store = EventStore()
// Insert
store.insert(event)
// Query
val events = store.query(
Filter(authors = listOf(pubKey), kinds = listOf(1), limit = 50)
)
// Count (NIP-45)
val count = store.count(Filter(kinds = listOf(1)))
// Full-text search (NIP-50)
val results = store.query(Filter(search = "bitcoin"))
```
---
## 15. Quick Reference
| Task | API | Package |
|------|-----|---------|
| Generate keys | `KeyPair()` | `nip01Core.crypto` |
| Create signer | `NostrSignerInternal(keyPair)` | `nip01Core.signers` |
| Build event | `TextNoteEvent.build(...)` or `Event.build(kind, content) { tags }` | `nip10Notes`, `nip01Core.core` |
| Sign event | `signer.sign(template)` | `nip01Core.signers` |
| Serialize | `event.toJson()` | `nip01Core.core` |
| Parse | `Event.fromJson(json)` | `nip01Core.core` |
| Normalize relay URL | `RelayUrlNormalizer.normalize("wss://...")` | `nip01Core.relay.normalizer` |
| Setup relay client | `NostrClient(BasicOkHttpWebSocket.Builder { okhttp })` | `nip01Core.relay.client` |
| Subscribe | `client.openReqSubscription(subId, mapOf(relay to filters), listener)` | `nip01Core.relay.client` |
| Publish | `client.send(event, setOf(relayUrl))` | `nip01Core.relay.client` |
| NIP-44 encrypt | `signer.nip44Encrypt(text, recipientPubKey)` | `nip01Core.signers` |
| Bech32 decode | `Nip19Parser.uriToRoute("npub1...")` | `nip19Bech32` |
| Bech32 encode | `Nip19Bech32.createNPub(pubKeyHex)` | `nip19Bech32` |
## Common Event Kinds
| Kind | Event Type | NIP | Quartz class |
|------|-----------|-----|-------------|
| 0 | User metadata | 01 | `MetadataEvent` |
| 1 | Text note | 10 | `TextNoteEvent` |
| 3 | Follow list | 02 | `ContactListEvent` |
| 4 | Legacy DM | 04 | `PrivateDmEvent` |
| 5 | Deletion | 09 | `DeletionEvent` |
| 6 | Repost | 18 | `RepostEvent` |
| 7 | Reaction | 25 | `ReactionEvent` |
| 14 | Chat message (sealed) | 17 | `NIP17GroupMessage` |
| 1059 | Gift wrap | 59 | `GiftWrapEvent` |
| 9734 | Zap request | 57 | `LnZapRequestEvent` |
| 9735 | Zap receipt | 57 | `LnZapEvent` |
| 10002 | Relay list | 65 | `AdvertisedRelayListEvent` |
| 30023 | Long-form content | 23 | `LongTextNoteEvent` |
## Related Skills
- **nostr-expert** — Internal Quartz patterns for Amethyst development
- **kotlin-multiplatform** — KMP source sets, expect/actual patterns
- **kotlin-coroutines** — Flow patterns for relay event streams

View File

@@ -0,0 +1,142 @@
# Quartz Gradle Dependency Setup
## Current version
```
com.vitorpamplona.quartz:quartz:1.08.0
```
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
---
## KMP Project Setup
### `gradle/libs.versions.toml`
```toml
[versions]
quartz = "1.08.0"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
```
### `build.gradle.kts` (library/app module)
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary) // or androidLibrary/androidApplication
}
kotlin {
jvm()
androidLibrary {
namespace = "com.example.myapp"
compileSdk = 35
minSdk = 21
}
// optional: iOS targets
sourceSets {
commonMain.dependencies {
implementation(libs.quartz)
}
// No platform-specific deps needed — Quartz provides them transitively
}
}
```
---
## Android-only Project
```kotlin
// build.gradle.kts (app module)
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.08.0")
}
```
---
## JVM (Desktop) standalone
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.08.0")
// JNA needed for libsodium (NIP-44) on JVM
implementation("net.java.dev.jna:jna:5.18.1")
}
```
---
## Transitive Dependencies (what you get automatically)
### All platforms (`commonMain`)
- `org.jetbrains.kotlin:kotlin-stdlib`
- `org.jetbrains.kotlinx:kotlinx-coroutines-core`
- `org.jetbrains.kotlinx:kotlinx-collections-immutable`
- `org.jetbrains.kotlinx:kotlinx-serialization-json`
- `androidx.collection:collection` (LruCache)
- `androidx.compose.runtime:runtime-annotation` (@Immutable/@Stable)
- `fr.acinq.secp256k1:secp256k1-kmp` (Schnorr crypto — common)
### JVM + Android (`jvmAndroid`)
- `com.github.anthonynsimon:rfc3986-normalizer` (URL normalization)
- `com.fasterxml.jackson.module:jackson-module-kotlin` (JSON)
- `com.squareup.okhttp3:okhttp` (WebSocket)
- `ru.gildor.coroutines:kotlin-coroutines-okhttp`
- `nl.bommber:kchesslib` (NIP-64 chess, version pinned to 1.0.0)
### JVM only
- `fr.acinq.secp256k1:secp256k1-kmp-jni-jvm`
- `com.goterl:lazysodium-java` (NIP-44 encryption)
- `net.java.dev.jna:jna`
### Android only
- `fr.acinq.secp256k1:secp256k1-kmp-jni-android`
- `com.goterl:lazysodium-android`
- `net.java.dev.jna:jna` (aar)
- `androidx.core:core-ktx`
---
## Packaging / ProGuard
Quartz ships consumer ProGuard rules automatically (`publish = true` in the library).
You don't need to add manual keep rules for Quartz classes in your app.
For Android release builds, these classes are preserved:
- All `com.vitorpamplona.quartz.**` event and model classes
- Jackson serialization annotations
---
## Common Build Errors
### `Duplicate class kotlin.collections.jdk8`
Add to `gradle.properties`:
```properties
android.useFullClasspathForDexingTransform=true
```
### `Could not find lazysodium-java` on JVM
Ensure JNA is on the classpath:
```kotlin
implementation("net.java.dev.jna:jna:5.18.1")
```
### iOS: `Framework not found quartz-kmpKit`
Build the XCFramework first:
```bash
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
```
Then drag `quartz/build/XCFrameworks/release/quartz-kmpKit.xcframework` into Xcode.

View File

@@ -1,165 +1,23 @@
# Quartz KMP Conversion Skill
# Quartz KMP (Legacy Skill — Migration Complete)
When working with Quartz library conversion to Kotlin Multiplatform:
> The KMP migration of Quartz is **complete**. This file is kept for historical reference.
>
> For integrating Quartz into external projects, use the **`quartz-integration`** skill instead.
> For working with Quartz internals within Amethyst, use the **`nostr-expert`** skill.
## What was migrated
The Quartz library was successfully converted from Android-only to full KMP supporting:
- **commonMain** — All Nostr protocol logic, events, filters, tags
- **jvmAndroid** — OkHttp WebSocket, Jackson JSON, relay serializers
- **androidMain** — SQLite event store, NIP-55 Android signer
- **jvmMain** — Desktop JVM crypto (lazysodium-java, secp256k1-jni-jvm)
- **iosMain** — iOS targets (XCFramework `quartz-kmpKit`)
## Current artifact
## Current Structure (Android-only)
```
quartz/
├── build.gradle
└── src/
├── main/kotlin/ # All Nostr code here
├── test/
└── androidTest/
com.vitorpamplona.quartz:quartz:1.08.0
```
## Target Structure (KMP)
```
quartz/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/ # Shared protocol code
├── commonTest/kotlin/ # Shared tests
├── androidMain/kotlin/ # Android crypto, storage
├── androidTest/kotlin/
├── jvmMain/kotlin/ # Desktop crypto, storage
└── jvmTest/kotlin/
```
## Platform Abstractions Required
### 1. Cryptography (expect/actual)
```kotlin
// commonMain
expect object Secp256k1 {
fun sign(data: ByteArray, privateKey: ByteArray): ByteArray
fun verify(data: ByteArray, signature: ByteArray, pubKey: ByteArray): Boolean
fun pubKeyCreate(privateKey: ByteArray): ByteArray
}
// androidMain - uses secp256k1-kmp-jni-android
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
// jvmMain - uses secp256k1-kmp-jni-jvm
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
```
### 2. NIP-44 Encryption (Sodium)
```kotlin
// commonMain
expect object Nip44 {
fun encrypt(plaintext: String, sharedSecret: ByteArray): String
fun decrypt(ciphertext: String, sharedSecret: ByteArray): String
}
// androidMain - lazysodium-android
// jvmMain - lazysodium-java or libsodium-jni
```
### 3. Secure Random
```kotlin
// commonMain
expect fun secureRandomBytes(size: Int): ByteArray
// androidMain
actual fun secureRandomBytes(size: Int): ByteArray {
return SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
// jvmMain
actual fun secureRandomBytes(size: Int): ByteArray {
return java.security.SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
```
## Build Configuration
```kotlin
// quartz/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
jvm("desktop") {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.collections.immutable)
}
}
val androidMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.android)
implementation(libs.lazysodium.android)
}
}
val desktopMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
// lazysodium-java or alternative
}
}
}
}
android {
namespace = "com.vitorpamplona.quartz"
compileSdk = 35
defaultConfig.minSdk = 26
}
```
## Migration Steps
1. **Convert build.gradle to build.gradle.kts** with KMP plugin
2. **Move pure Kotlin code** to `commonMain/`
3. **Identify platform dependencies** (crypto, JNA, Android APIs)
4. **Create expect declarations** for platform-specific APIs
5. **Implement actuals** in androidMain and jvmMain
6. **Update imports** in amethyst module
7. **Test on both platforms**
## Files to Move to commonMain
Most of Quartz can be shared:
- Event classes and parsing
- Filter definitions
- Relay message types
- NIP implementations (logic only)
- Utilities (hex encoding, bech32)
## Files Needing expect/actual
- `Secp256k1.kt` - Signature operations
- `Nip04.kt` - Legacy encryption (uses AES)
- `Nip44.kt` - Modern encryption (uses ChaCha)
- `KeyPair.kt` - Key generation
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.

View File

@@ -12,8 +12,11 @@ echo "$JAVA_HOME"
echo "$(java -version)"
echo "Running test... "
./gradlew test
if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
./gradlew test --quiet -x :desktopApp:test -x :desktopApp:upxDownload -x :desktopApp:vlcDownload
else
./gradlew test --quiet
fi
status=$?
if [ "$status" = 0 ] ; then

View File

@@ -1,4 +1,4 @@
name: Test/Build Android
name: Test/Build
on:
pull_request:
@@ -6,78 +6,201 @@ on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Linter (gradle)
run: ./gradlew spotlessCheck
test:
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Test (gradle)
run: ./gradlew test --no-daemon
- name: Stop Gradle Daemon
if: always()
run: ./gradlew --stop
- name: Android Test Report
uses: asadmansr/android-test-report-action@v1.2.0
if: ${{ always() }} # IMPORTANT: run Android Test Report regardless
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
- name: Upload Test Results
uses: actions/upload-artifact@v6
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
with:
name: Test Reports
path: amethyst/build/reports
build-android:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build APK (gradle)
run: ./gradlew assembleDebug
- name: Upload Play APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Play Debug APK
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk
- name: Upload FDroid APK
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: FDroid Debug APK
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk
- name: Build APK (gradle)
- name: Build Benchmark APK (gradle)
run: ./gradlew assembleBenchmark
- name: Upload Play APK Benchmark
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Play Benchmark APK
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
- name: Upload FDroid APK Benchmark
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: FDroid Benchmark APK
path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
- name: Upload Compose Reports
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Compose Reports
path: amethyst/build/compose_compiler
- name: Upload Test Results
uses: actions/upload-artifact@v4
with:
name: Test Reports
path: amethyst/build/reports
build-desktop:
needs: test
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
task: packageDeb
artifact-name: Desktop Linux DEB
artifact-path: desktopApp/build/compose/binaries/main/deb/*.deb
- os: macos-latest
task: packageDmg
artifact-name: Desktop macOS DMG
artifact-path: desktopApp/build/compose/binaries/main/dmg/*.dmg
- os: windows-latest
task: packageMsi
artifact-name: Desktop Windows MSI
artifact-path: desktopApp/build/compose/binaries/main/msi/*.msi
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build Desktop Distribution
run: ./gradlew :desktopApp:${{ matrix.task }}
- name: Stop Gradle Daemon
if: always()
run: ./gradlew --stop
- name: Upload Desktop Distribution
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-path }}

View File

@@ -5,24 +5,46 @@ on:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: write
jobs:
deploy:
create-release:
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: true
deploy-android:
needs: create-release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
@@ -38,7 +60,6 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "36.0.0"
- name: Sign AAB (F-Droid)
@@ -50,7 +71,6 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "36.0.0"
- name: Build APK
@@ -65,7 +85,6 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "36.0.0"
- name: Sign APK (F-Droid)
@@ -77,20 +96,8 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "36.0.0"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: true
# Google Play APK
- name: Upload Play APK Universal Asset
id: upload-release-asset-play-universal-apk
@@ -98,7 +105,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -109,7 +116,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -120,7 +127,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -131,7 +138,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -142,7 +149,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -154,7 +161,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -165,7 +172,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -176,7 +183,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -187,7 +194,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -198,13 +205,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
# Google Play AAB
- name: Upload Google Play AAB Asset
id: upload-release-asset-play-aab
@@ -212,7 +217,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip
@@ -224,7 +229,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip
@@ -236,3 +241,65 @@ jobs:
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
deploy-desktop:
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
task: packageDeb
format: deb
platform: linux
- os: macos-latest
task: packageDmg
format: dmg
platform: macos
- os: windows-latest
task: packageMsi
format: msi
platform: windows
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build Desktop Distribution
run: ./gradlew :desktopApp:${{ matrix.task }}
- name: Find distribution file
id: find-dist
run: |
DIST_FILE=$(find desktopApp/build/compose/binaries/main/${{ matrix.format }} -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.msi" \) | head -1)
echo "path=$DIST_FILE" >> $GITHUB_OUTPUT
echo "name=$(basename $DIST_FILE)" >> $GITHUB_OUTPUT
- name: Upload Desktop Distribution to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ steps.find-dist.outputs.path }}
asset_name: amethyst-desktop-${{ matrix.platform }}-${{ github.ref_name }}.${{ matrix.format }}
asset_content_type: application/octet-stream

View File

@@ -5,13 +5,17 @@ on:
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
branches: [ main ]
permissions:
contents: write
pull-requests: write
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: crowdin action
uses: crowdin/github-action@v2

22
.gitignore vendored
View File

@@ -22,11 +22,22 @@
/.idea/AndroidProjectSystem.xml
/.idea/deviceManager.xml
/.idea/inspectionProfiles/
/.idea/migrations.xml
/desktopApp/vlc-temp/
/commons/.idea/gradle.xml
/commons/.idea/misc.xml
/commons/.idea/workspace.xml
/quartz/.idea/modules.xml
/quartz/.idea/workspace.xml
.DS_Store
/build
/captures
.cxx
# superpowers skill
.superpowers
docs/brainstorms
docs/superpowers
# Built application files
*.apk
@@ -144,3 +155,14 @@ lint/tmp/
# Local task tracking
TASKS.md
# Claude Code local settings
.claude/settings.local.json
# Downloaded VLC binaries (vlc-setup plugin)
desktopApp/src/jvmMain/appResources/linux/
desktopApp/src/jvmMain/appResources/macos/
desktopApp/src/jvmMain/appResources/windows/
# Git worktrees
.worktrees/

7
.idea/kotlinc.xml generated
View File

@@ -6,11 +6,8 @@
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="21" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.0" />
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.20" />
</component>
</project>

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,5 +1,508 @@
<a id="v1.08.0"></a>
# [Release v1.08.0: Arti](https://github.com/vitorpamplona/amethyst/releases/tag/v1.08.0) - 2026-04-01
- Migrates C Tor to Arti Tor (hopefully no more random crashes)
- Fixes highlight of users when composing and tagging
<a id="v1.07.5"></a>
# [Release v1.07.5: Image upload fix](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.5) - 2026-03-31
- Fixes Image uploads crashing the app
<a id="v1.07.4"></a>
# [Release v1.07.4: NWC fix](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.4) - 2026-03-31
- Fixes Nostr wallet connect receiving the secret.
<a id="v1.07.3"></a>
# [Release v1.07.3: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.3) - 2026-03-31
- Migrates Shorts UI out of a paged design
- New edge-to-edge feed for Pictures only
- New edge-to-edge feed for Shorts only
- New edge-to-edge feed for long Videos only
- Migrates Badges to kind 10008
- Migrates Bookmarks to kind 10003
- Fixes AOSP keyboard auto-correction bug
- Fixes Poll's top nav filter
- Fixes loading of Bookmark sets
- Fixes sorting stability by including event ids
- Fixes cursor position in quote post field
- Fixes rendering of nostr: uris when composing notes
- Adds Basic support for reaction notifications
- Refactors NIP-58 Badges on Quartz
- Refactors LocalCache methods
- Improvements to the Chess engine (debug only)
- Add error handling to Tor service initialization and control
<a id="v1.07.2"></a>
# [Release v1.07.2: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.2) - 2026-03-30
- Adds GIF uploads support from Keyboard
- Migrates text fields and @ modifiers to the new Jetpack Compose states
- Fixes Bug that wasn't openning Amber to sign
- Fixes Bug on rejections using old Ambers
- Replaces hex input with user search dialog in relay management (allow/ban user)
<a id="v1.07.1"></a>
# [Release v1.07.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.1) - 2026-03-30
- New translations
- Fixes BLE's module need for synchronized events.
<a id="v1.07.0"></a>
# [Release v1.07.0: Vanish, Relay Management, Pin Notes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.0) - 2026-03-30
Amethyst:
- Adds support to Pin Notes
- Adds support to Polls feed screen
- Adds support for Requests to Vanish
- Adds support for Relay management (NIP-86) from Amethyst
- Adds support for Relay monitor assessments (NIP-66) in the Relay Info screen.
- Adds support for Relay member information (NIP-43) in the Relay Info screen
- Adds support for WebBookmarks
- Adds support for Zap Goals
Quartz:
- Rewords the NostrClient API for simpler commands.
- Adds NIP-15: Nostr Marketplace protocol — product listings, stalls, merchant events (#2020)
- Adds NIP-24: Birthday field support added to UserMetadata (#1979)
- Adds NIP-29: Relay-based groups — group events, member management, moderation (#2021)
- Adds NIP-32: Labeling protocol — LabelEvent and tag parsing (#1975)
- Adds NIP-43: Relay access metadata and membership management with UI screens (#2010)
- Adds NIP-60: Cashu wallet and spending history event support (#2009)
- Adds NIP-61: Nutzaps protocol support (#2008)
- Adds NIP-62: Request to Vanish feature — data deletion with relay compliance testing (#1958)
- Adds NIP-66: Relay monitor discovery events — UI, filter assembler, subscription composable (#1968)
- Adds NIP-69: P2P Order Events (kind 38383) (#2019)
- Adds NIP-75: Zap Goals support with creation and rendering (#1965)
- Adds NIP-77: Negentropy set reconciliation protocol support (#1955)
- Adds NIP-7D: Thread events (kind 11) rendering (#2016)
- Adds NIP-85: Trusted Assertions — assertions for events and addressables (#1981)
- Adds NIP-86: Relay management UI and client implementation (#1954)
- Adds NIP-87: Ecash mint discoverability — Cashu and Fedimint event types (#2015)
- Adds NIP-89: Compliance fixes, PlatformLinkTag parsing, app handler extensions (#2030)
- Adds NIP-90: All DVM kind event classes from data-vending-machines spec, restructured packages (#2023, #2025)
- Adds NIP-A4: Add k tag to zap requests and enforce e tag prohibition (#1978)
- Adds NIP-BE: Bluetooth Low Energy mesh networking / Nostr BLE Communications Protocol (#2022)
- Adds NIP-C7: Chat messages (kind 9) implementation (#2018)
- Adds NIP-5A: Static website event rendering (#2017)
- Adds NIP-51, kind 10001: Pinned notes feature (#1956)
Improvements and Fixes:
- Show toast instead of dialog on media download success
- Dynamically adjust preferred Blossom server when list changes
- Add relay discovery to node master rendering list
- Add scroll to settings page
- Solves crashing when multiple relays with the same url are included in the resulting list.
- Keep screen on during PiP playback and survive screen lock (#1999)
- Desktop feed loading — missing events, broken profile navigation (#2027)
- Use getOrCreateNote for reply linking to fix flaky thread test (#2027)
- Route ReadsScreen following-mode events through cache (#2027)
- Pin ElectrumX server certs for Samsung One UI 7 / Android 16 compatibility (#1937)
- Duplicate keys in relay management lazy column — sort pubkeys
- URL detector — fixes localhost:3030 strings, Japanese character URLs
- Web Bookmarks floating action button shape (circle) and open graph previews.
- NIP-86 requests now send Accept and Content-Type headers
- On DMs, activates decryption for all filetypes that match decryption url with the cipher info, not only binaries
- Adds a try/finally to subscriptions to make sure they close even in crashes.
- Protects against crashes when the signer sends an unverifiable payload back to Amethyst
Desktop:
- Cache-centric architecture for desktop feeds (#1905)
- Render reposts and quoted notes in feed (#2027)
Performance
- Faster startup procedures with less loading on the main thread.
- 20x Faster Rfc3986Normalizer and way less objects being created.
- Url Detector without using regex
- Parallelize preference file access at startup for faster launch
- Remove internal runBlocking calls
- Lazy loading the memory trimming service
- Speeding up DrawerContent rendering
- Eagerly delete intermediate temp files in upload pipeline
Refactoring:
- Simplify NostrClient API for beginner-friendliness (#1986)
- Simplify relay API with AutoCloseable and serve() helper (#2031)
- URL detector performance and readability improvements (#2013)
- Restructure NIP-90 DVMs to match NIP-88 Polls pattern (#2023)
- Reorganize NIP-BE into subpackages
- Move public messages and trusted assertions out of experimental
Migrations & Deprecation Fixes
- Migrate `LocalClipboardManager` to `LocalClipboard` (#1995)
- Migrate `LocalAutofillTree` to semantics-based Autofill API (#1996)
- Migrate `TabRow`/`ScrollableTabRow` to Material 3 Secondary variants (#1994)
- Replace `ContextCompat.startActivity` with `Context.startActivity` (#1993)
- Suppress deprecation and unchecked cast warnings in Quartz internal code (#1985)
Platform & Build
- Add linuxX64 target and restructure native source sets
- Add explicit permissions to GitHub Actions workflows
- Stop Gradle daemons after build steps
- Increase Android CI build timeout to 45 minutes
- Update video compression library to latest
- Update dependencies, add local maven repo for easier library development
- Remove libsodium files
- Git hooks installation in worktrees (.git file vs directory)
- Fixes lack of proxy access to download VLC files in claude's web environment.
- Log level filtering — LogLevel enum, PlatformLog, lambda overloads for 194+ Log.d() calls (#2005)
- Parallelize BaseDBTest forEachDB using coroutines
Documentation
- Add CLIENT.md guide for building Nostr clients with Quartz
- Add RELAY.md guide for building relays with Ktor, NostrServer, SQLite
Contributors
- @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z — Primary development
- @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef — Log level filtering, video compression, toast downloads, CI fixes, git worktree fix
- @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c — Desktop cache architecture, desktop bugfixes
- @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 — CI workflow permissions
- @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k — Remove libsodium files
- **mstrofnone** — ElectrumX cert pinning for Samsung One UI 7
Translations
- Czech, German, Swedish, and Portuguese by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Hungarian by @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp
- French by @npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz
- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm
- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6
- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw
- Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
- Chinese by hypnotichemionus4 and @npub1gd8e0xfkylc7v8c5a6hkpj4gelwwcy99jt90lqjseqjj2t253s2s6ch58h
- Russian by Anton Zhao
<a id="v1.06.3"></a>
# [Release v1.06.3: Anon Posting](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.3) - 2026-03-24
Improvements to Anon Posting and Reply navigation on DMs
<a id="v1.06.2"></a>
# [Release v1.06.2: Multi-choice Polls](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.2) - 2026-03-24
- Add poll type selector for single and multiple choice polls
- Add drag-to-seek support on the video progress indicator
- Hide video controls on playback start
- Recording indicator bar stops recording when tapped, not just the small stop icon.
- Fixes non http uris in the references tag
- Makes sure quote tags only happen once in events.
- Removes the clickable NIP-05 URL in the @ tagging of users
- Reduces the need to compute naddr to index articles in bookmarks
- Checks if p tags are user pubkeys when loading drafts.
- Scroll to the replied message when clicking reply preview in chat
- Add an anonymous reply with a throwaway keypair
- Fixes pool rendering when one label is large and the other is small
- Use uri-reference-kmp in commonMain. Remove platform-specific implementations.
- Avoids crashing the app in URLs with japanese chars
- Sorts followers into a set to avoid LazyColumn key conflicts.
- Clickable relay rows on the profile page
- Move cache lookups from NavHost route lambdas into screen composables
- Improvements to the status of attestations. Validity first, then Processing status.
- Fixes Tor Manager flow value and improves Tor binding lifecycle.
- Simplifies the report feed in the user profile
- Clears some inconsistencies in translated strings
- Fixes client tags
- Fixes routes for AppDefinition events.
<a id="v1.06.1"></a>
# [Release v1.06.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.1) - 2026-03-23
- Improvements to the rendering of Polls.
- Solves some of the crashes of the concurrent modification exception
- Fixes URL parsers with Japanese chars
- Fixes Wallet import from Primal on Poco phones
- Improves the wording of the Last Seen
- Fixes for "Cannot disable reuse from root if it was caused by other groups"
- Fixes comparator to avoid Comparison method violates its general contract!
- Improves zap-store settings
<a id="v1.06.0"></a>
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2026-03-21
Polls:
- Adds support for creating and rendering NIP-85
- Redesign of the poll and zap poll cards
- Adds special notification card while the poll is running
Relay Feeds
- Adds support for rendering relay feeds
- Adds support for NIP-51 favorite relay feeds
- Shows favorite relays in the top navigation filter
- Clicking wss:// links shows the global feed for that relay.
- New user account adds nostr.wine to favorite relay feeds
Media Player
- Redesigned player controls for videos, audios, and picture-in-picture.
- Adds our own buttons and indicators for the video playback
- Adds Music support with waveform animations
- Migrates to new Media3 content view frames
- Improved Picture in Picture actions
- Turn video controller creation into a flow to fix playback lifecycle issues
- Adds support for uploading audio
NWC Wallets:
- Adds support for NIP-47 Wallets and compete NWC spec
- Adds views for Balance and Transactions
- Add transaction filtering and pagination to wallet screen
- Added several test cases from other repos to guarantee interoperability
Calendar:
- Adds support for NIP-52 Calendar appointments
- Adds proper display of calendar time slot and date slot events in the note feed
- Refactored the early implementation on Quartz for easier use
Code Snippets:
- Adds support for NIP-C0 Code Snippets
- Replies using NIP-22
NIPs on Nostr
- Adds support for event kind 30817
- Replies using NIP-22
PayTo:
- Adds support for NIP-A3 Payment targets by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
Blossom BUD-10:
- Adds support for "Blossom:" URIs on the post
- Supports automatic discovery of blossom servers
- Renders/Previews images, audios, videos, and documents
- Includes support for encryption when using it in NIP-17 DMs.
Expirations
- Adds enhanced support for custom expirations in any new post.
- Displays expirations on posts and DMs
Relay Monitors:
- Adds support for NIP-66 Relay monitor and discovery support to Quartz
Attestations:
- Adds support for rendering Attestations (https://attestr.xyz/)
- Recommendations, Requests and Attestor Declarations are also included.
Chess:
- Adds basic support for Chess with Jester protocol
- Full chess game implemented
- Supports for game challenges and view external games
- Running on debug only for now
DMs:
- Removes NIP-04 DMs
- Blocks DM sending if the receiver doesn't have NIP-17 relay lists.
- Removed incognito icon from the new post field.
Push Notifications:
- Adds support for inline reply
- Adds support for notification grouping
- Adds support for Async image Loading
- Removed NIP-04 notifications
Long Form:
- Adds support for writing Long Form/Markdown content
- Includes support for automatic Draft saving and editing
- Includes support for editing
Uploads:
- Adds support to upload Documents to all new post screens.
- Adds toggle to stip file metadata regardless of compression by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Adds encrypted file upload fallback option for NIP-17 chats
- Removes support for NIP-96 and updates Blossom recommendations
Content Warning:
- Adds an optional description field for sensitive content warnings in new posts.
- Displays additional information on warning composables
Settings redesign:
- Consolidate drawer settings into a single Settings hub screen
- Redesigns Zap Amount and NWC setup screens
- Redesigns Custom zap amount screens
- Adds brand new Translation Settings screen
- Adds blockchain explorer settings page for OTS verification
- Adds reactions row settings (enable/disable, order, show/hide counters) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
- Tapping on Zap without any pre-configured amount opens the custom dialog
Content parsers:
- URL/URI parser rewrite in Kotlin multiplatform (KMP)
- Fixes characters attached to URLs or nostr URLs without a space
- Massively increases parsing performance
- Treat multibyte characters as URL terminators in RichTextParser by @npub1k0jrarx8um0lyw3nmysn50539ky4k8p7gfgzgrsvn8d7lccx3d0s38dczd
- Adds a parser for blossom: uris
UI Improvements:
- Minimizes parent thread rendering in quoted notes by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
- New Material 3 UI for DropDowns by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- New Material 3 UI for feed filters by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Draft Screen requests confirmation before deleting drafts on swipe
- Swipe to switch tabs. Main screen and messages by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Adds support for rendering Zap events when quoted inside of posts.
- Adds a Broadcasting feedback pop-up in the Complete UI mode
Relay Management:
- Adds relay search tooltip when adding relays
- Adds the list of keys using each relay to the relay information
- Adds active subscriptions and outbox event in the queue to relay information
- Adds a complete list of event kind names to the subscription card to relay information
- Tracks and displays connection success rate on relay settings
- Adds relay settings export functionality
- Adds NIP-45 count queries to show how many events each relay has.
- Adds Relay sync utility to help users move posts between relays.
Search:
- Breaks the search filter into two subscriptions to prioritize Metadata without punishing content.
- Fixes the need to start user searches with @ in user fields
- Fixes the stability of the search feed when the user navigates away and back.
- Replaces about me for NIP-05 in the user search results
- Adds relay URL search to the search page
- Forces returning one user when searching by nip-05
- Removes outdated versions of addressables from the search results
Profiles:
- Adds support for NIP-39 External Identities with kind 10011
- Adds a profile picture upload button when the user has no picture
- Adds last seen to the user profile
- Adds nprofile and npub copy options to the profile
- Groups received zap amounts by sending the user in the profile tab
- Increases the limit of Zap downloads for profiles to 1000
- Simplifies profile edit screen layout by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
- Migrates profile galleries to display a thumbnail for videos
- Fixes profile galleries' aspect ratios
- Adds support for Namecoin .bit urls to NIP-05 and choice of ElectrumX server to resolve namecoins.
Onboarding
- Adds bulk follow screens to search for a user and to copy his/her follow list
Voice message by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Adds voice anonymization
- Change from "hold to record" to "click to start, click to stop"
- Display kind 1 voice replies as an audio waveform
- Increases max voice record duration to 600 seconds
- Switches the public message event to use quoted posts for replies
Fixes:
- Fixes "forked from" label rendering over the name
- Avoids crashing when the `k` tag cannot be parsed to a number
- Only use Voice Reply events when replying to voice notes. Others just receive a URL.
- Fixes the lack of update in the follow count on the UserProfile page
- Fixes out of memory when downloading large videos
- Fixes Jackson deserialization for empty Filters and add regression test by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
- Fixes NullPointerException when the filter contains tags
- Fixes download cancellations when screen components disappear
- Migrates to use "title" instead of "name" tags for NIP-51 lists
- Adds a longer crop for npubs so that we can see vanity keys better
- Fixes the need to have tags and kinds for inbox.nostr.wine to work
- Blocks the size of Relay Auth Status arrays from growing forever with auth messages
- Fixes crash when getting OpenGraph tags of invalid URLs
- Fixes NIP-44 key mutation in NIP-46 connect
- Location permission watcher moved outside screens to avoid recreation
- Solves the sorting contract crash on search by precaching all values before sorting users.
- Fixes lingering relay connections from loading follows outbox's settings.
- Enhance NIP-38 user status display with emoji support and metadata tags
- Fixes bug on Show More calculations for very long texts without spaces
- Fixing IO Dispatchers and coroutine scopes of choice
- Fixes anySync parallel operation that was returning the first result, not the first positive "any".
- Fixes Req onCannotConnect listeners to the relays that actually sent the req
- Fixes hanging subscriptions when exceptions happen during NostrClient utility methods
Defaults:
- Switches wss://nostr.band to wss://antiprimal.net, wss://relay.ditto.pub on app defaults
- Adds wss://nostr.wine, wss://news.utxo.one as favorite relay feeds
- Adds wss://directory.yabu.me and wss://profiles.nostr1.com as index relays
- Adds electrumx.testls.space, nmc2.bitcoins.sk, 46.229.238.187 and i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion as ElectrumX servers
Quartz:
- Adds Relay Server implementation with NIP-45 COUNT and NIP-42 AUTH support
- Adds support for dynamic auth policies to the relay implementation.
- Migrates Quartz EventStore from Android-only to KMP
- Adds a reqUntilEoseAsFlow extension to the Nostr Client
- Adds a reqBypassingRelayLimits extension to the Nostr Client
- Adds comprehensive NIP-46 Bunker support
- Adds comprehensive support for NIP-47 non-payment methods.
Adds complete support for iOS to Quartz by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k
- Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge.
- Provide implementation for LargeCache, using a CacheMap
- Provide implementation for fastFindURLs()
- Provide implementation for makeAbsoluteIfRelativeUrl() in ServerInfoParser.ios.kt
- Provide implementation for UrlEncoder
- Provide implementation for UnicodeNormalizer
- Provide implementation for GZip compression/decompression. Some small fixes in URLs.ios.kt
- Provide implementation for AESCBC
- Provide implementation for AESGCM
- Provide implementation for DigestInstance
- Provide implementation for LibSodium
Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c
- Adds NIP-46 Bunker Login
- Adds Support for Chess
- Adds Thread Screens
- Adds advanced search with query engine and filter panel
- Adds encrypted DMs (NIP-04/NIP-17)
- Adds proper empty states with EOSE tracking
- Adds multi-column deck layout
- Adds Full media parity — images, video, audio, encrypted DMs, upload, lightbox
- Adds advanced search with NIP-50, collapsible sections, and nav state preservation
- Clear stored credentials on logout
- Adds bunker heartbeat indicator
- Adds QR-based signer pairing
- Migrates lifecycle-viewmodel KMP dependencies to KMP/Commons
- Migrates drawReplyLevel modifier to KMP/Commons
- Migrates ThreadFilter to KMP/Commons
- Migrates Card interface and CardFeedState to KMP/Commons
- Migrates Channels (public chats, ephemeral channels, and live streams) Account modules to KMP/Commons
- Migrates private chatroom models to KMP/Commons
- Migrates reports states to KMP/Commons
- Migrates Emoji State to KMP/Commons
- Migrates lud06 to lud16 mapping to KMP/Quartz
- Migrates the new LocalCache observables to KMP/Commons
- Migrates rich text parser from JVM to KMP/Commons
Code Quality
- Migrates to AGP 9.0
- Adds Amethyst Desktop to CI/CD and Release builds
- Removes the in-app memory counter methods
- Refactors the old NIP-05 code on Quartz
- Migrates contact list management to addressable notes
- Creates new observable flows for LocalCache.
- Moves metadata methods from User to UserCache objects
- Separate Addressable vs Replaceable event class bases
- Avoid dependency on AccountSettings for NwcSignerState
- Finishes the transition to EventHint objects for building events.
- Lots of code review fixes by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Large accessibility review by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Moves Top Nav Filter markers from Strings to full objects.
- Removes support for feed definitions
- AccountState refactoring
AI:
- Add SKILL.md for AI agent customization
- Add settings and hooks to setup Android Development for the agent
Updated translations:
- Czech, German, Swedish, and Portuguese by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Hungarian by @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp
- French by @npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz
- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm
- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6
- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw
- Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
- Chinese by hypnotichemionus4
- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
- Russian by Anton Zhao
<a id="v1.05.1"></a>
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
- Fixed mixed DMs between logged in users.
- Fixed draft screen click to edit post.
<a id="v1.05.0"></a>
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
#Amethyst v1.05.0: Bookmark Lists, Voice Notes, and WoT Scores
@@ -6800,4 +7303,4 @@ First public version with:
[v0.4]: https://github.com/vitorpamplona/amethyst/compare/v0.3...v0.4
[v0.3]: https://github.com/vitorpamplona/amethyst/compare/v0.2...v0.3
[v0.2]: https://github.com/vitorpamplona/amethyst/compare/v0.1...v0.2
[v0.1]: https://github.com/vitorpamplona/amethyst/tree/v0.1
[v0.1]: https://github.com/vitorpamplona/amethyst/tree/v0.1

185
README.md
View File

@@ -39,85 +39,104 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
- [x] Events / Relay Subscriptions (NIP-01)
- [x] Basic protocol flow (NIP-01)
- [x] Follow List (NIP-02)
- [x] OpenTimestamps Attestations (NIP-03)
- [x] Private Messages (NIP-04)
- [x] DNS Address (NIP-05)
- [x] Mnemonic seed phrase (NIP-06)
- [ ] WebBrowser Signer (NIP-07, Not applicable)
- [x] Old-style mentions (NIP-08)
- [x] Event Deletion (NIP-09)
- [x] Replies, mentions, Threads, and Notifications (NIP-10)
- [x] Encrypted Direct Message (NIP-04)
- [x] DNS-based Identifiers (NIP-05)
- [x] Key Derivation from Mnemonic (NIP-06)
- [ ] window.nostr for Web Browsers (NIP-07, Not applicable)
- [x] Handling Mentions (NIP-08)
- [x] Event Deletion Request (NIP-09)
- [x] Text Notes and Threads (NIP-10)
- [x] Relay Information Document (NIP-11)
- [x] Generic Tag Queries (NIP-12)
- [x] Proof of Work Display (NIP-13)
- [ ] Proof of Work Calculations (NIP-13)
- [x] Events with a Subject (NIP-14)
- [ ] Marketplace (NIP-15)
- [x] Event Treatment (NIP-16)
- [x] Proof of Work (NIP-13)
- [x] Subject Tag in Text Events (NIP-14)
- [x] Nostr Marketplace (NIP-15)
- [x] Private Direct Messages (NIP-17)
- [x] Image/Video/Url/LnInvoice Previews
- [x] Reposts, Quotes, Generic Reposts (NIP-18)
- [x] Bech Encoding support (NIP-19)
- [x] Command Results (NIP-20)
- [x] URI Support (NIP-21)
- [x] Long-form Content (NIP-23) (view only)
- [x] User Profile Fields / Relay list (NIP-24)
- [x] Reposts (NIP-18)
- [x] bech32-encoded Entities (NIP-19)
- [x] nostr: URI Scheme (NIP-21)
- [x] Comment (NIP-22)
- [x] Long-form Content (NIP-23)
- [x] Extra Metadata Fields and Tags (NIP-24)
- [x] Reactions (NIP-25)
- [ ] Delegated Event Signing (NIP-26, Will not implement)
- [x] Delegated Event Signing (NIP-26)
- [x] Text Note References (NIP-27)
- [x] Public Chats (NIP-28)
- [ ] Relay-based Groups (NIP-29)
- [x] Public Chat (NIP-28)
- [x] Relay-based Groups (NIP-29)
- [x] Custom Emoji (NIP-30)
- [x] Event kind summaries (NIP-31)
- [ ] Labeling (NIP-32)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Git Stuff (NIP-34)
- [ ] Torrents (NIP-35)
- [x] Dealing with Unknown Events (NIP-31)
- [x] Labeling (NIP-32)
- [x] git stuff (NIP-34)
- [x] Torrents (NIP-35)
- [x] Sensitive Content (NIP-36)
- [x] Edits (NIP-37/Draft)
- [x] User Status Event (NIP-38)
- [x] External Identities (NIP-39)
- [x] Expiration Support (NIP-40)
- [x] Relay Authentication (NIP-42)
- [x] Versioned Encrypted Payloads (NIP-44)
- [ ] Event Counts (NIP-45, Will not implement)
- [ ] Nostr Connect (NIP-46)
- [x] Wallet Connect API (NIP-47)
- [ ] Proxy Tags (NIP-48, Not applicable)
- [x] Private key encryption for import/export (NIP-49)
- [x] Online Relay Search (NIP-50)
- [x] Draft Events (NIP-37)
- [x] User Statuses (NIP-38)
- [x] External Identities in Profiles (NIP-39)
- [x] Expiration Timestamp (NIP-40)
- [x] Authentication of Clients to Relays (NIP-42)
- [x] Relay Access Metadata and Requests (NIP-43)
- [x] Encrypted Payloads / Versioned (NIP-44)
- [x] Counting Results (NIP-45)
- [x] Nostr Remote Signing (NIP-46)
- [x] Nostr Wallet Connect (NIP-47)
- [x] Proxy Tags (NIP-48)
- [x] Private Key Encryption (NIP-49)
- [x] Search Capability (NIP-50)
- [x] Lists (NIP-51)
- [ ] Calendar Events (NIP-52)
- [x] Live Activities & Live Chats (NIP-53)
- [x] Calendar Events (NIP-52)
- [x] Live Activities (NIP-53)
- [x] Wiki (NIP-54)
- [x] Inline Metadata (NIP-55 - Draft)
- [x] Android Signer Application (NIP-55)
- [x] Reporting (NIP-56)
- [x] Lightning Tips
- [x] Zaps (NIP-57)
- [x] Private Zaps
- [x] Lightning Zaps (NIP-57)
- [x] Zap Splits (NIP-57)
- [x] Private Zaps (NIP-57)
- [x] Zapraiser (NIP-57)
- [x] Badges (NIP-58)
- [x] Gift Wraps & Seals (NIP-59)
- [x] Zapraiser (NIP-TBD)
- [ ] Relay List Metadata (NIP-65)
- [x] Polls (NIP-69)
- [x] Gift Wrap (NIP-59)
- [x] Pubkey Static Websites (NIP-5A)
- [x] Cashu Wallet (NIP-60)
- [x] Nutzaps (NIP-61)
- [x] Request to Vanish (NIP-62)
- [x] Chess / PGN (NIP-64)
- [x] Relay List Metadata (NIP-65)
- [x] Relay Discovery and Liveness Monitoring (NIP-66)
- [x] Picture-first Feeds (NIP-68)
- [x] Peer-to-peer Order Events (NIP-69)
- [x] Protected Events (NIP-70)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [ ] Zap Goals (NIP-75)
- [x] Arbitrary Custom App Data (NIP-78)
- [x] External Content IDs (NIP-73)
- [x] Zap Goals (NIP-75)
- [x] Negentropy Syncing (NIP-77)
- [x] Application-specific Data (NIP-78)
- [x] Threads (NIP-7D)
- [x] Highlights (NIP-84)
- [x] Notify Request (NIP-88/Draft)
- [x] Trusted Assertions (NIP-85)
- [x] Relay Management API (NIP-86)
- [x] Ecash Mint Discoverability (NIP-87)
- [x] Polls (NIP-88)
- [x] Recommended Application Handlers (NIP-89)
- [x] Data Vending Machine (NIP-90)
- [x] Inline Metadata (NIP-92)
- [x] Verifiable file URLs (NIP-94)
- [x] Data Vending Machines (NIP-90)
- [x] Media Attachments (NIP-92)
- [x] File Metadata (NIP-94)
- [x] Binary Blobs (NIP-95/Draft)
- [x] HTTP File Storage Integration (NIP-96)
- [x] HTTP Auth (NIP-98)
- [x] Classifieds (NIP-99)
- [x] Classified Listings (NIP-99)
- [x] Voice Messages (NIP-A0)
- [x] Public Messages (NIP-A4)
- [x] Web Bookmarks (NIP-B0)
- [x] Blossom (NIP-B7)
- [x] Nostr BLE Communications Protocol (NIP-BE)
- [x] Code Snippets (NIP-C0)
- [x] Chats (NIP-C7)
- [ ] MLS Protocol (NIP-EE)
- [x] Audio Tracks (zapstr.live) (kind:31337)
- [x] Lightning Tips
- [x] Image/Video/Url/LnInvoice/Cashu Previews
- [x] Push Notifications (Google and Unified Push)
- [x] In-Device Automatic Translations
- [x] Hashtag Following and Custom Hashtags
@@ -128,18 +147,15 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Markdown Support
- [x] Medical Data (NIP-xx/Draft)
- [x] Embed events (NIP-xx/Draft)
- [x] Draft Events (NIP-xx/Draft)
- [ ] Event Sets (NIP-xx/Draft)
- [ ] Topical Notes (NIP-xx/Draft)
- [x] Edit Short Notes (NIP-xx/Draft)
- [x] NIP Events (NIP-xx/Draft)
- [ ] Relationship Status (NIP-xx/Draft)
- [ ] Signed Filters (NIP-xx/Draft)
- [ ] Key Migration (NIP-xx/Draft)
- [ ] Time-based Sync (NIP-xx/Draft)
- [x] Image Capture in the app
- [x] Video Capture in the app
- [ ] Local Database
- [ ] Workspaces
- [ ] Infinity Scroll
## Privacy and Information Permanence
@@ -199,6 +215,13 @@ Build and run the Desktop app (requires Java 21+):
```bash
./gradlew :desktopApp:run
```
Full build (including tests)
```bash
./gradlew build
```
Requirements:
- Xcode and iOS simulator
- libsodium installed (e.g. via brew: `brew install libsodium`
## Testing
```bash
@@ -379,24 +402,24 @@ to `onPause` methods.
### Feature Parity Table
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
| :--- | :--- | :---: | :---: | :--- |
| **Cryptography** | Secp256k1 (Schnorr, Keys) | Full | No | Core Nostr signing/verification is missing on iOS. |
| | LibSodium (ChaCha20, Poly1305) | Full | No | AEAD and stream ciphers are unimplemented. |
| | AES Encryption (CBC & GCM) | Full | No | `AESCBC` and `AESGCM` are stubs on iOS. |
| | Hashing (SHA-256, etc.) | Full | No | `DigestInstance` is unimplemented. |
| | MAC (HmacSHA256, etc.) | Full | No | `MacInstance` is unimplemented. |
| **Data & Serialization** | JSON Mapping (Optimized) | Full | No | `OptimizedJsonMapper` is a stub; cannot parse/serialize Events. |
| | GZip Compression | Full | No | `GZip` implementation is missing. |
| | BitSet | Full | No | `BitSet` utility is unimplemented. |
| | LargeCache | Full | No | `LargeCache` methods (get, keys, size, etc.) are stubs. |
| **NIP Support** | NIP-96 (File Storage Info) | Full | No | `ServerInfoParser` is unimplemented. |
| | NIP-46 (Remote Signer) | Full | Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
| | NIP-03 (OTS / Timestamps) | Full | No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
| **Utilities** | URL Encoding / Decoding | Full | No | `UrlEncoder` and `URLs.ios.kt` are unimplemented. |
| | Unicode Normalization | Full | No | `UnicodeNormalizer` is a stub. |
| | Platform Logging | Full | Full | iOS uses `NSLog`, Android uses standard Log. |
| | Current Time | Full | Full | Implemented using `NSDate` on iOS. |
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
|:-------------------------|:-------------------------------|:---------------------:|:-----------:|:-----------------------------------------------------------------------|
| **Cryptography** | Secp256k1 (Schnorr, Keys) | Full | Full | |
| | LibSodium (ChaCha20, Poly1305) | Full | Full | |
| | AES Encryption (CBC & GCM) | Full | Full | |
| | Hashing (SHA-256, etc.) | Full | Full | |
| | MAC (HmacSHA256, etc.) | Full | Full | |
| **Data & Serialization** | JSON Mapping (Optimized) | Full | Full | A fully custom implementation exists in `commonMain`. |
| | GZip Compression | Full | Full | |
| | BitSet | Full | Full | |
| | LargeCache | Full | Full | |
| **NIP Support** | NIP-96 (File Storage Info) | Full | Full | |
| | NIP-46 (Remote Signer) | Full | Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
| | NIP-03 (OTS / Timestamps) | Full | No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
| **Utilities** | URL Encoding / Decoding | Full | Full | |
| | Unicode Normalization | Full | Full | |
| | Platform Logging | Full | Full | iOS uses `NSLog`, Android uses standard Log. |
| | Current Time | Full | Full | Implemented using `NSDate` on iOS. |
## Contributing

230
SKILL.md Normal file
View File

@@ -0,0 +1,230 @@
# Amethyst Builder Skill
Build customized Amethyst Nostr clients for Android. Fork, rebrand, customize, and distribute your own version.
## Overview
[Amethyst](https://github.com/vitorpamplona/amethyst) is the premier Nostr client for Android. This skill enables you to:
- Create rebranded versions (custom name, package, icons)
- Build F-Droid-compatible releases (no Google Play dependencies)
- Add or modify features
- Sign and distribute APKs
## Prerequisites
### Required Tools
1. **Java 21** (via SDKMAN)
```bash
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.5-tem
sdk use java 21.0.5-tem
```
2. **Android SDK**
- Command-line tools from https://developer.android.com/studio#command-line-tools-only
- Required components: build-tools, platform-tools, platforms;android-35
3. **Git** for cloning the repository
### Environment Setup
```bash
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
## Build Workflow
### 1. Clone the Repository
```bash
mkdir -p ~/projects/your-app-name
cd ~/projects/your-app-name
git clone https://github.com/vitorpamplona/amethyst.git .
```
### 2. Create Signing Key
```bash
keytool -genkeypair -v \
-keystore ./release-key.jks \
-alias your-app \
-keyalg RSA -keysize 2048 \
-validity 10000
```
Create `keystore.properties` in project root:
```properties
storeFile=release-key.jks
storePassword=your-password
keyAlias=your-app
keyPassword=your-password
```
### 3. Configure Signing
Add to `amethyst/build.gradle` inside the `android {}` block:
```gradle
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
storeFile rootProject.file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}
```
Update the release buildType to use the signing config:
```gradle
buildTypes {
release {
signingConfig signingConfigs.release
// ... existing config
}
}
```
### 4. Disable Google Services (Required for F-Droid)
**⚠️ CRITICAL:** The Google Services plugin fails when you change the package name. For F-Droid builds, disable it.
Edit `amethyst/build.gradle`, comment out the plugin:
```gradle
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
// alias(libs.plugins.googleServices) // DISABLED for F-Droid
alias(libs.plugins.jetbrainsComposeCompiler)
}
```
### 5. Build
```bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk use java 21.0.5-tem
./gradlew assembleFdroidRelease
```
**Build time:** ~9 minutes first build, faster on subsequent builds.
### 6. Locate APKs
Output directory: `amethyst/build/outputs/apk/fdroid/release/`
Files generated:
- `amethyst-fdroid-arm64-v8a-release.apk` - ARM64 (most phones)
- `amethyst-fdroid-universal-release.apk` - All architectures (larger)
## Customizations
### Change App Name
Edit `amethyst/src/main/res/values/strings.xml`:
```xml
<string name="app_name" translatable="false">YourAppName</string>
<string name="app_name_debug" translatable="false">YourAppName Debug</string>
```
### Change Package ID
Edit `amethyst/build.gradle`:
```gradle
android {
defaultConfig {
applicationId = "com.yourcompany.yourapp"
}
}
```
### Change Project Name
Edit `settings.gradle`:
```gradle
rootProject.name = "YourAppName"
```
### Change App Icon
Replace icon files in:
- `amethyst/src/main/res/mipmap-*/ic_launcher.webp`
- `amethyst/src/main/res/mipmap-*/ic_launcher_round.webp`
### Add Client Tag to Posts
Make your app identify itself on posts with `["client", "YourAppName"]`.
**1. Create tag builder extension:**
Create `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/clientTag/TagArrayBuilderExt.kt`:
```kotlin
package com.vitorpamplona.quartz.nip01Core.tags.clientTag
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun <T : Event> TagArrayBuilder<T>.client(clientName: String) =
addUnique(arrayOf(ClientTag.TAG_NAME, clientName))
```
**2. Add to TextNoteEvent:**
Edit `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt`:
Add import:
```kotlin
import com.vitorpamplona.quartz.nip01Core.tags.clientTag.client
```
In both `build()` functions, add after `alt(...)`:
```kotlin
client("YourAppName")
```
### Modify Default Relays
Edit relay configuration in `quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/` or the UI settings files.
## Troubleshooting
### google-services.json error
Disable the Google Services plugin (see step 4).
### Java version error
```bash
sdk use java 21.0.5-tem
java -version # Must show 21.x
```
### Out of memory
Edit `gradle.properties`:
```properties
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m
```
### Clean build
```bash
./gradlew --stop
./gradlew clean
./gradlew assembleFdroidRelease
```
## Distribution
Deploy APKs via:
- **Surge.sh:** `surge ./releases your-app.surge.sh`
- **Zapstore:** Submit to zapstore.dev
- **Direct download:** Host on any web server

View File

@@ -2,11 +2,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.googleServices)
alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.serialization)
alias(libs.plugins.stability.analyzer)
}
def getCurrentBranch() {
@@ -37,6 +35,17 @@ def generateVersionName(String baseVersion) {
}
}
// Workaround: stability.analyzer plugin doesn't declare task dependencies properly for Gradle 9.x
afterEvaluate {
def stabilityNames = tasks.names.findAll { it.contains("StabilityCheck") }
def compileNames = tasks.names.findAll { it.matches("compile.*UnitTestKotlin") }
stabilityNames.each { scName ->
compileNames.each { ctName ->
tasks.named(scName).configure { mustRunAfter(tasks.named(ctName)) }
}
}
}
android {
namespace = 'com.vitorpamplona.amethyst'
compileSdk = libs.versions.android.compileSdk.get().toInteger()
@@ -45,9 +54,9 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 431
versionName = generateVersionName("1.05.0")
buildConfigField "String", "RELEASE_NOTES_ID", "\"b457a20195ffcf501389fcb708f0ef73f4ee263e3bba63f1b893a896129e4c79\""
versionCode = 442
versionName = generateVersionName("1.08.0")
buildConfigField "String", "RELEASE_NOTES_ID", "\"be99e8c8d4df0f54b44eb6c96976ccb38baeea0192436a1c6fc8bc5e930da6b0\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -192,6 +201,7 @@ android {
buildFeatures {
compose = true
buildConfig = true
resValues = true
}
packagingOptions {
@@ -280,7 +290,7 @@ dependencies {
// view videos
implementation libs.androidx.media3.exoplayer
implementation libs.androidx.media3.exoplayer.hls
implementation libs.androidx.media3.ui
implementation libs.androidx.media3.ui.compose.material3
implementation libs.androidx.media3.session
// important for proxy / tor
@@ -294,6 +304,10 @@ dependencies {
implementation libs.coil.svg
// enables network for coil
implementation libs.coil.okhttp
// loads thumbnails for media3
// TODO: Replace this to the FrameExtractor in media 3
// when FrameExtractor accepts custom data sources.
implementation(libs.coil.video)
// Permission to upload pictures:
implementation libs.accompanist.permissions
@@ -332,9 +346,7 @@ dependencies {
fdroidImplementation libs.unifiedpush
// Charts
implementation libs.vico.charts.core
implementation libs.vico.charts.compose
implementation libs.vico.charts.views
implementation libs.vico.charts.m3
// GeoHash
@@ -348,15 +360,18 @@ dependencies {
// Image compression lib
implementation libs.zelory.image.compressor
// EXIF metadata stripping
implementation libs.androidx.exifinterface
// Voice anonymization DSP
implementation libs.tarsosdsp
// Cbor for cashuB format
implementation libs.kotlinx.serialization.cbor
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
implementation(libs.kotlinx.serialization.json)
implementation libs.tor.android
implementation libs.jtorctl
testImplementation libs.junit
testImplementation libs.mockk
testImplementation libs.kotlinx.coroutines.test

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -80,7 +80,7 @@ class ImageUploadTesting {
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/${BuildConfig.VERSION_NAME}"))
.build()
private suspend fun getBitmap(): ByteArray {
private fun getBitmap(): ByteArray {
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
@@ -121,20 +121,20 @@ class ImageUploadTesting {
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
assertEquals("image/png", result.type)
assertEquals(paylod.size.toLong(), result.size)
assertEquals(initialHash, result.sha256)
assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
assertEquals(server.baseUrl, "image/png", result.type)
assertEquals(server.baseUrl, paylod.size.toLong(), result.size)
assertEquals(server.baseUrl, initialHash, result.sha256)
// assertEquals(server.baseUrl, "${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes
ImageDownloader().waitAndGetImage(result.url!!) { client }?.bytes
?: run {
fail("${server.name}: Should not be null")
return
}
val downloadedHash = sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
assertEquals(server.baseUrl, initialHash, downloadedHash)
}
private suspend fun testNip96(server: ServerName) {
@@ -142,8 +142,7 @@ class ImageUploadTesting {
ServerInfoRetriever()
.loadInfo(
server.baseUrl,
{ client },
)
) { client }
val payload = getBitmap()
val inputStream = payload.inputStream()
@@ -172,7 +171,7 @@ class ImageUploadTesting {
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(url, { client })?.bytes
ImageDownloader().waitAndGetImage(url) { client }?.bytes
?: run {
fail("${server.name}: Should not be null")
return
@@ -207,8 +206,13 @@ class ImageUploadTesting {
runBlocking {
DEFAULT_MEDIA_SERVERS.forEach {
// skip paid servers and primal server is buggy.
if (!it.name.contains("Paid") && !it.name.contains("Primal")) {
testBase(it)
try {
if (!it.name.contains("Paid") && !it.name.contains("Primal")) {
testBase(it)
}
} catch (e: Exception) {
e.printStackTrace()
fail("Could not upload to: ${it.baseUrl}: ${e.message}")
}
}
}
@@ -226,14 +230,6 @@ class ImageUploadTesting {
testBase(ServerName("nostrage", "https://nostrage.com", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testSove() =
runBlocking {
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
}
@Ignore("Not Working anymore")
@Test()
fun testNostrBuild() =
runBlocking {
@@ -241,10 +237,10 @@ class ImageUploadTesting {
}
@Test()
@Ignore("Not Working anymore")
@Ignore("Returns invalid hash")
fun testSovbit() =
runBlocking {
testBase(ServerName("sovbit", "https://files.sovbit.host", ServerType.NIP96))
testBase(ServerName("sovbit", "https://cdn.sovbit.host", ServerType.Blossom))
}
@Test()
@@ -260,13 +256,6 @@ class ImageUploadTesting {
testBase(ServerName("sprovoost.nl", "https://img.sprovoost.nl/", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =
runBlocking {
testBase(ServerName("nostr.onch.services", "https://nostr.onch.services", ServerType.NIP96))
}
@Ignore("Changes sha256")
fun testPrimalBlossom() =
runBlocking {

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.verify
@@ -70,9 +70,9 @@ class ThreadDualAxisChartAssemblerTest {
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
nwcFilterAssembler = NWCPaymentFilterAssembler(client),
otsResolverBuilder = EmptyOtsResolverBuilder,
geolocationFlow = { MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading) },
nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
cache = LocalCache,
client = client,
scope = scope,
@@ -174,7 +174,7 @@ class ThreadDualAxisChartAssemblerTest {
null,
)
val filter = ThreadFeedFilter(account, naddr.toTag())
val filter = ThreadFeedFilter(account, naddr.toTag(), LocalCache)
val calculatedFeed = filter.feed()
val expectedOrder =

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -28,6 +28,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import org.junit.Assert.assertEquals
@@ -77,8 +78,19 @@ class UrlUserTagTransformationTest {
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val original = "@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
@@ -185,8 +197,19 @@ class UrlUserTagTransformationTest {
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
@@ -224,8 +247,20 @@ class UrlUserTagTransformationTest {
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val transformedText =
buildAnnotatedStringWithUrlHighlighting(

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EventSyncTest {
companion object {
val vitor = "wss://vitor.nostr1.com".normalizeRelayUrl()
val fiatjaf = "wss://pyramid.fiatjaf.com".normalizeRelayUrl()
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val rootClient =
OkHttpClient
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/v1.05"))
.build()
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
}
@Test
fun testSync() =
runBlocking {
val sync =
EventSync(
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
relayDb = {
listOf(Constants.mom, Constants.nos)
},
outboxTargets = { setOf(vitor) },
inboxTargets = { setOf(vitor) },
dmTargets = { setOf(vitor) },
clientBuilder = {
NostrClient(socketBuilder, appScope)
},
scope = appScope,
)
sync.runSync()
}
@Test
fun testFiatjafSync() =
runBlocking {
val sync =
EventSync(
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
relayDb = { listOf(fiatjaf) },
outboxTargets = { setOf(vitor) },
inboxTargets = { setOf(vitor) },
dmTargets = { setOf(vitor) },
clientBuilder = {
val newClient = NostrClient(socketBuilder, appScope)
val logger = RelayLogger(newClient, debugSending = true, debugReceiving = false)
val signer = NostrSignerInternal(KeyPair())
// Authenticates with relays.
val auth =
RelayAuthenticator(
newClient,
appScope,
signWithAllLoggedInUsers = { authTemplate ->
listOf(signer.sign(authTemplate))
},
)
newClient
},
scope = appScope,
)
sync.runSync()
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -46,7 +46,7 @@ object PushDistributorHandler : PushDistributorActions {
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
Log.d("PushHandler") { "New endpoint saved : $endpointInternal" }
}
fun removeEndpoint() {

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -53,7 +53,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val messageStr = message.content.decodeToString()
Log.d(TAG, "New message $messageStr for Instance: $instance")
Log.d(TAG) { "New message $messageStr for Instance: $instance" }
scope.launch {
try {
parseMessage(messageStr)?.let {
@@ -61,7 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message could not be parsed: ${e.message}")
Log.d(TAG) { "Message could not be parsed: ${e.message}" }
}
}
}
@@ -87,7 +87,7 @@ class PushMessageReceiver : MessagingReceiver() {
) {
val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
Log.d(TAG) { "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint" }
pushHandler.setEndpoint(sanitizedEndpoint)
scope.launch(Dispatchers.IO) {
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
@@ -97,7 +97,7 @@ class PushMessageReceiver : MessagingReceiver() {
NotificationUtils.getOrCreateDMChannel(appContext)
}
} else {
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
Log.d(TAG) { "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint" }
}
}
@@ -106,7 +106,7 @@ class PushMessageReceiver : MessagingReceiver() {
reason: FailedReason,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
Log.d(TAG) { "Registration failed for Instance: $instance" }
pushHandler.forceRemoveDistributor(context)
}
@@ -115,7 +115,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val removedEndpoint = pushHandler.getSavedEndpoint()
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
Log.d(TAG) { "Endpoint: $removedEndpoint removed for Instance: $instance" }
Log.d(TAG, "App is unregistered. ")
pushHandler.forceRemoveDistributor(context)
pushHandler.removeEndpoint()

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -35,7 +35,7 @@ object PushNotificationUtils {
accounts: List<AccountInfo>,
okHttpClient: (String) -> OkHttpClient,
) = with(Dispatchers.IO) {
if (!pushHandler.savedDistributorExists()) return
if (!pushHandler.savedDistributorExists()) return@with
val currentDistributor = PushDistributorHandler.getSavedDistributor()
PushDistributorHandler.saveDistributor(currentDistributor)

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -26,10 +26,10 @@ import androidx.compose.runtime.getValue
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.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
@Composable
fun TranslatableRichTextViewer(

View File

@@ -76,7 +76,6 @@
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|screenLayout"
android:taskAffinity=".service.playback.pip.PipVideoActivity"
android:theme="@style/Theme.Amethyst">
<intent-filter android:label="Amethyst">
@@ -241,6 +240,11 @@
<action android:name="com.shared.NOSTR" />
</intent-filter>
</receiver>
<receiver
android:name=".service.notifications.NotificationReplyReceiver"
android:exported="false" />
</application>

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -23,10 +23,12 @@ package com.vitorpamplona.amethyst
import android.app.Application
import com.vitorpamplona.amethyst.service.logging.Logging
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.LogLevel
class Amethyst : Application() {
init {
Log.d("AmethystApp", "Creating App $this")
Log.minLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.ERROR
Log.d("AmethystApp") { "Creating App $this" }
}
companion object {
@@ -36,7 +38,7 @@ class Amethyst : Application() {
override fun onCreate() {
super.onCreate()
Log.d("AmethystApp", "onCreate $this")
Log.d("AmethystApp") { "onCreate $this" }
instance = AppModules(this)
if (isDebug) {
@@ -48,7 +50,7 @@ class Amethyst : Application() {
override fun onTerminate() {
super.onTerminate()
Log.d("AmethystApp", "onTerminate $this")
Log.d("AmethystApp") { "onTerminate $this" }
instance.terminate(this)
}
@@ -59,7 +61,7 @@ class Amethyst : Application() {
*/
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.d("AmethystApp", "onTrimMemory $level")
Log.d("AmethystApp") { "onTrimMemory $level" }
instance.trim()
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -20,16 +20,21 @@
*/
package com.vitorpamplona.amethyst
import android.content.ContentResolver
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
import com.vitorpamplona.amethyst.model.preferences.NamecoinSharedPreferences
import com.vitorpamplona.amethyst.model.preferences.OtsSharedPreferences
import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
@@ -44,6 +49,7 @@ import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
@@ -53,10 +59,17 @@ import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.resourceCacheInit
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
@@ -65,13 +78,27 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.TOR_ELECTRUMX_SERVERS
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
class AppModules(
@@ -86,23 +113,55 @@ class AppModules(
}
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
val applicationDefaultScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
// Pre-load both preference DataStores in parallel on IO threads.
// Both constructors use runBlocking internally, so starting them concurrently
// reduces total blocking time from (torPrefs + uiPrefs) to ~max(torPrefs, uiPrefs).
private val uiPrefsDeferred =
applicationIOScope.async {
val prefs = UiSharedPreferences.uiPreferences(appContext) ?: UiSettings()
UiSharedPreferences(prefs, appContext, applicationIOScope)
}
private val torPrefsDeferred =
applicationIOScope.async {
val prefs = TorSharedPreferences.torPreferences(appContext) ?: TorSettings()
TorSharedPreferences(prefs, appContext, applicationIOScope)
}
// Blocking load of UI Preferences to avoid theme/language blinking
val uiPrefs by lazy {
UiSharedPreferences(appContext, applicationIOScope)
Log.d("AppModules", "UiSharedPreferences Init")
runBlocking { uiPrefsDeferred.await() }
}
// Blocking load of Tor Settings to avoid connection leaks
val torPrefs by lazy {
TorSharedPreferences(appContext, applicationIOScope)
Log.d("AppModules", "TorSharedPreferences Init")
runBlocking { torPrefsDeferred.await() }
}
// Namecoin ElectrumX server preferences (global, like Tor settings)
val namecoinPrefs by lazy {
Log.d("AppModules", "NamecoinSharedPreferences Init")
NamecoinSharedPreferences(appContext, applicationIOScope)
}
// OTS blockchain explorer preferences (global, like Tor settings)
val otsPrefs by lazy {
Log.d("AppModules", "OtsSharedPreferences Init")
OtsSharedPreferences(appContext, applicationIOScope)
}
// App services that should be run as soon as there are subscribers to their flows
val locationManager = LocationState(appContext, applicationIOScope)
val locationManager by lazy {
Log.d("AppModules", "LocationManager Init")
LocationState(appContext, applicationIOScope)
}
val connManager = ConnectivityManager(appContext, applicationIOScope)
val uiState by lazy {
Log.d("AppModules", "UiSettingsState Init")
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
}
@@ -124,40 +183,88 @@ class AppModules(
scope = applicationIOScope,
)
// manages all relay connections
val okHttpClientForRelays =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
// Offers easy methods to know when connections are happening through Tor or not
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
// Application-wide block height request cache
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
val electrumXClient by lazy {
Log.d("AppModules", "ElectrumXClient Init")
val client =
ElectrumXClient(
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
)
applicationIOScope.launch {
try {
val pinnedCerts = namecoinPrefs.loadPinnedCerts()
if (pinnedCerts.isNotEmpty()) {
client.setDynamicCerts(pinnedCerts)
}
} catch (_: Exception) {
// Non-fatal — defaults will still work
}
}
client
}
val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
TorAwareOkHttpOtsResolverBuilder(
roleBasedHttpClientBuilder::okHttpClientForMoney,
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
otsBlockHeightCache,
)
val namecoinResolver by
lazy {
Log.d("AppModules", "Namecoin Resolver Init")
NamecoinNameResolver(
electrumxClient = electrumXClient,
serverListProvider = {
// User-configured custom servers take priority
namecoinPrefs.customServersOrNull
?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
TOR_ELECTRUMX_SERVERS
} else {
DEFAULT_ELECTRUMX_SERVERS
}
},
)
}
val nip05Client by
lazy {
Log.d("AppModules", "NIP05Client Init")
Nip05Client(
fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05),
namecoinResolverBuilder = { namecoinResolver },
)
}
val otsResolverBuilder by
lazy {
Log.d("AppModules", "OtsResolverBuilder Init")
TorAwareOkHttpOtsResolverBuilder(
roleBasedHttpClientBuilder::okHttpClientForMoney,
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
OtsBlockHeightCache(),
customExplorerUrl = { otsPrefs.current.normalizedUrl() },
)
}
// Application-wide ots verification cache
val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
val otsVerifCache by lazy {
Log.d("AppModules", "OtsCache Init")
VerificationStateCache(otsResolverBuilder)
}
val torEvaluatorFlow =
TorRelayState(
okHttpClients,
torPrefs.value,
applicationDefaultScope,
applicationIOScope,
)
// Connects the NostrClient class with okHttp
// manages all relay connections
val okHttpClientForRelays =
DualHttpClientManagerForRelays(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
scope = applicationIOScope,
)
// Connects the INostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder { url ->
val useTor = torEvaluatorFlow.flow.value.useTor(url)
@@ -168,7 +275,7 @@ class AppModules(
val cache: LocalCache = LocalCache
// Provides a relay pool
val client: INostrClient = NostrClient(websocketBuilder, applicationDefaultScope)
val client: INostrClient = NostrClient(websocketBuilder, applicationIOScope)
// Watches for changes on Tor and Relay List Settings
val relayProxyClientConnector =
@@ -178,7 +285,7 @@ class AppModules(
connManager,
torManager,
client,
applicationDefaultScope,
applicationIOScope,
)
// Verifies and inserts in the cache from all relays, all subscriptions
@@ -188,10 +295,15 @@ class AppModules(
val notifyCoordinator = NotifyCoordinator(client)
// Authenticates with relays.
val authCoordinator = AuthCoordinator(client, applicationDefaultScope)
val authCoordinator = AuthCoordinator(client, applicationIOScope)
// Tries to verify new OTS events when they arrive.
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
val otsEventVerifier =
IncomingOtsEventVerifier(
otsVerifCache = { otsVerifCache },
cache = cache,
scope = applicationIOScope,
)
// Tracks if it is possible to connect to relays.
val failureTracker = RelayOfflineTracker(client)
@@ -200,7 +312,7 @@ class AppModules(
val relayStats = RelayStats(client)
// Logs debug messages when needed
val detailedLogger = if (isDebug) RelayLogger(client, false, false) else null
val detailedLogger = if (isDebug) RelayLogger(client, debugSending = false, debugReceiving = false) else null
val relayReqStats = if (isDebug) RelayReqStats(client) else null
val logger = if (isDebug) RelaySpeedLogger(client) else null
@@ -211,53 +323,121 @@ class AppModules(
client,
authCoordinator.receiver,
failureTracker,
applicationDefaultScope,
applicationIOScope,
)
// keeps all accounts live
val accountsCache =
AccountCacheState(
geolocationFlow = locationManager.geohashStateFlow,
nwcFilterAssembler = sources.nwc,
contentResolverFn = ::contentResolverFn,
otsResolverBuilder = otsResolverBuilder,
geolocationFlow = { locationManager.geohashStateFlow },
nwcFilterAssembler = { sources.nwc },
contentResolverFn = { appContext.contentResolver },
otsResolverBuilder = { otsResolverBuilder.build() },
cache = cache,
client = client,
)
val sessionManager =
AccountSessionManager(
accountsCache = accountsCache,
nip05ClientBuilder = { nip05Client },
clientBuilder = { client },
localPreferences = LocalPreferences,
scope = applicationIOScope,
)
fun subscribedFlow(
address: Address,
account: Account,
): Flow<NoteState> {
val note = cache.getOrCreateAddressableNote(address)
val userSub = UserFinderQueryState(note.author ?: cache.getOrCreateUser(address.pubKeyHex), account)
val noteSub = EventFinderQueryState(note, account)
return note
.flow()
.metadata.stateFlow
.onStart {
sources.userFinder.subscribe(userSub)
sources.eventFinder.subscribe(noteSub)
}.onCompletion {
sources.eventFinder.unsubscribe(noteSub)
sources.userFinder.unsubscribe(userSub)
}
}
val blossomResolver by lazy {
Log.d("AppModules", "BlossomServerResolver Init")
BlossomServerResolver(
loggedInUsers = { listOfNotNull(sessionManager.loggedInAccount()?.pubKey) },
blossomServers = { addressesToSubscribe ->
val account = sessionManager.loggedInAccount() ?: return@BlossomServerResolver listOf()
addressesToSubscribe.map { address ->
subscribedFlow(address, account).transform {
val event = it.note.event as? BlossomServersEvent
if (event != null) {
emit(event)
}
}
}
},
httpClientBuilder = roleBasedHttpClientBuilder,
)
}
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
val trimmingService by
lazy {
MemoryTrimmingService(cache)
}
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
// and reconnects relays if the configuration has been changed.
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationDefaultScope)
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationIOScope)
// saves the .content of NIP-95 blobs in disk to save memory
val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
val nip95cache: File by lazy {
Log.d("AppModules", "NIP95 Cache Init")
Nip95CacheFactory.new(appContext)
}
// local video cache with disk + memory
val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
val videoCache: VideoCache by lazy {
Log.d("AppModules", "VideoCache Init")
VideoCacheFactory.new(appContext)
}
// image cache in disk for coil
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
val diskCache: DiskCache by lazy {
Log.d("AppModules", "ImageCacheFactory Init")
ImageCacheFactory.newDisk(appContext)
}
// image cache in memory for coil
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
val memoryCache: MemoryCache by lazy {
Log.d("AppModules", "MemoryCache Init")
ImageCacheFactory.newMemory(appContext)
}
// crash report storage
val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
val crashReportCache = CrashReportCache(appContext)
// cache for NIP-11 documents
val nip11Cache: Nip11CachedRetriever by lazy {
Log.d("AppModules", "Nip11CachedRetriever Init")
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
}
fun contentResolverFn(): ContentResolver = appContext.contentResolver
fun setImageLoader() {
ImageLoaderSetup.setup(appContext, { diskCache }, { memoryCache }) { url ->
okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
}
Log.d("AppModules", "ImageLoaderSetup Init")
ImageLoaderSetup.setup(
app = appContext,
diskCache = { diskCache },
memoryCache = { memoryCache },
blossomServerResolver = { blossomResolver },
callFactory = { okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(it)) },
)
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
@@ -268,6 +448,7 @@ class AppModules(
applicationIOScope.launch {
// loads main account quickly.
LocalPreferences.loadAccountConfigFromEncryptedStorage()
sessionManager.loginWithDefaultAccountIfLoggedOff()
}
// forces initialization of uiPrefs in the main thread to avoid blinking themes
@@ -275,29 +456,41 @@ class AppModules(
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// preloads tor preferences
torPrefs
// Sets Coil - Tor - OkHttp link
setImageLoader()
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Sets Coil - Tor - OkHttp link
setImageLoader()
uiState
}
// LRUCache should not be instanciated in the Main thread due to blocking
applicationIOScope.launch {
CachedRobohash
resourceCacheInit()
}
// registers to receive events
pokeyReceiver.register(appContext)
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Prepares video cache later
delay(10_000)
videoCache
}
}
fun terminate(appContext: Context) {
pokeyReceiver.unregister(appContext)
applicationIOScope.cancel("Application onTerminate $appContext")
applicationDefaultScope.cancel("Application onTerminate $appContext")
accountsCache.clear()
}
fun trim() {
applicationDefaultScope.launch {
applicationIOScope.launch {
val loggedIn = accountsCache.accounts.value.values
trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts())
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -26,8 +26,11 @@ import android.content.pm.ApplicationInfo
import android.os.Debug
import androidx.core.content.getSystemService
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
@@ -43,19 +46,19 @@ fun debugState(context: Context) {
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
Log.d(STATE_DUMP_TAG, "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
Log.d(STATE_DUMP_TAG) { "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB" }
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
Log.d(STATE_DUMP_TAG, "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
Log.d(STATE_DUMP_TAG) { "Total Native Heap Allocated: $nativeHeap/$maxNative MB" }
val activityManager: ActivityManager? = context.getSystemService()
if (activityManager != null) {
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
Log.d(STATE_DUMP_TAG) { "Memory Class $memClass MB (largeHeap $isLargeHeap)" }
}
Log.d(
@@ -65,14 +68,8 @@ fun debugState(context: Context) {
.size() + "/" + normalizedUrls.size(),
)
Log.d(
STATE_DUMP_TAG,
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
STATE_DUMP_TAG,
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(STATE_DUMP_TAG) { "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB" }
Log.d(STATE_DUMP_TAG) { "Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB" }
Log.d(
STATE_DUMP_TAG,
@@ -95,9 +92,7 @@ fun debugState(context: Context) {
Log.d(
STATE_DUMP_TAG,
"Users: " +
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.users.filter { _, it -> it.latestMetadata != null }.size +
LocalCache.users.filter { _, it -> it.metadataOrNull() != null }.size +
" / " +
LocalCache.users.size(),
)
@@ -129,13 +124,12 @@ fun debugState(context: Context) {
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
)
LocalCache.chatroomList.forEach { key, room ->
Log.d(
STATE_DUMP_TAG,
Log.d(STATE_DUMP_TAG) {
"Private Chats $key: " +
room.rooms.size() +
" / " +
room.rooms.sumOf { key, value -> value.messages.size },
)
room.rooms.sumOf { key, value -> value.messages.size }
}
}
Log.d(
STATE_DUMP_TAG,
@@ -144,10 +138,8 @@ fun debugState(context: Context) {
)
Log.d(
STATE_DUMP_TAG,
"Observable Events: " +
LocalCache.observablesByKindAndETag.size +
" / " +
LocalCache.observablesByKindAndAuthor.size,
"Observables: " +
LocalCache.observables.size,
)
Log.d(
@@ -174,10 +166,10 @@ fun debugState(context: Context) {
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB " }
}
qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB " }
}
}
@@ -189,7 +181,7 @@ inline fun <T> logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage" }
}
result
} else {
@@ -204,7 +196,7 @@ inline fun <T> logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}" }
}
result
} else {
@@ -228,3 +220,12 @@ inline fun debug(
Log.d(tag, debugMessage())
}
}
fun Event.countMemory(): Int =
7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
12 + // createdAt + kind
id.bytesUsedInMemory() +
pubKey.bytesUsedInMemory() +
tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } +
content.bytesUsedInMemory() +
sig.bytesUsedInMemory()

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -25,15 +25,14 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Immutable
import androidx.core.content.edit
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
@@ -54,13 +53,16 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -90,10 +92,15 @@ private object PrefKeys {
const val NOSTR_PUBKEY = "nostr_pubkey"
const val LOCAL_RELAY_SERVERS = "localRelayServers"
const val DEFAULT_FILE_SERVER = "defaultFileServer"
const val STRIP_LOCATION_ON_UPLOAD = "stripLocationOnUpload"
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val DEFAULT_POLLS_FOLLOW_LIST = "defaultPollsFollowList"
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_USER_METADATA = "latestUserMetadata"
const val LATEST_CONTACT_LIST = "latestContactList"
@@ -101,6 +108,7 @@ private object PrefKeys {
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
const val LATEST_INDEX_RELAY_LIST = "latestIndexRelayList"
const val LATEST_RELAY_FEEDS_LIST = "latestRelayFeedsList"
const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList"
const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList"
const val LATEST_MUTE_LIST = "latestMuteList"
@@ -126,6 +134,7 @@ private object PrefKeys {
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
const val SHARED_SETTINGS = "shared_settings"
const val LATEST_PAYMENT_TARGETS = "latestPaymentTargets"
}
object LocalPreferences {
@@ -249,7 +258,7 @@ object LocalPreferences {
val prefsDir = File(prefsDirPath)
prefsDir.list()?.forEach {
if (it.contains(npub) && !File(prefsDir, it).delete()) {
Log.w("LocalPreferences", "Failed to delete preference file: $it")
Log.w("LocalPreferences") { "Failed to delete preference file: $it" }
}
}
}
@@ -277,7 +286,7 @@ object LocalPreferences {
*/
@SuppressLint("ApplySharedPref")
suspend fun deleteAccount(accountInfo: AccountInfo) {
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
Log.d("LocalPreferences") { "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}" }
withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
removeAccount(accountInfo)
@@ -318,16 +327,18 @@ object LocalPreferences {
PrefKeys.DEFAULT_FILE_SERVER,
JsonMapper.toJson(settings.defaultFileServer),
)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
putString(
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
settings.defaultNotificationFollowList.value,
)
putString(
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
settings.defaultDiscoveryFollowList.value,
)
putBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, settings.stripLocationOnUpload)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, JsonMapper.toJson(settings.defaultHomeFollowList.value))
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultStoriesFollowList.value))
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNotificationFollowList.value))
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, JsonMapper.toJson(settings.defaultDiscoveryFollowList.value))
putString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPollsFollowList.value))
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
putOrRemove(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, settings.zapPaymentRequest.value?.denormalize())
@@ -338,6 +349,7 @@ object LocalPreferences {
putOrRemove(PrefKeys.LATEST_NIP65_RELAY_LIST, settings.backupNIP65RelayList)
putOrRemove(PrefKeys.LATEST_SEARCH_RELAY_LIST, settings.backupSearchRelayList)
putOrRemove(PrefKeys.LATEST_INDEX_RELAY_LIST, settings.backupIndexRelayList)
putOrRemove(PrefKeys.LATEST_RELAY_FEEDS_LIST, settings.backupRelayFeedsList)
putOrRemove(PrefKeys.LATEST_BLOCKED_RELAY_LIST, settings.backupBlockedRelayList)
putOrRemove(PrefKeys.LATEST_TRUSTED_RELAY_LIST, settings.backupTrustedRelayList)
@@ -357,6 +369,7 @@ object LocalPreferences {
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
putOrRemove(PrefKeys.LATEST_TRUST_PROVIDER_LIST, settings.backupTrustProviderList)
putOrRemove(PrefKeys.LATEST_PAYMENT_TARGETS, settings.backupNipA3PaymentTargets)
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
@@ -442,106 +455,155 @@ object LocalPreferences {
}
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
Log.d("LocalPreferences", "Load account from file $npub")
Log.d("LocalPreferences") { "Load account from file $npub" }
val result =
withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
Log.d("LocalPreferences") { "Load account from file $npub - opened file" }
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
val externalSignerPackageName =
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val externalSignerPackageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val defaultHomeFollowList =
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS
val defaultStoriesFollowList =
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultNotificationFollowList =
getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultDiscoveryFollowList =
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
val latestIndexRelayList = parseEventOrNull<IndexerRelayListEvent>(PrefKeys.LATEST_INDEX_RELAY_LIST)
val latestBlockedRelayList = parseEventOrNull<BlockedRelayListEvent>(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
val latestTrustedRelayList = parseEventOrNull<TrustedRelayListEvent>(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
val latestChannelList = parseEventOrNull<ChannelListEvent>(PrefKeys.LATEST_CHANNEL_LIST)
val latestCommunityList = parseEventOrNull<CommunityListEvent>(PrefKeys.LATEST_COMMUNITY_LIST)
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
val latestTrustProviderList = parseEventOrNull<TrustProviderListEvent>(PrefKeys.LATEST_TRUST_PROVIDER_LIST)
Log.d("LocalPreferences") { "Load account from file $npub - keys ready" }
val stripLocationOnUpload = getBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, true)
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null)
val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null)
val defaultNotificationFollowListStr = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null)
val defaultDiscoveryFollowListStr = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null)
val defaultPollsFollowListStr = getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null)
val defaultPicturesFollowListStr = getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null)
val defaultShortsFollowListStr = getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null)
val defaultLongsFollowListStr = getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null)
val zapPaymentRequestServerStr = getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)
val defaultFileServerStr = getString(PrefKeys.DEFAULT_FILE_SERVER, null)
val pendingAttestationsStr = getString(PrefKeys.PENDING_ATTESTATIONS, null)
val latestUserMetadataStr = getString(PrefKeys.LATEST_USER_METADATA, null)
val latestContactListStr = getString(PrefKeys.LATEST_CONTACT_LIST, null)
val latestDmRelayListStr = getString(PrefKeys.LATEST_DM_RELAY_LIST, null)
val latestNip65RelayListStr = getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)
val latestSearchRelayListStr = getString(PrefKeys.LATEST_SEARCH_RELAY_LIST, null)
val latestIndexRelayListStr = getString(PrefKeys.LATEST_INDEX_RELAY_LIST, null)
val latestRelayFeedsListStr = getString(PrefKeys.LATEST_RELAY_FEEDS_LIST, null)
val latestBlockedRelayListStr = getString(PrefKeys.LATEST_BLOCKED_RELAY_LIST, null)
val latestTrustedRelayListStr = getString(PrefKeys.LATEST_TRUSTED_RELAY_LIST, null)
val latestMuteListStr = getString(PrefKeys.LATEST_MUTE_LIST, null)
val latestPrivateHomeRelayListStr = getString(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, null)
val latestAppSpecificDataStr = getString(PrefKeys.LATEST_APP_SPECIFIC_DATA, null)
val latestChannelListStr = getString(PrefKeys.LATEST_CHANNEL_LIST, null)
val latestCommunityListStr = getString(PrefKeys.LATEST_COMMUNITY_LIST, null)
val latestHashtagListStr = getString(PrefKeys.LATEST_HASHTAG_LIST, null)
val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null)
val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null)
val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null)
val latestPaymentTargetsStr = getString(PrefKeys.LATEST_PAYMENT_TARGETS, null)
val lastReadPerRouteStr = getString(PrefKeys.LAST_READ_PER_ROUTE, null)
Log.d("LocalPreferences") { "Load account from file $npub - before parsing events" }
val defaultHomeFollowList = async { parseOrNull<TopFilter>(defaultHomeFollowListStr) ?: TopFilter.AllFollows }
val defaultStoriesFollowList = async { parseOrNull<TopFilter>(defaultStoriesFollowListStr) ?: TopFilter.Global }
val defaultNotificationFollowList = async { parseOrNull<TopFilter>(defaultNotificationFollowListStr) ?: TopFilter.Global }
val defaultDiscoveryFollowList = async { parseOrNull<TopFilter>(defaultDiscoveryFollowListStr) ?: TopFilter.Global }
val defaultPollsFollowList = async { parseOrNull<TopFilter>(defaultPollsFollowListStr) ?: TopFilter.Global }
val defaultPicturesFollowList = async { parseOrNull<TopFilter>(defaultPicturesFollowListStr) ?: TopFilter.Global }
val defaultShortsFollowList = async { parseOrNull<TopFilter>(defaultShortsFollowListStr) ?: TopFilter.Global }
val defaultLongsFollowList = async { parseOrNull<TopFilter>(defaultLongsFollowListStr) ?: TopFilter.Global }
val zapPaymentRequestServer = async { parseOrNull<Nip47WalletConnect.Nip47URI>(zapPaymentRequestServerStr) }
val defaultFileServer = async { parseOrNull<ServerName>(defaultFileServerStr) ?: DEFAULT_MEDIA_SERVERS[0] }
val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() }
val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) }
val latestContactList = async { parseEventOrNull<ContactListEvent>(latestContactListStr) }
val latestDmRelayList = async { parseEventOrNull<ChatMessageRelayListEvent>(latestDmRelayListStr) }
val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) }
val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) }
val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) }
val latestRelayFeedsList = async { parseEventOrNull<RelayFeedsListEvent>(latestRelayFeedsListStr) }
val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) }
val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) }
val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) }
val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) }
val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) }
val latestChannelList = async { parseEventOrNull<ChannelListEvent>(latestChannelListStr) }
val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) }
val latestHashtagList = async { parseEventOrNull<HashtagListEvent>(latestHashtagListStr) }
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
val latestTrustProviderList = async { parseEventOrNull<TrustProviderListEvent>(latestTrustProviderListStr) }
val latestPaymentTargets = async { parseEventOrNull<PaymentTargetsEvent>(latestPaymentTargetsStr) }
val lastReadPerRoute =
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
MutableStateFlow(it.value)
} ?: mapOf()
async {
parseOrNull<Map<String, Long>>(lastReadPerRouteStr)?.mapValues {
MutableStateFlow(it.value)
} ?: mapOf()
}
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
Log.d("LocalPreferences") { "Load account from file $npub - asyncs created" }
return@with AccountSettings(
keyPair = keyPair,
transientAccount = false,
externalSignerPackageName = externalSignerPackageName,
localRelayServers = MutableStateFlow(localRelayServers),
defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
defaultFileServer = defaultFileServer.await(),
stripLocationOnUpload = stripLocationOnUpload,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList.await()),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList.await()),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList.await()),
defaultPollsFollowList = MutableStateFlow(defaultPollsFollowList.await()),
defaultPicturesFollowList = MutableStateFlow(defaultPicturesFollowList.await()),
defaultShortsFollowList = MutableStateFlow(defaultShortsFollowList.await()),
defaultLongsFollowList = MutableStateFlow(defaultLongsFollowList.await()),
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer.await()?.normalize()),
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
backupUserMetadata = latestUserMetadata,
backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
backupSearchRelayList = latestSearchRelayList,
backupIndexRelayList = latestIndexRelayList,
backupBlockedRelayList = latestBlockedRelayList,
backupTrustedRelayList = latestTrustedRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupChannelList = latestChannelList,
backupCommunityList = latestCommunityList,
backupHashtagList = latestHashtagList,
backupGeohashList = latestGeohashList,
backupEphemeralChatList = latestEphemeralList,
backupTrustProviderList = latestTrustProviderList,
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
backupUserMetadata = latestUserMetadata.await(),
backupContactList = latestContactList.await(),
backupNIP65RelayList = latestNip65RelayList.await(),
backupDMRelayList = latestDmRelayList.await(),
backupSearchRelayList = latestSearchRelayList.await(),
backupIndexRelayList = latestIndexRelayList.await(),
backupRelayFeedsList = latestRelayFeedsList.await(),
backupBlockedRelayList = latestBlockedRelayList.await(),
backupTrustedRelayList = latestTrustedRelayList.await(),
backupPrivateHomeRelayList = latestPrivateHomeRelayList.await(),
backupMuteList = latestMuteList.await(),
backupAppSpecificData = latestAppSpecificData.await(),
backupChannelList = latestChannelList.await(),
backupCommunityList = latestCommunityList.await(),
backupHashtagList = latestHashtagList.await(),
backupGeohashList = latestGeohashList.await(),
backupEphemeralChatList = latestEphemeralList.await(),
backupTrustProviderList = latestTrustProviderList.await(),
lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations),
pendingAttestations = MutableStateFlow(pendingAttestations.await()),
backupNipA3PaymentTargets = latestPaymentTargets.await(),
)
}
}
Log.d("LocalPreferences", "Loaded account from file $npub")
Log.d("LocalPreferences") { "Loaded account from file $npub" }
return result
}
private inline fun <reified T : Any> SharedPreferences.parseOrNull(key: String): T? {
val value = getString(key, null)
private inline fun <reified T : Any> parseOrNull(value: String?): T? {
if (value.isNullOrEmpty() || value == "null") {
return null
}
@@ -553,13 +615,12 @@ object LocalPreferences {
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
null
}
}
private inline fun <reified T> SharedPreferences.parseEventOrNull(key: String): T? {
val value = getString(key, null)
private inline fun <reified T> parseEventOrNull(value: String?): T? {
if (value.isNullOrEmpty() || value == "null") {
return null
}
@@ -567,7 +628,7 @@ object LocalPreferences {
Event.fromJson(value) as T?
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
null
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -21,11 +21,14 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
@@ -43,6 +46,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
@@ -52,12 +56,13 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import java.util.Locale
import kotlinx.serialization.Serializable
val DefaultChannels =
listOf(
@@ -76,11 +81,13 @@ val DefaultNIP65List =
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
)
val DefaultGlobalRelays = listOf(Constants.wine, Constants.news)
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
val DefaultSearchRelayList = setOf(Constants.wine, Constants.where, Constants.nostoday, Constants.antiprimal, Constants.ditto)
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds)
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds, Constants.yabu, Constants.nostr1)
val DefaultSignerPermissions =
listOf(
@@ -93,20 +100,58 @@ val DefaultSignerPermissions =
Permission(CommandType.DECRYPT_ZAP_EVENT),
)
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val GLOBAL_FOLLOWS = " Global "
@Serializable
sealed class TopFilter(
val code: String,
) {
@Serializable
object Global : TopFilter(" Global ")
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_FOLLOWS = " All Follows "
@Serializable
object AllFollows : TopFilter(" All Follows ")
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_USER_FOLLOWS = " All User Follows "
@Serializable
object AllUserFollows : TopFilter(" All User Follows ")
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val KIND3_FOLLOWS = " Main User Follows "
@Serializable
object DefaultFollows : TopFilter(" Main User Follows ")
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val AROUND_ME = " Around Me "
@Serializable
object AroundMe : TopFilter(" Around Me ")
@Serializable
object Chess : TopFilter(" Chess ")
@Serializable
class PeopleList(
val address: Address,
) : TopFilter(address.toValue())
@Serializable
class MuteList(
val address: Address,
) : TopFilter(address.toValue())
@Serializable
class Community(
val address: Address,
) : TopFilter("Community/${address.toValue()}")
@Serializable
class Hashtag(
val tag: String,
) : TopFilter("Hashtag/$tag")
@Serializable
class Geohash(
val tag: String,
) : TopFilter("Geohash/$tag")
@Serializable
class Relay(
val url: String,
) : TopFilter("Relay/$url")
}
@Stable
class AccountSettings(
@@ -115,11 +160,16 @@ class AccountSettings(
var externalSignerPackageName: String? = null,
var localRelayServers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0],
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(ALL_FOLLOWS),
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
var zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
var stripLocationOnUpload: Boolean = true,
val defaultHomeFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows),
val defaultStoriesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultNotificationFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultDiscoveryFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultPollsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultLongsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
var hideNIP17WarningDialog: Boolean = false,
@@ -129,6 +179,7 @@ class AccountSettings(
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var backupSearchRelayList: SearchRelayListEvent? = null,
var backupIndexRelayList: IndexerRelayListEvent? = null,
var backupRelayFeedsList: RelayFeedsListEvent? = null,
var backupBlockedRelayList: BlockedRelayListEvent? = null,
var backupTrustedRelayList: TrustedRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null,
@@ -141,9 +192,11 @@ class AccountSettings(
var backupEphemeralChatList: EphemeralChatListEvent? = null,
var backupTrustProviderList: TrustProviderListEvent? = null,
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
) {
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow(mapOf()),
var backupNipA3PaymentTargets: PaymentTargetsEvent? = null,
) : EphemeralChatRepository,
PublicChatListRepository {
val saveable = MutableStateFlow(AccountSettingsUpdater(null))
val syncedSettings: AccountSyncedSettings = AccountSyncedSettings(AccountSyncedSettingsInternal())
@@ -188,6 +241,15 @@ class AccountSettings(
return false
}
fun changeReactionRowItems(newItems: List<ReactionRowItem>): Boolean {
if (syncedSettings.reactions.reactionRowItems.value != newItems) {
syncedSettings.reactions.reactionRowItems.tryEmit(newItems.toImmutableList())
saveAccountSettings()
return true
}
return false
}
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
if (zapPaymentRequest.value != newServer) {
zapPaymentRequest.tryEmit(newServer)
@@ -208,6 +270,13 @@ class AccountSettings(
}
}
fun changeStripLocationOnUpload(strip: Boolean) {
if (stripLocationOnUpload != strip) {
stripLocationOnUpload = strip
saveAccountSettings()
}
}
// ---
// list names
// ---
@@ -216,7 +285,7 @@ class AccountSettings(
changeDefaultHomeFollowList(name.code)
}
fun changeDefaultHomeFollowList(name: String) {
fun changeDefaultHomeFollowList(name: TopFilter) {
if (defaultHomeFollowList.value != name) {
defaultHomeFollowList.tryEmit(name)
saveAccountSettings()
@@ -227,7 +296,7 @@ class AccountSettings(
changeDefaultStoriesFollowList(name.code)
}
fun changeDefaultStoriesFollowList(name: String) {
fun changeDefaultStoriesFollowList(name: TopFilter) {
if (defaultStoriesFollowList.value != name) {
defaultStoriesFollowList.tryEmit(name)
saveAccountSettings()
@@ -238,7 +307,7 @@ class AccountSettings(
changeDefaultNotificationFollowList(name.code)
}
fun changeDefaultNotificationFollowList(name: String) {
fun changeDefaultNotificationFollowList(name: TopFilter) {
if (defaultNotificationFollowList.value != name) {
defaultNotificationFollowList.tryEmit(name)
saveAccountSettings()
@@ -249,13 +318,57 @@ class AccountSettings(
changeDefaultDiscoveryFollowList(name.code)
}
fun changeDefaultDiscoveryFollowList(name: String) {
fun changeDefaultDiscoveryFollowList(name: TopFilter) {
if (defaultDiscoveryFollowList.value != name) {
defaultDiscoveryFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultPollsFollowList(name: FeedDefinition) {
changeDefaultPollsFollowList(name.code)
}
fun changeDefaultPollsFollowList(name: TopFilter) {
if (defaultPollsFollowList.value != name) {
defaultPollsFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultPicturesFollowList(name: FeedDefinition) {
changeDefaultPicturesFollowList(name.code)
}
fun changeDefaultPicturesFollowList(name: TopFilter) {
if (defaultPicturesFollowList.value != name) {
defaultPicturesFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultShortsFollowList(name: FeedDefinition) {
changeDefaultShortsFollowList(name.code)
}
fun changeDefaultShortsFollowList(name: TopFilter) {
if (defaultShortsFollowList.value != name) {
defaultShortsFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultLongsFollowList(name: FeedDefinition) {
changeDefaultLongsFollowList(name.code)
}
fun changeDefaultLongsFollowList(name: TopFilter) {
if (defaultLongsFollowList.value != name) {
defaultLongsFollowList.tryEmit(name)
saveAccountSettings()
}
}
// ---
// language services
// ---
@@ -264,9 +377,21 @@ class AccountSettings(
saveAccountSettings()
}
fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language)
fun addDontTranslateFrom(languageCode: String) {
syncedSettings.languages.addDontTranslateFrom(languageCode)
saveAccountSettings()
}
fun updateTranslateTo(languageCode: Locale): Boolean {
fun removeDontTranslateFrom(languageCode: String) {
syncedSettings.languages.removeDontTranslateFrom(languageCode)
saveAccountSettings()
}
fun translateToContains(languageCode: String) =
syncedSettings.languages.translateTo.value
.contains(languageCode)
fun updateTranslateTo(languageCode: String): Boolean {
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
saveAccountSettings()
return true
@@ -339,6 +464,16 @@ class AccountSettings(
}
}
fun updateNIPA3PaymentTargets(newNIPA3PaymentTargets: PaymentTargetsEvent?) {
if (newNIPA3PaymentTargets == null || newNIPA3PaymentTargets.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupNipA3PaymentTargets?.id != newNIPA3PaymentTargets.id) {
backupNipA3PaymentTargets = newNIPA3PaymentTargets
saveAccountSettings()
}
}
fun updateSearchRelayList(newSearchRelayList: SearchRelayListEvent?) {
if (newSearchRelayList == null || newSearchRelayList.tags.isEmpty()) return
@@ -359,6 +494,16 @@ class AccountSettings(
}
}
fun updateRelayFeedList(newRelayFeedList: RelayFeedsListEvent?) {
if (newRelayFeedList == null || newRelayFeedList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupRelayFeedsList?.id != newRelayFeedList.id) {
backupRelayFeedsList = newRelayFeedList
saveAccountSettings()
}
}
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return
@@ -389,7 +534,9 @@ class AccountSettings(
}
}
fun updateChannelListTo(newChannelList: ChannelListEvent?) {
override fun channelList() = backupChannelList
override fun updateChannelListTo(newChannelList: ChannelListEvent?) {
if (newChannelList == null || newChannelList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
@@ -429,7 +576,9 @@ class AccountSettings(
}
}
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
override fun ephemeralChatList() = backupEphemeralChatList
override fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
@@ -532,7 +681,7 @@ class AccountSettings(
route: String,
timestampInSecs: Long,
): MutableStateFlow<Long> =
MutableStateFlow<Long>(timestampInSecs).also { newFlow ->
MutableStateFlow(timestampInSecs).also { newFlow ->
lastReadPerRoute.update { it + Pair(route, newFlow) }
saveAccountSettings()
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -27,13 +27,16 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.util.Locale
@Stable
class AccountSyncedSettings(
internalSettings: AccountSyncedSettingsInternal,
) {
val reactions = AccountReactionPreferences(MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList()))
val reactions =
AccountReactionPreferences(
MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList()),
MutableStateFlow(internalSettings.reactions.reactionRowItems.toImmutableList()),
)
val zaps =
AccountZapPreferences(
MutableStateFlow(internalSettings.zaps.zapAmountChoices.toImmutableList()),
@@ -41,9 +44,9 @@ class AccountSyncedSettings(
)
val languages =
AccountLanguagePreferences(
internalSettings.languages.dontTranslateFrom,
internalSettings.languages.languagePreferences,
internalSettings.languages.translateTo,
MutableStateFlow(internalSettings.languages.dontTranslateFrom),
MutableStateFlow(internalSettings.languages.languagePreferences),
MutableStateFlow(internalSettings.languages.translateTo),
)
val security =
AccountSecurityPreferences(
@@ -54,7 +57,7 @@ class AccountSyncedSettings(
fun toInternal(): AccountSyncedSettingsInternal =
AccountSyncedSettingsInternal(
reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value),
reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value, reactions.reactionRowItems.value),
zaps =
AccountZapPreferencesInternal(
zaps.zapAmountChoices.value,
@@ -62,9 +65,9 @@ class AccountSyncedSettings(
),
languages =
AccountLanguagePreferencesInternal(
languages.dontTranslateFrom,
languages.languagePreferences,
languages.translateTo,
languages.dontTranslateFrom.value,
languages.languagePreferences.value,
languages.translateTo.value,
),
security =
AccountSecurityPreferencesInternal(
@@ -80,6 +83,11 @@ class AccountSyncedSettings(
reactions.reactionChoices.tryEmit(newReactionChoices)
}
val newReactionRowItems = syncedSettingsInternal.reactions.reactionRowItems.toImmutableList()
if (!equalImmutableLists(reactions.reactionRowItems.value, newReactionRowItems)) {
reactions.reactionRowItems.tryEmit(newReactionRowItems)
}
val newZapChoices = syncedSettingsInternal.zaps.zapAmountChoices.toImmutableList()
if (!equalImmutableLists(zaps.zapAmountChoices.value, newZapChoices)) {
zaps.zapAmountChoices.tryEmit(newZapChoices)
@@ -89,16 +97,16 @@ class AccountSyncedSettings(
zaps.defaultZapType.tryEmit(syncedSettingsInternal.zaps.defaultZapType)
}
if (languages.dontTranslateFrom != syncedSettingsInternal.languages.dontTranslateFrom) {
languages.dontTranslateFrom = syncedSettingsInternal.languages.dontTranslateFrom
if (languages.dontTranslateFrom.value != syncedSettingsInternal.languages.dontTranslateFrom) {
languages.dontTranslateFrom.value = syncedSettingsInternal.languages.dontTranslateFrom
}
if (languages.languagePreferences != syncedSettingsInternal.languages.languagePreferences) {
languages.languagePreferences = syncedSettingsInternal.languages.languagePreferences
if (languages.languagePreferences.value != syncedSettingsInternal.languages.languagePreferences) {
languages.languagePreferences.value = syncedSettingsInternal.languages.languagePreferences
}
if (languages.translateTo != syncedSettingsInternal.languages.translateTo) {
languages.translateTo = syncedSettingsInternal.languages.translateTo
if (languages.translateTo.value != syncedSettingsInternal.languages.translateTo) {
languages.translateTo.value = syncedSettingsInternal.languages.translateTo
}
if (security.showSensitiveContent.value != syncedSettingsInternal.security.showSensitiveContent) {
@@ -114,12 +122,13 @@ class AccountSyncedSettings(
}
}
fun dontTranslateFromFilteredBySpokenLanguages(): Set<String> = languages.dontTranslateFrom - getLanguagesSpokenByUser()
fun dontTranslateFromFilteredBySpokenLanguages(): Set<String> = languages.dontTranslateFrom.value - getLanguagesSpokenByUser()
}
@Stable
class AccountReactionPreferences(
var reactionChoices: MutableStateFlow<ImmutableList<String>>,
var reactionRowItems: MutableStateFlow<ImmutableList<ReactionRowItem>>,
)
@Stable
@@ -130,27 +139,36 @@ class AccountZapPreferences(
@Stable
class AccountLanguagePreferences(
var dontTranslateFrom: Set<String>,
var languagePreferences: Map<String, String>,
var translateTo: String,
var dontTranslateFrom: MutableStateFlow<Set<String>>,
var languagePreferences: MutableStateFlow<Map<String, String>>,
var translateTo: MutableStateFlow<String>,
) {
// ---
// language services
// ---
fun toggleDontTranslateFrom(languageCode: String) {
dontTranslateFrom =
if (!dontTranslateFrom.contains(languageCode)) {
dontTranslateFrom.plus(languageCode)
dontTranslateFrom.update {
if (it.contains(languageCode)) {
it - languageCode
} else {
dontTranslateFrom.minus(languageCode)
it + languageCode
}
}
}
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
fun addDontTranslateFrom(languageCode: String) {
dontTranslateFrom.update { it + languageCode }
}
fun updateTranslateTo(languageCode: Locale): Boolean {
if (translateTo != languageCode.language) {
translateTo = languageCode.language
fun removeDontTranslateFrom(languageCode: String) {
dontTranslateFrom.update { it - languageCode }
}
fun translateToContains(languageCode: String) = translateTo.value.contains(languageCode)
fun updateTranslateTo(languageCode: String): Boolean {
if (translateTo.value != languageCode) {
translateTo.tryEmit(languageCode)
return true
}
return false
@@ -162,13 +180,15 @@ class AccountLanguagePreferences(
preference: String,
) {
val key = "$source,$target"
if (key !in languagePreferences) {
languagePreferences = languagePreferences + Pair(key, preference)
} else {
if (languagePreferences.get(key) == preference) {
languagePreferences = languagePreferences.minus(key)
languagePreferences.update {
if (key !in it) {
it + Pair(key, preference)
} else {
languagePreferences = languagePreferences + Pair(key, preference)
if (it.get(key) == preference) {
it.minus(key)
} else {
it + Pair(key, preference)
}
}
}
}
@@ -176,7 +196,7 @@ class AccountLanguagePreferences(
fun preferenceBetween(
source: String,
target: String,
): String? = languagePreferences["$source,$target"]
): String? = languagePreferences.value["$source,$target"]
}
@Stable

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -39,6 +39,31 @@ val DefaultReactions =
val DefaultZapAmounts = listOf(100L, 500L, 1000L)
@Serializable
enum class ReactionRowAction {
Reply,
Boost,
Like,
Zap,
Share,
}
@Serializable
data class ReactionRowItem(
val action: ReactionRowAction,
val enabled: Boolean = true,
val showCounter: Boolean = true,
)
val DefaultReactionRowItems =
listOf(
ReactionRowItem(ReactionRowAction.Reply),
ReactionRowItem(ReactionRowAction.Boost),
ReactionRowItem(ReactionRowAction.Like),
ReactionRowItem(ReactionRowAction.Zap),
ReactionRowItem(ReactionRowAction.Share, showCounter = false),
)
fun getLanguagesSpokenByUser(): Set<String> {
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
val codedList = mutableSetOf<String>()
@@ -59,6 +84,7 @@ class AccountSyncedSettingsInternal(
@Serializable
class AccountReactionPreferencesInternal(
var reactionChoices: List<String> = DefaultReactions,
var reactionRowItems: List<ReactionRowItem> = DefaultReactionRowItems,
)
@Serializable

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -87,7 +87,7 @@ class AntiSpamFilter {
val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay))
val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
// Log down offenders
val spammer = logOffender(hash, event)
@@ -106,16 +106,15 @@ class AntiSpamFilter {
recentAddressables.put(hash, address)
} else {
// normal event
val existingEvent = recentEventIds[hash]
if (
(recentEventIds[hash] != null && recentEventIds[hash] != event.id) ||
(existingEvent != null && existingEvent != event.id) ||
(spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id))
) {
val existingEvent = recentEventIds[hash]
val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay))
val link2 = njumpLink(NEvent.create(event.id, null, null, relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
// Log down offenders
val spammer = logOffender(hash, event)
@@ -171,7 +170,7 @@ class AntiSpamFilter {
}
}
val flowSpam = MutableStateFlow<AntiSpamState>(AntiSpamState(this))
val flowSpam = MutableStateFlow(AntiSpamState(this))
}
class AntiSpamState(

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -28,7 +28,6 @@ object Constants {
val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net")
val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val wine = RelayUrlNormalizer.normalize("wss://nostr.wine")
val band = RelayUrlNormalizer.normalize("wss://relay.nostr.band")
val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com")
val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org")
@@ -37,16 +36,20 @@ object Constants {
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today")
val antiprimal = RelayUrlNormalizer.normalize("wss://antiprimal.net")
val ditto = RelayUrlNormalizer.normalize("wss://relay.ditto.pub")
val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com")
val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com")
val news = RelayUrlNormalizer.normalize("wss://news.utxo.one")
val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es")
val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social")
val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es")
val yabu = RelayUrlNormalizer.normalize("wss://directory.yabu.me")
val nostr1 = RelayUrlNormalizer.normalize("wss://profiles.nostr1.com")
val bootstrapInbox = setOf(band, damus, primal, mom, nos, bitcoiner, oxtr)
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr)
val defaultSearchRelaySet = setOf(band, wine, where)
val bootstrapInbox = setOf(damus, primal, mom, nos, bitcoiner, oxtr, yabu)
val eventFinderRelays = setOf(wine, damus, primal, mom, nos, bitcoiner, oxtr)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.Skull
import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
import com.vitorpamplona.amethyst.commons.hashtags.Weed
import com.vitorpamplona.amethyst.commons.hashtags.Zap
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
import com.vitorpamplona.amethyst.ui.components.HashTag
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.components.RenderRegular
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
@Preview
@Composable

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
@@ -67,6 +68,18 @@ fun LargeSoftCache<Address, AddressableNote>.filter(
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): List<AddressableNote> = filter(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
fun LargeSoftCache<Address, AddressableNote>.filter(
kinds: List<Int>,
pubKey: HexKey,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): Set<AddressableNote> {
val set = mutableSetOf<AddressableNote>()
kinds.forEach {
set.addAll(filterIntoSet(kindStart(it, pubKey), kindEnd(it, pubKey), consumer))
}
return set
}
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kind: Int,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.LruCache
import androidx.collection.LruCache
interface MutableMediaAspectRatioCache {
fun get(url: String): Float?

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -168,6 +168,7 @@ fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
is Bundle -> {
resource.entry.associateBy { it.id }.toImmutableMap()
}
else -> {
persistentMapOf(resource.id to resource)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -22,8 +22,5 @@ package com.vitorpamplona.amethyst.model
// Re-export from commons for backwards compatibility
typealias Note = com.vitorpamplona.amethyst.commons.model.Note
typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer
typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote
typealias NoteFlowSet = com.vitorpamplona.amethyst.commons.model.NoteFlowSet
typealias NoteBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.NoteBundledRefresherFlow
typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -54,9 +54,7 @@ fun parseThemeType(code: Int?): ThemeType =
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
ThemeType.DARK.screenCode -> ThemeType.DARK
else -> {
ThemeType.SYSTEM
}
else -> ThemeType.SYSTEM
}
enum class ConnectivityType(
@@ -91,9 +89,7 @@ fun parseConnectivityType(code: Boolean?): ConnectivityType =
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
else -> ConnectivityType.ALWAYS
}
fun parseConnectivityType(screenCode: Int): ConnectivityType =
@@ -101,9 +97,7 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType =
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
else -> ConnectivityType.ALWAYS
}
fun parseFeatureSetType(screenCode: Int): FeatureSetType =
@@ -111,18 +105,14 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType =
FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE
FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED
FeatureSetType.PERFORMANCE.screenCode -> FeatureSetType.PERFORMANCE
else -> {
FeatureSetType.COMPLETE
}
else -> FeatureSetType.COMPLETE
}
fun parseGalleryType(screenCode: Int): ProfileGalleryType =
when (screenCode) {
ProfileGalleryType.CLASSIC.screenCode -> ProfileGalleryType.CLASSIC
ProfileGalleryType.MODERN.screenCode -> ProfileGalleryType.MODERN
else -> {
ProfileGalleryType.CLASSIC
}
else -> ProfileGalleryType.CLASSIC
}
enum class BooleanType(
@@ -138,18 +128,14 @@ fun parseBooleanType(code: Boolean?): BooleanType =
when (code) {
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
BooleanType.NEVER.prefCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
else -> BooleanType.ALWAYS
}
fun parseBooleanType(screenCode: Int): BooleanType =
when (screenCode) {
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
BooleanType.NEVER.screenCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
else -> BooleanType.ALWAYS
}
enum class WarningType(
@@ -167,9 +153,7 @@ fun parseWarningType(screenCode: Int): WarningType =
WarningType.WARN.screenCode -> WarningType.WARN
WarningType.SHOW.screenCode -> WarningType.SHOW
WarningType.HIDE.screenCode -> WarningType.HIDE
else -> {
WarningType.WARN
}
else -> WarningType.WARN
}
fun parseWarningType(code: Boolean?): WarningType =
@@ -177,7 +161,5 @@ fun parseWarningType(code: Boolean?): WarningType =
WarningType.WARN.prefCode -> WarningType.WARN
WarningType.HIDE.prefCode -> WarningType.HIDE
WarningType.SHOW.prefCode -> WarningType.SHOW
else -> {
WarningType.WARN
}
else -> WarningType.WARN
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -20,10 +20,12 @@
*/
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@Stable
class UiSettingsFlow(
val theme: MutableStateFlow<ThemeType> = MutableStateFlow(ThemeType.SYSTEM),
val preferredLanguage: MutableStateFlow<String?> = MutableStateFlow(null),
@@ -149,19 +151,19 @@ class UiSettingsFlow(
}
companion object {
fun build(torSettings: UiSettings): UiSettingsFlow =
fun build(uiSettings: UiSettings): UiSettingsFlow =
UiSettingsFlow(
MutableStateFlow(torSettings.theme),
MutableStateFlow(torSettings.preferredLanguage),
MutableStateFlow(torSettings.automaticallyShowImages),
MutableStateFlow(torSettings.automaticallyStartPlayback),
MutableStateFlow(torSettings.automaticallyShowUrlPreview),
MutableStateFlow(torSettings.automaticallyHideNavigationBars),
MutableStateFlow(torSettings.automaticallyShowProfilePictures),
MutableStateFlow(torSettings.dontShowPushNotificationSelector),
MutableStateFlow(torSettings.dontAskForNotificationPermissions),
MutableStateFlow(torSettings.featureSet),
MutableStateFlow(torSettings.gallerySet),
MutableStateFlow(uiSettings.theme),
MutableStateFlow(uiSettings.preferredLanguage),
MutableStateFlow(uiSettings.automaticallyShowImages),
MutableStateFlow(uiSettings.automaticallyStartPlayback),
MutableStateFlow(uiSettings.automaticallyShowUrlPreview),
MutableStateFlow(uiSettings.automaticallyHideNavigationBars),
MutableStateFlow(uiSettings.automaticallyShowProfilePictures),
MutableStateFlow(uiSettings.dontShowPushNotificationSelector),
MutableStateFlow(uiSettings.dontAskForNotificationPermissions),
MutableStateFlow(uiSettings.featureSet),
MutableStateFlow(uiSettings.gallerySet),
)
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -21,9 +21,4 @@
package com.vitorpamplona.amethyst.model
// Re-export from commons for backwards compatibility
typealias UserDependencies = com.vitorpamplona.amethyst.commons.model.UserDependencies
typealias User = com.vitorpamplona.amethyst.commons.model.User
typealias UserFlowSet = com.vitorpamplona.amethyst.commons.model.UserFlowSet
typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo
typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.UserBundledRefresherFlow
typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -31,8 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -44,10 +45,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class AccountCacheState(
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val contentResolverFn: () -> ContentResolver,
val otsResolverBuilder: OtsResolverBuilder,
val otsResolverBuilder: () -> OtsResolver,
val cache: LocalCache,
val client: INostrClient,
) {
@@ -68,13 +69,17 @@ class AccountCacheState(
NostrSignerInternal(accountSettings.keyPair)
} else {
when (val packageName = accountSettings.externalSignerPackageName) {
null -> NostrSignerInternal(accountSettings.keyPair)
else ->
null -> {
NostrSignerInternal(accountSettings.keyPair)
}
else -> {
NostrSignerExternal(
pubKey = accountSettings.keyPair.pubKey.toHexKey(),
packageName = packageName,
contentResolver = contentResolverFn(),
)
}
}
},
accountSettings = accountSettings,
@@ -87,9 +92,11 @@ class AccountCacheState(
val cached = accounts.value[signer.pubKey]
if (cached != null) return cached
val signerWithClientTag = NostrSignerWithClientTag(signer, CLIENT_TAG_NAME)
return Account(
settings = accountSettings,
signer = signer,
signer = signerWithClientTag,
geolocationFlow = geolocationFlow,
nwcFilterAssembler = nwcFilterAssembler,
otsResolverBuilder = otsResolverBuilder,
@@ -118,4 +125,8 @@ class AccountCacheState(
emptyMap()
}
}
companion object {
const val CLIENT_TAG_NAME = "Amethyst"
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
@@ -92,9 +91,9 @@ class PrivateStorageRelayListState(
init {
settings.backupPrivateHomeRelayList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved private home relay list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
@@ -102,7 +101,7 @@ class PrivateStorageRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
getPrivateOutboxRelayListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Private Home Relay List for ${signer.pubKey}" }
(noteState.note.event as? PrivateOutboxRelayListEvent)?.let {
settings.updatePrivateHomeRelayList(it)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -22,15 +22,13 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class UserMetadataState(
@@ -42,11 +40,22 @@ class UserMetadataState(
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val user = cache.getOrCreateUser(signer.pubKey)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val note = cache.getOrCreateAddressableNote(getMetadataEventAddress())
fun getMetadataEventAddress() = MetadataEvent.createAddress(signer.pubKey)
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
fun getUserMetadataFlow(): StateFlow<UserState> = user.flow().metadata.stateFlow
fun getUserMetadataFlow() = note.flow().metadata.stateFlow
fun getUserMetadataEvent(): MetadataEvent? = user.latestMetadata
fun getUserMetadataEvent(): MetadataEvent? = note.event as? MetadataEvent
fun getIdentitiesEventAddress() = ExternalIdentitiesEvent.createAddress(signer.pubKey)
val identitiesNote = cache.getOrCreateAddressableNote(getIdentitiesEventAddress())
fun getExternalIdentitiesEvent(): ExternalIdentitiesEvent? = identitiesNote.event as? ExternalIdentitiesEvent
suspend fun sendNewUserMetadata(
name: String? = null,
@@ -59,9 +68,6 @@ class UserMetadataState(
nip05: String? = null,
lnAddress: String? = null,
lnURL: String? = null,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
): MetadataEvent {
val latest = getUserMetadataEvent()
@@ -79,9 +85,6 @@ class UserMetadataState(
nip05 = nip05,
lnAddress = lnAddress,
lnURL = lnURL,
twitter = twitter,
mastodon = mastodon,
github = github,
)
} else {
MetadataEvent.createNew(
@@ -95,6 +98,29 @@ class UserMetadataState(
nip05 = nip05,
lnAddress = lnAddress,
lnURL = lnURL,
)
}
return signer.sign(template)
}
suspend fun sendNewUserIdentities(
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
): ExternalIdentitiesEvent {
val latest = getExternalIdentitiesEvent()
val template =
if (latest != null) {
ExternalIdentitiesEvent.updateFromPast(
latest = latest,
twitter = twitter,
mastodon = mastodon,
github = github,
)
} else {
ExternalIdentitiesEvent.createNew(
twitter = twitter,
mastodon = mastodon,
github = github,
@@ -106,18 +132,20 @@ class UserMetadataState(
init {
settings.backupUserMetadata?.let {
Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved user metadata ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
// saves contact list for the next time.
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
getUserMetadataFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
settings.updateUserMetadata(it.user.latestMetadata)
Log.d("AccountRegisterObservers") { "Updating Kind 0 ${user.toBestDisplayName()}" }
(it.note.event as? MetadataEvent)?.let {
settings.updateUserMetadata(it)
}
}
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip02FollowLists
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.topNavFeeds.UsingRelayUnwrapper
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
/**
* Computes a list of all declared relays for each user in the follow list.
*/
class DeclaredFollowsPerUsingRelay(
kind3Follows: Kind3FollowListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
val calculator = UsingRelayUnwrapper()
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
kind3Follows.flow
.transformLatest {
emitAll(
calculator.toUsersPerRelayFlow(it.authors, cache) { it },
)
}.onStart {
emit(
calculator.usersPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyMap(),
)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -21,10 +21,10 @@
package com.vitorpamplona.amethyst.model.nip02FollowLists
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@@ -52,14 +52,19 @@ class Kind3FollowListState(
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val user = cache.getOrCreateUser(signer.pubKey)
fun getFollowListFlow(): StateFlow<UserState> = user.flow().follows.stateFlow
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val note = cache.getOrCreateAddressableNote(getFollowListAddress())
fun getFollowListEvent(): ContactListEvent? = user.latestContactList
fun getFollowListAddress() = ContactListEvent.createAddress(signer.pubKey)
fun getFollowListFlow(): StateFlow<NoteState> = note.flow().metadata.stateFlow
fun getFollowListEvent(): ContactListEvent? = note.event as? ContactListEvent
@OptIn(ExperimentalCoroutinesApi::class)
private val innerFlow: Flow<Kind3Follows> =
getFollowListFlow().transformLatest {
emit(buildKind3Follows(it.user.latestContactList ?: settings.backupContactList))
emit(buildKind3Follows(it.note.event as? ContactListEvent ?: settings.backupContactList))
}
val flow =
@@ -108,6 +113,25 @@ class Kind3FollowListState(
)
}
suspend fun follow(users: List<User>): ContactListEvent {
val contactList = getFollowListEvent()
val contacts =
users.map {
ContactTag(it.pubkeyHex, it.bestRelayHint(), null)
}
return if (contactList != null) {
ContactListEvent.followUsers(contactList, contacts, signer)
} else {
ContactListEvent.createFromScratch(
followUsers = contacts,
relayUse = emptyMap(),
signer = signer,
)
}
}
suspend fun follow(user: User): ContactListEvent {
val contactList = getFollowListEvent()
@@ -138,7 +162,7 @@ class Kind3FollowListState(
init {
settings.backupContactList?.let {
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
Log.d("AccountRegisterObservers") { "Loading saved ${it.tags.size} contacts" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
@@ -148,8 +172,10 @@ class Kind3FollowListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
getFollowListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
settings.updateContactListTo(it.user.latestContactList)
Log.d("AccountRegisterObservers") { "Updating Kind 3 ${signer.pubKey}" }
(it.note.event as? ContactListEvent)?.let {
settings.updateContactListTo(it)
}
}
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
class IncomingOtsEventVerifier(
private val otsVerifCache: VerificationStateCache,
private val otsVerifCache: () -> VerificationStateCache,
private val cache: LocalCache,
private val scope: CoroutineScope,
) {
@@ -52,7 +52,7 @@ class IncomingOtsEventVerifier(
suspend fun consume(note: Note) {
note.event?.let { event ->
if (event is OtsEvent) {
otsVerifCache.cacheVerify(event)
otsVerifCache().cacheVerify(event)
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip03Timestamp
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer
import kotlinx.serialization.Serializable
/**
* Immutable data class representing the current OTS blockchain explorer config.
*
* When a custom URL is configured, it is used instead of the automatic
* Tor-aware selection (Mempool when Tor is active, Blockstream otherwise).
* This gives users control over which explorer observes their OTS verifications.
*/
@Serializable
@Stable
data class OtsSettings(
/**
* Custom blockchain explorer base API URL.
* When null/blank, the default Tor-aware selection is used.
* Must be a Mempool-compatible REST API (e.g. https://mempool.space/api/).
*/
val customExplorerUrl: String? = null,
) {
/** True when the user has configured a custom explorer URL. */
val hasCustomExplorer: Boolean get() = !customExplorerUrl.isNullOrBlank()
/**
* Returns the normalized custom URL (trailing slash ensured) or null if not set.
*/
fun normalizedUrl(): String? {
val url = customExplorerUrl?.trim()?.takeIf { it.isNotBlank() } ?: return null
return if (url.endsWith("/")) url else "$url/"
}
companion object {
val DEFAULT = OtsSettings()
val KNOWN_EXPLORERS =
listOf(
OkHttpBitcoinExplorer.MEMPOOL_API_URL to "mempool.space (Tor-friendly)",
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL to "blockstream.info",
)
fun isValidUrl(url: String): Boolean {
val trimmed = url.trim()
if (trimmed.isBlank()) return false
return trimmed.startsWith("http://") || trimmed.startsWith("https://")
}
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
@@ -37,7 +37,7 @@ import java.util.Base64
class OtsState(
val signer: NostrSigner,
val cache: LocalCache,
val otsResolver: OtsResolverBuilder,
val otsResolver: () -> OtsResolver,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
@@ -55,10 +55,10 @@ class OtsState(
}
suspend fun updateAttestations(): List<OtsEvent> {
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
Log.d("Pending Attestations") { "Updating ${settings.pendingAttestations.value.size} pending attestations" }
return settings.pendingAttestations.value.toList().mapNotNull { (key, value) ->
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build())
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver())
if (otsState != null) {
val hint = cache.getNoteIfExists(key)?.toEventHint<Event>()
@@ -96,7 +96,7 @@ class OtsState(
Base64.getEncoder().encodeToString(
OtsEvent.stamp(
id,
otsResolver.build(),
otsResolver(),
),
),
)

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -31,22 +31,24 @@ class TorAwareOkHttpOtsResolverBuilder(
val okHttpClient: (url: String) -> OkHttpClient,
val isTorActive: (url: String) -> Boolean,
val cache: OtsBlockHeightCache,
val customExplorerUrl: () -> String? = { null },
) : OtsResolverBuilder {
fun getAPI(usingTor: Boolean) =
if (usingTor) {
OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL
} else {
OkHttpBitcoinExplorer.Companion.BLOCKSTREAM_API_URL
}
fun getAPI(usingTor: Boolean): String =
customExplorerUrl()
?: if (usingTor) {
OkHttpBitcoinExplorer.MEMPOOL_API_URL
} else {
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
}
override fun build(): OtsResolver =
OtsResolver(
explorer =
OkHttpBitcoinExplorer(
baseUrl = {
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL))
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.MEMPOOL_API_URL))
},
client = okHttpClient(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL),
client = okHttpClient(OkHttpBitcoinExplorer.MEMPOOL_API_URL),
cache = cache,
),
calendar = OkHttpCalendar(okHttpClient),

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -46,7 +46,7 @@ fun loadRelayInfo(
value = it
},
onError = { url, errorCode, exceptionMessage ->
Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage")
Log.e("RelayInfo") { "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage" }
},
)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -21,12 +21,14 @@
package com.vitorpamplona.amethyst.model.nip11RelayInfo
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import okhttp3.OkHttpClient
@Stable
class Nip11CachedRetriever(
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
) {
@@ -74,7 +76,10 @@ class Nip11CachedRetriever(
val doc = relayInformationDocumentCache.get(relay)
if (doc != null) {
when (doc) {
is RetrieveResult.Success -> onInfo(doc.data)
is RetrieveResult.Success -> {
onInfo(doc.data)
}
is RetrieveResult.Loading -> {
if (doc.isValid()) {
// just wait.
@@ -82,6 +87,7 @@ class Nip11CachedRetriever(
retrieve(relay, onInfo, onError)
}
}
is RetrieveResult.Error -> {
if (doc.isValid()) {
onError(relay, doc.error, null)
@@ -89,7 +95,10 @@ class Nip11CachedRetriever(
retrieve(relay, onInfo, onError)
}
}
is RetrieveResult.Empty -> retrieve(relay, onInfo, onError)
is RetrieveResult.Empty -> {
retrieve(relay, onInfo, onError)
}
}
} else {
retrieve(relay, onInfo, onError)

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
@@ -89,9 +88,9 @@ class DmRelayListState(
init {
settings.backupDMRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved DM Relay List ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(it)
}
}
@@ -99,7 +98,7 @@ class DmRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating DM Relay List for ${signer.pubKey}" }
(it.note.event as? ChatMessageRelayListEvent)?.let {
settings.updateDMRelayList(it)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -21,26 +21,27 @@
package com.vitorpamplona.amethyst.model.nip47WalletConnect
import com.vitorpamplona.amethyst.commons.model.INwcSignerState
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectRequestCache
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectResponseCache
import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectRequestCache
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -59,14 +60,14 @@ import kotlinx.coroutines.launch
* @property signer the main Nostr signer used for general Nostr operations
* @property cache the local cache for handling notes and events
* @property scope the coroutine scope used for async operations
* @property settings the account settings containing NIP-47 configuration
* @property nip47Setup the NIP-47 configuration
*/
class NwcSignerState(
val signer: NostrSigner,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
val nip47Setup: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?>,
) : INwcSignerState {
/**
* Derives a NIP-47 signer from the zap payment request in settings.
@@ -74,14 +75,14 @@ class NwcSignerState(
* Flows updates whenever settings change.
*/
val nip47Signer =
settings.zapPaymentRequest
nip47Setup
.map {
buildSigner(it) ?: signer
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
buildSigner(settings.zapPaymentRequest.value) ?: signer,
buildSigner(nip47Setup.value) ?: signer,
)
/**
@@ -111,30 +112,71 @@ class NwcSignerState(
NostrSignerInternal(KeyPair(it))
}
fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null
fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null
override fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex
override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey
/**
* Decrypts a NIP-47 payment request using the current signer.
*
* @param nwcRequest the NIP-47 payment request event to decrypt
* @param event the NIP-47 payment request event to decrypt
* @return the decrypted request or null if not set up or decryption fails
*/
override suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? {
override suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? {
if (!hasWalletConnectSetup()) return null
return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest)
return zapPaymentRequestDecryptionCache.value.decryptRequest(event)
}
/**
* Decrypts a NIP-47 payment response using the current signer.
*
* @param nwsResponse the NIP-47 payment response event to decrypt
* @param event the NIP-47 payment response event to decrypt
* @return the decrypted response or null if not set up or decryption fails
*/
override suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? {
override suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? {
if (!hasWalletConnectSetup()) return null
return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse)
return zapPaymentResponseDecryptionCache.value.decryptResponse(event)
}
/**
* Sends a generic NIP-47 request to the connected wallet.
* Subscribes to responses and waits up to 60s for a reply.
*
* @param request the NIP-47 request to send
* @param onResponse callback to handle the response from the wallet
* @return a pair containing the request event and target relay URL
* @throws IllegalArgumentException if no NIP-47 wallet is set up
*/
suspend fun sendNwcRequest(
request: Request,
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
val walletService = nip47Setup.value ?: throw IllegalArgumentException("No NIP47 setup")
val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, nip47Signer.value)
val filter =
NWCPaymentQueryState(
fromServiceHex = walletService.pubKeyHex,
toUserHex = event.pubKey,
replyingToHex = event.id,
relay = walletService.relayUri,
)
val assembler = nwcFilterAssembler()
assembler.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000)
assembler.unsubscribe(filter)
}
cache.consume(event, null, true, walletService.relayUri) {
onResponse(decryptResponse(it))
}
return Pair(event, walletService.relayUri)
}
/**
@@ -152,8 +194,7 @@ class NwcSignerState(
zappedNote: Note?,
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
val walletService = settings.zapPaymentRequest.value
if (walletService == null) throw IllegalArgumentException("No NIP47 setup")
val walletService = nip47Setup.value ?: throw IllegalArgumentException("No NIP47 setup")
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value)
@@ -165,11 +206,13 @@ class NwcSignerState(
relay = walletService.relayUri,
)
nwcFilterAssembler.subscribe(filter)
val assembler = nwcFilterAssembler()
assembler.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000) // waits 1 minute to complete payment.
nwcFilterAssembler.unsubscribe(filter)
assembler.unsubscribe(filter)
}
cache.consume(event, zappedNote, true, walletService.relayUri) {

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -20,282 +20,6 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
typealias BookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
@Stable
class BookmarkListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
class BookmarkList(
val public: List<Note> = emptyList(),
val private: List<Note> = emptyList(),
)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val bookmarkList = cache.getOrCreateAddressableNote(getBookmarkListAddress())
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.publicBookmarks() ?: emptyList()
}
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.privateBookmarks(signer) ?: emptyList()
}
@OptIn(FlowPreview::class)
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
publicBookmarks(noteState.note)
}.onStart {
emit(publicBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
@OptIn(FlowPreview::class)
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
privateBookmarks(noteState.note)
}.onStart {
emit(privateBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkEventIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkAddressIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkEventIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkAddressIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun bookmarkList(
privateBookmarks: List<BookmarkIdTag>,
publicBookmarks: List<BookmarkIdTag>,
): BookmarkList =
BookmarkList(
public =
publicBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
private =
privateBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
)
@OptIn(FlowPreview::class)
val bookmarks: StateFlow<BookmarkList> =
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
emit(bookmarkList(private, public))
}.onStart {
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
BookmarkList(),
)
fun isInPrivateBookmarks(note: Note): Boolean {
if (!signer.isWriteable()) return false
return if (note is AddressableNote) {
privateBookmarkAddressIdSet.value.contains(note.address)
} else {
privateBookmarkEventIdSet.value.contains(note.idHex)
}
}
fun isInPublicBookmarks(note: Note): Boolean =
if (note is AddressableNote) {
publicBookmarkAddressIdSet.value.contains(note.address)
} else {
publicBookmarkEventIdSet.value.contains(note.idHex)
}
suspend fun addBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent {
val bookmarkList = getBookmarkList()
return if (bookmarkList == null) {
if (note is AddressableNote) {
BookmarkListEvent.create(
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.create(
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
if (note is AddressableNote) {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
}
}
suspend fun removeBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
null
}
}
suspend fun removeBookmark(note: Note): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
signer = signer,
)
}
} else {
null
}
}
}
typealias OldBookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -47,7 +47,7 @@ class HiddenUsersState(
) {
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
suspend fun assembleLiveHiddenUsers(
fun assembleLiveHiddenUsers(
blockList: List<MuteTag>,
muteList: List<MuteTag>,
transientHiddenUsers: Set<String>,

View File

@@ -0,0 +1,130 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip51Lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@Stable
class PinListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
val pinList = cache.getOrCreateAddressableNote(PinListEvent.createPinAddress(signer.pubKey))
fun getPinListFlow(): StateFlow<NoteState> = pinList.flow().metadata.stateFlow
fun getPinList(): PinListEvent? = pinList.event as? PinListEvent
fun pinnedEvents(note: Note): List<EventBookmark> {
val noteEvent = note.event as? PinListEvent
return noteEvent?.pinnedEvents() ?: emptyList()
}
@OptIn(FlowPreview::class)
val pinnedNotes: StateFlow<List<EventBookmark>> =
getPinListFlow()
.map { noteState ->
pinnedEvents(noteState.note)
}.onStart {
emit(pinnedEvents(pinList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val pinnedEventIdSet: StateFlow<Set<String>> =
pinnedNotes
.map { pins ->
pins.map { it.eventId }.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(FlowPreview::class)
val pinnedNotesList: StateFlow<List<Note>> =
pinnedNotes
.map { pins ->
pins.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed()
}.onStart {
emit(
pinnedNotes.value.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed(),
)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun isPinned(note: Note): Boolean = pinnedEventIdSet.value.contains(note.idHex)
suspend fun addPin(note: Note): PinListEvent {
val currentList = getPinList()
val pin = EventBookmark(note.idHex, note.relayHintUrl())
return if (currentList == null) {
PinListEvent.create(
pin = pin,
signer = signer,
)
} else {
PinListEvent.add(
earlierVersion = currentList,
pin = pin,
signer = signer,
)
}
}
suspend fun removePin(note: Note): PinListEvent? {
val currentList = getPinList() ?: return null
val pin = EventBookmark(note.idHex, note.relayHintUrl())
return PinListEvent.remove(
earlierVersion = currentList,
pin = pin,
signer = signer,
)
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -81,7 +81,7 @@ class BlockPeopleListState(
)
} else {
PeopleListEvent.create(
name = PeopleListEvent.BLOCK_LIST_D_TAG,
title = PeopleListEvent.BLOCK_LIST_D_TAG,
person = UserTag(pubkeyHex),
isPrivate = true,
signer = signer,

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
@@ -94,15 +93,15 @@ class BlockedRelayListState(
init {
settings.backupBlockedRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Blocked relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
getBlockedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Blocked Relay List for ${signer.pubKey}" }
(it.note.event as? BlockedRelayListEvent)?.let {
settings.updateBlockedRelayList(it)
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
@@ -107,9 +106,9 @@ class GeohashListState(
init {
settings.backupGeohashList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Geohash list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
@@ -117,7 +116,7 @@ class GeohashListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
getGeohashListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Geohash List for ${signer.pubKey}" }
(noteState.note.event as? GeohashListEvent)?.let {
settings.updateGeohashListTo(it)
}

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