@desklink/react-native 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,16 @@
1
- # @desklink/react-native
1
+ <h1 align="center">@desklink/react-native</h1>
2
2
 
3
- Show a live desktop in a React Native app, and drive it from the phone.
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/@desklink/react-native"><img alt="npm" src="https://img.shields.io/npm/v/@desklink/react-native?style=flat&label=npm" /></a>
5
+ <a href="https://github.com/umeranjum17/desklink/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/umeranjum17/desklink/ci.yml?style=flat&branch=main" /></a>
6
+ <a href="LICENSE"><img alt="Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-666?style=flat" /></a>
7
+ <img alt="Android, iOS and web" src="https://img.shields.io/badge/Android%20%7C%20iOS%20%7C%20web-111?style=flat" />
8
+ </p>
9
+
10
+ <p align="center">
11
+ <strong>Show a live desktop in a React Native app, and drive it from the phone.</strong><br/>
12
+ It renders a desktop that <code>@desklink/host</code> serves on Android, iOS and the web, with touch as a desktop pointer the phone can see, sticky modifier keys, and a session that recovers from a dropped network on its own.
13
+ </p>
4
14
 
5
15
  The package is a **view and a session**, not a screen: it renders the picture and
6
16
  turns touch, the keyboard and the clipboard into that session's input. Where it
@@ -9,6 +19,10 @@ the application.
9
19
 
10
20
  ## Install
11
21
 
22
+ The npm badge at the top of this page tracks the registry version, and npm
23
+ verifies package integrity on install. See the [changelog](../../CHANGELOG.md)
24
+ for release changes. Supported platforms: Android, iOS and web.
25
+
12
26
  The package contains native code and is not an Expo Go package. Inside this
13
27
  repository it resolves through the workspace; anywhere else, install it from npm:
14
28
 
@@ -17,10 +31,29 @@ npx expo install @desklink/react-native # or yarn add, then prebuild/rebuil
17
31
  ```
18
32
 
19
33
  Android uses an Expo native module; iOS uses the app's existing
20
- `react-native-webrtc` peer connection and `RTCView` from JavaScript. Both need a
21
- dev client or native build containing that binding. Web uses the browser's WebRTC.
34
+ `react-native-webrtc` peer connection and `RTCView` from JavaScript, plus a small
35
+ Expo module of its own for the hardware keyboard and an iPad pointer. Both need a
36
+ dev client or native build containing that binding ([development builds](https://docs.expo.dev/develop/development-builds/introduction/); [Expo Go](https://docs.expo.dev/workflow/expo-go/) cannot load them). Web uses the browser's WebRTC.
22
37
  No second WebRTC binary is linked. The Android module has its own hardware-first
23
- decoder factory; iOS uses the decoder shipped by `react-native-webrtc`.
38
+ decoder factory; iOS uses the decoder shipped by `react-native-webrtc` and asks
39
+ the host for H.264, which that decoder runs in hardware.
40
+
41
+ On iOS the WebRTC binding also needs its config plugin
42
+ (`@config-plugins/react-native-webrtc` — 14.x for Expo SDK 55, 15.x for
43
+ Expo SDK 56 and later):
44
+
45
+ ```json
46
+ { "expo": { "plugins": ["@config-plugins/react-native-webrtc", "@desklink/react-native"] } }
47
+ ```
48
+
49
+ On iPad, a trackpad or mouse hovers and scrolls the desktop as it is. For its
50
+ presses to carry their buttons — a click-drag that selects, a secondary click
51
+ that is a right click — the package's entry in the plugins list above sets
52
+ [`UIApplicationSupportsIndirectInputEvents`](https://developer.apple.com/documentation/bundleresources/information-property-list/uiapplicationsupportsindirectinputevents).
53
+
54
+ That key changes how every screen of the app receives trackpad input, so the
55
+ package never sets it on its own; without the plugin nothing about the app
56
+ changes, and trackpad presses reach the desktop as touches.
24
57
 
25
58
  ## Use
26
59
 
@@ -56,21 +89,52 @@ import { desktopAvailable } from '@desklink/react-native/availability';
56
89
  {desktopAvailable && <ComputerAction onPress={openDesktop} />}
57
90
  ```
58
91
 
92
+ ### Frame rate and encode size
93
+
94
+ The engine streams at up to 60 fps unless `session.maxFps` names a lower cap —
95
+ omit it for the smooth path. A still desktop costs nothing at 60, because the
96
+ engine only produces frames on change.
97
+
98
+ Keep the encoded picture inside a 1920×1080 box:
99
+
100
+ ```tsx
101
+ session: { permissions: CONTROL_PERMISSIONS, maxWidth: 1920, maxHeight: 1080 },
102
+ ```
103
+
104
+ The engine never upscales, so a smaller desktop is sent at its own size and a
105
+ larger one is scaled to fit. Above that box the encoder cannot sustain 50+ fps
106
+ under full-screen motion.
107
+
108
+ On iOS, keep the box at or below the device's own pixels and `maxFps` at 30:
109
+ the iOS receiver asks the engine for H.264, which iOS decodes in hardware
110
+ (VideoToolbox), but a host without an H.264 encoder still sends VP9, which
111
+ iOS decodes in software — so a 1440p or 4K picture on such a host costs CPU,
112
+ heat and battery.
113
+
59
114
  ### What the package guarantees
60
115
 
61
- - **VP9 decoding.** Android builds a session-scoped hardware-first decoder
116
+ - **Hardware decoding where the platform has it.** Android and web decode the
117
+ host's default VP9; Android builds a session-scoped hardware-first decoder
62
118
  factory with software fallback. iOS uses the app's `react-native-webrtc`
63
- decoder; neither platform links a second WebRTC stack.
119
+ decoder, which decodes H.264 through VideoToolbox but VP9 only in software,
120
+ so the iOS receiver asks for H.264 by putting it first in the offer it
121
+ applies; a host with an H.264 encoder then sends H.264, and one without sends
122
+ VP9. Neither platform links a second WebRTC stack.
64
123
  - **A sharp, zoomable picture.** The desktop fits the view by default; a pinch
65
124
  zooms up to 2.5 view pixels per desktop pixel and one finger moves around the
66
125
  zoomed desktop. On Android the decoded frame is copied once into the view's
67
126
  own texture and drawn with a multi-tap filter when it is shown smaller than
68
127
  its size, so a fitted 4K desktop does not alias and a pinch redraws at once.
69
- iOS uses `RTCView`'s native renderer. `fitToView()` shows the whole desktop again.
128
+ iOS uses `RTCView`'s native renderer. That renderer draws a frame only when
129
+ its timestamp is new, and a receiver honouring the engine's zero playout delay
130
+ stamps every frame alike, so the iOS peer declines that hint and keeps the
131
+ receiver's usual jitter buffer. `fitToView()` shows the whole desktop again.
70
132
  - **A pointer a phone can see.** Once a touch has sent the desktop's pointer
71
133
  somewhere, the view draws it there at a readable size, over a picture whose
72
134
  own cursor is a few pixels tall or not captured at all. Android hides the
73
- extra mark when a mouse is attached.
135
+ extra mark when a mouse is attached, iOS when an iPad trackpad or mouse
136
+ drives it, and the `device` profile draws no mark on any platform — the
137
+ screen being touched shows the finger itself.
74
138
  - **The picture above the keyboard.** While the phone's keyboard is up, the
75
139
  picture sits above it and above the room the app keeps for its own controls
76
140
  (`keyboardClearance`). Android follows the keyboard animation and keeps the
@@ -88,14 +152,65 @@ import { desktopAvailable } from '@desklink/react-native/availability';
88
152
  under the finger while the picture catches up; two fingers scroll the desktop
89
153
  under them, or pinch; a quick two-finger tap is a right click too. Scrolling
90
154
  is fractional wheel steps, smooth where the desktop supports high-resolution
91
- wheels. Android coalesces pending pointer moves under congestion.
155
+ wheels. Android coalesces pending pointer moves under congestion. The
156
+ `gestures` prop reshapes one finger for what the picture shows, and can be
157
+ changed mid-session: `browser` scrolls the page under one finger (a tap
158
+ still clicks and a hold still right-clicks), `device` presses the screen —
159
+ the finger lands with the button down, drags with it held and lifts to
160
+ release, so a tap clicks and a hold holds with no long-press right click,
161
+ and no drawn pointer — while the default `desktop` is the trackpad above.
162
+ Two fingers scroll and pinch the same under every profile.
163
+ - **A hardware keyboard and a trackpad.** A hardware keyboard's keys reach the
164
+ desktop with their down and up and what is held: arrows, Esc, Tab, Home/End,
165
+ Page Up/Down, Delete and the modifiers, with Control and Command
166
+ chords as the key's own letter (iOS sends Command as the desktop's Meta);
167
+ other keys type their text. On iOS the view holds the keyboard while control
168
+ is enabled and gives it to the on-screen keyboard while the app shows it;
169
+ shortcuts the system keeps, such as Command-Tab and the Globe key, stay with
170
+ the system. An iPad pointer hovers the desktop's pointer, scrolls under it,
171
+ and — with the config plugin — presses, drags and right-clicks with its own
172
+ buttons.
92
173
  - **The keys a phone lacks.** `modifiers`, `tapModifier` and `pressKey` give
93
174
  sticky Ctrl and Shift: tap arms one for the next key, tap again locks it.
94
175
  While one is armed, the next key or character the phone's keyboard types is
95
176
  sent as that key's chord, so Ctrl then v is Ctrl+V. The app draws the keys.
96
- - **Readiness is a rendered frame.** Android marks the first draw; iOS uses
97
- `RTCView`'s native video-dimensions callback, which fires after a decoded
98
- picture arrives. Neither marks a track or ICE connection as live.
177
+ - **Readiness is a rendered frame.** Android marks the first draw; iOS marks
178
+ the first `RTCView` video-dimensions callback and ignores later ones, so a
179
+ resize or remount during a reconnect does not read as live. Neither marks
180
+ a track or ICE connection as live.
181
+ - **Connection states, as they happen.** The session status is `idle`,
182
+ `opening`, `connecting`, `live`, `reconnecting`, `ended` or `failed`. The
183
+ peer's ICE state and the engine's `session.state` both feed it: a
184
+ `disconnected` path reads as `reconnecting` within about a second of the
185
+ drop — never a frozen picture that still says live — and a recovered path
186
+ reads as `live` again once ICE is `connected`. A path that reports
187
+ `failed` reopens the session with fresh authority instead of staying stuck.
188
+ Every event carries its engine session id where the carrier preserves it,
189
+ and the session ignores anything naming a session it no longer holds, so a
190
+ previous generation's queued offer or revocation cannot corrupt or kill a
191
+ healthy recovery.
192
+ Detection is two-layered: ICE and engine `session.state` events, plus the
193
+ engine's twice-a-second control-channel heartbeat (`{"kind":"ping"}`) — a
194
+ path that stops heartbeating reads as stalled within ~1.5 s even while ICE
195
+ consent timers are still making up their minds, and resumed heartbeats read
196
+ as recovered. Clients on engines that predate the heartbeat simply never
197
+ arm that watchdog.
198
+ - **ICE restarts before reopens.** When the path drops after frames were
199
+ shown, the session asks the engine (`session.restart_ice`) to re-offer on
200
+ the same peer connection: the fresh offer arrives as the usual
201
+ `session.description` event and is answered as usual, all without reopening
202
+ the session. The first restart waits ~2 s for blips that heal alone;
203
+ retries back off across ~60 s, so a 30–60 s outage still recovers when the
204
+ network returns. When the restart schedule is spent without the peer
205
+ reporting `failed`, the session reopens instead of sitting in
206
+ `reconnecting`. An engine revocation with code `transport` (the path was
207
+ lost) also reopens the session; any other revocation code, or none, ends
208
+ it, as does a deliberate close. A return to the foreground after ~30 s away
209
+ reopens at once rather than spending restarts on a lapsed path. Those
210
+ reopens back off across ~60 s too instead of spending one attempt while the
211
+ network is still down. Renegotiation transients from a restart the session
212
+ asked for itself are expected, not a new outage: they neither flip the
213
+ status nor trigger another restart for a short grace window.
99
214
  - **Control starts off.** `setInputEnabled(true)` enables pointer, keyboard and
100
215
  clipboard input; `setInputEnabled(false)` blocks new input, releases held keys
101
216
  and buttons, and resets pending gestures. The app decides when to enable it
@@ -109,7 +224,10 @@ import { desktopAvailable } from '@desklink/react-native/availability';
109
224
  ### `Signaling`
110
225
 
111
226
  The one thing the app must supply. It carries the engine's local protocol over
112
- whatever authenticated channel the app already has:
227
+ whatever authenticated channel the app already has.
228
+
229
+ See the [BYOKit integration guidance](../../README.md#why-desklink-exists)
230
+ for the application's pairing, identity and signaling transport.
113
231
 
114
232
  ```ts
115
233
  interface Signaling {
@@ -119,9 +237,72 @@ interface Signaling {
119
237
  ```
120
238
 
121
239
  `method` is one of `session.open`, `session.description`, `session.candidate`,
122
- `session.close`; `SessionEvent` is the engine's offer, candidates, state and
123
- revocation. Pixels and input do **not** use this channel — they are the
124
- desktop's own WebRTC session.
240
+ `session.restart_ice`, `session.close`; `SessionEvent` is the engine's offer,
241
+ candidates, state and revocation. Pixels and input do **not** use this
242
+ channel — they are the desktop's own WebRTC session.
243
+
244
+ ## Shipping on iOS
245
+
246
+ What the consuming app — not this package — must get right before review.
247
+
248
+ - **Local network.** A direct LAN session connects to host ICE candidates,
249
+ which prompts for local-network access, so the app needs
250
+ `NSLocalNetworkUsageDescription`. The first connection can be denied before
251
+ the user answers; the session's restart/reopen path recovers from that.
252
+ `NSBonjourServices` is only needed if the app adds its own Bonjour
253
+ discovery — desklink discovers no hosts. Relay-only sessions never prompt.
254
+ For app-supplied routes, see the
255
+ [BYOKit integration guidance](../../README.md#why-desklink-exists).
256
+ See [TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy).
257
+ - **Export compliance.** The viewer's DTLS-SRTP comes from libwebrtc bundled
258
+ with `react-native-webrtc`, not from the OS, so the app must check the
259
+ exemption criteria itself rather than assume
260
+ `ITSAppUsesNonExemptEncryption` is `NO` — standard-protocol encryption is
261
+ commonly exempt but can still need a yearly self-classification report.
262
+ See [complying with encryption export regulations](https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations).
263
+ - **Review.** A viewer that mirrors the whole host device generically falls
264
+ outside the remote-desktop conditions; one that mirrors only specific
265
+ software must connect LAN-only to a user-owned host. Which case applies is
266
+ the app's product shape to decide. Declare no `audio` or `voip` background
267
+ mode to keep the stream alive — review rejects that. See the
268
+ [App Store review guidelines](https://developer.apple.com/app-store/review/guidelines/)
269
+ (§4.2.7, §2.5.4).
270
+ - **Backgrounding.** There is no compliant background mode for a desktop
271
+ stream: an app away longer than ~30 s comes back to a closed session, and
272
+ the session reopens on return to the foreground rather than resuming. Keep
273
+ disabling input on background; the engine releases held keys within
274
+ seconds of the path dying, so nothing stays stuck.
275
+
276
+ ## iOS simulator proof
277
+
278
+ `example/` is the smallest app that shows a desktop: a native (non-Expo Go)
279
+ build with `react-native-webrtc`, connecting to `desklink-host bridge` with the
280
+ URL it is launched with. `test/ios-flow.mjs` builds it for the iOS simulator on
281
+ a Mac over ssh, serves a private Xvfb desktop from this machine through the
282
+ bridge, and checks that the first frame is presented, that the answer puts
283
+ H.264 first with NACK when the host offers it (VP9 otherwise; both descriptions
284
+ are written out), that a tap clicks the host, that
285
+ the picture keeps following the desktop afterwards, and that hardware keys
286
+ pressed on the simulator reach the desktop. With `DESKLINK_IOS_IPAD=1` it runs on
287
+ an iPad simulator and also drives a trackpad through XCUITest
288
+ (`test/ios-pointer/`): hover, a click-drag that selects text, a right click and
289
+ a scroll:
290
+
291
+ ```sh
292
+ npm run build && cargo build --manifest-path packages/desktop-host/engine/Cargo.toml
293
+ DESKLINK_IOS_MAC=user@mac npm run test:ios --workspace @desklink/react-native
294
+ ```
295
+
296
+ The Mac needs Xcode with an iOS simulator runtime, CocoaPods, node and
297
+ [`axe`](https://github.com/cameroncooke/AXe) on its login `PATH`; everything
298
+ the run builds or caches stays in `~/desklink-ios` there. When the selected
299
+ Xcode has no simulator platform, point `DESKLINK_IOS_XCODE` at one that does;
300
+ `DESKLINK_IOS_RUNTIME` picks the iOS version (for example `18.6`); the
301
+ script's header lists the other settings. To try the example by hand against any bridge:
302
+
303
+ ```sh
304
+ xcrun simctl launch <device> dev.desklink.example -desklinkUrl 'ws://HOST:PORT/desktop?token=TOKEN'
305
+ ```
125
306
 
126
307
  ## Licence
127
308
 
@@ -126,6 +126,10 @@ class DesklinkModule : Module() {
126
126
  view.setKeyboardClearance(clearance ?: 0f)
127
127
  }
128
128
 
129
+ Prop("gestures") { view: DesktopView, gestures: String? ->
130
+ view.setGestures(gestures ?: "desktop")
131
+ }
132
+
129
133
  OnViewDestroys { view: DesktopView -> view.release() }
130
134
 
131
135
  OnViewDidUpdateProps { view: DesktopView ->
@@ -66,7 +66,9 @@ private const val MIN_WHEEL_STEP = 0.05f
66
66
  * - press and hold: a right click on release, or drag after it to hold the left
67
67
  * button (select text, move a window);
68
68
  * - one finger: move around a zoomed-in desktop; on the whole desktop,
69
- * move its pointer, which follows the finger without pressing a button;
69
+ * move its pointer, which follows the finger without pressing a button —
70
+ * unless `gestures` is `browser`, where it scrolls the page, or `device`,
71
+ * where it lands with the button down and drags with it held;
70
72
  * - two fingers: scroll the desktop under them, or pinch to zoom (and move) the
71
73
  * picture; a quick two-finger tap is a right click.
72
74
  */
@@ -81,6 +83,23 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
81
83
  private var surfaceWidth = 0
82
84
  private var surfaceHeight = 0
83
85
 
86
+ /** Which touch meaning this surface uses; a browser page and a device screen answer one finger differently. */
87
+ var gestures = "desktop"
88
+ private set
89
+
90
+ fun setGestures(value: String) {
91
+ if (value == gestures) return
92
+ // A held drag belongs to the old meaning: let go rather than release the
93
+ // button somewhere the finger never meant.
94
+ if (gesture == Gesture.DRAG) session?.sendCancel()
95
+ cancelGesture()
96
+ gestures = value
97
+ if (value == "device" && pointerAt != null) {
98
+ pointerAt = null
99
+ placePointer()
100
+ }
101
+ }
102
+
84
103
  // The picture's placement: surface pixels per desktop pixel, and where the
85
104
  // desktop's top-left corner sits in this view. `fitted` keeps a picture that
86
105
  // was showing the whole desktop showing it when the view changes size.
@@ -449,7 +468,8 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
449
468
  /** Send the desktop's pointer somewhere, and show it there. */
450
469
  private fun pointerTo(active: DesktopSession, phase: String, at: Pair<Int, Int>, withButton: Boolean = false) {
451
470
  active.sendPointer(phase, at.first, at.second, withButton)
452
- if (!mouseInput) showPointer(at)
471
+ // A device screen shows the touch itself; a mark would be a second finger that lies.
472
+ if (!mouseInput && gestures != "device") showPointer(at)
453
473
  }
454
474
 
455
475
  private fun showPointer(at: Pair<Int, Int>) {
@@ -519,6 +539,25 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
519
539
  }
520
540
  when (event.actionMasked) {
521
541
  MotionEvent.ACTION_DOWN -> {
542
+ // A device screen is pressed, not pointed at: the finger lands with the
543
+ // button down, drags with it held, and lifts to release. A tap is the
544
+ // same down and up with no move, so it clicks for free, and a hold
545
+ // holds for free — there is no long press to arm.
546
+ if (gestures == "device") {
547
+ downX = event.x
548
+ downY = event.y
549
+ lastX = event.x
550
+ lastY = event.y
551
+ val at = point(event.x, event.y)
552
+ if (at == null) {
553
+ gesture = Gesture.LETTERBOX
554
+ } else {
555
+ gesture = Gesture.DRAG
556
+ lastTapPoint = null
557
+ pointerTo(active, "down", at)
558
+ }
559
+ return true
560
+ }
522
561
  downOnPicture = point(event.x, event.y) != null
523
562
  gesture = if (fitted && !downOnPicture) Gesture.LETTERBOX else Gesture.PENDING
524
563
  downX = event.x
@@ -573,6 +612,15 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
573
612
  Gesture.PENDING -> if (hypot(event.x - downX, event.y - downY) > touchSlop) {
574
613
  removeCallbacks(longPress)
575
614
  lastTapPoint = null
615
+ // A page scrolls under one finger; the desktop's pointer rides along
616
+ // so the scroll lands where the finger is.
617
+ if (gestures == "browser") {
618
+ gesture = Gesture.SCROLL
619
+ lastX = event.x
620
+ lastY = event.y
621
+ point(event.x, event.y)?.let { pointerTo(active, "move", it) }
622
+ return true
623
+ }
576
624
  // The whole desktop has nowhere to move to, so the finger moves the
577
625
  // desktop's pointer instead, without a button.
578
626
  gesture = if (!fitted) Gesture.PAN else if (downOnPicture) Gesture.HOVER else Gesture.LETTERBOX
@@ -599,6 +647,15 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
599
647
  Gesture.DRAG -> dragTo(active, event.x, event.y)
600
648
 
601
649
  Gesture.TWO, Gesture.PINCH, Gesture.SCROLL -> if (event.pointerCount >= 2) twoFingers(active, event)
650
+ else if (gesture == Gesture.SCROLL && gestures == "browser") {
651
+ // One finger dragging a page: content follows it, in pixel deltas
652
+ // turned to detents like the two-finger scroll.
653
+ wheelX -= (event.x - lastX) / scale / PIXELS_PER_DETENT
654
+ wheelY -= (event.y - lastY) / scale / PIXELS_PER_DETENT
655
+ lastX = event.x
656
+ lastY = event.y
657
+ flushWheel(active, force = false)
658
+ }
602
659
 
603
660
  else -> {}
604
661
  }
@@ -618,6 +675,7 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
618
675
  removeCallbacks(longPress)
619
676
  when (gesture) {
620
677
  Gesture.PENDING -> tap(active, event.x, event.y)
678
+ Gesture.SCROLL -> flushWheel(active, force = true)
621
679
  Gesture.HOVER -> hoverTo(active, event.x, event.y)
622
680
  Gesture.ARMED -> point(downX, downY)?.let { rightClick(active, it) }
623
681
  Gesture.DRAG -> endDrag(active, event.x, event.y)
@@ -669,7 +727,7 @@ class DesktopView(context: Context, appContext: AppContext) : ExpoView(context,
669
727
 
670
728
  private fun rightClick(active: DesktopSession, at: Pair<Int, Int>) {
671
729
  active.sendRightClick(at.first, at.second)
672
- if (!mouseInput) showPointer(at)
730
+ if (!mouseInput && gestures != "device") showPointer(at)
673
731
  }
674
732
 
675
733
  private fun twoFingers(active: DesktopSession, event: MotionEvent) {
package/app.plugin.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Opt-in config plugin: an iPad trackpad's or mouse's presses reach the
3
+ * desktop with the button that made them — a drag selects, a secondary click
4
+ * is a right click. It sets `UIApplicationSupportsIndirectInputEvents`, which
5
+ * changes how every screen of the app receives that input, so the package
6
+ * never sets it on its own. Hover, scroll and the hardware keyboard work
7
+ * without it.
8
+ *
9
+ * "plugins": ["@desklink/react-native"]
10
+ */
11
+ module.exports = function withDesklinkPointer(config) {
12
+ return {
13
+ ...config,
14
+ ios: { ...config.ios, infoPlist: { ...config.ios?.infoPlist, UIApplicationSupportsIndirectInputEvents: true } },
15
+ };
16
+ };
@@ -1,6 +1,9 @@
1
1
  {
2
- "platforms": ["android"],
2
+ "platforms": ["android", "ios"],
3
3
  "android": {
4
4
  "modules": ["expo.modules.desklink.DesklinkModule"]
5
+ },
6
+ "ios": {
7
+ "modules": ["DesklinkInputModule"]
5
8
  }
6
9
  }
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'DesklinkInput'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = 'desklink'
12
+ s.homepage = 'https://github.com/umeranjum17/desklink'
13
+ s.platforms = { :ios => '15.1' }
14
+ s.swift_version = '5.9'
15
+ s.source = { git: 'https://github.com/umeranjum17/desklink.git' }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+
20
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
21
+ s.source_files = '*.swift'
22
+ end
@@ -0,0 +1,20 @@
1
+ import ExpoModulesCore
2
+
3
+ /**
4
+ * The iOS view's hardware input: the keys and the pointer React Native's touch
5
+ * system cannot see. The session, the picture and touch gestures stay in
6
+ * JavaScript; this module only reports what UIKit delivers.
7
+ */
8
+ public final class DesklinkInputModule: Module {
9
+ public func definition() -> ModuleDefinition {
10
+ Name("DesklinkInput")
11
+
12
+ View(DesklinkInputView.self) {
13
+ Events("onKey", "onPointer", "onWheel")
14
+
15
+ Prop("enabled") { (view: DesklinkInputView, enabled: Bool) in
16
+ view.enabled = enabled
17
+ }
18
+ }
19
+ }
20
+ }