When the app restarts and reconnects to relays, old call offer events
from completed calls could be replayed, causing the phone to ring for
calls that already ended. This was especially noticeable when the user
killed and restarted the app shortly after a call.
Three protections added to CallManager:
1. Track completed call IDs: hangup/reject events mark their call-id as
completed. Subsequent offer events for the same call-id are ignored.
2. Init timestamp guard: events created before the CallManager was
initialized (minus a grace period) are rejected, preventing stale
events from a previous app session from triggering ringing.
3. Completed call IDs survive reset(): the set is intentionally not
cleared when the call state machine resets to Idle, ensuring that
stale offers remain blocked for the lifetime of the CallManager.
https://claude.ai/code/session_0145VKiG8yZMqcMsaBEjNPxv
Move transitionToEnded() before the signing + relay publish in hangup()
so the UI stops ringing/ringback immediately, matching the pattern
already used by rejectCall(). Add onDestroy safety net in CallActivity
to hang up if the Activity is destroyed while a call is active. Wrap
audio stop methods in try-catch to prevent one failure from blocking
the others.
https://claude.ai/code/session_01Rip2HPCbF48PPFDiB2X5ik
After swapping with an adjacent item, the dragOffset adjustment can
flip its sign (e.g. negative becomes positive), which immediately
triggers the second if-block to swap back in the opposite direction
within the same onDrag call. This causes the dragged item to jump
and the user to end up dragging a different item. Fix by returning
after each successful swap so only one swap occurs per drag event.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
Remove the sortedBy(receivedBytes) from relayListBuilder and
Nip65RelayListViewModel.clear() so relays keep their order as
stored in the Nostr event. This makes drag-and-drop reordering
meaningful since the saved order is now preserved on reload.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
When items swap during drag, the composable gets a new index value.
Using pointerInput(index) caused the gesture scope to restart, killing
the in-progress drag and leaving the UI stuck. Fix by using
rememberUpdatedState for the index so the pointerInput scope stays
alive across recompositions while always reading the current index.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
When accepting an incoming WebRTC call via the notification's Accept
button, CallActivity now checks for RECORD_AUDIO and CAMERA permissions
before proceeding. If permissions are missing, the system permission
dialog is shown and the call is accepted only after permissions are
granted. This prevents the crash that occurred when accepting calls
from the notification without prior permission grants.
https://claude.ai/code/session_0193KQShzBh4puYh74dHgSbE
Set userScrollEnabled = !isDragging on all LazyColumns that host
draggable relay items so the scroll gesture doesn't fight the drag
gesture. In AllRelayListScreen, scroll is disabled when any of the
12 category drag states is active.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
Replace the single-item Column approach with RelayDragState that works
within itemsIndexed. Each relay item remains a separate lazy item for
better performance with large lists. The drag handle icon on each row
captures drag gestures while the item modifier applies visual feedback
(elevation, scale, translation).
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
Add DraggableRelayList composable with gesture-based drag-and-drop,
following the same pattern used in ReactionsSettingsScreen. Each relay
category (DM, NIP65 home/notif, search, blocked, trusted, local,
broadcast, indexer, proxy, private outbox, favorites) now shows a
drag handle and supports reordering via drag gestures.
https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd
The publish KeyPackage button was always active because the app didn't
track whether a key package had already been published. This adds:
- hasActiveKeyPackages() to KeyPackageRotationManager and MarmotManager
- hasPublishedKeyPackage() to Account, checking both in-memory bundles
and the local cache for existing kind:30443 events
- Own key package filter in MarmotSubscriptionManager and the EOSE
manager so previously published key packages are downloaded from
relays on app restart
- UI feedback: primary-colored key icon when published, contextual
empty-state message, and a spinner during publishing
https://claude.ai/code/session_01BVe7aSEWd2KLi5Ks6RZkcc
Replace the raw OutlinedTextField + IconButton Row with
ThinPaddingTextField, ThinSendButton, EditFieldBorder, and
EditFieldModifier to match the private DM and channel chat layouts.
https://claude.ai/code/session_013dhfy18dfuSYN8khXiLLBb
The publish KeyPackage button had no visual feedback, making it unclear
whether anything happened. Added a Snackbar to confirm success or show
errors, disabled the button during publishing, and updated the hint text
to direct users to the key icon.
https://claude.ai/code/session_013dhfy18dfuSYN8khXiLLBb
The share dialog in zoomable/fullscreen media view was auto-hiding
before users could select an option. This was caused by:
1. A 2-second timer in ZoomableContentDialog that hides all controls
(including the share dialog) via AnimatedVisibility
2. A LaunchedEffect in RenderTopButtons that explicitly closed the
share dialog whenever video controls auto-hid
3. Tap-to-toggle on the background also hiding controls while dialog
was open
Fix: Hoist the share popup state and guard the auto-hide timer,
tap-to-toggle, and video control hide from dismissing when the
share dialog is open.
https://claude.ai/code/session_01YLujgxRDuujKCS3vgHc6Gq
The open polls notification flow only re-evaluated when new notes arrived
or dismissed IDs changed. Polls that passed their close date remained
visible until something else triggered the flow. Adding a 1-minute ticker
to the combine ensures expired polls are filtered out promptly.
https://claude.ai/code/session_01VY9FRuzHzwTVrePaJiHuHx
Add three new test files for the Marmot MLS implementation:
- MlsGroupLifecycleTest: End-to-end lifecycle tests covering Welcome
processing, cross-member encrypt/decrypt, multi-member groups, commit
processing, external joins, PSK proposals, and ReInit proposals.
- MlsGroupEdgeCaseTest: Security boundary tests for wrong epoch rejection,
corrupted ciphertext detection, invalid KeyPackage rejection, out-of-range
leaf indices, empty/large message handling, and add/remove/add cycles.
- MlsConformanceTest: Cross-implementation comparison tests verifying
KeyPackage structure, GroupInfo signatures, Welcome message format,
HPKE seal/open, SignWithLabel/VerifyWithLabel, LeafNode signatures,
deterministic key schedule, and commit structure conformance.
Fix GroupInfo signature bug (RFC 9420 Section 12.4.3.1):
- buildWelcome() and groupInfo() were signing only groupContext.toTlsBytes()
but verifySignature() checked against encodeTbs() which includes
GroupContext + extensions + confirmationTag + signer. Now both methods
build an unsigned GroupInfo first and sign its full TBS encoding.
Enhance MlsGroupManager KDoc with usage examples, responsibility breakdown,
and cross-implementation notes.
8 tests are @Ignore'd documenting a known bug: processCommit() does not
derive the same epoch secrets as commit(), causing cross-member AEAD
failures after epoch transitions.
https://claude.ai/code/session_018f67fqNReg3dEXcDimYLY1
Implement end-to-end reading of MarmotGroupData (extension 0xF2EE)
from the MLS GroupContext.extensions and sync to the UI layer.
Quartz (protocol):
- MarmotGroupData.decodeTls(): deserialize from TLS wire format
(version, nostrGroupId, name, description, admin pubkeys, relays,
image fields)
- MarmotGroupData.fromExtensions(): find and decode from extension list
- MlsGroup.extensions: expose groupContext.extensions publicly
Commons (shared logic):
- MarmotGroupChatroom: add description, adminPubkeys, relays fields
as MutableStateFlow for reactive UI
- MarmotManager.groupMetadata(): extract MIP-01 data from group
- MarmotManager.syncMetadataTo(): sync metadata + member count to
a MarmotGroupChatroom
Sync points (amethyst):
- Account init: sync metadata after restoreAll() for all restored groups
- GiftWrapEventHandler: sync after Welcome processing (group join)
- GroupEventHandler: sync after CommitProcessed (epoch advances may
update extensions via GroupContextExtensions proposals)
UI (GroupInfoScreen):
- Display group description from MIP-01 metadata
- Display relay URLs from MIP-01 metadata
- Mark admin members with "- admin" suffix (from adminPubkeys)
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Replace the plain text input with the existing UserSuggestionState +
ShowUserSuggestionList pattern used throughout the app (NewGroupDM,
edit post, etc.).
The flow is now:
1. Type a name, npub, or NIP-05 in the search field
2. Debounced search queries local cache + relays + NIP-05 DNS
3. ShowUserSuggestionList displays matching users with profile
pictures and display names
4. Tap a user to select them — shows their profile in a selected
user row with picture, display name, truncated pubkey, and a
Clear button
5. Tap "Add" to fetch their KeyPackage and complete the invite
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
AddMember flow (was TODO, now functional):
- Account.fetchKeyPackageAndAddMember(): queries outbox relays for the
target user's KeyPackageEvent (kind:30443) using client.fetchFirst(),
decodes the base64 KeyPackage content, and calls addMarmotGroupMember()
which publishes the commit then sends the Welcome gift wrap
- AddMemberScreen now calls the real implementation and navigates back
on success
Member display names:
- MemberRow now uses LoadUser to resolve Nostr pubkeys to User profiles
from LocalCache, showing bestDisplayName() instead of raw hex
- UserPicture composable shows profile pictures next to member names
- "(you)" indicator for the current user's entry
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Replace AlertDialog-based CreateGroupDialog and AddMemberDialog with
proper full-screen routes that slide up from the bottom, following the
existing app patterns (ChannelMetadataEdit, NewGroupDM, etc.).
New screens:
- CreateGroupScreen: uses CreatingTopBar with close/create buttons,
replaces CreateGroupDialog
- AddMemberScreen: uses ActionTopBar with close/add buttons,
replaces AddMemberDialog
New routes:
- Route.CreateMarmotGroup → composableFromBottom
- Route.MarmotGroupAddMember(nostrGroupId) → composableFromBottomArgs
Updated call sites:
- MarmotGroupListScreen: FAB navigates to Route.CreateMarmotGroup
- MarmotGroupChatScreen: add-member icon navigates to
Route.MarmotGroupAddMember
- MarmotGroupInfoScreen: add-member icon navigates to
Route.MarmotGroupAddMember
Deleted old files:
- CreateGroupDialog.kt
- AddMemberDialog.kt
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Based on MIP spec review, add missing UI screens:
MarmotManager additions (commons):
- memberPubkeys(): extract Nostr pubkeys from MLS ratchet tree
leaf nodes via Credential.Basic identity
- memberCount(): get group member count
- groupEpoch(): expose current MLS epoch
- GroupMemberInfo data class for leaf index + pubkey pairs
Account & AccountViewModel:
- leaveMarmotGroup(): publish SelfRemove proposal and clean up
- marmotGroupMembers(): expose member list to UI
MarmotGroupInfoScreen (new):
- Group metadata header (name, ID, epoch, member count)
- Member list with leaf indices, clickable to view profiles
- "Leave Group" with confirmation dialog
- "Add Member" access from group info
Navigation:
- Route.MarmotGroupInfo added and wired into nav graph
- Chat screen title now clickable to navigate to group info
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM