iOS: screen sharing (ReplayKit) + long-press context menus #12

Merged
thatumi merged 12 commits from feat/ios-screenshare-and-longpress into main 2026-09-27 20:12:19 +00:00
Owner

Mindkettő valódi iPhone-on ellenőrizve (TestFlight build 100).

  • Képernyőmegosztás iOS-en: ReplayKit broadcast extension (új ScreenShareBroadcast target, App Group), egy koppintásos indítás, leállítás az appból is. Részletek és korlátok: docs/IOS_SCREENSHARE.md.
  • Hosszú nyomás = helyi menü iOS-en (a WKWebView nem küld contextmenu eseményt), enyhe rezgéssel (@capacitor/haptics).
  • A hívásablak mostantól kiírja, ha a képernyőmegosztás nem indul el (eddig némán elnyelte - ez rejtette el az iOS hibákat).
  • codemagic.yaml: extension provisioning profil + FORCE_BUILD_NUMBER vészkijárat.

Build + 509 unit teszt zöld; új teszt a getDisplayMedia-telepítés regressziójára.

🤖 Generated with Claude Code

Mindkettő valódi iPhone-on ellenőrizve (TestFlight build 100). - **Képernyőmegosztás iOS-en:** ReplayKit broadcast extension (új `ScreenShareBroadcast` target, App Group), egy koppintásos indítás, leállítás az appból is. Részletek és korlátok: `docs/IOS_SCREENSHARE.md`. - **Hosszú nyomás = helyi menü iOS-en** (a WKWebView nem küld `contextmenu` eseményt), enyhe rezgéssel (`@capacitor/haptics`). - A hívásablak mostantól kiírja, ha a képernyőmegosztás nem indul el (eddig némán elnyelte - ez rejtette el az iOS hibákat). - `codemagic.yaml`: extension provisioning profil + `FORCE_BUILD_NUMBER` vészkijárat. Build + 509 unit teszt zöld; új teszt a getDisplayMedia-telepítés regressziójára. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two live-testing findings from ios-v0.1.10:

- Long-press context menus never worked on iOS: WKWebView, unlike
  Android's Chromium WebView, never synthesizes a `contextmenu` DOM
  event from a long-press touch. installIosLongPressContextMenu()
  times touches by hand and dispatches a synthetic `contextmenu` at
  the touch point once held ~500ms, so every existing onContextMenu
  handler (all wired the same way, per index.css's .jk-ctx-target
  comment) works unmodified. Swallows the trailing ghost click so a
  long-press doesn't also fire the row's onClick.

- iOS screen sharing implemented via a ReplayKit broadcast extension
  (new ScreenShareBroadcast Xcode target, App Group
  group.dev.szabotar.jorkonvo, hand-edited project.pbxproj - no Mac/
  Xcode available locally, structurally validated with a hand-rolled
  ASCII-plist parser but never compiled by a real Xcode yet). Full
  architecture, setup steps done in Apple Developer Portal, and known
  platform-level limitations (no app-side stop, no cancel signal) in
  docs/IOS_SCREENSHARE.md.

codemagic.yaml: added the jorkonvo-screenshare-broadcast provisioning
profile alongside the existing jorkonvo one.

Not yet: compiled by Codemagic, or tested on a real device. Treat the
next Codemagic build as a compile smoke test before any TestFlight
device testing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get-latest-testflight-build-number only sees builds ASC has finished
processing, so a build number can already be reserved (blocking reuse
with a 409) well before the automatic +1 lookup would ever see it -
hit this getting the first ScreenShareBroadcast-extension build
through. Lets a one-off manual build pass an explicit number instead
of blindly retrying the same blocked one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ScreenSharePlugin.start(): the picker's internal button wasn't found
  with a degenerate (1x1, off-screen) frame or a shallow direct-
  subviews search - use a small real-size near-invisible frame inside
  the window and search the whole subtree, retrying briefly since the
  button isn't always present the instant the view is laid out. First
  live-device report: tapping Share screen did nothing.
- add @capacitor/haptics; installIosLongPressContextMenu() fires a
  light haptic tap only when the synthetic contextmenu actually opened
  a menu (event.defaultPrevented), which also fixes a latent bug where
  a long-press over empty space swallowed the next tap for nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The invisible-picker + sendActions() auto-tap (previous commit)
confirmed dead on a real device: tapping Share screen did nothing, no
error, no way to inspect why without device console access this setup
doesn't have. Rather than keep guessing at private view internals,
ScreenSharePlugin now presents the real RPSystemBroadcastPickerView on
a dimmed backdrop and has the user tap it themselves - Apple's
actually-supported flow. Bumped iosScreenShare.ts's start-timeout to
30s to give room for the extra human tap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The screen-share button did nothing on a real iPhone because the
plugin never existed at runtime, for two stacked reasons:
- ScreenSharePlugin.swift was never added to the App target in
  project.pbxproj (only the extension's files were), so it wasn't
  even compiled;
- Capacitor iOS only registers plugins from capacitor.config.json's
  packageClassList, which cap sync fills from npm packages only.

JS ScreenShare.start() therefore rejected as unimplemented, and the
call UI's toggle swallowed the rejection. The two earlier picker
"fixes" were never reached. Adds MainViewController (a
CAPBridgeViewController subclass registering the plugin in
capacitorDidLoad, the iOS twin of Android's MainActivity
registerPlugin) and points Main.storyboard at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GroupCallScreen fired setScreenShareEnabled(true) through a bare
`void toggle(...)`, so every failure - a denied browser prompt, a
native bridge rejecting - vanished and the button simply did nothing.
startScreenShare() now alerts the error name + message.

iosScreenShare.ts tags each native step (addListener, start, waiting
for the broadcast) and bounds each in time, so the message says where
it broke even when a call never settles. There is no device console
access in this project's iOS test loop; this is how the next device
test tells us the real failure instead of another blind rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 70 on a real iPhone finally surfaced the error: livekit-client
threw "DeviceUnsupportedError: getDisplayMedia not supported" - the
polyfill wasn't on navigator.mediaDevices at share time, so the
native bridge was never reached. The installer ran once at boot and
bailed if navigator.mediaDevices didn't exist yet, and only set an
own property on whatever instance existed then.

Now defines it on MediaDevices.prototype (covers a later-created or
swapped instance) as well as the current instance, idempotently, and
GroupCallScreen re-runs it right before each share start. The alert
for this error now also reports typeof mediaDevices/getDisplayMedia.
Unit tests pin both failure modes (fail on the old code, pass now).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 80 got past getDisplayMedia and failed with '"ScreenShare"
plugin is not implemented on ios': SceneDelegate creates the root
view controller in code (CAPBridgeViewController()), so the earlier
Main.storyboard change never took effect and MainViewController -
which registers ScreenSharePlugin - was never instantiated.
SceneDelegate now builds MainViewController().

Also: SampleHandler wrote frames via tmp file + replaceItemAt, which
can fail when the target doesn't exist yet (the first frame of every
broadcast); plain .atomic writes already rename a temp file into place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 90 worked end to end on a real iPhone, but with a visible
picker button to tap and stopping only via the Dynamic Island.

- Start: the picker is added invisibly and its internal button is
  sendActions()-tapped, so our own button goes straight to Apple's
  mandatory sheet. Falls back to the visible picker if the button
  can't be found. The earlier note that this auto-tap was "confirmed
  dead on a real device" was wrong - in those builds the plugin was
  never compiled in or registered, so it never ran.
- Stop: ScreenSharePlugin.stop() posts a Darwin notification the
  extension answers with finishBroadcastWithError, ending the system
  broadcast from inside (iOS shows a short "stopped" alert; there's
  no silent variant). Previously the docs said in-app stop was
  impossible - also wrong.

Docs/comments corrected accordingly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hub: iOS screen share + long-press device-verified; refresh next/needs
Some checks failed
CI / build-and-test (pull_request) Successful in 38s
CI / e2e (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled
ad50dadaf3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(privacy): describe iOS screen sharing (ReplayKit broadcast)
Some checks failed
CI / build-and-test (pull_request) Successful in 40s
CI / android (pull_request) Successful in 2m48s
CI / e2e (pull_request) Has been cancelled
6f17b661d9
Mirrors the Android MediaProjection paragraph in both languages and
the public privacy.html: user-started, Apple's system prompt + iOS
recording indicator, frames stay on-device until sent into the call,
only the latest frame kept temporarily while sharing, no audio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(mobile): layout stuck shifted after going from Home/a DM into a server
All checks were successful
CI / build-and-test (pull_request) Successful in 38s
CI / android (pull_request) Successful in 2m44s
CI / e2e (pull_request) Successful in 3m24s
ad883ae28c
Owner report (iOS): the whole picture slides off, only a force-quit
fixes it - almost always when switching from Home/a DM to a server.

Root cause, reproduced in WebKit (Playwright, same structure as the
mobile shell): entering a server jumps its channel to the unread
divider / saved position via el.scrollIntoView() on the next frame -
while MainLayout's 240ms slide-in still has the chat pane off screen.
scrollIntoView scrolls every scrollable ancestor sideways, including
the overflow-hidden swipe row: shell scrollLeft became 390 (one full
screen width) and nothing ever reset it. DMs rarely take the jump
path, servers usually do - hence "Home/DM -> server".

- Timeline: centerInTimeline() scrolls only the timeline's own
  scroller (both jump-to-event and scroll-to-unread). WebKit repro:
  shell scrollLeft stays 0, timeline lands on the target.
- Backstop: swipe containers use .jk-swipe-clip (overflow: clip, with
  hidden as the iOS<16 fallback) + an onScroll reset, and
  applyTransform() zeroes any container scroll offset.
- Related iOS keyboard variant: blur a focused text field before a
  mobile pane slide, and reset the document scroll on focusout (the
  shell never scrolls as a document; iOS can leave it scrolled after
  the keyboard closes on an unmounting input).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thatumi/JorKonvo!12
No description provided.