Android 12+ blocks starting activities from BroadcastReceivers used
as notification trampolines. The Accept action now uses
PendingIntent.getActivity to launch CallActivity directly with
EXTRA_ACCEPT_CALL. CallActivity.onCreate/onNewIntent checks this
extra and accepts the incoming call.
The Reject action remains as a BroadcastReceiver since it doesn't
need to start an activity.
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
1. onStop() no longer hangs up call when user switches apps
- Added wasInPipMode flag to track if the activity was ever in PiP
- onStop only hangs up + finishes when PiP was dismissed (swiped away),
not when the user simply presses Home from the full-screen call UI
- Pressing Home from full-screen enters PiP via onUserLeaveHint
2. Removed triple-hangup from overlapping lifecycle methods
- onPictureInPictureModeChanged now only updates UI state
- onStop handles PiP dismissal only
- onDestroy only cleans up the PiP receiver (hangup already handled
by CallController's state collector on Ended)
3. CallNotificationReceiver guards against stale notifications
- Returns early if callManager/callController are null
- Only launches CallActivity if the call is still IncomingCall
- Cancels notification before any other action to prevent re-taps
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
Enable proper full-screen call experience when the phone is locked
or the app is in the background:
- CallActivity shows over lock screen (setShowWhenLocked/turnScreenOn)
- Manifest adds showOnLockScreen and turnScreenOn attributes
- Remove setSilent(true) from notification — it was blocking the
full-screen intent from triggering (channel silence is enough)
- Populate ActiveCallHolder eagerly in initCallController() so the
full-screen intent can launch CallActivity even when the Compose
UI is paused in the background
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
Both CallController and EventNotificationConsumer now load the
caller's profile picture via Coil before showing the notification,
so the large icon displays the caller's avatar instead of being
empty. The caller name was already loaded from LocalCache but now
the bitmap accompanies it.
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
- Track remote video aspect ratio in CallController via VideoFrame
rotatedWidth/rotatedHeight
- PiP uses video's aspect ratio when remote video is active, falls
back to 9:16 portrait for audio-only calls
- Observe isRemoteVideoActive to update PiP params when video
starts/stops during a call
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
- Restore Rational(9, 16) portrait aspect ratio for PiP window
- Add onStop handler: when PiP is dismissed via X button, hang up
the call and finish the activity (mirrors PipVideoActivity pattern)
- onPictureInPictureModeChanged also hangs up if activity is finishing
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
1. Notification: silence channel sound (CallAudioManager handles ringtone),
use CallNotificationReceiver for accept/reject actions instead of
launching MainActivity
2. Cancel call notification when call is accepted (Connecting state)
3. PiP: hang up call when PiP is dismissed (activity destroyed)
4. PiP: add RemoteAction buttons (hangup, mute toggle) since Compose
buttons are not interactive in PiP mode
5. PiP: show compact UI with smaller avatar (48dp) and smaller text
when in picture-in-picture mode
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
Subscribe all bookmark and pinned note IDs to the EventFinderFilterAssembler
when the bookmark screen opens, so events are fetched in bulk via a single
REQ rather than loading one-by-one as the user scrolls.
Applied to both BookmarkListScreen (new bookmarks + pinned notes) and
OldBookmarkListScreen (legacy bookmarks).
https://claude.ai/code/session_01C8gevBfB8vLDFoBW3hnPeU
Separate the call UI into a dedicated CallActivity so it can enter
Picture-in-Picture mode independently of the main activity, allowing
users to continue browsing the app during an active call.
- Add CallActivity with PiP support via onUserLeaveHint
- Add ActiveCallHolder singleton to share call state between activities
- Launch CallActivity from call buttons and incoming call observer
- Remove in-app nav route for ActiveCall (now a separate activity)
- Remove EnterPipOnLeave composable (activity handles PiP directly)
https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
Call screen elements were too close to the screen edges because they
didn't account for status bar and navigation bar insets. Apply
WindowInsets.statusBars and WindowInsets.navigationBars padding to
all call screen layouts while keeping video backgrounds full-bleed.
https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
Videos with extreme aspect ratios (e.g. very tall or very wide) cause
an IllegalArgumentException when entering PiP mode because Android
requires the ratio to be between ~0.4184 and 2.39. Clamp the ratio
before passing it to setAspectRatio.
https://claude.ai/code/session_018uqcACcXpLQxwzVFhePisJ
When the app is alive but backgrounded, the relay subscription delivers
the call event first, marking it as consumed. The push notification
path then skips showing the notification because the event is already
consumed. This left the phone ringing with no notification and no way
to bring the app to the foreground.
Now CallController shows the call notification directly when entering
IncomingCall state, regardless of how the event was received. The
notification's fullScreenIntent brings the app to the foreground.
https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
AudioManager.isSpeakerphoneOn is deprecated since API 31 and silently
ignored on modern devices. Switch to setCommunicationDevice() which
properly routes call audio to speaker, earpiece, or Bluetooth on
Android 12+, with fallback to the legacy API on older versions.
https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
Our own renegotiation answers can echo back through the relay and
get processed as if from the peer. Check that the peer connection
is in HAVE_LOCAL_OFFER state before applying a remote answer SDP,
preventing the "Called in wrong state: stable" error.
https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
Uses the proximity sensor to detect when the phone is held to the ear
and automatically pauses the camera/video track. Video resumes when the
phone is moved away, but only if the user had video enabled.
https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
Change default port from 19050 to 17392 to avoid conflicts with other
Tor-using apps. When binding fails (address in use), increment the port
and retry up to 10 times before giving up.
https://claude.ai/code/session_01JEzjceY3ZaCHz5kL4DsKic
When a user is logged in on multiple devices, all devices ring on an
incoming call. Previously, accepting or rejecting on one device left the
others still ringing.
Now, acceptCall() and rejectCall() also publish a self-addressed
gift-wrapped event (to the user's own pubkey) so other devices receive
it and transition out of the IncomingCall state. Added ANSWERED_ELSEWHERE
end reason and handling in onCallAnswered/onCallRejected for IncomingCall
state.
https://claude.ai/code/session_01X9juzyPYWqxRMCgDWEw8R1
When toggling video mid-call, addVideoTrack triggers
onRenegotiationNeeded but the callback was empty — the remote peer
never received the updated SDP, so video never appeared on their side.
This wires up the full renegotiation flow using the existing
CallRenegotiateEvent (kind 25055):
- WebRtcCallSession: forward onRenegotiationNeeded to a callback
- CallController: create and send renegotiation offers, handle
incoming renegotiation offers by creating answers
- CallManager: route CallRenegotiateEvent, allow CallAnswerEvent
during Connected state for renegotiation answers
https://claude.ai/code/session_01WdmqktFBjXKB6PYRZ5b4FD
When a peer disables their camera, the remote side was showing a frozen
last frame because the VideoTrack reference remained non-null. This adds
a VideoSink-based frame monitor that detects when frames stop arriving
and transitions the UI back to the voice call avatar view.
https://claude.ai/code/session_01WdmqktFBjXKB6PYRZ5b4FD
Adds an ephemeral variant of GiftWrap for transient encrypted messages
that relays don't need to persist. EphemeralGiftWrapEvent extends
GiftWrapEvent so all existing processing paths (decryption, routing,
notifications, call signaling) automatically handle it via type
hierarchy. Relay subscription filters are updated to request both kinds.
https://claude.ai/code/session_0157X96G6HLTzYxkdX9pyTSJ
When the user disables video, the SurfaceViewRenderer kept showing
the last captured frame (frozen). Now the local video PiP is only
rendered when isVideoEnabled is true — when camera is off, the PiP
disappears entirely revealing the black background.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
The d-tag is the article's identity in Nostr addressable events.
Changing it on edit would create a new article instead of updating
the existing one. The slug field is now disabled when isEditing is true.
https://claude.ai/code/session_011JuQbdA12WdPxC7aGvUBqJ
Four bugs fixed:
1. _isVideoEnabled defaulted to true — voice calls showed the
camera icon as "active" even though no video was being sent.
Changed default to false; only set true when video track is
actually created (video call initiation or acceptance).
2. cleanup() reset _isVideoEnabled to true — next call started
with wrong state. Changed to false.
3. stopCamera() disposed the capturer but toggleVideo re-enable
path only called setVideoEnabled(true) without restarting the
camera. No frames were captured. Now re-enable path calls
startCamera() to create a fresh capturer.
4. startCamera() made public on WebRtcCallSession with guard
against double-start (returns early if cameraCapturer != null).
State flow is now:
- Voice call: _isVideoEnabled=false, icon shows "camera off"
- Tap camera: creates track + starts camera, _isVideoEnabled=true
- Tap camera again: stops camera + disables track, =false
- Tap camera again: restarts camera + enables track, =true
- Video call: _isVideoEnabled=true from start, camera running
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Redesigns the LongFormPostScreen to match the reference UI with:
- Visual banner image placeholder with dashed border (clickable to upload)
- Borderless large title field with placeholder styling
- Labeled Summary section
- URL Slug field mapped to the d-tag for user-defined article identifiers
- Tag/hashtag chip input with add button and removable chips
- Tags are included as hashtags in the published event
Adds tags and slug state to LongFormPostViewModel, wires them into
createTemplate() for event publishing, and loads them from drafts.
https://claude.ai/code/session_011JuQbdA12WdPxC7aGvUBqJ
When a call starts as voice-only, no video track exists. Tapping
the camera toggle was calling setVideoEnabled(true) on a null
track, doing nothing.
Now toggleVideo() checks if a video track exists. If not, it
creates one on-demand (addVideoTrack + startCamera), upgrading
the voice call to include video. If toggling off, it stops the
camera to save battery.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
WebRTC's Camera2Session uses WindowManager to get device orientation
for frame rotation. WindowManager requires a visual (Activity)
context — using Application context throws IllegalAccessException.
Changed initCallController to pass the Activity context directly
instead of context.applicationContext.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Video calls were failing with SecurityException because only
RECORD_AUDIO was requested at runtime. CAMERA permission is also
required for Camera2Capturer to open the front-facing camera.
Changes:
- CallPermissions now uses RequestMultiplePermissions instead of
RequestPermission, with isVideo parameter to request CAMERA
alongside RECORD_AUDIO for video calls
- ChatroomScreen passes isVideo=true for the video call button
- CallScreen passes isVideo based on call type when accepting
incoming video calls
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS