The Crowdin-synced values-fr-rCA/strings.xml had unescaped apostrophes inside the
CDATA sections of block_hide_user and report_dialog_block_hide_user_btn, which
aapt2 rejects ("Invalid unicode escape sequence"), breaking resource compilation.
Escape them as \' to match the known-good values-fr translations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
The lightning, nutzap, and onchain activity cards embed the zapped post via
RenderZappedPost with makeItShort = true. The 2-line compact preview, however,
only triggered when the logged-in user authored the post
(makeItShort && isLoggedUser(author)). When the user is merely a zap-split
beneficiary of someone else's post, that check failed and the post rendered in
full instead of the intended compact preview.
Gate the short preview on the boosted-note flag as well, so any post embedded in
one of these activity cards (the only makeItShort callers that pass
isBoostedNote = true) is always shown as a 2-line preview, regardless of author.
Other makeItShort callers (Report, community post approval, attestation, reply
composition, compose screens) all use isQuotedNote instead, so their behavior is
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
The embedded in-app browser and napplet/nSite surfaces rendered web content in
the device theme, ignoring the app's DARK/LIGHT preference — a site with dark
support stayed light when the app was dark (and vice versa). The full-screen
activities had the same latent gap (they followed the device, not the app).
Root cause: WebView's dark decision (prefers-color-scheme via algorithmic
darkening) reads the context's THEME (?android:attr/isLightTheme), not just the
Configuration uiMode. The off-window SurfaceControlViewHost surface context
carries neither the host window's theme nor its night mode, so the renderer came
up light. The old applyNightMode() used UiModeManager.setNightMode — a
permission-gated no-op — so the theme never reached the WebView at all.
Fix: build every embed/host WebView from nightThemedContext() — a
ContextThemeWrapper over a forced-night/day Configuration with a DayNight theme,
so the theme's isLightTheme resolves from the app's resolved theme. Shared in
EmbedWebViewTheme.kt; used by NappletBrowserService, NappletHostService,
NappletBrowserActivity, and NappletHostActivity. Removed the dead applyNightMode
no-op from all four. (Verified on device with a throwaway SurfaceControlViewHost
repro: config-only context does NOT work; setForceDark is a no-op at targetSdk
37; setApplicationNightMode does nothing; the DayNight ContextThemeWrapper is
what flips the renderer, even across the cross-process embedded surface.)
Device-verified: all four surfaces follow the app theme even when it differs
from the device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NIP-34 pull requests (kind 1618) and pull-request updates (kind 1619)
previously had no renderer and fell through to the plain text-note
branch, so a PR notification opened onto an unstyled markdown blob with
none of its structured data shown. Add dedicated cards that reuse the
existing patch/issue card vocabulary (bordered container, type chip,
status pill, embedded repository header) and surface the PR-specific
metadata the event carries:
- Pull Request card: "Pull Request" chip with a merge glyph, status
pill, subject title, branch name, current commit, merge base, and
clone-URL download rows.
- PR Update card: "PR Update" chip, repository header, new commit /
merge base, clone URLs, and an explanatory line (updates carry no
body content).
While here, modernize the existing cards consistently:
- Factor the shared markdown body, metadata row, and subject title into
reusable composables (GitMarkdownBody / GitMetaRow / GitSubjectTitle).
- Show the issue subject as a proper title. The old code cast the event
to TextNoteEvent to read the subject, which always returned null
(GitIssueEvent is not a TextNoteEvent), so issue subjects were never
displayed; read it from GitIssueEvent.subject() instead.
- Render the patch commit as an iconed metadata row.
Wire the two new kinds into NoteCompose and the thread detail view, add
the CallMerge / Commit / AltRoute Material symbols (font subset
regenerated), and add the new string resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
Two French-Canadian CDATA string resources carried a raw apostrophe
(`l'utilisateur`), which aapt2 rejects with "Invalid unicode escape
sequence", breaking every Android resource merge (merge*Resources) and
therefore the whole Android build. Escape them as `\'` like the other
apostrophes in the file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
New pull request events (GitPullRequestEvent, kind 1618) and pull
request updates (GitPullRequestUpdateEvent, kind 1619) were never wired
into the notification pipeline, which only handled patches (1617) and
issues (1621). As a result repo maintainers received no notification
when a PR was opened against their repository, even though the PR event
p-tags the repo owner.
Add both kinds in all four places that gate notifications:
- FilterNotificationsToPubkey: request the kinds from relays so the
notification subscription actually pulls PR events.
- NotificationFeedFilter.NOTIFICATION_KINDS: let cached PR events through
to the in-app Notifications tab.
- NotificationFeedFilter.tagsAnEventByUser: treat PR/PR-update as
notifiable (mirrors the existing patch/issue handling).
- EventNotificationConsumer: dispatch PR/PR-update via notifyMention so
push notifications fire.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
Opening an embedded page full-screen and returning left every embedded WebView
in the :napplet process broken: dead DNS (ERR_NAME_NOT_RESOLVED), DOM reads
returning empty (a field that visibly shows text reports value==""), dead
text-selection highlight, and broken IME (caret stuck at 0, can't delete).
Two process-global defects in the full-screen hosts were corrupting the shared
WebView state the embedded surfaces rely on:
- pauseTimers()/resumeTimers() are PROCESS-GLOBAL — they pause/resume JS,
layout and parsing timers for every WebView in the process. The full-screen
activities (and the napplet embed pause/resume path) called them on their own
lifecycle, so returning from full-screen froze the embedded surfaces, which
have no resume of their own. Replaced with per-WebView onPause()/onResume()
(which pause only that surface's JS/DOM — still satisfies the napplet
background-security goal). No process-global timer calls remain.
- WebView.destroy() was called while the WebView was still attached to the
window, which corrupts the shared multiprocess renderer. Detach (removeView)
and stopLoading() before destroy() in both full-screen activities.
This also resolves the long-standing "selection highlight dead after a
full-screen excursion" blocker — same root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes parity #5. `fieldExtend` kept a pure word-snap, so the in-field
start/end handles couldn't be fine-tuned to a single character. Now it keeps
per-drag state (reset on a >250ms gap or edge switch) and matches native
`Editor` word-selection drags: the gesture baselines at the current selection
edge, sweeping past that word's far boundary snaps to the next whole word
(never stopping mid-gap), and moving within / back from the furthest-reached
word gives character precision. Symmetric for both handles. Page-text extend
stays character-granular.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds out host-drawn text selection for embedded napplet/nsite/browser
surfaces toward native parity, and fixes the bugs found while exercising it.
- Magnifier loupe (#4): EmbeddedMagnifier + provider-side pixel capture
(EmbeddedMagnifierProbe) shipped over IPC for both embed paths; the
caret/selection handles drive it via OnMagnify.
- SelectionUiState: single source of truth for the overlay show/hide rules
(insertion caret / in-field range / page-text range + dragging/scrolling).
- EmbeddedSelectionDrag: suspends the nav drawer's edge swipe while a handle
is dragged (auto-scroll #9).
Bug fixes:
- No more overlay blink on word-select: the shim's selection-reveal scrolls
(a textarea auto-scrolling to show a forming/re-asserted range) no longer
trip the hide-on-scroll path, and the hide self-heals instead of being
re-armed indefinitely.
- RemoteImeView debounces the range-lost signal so a transient collapse that
gets re-asserted doesn't flicker the handles/toolbar.
- Focusing a field clears any page-text selection (shim + host), so the stale
page handles/Copy bar no longer linger above — and stop stealing drags from —
the field overlays; also cancels any in-flight scroll-hide on focus.
- Caret insertion-handle drag now actually moves the caret: read the pointer
delta with positionChangeIgnoreConsumed() before consuming, so the value
isn't zeroed by our own consume (or the sandbox surface consuming the move).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/ime-test/index.html is a single-page input+textarea harness with an
on-page log timestamping focus/selection/input/composition events, paint
latency, long-tasks, and main-thread blocks — the instrumentation used to pin
the erase / caret-jump / first-letter-freeze bugs and what we'll use to profile
the magnifier. README documents serving it (python http.server on 8765,
10.0.2.2 for emulator / adb reverse for USB) and opening it as an embedded tab
via the in-app browser address bar. Plan gets a matching "How to test" section.
Dev tool only — nothing under tools/ ships, so the [ImeDiag] strings stay out of src/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inventory native Android text-field features (insertion/selection handles,
floating toolbar, magnifier, smart selection, etc.) with activation rules and
our current coverage; prioritize the magnifier and a centralized SelectionUiState.
Document the open full-screen round-trip bug that kills highlight paint across
all :napplet embeds, what was already ruled out, and the next hypotheses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Embedded WebView surfaces (:napplet process, SurfaceControlViewHost) can't
host the soft keyboard or present Chrome's own selection UI, so editing and
selection are relayed to the main process. This lands the working set of that
relay:
- shim.js: fix React-controlled input erase by writing through the native
HTMLInputElement/HTMLTextAreaElement value setter (so React's value tracker
stays in sync); make the host authoritative for selection re-assert (the
editable selectionchange handler only mirrors); report field/caret geometry
and page-text selection geometry; add pageExtend + caret coords (border-width
corrected) for drag-to-extend and the insertion handle.
- RemoteImeView: land caret where tapped on focus (requestFocus before applying
remote state); host-authoritative selection re-assert within a time window;
setText only when text actually changed; wire copy/cut/paste/select-all and
edit callbacks.
- EmbeddedTabLayer: stop resizing the surface on IME show (removes the ~1s
first-letter freeze); draw the selection overlay — toolbar, teardrop
selection handles, and the insertion (cursor) handle.
- EmbeddedImeBridge / Embedded{Napplet,Browser}Controller: carry selection +
caret geometry and the page-selection event across the Messenger channel.
Known limitation (not fixed here): after a field's page is opened in its own
full-screen activity and the user returns, the selection-highlight paint stays
off across all embedded surfaces. DOM selection, the toolbar, and copy still
work — only the native highlight is gone. This is a WebView/Chromium behavior
in off-window surfaces and is not reachable from the app layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The global Apps feed (NIP-82 software apps) was sending a kinds-only REQ
filter with limit=200. zapstore's relay scores each REQ filter for
specificity and rejects anything that scores below 3, treating a kinds-only
filter with a limit >= 100 as "too vague" — so the whole subscription was
refused.
- Drop the global Apps filter limit from 200 to 99 so a kinds-only filter
clears the bar (kinds +1, limit < 100 +2 = 3).
- Always include wss://relay.zapstore.dev in the global Apps feed relays,
since it indexes the full app catalog, unless the user has added it to
their blocked relay list (NIP-51 kind 10006).
- Re-evaluate the subscription when the blocked relay list changes so
blocking/unblocking zapstore takes effect without an app restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US2aK3igzfeo4xtcvg3mKc
innerLoadCurrentAccountFromEncryptedStorage awaited ~27 parallel parse
Deferreds directly inside the single 70-argument AccountSettings(...)
constructor expression. Each await is a suspension point, so the coroutine
state machine had to spill and restore the entire partially-evaluated
operand stack (dozens of already-built MutableStateFlow args) at every one
of them. That inflated the generated invokeSuspend method past the JVM
per-method limit:
Method exceeds compiler instruction limit: 57866 in
LocalPreferences$innerLoadCurrentAccountFromEncryptedStorage$result$1.invokeSuspend
Resolve every Deferred into a local val first, then build AccountSettings
as a single straight-line, suspension-free expression. Each await now sits
at a statement boundary with a near-empty operand stack, so only a small,
linear set of locals is spilled. Parallel parsing behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6VdHHFRNYXKXFcW7j5sF7
Android's AAPT2 resource processor only recognizes a small set of valid
escape sequences: \, \', ", \n, \t, \r, \@, \?. All other \X sequences
(backslash-space, \-, \., \(, \), $, etc.) render as a literal backslash
followed by the character in the UI.
These were introduced by the batch AI translation. Remove the backslash
before every character that does not need escaping across 38 locale files.
The appended translation blocks from the previous batch-translation session
overlapped with existing Crowdin translations already present in the files,
causing 31 duplicate keys in values-hu-rHU and 3 in values-sl-rSI.
Keep the first (original Crowdin) occurrence of each key.
AGP 9.2.1 treats resources defined in both an app module and a library module
with the same key (qualifiers="") as an error in non-debug builds.
`browser_address_hint`, `browser_console_title_short`, `browser_console_title`,
`browser_console_clear`, and `napplet_untitled` were defined in both
`:amethyst/values/strings.xml` and `:nappletHost/values/strings.xml`.
Both `:amethyst` and `:nappletHost` depend on `:commons`, so the canonical
home for these shared strings is `commons/src/androidMain/res/values/strings.xml`.
Update callers in both modules to use `com.vitorpamplona.amethyst.commons.R as
CommonsR`. Locale translations in amethyst's `values-*/` directories remain as
Android resource overlays (app module overrides library module at merge time).
Fixes: Found item String/browser_console_clear more than one time (packageFdroidBenchmarkResources)
The previous commit incorrectly removed browser_address_hint,
browser_console_clear, browser_console_title, browser_console_title_short,
and napplet_untitled from values/strings.xml. These are needed for
amethyst's own browser and napplet screens — both amethyst and nappletHost
use them independently, and Android allows app-level strings to coexist
with same-named library strings (app wins).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfm5Lf3FfwDgPn5oiMVqUo
Remove 2959 strings from values-hu/strings.xml that were fully duplicated
in values-hu-rHU/strings.xml (added by prior commit), causing aapt2 to
error on `browser` and all other keys for the hu-HU build target.
Remove browser_console_clear, browser_address_hint, browser_console_title,
browser_console_title_short, and napplet_untitled from the app's default
values/strings.xml — these are already defined in the :nappletHost library
module, and having them in both causes aapt2 duplicate-key errors when the
library resources are merged into the app. Locale translations in
values-*/strings.xml continue to override the library defaults at runtime.
Fixes CI failures reported in PR #3371.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfm5Lf3FfwDgPn5oiMVqUo
266 string replacements across 41 locale files to reduce UI overflow risk.
Strings shortened to fit labels and menu items; technical terms (relay,
DM, NIP, Tor, pubkey, Cashu) kept as-is per project convention.
Languages updated: ar, cs, de, el, es, fa, fi, fr, hi, hu, in, it, nl,
pl, pt-BR, pt-PT, ru, sl, sv, sw, ta, th, uk, uz + regional siblings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfm5Lf3FfwDgPn5oiMVqUo
- Escape unescaped `?` in calendar_rsvp_maybe_prefixed (it, ru, ru-rRU,
ru-rUA) — Android treats `?` at string start as a theme-attr reference
- Escape unescaped `@` in my_name (fi, tr, tr-rTR) and
quick_action_copy_user_id (zh) — same issue with `@` references
- Convert <plurals> to <string> for accounts_found, num_selected,
follow_accounts in ru/ru-rRU/ru-rUA — English source uses <string>,
so type-mismatched <plurals> were silently removed by AAPT2
- Fix orphaned key in el-rGR: error_parsing_json_from_lightning_
address_check_the_user_s_lightning_address_with_user renamed to
correct _setup_with_user suffix matching the English source
All locale files now build without errors or warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfm5Lf3FfwDgPn5oiMVqUo
Fill all missing strings in Arabic (ar-rSA), Greek (el-rGR), Persian
(fa, fa-rIR), Finnish (fi-rFI), Indonesian (in, in-rID), Italian
(it-rIT), Japanese (ja, ja-rJP), Korean (ko-rKR), Russian (ru, ru-rRU,
ru-rUA), Thai (th, th-rTH), Turkish (tr, tr-rTR), Ukrainian (uk,
uk-rUA), Vietnamese (vi-rVN), and Traditional Chinese Taiwan (zh-rTW).
Most locale files now have all 2961 strings matching the English source.
Missing strings fall back to English per Android resource resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfm5Lf3FfwDgPn5oiMVqUo
The single-zap/single-nutzap branch of MultiSetCompose dropped the bare
RenderLnZap/RenderNutzap activity card straight into the feed, with no
author column on the left, so it didn't read like the surrounding notes.
Reuse NoteCompose on the zap-receipt note instead: it wraps the same zap
card as the note body and gives it the standard note chrome — author
picture on the left, username header, reactions row. Padding is handed to
NoteCompose for this branch so it isn't doubled against the card column.
NoteCompose's author column previously resolved baseNote.author, which for
a kind-9735 zap receipt is the recipient's lightning provider (the signer),
not the zapper. Resolve the sender from the embedded zap request the same
way the thread's master note already does, so the picture on the left is
the actual zapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cg8mHZDCfGmGAVDaeJxPFz
The outlined service gem was authored at ~half the 512x512 viewport size,
so it rendered noticeably smaller (both width and height) than every other
notification small icon, which fill the viewport. Wrap the path in a group
that scales it ~1.8x about its centre, leaving a margin so the stroke stays
within bounds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVA2C5FyYWSmU7EzdFaAD2
NutzapEvent was missing from the textNoteCards exclusion filter in
convertToCard, so every nutzap was both grouped into the MultiSetCard /
NutzapUserSetCard path AND fell through to a standalone NoteCard, which
renders kind 9321 as a big RenderNutzap card. The result was a duplicate:
the same nutzap appeared once in the grouped card and once as an
individual note — the way a reply would.
LnZapEvent is already excluded for exactly this reason; nutzaps were
simply overlooked when the nutzap grouping was added. Exclude NutzapEvent
too so it only renders through the grouped path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8ZruubvykNhfmrJXJA8s7
A MultiSetCard bundles many notifications on a post into one compact stream
of icons, which is great when there are several. But a freshly-arrived zap
is usually alone in its own card (the additive feed path builds a
single-item MultiSetCard, and only a full rebuild groups same-post
notifications together), so it rendered as one tiny gallery icon.
When a MultiSetCard carries a single lightning zap or nutzap and nothing
else, render the existing large activity card (RenderLnZap / RenderNutzap)
— the same big, gradient "appreciation" display already used for onchain
zaps and the thread view — instead of the one-icon gallery. As soon as a
rebuild groups several notifications onto the same post (size > 1), it
falls back to the compact gallery as before.
This is purely a rendering decision in MultiSetCompose; the card-building
and grouping logic is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8ZruubvykNhfmrJXJA8s7
The cashu outline icon next to author avatars in the nutzap galleries
rendered in the default content color, while the lightning equivalent
(ZappedIcon) uses BitcoinOrange. Tint the cashu mark BitcoinOrange in the
reaction-row gallery (NutzapGallery), the notification multi-set gallery
(MultiSetCompose), and the notification user-set card (NutzapUserSetCompose)
so nutzaps read as value transfers consistent with lightning zaps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
The cashew + pixel-shades mark was visually weighted to the right of the
24x24 viewport. Shift every x-coordinate in both paths (the stroked cashew
body outline and the filled pixel sunglasses) by -2.4 units so the drawing
sits balanced within the icon bounds. Shape is unchanged; only position.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu