Commit Graph

331 Commits

Author SHA1 Message Date
Sravan f3b6e67c19 HOTFIX: add #18/#13 schema to Postgres (chat history returned empty)
Production runs DB_BACKEND=pg, but the message_hidden table (#18) and
pinned_at/pinned_by columns (#13) were only added to the SQLite migrations in
db.js — never to schema.pg.sql. So thread/conversations/pinned queries hit a
missing relation/column and 500'd, which surfaced as "chat history removed"
(no data was ever deleted — the reads just errored).

pg.js init() runs schema.pg.sql on every boot. Added the message_hidden table
and, because CREATE TABLE IF NOT EXISTS can't add columns to the existing
messages table, idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS for
pinned_at/pinned_by. Restores all chat history and re-enables pin + delete-for-me.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 07:53:13 +05:30
Sravan d0863351d2 #13: pin a message
Any participant can pin/unpin a message from its ⋮ menu. Adds pinned_at/pinned_by
columns, /api/messages/pin (toggle, broadcasts chat-pinned) and
/api/messages/pinned (list, newest first, excludes deleted + delete-for-me).
The conversation shows a pinned strip under the header (latest pin + count);
tap it to jump to the message, × to unpin. Live-updates across participants and
devices. Added pin/pinOff Lucide icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 23:00:06 +05:30
Sravan 9017d2ff25 #9: mobile — single tap acts, long-press reveals message actions
On touch, tapping a message used to reveal the reply/react/more bar (so the
action needed a second tap). Now a single tap performs the primary action
(open image/file, jump to a reply), and the action bar is revealed by a
~420ms long-press (with a small haptic); movement or an early release cancels,
and a fired long-press suppresses the trailing tap. A tap elsewhere dismisses
the revealed bar. Desktop hover behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:51:35 +05:30
Sravan c1b67d38d3 #16: trim the schedule form when scheduling from a group
A group's members are automatically the meeting's participants, so the
Invite-participants, Invite-by-email and "Guests must be admitted by host"
sections are noise there. openScheduleModal now omits them when a group id is
present (inviteBlock is empty), and the email/participant/lobby handlers are
null-guarded so the save path still works without those fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:47:15 +05:30
Sravan 276c5e0929 Fix #1 (input-focus zoom), #6 (last-image arrow), #7 (join sound)
#1: focusing a text box zoomed + stretched the whole page on iOS. Several
inputs were < 16px and the existing 16px rule lived in a width-based mobile
@media that misses iPad/landscape (and maximum-scale is unreliable on iOS).
Added a @media (pointer: coarse) rule forcing 16px on every focusable field
for ALL touch devices; desktop is untouched.

#6: the image lightbox used visibility:hidden for the end arrows (invisible but
still occupying space / reading as a ghost button). Switched to display toggling
so there's truly no right arrow at the last image (and no left at the first).

#7: play a soft two-note chime + a brief "<name> joined the call" toast when a
new participant joins the meeting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:44:00 +05:30
Sravan ecc8be3dba Fix #14 (edit leaves a draft) and #8 (last-seen flapping)
#14: after editing a message the edited text reappeared in the composer as a
draft and re-sent as a new message. The input listener saved a draft while
editing, but saveEdit/cancelEdit cleared only the input value, not the stored
draft — so openConvo restored it. Now editing never writes a draft, and
save/cancel clear it.

#8: a contact's subtitle flapped between "last seen …" and "Offline". A
loadSidebar rebuild replaced the row with the server's lastSeen, which is
sometimes null (the disconnect touchSeen is fire-and-forget and can lag the
presence broadcast). Now the client keeps last-seen sticky (never overwrites a
known value with null) and the server never broadcasts a null last-seen for an
offline user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:37:06 +05:30
Sravan e359271157 #10 deleted-last-message preview + #18 delete for me / for everyone
#10: deleting the last message showed "No messages yet" in the chat list.
The sidebar sent an empty last_body for a deleted (content-cleared) row; now
it sends a last_deleted flag and the row renders "This message was deleted"
(or "You deleted this message"), matching the in-thread placeholder.

#18: added "Delete for me" alongside "Delete for everyone". A new message_hidden
table records a per-user hide; the thread + sidebar (last message, unread) filter
out the requesting user's hidden messages, and the hide is echoed to their other
devices (chat-hidden). "Delete for me" is offered on any message; "Delete for
everyone" stays sender-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:31:17 +05:30
Sravan 9e80aee1c6 Fix notification bugs #2 (open+backgrounded chat) and #3 (reactions)
#2: a message arriving in the currently-OPEN chat produced no notification
when the app was minimized. onChatMessage marked the open chat read even
while document.hidden, which fired a notif-clear that closed the very
notification the service worker had just shown. Now the open chat is only
marked read while visible; markOpenChatRead() catches up on focus/visibility
return, and a message received while hidden stays unread with its alert intact.

#3: reacting to a message fired no notification. The react route only pushed
over the live socket (nothing for a closed app) and the client added a silent
bell entry. Now the server sends a native/web push to the message owner, and
onChatReaction pings + shows an OS/in-page popup (unless you're viewing that chat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:23:16 +05:30
Sravan a0f9ab10f2 #15: adding a 3rd person to a 1:1 call promotes it to a persistent group call
When someone is added to a 1:1 (DM) call, promoteDmToGroup() now creates a
real group conversation (named after the participants) and migrates the live
call from dmCalls -> groupCalls, keeping the same room/uuid/history so media
and the transcript continue uninterrupted. Two wins:
 - the call survives anyone leaving (group calls only end when the room empties)
 - an added person who drops can rejoin from the group's active-call banner
   (they're now a member, so replayActiveCalls / group-call resurface it)

/api/calls/invite promotes on a DM call and lets the group-call broadcast ring
the invitees in; it only sends the plain call-invite when NOT promoted. Guarded
so inviting an existing pair-member (memberIds < 3) stays a 1:1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 22:13:41 +05:30
Sravan 100a092ff9 Fix call-cluster bugs #11, #4a, #4b
#11: adding a 3rd person to a 1:1 call then having one participant close
the app disconnected the call for everyone. leaveMeeting() ended a DM room
for ALL peers on any leave; now it only tears down when <2 people remain,
otherwise it falls through to the normal peer-left path (call continues).

#4b: after someone left a call they never reappeared under "Add people".
meeting-peer-left cleaned meetPeers/tiles but not meetPeerUids/meetNames,
so the departed uid stayed in hereUids and was filtered out. Now deleted.

#4a: a guest who enabled mic/cam on the pre-join screen had to re-tap after
being admitted — the choices were applied on a blind 900ms timer that fired
while still in the lobby. Now applied in the meeting-joined handler, after
admission + media connect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:51:49 +05:30
Sravan 0cee72e73c Add a clear "Stop sharing" banner while sharing your screen
The only way to stop a screen share was toggling the share button again —
buried in the ⋮ More menu on mobile. Now a floating "You're sharing your
screen · Stop" banner appears at the top whenever you're sharing (driven by
meetScreen via updateScreenBtn), with a red Stop button that calls
toggleScreen. Works on iOS/desktop/web. Web-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 16:56:00 +05:30
Sravan c24d5d828a Hole-punch attempt 3: set WebView transparent at startup + scrollView.isOpaque
Previous hole-punch builds showed no video because WKWebView can IGNORE
isOpaque=false when it's set AFTER the page has already rendered (during the
call). Now:
- makeWebViewTransparent() runs once at plugin load (startup): isOpaque=false,
  backgroundColor=.clear, and crucially scrollView.isOpaque=false +
  scrollView.backgroundColor=.clear (an opaque scrollView occludes content
  behind the WebView).
- setHolePunch now just toggles a BLACK backing on the WebView's parent during
  the call (transparency is already applied); restored after.
- Tile frames use host.convert(rect, to: superview) instead of a manual origin
  offset — robust to any WebView inset.

Plugin-only — needs a Codemagic build. If this still shows no video, WKWebView
hole-punch is a dead end here and we revert to native-video-on-top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 14:13:22 +05:30
Sravan d9ea104b73 Fix hole-punch: put native video BEHIND the WebView, not under its scrollView
Diagnosis: no native video rendered at all (bg went black, but camera AND
screen tiles were empty). WKWebView does NOT composite native subviews placed
under its scrollView through transparent web content — only the webView's own
layer background shows through. So the video was effectively invisible.

Fix (the standard hole-punch): insert the video tiles into the WebView's
SUPERVIEW, BEHIND the (fully transparent) WebView, so the web content paints
on top and the video shows through. Changes:
- makeTileView / syncVideoTiles: insert belowSubview: host (the WebView) in
  host.superview, and offset frames by the WebView's origin (getBoundingClientRect
  is viewport-relative).
- webView.backgroundColor = .clear (was .black — a black webView bg would have
  occluded the video behind it); the VC view's black background is the backing.

Plugin-only change — needs a Codemagic build. Web (transparent chain, zoom,
bz-hasvid) already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 12:29:01 +05:30
Sravan c79d78485b Fix hole-punch regressions: transparency chain, black backing, keyboard
Three fixes for the hole-punch build:
1. White background + shared screen not showing: body and .content both use
   var(--bg) (a light colour) and were still opaque, occluding the native
   video behind the WebView. Make the WHOLE meeting chain transparent under
   .bz-hp (body + .content, on top of .meet-grid/tiles).
2. Backing: also set the view controller's view background to black (saved/
   restored) so any transparent gap reads black, not white — belt-and-braces
   with webView.backgroundColor.
3. Keyboard covering the in-call chat: keyboard resize is "none", so the
   absolutely-positioned meet panels don't move for the keyboard. Lift
   .meet-panel by the reported keyboard height (body.kb-open → bottom:
   calc(var(--kb)+12px)); --kb/kb-open are already set globally by the
   Keyboard listener.

Plugin change (VC background) needs a build; the CSS is web-deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 12:13:09 +05:30
Sravan bdd0058c5c Hole-punch: render native video BEHIND a transparent WebView
Permanent fix for the z-order whack-a-mole (controls/menus/panels hiding
behind the native video). Instead of drawing native video ON TOP of the
WebView, draw it BEHIND a transparent WebView so ALL web UI floats on top
naturally — no suppressing, no clamping, no docked-only bar.

Plugin:
- setHolePunch(on): webView.isOpaque=false + black layer bg + clear scroll
  bg (restored on off / call end). Tiles inserted belowSubview:scrollView.
- Dropped native name/mute overlays (the web tile's own .nm/.meet-mute/avatar
  render on top now) and the PaddingLabel.
- Zoom re-plumbed: touches hit the WebView, so native gesture zoom can't work;
  new setTileZoom({uid,scale,tx,ty}) applies a web-forwarded transform to the
  tile's inner video. TileVideoView simplified to a container + applyZoom.

Web:
- bzNativeStartTiles/StopTiles toggle NC.setHolePunch + a body.bz-hp class
  (only when the plugin supports it — old builds keep the suppress fallback).
- Tiles with live native video get .bz-hasvid → CSS makes them transparent +
  hides the web avatar so the video shows through; name/mute/border stay.
- New web-forwarded pinch/pan/double-tap on the shared screen → setTileZoom.
- Suppress-on-overlay + the height clamp now only apply when NOT hole-punched.

Needs a Codemagic build (plugin). Web deployed; no-ops to the prior behavior
on builds without setHolePunch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 11:34:09 +05:30
Sravan f971908d80 Fix: hide native video while a meeting menu/panel is open (z-order)
Native video draws on top of the WebView, so the More/audio menu,
participant panel, meeting chat, modals and the image lightbox all opened
BEHIND the shared screen. Now bzNativeSyncTiles clears the native tiles
whenever any of those overlays is present (.meet-panel/.spk-menu/.modal-ov/
.lightbox), and the menu/panel toggles trigger an immediate sync so there's
no lag; the video returns when they close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 11:14:41 +05:30
Sravan f2efff394b Fix: dock the meeting bar on native calls (decouple it from the shared screen)
The control bar is a draggable FLOATING bar (position:fixed when moved/
restored) meant to sit over the shared screen — which works on web (video is
DOM) but on a native call the screen is a native overlay ABOVE the WebView,
so the floating bar hides behind it, and its live position fed my height
clamp → dragging it resized/reoriented the screen.

Fix: skip makeDraggable on native calls so the bar stays DOCKED in flow at
the bottom. The grid then reserves space above it and the native shared
screen fills that stable area — bar and screen are independent, controls
stay visible and tappable. (A floating bar that overlays the native video
would require a hole-punch rework.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 11:03:35 +05:30
Sravan 5a140e45a1 Fix: clamp native tile height so shared screen never covers the controls
The CSS-only stage sizing wasn't enough — the shared screen still overlapped
the meeting controls. Since the native video is drawn ON TOP of the WebView,
now clamp each tile's height in bzNativeSyncTiles so it can never extend past
the top of the .meet-bar (control bar) — regardless of how the web lays out
the stage. Robust and web-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 10:51:32 +05:30
Sravan b35c95a5be Fix: shared screen no longer overlaps the meeting controls
scr-full forced #meetGrid to height:100%, so the stage (and the native
video positioned on its rect) extended down over the control bar. The grid
is already flex:1 — it should fill only the space above the bar. Now the
stage flexes to fill that area (flex:1 1 auto) instead of height:100%, so
the native screen view sits above the controls, not over them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 10:44:29 +05:30
Sravan a561852067 Fix build: TileVideoView composes VideoView (can't subclass a non-open class)
Build error: `cannot inherit from non-open class 'VideoView' outside of its
defining module` — VideoView is public, not open, so it can't be subclassed
in the plugin module. Reworked TileVideoView from a VideoView subclass into a
UIView CONTAINER that holds a VideoView: the container is frame-synced to the
web tile rect, the pinch/pan zoom transform lives on the inner video (so it
never fights the position poll), and the name/mute overlays sit on the
container so they no longer scale with zoom. track/layoutMode are forwarded.

Also silenced the extension warning: SampleHandler now restates
`@unchecked Sendable`.

The broadcast extension itself compiled + linked in the failed build, so the
SPM-linked extension injection + App Group setup are working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 10:25:46 +05:30
Sravan 27c582bceb Outgoing iOS screen-share via ReplayKit broadcast extension
Lets a native-call user share their iPhone screen (whole device, works
backgrounded). LiveKit 2.15.3 ships the broadcast stack (BroadcastManager +
LKSampleHandler + IPC), so:

- New Broadcast Upload Extension target "BroadcastExtension"
  (com.bizgaze.connect.broadcast): SampleHandler.swift subclasses
  LKSampleHandler; injected by mobile/scripts/add-broadcast-extension.rb
  which also LINKS the LiveKit SPM product into the extension + sets the
  App Group. Sources in mobile/ios-broadcast/.
- Plugin: Room created with ScreenShareCaptureOptions(useBroadcastExtension:
  true); startScreenShare -> BroadcastManager.requestActivation() (system
  picker); stopScreenShare -> requestStop(); BroadcastManagerDelegate ->
  fires screenShareState to the web. LiveKit auto-publishes the track.
- ios-patch.sh: RTCScreenSharingExtension + RTCAppGroupIdentifier keys.
- codemagic.yaml: run the injector + sign the 3rd bundle id (.broadcast).
- home.html: toggleScreen native -> start/stop; screenShareState listener
  reflects state + broadcasts meeting-screen so peers' stage shows it.

REQUIRES a one-time manual Apple portal step: enable the App Group on the
com.bizgaze.connect.broadcast App ID (see mobile/IOS_SETUP.md) or the
archive fails code-signing. SPM-linked extension is new on our CI — expect
build iteration. Web deployed (no-ops on builds without startScreenShare).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 21:09:02 +05:30
Sravan aed3d22675 Native screen-share: maximize the shared screen + pinch-to-zoom
Better visibility: on a native call, a shared screen now fills the whole
meeting area and hides the small participant tiles (new .scr-full grid
class, toggled when meetNative && sharing). The plugin renders the screen
over that full-area stage rect.

Zoom: the tile video view is now TileVideoView with pinch-to-zoom + pan
(and double-tap to reset) enabled only while it's showing a screen. While
zoomed the view holds a transform and syncVideoTiles stops overwriting its
frame; name/mute overlays hide during zoom.

Needs a Codemagic build (plugin change). Web deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 20:56:27 +05:30
Sravan e6d3d2d66a Native video: render others' screen-share on native calls
Last native-video gap: on a native (iOS) call the WebView has no LiveKit
connection, so a screen shared by a web/desktop participant never rendered.
The mesh already flips the sharer's tile into the big "stage" (meeting-
peer-screen -> meetSharers -> sharing-mode), so extend the tile sync: each
tile now carries a `screen` flag (meetSharers.has(id)). The plugin renders
that participant's screen-share track (source .screenShareVideo) on the
tile with layoutMode .fit (contain, no crop) instead of the camera.

Outgoing screen-share from iOS is still unsupported (no getDisplayMedia /
ReplayKit broadcast extension) — toggleScreen now shows a clear toast on
native instead of silently failing.

Needs a Codemagic build (plugin change). Web deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:39:39 +05:30
Sravan a5e2ed8a9d Native video: readable tile labels (red mute, name chip) + flip camera
Label legibility: the name + mute overlays were both white/invisible.
- Mute badge is now a RED (#dc2626) circle with a white mic-slash, matching
  the web tile's .meet-mute, moved to the top-left.
- Name is now white on a dark translucent pill (PaddingLabel) so it stays
  legible over any video, bottom-left.

Front/back camera switch: new NativeCall.switchCamera() flips the local
CameraCapturer front<->back (switchCameraPosition, verified in 2.15.3). New
"Flip camera" button on the meeting bar (switchCamera icon), shown only
while a native call's camera is on (updateFlipBtn). Mirroring auto-corrects
(VideoView mirrorMode .auto only mirrors the front camera).

Needs a Codemagic build (plugin change). Web deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 15:58:12 +05:30
Sravan 34bdd8ea16 Native video: name + mute labels on the native video tiles
The native video covers the web tile's name/mute badges, so redraw those
natively. syncVideoTiles now also carries each tile's name + muted state;
each tile VideoView gets a bottom-left name label (with shadow for
legibility) and a bottom-right mic-slash badge shown when that participant
is muted. Kept above the video renderer via bringSubviewToFront. Web sends
name/muted from meetNames/meetMuted (and ME.name/!meetMic for __local).

Front/back camera switch + screen-share rendering still deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 14:10:54 +05:30
Sravan 4415225407 Native video Increment 2b: remote tiles + self-view on tile; speaker fix
Video (2b): render every participant's camera natively, positioned to
match the web meeting tiles. The web has no LiveKit connection on a native
call, so the plugin draws native VideoViews over the WebView. New
NativeCall.syncVideoTiles({tiles:[{uid,local,x,y,w,h}]}) — the web polls
each tile's getBoundingClientRect + user id (400ms + on camera toggle) and
the plugin places a VideoView (subview of the WKWebView, so CSS-px rects ==
points) for whoever has a live, unmuted camera track; camera-off keeps the
web avatar. Replaces the 2a fixed-corner self-view: your own camera now
renders on the __local tile. Remote video correlated by LiveKit identity ==
meetPeerUids user id. APIs verified vs client-sdk-swift 2.15.3 source:
Room.remoteParticipants[Participant.Identity(from:)], Participant.videoTracks,
TrackPublication.source/.track/.isMuted, Track.Source.camera.

Audio: fix "sound starts on the earpiece until I tap something" — the
LiveKit audio engine starting after CallKit activates the session flips the
route to the receiver. Added an AVAudioSession routeChange observer that
re-asserts the loudspeaker (via preferSpeaker) whenever we land on the
built-in receiver mid-call (headset/BT still win).

Web change is safe on the current (2a) build: syncVideoTiles is absent so
the poll no-ops. Needs a Codemagic build to take effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 11:59:52 +05:30
Sravan 149e32b8d8 Native video Increment 2a: publish local camera + self-view
Wire the camera button on native calls to the plugin instead of a
"not available" toast. New NativeCall.setCamera({on}) calls LiveKit
localParticipant.setCamera(enabled:) so the iOS user's camera is
published to the room — every web/desktop peer renders it via their
existing SFU subscription. Locally the plugin shows a small rounded
self-view (VideoView) pinned top-right over the WebView.

APIs verified against client-sdk-swift 2.15.3 source: setCamera ->
LocalTrackPublication?, TrackPublication.track, VideoView(.track/.layoutMode),
CameraCaptureOptions(position:.front). Front camera only for now.

Rendering the OTHER participants as native tiles synced to the web
meeting grid is Increment 2b (the fragile part) — next build. Until the
new IPA ships, the web branch falls back to an "update the app" toast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 23:10:19 +05:30
Sravan f27b7af9e4 Remove temporary push/call/photos debug telemetry
Push, native calling, and Photos save are all confirmed working, so strip
the diagnostic instrumentation: the pdbg() helper and all its call sites in
home.html (native-setup-*, registration-*, perm-*, nc-* call events,
photos-fail) and the matching /api/push-debug route in routes.js. Real
console.log/console.warn lines and all functional logic are kept; a couple
of pdbg-only error paths now log via console.warn instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 23:00:03 +05:30
Sravan 2c1e1c7ca5 Fix Photos save for images + add native Quick Look file preview
Photos/Files bug: the lightbox download handed nativeSaveFile a bare
"/files/<id>" with no name, extension, or mime. Empty mime made
bzSaveToPhotos short-circuit as "notmedia" — so saveToAlbum (and its
permission prompt) never ran, and the image landed in the generic Files
folder as an extension-less blob. Now nativeSaveFile trusts the server's
Content-Type when the mime is unknown and appends a real extension
(bzExtForMime/bzEnsureExt), so images reach the Images folder AND Photos.

File preview: replace the @capacitor/share "share sheet" open with a new
native FileOpener plugin (QLPreviewController). bzOpenFile now prefers a
real Quick Look preview and only falls back to the share sheet if the
plugin isn't in the build. Wired file-opener into mobile/package.json and
the codemagic SPM diagnostics loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 21:38:48 +05:30
Sravan b2c2acbbc7 Chat files: native download→open control; diagnose Photos-save failures
3. File attachments on native now work like videos: a download icon (with %),
   tap to download into the app's Files folder, then the icon becomes an "open"
   (external-link) control — tap again to open the file via the iOS share/preview
   sheet (@capacitor/share; Quick Look / open-in). State tracked in the local
   library (bzSyncFileTiles), reconciled at startup. Web/PWA keeps the plain
   <a download> link. Added an externalLink icon.

2. Photos save: bzSaveToPhotos now returns a reason; the toast tells the user to
   allow Photos access in Settings when it's permission-denied (the likely cause
   after repeated reinstall testing), and pdbg logs the reason otherwise (noplugin
   vs error) so we can pinpoint it. media-library plugin Swift is unchanged/correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 11:48:47 +05:30
Sravan b68ba94d2a SPM: wire local plugins correctly — add Package.swift to each plugin's files array
Root cause of the SPM runtime regressions (call/chat push when closed, photo save):
our local file: plugins didn't function because their package.json `files` array
omitted "Package.swift", so npm can drop it from node_modules on install → cap sync
can't wire the plugin into the CapApp-SPM package → the plugin isn't loaded at
runtime. Official plugins all list Package.swift in files; ours now do too.

Also add build-log diagnostics (node_modules symlink/copy + Package.swift presence,
and the generated CapApp-SPM/Package.swift) so the wiring is provable, not guessed.

Reapplies the SPM migration (Step B) that was reverted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 15:56:13 +05:30
Sravan 7f54182186 Reapply "Capacitor 8 — Step B: migrate iOS from CocoaPods to Swift Package Manager"
This reverts commit 166bea4314.
2026-08-03 15:52:46 +05:30
Sravan 166bea4314 Revert "Capacitor 8 — Step B: migrate iOS from CocoaPods to Swift Package Manager"
This reverts commit 92f1f8f1a3.
2026-08-03 15:43:23 +05:30
Sravan 92f1f8f1a3 Capacitor 8 — Step B: migrate iOS from CocoaPods to Swift Package Manager
Move off CocoaPods (sunsetting; trunk goes read-only Dec 2026) to SPM, the Cap 8
default. This also ends the LiveKit git-pin hack — LiveKit becomes a real SPM dep.

- Each local plugin gets a Package.swift (swift-tools 5.9, iOS 15, capacitor-swift-pm
  from 8.0.0, source path ios/Sources/<Plugin>). native-call additionally declares
  LiveKit: .package(client-sdk-swift, exact 2.15.3) + product "LiveKit".
- native-call Swift: import LiveKitClient -> import LiveKit (SPM product name).
- codemagic.yaml: `cap add ios --packagemanager SPM`; removed the "Install CocoaPods"
  (pod install) step; XCODE_WORKSPACE -> XCODE_PROJECT (App.xcodeproj); build-ipa
  --workspace -> --project. Xcode resolves the Swift packages during archive.
- ios-patch.sh: removed the Podfile LiveKit git-pin injection + Podfile.lock deletion
  (no Podfile under SPM). Info.plist/entitlements/AppDelegate/notif.wav patches stay.
- add-share-extension.rb is SPM-safe (operates on App.xcodeproj, no workspace/Pods refs).

Authored blind (no local Xcode) — expect build iterations on the first SPM build
(cap-add SPM layout, CapApp-SPM plugin wiring, LiveKit SPM resolution, signing).
Fully revertible: git revert -> back to Cap 8 + CocoaPods (working).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 11:48:45 +05:30
Sravan 9d0fd93d7e Fix outgoing-call re-ring: never send the cancel push to the caller
Hanging up an UNANSWERED outgoing call re-rang the caller's own phone: the server
sent the "cancel" VoIP push to BOTH parties, and the plugin must reportNewIncomingCall
for every VoIP push (iOS rule) → a phantom ring on the caller who just hung up.
Incoming calls don't hit this (an answered call sends no cancel). Fix: DM cancel goes
only to the callee (not startedBy); group cancel skips the starter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 11:38:53 +05:30
Sravan 29106370fd Capacitor 8 upgrade — Step A (keep CocoaPods)
Bump to Capacitor 8 while staying on CocoaPods (decouples the major-version bump
from the SPM switch, per the verified plan). Changes:
- mobile/package.json: all @capacitor/* + @capacitor-community/safe-area + cli → ^8.0.0
  (safe-area 8.0.1 verified Cap 8 compatible; @capacitor/assets stays 3.x, version-agnostic).
- 4 local plugins: @capacitor/core peer/dev dep → ^8.0.0; podspec ios deployment_target 14→15.
- codemagic.yaml: node 20→22 (Cap 8 requires Node 22+); `cap add ios --packagemanager Cocoapods`
  (Cap 8 defaults to SPM — force CocoaPods); fail loudly if no Podfile is generated.
- add-share-extension.rb: fallback deployment target 14→15.
Requirements per the Cap 7→8 guide: Node 22+, Xcode 26+, iOS 15 min. capacitor.config has no
adjustMarginsForEdgeToEdge to remove. LiveKit git-pins + native calling unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 18:48:57 +05:30
Sravan fafc480904 Fix crash after multiple calls: always reportNewIncomingCall for every VoIP push
iOS 13+ terminates the app if a PushKit VoIP push doesn't call reportNewIncomingCall
before completion(). My earlier "no re-ring blip" change made the cancel handler SKIP
the report for already-known/ended calls — which is exactly what iOS kills the app
for, surfacing as a crash after several calls (a cancel push for a prior call's UUID).
Revert to ALWAYS report then immediately end: for a still-active/ringing UUID the
report errors harmlessly (no second ring) and reportCall ends it; only a late duplicate
cancel shows a brief, unavoidable blip. A crash is far worse than a blip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 13:09:25 +05:30
Sravan f4dbbde558 Native calls: route audio to the speaker by default (was earpiece)
The .voiceChat AVAudioSession mode defaults to the earpiece, so call audio came
out of the earpiece and only settled after mute/unmute toggling. Switch to
.videoChat + .defaultToSpeaker + allow Bluetooth so audio goes to the loudspeaker
by default while wired/BT headsets still win. Add preferSpeaker() (override to
speaker when on the built-in receiver) in didActivate AND after mic toggles (the
route can flip back to earpiece on unmute). Report the chosen route in telemetry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 00:50:40 +05:30
Sravan 8c737372de Fix pod install: git-pin LiveKitWebRTC 144.7559.11 too (last SPM-only binary)
LiveKitClient + LiveKitUniFFI now pre-download OK; the last unresolved dep is
LiveKitWebRTC (= 144.7559.11), also SPM-only (CDN only has 125.x). Its repo ships
a podspec at that tag with NO further deps, but its source is an :http release-zip
(not in the git tree), so we pin via :podspec => <podspec URL> (not :git, which
would clone a repo with no xcframework). SwiftProtobuf resolves from the CDN. This
should complete resolution for LiveKit 2.15.3 on CocoaPods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 00:16:59 +05:30
Sravan 21430bd590 Fix pod install: also git-pin LiveKitUniFFI 0.0.6 (LiveKit 2.15.3's SPM-only dep)
--repo-update still failed: "Unable to find a specification for LiveKitUniFFI
(= 0.0.6)". LiveKitUniFFI is genuinely NOT on the CocoaPods CDN (trunk returns
"No pod found") — it's SPM-only — but its repo ships a podspec at tag 0.0.6 with
no further deps (it just downloads its prebuilt XCFramework). So pin it to its
git tag too, alongside LiveKitClient. LiveKitWebRTC 144.x + SwiftProtobuf resolve
from the CDN. This should complete resolution and keep us on CocoaPods (no SPM
migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 20:25:35 +05:30
Sravan 46cc2610b9 Fix pod install: --repo-update for LiveKit 2.15.3's transitive deps
Git pin now works (LiveKitClient 2.15.3 pre-downloaded), but the build box's
cached spec repo is stale: "Unable to find a specification for LiveKitUniFFI
(= 0.0.6)". Both LiveKitUniFFI 0.0.6 and LiveKitWebRTC 144.7559.11 are published
on the trunk, so --repo-update on the real pod install refreshes the repo and
resolves them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 20:14:57 +05:30
Sravan bcd5699e2a Fix pod install: drop the stale Podfile.lock that cap-sync's pod install created
The git-tag pin worked, but the build failed: "could not find compatible versions
… In snapshot (Podfile.lock): LiveKitClient (= 2.0.18) … In Podfile: LiveKitClient
(from git, tag 2.15.3)". Cause: `npx cap sync` runs `pod install` internally with
the PRE-pin Podfile, creating a Podfile.lock pinned to 2.0.18; our injected git-tag
source then conflicts with that lock. Fix: rm the Podfile.lock right after injecting
the pin so the real "Install CocoaPods" step re-resolves against tag 2.15.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 19:55:10 +05:30
Sravan 2c97f45245 Remove stray scratchpad probe files committed by mistake
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 19:47:36 +05:30
Sravan a92cdd69f8 Native calls: get LiveKit 2.15.3 via git-tag pin (stay on CocoaPods) + audio fix
The LiveKitClient CocoaPod on trunk caps at 2.0.18 (2.1+ is SPM-only), so the
2.15 CallKit audio API was unreachable. BUT the repo still ships a valid podspec
at tag 2.15.3, and its deps (LiveKitWebRTC 144.7559.11, LiveKitUniFFI 0.0.6,
SwiftProtobuf) ARE on trunk. So instead of a risky SPM migration:
- ios-patch.sh injects `pod 'LiveKitClient', :git => <repo>, :tag => '2.15.3'`
  into the generated Podfile (after cap sync, before pod install). The NativeCall
  podspec's '~> 2.0' is satisfied by 2.15.3. Idempotent; hard-fails if it can't
  find the App target so we never silently fall back to 2.0.18.
- Plugin re-adds the CallKit<->LiveKit audio-session coordination, now compilable:
  auto-config OFF + engine OFF at load; configure session + enable engine in
  didActivate; disable in didDeactivate; request mic permission on connect.

Core Room APIs (connect/disconnect/setMicrophone) verified compatible between
2.0.18 and 2.15.3 against the real source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 19:46:48 +05:30
Sravan c3de5ac8e3 Revert "Native calls: upgrade to LiveKit 2.15 + add CallKit audio-session coordination"
This reverts commit 455d16abcc.
2026-07-31 19:35:57 +05:30
Sravan 455d16abcc Native calls: upgrade to LiveKit 2.15 + add CallKit audio-session coordination
Root cause of the intermittent dead-mic / no-audio / late-speaker: LiveKit's
automatic AVAudioSession config races CallKit's activation. The fix needs the
2.15+ audio API, which the build wasn't getting ('~> 2.0' resolved an older 2.x
from a stale spec cache). So:
- NativeCall.podspec: pin LiveKitClient '~> 2.15'.
- codemagic.yaml: 'pod install --repo-update' so the spec repo knows 2.15.x.
- Plugin: disable LiveKit auto audio-session config + keep the engine OFF; in
  CXProvider didActivate set the session category and enable the engine; in
  didDeactivate disable it. Request mic permission on connect so enabling the
  engine in didActivate doesn't block on undetermined permission.

All AudioManager APIs verified against the raw 2.15.3 source (setEngineAvailability,
AudioEngineAvailability.default/.none, audioSession.isAutomaticConfigurationEnabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 19:21:39 +05:30
Sravan 155e89c6c0 Fix iOS build: remove LiveKit AudioManager usage (build resolves older 2.x)
Second failure showed 'AudioManager' has no member 'audioSession' either — even
though the 2.15.3 source has both audioSession and setEngineAvailability. So the
build's CocoaPods is resolving an OLDER 2.x (stale spec repo/cache) that predates
the engine-observer audio API. Rather than keep guessing, drop ALL LiveKit
AudioManager audio-session code and keep the last known-good audio behavior
(LiveKit defaults). The valuable fixes that use only CallKit/AVFoundation stay:
re-ring blip guard, CallKit<->UI mute sync, and the WS reportIncomingCall ring
path. Proper CallKit audio-session coordination is deferred until the pod is
pinned/upgraded to LiveKit 2.15+.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 18:36:20 +05:30
Sravan c472ee2618 Fix iOS build: use LiveKit 2.15.3 audio API (drop nonexistent setEngineAvailability)
The build failed: 'AudioManager' has no member 'setEngineAvailability' — that API
is only on unreleased/main docs, not in the resolved LiveKitClient 2.15.3. Use the
API that actually ships (per audio.md): disable LiveKit's automatic AVAudioSession
configuration (AudioManager.shared.audioSession.isAutomaticConfigurationEnabled =
false) and configure the session ourselves in CXProvider didActivate. Removed all
setEngineAvailability(.none/.default) calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 18:24:54 +05:30
Sravan 36edc6de12 Native calls: CallKit audio-session coordination, no re-ring blip, mute sync, WS ring path
Plugin (rides next build):
- Audio: disable LiveKit auto audio-session config + keep the engine OFF, then
  configure the session and start the engine ONLY in CXProvider didActivate
  (stop in didDeactivate). Fixes intermittent dead mic / no audio and the "speaker
  turns on late" routing. Request mic permission on connect so enabling the engine
  in didActivate can't block on undetermined permission (SDK #815).
- Re-ring blip: a cancel push for a call we already ended/known no longer reports
  a NEW incoming call (that was the phantom "rings back for a second"); it ends
  the known call cleanly, and only reports+ends for a truly unknown (cold) call.
- Mute display: answer/outgoing reflect muted-by-default on the CallKit screen;
  setMuted now drives mute THROUGH CallKit so the system screen and the in-app
  meeting UI stay in sync.
- reportIncomingCall: new method to ring CallKit from a WebSocket call event — a
  2nd path alongside the VoIP push for when the app is open (push can be delayed);
  deduped by UUID.

Web (deploys now; the WS ring path activates once the build has the new method):
- onDmCall/onGroupCall call nativeReportIncoming for native incoming calls.
- audioActivated telemetry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 18:09:32 +05:30
Sravan 199c32a319 Native calls: answer MUTED by default (house rule)
Plugin connects the LiveKit room without enabling the mic (nothing captured/
published until the user taps Mic, which is also when iOS asks permission).
Web: meetMic starts false so the mic button shows muted; on callConnected the
web pushes the muted state to the plugin so builds whose plugin still connects
the mic live are muted too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:46:13 +05:30