@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 +198 -17
- package/android/src/main/java/expo/modules/desklink/DesklinkModule.kt +4 -0
- package/android/src/main/java/expo/modules/desklink/DesktopView.kt +61 -3
- package/app.plugin.js +16 -0
- package/expo-module.config.json +4 -1
- package/ios/DesklinkInput.podspec +22 -0
- package/ios/DesklinkInputModule.swift +20 -0
- package/ios/DesklinkInputView.swift +209 -0
- package/package.json +11 -2
- package/src/DesktopView.ios.spec.tsx +354 -0
- package/src/DesktopView.ios.tsx +244 -27
- package/src/DesktopView.tsx +9 -2
- package/src/DesktopView.web.tsx +14 -2
- package/src/hardwareKeys.ts +61 -0
- package/src/native.ios.ts +53 -5
- package/src/native.web.ts +86 -5
- package/src/protocol.ts +34 -7
- package/src/useDesktopSession.ts +496 -63
- package/src/webrtc.ios.ts +10 -0
package/src/native.web.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NativeDesklinkModule, NativeEventName, NativeSessionEvent } from './native';
|
|
2
|
+
import type { GestureProfile } from './protocol';
|
|
2
3
|
import { observeWebKeyboardMotion } from './webKeyboardMotion';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -114,6 +115,8 @@ interface WebSession {
|
|
|
114
115
|
longPress: ReturnType<typeof setTimeout> | null;
|
|
115
116
|
/** While a sticky modifier waits for its key, typing is the session's to chord. */
|
|
116
117
|
captured: boolean;
|
|
118
|
+
/** Which touch meaning this surface uses; a browser page and a device screen answer one finger differently. */
|
|
119
|
+
gestures: GestureProfile;
|
|
117
120
|
inputEnabled: boolean;
|
|
118
121
|
/** The word the keyboard is composing, and how much of it has already gone. */
|
|
119
122
|
composition: string;
|
|
@@ -378,8 +381,9 @@ function cancelLongPress(session: WebSession): void {
|
|
|
378
381
|
function pointer(session: WebSession, phase: 'move' | 'down' | 'up' | 'cancel', at: { x: number; y: number }, button?: number): void {
|
|
379
382
|
control(session, { kind: 'pointer', phase, x: at.x, y: at.y, ...(button === undefined ? {} : { button }), seq: seq(session) });
|
|
380
383
|
// A mouse brings its own cursor; a finger leaves nothing on the glass to
|
|
381
|
-
// show where the desktop's pointer went, so the mark does.
|
|
382
|
-
|
|
384
|
+
// show where the desktop's pointer went, so the mark does. A device screen
|
|
385
|
+
// shows the touch itself, so a mark would be a second finger that lies.
|
|
386
|
+
if (session.mouse || phase === 'cancel' || session.gestures === 'device') return;
|
|
383
387
|
session.pointerAt = { x: at.x, y: at.y };
|
|
384
388
|
placePointer(session);
|
|
385
389
|
}
|
|
@@ -391,13 +395,15 @@ function click(session: WebSession, at: { x: number; y: number }, button: number
|
|
|
391
395
|
|
|
392
396
|
/**
|
|
393
397
|
* Touch gestures, the ones mature remote-desktop viewers settled on — the same
|
|
394
|
-
* as the native view's:
|
|
398
|
+
* as the native view's. The profile reshapes one finger only:
|
|
395
399
|
*
|
|
396
400
|
* - tap: click where the finger lands; two taps: a double click on one spot;
|
|
397
401
|
* - press and hold: a right click on release, or drag after it to hold the
|
|
398
402
|
* left button (select text, move a window);
|
|
399
403
|
* - one finger: move around a zoomed-in desktop; on the whole desktop,
|
|
400
|
-
* move its pointer, which follows the finger without pressing a button
|
|
404
|
+
* move its pointer, which follows the finger without pressing a button —
|
|
405
|
+
* unless the profile is `browser`, where it scrolls the page, or `device`,
|
|
406
|
+
* where it lands with the button down and drags with it held;
|
|
401
407
|
* - two fingers: scroll the desktop under them, or pinch to zoom (and move)
|
|
402
408
|
* the picture; a quick two-finger tap is a right click.
|
|
403
409
|
*
|
|
@@ -534,6 +540,27 @@ function attachGestures(session: WebSession): () => void {
|
|
|
534
540
|
}
|
|
535
541
|
session.touches.set(event.pointerId, { x, y });
|
|
536
542
|
if (session.touches.size === 1) {
|
|
543
|
+
// A device screen is pressed, not pointed at: the finger lands with
|
|
544
|
+
// the button down, drags with it held, and lifts to release. A tap
|
|
545
|
+
// is the same down and up with no move, so it clicks for free, and
|
|
546
|
+
// a hold holds for free — there is no long press to arm.
|
|
547
|
+
if (session.gestures === 'device') {
|
|
548
|
+
session.downX = x;
|
|
549
|
+
session.downY = y;
|
|
550
|
+
session.lastX = x;
|
|
551
|
+
session.lastY = y;
|
|
552
|
+
const at = desktopPoint(session, x, y);
|
|
553
|
+
if (at === null) {
|
|
554
|
+
session.gesture = 'letterbox';
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
session.gesture = 'drag';
|
|
558
|
+
session.lastTap = null;
|
|
559
|
+
pointer(session, 'down', at, 1);
|
|
560
|
+
session.dragX = at.x;
|
|
561
|
+
session.dragY = at.y;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
537
564
|
session.downOnPicture = desktopPoint(session, x, y) !== null;
|
|
538
565
|
session.gesture = session.view.fitted && !session.downOnPicture ? 'letterbox' : 'pending';
|
|
539
566
|
session.downX = x;
|
|
@@ -586,6 +613,16 @@ function attachGestures(session: WebSession): () => void {
|
|
|
586
613
|
if (Math.hypot(x - session.downX, y - session.downY) > TOUCH_SLOP) {
|
|
587
614
|
cancelLongPress(session);
|
|
588
615
|
session.lastTap = null;
|
|
616
|
+
// A page scrolls under one finger; the desktop's pointer
|
|
617
|
+
// rides along so the scroll lands where the finger is.
|
|
618
|
+
if (session.gestures === 'browser') {
|
|
619
|
+
session.gesture = 'scroll';
|
|
620
|
+
session.lastX = x;
|
|
621
|
+
session.lastY = y;
|
|
622
|
+
const at = desktopPoint(session, x, y);
|
|
623
|
+
if (at !== null) pointer(session, 'move', at);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
589
626
|
// The whole desktop has nowhere to move to, so the finger
|
|
590
627
|
// moves the desktop's pointer instead, without a button.
|
|
591
628
|
session.gesture = session.view.fitted
|
|
@@ -620,6 +657,15 @@ function attachGestures(session: WebSession): () => void {
|
|
|
620
657
|
case 'pinch':
|
|
621
658
|
case 'scroll':
|
|
622
659
|
if (session.touches.size >= 2) twoFingers();
|
|
660
|
+
else if (session.gesture === 'scroll' && session.gestures === 'browser') {
|
|
661
|
+
// One finger dragging a page: content follows it, in pixel
|
|
662
|
+
// deltas turned to detents like the two-finger scroll.
|
|
663
|
+
session.wheelX -= (x - session.lastX) / session.view.scale / PIXELS_PER_DETENT;
|
|
664
|
+
session.wheelY -= (y - session.lastY) / session.view.scale / PIXELS_PER_DETENT;
|
|
665
|
+
session.lastX = x;
|
|
666
|
+
session.lastY = y;
|
|
667
|
+
flushWheel(false);
|
|
668
|
+
}
|
|
623
669
|
return;
|
|
624
670
|
default:
|
|
625
671
|
return;
|
|
@@ -662,6 +708,7 @@ function attachGestures(session: WebSession): () => void {
|
|
|
662
708
|
return;
|
|
663
709
|
}
|
|
664
710
|
if (gesture === 'pending') tap(x, y);
|
|
711
|
+
else if (gesture === 'scroll') flushWheel(true);
|
|
665
712
|
else if (gesture === 'hover') hoverTo(x, y);
|
|
666
713
|
else if (gesture === 'armed') {
|
|
667
714
|
const at = desktopPoint(session, session.downX, session.downY);
|
|
@@ -902,6 +949,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
902
949
|
lastTap: null,
|
|
903
950
|
longPress: null,
|
|
904
951
|
captured: false,
|
|
952
|
+
gestures: 'desktop',
|
|
905
953
|
inputEnabled: false,
|
|
906
954
|
composition: '',
|
|
907
955
|
compositionSent: '',
|
|
@@ -959,6 +1007,13 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
959
1007
|
emit(id, 'failure', { code: 'transport', message: 'the connection to the desktop was lost' });
|
|
960
1008
|
}
|
|
961
1009
|
};
|
|
1010
|
+
peer.oniceconnectionstatechange = () => {
|
|
1011
|
+
if (sessions.get(id) !== session) return;
|
|
1012
|
+
// Surfaced as it happens — upstream of any failure timeout — so a
|
|
1013
|
+
// short drop reads as reconnecting within about a second, not as
|
|
1014
|
+
// a frozen picture that still says live.
|
|
1015
|
+
emit(id, 'ice', { state: peer.iceConnectionState.toUpperCase() });
|
|
1016
|
+
};
|
|
962
1017
|
return id;
|
|
963
1018
|
},
|
|
964
1019
|
|
|
@@ -1130,9 +1185,35 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1130
1185
|
* surface, and removing the old one is what keeps a re-render from leaving two
|
|
1131
1186
|
* live videos behind.
|
|
1132
1187
|
*/
|
|
1133
|
-
|
|
1188
|
+
/** Which touch meaning a surface uses; changing it mid-session is safe. */
|
|
1189
|
+
export function setGestures(id: string, gestures: GestureProfile): void {
|
|
1190
|
+
const session = sessions.get(id);
|
|
1191
|
+
if (session === undefined) return;
|
|
1192
|
+
if (session.gestures === gestures) return;
|
|
1193
|
+
// A held drag belongs to the old meaning: let go rather than release the
|
|
1194
|
+
// button somewhere the finger never meant.
|
|
1195
|
+
if (session.gesture === 'drag' || session.gesture === 'mouse') endGestureDrag(session);
|
|
1196
|
+
cancelLongPress(session);
|
|
1197
|
+
session.gesture = 'none';
|
|
1198
|
+
session.touches.clear();
|
|
1199
|
+
session.gestures = gestures;
|
|
1200
|
+
// A device screen shows the touch itself: a mark left over from another
|
|
1201
|
+
// profile would be a finger that is not there.
|
|
1202
|
+
if (gestures === 'device' && session.pointerAt !== null) {
|
|
1203
|
+
session.pointerAt = null;
|
|
1204
|
+
placePointer(session);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** Release a held button at the last desktop point it was sent to. */
|
|
1209
|
+
function endGestureDrag(session: WebSession): void {
|
|
1210
|
+
pointer(session, 'up', { x: session.dragX, y: session.dragY }, 1);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export function attachSurface(id: string, container: HTMLElement | null, gestures: GestureProfile = 'desktop'): void {
|
|
1134
1214
|
const session = sessions.get(id);
|
|
1135
1215
|
if (session === undefined) return;
|
|
1216
|
+
session.gestures = gestures;
|
|
1136
1217
|
session.detach?.();
|
|
1137
1218
|
session.detach = null;
|
|
1138
1219
|
if (session.surface !== null) session.surface.remove();
|
package/src/protocol.ts
CHANGED
|
@@ -18,6 +18,14 @@ export const PROTOCOL_VERSION = 3;
|
|
|
18
18
|
|
|
19
19
|
export type Permission = 'view' | 'control' | 'clipboard';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Which touch meaning a surface uses. `desktop` is the trackpad-shaped
|
|
23
|
+
* default; `browser` scrolls a page with one finger; `device` presses the
|
|
24
|
+
* screen with one finger, like the finger itself was there. The wire messages
|
|
25
|
+
* are the same under all three — only which gesture sends which changes.
|
|
26
|
+
*/
|
|
27
|
+
export type GestureProfile = 'desktop' | 'browser' | 'device';
|
|
28
|
+
|
|
21
29
|
export interface IceServerConfig {
|
|
22
30
|
urls: string[];
|
|
23
31
|
username?: string;
|
|
@@ -61,13 +69,25 @@ export interface RtcCandidate {
|
|
|
61
69
|
sdpMLineIndex?: number | null;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Engine → client lifecycle events, already unwrapped from the local protocol.
|
|
74
|
+
*
|
|
75
|
+
* `sessionId` names the engine session the event belongs to, when the
|
|
76
|
+
* carrier preserves it. The hook ignores an event whose id does not match
|
|
77
|
+
* the session it holds: after an outage, a previous session's queued offer
|
|
78
|
+
* or revocation can arrive beside the new session's, and applying those to
|
|
79
|
+
* the new session corrupts or kills a healthy recovery.
|
|
80
|
+
*/
|
|
65
81
|
export type SessionEvent =
|
|
66
|
-
| { kind: 'description'; description: RtcDescription }
|
|
67
|
-
| { kind: 'candidate'; candidate: RtcCandidate }
|
|
68
|
-
| { kind: 'state'; capture: string; transport: string; firstFrame: boolean }
|
|
69
|
-
| { kind: 'restoreToken'; token: string }
|
|
70
|
-
|
|
82
|
+
| { kind: 'description'; description: RtcDescription; sessionId?: string }
|
|
83
|
+
| { kind: 'candidate'; candidate: RtcCandidate; sessionId?: string }
|
|
84
|
+
| { kind: 'state'; capture: string; transport: string; firstFrame: boolean; sessionId?: string }
|
|
85
|
+
| { kind: 'restoreToken'; token: string; sessionId?: string }
|
|
86
|
+
/**
|
|
87
|
+
* `code` is the engine's revocation code: `transport` means the path was
|
|
88
|
+
* lost and a new session may recover it; anything else, or none, is final.
|
|
89
|
+
*/
|
|
90
|
+
| { kind: 'revoked'; reason: string; code?: string; sessionId?: string };
|
|
71
91
|
|
|
72
92
|
/**
|
|
73
93
|
* The application's own authenticated channel to the host engine.
|
|
@@ -139,7 +159,14 @@ export type ControlReply =
|
|
|
139
159
|
| { kind: 'ack'; seq: number }
|
|
140
160
|
| { kind: 'rejected'; seq: number; code: string; message: string }
|
|
141
161
|
| { kind: 'clipboard'; request: string; text: string; truncated?: boolean; error?: string }
|
|
142
|
-
| { kind: 'revoked'; reason: string }
|
|
162
|
+
| { kind: 'revoked'; reason: string; code?: string }
|
|
163
|
+
/**
|
|
164
|
+
* The engine's heartbeat, twice a second while the control channel is
|
|
165
|
+
* open. A client that has seen one and then sees none for a while reads
|
|
166
|
+
* the path as stalled, long before ICE consent timers notice. Clients that
|
|
167
|
+
* predate it simply ignore it.
|
|
168
|
+
*/
|
|
169
|
+
| { kind: 'ping' };
|
|
143
170
|
|
|
144
171
|
export function parseControlReply(raw: string): ControlReply | null {
|
|
145
172
|
try {
|