Commit Graph
14641 Commits
Author SHA1 Message Date
nrobi144 0a4550af26 feat(commons): add image format sniffer + compression types
Phase 1 part A of desktop image compression. Pure data types — no
external dependencies yet beyond Thumbnailator already on classpath.

  - CompressionQuality enum (Low / Medium / Desktop High) with
    JPEG quality values tuned for 2026 displays (0.65 / 0.75 / 0.90)
    rather than the obsolete 2014-era Android values.
  - ImageFormat sealed class covering the 9 formats the orchestrator
    must distinguish (JPEG/PNG/BMP/TIFF re-encoded, animated GIF and
    animated WebP byte-identical pass-through, SVG pass-through,
    AVIF and HEIC refused for lack of a pure-Java decoder).
  - ImageFormatSniffer with magic-byte detection + RFC 9649 VP8X
    animation-flag check + GIF NETSCAPE2.0 application-extension
    scan + ISO BMFF ftyp brand match for AVIF / HEIC variants.
  - CompressionException sealed hierarchy (UnsupportedFormat /
    InputTooLarge / EncodeFailed) with initCause for chain
    preservation through logging.

23 sniffer unit tests cover each format including JPEG-via-file,
empty input, missing file, and both WebP animation paths.
2026-06-09 11:41:59 +03:00
nrobi144 e17f04eb54 build(commons,cli): add Thumbnailator + force AWT headless for image compression
Phase 0 of the desktop image compression plan
(docs/plans/2026-06-08-feat-desktop-image-compression-plan.md).

  - commons jvmMain gains net.coobird:thumbnailator:0.4.21 (pure-Java,
    MIT) — to be consumed by the new ImageReencoder in Phase 1.
  - amy CLI now sets -Djava.awt.headless=true via three paths so any
    transitive ImageIO/AWT touch never spawns a GUI thread:
      * applicationDefaultJvmArgs in cli/build.gradle.kts (covers the
        installDist startup scripts and any future jpackage launcher),
      * the amyImage custom Unix launcher in cli/build.gradle.kts,
      * System.setProperty as the first line of cli Main.kt — belt-
        and-braces for invocations that bypass the launcher scripts.
  - commons:jvmTest forces -Djava.awt.headless=true for the same
    reason during test runs.

Smoke tests (CompressionSmokeTest.kt) document Thumbnailator's
upscale-by-default behavior — ImageReencoder must gate the resize
itself in Phase 1.
2026-06-09 11:41:59 +03:00
nrobi144 d9c39b6065 docs(plans): add desktop image compression plan
Plan for adding JPEG-only image re-encode + downscale pipeline to
desktop (and the Amy CLI), with EXIF strip, per-post quality override,
clipboard-paste integration, and a fail-loud failure dialog.

WebP encoding and HEIC input deferred to a JNI follow-up: no usable
pure-Java implementations exist in 2026 (sejda webp-imageio is JNI +
abandoned, TwelveMonkeys has no HEIC plugin per issue #976).
2026-06-09 11:41:59 +03:00
Vitor PamplonaandGitHub 919e45eff1 Merge pull request #3154 from vitorpamplona/claude/equation-rendering-klegT
fix(math): center inline equations on the text line
2026-06-08 21:21:49 -04:00
Claude 65b74fe150 fix(math): center inline equations on the text line
The equation image is taller than a text line, and the paragraph FlowRow
top-aligned its items, so equations hung below the baseline. Center items
on the cross axis (itemVerticalAlignment) so the equation sits centered
on the line; no-op for the common all-text row where every item is the
same height.
2026-06-09 01:20:13 +00:00
Vitor PamplonaandGitHub 8afdfba1c2 Merge pull request #3153 from vitorpamplona/claude/equation-rendering-klegT
fix(math): collapse over-escaped backslashes in inline equations
2026-06-08 20:57:56 -04:00
Claude be06b7335b fix(math): collapse over-escaped backslashes in inline equations
Some sources (e.g. the math-academy posts) over-escape their LaTeX, so
the content carries `\\ldots` / `\\cdots` instead of `\ldots` / `\cdots`.
JLaTeXMath reads the `\\` as a forced TeX line break — splitting every
inline equation across two lines — and renders the trailing command name
as the literal letters "ldots"/"cdots", so the ellipsis symbol is lost.

Collapse doubled backslashes (`\\cmd` -> `\cmd`) before rendering inline
math; display math is left untouched since `\\` can be a genuine line
break there. No-op when the content isn't over-escaped.
2026-06-09 00:52:47 +00:00
Vitor PamplonaandGitHub dd88ae3167 Merge pull request #3152 from vitorpamplona/claude/equation-rendering-klegT
Add LaTeX math rendering for $...$ and $$...$$ equations
2026-06-08 20:36:11 -04:00
Vitor PamplonaandGitHub faa058c97d Merge pull request #3150 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-08 20:25:37 -04:00
Crowdin Bot d23b69cc9b New Crowdin translations by GitHub Action 2026-06-08 23:33:06 +00:00
Vitor PamplonaandGitHub 159f8c92c0 Merge pull request #3149 from davotoula/audio-square-player
Square the feed audio player
2026-06-08 19:31:20 -04:00
davotoula 14b5e9043d Code review:
- refactor(audio): tighten visibility, close track-listener race, reorder isAudio param
- refactor(audio): simplify audioSquare guard and dedup the audio-track listener
2026-06-08 23:37:52 +02:00
Claude b8ac919acd test(richtext): guard math/link/image/hashtag adjacency
Pins that a MathSegment covers only its $-delimited span (not the
paragraph), that space-separated hashtags, URLs and images next to math
stay independently detected, that `$x$.` keeps its trailing period while
a following hashtag remains its own segment, and that currency `$5`
doesn't pair with a later equation. Also documents the glued (no-space)
edge cases.
2026-06-08 21:27:15 +00:00
davotoula 0feac36af2 feat(audio): square the feed audio player so controls get room
Audio files have no video dimensions, so the player collapsed to a thin
strip and the playback controls were cramped. Size the inline-feed player
square (full width, height capped at 400dp) for every audio style except
voice notes, which keep their seek-bar strip. Full-screen still fills the
screen.

- AudioPlayerSquare: pure shouldSquareAudioPlayer decision + audioSquareSide
  math + a Modifier.audioSquare layout modifier (guards zero-width/unbounded
  first-layout passes), with unit tests.
- Detect audio via imeta mimeType (up front, no resize) plus a shared
  rememberIsAudioTrack composable (runtime fallback for bare-URL audio),
  extracted from AudioPlayingAnimation so the visualizer and the container
  share one signal.
- CLASSIC keeps its compact 48dp wave centered inside the square; the
  spectrum styles fill it via the existing audioVisualizerHeight policy.
2026-06-08 22:48:18 +02:00
David KasparandGitHub 8a0fadbd07 Merge pull request #3148 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-08 21:32:43 +02:00
Crowdin Bot 8567a4b9e8 New Crowdin translations by GitHub Action 2026-06-08 19:21:32 +00:00
davotoula d6586fe898 update cs,sv,de,pt 2026-06-08 21:17:50 +02:00
Vitor PamplonaandGitHub 8c6b94fc6f Merge pull request #3147 from davotoula/audio-visualiser
Audio visualiser for audio notes
2026-06-08 12:33:16 -04:00
David KasparandGitHub 6bf9f0e929 Merge pull request #3146 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-08 18:02:31 +02:00
davotoula 96237535ec Delay live visualizer to compensate output latency
- fix(audio): delay live visualizer to compensate output latency
- feat(audio): route-aware visualizer delay (wired vs Bluetooth)
- docs(audio): explain the hardcoded visualizer delay; note auto-detect was rejected
2026-06-08 17:46:58 +02:00
davotoula 0e3c597b96 Final code review
- RadialRenderer: guard minDim <= 0 so a transient 0-size canvas never builds Brush.radialGradient(radius = 0f), which throws.
- AuroraRenderer: clamp y with coerceIn(half, maxOf(half, h - half))
- PcmTapRegistry: never evict a flow that a composable is still collecting (subscriptionCount > 0)
- Aurora/Waves: build the palette-only gradient brushes once via remember(palette)
- audioVisualizerHeight: clamp the fallback strip to min(fallback, maxHeight)
- normalizeToPeakInPlace gains a fromIndex param
- Remove unused silentSpectrum()
2026-06-08 17:46:58 +02:00
davotoula 40642fbcad Manual visual tuning
fix(audio): make Waves & Aurora spectrum-shaped and bounded to the canvas
2026-06-08 17:46:58 +02:00
davotoula da9ac36957 Code review and manual testing fixes
refactor(audio): narrow sink visibility, exhaustive style when, clarifying docs
fix(audio): bound tap registry, uniform blurhash scrim, robust fullscreen sizing, settings preview
refactor(audio): dealias log bins, static renderer, opt-in clock, path reuse, hue util
fix(audio): persist visualizer choice via NIP-78 (publish on change)
perf(audio): reuse FFT/window buffers to cut audio-thread allocations
feat(audio): fill height in fullscreen, fixed strip in feed; punchier aurora
2026-06-08 17:46:58 +02:00
davotoula be6acabdee Add audio-visualizer settings
feat(audio): wire audio-visualizer settings into navigation and menu
feat(audio): add audio-visualizer settings screen with live previews
feat(audio): add audio-visualizer settings strings
fix(audio): move visualizer setting to Account section; render at feed size
feat(audio): add Classic (default) and Static visualiser styles
2026-06-08 17:46:58 +02:00
davotoula 032e1bf7fb Add audio visualisers
feat(audio): show selected live visualiser for audio notes
feat(audio): add change/read accessors for audio-visualizer preference
feat(audio): expose synced audio-visualizer preference flow
feat(audio): add media prefs to synced-settings internal model
fix(audio): thread-safe tap registry, reset spectrum on track reuse
feat(audio): tap decoded PCM via TeeAudioProcessor in pooled players
test(audio): unit-test PCM sink with synthetic sine waves
feat(audio): add PCM-tap registry and FFT audio-buffer sink
fix(audio): continuous viz clock, safe peak-normalize, OFF layout, palette guards
feat(audio): add AudioVisualizer dispatcher composable
feat(audio): add renderer interface, canvas scaffold, registry, and all five styles
feat(audio): add deterministic synthetic spectrum for previews
feat(audio): add VisualizerStyle enum and palette
refactor(audio): drop Visualizer FFT helper, add peak normalization
feat(audio): add Hann windowing + PCM-to-float conversion
feat(audio): add pure-Kotlin radix-2 FFT for the visualiser
2026-06-08 17:46:58 +02:00
Claude b4e2b5f651 refactor: cleaner math/parser integration via space-split tokens
The first cut special-cased the whole per-line loop with an
`if (mightContainMath)` branch that duplicated the word-split +
wordIdentifier logic and filtered empty words inconsistently with the
non-math path (which preserves them to keep double-spaces).

Reframe MathParser.split as a drop-in replacement for `line.split(' ')`
that returns typed Word|Math tokens, keeping math spans whole instead of
tearing them at internal spaces. For a math-free line it yields exactly
the same words (empties included), so RichTextParser collapses to a
single uniform map with an exhaustive when — no branch, no duplication.

Also fixes end-of-sentence math: a span glued to trailing punctuation
(`$x$.`) now carries that punctuation as a `trailing` field rendered
adjacent to the equation, mirroring HashTagSegment's extras, instead of
being dropped to a plain word.

https://claude.ai/code/session_01N8ZhVv9912DLGNJiErVTR4
2026-06-08 15:31:46 +00:00
Claude 1096647191 feat: render LaTeX math in notes with $...$ and $$...$$ delimiters
Posts that use the common dollar-delimiter convention (e.g. the
math-academy "Linear Independence" note) now render their formulas
as real equations instead of raw LaTeX text.

- commons: new MathParser tokenizes a line into atomic math spans
  (kept whole, since they contain spaces) interleaved with plain text,
  following the pandoc/remark-math dollar rules so currency like
  "$5 and $10" and escaped "\$" don't false-fire. New MathSegment
  carries the inner LaTeX + display flag through the rich-text pipeline.
- RichTextParser splits math out before the whitespace word-splitter
  when a line might contain math; non-math lines keep the existing path.
- amethyst: LatexEquation renders a MathSegment via JLaTeXMath, tinted
  to the current text color and sized to the font, with a raw-text
  fallback when the formula fails to parse. Wired into both the
  preview and no-preview render paths of RichTextViewer.

Scope: dollar delimiters only, regular (non-markdown) render path.

https://claude.ai/code/session_01N8ZhVv9912DLGNJiErVTR4
2026-06-08 14:47:56 +00:00
Crowdin Bot 56710c63bd New Crowdin translations by GitHub Action 2026-06-08 14:33:36 +00:00
Vitor PamplonaandGitHub 46e29ad3f8 Merge pull request #3145 from vitorpamplona/claude/poll-text-persistence-hFoho
Unify poll and zap poll options into single state map
2026-06-08 10:31:23 -04:00
Vitor PamplonaandGitHub aa670a4587 Merge branch 'main' into claude/poll-text-persistence-hFoho 2026-06-08 10:31:15 -04:00
Vitor PamplonaandGitHub f05500792c Merge pull request #3144 from davotoula/fix/resilient-profile-metadata
Resilient profile metadata (birthday)
2026-06-07 17:36:11 -04:00
Vitor PamplonaandGitHub 021361a72e Merge pull request #3143 from davotoula/feat/render-birdstar-birdex
Minimal first-class rendering for Birdstar "Birdex" species collections (kind 12473)
2026-06-07 17:22:08 -04:00
davotoula 0107808ef6 Code review:
- Expose a nullable descriptor
- Log the JSON element kind instead of the raw, network-sourced value.
- drop birthday happy-path tests duplicated by UpdateMetadataTest
2026-06-07 23:18:57 +02:00
davotoula 39531b85fb fix(metadata): tolerate non-spec birthday so it can't drop the profile 2026-06-07 23:05:11 +02:00
David KasparandGitHub 47f346879c Merge pull request #3142 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-07 22:56:39 +02:00
davotoula fba4b933b0 Code review:
- Make birdex_species_preview_more a <plurals> keyed on the remaining count
- Bound the species preview with maxLines=2
- Hoist the joined-names remember out of the conditional (stable slot).
- Drop the unused accountViewModel parameter
- BirdexEvent.speciesCount() derives from speciesNames().size instead of re-scanning tags
- remember() the joined species-name string so it is not rebuilt on every recomposition.
2026-06-07 22:37:05 +02:00
davotoula 98ff13b83f feat(birdstar): render Birdex species collections (kind 12473) 2026-06-07 22:18:29 +02:00
Crowdin Bot cdb76e448c New Crowdin translations by GitHub Action 2026-06-07 12:21:45 +00:00
Vitor PamplonaandGitHub 856ff3293b Merge pull request #3141 from nrobi144/fix/nip46-bunker-double-resume
fix(quartz): NIP-46 bunker double-resume + retry id-reuse races
2026-06-07 08:19:58 -04:00
nrobi144 8b9875d9cc fix(quartz): NIP-46 bunker double-resume + retry id-reuse races
Two correctness bugs in `RemoteSignerManager` (NIP-46) and its NIP-55
sibling `IntentRequestManager`:

1. **Double-resume crash** — `awaitingRequests.get(id)?.resume(value)`
   was non-atomic. Multi-relay delivery, bunker echo/retry, and
   late-after-timeout responses could call `resume` twice for the same
   continuation, throwing `IllegalStateException: Already resumed` on a
   `Dispatchers.Default` worker.
2. **Retry id-reuse → wrong data** (NIP-46 only) —
   `launchWaitAndParse` built the request and event once, then re-used
   the same `request.id` across retry attempts. A late response from
   attempt N could resume attempt N+1's continuation with stale data.

Replace the cached-`Continuation` map with the in-house Channel-per-request
correlation pattern already used in `quartz/.../accessories/NostrClientPublishExt.kt`
(`LargeCache<id, Channel<Response>(capacity=1)>` + atomic `remove` +
`trySend` + `withTimeoutOrNull { receive() }`). Each retry attempt now
builds a fresh request with a new id; the builder is still called only
once. `finally`-block cleanup removes the cache entry on every path,
incidentally fixing a slow leak on the success path.

Adds three regression tests:
- duplicate responses → no crash + single resume (fails on \`main\`
  with \`IllegalStateException\`)
- late response after timeout → silently discarded
- late attempt-1 response does not corrupt attempt-2 result (fails on
  \`main\`: the two attempts share an id)

Design + review notes: \`quartz/plans/2026-06-03-fix-nip46-bunker-double-resume-plan.md\`
2026-06-07 14:36:29 +03:00
Claude 27d03b4a54 fix: share poll option text between zap and non-zap polls
On the short new note screen, polls and zap polls kept their option text in
two separate maps (`pollOptions` vs `zapPollOptions`). Switching the poll type
only toggled the `wantsPoll`/`wantsZapPoll` flags, so the text typed for one
type was hidden and never reused for the other.

Make both poll types read and write the same `pollOptions` text fields, so
switching between zap and non-zap polls preserves whatever the user already
typed. The zap-specific settings (deadline, vote value range, consensus
threshold) remain independent.
2026-06-06 22:46:36 +00:00
David KasparandGitHub 09a166c7c0 Merge pull request #3140 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-06 16:20:18 +02:00
Crowdin Bot 21f5acec51 New Crowdin translations by GitHub Action 2026-06-06 14:19:43 +00:00
David KasparandGitHub f7945d7862 Merge pull request #3139 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-06 16:18:23 +02:00
davotoula a4f883eaa8 fix(scheduling): route always-on prompt to Notification Settings 2026-06-06 16:14:09 +02:00
Crowdin Bot 68d2d68edf New Crowdin translations by GitHub Action 2026-06-06 13:15:23 +00:00
Vitor PamplonaandGitHub e2b1b03d84 Merge pull request #3137 from davotoula/feat/agora-fundraiser-kind-33863
Render Agora fundraiser campaigns (kind 33863)
2026-06-06 09:13:43 -04:00
Vitor PamplonaandGitHub 8379134179 Merge pull request #3138 from nrobi144/fix/desktop-feed-reply-context
feat(desktop): reply context in feeds + profile Replies tab
2026-06-06 09:13:19 -04:00
nrobi144andClaude Opus 4.7 3a21abb0f7 fix(desktop): load parent-author metadata and make parent embed clickable
Two follow-ups to the reply-context PR.

1) Parent-author metadata wasn't reaching the embed / "Replying to @X"
   label, so they rendered the truncated hex indefinitely.
   - FeedScreen.missingNoteIds: also fetch the immediate parent EVENT
     for visible replies (was only repost originals + bech32 quotes).
   - FeedScreen.missingAuthorPubkeys: also include the parent AUTHOR
     hex, extracted DIRECTLY from each reply's tags
     (CommentEvent.replyAuthor() for NIP-22; taggedUsers().lastOrNull()
     for NIP-10) so the kind 0 request fires even before the parent
     event itself arrives in cache.
   - NoteCard.QuotedNoteEmbed + FeedScreen.rememberReplyContext:
     produceState observation of the parent author's
     metadata().flow so the embed and label recompose to display name
     + avatar once kind 0 lands.

2) Embedded parent appeared clickable but did nothing — the outer
   NoteCard's OutlinedCard onClick was catching the click and
   re-navigating to the reply's own thread (the current view). Make
   the wrapping Box itself clickable, route it to the parent thread,
   and drop the inner OutlinedCard's onClick so there's a single
   explicit click surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:11 +03:00
nrobi144andClaude Opus 4.7 0c681ecd9f fix(desktop): restore inter-word spaces in rich text with mentions/hashtags
RichTextParser splits each paragraph on ' ' so every segment is one
space-delimited token; the source space lives BETWEEN segments, not
within them. When a paragraph contains only RegularTextSegments the
parser collapses them back to one segment rejoined with " ". When the
paragraph also contains a mention/hashtag/link the segments stay split
and DesktopRichTextViewer rendered them in a FlowRow with no horizontal
gap — every word glued together.

Set the FlowRow's horizontalArrangement to Arrangement.spacedBy(4.dp)
(the same constant the file already uses for ImageGalleryParagraph),
preserving the RTL alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:11 +03:00