- Reply: keep the arrow at its size but move it to the bottom-left and put
the Amethyst gem in the top-right corner.
- Zap: scale the bolt up to fill the canvas and knock the gem out of its
center (inverted), so the logo reads in front of the bolt.
- Reaction: grow the heart's knocked-out gem ~10% and nudge it toward the
bottom of the heart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
- Repost: scale the repeat-arrow glyph up ~15% so it fills the canvas like
the original, with the centered gem enlarged to match.
- Badge: replace the medal ring + inner-circle-with-logo with a solid disc
that has the Amethyst gem knocked straight out of it (inverted), sized to
fill the disc. Ribbon tails kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
Bumps the gem so the brand mark reads bigger inside each glyph: the heart
knockout (6.7 -> 8.2), the repost loop center (5.4 -> 6.9) and between the
code brackets (6.2 -> 7.6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
The inverted DM bubble read smaller than the original because its body
stopped short and the tail was a small stub. Grow the bubble to the same
footprint as the original glyph, scale the knocked-out gem and the three
chat lines up to match, and give it a full corner tail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
The inverted reaction icon carried a leftover inner-heart cutout from the
previous outlined design plus a duplicated gem subpath, so the gem rendered
as a solid island inside a heart-shaped hole instead of a clean knockout.
Regenerated as a solid heart with the Amethyst gem punched straight out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
Per design feedback, the direct-message and reaction icons now invert:
instead of an outlined shape with a filled gem inside, the shape is solid
and the Amethyst gem is punched out of it (evenOdd knockout).
- Direct message: solid speech bubble; the gem is knocked out full-height
on the left, with three chat lines knocked out on the right to read
clearly as a conversation. The inner rectangle cutout is gone.
- Reaction: solid heart with the gem knocked out of its center.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
Every per-category status-bar icon now carries the Amethyst crystal so a
notification reads as an *Amethyst* reply/zap/mention at a glance, not a
generic Material glyph. Android renders small icons as a flat alpha-only
silhouette, so the gem is composed as a spatially distinct mark rather than
layered behind the glyph:
- Direct message, mention, repost, reaction, code and badge: gem sits in
the natural cavity of the glyph (bubble, @ ring, repeat loop, heart,
brackets, medal center).
- Media: gem is the sun rising over the hills.
- Article: gem is the drop-cap starting the first line.
- Chess: switched to the outlined knight from the app's icon set
(MaterialSymbols.ChessKnight) with a small brand gem.
- Reply and zap: open glyphs that can't hold a centered gem keep a small
gem corner mark.
Action buttons stay unbranded — a gem on a "Reply"/"Mark read" button is
noise, not identity — so those now use dedicated plain glyphs
(ic_action_reply, ic_action_mark_read).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXfHYcViokBQRtqCW9vLQi
1. `title="$(git log -1 … | cut …)"` aborts the whole script under
`set -euo pipefail` when the worktree has no commits
2. `base_branch` only fell back to `main`
This reverts commit 76b3323583. The probes did their job.
Verified on a Pixel 9a / Android 17 against beam.mapboss.co.th — a
third-party LL-HLS packager, unrelated to zap-stream-core, so this is not
tuned to one vendor's output:
PARSE STRIPPED 2360B of 3426B chunklist_0_video_..._llhls.m3u8
PARSE STRIPPED 3002B of 4068B chunklist_2_audio_..._llhls.m3u8
26 reloads of each rendition over 50s of continuous playback, ~70% of every
playlist removed, and zero DataSpec.subrange occurrences — the crash that
androidx/media#3350 tracks.
Both branches confirmed. Eight ordinary HLS streams logged "no LL tags" and
took the identity path untouched, so the stripper is inert on normal
playlists. Routing was correct on ~15 distinct real URLs:
`hls=true -> HlsMediaSource` for application/x-mpegURL, and
`hls=false -> ProgressiveMediaSource` for video/mp4 — which is the
isHlsMediaItem coverage that unit tests cannot provide.
The 18 ERROR states in the same capture were all unrelated: dead URLs
(403/404/502/504), four UnknownHostException from a network drop, and two
UnrecognizedInputFormatException from a youtu.be link on the progressive
path, which ExoPlayer has never been able to play directly.
The pure predicates keep their unit tests, which are the durable guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m
DO NOT MERGE — revert this commit before the branch goes anywhere.
PLAYBACK_DIAG_TAG logs at DEBUG and Amethyst.onCreate sets
`Log.minLevel = if (BuildConfig.DEBUG) DEBUG else ERROR`, so nothing on that
tag survives in the benchmark build. Three logcat captures during this work
had zero PlaybackDiag lines for exactly that reason, which left the routing
decision unverifiable — the fix was only ever confirmed indirectly, by the
crash no longer happening.
Adds one ERROR-level tag, HlsVerify, on the three paths unit tests cannot
reach:
- CustomMediaSourceFactory.createMediaSource — the routing decision, so
isHlsMediaItem's verdict and the chosen factory are visible. This is the
predicate with no test coverage, since android.net.Uri stubs to null
under unitTests.isReturnDefaultValues.
- LowLatencyStrippingParser.parse — whether the stripper actually engaged
and how many bytes it removed, so a silent stop-stripping regression is
visible rather than inferred.
- HlsLivenessRecorder.maybeRecord — the other consumer of the shared
predicate, unreachable without a Player.
The pure predicates underneath are already unit-tested and remain the durable
regression guard; this is only for confirming the wiring on a device.
Capture with `adb logcat -s HlsVerify:E`. Remove with `grep -rn HlsVerify` or
by reverting this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m
- retire the `.m3u8` substring predicate across the UI
- cover the preload hint and the byte/charset layer
- share one HLS predicate with the liveness recorder
- unify the HLS predicate and track the media3 bug upstream
media3 crashes fatally on an LL-HLS playlist whose parts are byte ranges —
what zap-stream-core emits (`#EXT-X-PART:...,BYTERANGE="359712@0"`).
Reproduced on media3 1.10.1, Pixel 9a / Android 17, ~0.6s after
ExoPlayerImpl.Init:
IllegalArgumentException
at DataSpec.<init> // checkArgument(length > 0 || length == LENGTH_UNSET)
at DataSpec.subrange
at HlsMediaChunk.feedDataToExtractor
at HlsMediaChunk.loadMedia
Use commons showAmount() for the Zaps tab total and each zapper row instead of
raw sats, matching Amethyst Android and other Nostr clients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter (your own posts that mention this profile) matches Android's
UserProfileMutualFeedFilter, but Android's user-facing string is 'Yours'
(<string name="mutual">Yours</string>). The internal filter name stays
DesktopMutualFeedFilter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Header actions were direct children of a SpaceBetween Row, so the overflow
(⋮) menu floated to the center. Group Edit/Follow/⋮ in their own Row and put
the overflow last (after the primary Follow action), per convention. Extract
the Follow button into FollowButtonRegion for the reorder.
2. Followers/Following rows now subscribe to kind-0 metadata (batched, capped)
and observe each user's metadata flow so names + avatars render instead of
raw npubs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wire real PrivateZapCache(signer) into DesktopIAccount (was a null stub)
- Subscribe to kind 9735 receipts p-tagging the profile; aggregate sats per
zapper (private zaps decrypt only on your own profile, else fall back to the
public zap-request pubkey)
- New ProfileZapRow; tab header shows the total sats
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Message opens the Messages column + 1:1 room via a DesktopDmRoute bridge
(observed by both deck and single-pane layouts)
- Add to list: pack picker from LocalFollowPacksState, appends via
FollowListEvent.add + broadcast
- Share copies a nostr: profile link to the clipboard
- Actions live in the existing writeable/other-profile overflow menu, so
self/read-only are already excluded
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Scrollable tab row; Followers/Following render UserSearchCard from live
contact-list subscriptions; Relays render a new RelayRowCard from kind 10002;
Bookmarks (kind 10003 → DesktopBookmarkFeedFilter) and Mutual (new
DesktopMutualFeedFilter) render FeedNoteCard
- Respects the shared mute/block hidden-set via DesktopFeedViewModel
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Render kind-0 banner image in the profile card header
- Render bio as rich text (mentions/hashtags/links) via DesktopRichText
- Add CLINK offer (noffer) field to Edit Profile + shared EditProfileFields
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address composer feedback:
- The annotation field is now the short-note composer's rich MessageField
(@-mention + custom-emoji autocomplete, inline previews). On publish it becomes
the highlight's `comment` tag, with the mentions (`p`), emoji, URLs (`r`),
hashtags and quotes it references emitted as their own tags — via
NewMessageTagger + a new tag-builder initializer on HighlightEvent.build().
- A nostr source now renders as a reply-style preview card (NoteCompose, quoted
style) instead of showing a URL field; the URL field appears only for web
sources. The source note is resolved from the a/e coordinate carried in the
route.
Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
- Add a "Highlight" action to the note action menu (3-dot menu + chat long-press
sheet). It opens the NIP-84 composer pre-tagged with the note as source — an
`a` reference for an addressable article, else an `e` reference, plus the
author `p` — with the passage left for the user to type/paste. Prose kinds
only (text notes, long-form), and never a private rumor (a public highlight
would leak an e-tag of the unsigned rumor). This is the in-app entry point for
"post with the source as an event".
- Remove the live preview from the New Highlight screen per review.
- Add two real-world regression tests for the shared-highlight parser: Chrome
"copy link to highlight" with a prefix/suffix fragment (incl. an encoded
hyphen inside the prefix) and a long start-only fragment with encoded commas.
Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
Redesign the New Highlight composer as a pull-quote you craft: a rounded tonal
hero card with a quotation-mark watermark, a highlighter-amber accent bar, and
the passage in large type, plus a live highlighter-pen preview (reusing the feed's
HighlightedQuote) and icon-led source/note fields.
Also extend HighlightEvent.create()/build() to emit a/e/p tags, so the builder
now covers every NIP-84 source — a web page (r), a nostr article (a), a nostr
note (e), each with optional author attribution (p) and context — not just web
highlights. Route.NewHighlight and the composer ViewModel carry the nostr source
(address/event/author) and context through so a future in-app "highlight this
passage" action can open the composer for a nostr article/note.
Covered by a new builder test for the a/e/p tags; verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
`trimUrlEnd` strips a wrapping `)` off the URL token, but the `(` it opened
stayed at the tail of the passage, so sharing
`See this quote (https://example.com/article)` published a highlight whose
content read `See this quote (`.
Trim an unbalanced bracket left at the end of the passage after the URL token
is removed. Only an unbalanced one at the very end goes, so `He said (see
below) https://…` keeps its matched pair, and `.../Mercury_(planet)` — where
nothing was trimmed off the URL — is untouched on both sides.
The existing paren tests only asserted `result.url`, which is why this slipped
through; the new cases assert `result.quote` too, including a regression guard
for the slug-paren direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a browsable Highlights feed (NIP-84 kind:9802) modeled file-for-file on
the Git Repositories feed pattern, since highlights are announcement-like
regular events with the same follow-list top-nav filtering.
New feed stack under ui/screen/loggedIn/highlights:
- dal/HighlightsFeedFilter — AdditiveFeedFilter scanning LocalCache.notes for
kind 9802 (regular events, so notes rather than addressables), gated by the
selected top-nav follow list via FilterByListParams.
- datasource/ assembler + compose subscription + PerUserAndFollowList sub-
assembler + makeHighlightsFilter dispatcher, with per-relay subassemblies for
follows / authors / muted / global / hashtag / geohash. Communities aren't a
highlight concept, so those sets fall through and aren't offered.
- HighlightsScreen (DisappearingScaffold + shared RenderFeedContentState; the
existing RenderHighlight card handles rendering) with a HighlightsTopBar
FeedFilterSpinner and a NewHighlightButton FAB into Route.NewHighlight.
Wiring into the shared plumbing mirrors gitRepositories exactly: Route.Highlights
+ AppNavigation, NavBarItem enum/catalog/drawer lists (FormatQuote icon, already
in the subset font), AccountFeedContentStates (feed state + update/delete/trim),
RelaySubscriptionsCoordinator, TopNavFilterState.highlightsRoutes,
AccountSettings.defaultHighlightsFollowList + changer, Account live follow-list
flows, LocalPreferences persistence, ScrollStateKeys, and the bottom-bar
preloader. Verified with :amethyst:compileFdroidDebugKotlin.
The URL trailing-punctuation trim now leaves a closing bracket in place when
it is balanced within the token, so a shared Wikipedia link like
`.../wiki/Mercury_(planet)` keeps its `)` while a wrapping `(https://…)` still
loses the paren the surrounding prose added.
Adds a "New Highlight" share target and composer so a user can select text in
a browser, hit Share → Amethyst, and publish it as a kind:9802 highlight.
- Manifest: a text/plain-only ShareAsHighlightAlias activity-alias, matched at
runtime by ShareIntentRouting.isShareAsHighlight (mirrors the Send-as-DM
alias pattern).
- AppNavigation: both the launch-intent and warm onNewIntent parse blocks now
branch on the highlight alias, run the shared text through
SharedHighlightParser, and open Route.NewHighlight pre-split into
quote/url/prefix/suffix.
- NewHighlightScreen + NewHighlightPostViewModel: a trimmed composer (passage,
source URL, optional note — no polls/zaps/media/scheduling) that publishes via
account.signAndComputeBroadcast(HighlightEvent.build(...)).
Also adds HighlightEvent.build(), the unsigned EventTemplate counterpart of
create(), for the sign-and-broadcast pipeline. Verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
Adds a shared-highlight parsing layer under nip84Highlights/parse that turns
the free-form text a browser hands Amethyst on "Share selection" into the
pieces of a kind:9802 highlight:
- SharedHighlightParser normalises selection-only, selection+URL, URL-only and
"link to highlight" (#:~:text= fragment) shares into a SharedHighlight.
- TextFragmentParser decodes/strips WICG text-fragment directives (prefix,
start, end, suffix), leaving literal '+' verbatim.
- UrlTrackerCleaner strips utm_*/fbclid/etc. from the source URL per NIP-84's
"clean the URL from trackers" guidance, preserving path and fragment.
Also adds a HighlightEvent.create() builder overload that assembles the r,
textquoteselector, context and comment tags from parsed data, centralising
what the desktop publish action does by hand.
Covered by commonTest suites for each piece plus a builder round-trip.
A kind:9802 highlight that mentions users before its author rendered the
wrong name and a stray "published by …" caption.
- HighlightEvent.author() took the first p tag regardless of role, so a
highlight with leading "mention" p tags was attributed to a mention
instead of the "author"-marked one. Prefer the NIP-84 author marker,
falling back to the first p tag for highlights (including Amethyst's own)
that omit the marker.
- DisplayEntryForNote used the source note's title/subject/alt as a caption.
For a kind-1 note there is no title/subject, so it fell to the NIP-31 alt
tag — which clients like Jumble fill with a generic "This event was
published by https://jumble.imwald.eu." line. Drop alt from the lookup and
name the source by its event kind instead, kept clickable to the note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJh3QYonEa9hw6mJcnrmve
The NIP-84 highlight comment was passed to TranslatableRichTextViewer with
an empty tag list, so custom emoji (:shortcode:) never resolved against the
event's `emoji` tags and rendered as raw text.
Thread the highlight event's tags through to the comment's rich-text viewer
so the emoji map is built from them, matching how regular text notes render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyyodHWxSHeiY6pndPxGSb