Commit Graph
14810 Commits
Author SHA1 Message Date
Claude 880995e17c feat: editable Notify block — pick receivers for private posts
Phase 3 of the private-notes plan: the composer's Notify row gains an
'+ Add' chip backed by the existing user-suggestion search, so users can
p-tag people who aren't cited in the text — for any post, public or
private. While the private toggle is ON the row is always visible,
relabeled 'Visible to' (the p-tags ARE the audience of the wrap), and an
empty list shows a 'only you will see this' hint for self-only notes.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:52 +00:00
Claude 0fc81cf79d feat: compose private replies and private posts via NIP-17 gift wraps
Phase 2 of the private-notes plan: the short-note composer gains a
private (lock) toggle that gift-wraps the kind-1 to its p-tagged users
plus a self-copy instead of publishing it.

- NIP17Factory.createNoteNIP17: wraps a TextNoteEvent template to its
  taggedUserIds + the sender (only the unsigned rumor form travels)
- Account.sendPrivateNote: signs, wraps, and routes each wrap to the
  recipient's DM relays via the existing broadcastPrivately path
- ShortNotePostViewModel: wantsPrivateNote/privateNoteLocked state;
  forced ON and locked when replying to an unsealed rumor (and when
  reloading a drafted private reply); private wins over anonymous and
  scheduled modes so a locked reply can never fall through to a public
  publish path
- ShortNotePostScreen: lock toggle in the bottom action row; mutually
  exclusive with polls; schedule and anonymous hidden while private
- ReactionsRow: reply re-enabled on private rumors now that the
  composer locks privacy for them

Drafts stay enabled: TextNoteEvent does not implement ExposeInDraft, so
draft wrappers carry no anchor e-tags — the parent rumor id only exists
inside the NIP-44 encrypted draft content.

Verified by PrivateNoteFactoryTest: wraps cover p-tags + self, and the
recipient's unwrap yields a rumor with the same id and an empty sig
(the Note.isPrivateRumor() discriminator).

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:52 +00:00
Claude 7e7d8e5325 feat: private reactions on unsealed rumors + leak prevention for private notes
Unsealed NIP-59 rumors (private replies/posts arriving in gift wraps
from other clients) were indexed as ordinary notes: public reactions,
reposts, edits, pins, OTS timestamps, labels, public bookmarks, and
deletion requests could all e-tag the private rumor id onto public
relays.

- Note.isPrivateRumor(): empty-signature discriminator (rumors are the
  only notes materialized with an empty sig; draft inners are never
  indexed as standalone notes)
- ReactionAction: reactions inherit the target's privacy — empty-sig
  targets get gift-wrapped kind-7s fanned to the rumor author, every
  tagged user, and the sender's self-copy (add-only; un-react would
  need a public NIP-09 deletion that leaks the rumor id)
- AccountViewModel.reactToOrDelete: never NIP-09-delete rumor reactions
  (also fixes the same leak for existing NIP-17 chat reactions),
  tracked-broadcast mode excluded for rumor targets
- ReactionsRow: hide reply/boost/zap on private rumors (each publishes
  a public e-tag of the target); like stays, now wrapped
- DropDownMenu/NoteQuickActionMenu: hide broadcast, edit, timestamp,
  pin, hashtag label, public bookmarks, deletion request for rumors;
  private bookmarks and block/report stay available
- Lock badge in the note header (reuses existing Lock glyph, no font
  regen needed)

Covered by ReactionActionTest (public vs rumor fan-out, jvmTest green).
Plan: commons/plans/2026-06-10-private-replies-reactions-posts.md

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:51 +00:00
Vitor PamplonaandGitHub 33f305b58e Merge pull request #3167 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-10 14:23:36 -04:00
Vitor PamplonaandGitHub 5e6c8ce641 Merge pull request #3168 from vitorpamplona/claude/upbeat-einstein-fn5a3j
Add InlineQuoteRenderer strategy for customizing quoted notes
2026-06-10 14:23:08 -04:00
Claude c5147b1203 fix: treat kind-9 ChatEvent as a chat kind for inline chat-style quotes
The MLS/Marmot inner message kind was missing from isChatEvent, so a
chat message quoted inside an MLS chatroom message still rendered as the
default NoteCompose card instead of the chat reply design.

https://claude.ai/code/session_01DSQW7kku5cGEL36icXg6BC
2026-06-10 17:37:33 +00:00
Claude 429aa4f4a6 fix: skip chat reply row when the reply is already cited inline in the message
In MLS kind-9 chats Note.replyTo keeps both replies and quotes (e and q
tags), so a message quoting another chat message rendered it twice: once
in the reply row and once at its inline nostr: mention. Render the reply
row only when the target is not cited in the content; the inline quote
renderer already draws it in place.

https://claude.ai/code/session_01DSQW7kku5cGEL36icXg6BC
2026-06-10 16:37:56 +00:00
Crowdin Bot 0800eecbe0 New Crowdin translations by GitHub Action 2026-06-10 16:36:02 +00:00
Vitor Pamplona b2ac311539 disabling relay logs for DMs 2026-06-10 12:30:49 -04:00
Vitor Pamplona 1e594a62c9 Removing warning 2026-06-10 12:30:49 -04:00
Claude 551705d14a refactor: render quote-mention leftover chars in DisplayFullNote, not the quote renderer
The trailing characters after a bech mention are part of the surrounding
text, so InlineQuoteRenderer implementations no longer receive extraChars;
DisplayFullNote draws them after delegating the note itself.

https://claude.ai/code/session_01DSQW7kku5cGEL36icXg6BC
2026-06-10 15:21:18 +00:00
Claude c88344c4c6 feat: render chat messages quoted inside chat bubbles with the chat reply design
Inline nostr: quotes used to always render through NoteCompose's quoted-note
card, even when the quoted event was itself a chat message shown inside a
chat bubble. Introduce an InlineQuoteRenderer strategy behind a
CompositionLocal: both rich-text paths (plain and markdown) funnel through
DisplayFullNote, which now reads LocalInlineQuoteRenderer, defaulting to the
existing NoteCompose card.

ChatroomMessageCompose provides a chat-aware renderer for everything inside a
bubble, so quoted chat events (NIP-17/NIP-04 DMs, NIP-28 channel messages,
NIP-53 live chat, ephemeral chat) reuse ChatroomMessageCompose with
innerQuote = true — the same design as the reply row — including
scroll-to-message on tap for quotes within the same room. Non-chat events and
not-yet-loaded quotes keep the default card.

https://claude.ai/code/session_01DSQW7kku5cGEL36icXg6BC
2026-06-10 14:59:36 +00:00
Vitor PamplonaandGitHub e41169efbf Merge pull request #3164 from vitorpamplona/claude/focused-faraday-z9zs87
Work around Compose ui-uikit prebuilt cache linking failure
2026-06-09 19:29:00 -04:00
Claude 84497bbd47 perf(commons): index URLs by first char in fixMissingSpaces
fixMissingSpaces runs on the main thread once per rendered note. The scan
introduced in the previous commit re-tested every detected URL at every
character position (and allocated an iterator per position via firstOrNull),
i.e. O(N*U*L) for a note with U URLs — noticeable on large notes that carry
many links.

Bucket the URLs by their first character once up front and only attempt a
match at positions whose character can actually start a URL; every other
character now costs a single map lookup, keeping the pass linear in the text
length for typical content. Buckets stay longest-first so prefix URLs still
don't shadow longer ones, so the output is identical (verified against the
commons richtext JVM corpus).
2026-06-09 22:52:00 +00:00
Claude f42cc836aa fix(commons): make fixMissingSpaces work on Kotlin/Native (iOS)
RichTextParser.fixMissingSpaces used a Regex of the form
`([^ \n])?(urls)([^ \n])?` to insert spaces around URLs glued to
neighbouring text. Kotlin/Native's regex engine fails to backtrack the
optional `([^ \n])?` capture groups to zero width, so on iOS every URL was
corrupted (e.g. "https://x" became "h https://x"). That broke the
downstream segmenter, which is why :commons:iosSimulatorArm64Test reported
19 failures across the RichText/Gallery/Pdf/F4a parsers once the test binary
finally linked.

Replace the regex with a direct left-to-right scan that inserts a single
space wherever a detected URL touches a non-space/non-newline neighbour. The
scan is engine-independent, so it behaves identically on JVM and Native.
Verified equivalent to the old behaviour across the full commons richtext
JVM corpus, and the new FixMissingSpacesTest pins the cases on every target
(including iosSimulatorArm64).
2026-06-09 22:42:50 +00:00
Claude 9671fc4d04 fix(commons): disable native cache for iOS test binary to fix ui-uikit link
The :commons:linkDebugTestIosSimulatorArm64 CI step fails under Xcode 16.4
with 'Undefined symbols: _OBJC_CLASS_$_UIViewLayoutRegion'. The symbol is
referenced by the prebuilt Kotlin/Native cache of Compose Multiplatform's
org.jetbrains.compose.ui:ui-uikit (CMPLayoutRegion), which was built against
a newer simulator SDK (18.5) than the test binary is linked for (14.0).

Disable the native compiler cache for the iOS test binaries so ui-uikit
recompiles against the active SDK, where UIViewLayoutRegion resolves. The
DisableCacheInKotlinVersion guard re-surfaces the workaround once we move
past Kotlin 2.3.21 so it can be removed when the cache is fixed upstream.
2026-06-09 21:44:11 +00:00
Vitor PamplonaandGitHub a7e892dc7c Merge pull request #3160 from davotoula/chore/remove-tarsosdsp-gpl
Replace GPLv3 TarsosDSP with an in-house pitch shifter
2026-06-09 17:09:11 -04:00
Vitor PamplonaandGitHub d8a37dfb0d Merge pull request #3163 from vitorpamplona/claude/nifty-cray-h4ctiu
Add NIP-14 group subjects and improve DM group creation UX
2026-06-09 17:08:53 -04:00
Claude 85f9998463 revert: drop ChatroomList.changes flow and the screen's reactive use
Backs out the reactive list-refresh plumbing added in the prior commit:
removes the `changes` SharedFlow from the shared `ChatroomList` (restoring
it to its original form) and reverts Desktop `ChatroomListState` to its
original 2s poll. Room assembly is expected to move to a LocalCache.observe
approach on both platforms later, which would supersede this.

Keeps the independent Desktop list improvements (per-room unread tracking
and the mute/acceptable filter), which don't depend on the flow.

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:57:45 +00:00
Vitor PamplonaandGitHub ff71bc1401 Merge pull request #3162 from vitorpamplona/claude/trusting-archimedes-hduulo
Replace String.format with toString(16).padStart in test helpers
2026-06-09 16:56:30 -04:00
Claude a6e9439c6d fix: use multiplatform hex padding in room state tests
String.format is JVM-only and broke the iOS Klib compile of commonTest.
Replace "%064x".format(...) with toString(16).padStart(64, '0').
2026-06-09 20:45:17 +00:00
Vitor PamplonaandGitHub 39d36441d4 Merge pull request #3159 from vitorpamplona/claude/brave-lovelace-d1rap2
fix(commons): make commonTest compile for iOS targets
2026-06-09 16:24:54 -04:00
Vitor PamplonaandGitHub 30dcd3aead Merge pull request #3161 from vitorpamplona/claude/friendly-mayer-g3lhj1
Count NIP-18 quote-reposts as boosts of quoted notes
2026-06-09 16:20:52 -04:00
Claude ed37020fbf ci: run shared commonTest on iOS for :commons
The test-quartz-ios job only compiled :commons production code for iOS
(compileKotlinIos*), never the commonTest source set, so a test using a
JVM-only API compiled fine on JVM/Android and shipped green while silently
breaking compileTestKotlinIosSimulatorArm64.

Mirror the quartz step: run :commons:iosSimulatorArm64Test (compiles +
runs the shared commonTest suite on the simulator) and
:commons:compileTestKotlinIosArm64 (device-only compile drift). This keeps
commonTest KMP-clean going forward.
2026-06-09 20:20:29 +00:00
Claude 1fbefb2efe feat(desktop): bring group DM parity with Android
Makes the Desktop DM client a first-class group participant and tightens
the shared/Desktop DM paths so they match Android behavior.

- commons ChatNewMessageState: actually attach the composed NIP-14 subject
  to sent messages (the field was previously collected but dropped).
- commons ChatroomList: emit a `changes` SharedFlow on add/remove so list
  UIs can refresh reactively; dedupe the User overloads onto the room ones.
- Desktop NewDmDialog: multi-recipient selection (chips + confirm button) so
  a Desktop user can start a group, not only a 1:1.
- Desktop ChatPane/ChatroomHeader: show a group's NIP-14 subject in the
  header and add a rename dialog that broadcasts a subject change to all
  members.
- Desktop Main.kt DM ingest: route any ChatroomKeyable inner event into the
  room (covers kind 14/15 and future variants) and store self-authored
  NIP-37 drafts instead of dropping them.
- Desktop ChatroomListState: refresh reactively off ChatroomList.changes
  (with a slower safety poll), track real per-room unread via a last-seen
  mark, and hide rooms whose latest message isn't acceptable (mute/filter).

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:16:55 +00:00
Claude 10f369c43c feat(model): count NIP-18 quote-reposts in the repost counter
A kind:1 note carrying a `q` tag is a quote-repost of the quoted note,
but Amethyst's reaction-row repost counter reads `Note.boosts`, which
only collected kind:6/kind:16 reposts. Quote-reposts were treated purely
as inline citations (stripped by `tagsWithoutCitations()`), so they never
appeared in the quoted note's repost count.

LocalCache now adds a `q`-tagged note as a boost of each quoted note when
consuming text notes/comments, and detaches it on deletion. The quoted
note is deliberately kept out of `replyTo` so the quote still renders as a
root post in the home feed (`Note.isNewThread`). The same wiring is added
to DesktopLocalCache for parity, with tests pinning the behavior using the
exact event reported.
2026-06-09 20:15:19 +00:00
davotoula 26fc0ea7bf docs(claude): require a license check when adding any dependency 2026-06-09 22:10:55 +02:00
David KasparandGitHub 445702a34a Merge pull request #3158 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-09 22:02:02 +02:00
davotoula 66c345717c Code review fixes:
- honor cancellation in voice anonymizer
- reuse shared Hann window and drop vestigial progress callback
2026-06-09 22:00:29 +02:00
davotoula 190316eb28 Replace GPLv3 TarsosDSP with in-house pitch shifter 2026-06-09 21:59:50 +02:00
Claude fc4f7a03fc fix(commons): make commonTest compile for iOS targets
The commons commonTest source set is shared across all KMP targets,
including the iosArm64/iosSimulatorArm64 spike, but several tests still
reached for JVM-only APIs that don't resolve on Kotlin/Native:

- JUnit (`org.junit.*`, `junit.framework.TestCase`) → kotlin.test, with
  message arguments moved from first (JUnit) to last (kotlin.test).
- `assertArrayEquals` → `assertContentEquals`.
- `@JvmStatic` on the `android.util.Log` test stub → removed (it only
  affects JVM bytecode; companion calls work without it).
- `seg.javaClass.simpleName` → `seg::class.simpleName!!`.
- A test function name containing `()` (illegal on Native) → renamed.
- `String(CharArray, offset, count)` → `CharArray.concatToString`.

CliffDetectorTest exercises `computeStalledSpeakers`/`defaultCliffBackoffMs`,
which live in the jvmAndroid-only NestViewModel and are invisible to iOS,
so it moves to jvmTest alongside NestViewModelTest.
2026-06-09 19:26:04 +00:00
Crowdin Bot 673a12452d New Crowdin translations by GitHub Action 2026-06-09 19:08:10 +00:00
Vitor PamplonaandGitHub 3dfe418150 Merge pull request #3151 from vitorpamplona/claude/relay-message-pagination-LMqSQ
DM history: per-relay backward paging, live-tail split + prune-aware window realignment
2026-06-09 15:05:58 -04:00
Vitor PamplonaandGitHub 696be6181e Merge pull request #3157 from davotoula/fix/latex-rendering
Inline LaTeX — render wrapped equations, upgrade renderer, baseline alignment
2026-06-09 15:05:31 -04:00
Vitor PamplonaandClaude Opus 4.8 39eb25bc17 fix(commons): show "N relays" on every history marker count chip
The in-stream loading marker spelled out "N relays" only for the fully-loaded
(done) chip; active frontiers showed a bare count ("Loading: ↓ 8"). Use the
relays plural for the count fallback on every state so a count chip always reads
as a sentence ("Loading: ↓ 8 relays"). 1–2 short host names still spell out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:17:07 -04:00
David KasparandGitHub 022aec6474 Merge pull request #3156 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-09 20:13:51 +02:00
Vitor PamplonaandClaude Opus 4.8 75315095ee refactor(commons): collapse pager status flows into one PagingStatus snapshot
BackwardRelayPager exposed five independently-updated StateFlows (exhausted,
relayCount, stalledCount, reachedBack, relayProgress) that are all recomputed
together on every page settle. Co-located consumers therefore paid up to five
separate recompositions per settle and could observe a torn read (e.g. an
updated relayCount against a still-stale relayProgress).

Combine them into one atomic PagingStatus snapshot, emitted by a single
publish(), collected once. updateStatus()/recomputeExhausted() merge into that
publish() (exhausted computed inline). loadingMore stays separate: its falling
edge is debounced on its own timer in PerRelayLoadTracker, decoupled from the
status recompute, so folding it in would miss that delayed transition.

Threaded through the 3 history managers and the 3 feed consumers
(ChatroomListFeedView, ChatroomView, LoadingReplyNote): 12 collectors -> 4 at
the heaviest views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:13:26 -04:00
davotoula c8de54d1c2 fix(math): align inline equations to the text baseline 2026-06-09 19:51:31 +02:00
davotoula 8097e6c52d fix(math): upgrade LaTeX renderer to maintained rikkahub jlatexmath fork 2026-06-09 19:51:31 +02:00
davotoula 711ca547b5 fix(math): render inline equations wrapped in opening punctuation 2026-06-09 19:51:31 +02:00
Vitor Pamplona b21aecdfca Improves markers 2026-06-09 13:47:46 -04:00
Claude d6603baf1a Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-09 12:54:51 +00:00
Crowdin Bot e9e5ce4152 New Crowdin translations by GitHub Action 2026-06-09 11:41:09 +00:00
Vitor PamplonaandGitHub fb31d5440a Merge pull request #3155 from nrobi144/feat/desktop-image-compression
feat(desktop): image compression + preview gate, plus lightbox URL hover/copy
2026-06-09 07:39:21 -04:00
nrobi144 c21317912e fix(commons): make UploadOrchestrator backward-compatible for non-image uploads
The pre-existing desktopApp:UploadOrchestratorTest started failing
after the desktop image-compression feature landed:

  - uploadCallsClientWithCorrectParameters (2x2 PNG, no quality set)
  - uploadPassesAuthHeaderToClient (.txt)
  - uploadPassesSameFileWhenNoStripExif (.txt)
  - uploadComputesMetadata (.txt)

The orchestrator was unconditionally calling ImageReencoder.reencode,
which (a) reencoded PNGs to JPEG even when the caller did not opt into
compression and (b) threw UnsupportedFormat for any file the sniffer
could not classify (.txt, voice memos, video files, DM attachments —
the orchestrator is the upload path for everything, not just images).

Two changes restore the orchestrator's original "upload as-is"
behavior for callers that have not opted into compression:

1. UploadOrchestrator.upload's quality parameter is now nullable
   (CompressionQuality? = null). Null means "do not reencode" —
   matches the orchestrator's behavior before this feature, so the
   Android, CLI, and any non-image upload path keeps working
   unchanged. The desktop compose flow continues to pass a non-null
   CompressionQuality so it still runs the reencoder.

2. ImageReencoder no longer throws UnsupportedFormat for
   ImageFormat.Unknown — it returns PassThrough(NotAnImage) instead.
   AVIF and HEIC still throw (those are recognized formats we
   explicitly refuse). The new PassReason.NotAnImage is rendered in
   the preview dialog as "Not an image · uploaded as-is" / "Metadata
   preserved (non-image — no re-encode applies)".

My UploadOrchestratorTest.refusesAvifWithUnsupportedFormat is updated
to pass quality = MEDIUM explicitly so it still exercises the refuse
path under the new opt-in model.
2026-06-09 12:25:41 +03:00
nrobi144 194a109a73 feat(desktop): copy Blossom URL on image click + hover tooltip + snackbar
In the lightbox/carousel:

  - Hover over the image → Material3 PlainTooltip shows the full
    Blossom URL above the image (TooltipAnchorPosition.Above, 8 dp
    gap). Same TooltipBox pattern already used in
    MediaServerSettings.

  - Single-click on the image → copies the URL to the system
    clipboard via AWT Toolkit, then surfaces a green snackbar
    banner at the top: "Copied <url> to clipboard". The banner
    slides in from above, sits below the download banner if both
    fire simultaneously, and auto-dismisses after 2.5 s
    (LaunchedEffect on the message state).

  - Double-click still resets zoom — unchanged.

  - The MoreOptionsMenu's "Copy URL" rows on both the image and
    video paths now route through the same copyUrlToClipboard
    helper so they also trigger the snackbar (previously they
    copied silently with no user feedback).

ZoomableImage gains an `onTap: (() -> Unit)?` parameter; null
keeps the old "consume single tap" behavior, set means the caller
handles the click (lightbox uses it for the copy action).
2026-06-09 11:42:01 +03:00
nrobi144 19e5b3f8de fix(desktop): rename preview dialog Cancel -> Back
"Cancel" implies the post is being abandoned. The actual behavior
is to return to the compose dialog with attachments still attached
so the user can adjust quality, swap files, or change copy before
re-triggering Preview. "Back" matches the semantic.
2026-06-09 11:42:01 +03:00
nrobi144 40d9fe6d97 fix(commons): two crash bugs in ImageReencoder/CompressionException
Reported via runtime crash dialog on the user's first PNG upload:

  Exception in thread "AWT-EventQueue-0":
    java.lang.IllegalStateException: Can't overwrite cause with
      javax.imageio.IIOException: Bogus input colorspace
        at java.lang.Throwable.initCause(Throwable.java:464)
        at CompressionException.<init>(CompressionException.kt:39)

Two real bugs:

1. CompressionException constructor double-set the cause.
   Exception(message, cause) super already wires the Throwable's
   cause slot; the init block then called initCause(cause) AGAIN
   which throws IllegalStateException by spec ("Can't overwrite
   cause"). The init block was added per a code-review note that
   was wrong about how Kotlin's primary constructor forwards
   cause. Removed the init block; relying on super does the
   right thing. Regression: encodeFailedWrapsCauseWithoutCrashing.

2. JPEG writer rejected non-RGB BufferedImages with "Bogus input
   colorspace". TYPE_INT_ARGB (typical PNG decode), TYPE_BYTE_GRAY,
   TYPE_CUSTOM (CMYK JPEGs, indexed PNGs) all blow up the stock
   JPEGImageWriter. encodeJpeg now flattens via toRgbCanvas — draws
   onto a fresh TYPE_INT_RGB canvas with white background for any
   transparent pixels. White matches what every major image viewer
   does for transparent PNGs over a light surface.
   Regression: reencodesPngWithAlphaToJpeg.

Both regressions are covered by new tests so the patterns cannot
silently come back. Reencoder test count: 13 -> 15.
2026-06-09 11:42:01 +03:00
nrobi144 150117241b fix(desktop): preview "skip" now means "upload original", not "drop"
Reworked the per-row toggle in CompressionPreviewDialog to match the
user's actual intent. Previously the Switch meant "exclude this
attachment from the post entirely"; now it means "upload the original
bytes instead of the compressed version" — which is the only
meaningful per-row choice once you've already attached something.

Behavior:
  - Toggle off the compression on a Reencoded row → orchestrator
    uses bypassReencode=true (= upload original), the cached
    compressed temp is deleted right before upload so it never
    leaks.
  - The Publish button no longer changes count or disables —
    everything attached gets uploaded.
  - Cancel still cleans up every cached compressed temp.

Layout fix the user called out:
  - Only the compressed half of the row dims (thumbnail + arrow).
    The original thumbnail stays full-color because that's what's
    actually being uploaded when "use original" is on.
  - The stats/savings line is replaced by "compression skipped —
    original uploads as-is" when toggled.
  - The metadata-strip sub-line now flips dynamically:
      compressed → "All EXIF, GPS, camera tags stripped (re-encoded)"
      original + strip ON + JPEG → "EXIF, GPS, camera tags stripped
                                    from original before upload"
      original + strip ON + non-JPEG → red warning: "Metadata
                                       preserved — strip only runs
                                       on JPEG; original is non-JPEG"
      original + strip OFF → "Metadata preserved (EXIF strip off
                              in settings)"

Style fix: replaced the chunky Switch with a small TextButton —
"Use original" by default (muted color) → "Using original — undo"
when active (error color). Matches the rest of the dialog's
TextButton + DropdownMenu vocabulary; reads as a desktop action,
not a mobile preference.

The toggle is intentionally removed from PassThrough / Failed /
NonImage rows — those have no per-row choice (always-as-original
by design) and a control there would be deceptive.

API change: CompressionPreviewDialog.onPublish is now
(List<PreviewItem>, useOriginalPaths: Set<String>) -> Unit.
runPublish in ComposeNoteDialog routes Reencoded items in the
useOriginalPaths set through orchestrator.upload(bypassReencode =
true) and deletes the unused compressed temp inline.
2026-06-09 11:42:01 +03:00
nrobi144 b73f7fe4e2 feat(desktop): per-row skip toggle + explicit metadata-strip status in preview
Two manual-testing asks landed together — they share the same row
template inside CompressionPreviewDialog.

Per-row skip toggle:
  - Every preview row gains a Switch labeled "Include" / "Skipped"
    (the verb is shown so the user can't misread a bare switch).
  - Skipped rows dim the thumbnail (0.4 alpha) and tone down the
    surface, hide the "Click to compare" hint, and disable the
    click-to-zoom.
  - Publish button label now reflects the included count —
    "Publish (4)" when nothing skipped, "Publish (3 of 5)" with
    skips, "Nothing to publish" + disabled state when all skipped.
  - On Publish, the dialog calls cleanupPreviewTemps(skippedItems)
    so dropped re-encodes don't leak in ~/.amethyst/tmp/. The
    included subset is handed off to UploadOrchestrator via the
    preCompressed param as before.
  - onPublish signature changed: (List<PreviewItem>) -> Unit, and
    runPublish in ComposeNoteDialog now takes the filtered list
    rather than reading pendingPreview directly.

Explicit metadata-strip status on every row:
  - Reencoded rows: "All EXIF, GPS, camera tags stripped
    (re-encoded to JPEG)" in the tertiary color. Re-encode wipes
    metadata regardless of the strip-EXIF setting because we
    don't preserve any metadata in the JPEG writer.
  - PassThrough rows:
      Animated → "Metadata preserved (animated — re-encode would
                  drop frames)"
      Vector   → "No raster metadata (SVG)"
      Bypass   → "Metadata preserved per your override"
  - Failed rows (going to send original):
      JPEG + strip on → "EXIF, GPS, camera tags stripped before
                         upload" in tertiary color
      non-JPEG + strip on → "Metadata preserved — strip only runs
                             on JPEG; this is <Format>" in error
                             color (privacy warning)
      strip off → "Metadata preserved (EXIF strip off in settings)"
  - NonImage rows: "Metadata preserved — EXIF strip applies to
                    JPEG only"

The explicit per-row wording makes the strip-EXIF toggle's actual
behavior visible at the moment the user is deciding whether to
publish, rather than buried in the Settings panel.
2026-06-09 11:42:01 +03:00