Adds three override hooks so a single-OS developer can preview every
platform's theming without VMs:
AMETHYST_PLATFORM=GNOME ./gradlew :desktopApp:run
AMETHYST_APPEARANCE=light ./gradlew :desktopApp:run
AMETHYST_ACCENT=#3584E4 ./gradlew :desktopApp:run
Each override accepts either an env var or a `-Damethyst.<key>=<value>`
JVM property (forwarded from the gradle invocation via build.gradle.kts).
- PlatformInfo: `amethyst.platform` accepts MACOS, WINDOWS, GNOME, KDE,
LINUX_OTHER, UNKNOWN. Adds a `host` accessor for code that needs the
real underlying OS (for a future fallback hook).
- PlatformAppearance: `amethyst.appearance` accepts light/dark.
- PlatformAccent: `amethyst.accent` accepts `#RRGGBB`, `RRGGBB`, or any
libadwaita accent name (blue, teal, green, yellow, orange, red, pink,
purple, slate).
Window chrome stays native to the host OS — overriding the platform
swaps in-app theming only, not the AWT title bar — so on a Mac you
still see traffic lights even when previewing the GNOME theme.
https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
Replace the hard-coded Material3 dark theme with a per-OS theme that
follows the host system's appearance, accent color, fonts, and rounding
language. Adds a `desktop/platform/` module with:
- PlatformInfo: detects macOS, Windows, GNOME, KDE, other Linux via
XDG_CURRENT_DESKTOP / DESKTOP_SESSION.
- PlatformAppearance: reads OS dark/light preference (defaults on macOS,
gsettings on GNOME, kreadconfig on KDE, registry on Windows) and
refreshes on window focus.
- PlatformAccent: pulls the user's accent color from each OS (named
AppleAccentColor, gsettings accent-color, kdeglobals AccentColor,
DWM AccentColor) and falls back to Amethyst purple.
- PlatformFonts: resolves the OS's preferred UI font via Skia's
FontMgr (SF Pro Text on macOS, Segoe UI Variable on Win 11,
Cantarell/Adwaita Sans on GNOME, Noto Sans on KDE).
- PlatformShapes / PlatformTypography / PlatformColorScheme: per-OS
rounding (macOS 8/10/14, libadwaita 9/12/16, Breeze 6/8/12, WinUI
4/8/8), letter-spacing tightening for SF Pro / Adwaita, and surface
tones from each OS's reference palette.
macOS gets native chrome treatment: `apple.laf.useScreenMenuBar`
routes the MenuBar to the system menu bar; `apple.awt.transparentTitleBar`
+ `apple.awt.fullWindowContent` lets the deck/sidebar extend under the
traffic lights; a 28dp top strip in MainContent reserves space so
content doesn't underlap them.
DeckSidebar bumps from 48dp to 56dp (desktop density) and switches its
background to the Material3 `surfaceContainer` token, which our new
per-OS schemes drive to the right tone for each platform.
https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
Material Symbol icons were allocating a fresh Font wrapper on every
composition (Font(resource = ...) returns a new instance each call),
which invalidated the remember(font) cache in rememberMaterialSymbolsFontFamily
and forced a fresh TextMeasurer per call site. Tint was also baked into
the TextStyle passed to measure(), so any tint change (selected /
unselected reaction icons, hover states) produced a cache miss.
Hoist the FontFamily + TextMeasurer to CompositionLocals provided once
at the root (AmethystTheme on Android, MaterialTheme wrapper on desktop)
via ProvideMaterialSymbols. Every icon in the subtree now reuses:
- One FontFamily (Skia typeface cache hit for every subsequent glyph)
- One TextMeasurer with a 64-entry LRU, shared across all call sites
so its measurement cache is hit across 300+ MaterialSymbols usages
- Tint applied via drawText(layout, color = tint) instead of TextStyle,
so the measured TextLayoutResult is reused across tint variations
Icon(symbol = ...) now delegates to Material3's Icon(painter = ...),
restoring proper sizing/semantics without the custom Box+drawBehind
workaround. autoMirror handled inside the painter's onDraw. The
previously-unused MaterialSymbolPainter is now the single render path.
The weight parameter is dropped from the Icon/Painter APIs since the
app uses a single weight globally (MaterialSymbolsDefaults.WEIGHT).
A local fallback path keeps @Preview composables that don't wrap in
the theme renderable (per-composition allocation, same cost as before).
https://claude.ai/code/session_01V9esGUFH72ujCQNWCAnjKN
Moves five JVM-only upload helpers from desktopApp into commons so
the CLI can reuse the same Blossom upload + AES-GCM encryption path
without depending on :desktopApp:
desktop/service/upload/DesktopBlossomAuth.kt → commons/service/upload/BlossomAuth.kt
desktop/service/upload/DesktopBlossomClient.kt → commons/service/upload/BlossomClient.kt
desktop/service/upload/DesktopMediaCompressor.kt → commons/service/upload/MediaCompressor.kt
desktop/service/upload/DesktopMediaMetadata.kt → commons/service/upload/MediaMetadata.kt
desktop/service/upload/DesktopUploadOrchestrator.kt → commons/service/upload/UploadOrchestrator.kt
API tweaks:
- Drop the `Desktop` prefix on every class.
- Rename the `DesktopMediaMetadata` object to `MediaMetadataReader`
to avoid colliding with the `MediaMetadata` data class in the same
package.
- BlossomClient no longer reaches into desktop's `DesktopHttpClient`.
Callers pass an `OkHttpClient` (or use the default one-shot client);
desktop continues to inject its Tor-aware client.
- Tighten BlossomClient's response handling — the OkHttp version on
commons' classpath has a nullable `Response.body`, so explicitly
null-check it.
Mechanical updates to keep desktop building:
- ComposeNoteDialog / ChatPane / DesktopUploadTracker switch to the
new commons imports.
- DimensionTag-from-MediaMetadata: cross-module smart cast no longer
works on the public `width`/`height` properties — replace with
`?.let { … }` chains.
- commons/jvmMain now declares `commons-imaging` (used only by
MediaCompressor's EXIF stripping).
Tests come along too: file names follow the renamed classes
(spotless ktlint enforces this).
Next commit will add `amy dm send-file --file PATH --server URL`,
which calls `UploadOrchestrator.uploadEncrypted(...)` directly.
Replace the `material-icons-extended` library with Google's Material Symbols
variable font, rendered via a new `commons/icons/symbols/` package. Weight is
set to 100 (Thin) via a single tunable default (MaterialSymbolsDefaults.WEIGHT),
so we can re-tune line thickness globally in one line without regenerating
assets.
Key changes:
- commons/composeResources/font/material_symbols_outlined.ttf: Material Symbols
Outlined variable font (all 3600+ glyphs, 4 design axes: wght/FILL/GRAD/opsz).
- commons/icons/symbols/: MaterialSymbol data class, 219 codepoint constants,
variable-font FontFamily helper, Canvas-backed Icon composable (overload that
accepts MaterialSymbol and supports auto-mirror for RTL), and a Painter
wrapper for use with Image / AsyncImage.
- Bulk-rewrote 250+ call sites across amethyst/, commons/, desktopApp/:
Icons.{Default,Outlined,Filled,Rounded,AutoMirrored.*}.X → MaterialSymbols.X
or MaterialSymbols.AutoMirrored.X. `imageVector =` renamed to `symbol =`
inside Icon() calls; Image(imageVector = MS.X) converted to
Image(painter = rememberMaterialSymbolPainter(MS.X)).
- Dropped material-icons-extended dependency from commons, desktopApp,
amethyst build files.
Hand-drawn custom icons (Bookmark, Like, Reply, Zap, chess pieces, robohash)
remain as ImageVector and are untouched.
https://claude.ai/code/session_016gV9gEaSud6kDMdcaqy8ti
Three call sites did the same two-layer peel —
kind:1059 → (nip44 decrypt) → kind:13 sealed rumor → (nip44 decrypt) → rumor
Extracted as `GiftWrapEvent.unwrapAndUnsealOrNull(signer)` in
commons/relayClient/nip17Dm/, and collapsed:
- commons/marmot/MarmotIngest.kt — was an inline `when` switch; now a
one-liner. Behaviour unchanged (still passes through if the inner
isn't a seal, matching the previous defensive `else -> inner` branch).
- desktopApp/Main.kt — dropped the nested unwrap/unseal block in the
kind:1059 case of the LocalCache dispatcher.
- cli/DmCommands.kt — replaced the manual chain in `decryptChatMessages`.
Android's DecryptAndIndexProcessor keeps its two separate handlers
(processNewGiftWrap / processNewSealedRumor) because the LocalCache
pipeline re-enters on each peel — that's a different shape and not
touched.
No behaviour change on any platform.
- Persist relay list events (kinds 10050/10007/10006) as JSON to
java.util.prefs.Preferences with per-account key isolation
- Load persisted relay configs on startup before bootstrap subscription
- Validate loaded events (kind + pubkey check), 8KB guard on writes
- Fix SearchScreen relay count: "0 of 1" not "0 of 7" — uses searchRelays
- Fix FeedScreen relay count: shows feed relay count, not all connected
- Per-screen relay picker dialogs: Dns icon on Feed and Search screens
opens AlertDialog wrapping existing editors (Nip65RelayEditor,
SearchRelayEditor) — no new composable files
- Fix created_at dedup: use >= for replaceable event semantics
- Fix setters: use TimeUtils.now() not Long.MAX_VALUE
- Add consumePublishedEvent() for local immediate update after publish
- Remove stale "not loaded" warnings from Search/Blocked editors
- Fix FeedHeader type: Set<NormalizedRelayUrl> not Set<Any>
- Fix picker LaunchedEffect(Unit) to not overwrite user edits
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a parallel ThumbHash placeholder everywhere BlurHash is already used.
Remote events now carry both hashes; renderers prefer thumbhash when
available and fall back to blurhash. The MIP-04 `thumbhash` imeta field
that was previously reserved-but-unused is now wired end-to-end.
- New ThumbHash encoder/decoder in commons/commonMain (no new Gradle dep)
- New ThumbhashTag and thumbhash accessors/builders across NIP-17, 68,
71, 94, 95, 99, the experimental profile gallery, and MIP-04
- RichTextParser + MediaContentModels carry a thumbhash field alongside
blurhash so every downstream composable can pick its preferred placeholder
- New ThumbHashFetcher (Android + Desktop) registered with Coil, plus a
small placeholderModel(thumbhash, blurhash) helper centralising the
"prefer thumbhash, fall back to blurhash" rule
- Rename BlurhashMetadataCalculator -> PreviewMetadataCalculator; the
calculator decodes the bitmap / video thumbnail once and runs both
encoders on the same pixels to keep upload cost flat
- DesktopMediaMetadata, MediaUploadResult, and FileHeader now surface
both hashes through the NIP-96, Blossom, NIP-95, MIP-04, NIP-17 DM,
Classifieds, picture, video, and long-form upload paths
- Round-trip unit test for the ThumbHash port
Replace tint-only selection with Surface background + primaryContainer
color so the selected icon is clearly visible in the workspace editor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Shorten "Single Pane" to "Single" in SegmentedButton to prevent wrapping
- Add 8dp left margin before checkmark in workspace card
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PinnedNavBarState now takes WorkspaceManager reference. Pin/unpin
actions update the active workspace's singlePaneScreens list,
making sidebar customization and workspace editing the same action.
- PinnedNavBarState.syncToWorkspace() updates active workspace on pin/unpin
- PinnedNavBarState.loadFromWorkspace() loads from active workspace
- Remove separate DesktopPreferences.pinnedNavItems persistence
- Workspace is the single source of truth for nav bar screens
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change Workspace.singlePaneScreen (single string) to
singlePaneScreens (list of typeKey strings). First screen is
the default. On workspace switch, loads the screen list into
PinnedNavBarState and navigates to the first screen.
- Add SinglePaneScreensEditor to workspace editor dialog
- Add PinnedNavBarState.loadFromList() for workspace-driven nav
- Backward compat: load old "singlePaneScreen" format as single-item list
- Cmd+Shift+S captures current deck columns as singlePaneScreens
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove consumed flag (stale on re-open, double-fire is harmless)
- Fix keyboard nav for WORKSPACES tab (was broken, only worked for screens)
- Reset selectedIndex on tab switch
- Fix delete active workspace → reload columns for new active workspace
- Extract DeckColumnType.param() extension to eliminate 3x duplication
- Simplify moveSelection to handle all modes (tabs + unified search)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two-tab App Drawer (Screens/Workspaces), workspace cards with CRUD,
editor dialog with icon picker and column configuration, unified search
across screens and workspaces, layout mode auto-switching, and
Cmd+Shift+S to save current layout as workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add workspace presets for different usage modes. Each workspace stores
layout mode + column configuration. Switching destroys current layout
and rebuilds from saved config (no background state).
- Add Workspace data model with WorkspaceColumn
- Add WorkspaceManager with save/load/switch/add/delete
- Add WorkspaceIcons registry for Material icon resolution
- Add WorkspaceBar to AppDrawer footer with workspace chips
- Add DeckState.loadFromWorkspace() for column rebuilding
- Add workspaces persistence to DesktopPreferences
- Default workspace: "Social" (Home + Notifications + DMs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add PinnedNavBarState to manage user-customizable sidebar items.
Replace hardcoded navItems in SinglePaneLayout with pinnedScreens
from state, persisted as CSV to DesktopPreferences. Right-click
context menu on App Drawer items to pin/unpin from sidebar.
- Add PinnedNavBarState with pin/unpin/move/save/load
- Add pinnedNavItems to DesktopPreferences (CSV persistence)
- Update SinglePaneLayout to use pinnedScreens from state
- Update AppDrawer with isPinned indicator and context menu
- Remove NavItem data class and hardcoded navItems list
- HomeFeed and Settings are always pinned (non-removable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace AddColumnDialog with full-screen App Drawer overlay (Cmd+K).
Screens organized by category (Social, Long-Form, Discovery, Identity,
Play) with instant search, keyboard navigation (arrows + Enter), and
open-column indicators in Deck mode. Works in both Single Pane and
Deck layout modes.
- Add AppDrawer.kt with ScreenCategory, LAUNCHABLE_SCREENS registry
- Add SinglePaneState for hoisted single-pane navigation
- Wire Cmd+K shortcut, redirect Cmd+T to drawer
- Add "More" button to SinglePaneLayout nav rail
- Delete AddColumnDialog.kt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1 fixes (must-fix):
- AppRun: VLC path corrected to usr/lib/app/linux/vlc (jpackage actual
path), add VLC_PLUGIN_PATH env var for codec discovery
- linuxdeploy: use APPIMAGE_EXTRACT_AND_RUN=1 env var to bypass FUSE
on CI runners instead of fragile pre-extract + symlink approach
- Prerelease classifier: inverted to positive-match stable format
(^v[0-9]+\.[0-9]+\.[0-9]+$) across desktop, Android, and composite
action — eliminates regex drift between allowlists
- assert-stable-release: now accepts inputs (tag, is_prerelease,
is_draft) so workflow_dispatch on bump workflows also validates
P2 fixes (should-fix):
- .gitignore: add linuxdeploy-*.AppImage and extracted dirs
- RPM version: use replace("-", "~") instead of substringBefore("-")
for correct RPM prerelease ordering (1.08.0~rc1 < 1.08.0)
- linux-portable → linux alias moved into collect_assets() in
scripts/asset-name.sh (single source of truth)
- Android cache key: add gradle/libs.versions.toml to hashFiles
- r0adkll/sign-android-release: SHA-pinned to 349ebdef (v1)
P3 fixes (nice-to-have):
- LINUXDEPLOY_OUTPUT_VERSION env var replaced with
APPIMAGE_EXTRACT_AND_RUN (misleading comment + redundant var)
- nick-fields/retry timeout: 40m → 15m (surface real hangs)
- generate_release_notes: true only on Android job (avoid
last-writer-wins race across 6 concurrent uploaders)
- apt-get install rpm: tightened guard to matrix.family == 'linux'
(linux-portable leg doesn't need rpm tooling)
Complete Tor toggle UX:
- key(appRestartKey) wraps App — full Compose tree rebuild on toggle
- TorManager at Window level — survives rebuild, no stop/start collision
- Tor bootstrap gate at top of App — blocks ALL creation (clients, relays,
Coil, subscriptions) until Tor proxy is ready. Zero clearnet during bootstrap.
- Coil ImageLoader reinitialized after setInstance on each rebuild
- Connection pool evicted on rebuild
- DesktopTorManager retries start up to 3x (handles previous daemon stopping)
- Settings reload from prefs on rebuild (fixes stale toggle display)
- Confirmation dialog on mode change in both TorSettingsSection and Dialog
Known: 2-4 stale Coil CDN connections may persist briefly after toggle
(image loads, not relay traffic — close after OkHttp 5min keep-alive).
Relay WebSocket connections are fully through Tor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user changes Tor settings, shows "Restart Required" confirmation
dialog. On confirm: saves prefs, triggers appRestartKey++ which forces
Compose to unmount/remount entire App tree.
Key behaviors:
- Window stays open, user stays logged in (AccountManager outside App)
- Column layout preserved (DeckState outside App)
- DisposableEffect stops old Tor daemon before new one starts
- TorSettingsSection mode change → confirmation dialog
- TorSettingsDialog Save → confirmation dialog
- Relays, subscriptions, caches all recreated fresh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the LaunchedEffect/scope.launch reconnect code that didn't
work (relay subscriptions lost after disconnect+connect cycle).
Tor toggle will use app rebuild via key() instead (next commit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relay reconnect was only triggered when Tor transitioned to Active.
When toggling OFF, relays stayed on the dead SOCKS proxy and never
reconnected over clearnet — feed went blank.
Now triggers reconnect on both Active and Off transitions, so relays
switch between proxy and direct connections when toggling Tor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port Android's OkHttpFactory pattern to desktop — creates a custom
Fetcher.Factory<Uri> that calls DesktopHttpClient.currentClient() per
image request, routing all image loads through the SOCKS proxy when
Tor is active.
Avoids the OkHttpNetworkFetcher.factory() import issue by directly
constructing NetworkFetcher with the Tor-aware Call.Factory, matching
the proven Android implementation.
This closes the last network leak — ALL egress paths now route through
Tor when enabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SECURITY: With Tor ON, all HTTP traffic now routes through SOCKS proxy.
Previously only relay WebSocket connections used Tor; 8 other egress
paths created bare OkHttpClient() instances bypassing Tor entirely.
Fail-closed behavior:
- When Tor expected but bootstrapping → dead SOCKS proxy on port 1
(requests fail instead of leaking IP)
- lateinit var crashes on misconfiguration (loud failure vs silent leak)
Leak sites fixed (all use DesktopHttpClient.currentClient()):
- AnimatedGifImage: GIF fetch
- SaveMediaAction: media downloads
- EncryptedMediaService: NIP-17 DM media
- ServerHealthCheck: Blossom server probes
- NoteActions: zap/lightning LNURL resolution
- DesktopBlossomClient: media uploads
- AccountManager: NIP-46 bunker relay connections
- Coil image loader: TODO (OkHttpNetworkFetcher import issue)
Also:
- Relay reconnect on Tor Active (prevents stale clearnet connections)
- isTorExpected() for fail-closed logic
- Tests updated for new torTypeProvider parameter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shield was getting pushed off screen when RelayHealthIndicator ("x s ago")
appeared above it. Moved to very last position (after BunkerHeartbeat)
so it's always visible at the bottom edge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shield icon was only in DeckSidebar (deck mode) but missing from
SinglePaneLayout's NavigationRail. Added between RelayHealthIndicator
and BunkerHeartbeatIndicator. Clicking navigates to Settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UX improvements based on manual testing feedback:
- Default TorType changed from OFF to INTERNAL (Tor on by default)
- Default preset changed to Full Privacy (all routing via Tor)
- Shield icon always visible in sidebar (not hidden when Off)
- Shield icon clickable — opens Settings
- TorSettingsDialog Cancel/Save buttons sticky at bottom (don't scroll away)
- HorizontalDivider above buttons for visual separation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 6: Polish and wiring.
Per-relay routing:
- TorRelayEvaluation wired into DesktopHttpClient with actual settings
- shouldUseTorForRelay uses real evaluation instead of { false }
- Settings changes propagate to torTypeFlow/externalPortFlow reactively
CompositionLocal for Tor state:
- LocalTorState provides TorState (status, settings, onSettingsChanged)
- Avoids threading Tor params through every composable layer
- DeckColumnContainer reads from LocalTorState for RelaySettingsScreen
.onion relay badge:
- RelayStatusCard shows "Requires Tor" (red) when Tor is off
- Shows "via Tor" (green) when Tor is active
- Only on .onion relay URLs
Shutdown hook:
- activeTorManager.stopSync() in existing shutdown hook
- Ensures no orphaned Tor processes on app exit
- Set via volatile var from App composable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5: Desktop settings UI for Tor.
TorStatusIndicator:
- Shield icon in sidebar footer (gray/yellow/green/red)
- Tooltip shows status (no port number for security)
- Only visible when Tor is not Off
TorSettingsSection:
- Inline in settings screen between Media Server and Dev Settings
- Mode selector (Off/Internal/External) with segmented buttons
- External SOCKS port input with validation (1-65535)
- "Advanced..." button opens full dialog
TorSettingsDialog:
- Full DialogWindow with preset selector (radio buttons)
- 4 relay routing toggles + 7 content routing toggles
- Preset auto-detection (whichPreset)
- Save/Cancel with DesktopTorPreferences persistence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add complete UI layer for Marmot Protocol MLS group messaging:
State models (commons/commonMain/):
- MarmotGroupChatroom: tracks decrypted inner messages per group
- MarmotGroupList: manages all group chatrooms with change notifications
- MarmotGroupFeedFilter: feed filter for group message feeds
- MarmotGroupFeedViewModel: ViewModel exposing group messages
Account integration:
- Add marmotGroupList to IAccount interface and implementations
- GroupEventHandler indexes inner events into MarmotGroupList
- AccountViewModel gains sendMarmotGroupMessage, createMarmotGroup,
publishMarmotKeyPackage helper methods
Android screens (amethyst/):
- MarmotGroupListScreen: lists all groups with last message preview
- MarmotGroupChatScreen: group conversation with message composer
- MarmotGroupChatView: message feed + simple text composer
- CreateGroupDialog: create new MLS group with display name
- AddMemberDialog: add member by npub/hex (KeyPackage fetch TBD)
Navigation & integration:
- Route.MarmotGroupList and Route.MarmotGroupChat routes
- Wired into AppNavigation nav graph
- "MLS Groups" button added to ChannelFabColumn on Messages screen
- Marmot group newest messages appear in ChatroomListKnownFeedFilter
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Adds an ephemeral variant of GiftWrap for transient encrypted messages
that relays don't need to persist. EphemeralGiftWrapEvent extends
GiftWrapEvent so all existing processing paths (decryption, routing,
notifications, call signaling) automatically handle it via type
hierarchy. Relay subscription filters are updated to request both kinds.
https://claude.ai/code/session_0157X96G6HLTzYxkdX9pyTSJ
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
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
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