Commit Graph

11296 Commits

Author SHA1 Message Date
davotoula 3bb9aba8fb fix(hls): master playlist URL had ..m3u8 double extension
Root cause: Android's MimeTypeMap does not know
application/vnd.apple.mpegurl, so Nip96Uploader.upload() resolved the
extension to "" and sent the multipart filename as "abc." (trailing
dot). The NIP-96 server then echoed the upload name into its returned
URL, which arrived at the pipeline ending in ".". The pipeline's
withExtensionHint helper appended ".m3u8" on top of that, producing
"..m3u8" — which the subsequent playback GET 404s against because the
server stored the file at the single-dot path.

Two fixes, both needed:

1. Nip96Uploader.upload(InputStream, ...) now falls back to a small
   static MIME->extension table when MimeTypeMap returns null, covering
   the HLS playlist types and video/mp4 / fMP4 segment types. The file
   is uploaded with a real ".m3u8" / ".mp4" extension, so the server
   never has to invent one.

2. HlsUploadPipeline.withExtensionHint now trims trailing dots and
   collapses any existing "..ext" sequences before checking whether to
   append, so a server that still echoes a single trailing dot cannot
   produce unreachable URLs.

Regression test locks in that a fake uploader returning a
"https://server/bare-1." URL yields a single-dot ".m3u8" in the final
upload result.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula 1255ae3e61 fix(hls): disable per-write/read timeouts for large rendition uploads
The shared okHttpClientForUploads ships with a 30 s read and write
timeout on Wi-Fi. A 9 MB+ rendition where the server does synchronous
hashing / virus scanning easily takes longer than 30 s to return 200,
and OkHttp fires readTimeout while the request is still in flight.
The pipeline throws, the orchestrator moves to Failure, and the master
playlist upload never happens — matching the on-wire observation that
all 10 rendition files reach the server but the master does not.

HLS gets its own dedicated OkHttpClient derived from the shared upload
client: writeTimeout and readTimeout are set to 0 (disabled) so a slow
rendition can trickle through and a slow server can take as long as it
needs to respond. A generous 15 minute per-call timeout remains as a
hard cap so a silently dead connection eventually errors out.

Fixes the "server processed the upload 74 seconds after the client
marked it failed" race the upload report flagged as a classic wire-won
case.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula ddd1c19c47 fix(hls): read user's configured Blossom servers from account
The server dropdown on the HLS Upload screen was hardcoded to
DEFAULT_MEDIA_SERVERS, so any Blossom server the user configured in
Settings -> Media Servers did not appear. Wire it to
account.blossomServers.hostNameFlow (same StateFlow the existing
AllMediaBody settings screen consumes), which yields the signed
BlossomServersEvent normalized into List<ServerName> and falls back to
DEFAULT_MEDIA_SERVERS when the user has none configured.

Initial selectedServer prefers account.settings.defaultFileServer when
it is still in the list, otherwise the first available server. A
collect job re-syncs the list if the user adds/removes servers while
the screen is open.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula 9e49353acb fix(hls): move publish state ownership to the ViewModel
NewHlsVideoScreen crashed on open with IllegalStateException("load()
must be called first") because NewHlsVideoBody reads vm.state during
the first composition, but LaunchedEffect runs vm.load(account, context)
only AFTER that first composition completes — classic pre-load race.

Moves the MutableStateFlow<HlsPublishState> out of the orchestrator and
into the ViewModel, so vm.state is safe to read from composition start.
The orchestrator now receives the flow as a constructor param and
writes into it as before — same semantics, no new race windows. Tests
and the production factory are updated to pass the shared flow through.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula 9467500d4a feat(hls): wire NewHlsVideoScreen into drawer + navigation
- Renames the share_hls_video label to "HLS Upload" so the drawer row
  matches the on-device name the user sees everywhere.
- Adds Route.NewHlsVideo as a data object next to the other create
  routes in Routes.kt.
- Registers it with composableFromEnd<Route.NewHlsVideo> in
  AppNavigation.kt, sliding in from the end the same way the Longs,
  Shorts and Pictures creation surfaces do.
- Adds a drawer NavigationRow between Longs and Wallet in ListContent()
  with Icons.Outlined.SettingsInputAntenna — reads as "broadcasting /
  antenna" and matches the HLS-as-streaming metaphor.

Milestone 5d (final 5.x piece) of the HLS video sharing plan
(2026-04-13). The feature is now reachable end-to-end from the UI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula b1f100b213 feat(hls): NewHlsVideoScreen Compose UI
Four-state screen driven by HlsPublishState:

- Idle (no video): big "Pick a video" card using
  ActivityResultContracts.PickVisualMedia(VideoOnly).
- Idle (video picked): selected-file card with duration/resolution/size
  probed off-thread via MediaMetadataRetriever, then form fields
  (title, multi-line description, content warning switch + reason,
  server picker, H.265/H.264 FilterChip toggle that disables H.265
  when CompressorUtils.isHevcEncodingSupported() is false, a read-only
  renditions preview computed from HlsLadder.default().forSource(), and
  the primary "Publish HD video" button).
- In-flight (Transcoding / Uploading / Publishing): three-row phase
  view with a check icon for completed phases, a ring for the active
  one, and a LinearProgressIndicator underneath the active row; cancel
  button at the bottom.
- Success: green check + master playlist URL + Done button that resets
  the VM and pops back.
- Failure: error icon + message + Try Again button that resets the VM.

Server picker reuses the existing TextSpinner / TitleExplainer pattern
(same widgets FileServerSelectionRow uses), filtered to exclude NIP-95
since blob-per-event storage blows past relay size limits.

Strings live in values/strings.xml under the "HLS multi-resolution
video sharing" block and reuse the pre-existing content_warning,
file_server, cancel and dismiss entries where appropriate.

Drawer row + route wiring come in milestone 5d.

Milestone 5c of the HLS video sharing plan (2026-04-13).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula 6942314cf8 feat(hls): adapter for Nip96/Blossom + production orchestrator wiring
HlsBlobUploaderFactory turns a user-chosen ServerName into the simple
file+contentType HlsBlobUploader the pipeline expects by adapting
BlossomUploader or Nip96Uploader. Each adapter reuses the existing
roleBasedHttpClientBuilder.okHttpClientForUploads and the account's
signer helpers (createBlossomUploadAuth / createHTTPAuthorization),
matching how UploadOrchestrator wires them today. NIP-95 is rejected
explicitly — storing each rendition as an event would blow past relay
size limits.

createProductionHlsPublishOrchestrator binds the transcode to
HlsTranscoder, uploads to HlsBlobUploaderFactory, and signAndPublish to
account.signer.sign(...) + account.sendAutomatic(...). The Uri is
captured via a lazy provider so the orchestrator can be constructed at
VM load time before the user picks a video.

NewHlsVideoViewModel.load(account, context) is the new one-call setup
the screen will use; the existing load(account, orchestrator) overload
stays for unit tests.

Milestone 5b of the HLS video sharing plan (2026-04-13).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:33:47 +02:00
davotoula c008daee7e feat(hls): publish orchestrator + ViewModel state machine
feat(hls): build NIP-71 video event templates from upload result
feat(hls): orchestrate per-rendition and master uploads
feat(hls): wrap HlsPreparer into HlsTranscoder + HlsBundle
2026-04-15 15:33:47 +02:00
davotoula 07ae5d3ac7 test(hls): add byterange playlist rewrite case
Verifies HlsPlaylistRewriter handles the default single-file-per-rendition
output from HlsPreparer where every segment reference points to the same
combined fMP4 file and EXT-X-BYTERANGE lines must be preserved unchanged.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:29:48 +02:00
davotoula 80b7b38b27 feat(hls): add HlsPlaylistRewriter for post-upload URL substitution
Pure rewriter that walks HLS master or media playlists line-by-line and
substitutes each resource reference (segment file, EXT-X-MAP init,
variant media.m3u8) with its uploaded absolute URL. Preserves every
#EXT-X-STREAM-INF, #EXTINF and other directive line verbatim so the
BANDWIDTH/RESOLUTION/CODECS attributes that ExoPlayer's
AdaptiveTrackSelection reads stay intact. Loud failure on missing
entries in the URL map to avoid silent data loss.

Bumps lightcompressor-enhanced to 2.1.0 for the HlsPreparer API that
the following milestones will wrap.

Milestone 1 of the HLS video sharing plan (2026-04-13).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:29:48 +02:00
Vitor Pamplona ec40fba9ee Merge pull request #2398 from vitorpamplona/claude/update-sdk-37-HxSkv
Update Android SDK to platform 37 and build-tools 37.0.0
2026-04-15 08:36:04 -04:00
Claude b4f4acf30e chore: update session-start hook to install Android SDK 37
The project's compileSdk/targetSdk was bumped to 37 in
gradle/libs.versions.toml, but the web-session hook was still fetching
platform-36 and build-tools 36.0.0. Point it at platform-37.0_r01.zip
and build-tools_r37_linux.zip so Gradle finds the platform it needs
when Claude Code on the web bootstraps.
2026-04-15 12:31:39 +00:00
Vitor Pamplona 53060f255c Merge pull request #2395 from vitorpamplona/claude/separate-pinned-notes-screen-EMC6R
Extract pinned notes into separate screen and navigation route
2026-04-15 08:26:01 -04:00
Vitor Pamplona 43da67d676 Improves claude command 2026-04-15 08:25:14 -04:00
Vitor Pamplona 76f0e95be8 Spotless Apply 2026-04-15 08:21:28 -04:00
Vitor Pamplona 7cc683b7e5 Merge pull request #2397 from vitorpamplona/claude/add-video-feed-options-menu-EfqQl
Make editState parameter optional in ReactionsRow and related components
2026-04-15 08:18:16 -04:00
Vitor Pamplona 09f8eeb5a0 Merge pull request #2396 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-15 08:11:46 -04:00
Claude 3147e39aa4 refactor: drop editState from video/picture/file feed cards
These feed events (VideoEvent, PictureEvent, FileHeaderEvent) don't
support editing, so there's no editState to observe. Remove the
observeEdits calls and the editState parameter from UserCardHeader,
and make ReactionsRow's editState parameter nullable so these screens
can pass null.

https://claude.ai/code/session_01DrsvnguWHdZMgsRqNS8BUz
2026-04-15 12:07:46 +00:00
Crowdin Bot 1bebd6b5e4 New Crowdin translations by GitHub Action 2026-04-15 12:04:33 +00:00
Vitor Pamplona 7e0bd705bc Merge pull request #2394 from vitorpamplona/claude/merge-mls-groups-LHMHG
Add MarmotGroup chat room support to ChatroomHeaderCompose
2026-04-15 08:02:23 -04:00
Claude 8a276d6f1d feat: add 3-dot options menu to video/picture/file feed cards
Adds MoreOptionsButton (the same menu used in NoteCompose) to
UserCardHeader, shown to the right of TimeAgo. This gives users
the same follow/copy/share/bookmark/report actions on video,
picture, and file-header feed cards that they have on text notes.

https://claude.ai/code/session_01DrsvnguWHdZMgsRqNS8BUz
2026-04-15 03:20:50 +00:00
Claude d17b869dc2 feat: separate pinned notes into their own screen
Previously the Bookmarks screen bundled private, public, and pinned notes
into a single three-tab view. Pinned notes (NIP-51 kind 10001) are a
distinct concept from bookmarks (NIP-51 kind 10003) and deserve their
own destination.

- Remove the Pinned Notes tab from BookmarkListScreen (now two tabs)
- Add new PinnedNotesScreen and Route.PinnedNotes
- Move PinnedNotesFeedFilter/ViewModel to a new pinnednotes package
- List Pinned Notes as a sibling entry alongside Bookmarks and Old
  Bookmarks in ListOfBookmarkGroupsScreen so tapping Bookmarks in the
  drawer continues to surface all three lists
2026-04-15 02:47:22 +00:00
Claude 80518c55c9 feat: render MLS groups inline in the Messages list
MLS (Marmot) groups now appear in the regular Messages list alongside
DMs and public chats. Each entry shows the group's image (robohash
fallback from the nostr group id), name, and last message, marked with
an "MLS Group" label rendered in the same style as the existing "Public
Chat" badge. Tapping the row navigates to the dedicated MLS group chat
screen.

Notes are routed to the MLS row by checking for a MarmotGroupChatroom
in the note's gatherers, so this works without changing the feed filter
that already aggregates marmot newest messages.
2026-04-15 02:46:27 +00:00
Vitor Pamplona de94b28fc2 Spotless 2026-04-14 22:24:55 -04:00
Vitor Pamplona 469ee05f9e Merge pull request #2392 from vitorpamplona/claude/fix-marmot-initialization-UCHgi
Fix Marmot initialization error in group creation
2026-04-14 22:23:04 -04:00
Vitor Pamplona 33385d2b3a Merge pull request #2391 from vitorpamplona/claude/fix-ble-deprecations-bqy9Y
Update AndroidBleTransport for Android 13+ Bluetooth API changes
2026-04-14 22:19:11 -04:00
Claude b5d6c88775 fix: Marmot group add member flow (init, display, multi-select)
Three issues in the Marmot "new group → add members" flow:

1. "Marmot not initialized" error after creating a group.
   AccountCacheState silently swallowed any exception from constructing
   AndroidMlsGroupStateStore and set the store to null, which propagated
   to Account.marmotManager being null. createMarmotGroup then silently
   returned, leaving the UI in a half-created state where AddMemberScreen
   would always error out. Now we log the exception and fall back to an
   in-memory MlsGroupStateStore so the group operations at least work
   within the current session.

2. Hex pubkey shown under selected user's name. Replaced with the
   NIP-05 identifier when available, or the shortened npub otherwise,
   matching how ShowUserSuggestionList renders users.

3. Could only select one user at a time. AddMemberScreen now keeps a
   list of selected users, shows each with its own Remove button, and
   the Add action adds them sequentially, reporting per-user successes
   and failures. Failed users stay selected so the user can retry.
2026-04-15 02:17:51 +00:00
Claude ccba463686 fix(ble): migrate AndroidBleTransport off deprecated BLE APIs
Replaces deprecated BluetoothGattCharacteristic.value, writeCharacteristic,
notifyCharacteristicChanged, writeDescriptor and connectGatt overloads with
their Tiramisu (API 33+) replacements, falling back to the legacy API with
@Suppress("DEPRECATION") when running on older devices. Adds the new
onCharacteristicChanged(gatt, characteristic, value) overload and caches the
last notified payload so GATT read requests no longer read from the
deprecated characteristic.value field.
2026-04-15 02:16:15 +00:00
Vitor Pamplona 33da237ef4 Merge pull request #2386 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-14 22:10:15 -04:00
Crowdin Bot 118c2d4890 New Crowdin translations by GitHub Action 2026-04-15 02:08:38 +00:00
Vitor Pamplona b10a7a5153 Merge pull request #2390 from vitorpamplona/claude/fix-deprecated-warnings-iiTEr
Fix Bluetooth SCO receiver registration for Android 12+
2026-04-14 22:07:10 -04:00
Vitor Pamplona 6769b03ec2 Merge pull request #2389 from vitorpamplona/claude/fix-marmot-group-persistence-6Cfb7
Fix GCM IV length in KeyStore decryption
2026-04-14 22:02:55 -04:00
Claude 3a5319605f fix(call): suppress legacy Bluetooth SCO deprecation warnings
The legacy Bluetooth SCO APIs (EXTRA_SCO_AUDIO_STATE, SCO_AUDIO_STATE_*,
isBluetoothScoOn, ACTION_SCO_AUDIO_STATE_UPDATED) were deprecated in
API 31+ in favor of AudioManager.setCommunicationDevice(), which this
class already uses on S+. Gate the legacy SCO broadcast receiver to
pre-S builds and suppress the deprecation warnings on the function,
matching the pattern already used on startBluetoothSco/stopBluetoothSco.
2026-04-15 02:02:53 +00:00
Claude 552ff7a2ce fix: replace JVM-only synchronized with Mutex in quartz commonMain
MarmotInboundProcessor and CommitOrdering.EpochCommitTracker used
`synchronized(lock) { ... }`, which is a JVM-only intrinsic. Compiling
the quartz KMP module for iosSimulatorArm64 (and other non-JVM targets)
failed with "Unresolved reference 'synchronized'".

Switch to kotlinx.coroutines.sync.Mutex + withLock, matching the pattern
already used in MlsGroupManager. EpochCommitTracker's public API becomes
suspend — update the MarmotInboundProcessor delegates (pendingCommitGroupEpochs,
clearPendingCommits) and wrap the commonTest cases in runTest. The
MarmotPipelineTest jvmAndroid tests already run inside runBlocking, so
no changes needed there.

Also reshape the processGroupEvent dedup check to hoist the "already
processed?" read out of the lock block so the early return isn't a
non-local return from the withLock lambda.
2026-04-15 02:02:05 +00:00
Claude 8a7afdf794 fix: use 12-byte GCM IV length in KeyStoreEncryption.decrypt
Marmot MLS groups were being wiped on every app restart. The encrypted
state was written with a 12-byte GCM IV (from cipher.iv after ENCRYPT_MODE
init), but decrypt was reading cipher.blockSize (= 16, the AES block size)
bytes as the IV. Every decrypt therefore failed, MlsGroupManager.restoreAll
caught the exception and deleted the "corrupt" group directory, and the
Marmot group list came up empty after each restart.

Use a 12-byte IV constant in both encrypt and decrypt so the round-trip
works. This matches the sibling KeyStoreEncryption in commons/androidMain
(which already hardcodes 12).
2026-04-15 01:53:57 +00:00
Vitor Pamplona ed245ada57 Merge pull request #2388 from vitorpamplona/claude/fix-call-activity-lifecycle-ApVaS
Add per-peer invite timeouts and calls enable/disable setting
2026-04-14 21:34:00 -04:00
Claude 1290e9151c feat(settings): add toggle to disable voice and video calls
New "Enable voice and video calls" switch at the top of the Call
Settings screen. Default is ON (no behavior change for existing users).
When a user turns it OFF:

- Call buttons in the chat room top bar are hidden. ChatroomScreen
  observes account.settings.callsEnabled as a StateFlow and flips
  onCallClick / onVideoCallClick to null, which RenderRoomTopBar
  already treats as "no buttons".

- Incoming CallOfferEvents are silently dropped in CallManager.
  A new isCallsEnabled: () -> Boolean hook on the CallManager
  constructor gates onIncomingCallEvent *before* the followed-user
  check, so the device never rings and no state transition occurs.
  Existing in-flight call signaling (answers/hangups/ICE) still flows
  through so a call that was active when the user flipped the toggle
  continues to clean up normally.

- The rest of the call-related settings (video quality, max bitrate,
  TURN servers) are hidden on the settings screen since they have no
  effect while calls are disabled — the screen becomes just the single
  meaningful toggle.

The setting is persisted per account via SharedPreferences
(PrefKeys.CALLS_ENABLED = "calls_enabled"), loaded in
LocalPreferences.loadFromEncryptedStorageSync, and exposed as a
MutableStateFlow<Boolean> on AccountSettings so both the chat top bar
and the settings screen react to changes without needing to navigate
away and back.

AccountViewModel wires
  isCallsEnabled = { account.settings.callsEnabled.value }
into the CallManager constructor so the flag is read lazily on every
incoming event.

Tests in CallManagerTest:
- incomingOfferIgnoredWhenCallsDisabledInSettings — disabled flag
  drops the offer, no state change, no published events.
- disablingCallsAfterStartDoesNotTearDownInProgressCall — an active
  call keeps working after the toggle flips; only new offers are
  ignored.
- incomingOfferProcessedWhenCallsEnabled — regression guard for the
  default-enabled path.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-15 01:21:37 +00:00
Vitor Pamplona 235d87a386 Merge pull request #2387 from vitorpamplona/claude/secp256k1-implementation-comparison-ThHC3
Fix secp256k1 field arithmetic carry-fold bugs and optimize squaring
2026-04-14 20:02:37 -04:00
Claude b9d6cefbeb feat(call): per-peer 30s invite timeout and per-peer status in video grid
Two changes that tighten how the caller-side UX handles slow or
unresponsive callees in a group call:

1. Per-peer 30-second invite timeout

   Previously the caller used a single 60-second call-wide timeout: if
   *any* peer answered within the window the timer was cancelled and
   slow peers could remain "ringing" indefinitely. For a mid-call
   invite there was no timeout at all — the invitee stayed in
   pendingPeerPubKeys forever, burning a PeerConnection on the caller
   side and keeping the invitee's device ringing.

   CallManager now schedules an independent 30-second timer for every
   peer in pendingPeerPubKeys (or Offering.peerPubKeys in the initial
   offer phase). The timer is started when the peer is added to
   pending — in beginOffering, initiateCall and invitePeer — and is
   cancelled when the peer answers (onCallAnswered), rejects
   (onCallRejected) or hangs up (onPeerHangup). On expiry the peer is
   dropped from the group, a CallHangup is published to them so their
   device stops ringing, and onPeerLeft fires so CallController can
   dispose the per-peer PeerConnection. If the drop leaves the caller
   with zero connected and zero pending peers, the call ends with
   EndReason.TIMEOUT.

   Callee ringing still uses the 60-second CALL_TIMEOUT_MS in
   onIncomingCallEvent; the two timers now serve distinct roles.

   New constant CallManager.PEER_INVITE_TIMEOUT_MS = 30_000L.

   NIP-AC.md "Event Lifecycle" documents both timers.

2. Per-peer "Calling..." status in the video grid

   The shared "Waiting for others to join…" banner across the top of
   ConnectedCallUI is removed. PeerVideoGrid now takes a
   pendingPeerPubKeys set and, for each peer still pending, routes
   rendering through PeerAvatarCell with a "Calling…" status line
   under the username — so it's obvious *which* participants the call
   is waiting on, not just *that* it's waiting. A peer in pending
   never shows as a video cell even if a stale track is still in the
   map, because they haven't actually answered yet.

   The voice-only path in ConnectedCallUI now also uses PeerVideoGrid
   (with empty tracks/active-video) instead of the single
   GroupCallPictures + GroupCallNames stack, so the per-peer status
   behavior is consistent for audio calls.

Tests added in CallManagerTest:
- perPeerTimeoutEndsP2PCallWhenBobNeverAnswers
- perPeerTimeoutDropsSlowCalleeFromGroupCall
- perPeerTimeoutIsCancelledOnAnswer
- perPeerTimeoutDropsMidCallInviteeWhenNoAnswer
- perPeerTimeoutIsCancelledOnReject
- perPeerTimeoutEndsCallWhenAllCalleesIgnore

All existing CallManagerTest cases still pass unchanged.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 23:23:42 +00:00
Claude d087e1ac13 feat(call): full-mesh setup for mid-call invites
When an existing group call participant invites a new peer via
CallController.invitePeer(), the caller connects to the invitee via the
normal offer/answer flow, but the other existing callees were never
establishing their own PeerConnections to the new peer — breaking the
full-mesh invariant.

Root cause: the NIP-AC "callee-to-callee mesh setup" mechanism relies on
each callee observing the others' CallAnswers during its own ringing
window and applying a symmetric "lower pubkey initiates" tiebreaker.
A mid-call invitee's ringing window happens after all existing callees
have already answered, so the invitee's discoveredCalleePeers set is
empty. Worse, if the invitee's pubkey is higher than an existing
callee's, that callee also stays passive (waiting for the invitee to
initiate), and no mesh connection is ever attempted between them.

Fix — break the symmetry on mid-call invites:

- CallManager.onCallAnswered now expands peerPubKeys when an answer
  arrives in Connected (or Connecting) state from a peer that is not
  yet in the tracked group membership. This keeps the UI and state
  consistent with the expanded group and gives CallController a clear
  hook via onAnswerReceived.

- CallController.onCallAnswerReceived splits the NO_SESSION case:
    * Connected state → mid-call invite. Unconditionally initiate a
      mesh CallOffer to the new peer. The invitee stays passive, so
      exactly one side initiates per pair and glare is structurally
      impossible.
    * Connecting state → initial-call mesh observation. Keep the
      existing lower-pubkey tiebreaker via onNewPeerInGroupCall to
      avoid glare with the symmetric peer.

This fix uses the existing event kinds (CallOffer 25050 and CallAnswer
25051) — no new kinds or tags are required. NIP-AC.md is updated with
a new "Mid-Call Mesh Expansion" subsection under "Inviting New Peers"
documenting the asymmetric rule and a full-flow diagram.

Tests in CallManagerTest cover:
- Mid-call invitee's broadcast answer expands existing callee's
  peerPubKeys in Connected state.
- Same expansion works in Connecting state (existing callee still
  handshaking with caller when the invitee joins).
- Initial-call answers from already-tracked peers do NOT trigger the
  expansion branch (regression guard).
- Full end-to-end 3-way flow exercising Alice, Bob, Carol managers.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 22:54:07 +00:00
Claude 8390badadc perf(secp256k1/c): keep fe_sqr on fe_mul_asm for FE_MUL_ASM platforms
Measurement via bench_vs_acinq (native C-to-C comparison against ACINQ's
libsecp256k1) revealed that fe_sqr_inline's 10-mul __int128 path regressed
verify by ~10% and batch verify by ~25% on x86_64 GCC+BMI2+ADX. The ASM
fe_mul_asm uses MULX plus dual ADCX/ADOX carry chains that the compiler
cannot reproduce from __int128 arithmetic, so the extra 6 multiplications
are cheaper than losing the ASM's scheduling wins.

Restore fe_sqr to fe_mul_asm(r, a, a) whenever FE_MUL_ASM is set, and
document the measurement in the fe_sqr_inline comment block. The 10-mul
inline still wins for portable builds (clang without the GCC asm blocks,
MSVC, non-x86_64/ARM64 targets), where fe_mul falls back to the generic
__int128 row-schoolbook path and dedicated squaring genuinely cuts muls.

Baseline (HEAD~1) vs branch with this fix, averaged over 3 runs of
`bench_vs_acinq` on x86_64 (µs/op, lower is better):

  Operation          ACINQ  baseline  with fix
  pubkeyCreate        17.4     14.5     14.4
  sign (derive pk)    35.7     28.8     28.2
  sign (cached pk)    18.7     14.3     14.3
  verify (BIP340)     35.0     36.4     36.2
  verifyFast          35.0     33.0     32.4
  ECDH                35.8     31.6     31.5
  batch(32)           35.4      5.6      5.6
  batch(200)          36.5      4.9      4.7

Every measurement is within run-to-run noise of baseline, and our custom
C is ~1.2-1.3x faster than ACINQ on sign/pubkey and ~6-8x faster on batch
verify (which ACINQ has no public API for at all). Correctness verified:
`Cross-verification: ACINQ verifies ours=1, We verify ACINQ=1`, and all
188 Kotlin secp256k1 tests still pass.

https://claude.ai/code/session_01KExJURZATpL59ZKXP6AVP6
2026-04-14 22:48:56 +00:00
Claude 0fbe61274a fix(call): release WebRTC resources synchronously on CallActivity teardown
The normal cleanup path routes through CallManager -> state=Ended -> the
state-collector in viewModelScope -> CallController.cleanup(). If the
Activity is destroyed before the collector finishes (task removal, PiP
dismissal, system kill), the async path can leave PeerConnections, the
camera capturer, the proximity wake lock, and the audio-mode change
alive until AccountViewModel.onCleared runs later.

CallActivity.onDestroy / onStop now call CallController.cleanup()
directly as a synchronous safety net, in addition to publishing the
hangup/reject signaling on a detached scope.

To make that safe, CallController.cleanup() is now truly idempotent
within a call session: the cleanedUp flag stays sticky once set and is
re-armed only when a new call starts via initiateGroupCall() or
acceptIncomingCall(). This prevents a second sequential cleanup() call
from double-disposing WebRTC objects when both the state-collector path
and the Activity safety net run.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 22:16:17 +00:00
Vitor Pamplona 08e119ede4 Fixes another permission needed for phone calls in the app 2026-04-14 17:48:46 -04:00
Vitor Pamplona 1dc6c2d0d8 correctly saves channel lists from new users. 2026-04-14 17:33:03 -04:00
Vitor Pamplona 5cd94f98cc Reducing the space between the call option buttons and the bottom user to avoid overlap with each other 2026-04-14 17:32:47 -04:00
Vitor Pamplona c170dd2eb3 Fixes the shadow clipping issue 2026-04-14 10:55:24 -04:00
Vitor Pamplona 6d3582cecb removing log borders 2026-04-14 10:45:08 -04:00
Vitor Pamplona 080be06a2f Fixes messages FAB's position when open 2026-04-14 10:31:13 -04:00
Vitor Pamplona d23f6f8ec1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-14 10:30:51 -04:00
Vitor Pamplona 887e0bf302 Improves stability and permission checks for voice calls when the app is in the background. 2026-04-14 10:19:47 -04:00