Commit Graph

13530 Commits

Author SHA1 Message Date
Vitor Pamplona 78be534df5 nests room: stable reactions overlay + repeatable kind-7 + speaker controls
Reaction-side rework after extended testing on 2026-05-13:

* Promoted users weren't seeing speaker controls in their own
  Amethyst — `StageControlsBar` gates on `isOnStage && ui.onStageNow`
  and `ui.onStageNow` was stuck at false (never reset back to true
  after some earlier path flipped it). Added a LaunchedEffect in
  NestActivityContent that mirrors `isOnStageMe → ui.onStageNow`
  whenever it becomes true, symmetric to the auto-stop effect.

* Reactions overlay sat inside AvatarAndBadges' wrap-content Box,
  so adding/removing the chip shifted the inner Box's centre and
  the role badges drifted. Lifted the overlay out to be a sibling
  of AvatarAndBadges inside the outer fixed-size Box; badges now
  stay anchored regardless of chip presence or animation state.

* Reaction grouping was by `targetPubkey` (NIP-25 p-tag) — emojis
  showed on the speaker being reacted to, not the reactor. Switched
  RoomReactionsAggregator to group by `sourcePubkey`. AudienceGrid
  threads `reactionsByPubkey` so an audience reactor sees their own
  emoji float from their audience-tab avatar too.

* Reaction chip animation: progress was an `Animatable.value` that
  Compose wasn't refreshing in the layout-consuming layer (visible
  bug: chip "blinked" with no movement). Replaced with a manual
  `withFrameNanos` loop writing to a `MutableFloatState` read inside
  a `graphicsLayer { … }` lambda — frame-clock animation that works.
  Each kind-7 is its own chip keyed by event-id (no more
  groupBy-content collapsing same-emoji bursts into one shared chip
  that restarts on every arrival). Chips stack at a fixed-size 30 dp
  Box with the emoji centred, so the X position is invariant under
  glyph width. Multiple concurrent chips overlap at the right-bottom
  corner in a `Box(BottomEnd)` (newest on top) rather than sliding
  leftward in a `Row`.

* Bottom-drawer `RoomReactionPickerSheet` (hard-coded 6 emojis, no
  NIP-30) replaced by a forked `RoomReactionPopup` that reuses
  `ReactionChoicePopupContent` (same NIP-30 custom-emoji support,
  same user-configured reaction set) but with two semantic deviations
  for live audio rooms: empty `toRemove` so all buttons stay
  "fresh", and the click handler signs+broadcasts a fresh kind-7
  template directly instead of going through `Account.reactTo` —
  which delegates to `ReactionAction.reactTo(note, …)` and
  short-circuits on `note.hasReacted(by, reaction)`. The bypass
  lets the user fire the same emoji repeatedly during a moment.

* Chip rendering matches NoteCompose: `RenderReactionContent`
  handles `:shortcode:url` via `InLineIconRenderer` + `CustomEmoji`,
  `"+"`→❤️, `"-"`→👎, anything else as Text.

Tests updated for sender-grouped aggregator semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:45:21 -04:00
Vitor Pamplona d21eb0e226 fix nests room host UX: visible toasts, promote propagation, edit-sheet keyboard, reaction animation
This commit bundles several issues surfaced while testing host actions
inside an audio room.

NestActivity didn't mount `DisplayErrorMessages`, so every toast emitted
by nest code (promote, demote, kick, force-mute, errors) queued into a
StateFlow whose only collector lives in MainActivity. Mounted it inside
NestActivity.setContent so toasts now render in front of the room UI —
same way the leave-confirmation AlertDialog already does.

Host actions used to fire a synchronous "Promoted X" toast regardless of
whether `signAndComputeBroadcast` actually completed. Silent signer
failures (TimedOut, ManuallyUnauthorized, CouldNotPerform, etc. — all
Log.w-only in launchSigner) were invisible from the host's POV. Replaced
with a coroutine-bound failure toast that surfaces the exception class
+ message; success is implicit via the UI update.

The real cause of "promoted user stays in audience tab" turned out to be
stale presence: a kind-10312 emitted before the role grant (with
onstage=0) was pinning the freshly-promoted speaker to the audience tab.
buildParticipantGrid now takes a `roleGrantSec` parameter (the
kind-30312 created_at) and treats presence as authoritative only when
strictly newer. Pinned with two new tests covering both the
stale-ignore and fresh-respect cases. The leave-stage-on-another-client
flow keeps working because that emits a fresh onstage=0.

RoomParticipantActions.rebuild now uses
`(original.createdAt + 1L).coerceAtLeast(now())` so a same-second
promote→demote can't tie-break the wrong way under NIP-01's lowest-id
rule.

EditNestSheet's bottom row got squashed when the keyboard appeared —
the form fields couldn't shrink, so the Save/Cancel/Close row took the
hit. Split into a scrollable form column + sticky action row, wrapped
the outer column in imePadding.

SpeakerReactionOverlay was driving drift on a 100 ms `delay` loop over
the 10 s eviction window — produced 0.16 dp per tick (visibly stepped)
and the chip barely moved before disappearing. Replaced with
`Animatable.animateTo` on Compose's frame clock and a 6 s duration so
the chip pops, lingers visibly, then drifts up and fades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:45:20 -04:00
Vitor Pamplona 82ce4df201 Merge pull request #2880 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-14 08:38:13 -04:00
Vitor Pamplona 38b5b677e8 Merge pull request #2885 from vitorpamplona/claude/add-audio-playback-f4a-FZy9V
feat: add audio playback support for .f4a files
2026-05-14 08:38:04 -04:00
Claude 773d61692e feat: add audio playback support for .f4a files
F4A is AAC audio in an MP4 container (Adobe's variant of .m4a).
Register the extension in the media URL list so it routes to the
player, and map it to audio/mp4 so ExoPlayer picks the MP4 extractor.

https://claude.ai/code/session_01EuB46tfwBxtvRNDz9Ju99J
2026-05-14 10:56:45 +00:00
Crowdin Bot 5b87356416 New Crowdin translations by GitHub Action 2026-05-14 03:31:05 +00:00
Vitor Pamplona fb068a13c6 Merge pull request #2883 from vitorpamplona/claude/trace-audio-pipeline-performance-EHJ45
fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring
2026-05-13 23:29:32 -04:00
Claude cd28de4eb1 fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring
`AudioTrackPlayer` sized the AudioTrack ring at `max(minBuffer * 16,
250 ms)`. The intent (per the kdoc) was ~250 ms of jitter slack with
the device's `getMinBufferSize` as a floor — but the `* 16` multiplier
silently dominated on common devices. A Pixel reports `minBuffer ≈
40 ms` for 48 kHz mono; `× 16 = 640 ms`. Add the 200 ms preroll in
`NestPlayer` and decoded PCM sat in the ring for ~800 ms – 1 s before
the speaker played it.

Two-phone testing confirmed the symptom: the speaking-now ring lit up
~1 s before the listener's audio. The ring fires on
`onLevel(peakAmplitude(pcm))` in `NestPlayer.play()` immediately after
`decoder.decode()` succeeds — before `player.enqueue(pcm)` — so the
ring is real-time and the delay was entirely between `enqueue` and
the speaker. The web (NostrNests) listener uses an `AudioWorklet`
with no equivalent ring buffer, so its audio aligned with the wire.

Drop the `* 16` so the formula matches the intent: `max(minBuffer,
250 ms)`. On devices whose floor is below 250 ms (the common case)
the 250 ms target wins; on devices whose floor is above 250 ms (rare)
we still respect the device-reported minimum. Steady-state ear-to-
speaker delay drops from ~1 s to ~450 ms (250 ms ring + 200 ms preroll).
2026-05-14 03:25:51 +00:00
Claude dec56c2ee9 Revert: remove NestsTrace listener-pipeline instrumentation
Removes the four trace points + debug-build auto-enable hook added in
the prior commit on this branch. They served their purpose locally
(confirmed the ~1s startup lag lives between pcm_decoded and
pcm_enqueued, i.e. AudioTrack ring backpressure), and don't need to
land in main.
2026-05-14 03:23:02 +00:00
Vitor Pamplona 17f207bb31 Merge pull request #2882 from vitorpamplona/claude/fix-dialog-paging-dots-yCTp5
fix: pad zoomable dialog pager dots above gesture/nav bar
2026-05-13 23:02:32 -04:00
Vitor Pamplona 3d8945128f Merge pull request #2881 from vitorpamplona/claude/add-bottom-nav-favorite-feed-l91GR
feat: show bottom nav on Favorite Algo Feeds screen
2026-05-13 23:01:58 -04:00
Claude 12650bd7fd feat: show bottom nav on Favorite Algo Feeds screen
Switch FavoriteAlgoFeedsListScreen from a plain Material Scaffold to
DisappearingScaffold and host an AppBottomBar bound to
Route.EditFavoriteAlgoFeeds, matching the pattern used by every other
tab-root feed (Badges, Articles, Bookmark Groups, ...). Hoist the
LazyColumn state so re-tapping the bar item scrolls to top.

When the user pins this screen as a bottom-bar entry, navigation lands
here via nav.navBottomBar() which marks the entry as a tab root, so
AppBottomBar renders and TopBarWithBackButton hides its back arrow.
When reached as a pushed screen from elsewhere, AppBottomBar's existing
canPop() guard hides the bar and the back arrow stays — preserving the
old behaviour for non-bottom-bar entry points.
2026-05-14 02:53:02 +00:00
Claude d9ce949eda fix: pad zoomable dialog pager dots above gesture/nav bar
The page indicator in the image/video zoom dialog sat too low on
Android devices, getting covered by the gesture bar or 3-button nav.
Apply navigationBarsPadding() to the indicator surface so it floats
above the system bar inset.
2026-05-14 02:03:18 +00:00
Vitor Pamplona f3574081b5 Merge pull request #2879 from mstrofnone/docs/nip9b-renumber-comments
docs(community-rules): rename NIP-9A -> NIP-9B (upstream slot collision with #2194)
2026-05-13 21:48:02 -04:00
Vitor Pamplona 04743f700b Merge pull request #2878 from vitorpamplona/claude/modernize-security-filters-ui-IPUjW
Refactor SecurityFiltersScreen into modular settings screens
2026-05-13 19:57:57 -04:00
Claude a12d5d147d chore: regenerate MaterialSymbols font subset for Remove glyph
The Stepper introduced in this branch references MaterialSymbols.Remove
(U+E15B). Regenerated via tools/material-symbols-subset/subset.sh so the
shipped TTF actually contains the glyph.
2026-05-13 23:36:29 +00:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):

  https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289

This commit updates all human-readable references in the merged
community-rules code:

- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
  5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
  Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
  strings, untranslated "NIP-9A" token replaced with "NIP-9B"

Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.

Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
2026-05-14 09:29:59 +10:00
Claude 86c66920d3 chore(nests): instrument listener audio pipeline with NestsTrace
Adds four trace points on the listener pipeline so an end-to-end JSONL
capture (`adb logcat -s NestsTraceJsonl:D -v raw > nest-trace.jsonl`)
shows wall-clock latency at every stage:

  frame_received       (MoqLiteSession.drainOneGroup, after trySend)
  frame_object_mapped  (MoqLiteNestsListener.wrapSubscription map)
  pcm_decoded          (NestPlayer.play, after decoder.decode)
  pcm_enqueued         (NestPlayer.play, after player.enqueue)

Each event carries (sub_id, group_seq, ...) or (track_alias, obj_id, ...)
so the same frame can be followed through all four stages. Deltas
isolate which segment owns the observed startup-only ~1s lag:

  subscribe_send → first frame_received : relay forward latency
  frame_received → pcm_decoded          : Opus decode wall-time
  pcm_decoded   → pcm_enqueued          : AudioTrack ring backpressure
  pcm_enqueued  → next pcm_decoded      : decode-loop scheduling

Tracing auto-enables on debug builds when `NestActivity` opens and
disables on `onDestroy` so release apps pay only the field-load
guard inside `NestsTrace.emit`. Capture instructions are inline at
the call site in `NestActivity.onCreate`.
2026-05-13 23:09:31 +00:00
Claude 0b7399feb0 fix: pad Loading/Error states and tighten Stepper resync
- Wrap the when-branches in SelectableUserList, HiddenWordsList, and
  MutedThreadsList in Box(modifier.fillMaxSize()) so the Scaffold
  padding is applied to every state — Loading/Error/Empty/Loaded — not
  just the LazyColumn. Fixes a regression where the loading spinner
  and error UI rendered under the top bar.
- SettingsStepper now clamps `value` once into `[min, max]` and uses
  the raw `value` (re-clamped) in the +/- handlers. If the model
  starts below `min`, the first tap of `+` resyncs it to `min`
  instead of jumping `min+1` (skipping a step). Display still falls
  back to `unsetLabel` when `value <= 0`.
- WarnReportsTile no longer pre-coerces threshold to >= 1 at the call
  site; the stepper handles it.
- EmptyState drops its now-unused modifier parameter.
2026-05-13 23:09:06 +00:00
Claude 06e88cd30e refactor: address second-pass Security Filters audit
- Restore pull-to-refresh on BlockedUsers and SpammingUsers by wrapping
  SelectableUserList in RefresheableBox (regression from the first
  refactor; HiddenWords and MutedThreads remain non-pull-refresh to
  match the original behavior).
- Drop the redundant transientHiddenUsers subscription in
  InvalidateOnBlockListChange — hiddenUsers.flow already combines it.
  Also drop the meaningless accountViewModel key on LaunchedEffect.
- Move CountBadge and Stepper into SettingsSectionCard as
  SettingsCountBadge and SettingsStepper so other settings screens can
  reuse them. Widen the stepper value cell to min 40.dp so three-digit
  values like 999 fit comfortably.
- Drop the unused `enabled` flag from SettingsControlRow (sub-controls
  use SettingsSubControlRow for dimming). Tighten the docstring.
- Unify SwitchTile to take @StringRes Int params, matching the @StringRes
  annotations now applied to SettingsItem, SettingsSection, BlockListTopBar,
  SelectableUserList, EmptyState.
- Shorten property chains by binding `val security = …syncedSettings.security`
  per tile when more than one field is read; pass method references
  (accountViewModel::updateFilterSpam etc.) where possible.
- Hoist WarningType.entries to a local val so the segmented button row
  reads `count = options.size` instead of evaluating twice.
- MutedThreadsScreen: replace itemsIndexed with items (unused index) and
  use MaterialTheme.colorScheme.onPrimary instead of hardcoded Color.White
  for the "Unmute" button text.
- Add a @Preview for SecurityFiltersScreen, mirroring AllSettingsScreen.
2026-05-13 22:00:38 +00:00
Claude 6feca2cf17 refactor: address Security Filters audit findings
- Extract SettingsSection/SettingsItem/SettingsDivider/SettingsControlRow/
  SettingsBlockTile/SettingsSubControlRow into SettingsSectionCard.kt.
  AllSettingsScreen now consumes the shared components instead of carrying
  its own private copies, and SecurityFiltersScreen drops the parallel set
  it used to declare.
- BlockedContentRow folds into SettingsItem(trailing = { CountBadge(...) }),
  removing the bespoke navigation row.
- Replace the Spinner/Switch tile mix with WarnReportsTile = SwitchTile +
  SettingsSubControlRow(stepper), avoiding the nullable-icon code smell.
- Move the sensitive-content SegmentedButtonRow out of a cramped trailing
  slot and into SettingsBlockTile so it has full-width room.
- HiddenWordsScreen: replace the eager Column.forEach with LazyColumn, and
  add Modifier.imePadding() to the Scaffold so the keyboard no longer
  covers the Add-word field.
- Move SelectableUserList from BlockedUsersScreen.kt to SecurityListsCommon
  and drop the nullable onToggle (both call sites pass one).
- BlockListTopBar now takes selectedCount: Int instead of Set<*>; its
  UserFeedState branches are exhaustive (no else -> Unit fallthrough).
- CountBadge reserves min width so 0↔n transitions don't shift the row.
- Stepper relies on Material 3's enabled handling instead of computing
  alpha by hand and tinting LocalContentColor in three places.
- Distinct icons per tile/category (Visibility, Code, VisibilityOff, etc.)
  so the same MaterialSymbols.Tag isn't reused for unrelated rows.
2026-05-13 21:34:53 +00:00
Claude 2816ceb479 feat: split Security Filters into a hub with per-category screens
Replaces the cramped single-screen layout (header settings + four tabs)
with a hub that uses the same grouped-card pattern as AllSettingsScreen,
plus a dedicated screen per blocked-content category. The tile control
mix is also modernized:

- "Show sensitive content" is now a 3-way SegmentedButton instead of a
  free-floating spinner sitting awkwardly next to switches.
- Numeric prefs (report threshold, max hashtags) use a stepper (- / + )
  instead of free OutlinedTextFields with no commit affordance.
- The report-threshold tile renders inline under its toggle and dims
  when the toggle is off, instead of appearing/disappearing and shifting
  layout.
- Each tile is a row with leading icon, title, description, trailing
  control - so the spinner/switch/stepper visual rhythm stays consistent.

The hub's "Blocked content" section shows a count badge per category and
routes to its own screen (Routes.BlockedUsers, SpammingUsers, HiddenWords,
MutedThreads). The hidden-words screen now docks the "Add word" field as
a bottom bar so it doesn't compete with the list for vertical space, and
selection mode is scoped per-screen instead of shared across tabs.
2026-05-13 20:48:53 +00:00
Vitor Pamplona dbd7266d8f better description of the draft option 2026-05-13 16:29:17 -04:00
Vitor Pamplona 8dd4ed4c6d Merge pull request #2877 from vitorpamplona/claude/add-profile-settings-page-ezkgF
Add profile UI settings to customize visible sections and feeds
2026-05-13 15:56:26 -04:00
Claude 128048f043 perf: stop recomposing RenderScreen on every profile-tab swipe
viewedTab was a MutableState read inside RenderScreen at
visibleTabs.indexOf(viewedTab), so every pager swipe — which writes to
viewedTab from snapshotFlow — invalidated the enclosing restart group
even though rememberPagerState's initialPage is never re-applied after
the first composition.

Move the tracker to a plain holder class (ViewedTabRef). Reads inside
the key(visibleTabs) block no longer subscribe to snapshot changes, so
swipes only recompose the tab row / pager content as intended.
2026-05-13 19:50:52 +00:00
Claude 251699beaa fix: honor Profile UI toggles in subscriptions and pager state
- Gate WatchLifecycleAndUpdateModel(appRecommendations) on the
  showProfileAppRecommendations toggle so the viewmodel no longer
  refreshes when the section is hidden.
- Thread loadFollowers / loadZapsReceived through UserProfileQueryState
  and the matching sub-assemblers. When the user hides those tabs,
  UserProfileFilterAssembler stops emitting their relay filters and
  the live REQs drop.
- Re-pin the profile pager to the tab the user was viewing whenever
  the visible-tabs list changes, using key(visibleTabs) +
  rememberPagerState(initialPage = ...) and a snapshotFlow that tracks
  the currently-viewed ProfileTab. Prevents the pager from silently
  shifting to a different tab when one is toggled off in settings.
2026-05-13 19:50:52 +00:00
Claude f26f3626f1 feat: add Profile UI settings page
New "Profile UI" settings page lets users toggle which profile-screen
sections are loaded and displayed. All toggles default to on.

Toggles:
- Profile Badges (NIP-58 badges row)
- App Recommendations (apps row in profile header)
- Zap Received Feed (received zaps tab)
- Followers Feed (followers tab)

Tabs are filtered from the profile pager so disabled feeds no longer
allocate a page or fire their subscriptions.
2026-05-13 19:50:52 +00:00
Vitor Pamplona 6b06db9395 Merge pull request #2876 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 15:49:21 -04:00
Crowdin Bot 216ce24839 New Crowdin translations by GitHub Action 2026-05-13 19:45:26 +00:00
Vitor Pamplona 680aacf5b9 fix nests demoted speakers stuck on stage and broadcasting
Three related bugs when a host removes a speaker from the stage on
nostrnests by editing the kind-30312 to drop their p-tag:

1. NestRoomFilterAssembler didn't subscribe to the kind-30312 itself
   while in-room — only chat/presence/reactions (#a) and admin
   commands. Once joined, the room event was frozen on whatever
   version loaded the screen, so demotions never propagated. Added
   a per-relay filter on `kinds=[30312], authors=[host], #d=[dTag]`.

2. Even if the demotion did propagate, `BroadcastHandle` kept
   running. Only the manual Leave Stage button called
   `stopBroadcast()`. Added a LaunchedEffect that tears down the
   broadcast when the local user falls off `participantGrid.onStage`
   while still publishing — covers both demote-by-host and
   leave-stage-on-another-client.

3. The auto-stop reliably exercised a pre-existing
   NestForegroundService bug: when `startListening` was called to
   demote from mic+media to media-only, `intent.action == null`
   fell through to `else -> promoted`, keeping mic=true. The service
   then asked startForeground for FOREGROUND_SERVICE_TYPE_MICROPHONE
   after the mic had been released, threw SecurityException on
   Android 14+, runCatching swallowed it, stopSelf ran without
   startForeground → ForegroundServiceDidNotStartInTimeException.
   The explicit-demote branch now returns false; only intent==null
   (OS sticky-restart) preserves the prior promoted state.
2026-05-13 15:41:51 -04:00
Vitor Pamplona 20a2270434 fix nests room author shown in audience with no audio
nostrnests publishes kind-30312 without a self-`p`-tag for the room
author — they're the implicit host. The lobby card already handles
this (NestJoinCard) but the in-room code did not: the author fell
into the pure-audience branch with role=null AND was missing from
the audio subscription set, so the host appeared as a regular
listener and no sound played.

buildParticipantGrid now takes hostPubkey and synthesizes a virtual
`["p", host, "", "host"]` when absent, so the existing presence-aware
onstage/audience rules apply uniformly — including "host leaves
stage" (onstage=0 → drops to audience, role stays HOST).

NestActivityContent owns the grid now and derives onStageKeys from
it, so the StageGrid render and the MoQ subscription set stay in
lockstep when anyone (host or speaker) steps off stage.
2026-05-13 15:41:51 -04:00
Vitor Pamplona e7dc8ec25c Merge pull request #2875 from vitorpamplona/claude/timeago-toggle-format-Y3lGX
Make timestamps toggleable between relative and absolute formats
2026-05-13 15:40:10 -04:00
Vitor Pamplona 487e91eff6 Merge pull request #2874 from vitorpamplona/claude/add-compose-settings-IthDX
Add compose settings screen with auto-draft creation toggle
2026-05-13 15:40:01 -04:00
Vitor Pamplona 0618f8ded7 Merge pull request #2873 from vitorpamplona/claude/review-amethyst-issues-ePExg
Round-5 performance & security: frame decode, HP, AEAD, resumption
2026-05-13 15:39:30 -04:00
Claude 66202e667d refactor: switch Compose Settings toggles to Material Switch
Each binary setting (auto-create drafts, AI writing help, tracked
broadcasts) becomes a single-tap Switch instead of a two-tap
Always/Never spinner, and a shared BooleanSwitchRow helper removes the
per-toggle boilerplate.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:36:53 +00:00
Claude 4d2886d162 refactor(timeago): consolidate toggle into one stable composable
Audit follow-up — the toggle behaviour now lives in a single
`ToggleableTimeAgoText` core in `ui/note/elements/TimeAgo.kt`. `TimeAgo`,
`NormalTimeAgo`, `ChatTimeAgo`, and `ChatroomHeaderCompose.TimeAgo` are
thin wrappers that pick a `TimeAgoStyle` (Dotted / Short) and pass
colour/font params; no per-site duplication of state + clickable +
derivedStateOf.

Performance fixes that matter for a feed with hundreds of timestamps:

- `rememberSaveable` → `remember`. Persisting a transient peek-toggle to
  the SavedStateRegistry for every visible+scrolled-past note was pure
  memory bloat. Recycling now resets to relative, which is the expected
  behaviour for a transient inspect action.
- The relative-mode `derivedStateOf` lambda reads `nowState.value` only
  when displaying a relative time. An item the user has frozen to its
  absolute date no longer re-evaluates every 30-second tick.
- Core composable takes only stable primitive params (Long, Color,
  TextUnit, TextOverflow, enum) so Compose can skip it entirely when
  inputs don't change.
- Desktop wrapper dropped the redundant `derivedStateOf`: its formatter
  reads no State, so derivedStateOf had nothing to observe.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:31:15 +00:00
Claude d5860841c9 refactor: gate only draft create, and colocate compose composables
- sendDraftSync no longer short-circuits the delete-on-blank path when
  automatic draft creation is disabled. Clearing a composer now always
  cleans up any existing draft; the setting only suppresses creation.
- Physically move AiWritingHelpChoice and TrackedBroadcastsChoice from
  AppSettingsScreen.kt into ComposeSettingsScreen.kt so the composable
  definitions live alongside the screen that hosts them.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:28:27 +00:00
Claude 8a3bd52631 fix(quic): finish P1 + A2 audit follow-ups (PR #2873)
P1 (header protection — eliminate remaining HP allocations):
Add HeaderProtection.maskInto(hpKey, src, srcOffset, scratch16, dstMask)
that writes the 5-byte mask into a caller-owned buffer using a
caller-owned 16-byte AES scratch. Add `hpScratch` (16 bytes) and
`hpMask` (5 bytes) to PacketProtection — same per-direction
single-threaded analysis as nonceScratch from the prior commit. Thread
both buffers through Short/LongHeaderPacket build / parseAndDecrypt /
peekKeyPhase as optional parameters paired with nonceScratch; production
call sites in QuicConnectionWriter / QuicConnectionParser pass
`proto.hpScratch` + `proto.hpMask`. With both this commit and 09b28b8d
the AES-128-GCM hot path now allocates ZERO bytes for HP + nonce per
packet (down from 16-byte sample slice + 16-byte AES output + 5-byte
mask + 12-byte nonce = 49 bytes/pkt).

ChaCha20HeaderProtection.maskInto routes through the same shape but
still allocates a fresh 5-byte ciphertext + 12-byte nonce slice
internally — Quartz's `ChaCha20Core.chaCha20Xor` SPI takes a
standalone nonce ByteArray. Documented as a future cleanup; AES is
the dominant path in production.

A2 (TlsRunningSha256 — lazy fallback accumulator):
Probe `digest.clone()` ONCE at construction. Conscrypt's clone
support is a build-time property (native bridge presence), not state-
dependent, so a single probe is a reliable signal. Devices where the
probe succeeds skip the byte accumulator entirely (zero overhead);
devices where it fails get the byte-accumulator path from the very
first update so the first snapshot already has the complete
transcript. Replaces the previous "always accumulate" shape that
wasted a few KB per handshake on every device, including the
overwhelming majority that support clone.

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 19:27:47 +00:00
Claude 09b28b8d78 fix(quic): address self-audit findings on #2861 fixes (PR #2873)
Addresses three issues found in the self-audit of commit fbacef1f:

S2 clock bug (CRITICAL): the previous fix compared TlsResumptionState.
issuedAtMillis (wallclock — stored via System.currentTimeMillis() in
TlsClient) against QuicConnection.nowMillis() (monotonic — anchored at
construction). On any new connection the monotonic clock starts near
zero, so `(nowMillis() - issuedAtMillis)` produced a large negative
value, the coerceAtLeast(0L) clamped it to 0, and the expiry check
never fired. Add a dedicated `epochMillis: () -> Long` parameter on
QuicConnection (default System.currentTimeMillis()) and use that for
ticket-age comparisons — matches the source TlsClient stamps the
ticket with.

S3 null-ALPN filter: the effectiveResumption filter rejected ANY
ticket whose cached negotiatedAlpn was null. That breaks resumption
for servers that don't negotiate ALPN at all (legitimate cold-handshake
case), as well as for any persisted ticket predating the
negotiatedAlpn cache field. Treat absent cached ALPN as "no binding to
honour" and allow resumption — the RFC 9001 §4.6.1 restriction is
about ALPN MISMATCH, not absence. The post-EE rejected0Rtt check
mirrors the same null-tolerant shape.

P2 scratch threading: the previous commit added aeadNonceInto but
didn't thread a persistent scratch through the call sites, so the
hot path still allocated a fresh 12-byte nonce per packet. Add
PacketProtection.nonceScratch (sized to iv.size, single-direction so
single-threaded), thread it through Short/LongHeaderPacket build +
parseAndDecrypt as an optional `nonceScratch: ByteArray? = null`
parameter, and pass `proto.nonceScratch` from the four production call
sites in QuicConnectionWriter / QuicConnectionParser. Tests that
construct packets directly are unchanged (default null = allocate
fresh).

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 19:19:05 +00:00
Claude 38463e5f2c feat: tap TimeAgo to toggle relative ↔ absolute timestamp
Every TimeAgo composable (Android NoteCompose timestamp, NormalTimeAgo,
ChatTimeAgo, ChatroomHeaderCompose last-message time) and every Desktop
timestamp Text (NoteCard, NotificationsScreen, ChatPane, ConversationListPane)
is now clickable and toggles to a scale-adjusted absolute date/time:

  - same day → time only (e.g. "14:32"), locale-aware
  - same year → "MMM dd, HH:mm"
  - older    → "MMM dd, yyyy"

State is hoisted per-call site via rememberSaveable so the toggle survives
scroll-induced disposal in lazy lists. Desktop call sites share a small
ToggleableTimeAgoText wrapper; Android keeps its existing composable shapes
and just gains a clickable modifier + state.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:17:55 +00:00
Claude 1b8b508816 feat: add Compose Settings screen with auto-draft toggle
Introduces a new Compose Settings screen that groups composer-related
preferences. Adds a new "Automatically create drafts" toggle that gates
the automatic draft creation triggered on back-press / cancel across
every composer (short notes, public messages, long-form, classifieds,
public channels, private DMs, nests).

Moves "Propose text improvements" and "Tracked broadcasts" out of
Application Preferences and into the new Compose Settings screen.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:04:33 +00:00
Claude fbacef1f2a fix(quic): address #2861 code-review findings
Implements the 10 actionable items from the QUIC code review in issue #2861;
the lone P4 item (encrypt-under-streamsLock) is already tracked as deferred
phase 2 work in quic/plans/2026-05-08-lock-split-design.md and is unchanged.

Android-only correctness (would silently break on API 26–32):
- A1 JdkCertificateValidator: wrap Signature.getInstance("Ed25519") in
  try/catch and surface NoSuchAlgorithmException as a clean
  QuicCodecException so an Ed25519 leaf cert no longer crashes the
  TLS read loop on pre-API-33 Android.
- A2 TlsRunningSha256 (jvmAndroid): keep a parallel byte accumulator and
  fall back to one-shot SHA-256 on the rare API-26–28 Conscrypt builds
  whose OpenSSLMessageDigestJDK throws CloneNotSupportedException from
  MessageDigest.clone(). Latches the fallback after first failure so we
  don't pay the JCA throw per snapshot.

Hot-path performance:
- P1 HeaderProtection: add maskAt(hpKey, src, srcOffset) + extend
  AesOneBlockEncrypt with encryptInto(key, src, srcOffset, dst, dstOffset)
  so the per-packet HP path no longer allocates a 16-byte sample slice
  AND no longer allocates a 16-byte JCA Cipher output (the jvmAndroid
  impl uses Cipher.doFinal's range overload). Updated 5 call sites in
  ShortHeaderPacket and LongHeaderPacket.
- P2 aeadNonce: add aeadNonceInto(staticIv, packetNumber, dst) so call
  sites with a persistent 12-byte scratch can build the nonce without
  per-packet allocation; aeadNonce keeps its existing shape via the new
  helper. Threading the scratch through Short/LongHeaderPacket and the
  writer is deferred (similar shape to the documented P4 phase 2 work).
- P3 QuicConnectionParser: decode the frame list once per inbound packet,
  feed both qlog (frameNamesFor) and dispatch from the single decode.
  Pre-fix every qlog-attached packet ran decodeFrames twice.
- P5 QuicConnectionWriter: iterate pendingMaxStreamData /
  pendingNewConnectionId directly instead of allocating
  entries.toList() per drain.

Protocol / security:
- S1 QuicConnectionParser: cap MAX_STREAMS at 2^60 (RFC 9000 §19.11);
  a peer sending a larger value now triggers STREAM_LIMIT_ERROR close
  rather than overflowing the local nextLocalBidi/UniIndex counters.
- S2 QuicConnection.effectiveResumption: drop the cached session ticket
  when (now - issuedAt) ≥ min(ticketLifetimeSec, 7 days) per RFC 8446
  §4.6.1; expired tickets would otherwise silently fail server-side and
  lose any 0-RTT bytes.
- S3 QuicConnection: refuse to offer 0-RTT when our current alpnList
  doesn't include the resumed session's negotiated ALPN, and treat
  0-RTT as rejected on EE if the new ALPN differs from the cached one
  (RFC 9001 §4.6.1).
- S4 TlsExtension.encodeSignatureAlgorithms: drop rsa_pkcs1_sha256.
  The validator already rejects it in CertificateVerify per RFC 8446
  §4.2.3 — advertising it lied about what we accept.

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 18:55:52 +00:00
Vitor Pamplona 781e8484ac force video box to be there when the video is still building 2026-05-13 12:35:37 -04:00
Vitor Pamplona 5cf3a08b92 Correct way to show the video's blurhash when loading 2026-05-13 12:35:37 -04:00
Vitor Pamplona 51cb12e563 Merge pull request #2872 from vitorpamplona/claude/fix-shortnewpost-relays-pTAzG
Refactor relay broadcast logic for personal and channel events
2026-05-13 11:44:22 -04:00
David Kaspar 15dbde8cc3 Merge pull request #2871 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 17:22:12 +02:00
Crowdin Bot a119da177f New Crowdin translations by GitHub Action 2026-05-13 15:17:11 +00:00
Vitor Pamplona f6adc760bb Merge pull request #2870 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 11:15:17 -04:00
Crowdin Bot a76237e1ad New Crowdin translations by GitHub Action 2026-05-13 15:04:44 +00:00
davotoula 8585b0fcf4 i18n: translate muted-threads strings into cs/de/pt-BR/sv
Adds Czech, German, Brazilian Portuguese, and Swedish translations
for the 6 new muted-threads keys introduced with the mute-thread
feature (action_unmute, quick_action_mute_thread,
quick_action_unmute_thread, settings_muted_threads_empty,
settings_muted_threads_title, settings_muted_threads_unknown).
2026-05-13 17:01:18 +02:00