@desklink/react-native 0.3.0 → 0.4.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 +40 -17
- package/package.json +1 -1
- package/src/DesktopView.ios.spec.tsx +13 -3
- package/src/DesktopView.ios.tsx +4 -3
- package/src/pointer.png +0 -0
- package/src/pointer.png.d.ts +2 -0
- package/src/pointer.svg +7 -0
- package/src/pointer@2x.png +0 -0
- package/src/pointer@3x.png +0 -0
- package/src/protocol.ts +25 -1
- package/src/useDesktopSession.ts +25 -6
package/README.md
CHANGED
|
@@ -58,12 +58,12 @@ changes, and trackpad presses reach the desktop as touches.
|
|
|
58
58
|
## Use
|
|
59
59
|
|
|
60
60
|
```tsx
|
|
61
|
-
import { DesktopView, useDesktopSession
|
|
61
|
+
import { DesktopView, useDesktopSession } from '@desklink/react-native';
|
|
62
62
|
|
|
63
63
|
const desktop = useDesktopSession({
|
|
64
64
|
authorize: async () => ({
|
|
65
65
|
signaling, // your authenticated channel to the host
|
|
66
|
-
session: {
|
|
66
|
+
session: {},
|
|
67
67
|
}),
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -78,6 +78,13 @@ desktop.setOrientation('landscape'); // Android only; iOS apps configure suppo
|
|
|
78
78
|
await desktop.pasteLocalToRemote(await Clipboard.getStringAsync());
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
Pairing is the only trust boundary. `authorize` is the app's pairing check: it
|
|
82
|
+
returns a channel to a paired computer or throws, and it runs again on every
|
|
83
|
+
reconnect, so a revoked pairing cannot reopen. A paired session has view,
|
|
84
|
+
input and clipboard; there are no in-app grants, and `session.permissions` is
|
|
85
|
+
ignored. Only the operating system's own consent (screen recording,
|
|
86
|
+
accessibility, a screen-sharing prompt) still applies on the computer.
|
|
87
|
+
|
|
81
88
|
A screen that only decides *whether* to offer a desktop does not need the
|
|
82
89
|
session: import the flag from its own entry, which is a constant on web and a
|
|
83
90
|
check for the platform module elsewhere, so the hook and the view stay out of
|
|
@@ -98,7 +105,7 @@ engine only produces frames on change.
|
|
|
98
105
|
Keep the encoded picture inside a 1920×1080 box:
|
|
99
106
|
|
|
100
107
|
```tsx
|
|
101
|
-
session: {
|
|
108
|
+
session: { maxWidth: 1920, maxHeight: 1080 },
|
|
102
109
|
```
|
|
103
110
|
|
|
104
111
|
The engine never upscales, so a smaller desktop is sent at its own size and a
|
|
@@ -130,11 +137,12 @@ heat and battery.
|
|
|
130
137
|
stamps every frame alike, so the iOS peer declines that hint and keeps the
|
|
131
138
|
receiver's usual jitter buffer. `fitToView()` shows the whole desktop again.
|
|
132
139
|
- **A pointer a phone can see.** Once a touch has sent the desktop's pointer
|
|
133
|
-
somewhere, the view draws
|
|
134
|
-
own cursor is a few pixels tall or not captured at all.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
somewhere, the view draws an outlined arrow there at a readable size, over a
|
|
141
|
+
picture whose own cursor is a few pixels tall or not captured at all. On iOS,
|
|
142
|
+
the arrow tip marks the desktop pointer position. Android hides the extra
|
|
143
|
+
mark when a mouse is attached, iOS when an iPad trackpad or mouse drives it,
|
|
144
|
+
and the `device` profile draws no mark on any platform — the screen being
|
|
145
|
+
touched shows the finger itself.
|
|
138
146
|
- **The picture above the keyboard.** While the phone's keyboard is up, the
|
|
139
147
|
picture sits above it and above the room the app keeps for its own controls
|
|
140
148
|
(`keyboardClearance`). Android follows the keyboard animation and keeps the
|
|
@@ -211,11 +219,13 @@ heat and battery.
|
|
|
211
219
|
network is still down. Renegotiation transients from a restart the session
|
|
212
220
|
asked for itself are expected, not a new outage: they neither flip the
|
|
213
221
|
status nor trigger another restart for a short grace window.
|
|
214
|
-
- **
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
a
|
|
222
|
+
- **Input starts disarmed.** This is a local arm against accidental touches,
|
|
223
|
+
not a permission: the session already has input. `setInputEnabled(true)` arms
|
|
224
|
+
pointer, keyboard and clipboard input; `setInputEnabled(false)` blocks new
|
|
225
|
+
input, releases held keys and buttons, and resets pending gestures. The app
|
|
226
|
+
decides when to arm it after a deliberate action and must disarm it on
|
|
227
|
+
background or lost readiness; a clipboard write already in flight may still
|
|
228
|
+
finish.
|
|
219
229
|
- **Released state.** The app disables input on background and unmount;
|
|
220
230
|
session close and a lost control channel release what the desktop was holding.
|
|
221
231
|
- **Explicit clipboard.** Two methods, called on a user action while control is
|
|
@@ -277,7 +287,11 @@ What the consuming app — not this package — must get right before review.
|
|
|
277
287
|
|
|
278
288
|
`example/` is the smallest app that shows a desktop: a native (non-Expo Go)
|
|
279
289
|
build with `react-native-webrtc`, connecting to `desklink-host bridge` with the
|
|
280
|
-
URL it is launched with.
|
|
290
|
+
URL it is launched with. Its status line shows plain messages: `Connected`
|
|
291
|
+
once a frame is presented, `Can’t reach your desktop. Reconnecting…` while
|
|
292
|
+
retrying, and `Your desktop ended this session` after revocation. It never
|
|
293
|
+
displays the bridge URL, pairing token, raw failure text or frame metrics.
|
|
294
|
+
`test/ios-flow.mjs` builds it for the iOS simulator on
|
|
281
295
|
a Mac over ssh, serves a private Xvfb desktop from this machine through the
|
|
282
296
|
bridge, and checks that the first frame is presented, that the answer puts
|
|
283
297
|
H.264 first with NACK when the host offers it (VP9 otherwise; both descriptions
|
|
@@ -290,15 +304,24 @@ a scroll:
|
|
|
290
304
|
|
|
291
305
|
```sh
|
|
292
306
|
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
|
|
307
|
+
env -u DISPLAY -u WAYLAND_DISPLAY DESKLINK_IOS_MAC=user@mac npm run test:ios --workspace @desklink/react-native
|
|
294
308
|
```
|
|
295
309
|
|
|
296
310
|
The Mac needs Xcode with an iOS simulator runtime, CocoaPods, node and
|
|
297
311
|
[`axe`](https://github.com/cameroncooke/AXe) on its login `PATH`; everything
|
|
298
|
-
the run builds or caches stays in `~/desklink-ios` there.
|
|
312
|
+
the run builds or caches stays in `~/desklink-ios` there. The run creates a
|
|
313
|
+
uniquely named simulator and deletes it during cleanup. For host desktop isolation, see the
|
|
314
|
+
[private-lab guidance](../../README.md#develop). When the selected
|
|
299
315
|
Xcode has no simulator platform, point `DESKLINK_IOS_XCODE` at one that does;
|
|
300
316
|
`DESKLINK_IOS_RUNTIME` picks the iOS version (for example `18.6`); the
|
|
301
|
-
script's header lists the other settings. To
|
|
317
|
+
script's header lists the other settings. To capture the cursor tip proof, set
|
|
318
|
+
`DESKLINK_IOS_POINTER_PROOF=before` or `after`; the run writes a 4x crop, device
|
|
319
|
+
frame, proof metadata, and host click log under `DESKLINK_IOS_OUT`. Set
|
|
320
|
+
`DESKLINK_IOS_APP` to reuse an app bundle under `DESKLINK_IOS_DIR` when running
|
|
321
|
+
with `DESKLINK_IOS_SKIP_BUILD=1`. Run once with `before` before the change and
|
|
322
|
+
once with `after` after it, on both iPhone and iPad simulators, to compare the
|
|
323
|
+
pointer crop and verify the host click is within two desktop pixels of the tip.
|
|
324
|
+
To try the example by hand against any bridge:
|
|
302
325
|
|
|
303
326
|
```sh
|
|
304
327
|
xcrun simctl launch <device> dev.desklink.example -desklinkUrl 'ws://HOST:PORT/desktop?token=TOKEN'
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ vi.mock('react-native', () => ({
|
|
|
18
18
|
Keyboard: { addListener: () => ({ remove: () => undefined }), dismiss: () => undefined },
|
|
19
19
|
StyleSheet: { create: (styles: unknown) => styles },
|
|
20
20
|
TextInput: 'TextInput',
|
|
21
|
+
Image: 'Image',
|
|
21
22
|
View: 'View',
|
|
22
23
|
}));
|
|
23
24
|
|
|
@@ -204,15 +205,24 @@ describe('DesktopView.ios parity with DesktopView.kt', () => {
|
|
|
204
205
|
});
|
|
205
206
|
|
|
206
207
|
it('puts the cursor mark tip on the pointer with a hotspot', async () => {
|
|
207
|
-
const { renderer, grant, move } = await openView();
|
|
208
|
+
const { renderer, sent, grant, move, release } = await openView();
|
|
208
209
|
grant([touch(195, 420)]);
|
|
209
210
|
move([touch(250, 430)]);
|
|
210
|
-
const
|
|
211
|
+
const image = renderer.root.findByType('Image');
|
|
212
|
+
const cursor = image.props.style as unknown[];
|
|
211
213
|
const mark = cursor[1] as { left: number; top: number };
|
|
212
214
|
const desktop = { x: Math.floor(250 / SCALE), y: Math.floor((430 - ORIGIN_Y) / SCALE) };
|
|
213
215
|
expect(mark.left).toBeCloseTo((desktop.x + 0.5) * SCALE - 3, 1);
|
|
214
216
|
expect(mark.top).toBeCloseTo(ORIGIN_Y + (desktop.y + 0.5) * SCALE - 3, 1);
|
|
215
|
-
expect(
|
|
217
|
+
expect(image.props.accessible).toBe(false);
|
|
218
|
+
expect(image.props.pointerEvents).toBe('none');
|
|
219
|
+
release();
|
|
220
|
+
grant([touch(mark.left + 3, mark.top + 3)]);
|
|
221
|
+
release();
|
|
222
|
+
expect(sent.filter((message) => message.kind === 'pointer').slice(-2)).toMatchObject([
|
|
223
|
+
{ kind: 'pointer', phase: 'down', ...desktop, button: 1 },
|
|
224
|
+
{ kind: 'pointer', phase: 'up', ...desktop, button: 1 },
|
|
225
|
+
]);
|
|
216
226
|
});
|
|
217
227
|
|
|
218
228
|
it('exposes the surface to VoiceOver only with a label', async () => {
|
package/src/DesktopView.ios.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { Keyboard, StyleSheet, TextInput, View, type GestureResponderEvent, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
2
|
+
import { Image, Keyboard, StyleSheet, TextInput, View, type GestureResponderEvent, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
3
|
+
import pointerArrow from './pointer.png';
|
|
3
4
|
import { requireNativeViewManager, requireOptionalNativeModule } from 'expo-modules-core';
|
|
4
5
|
import type { DesktopViewProps } from './DesktopView';
|
|
5
6
|
import { hardwareKey, type HardwareKey } from './hardwareKeys';
|
|
@@ -391,7 +392,7 @@ export function DesktopView({ sessionId, style, placeholder, accessibilityLabel,
|
|
|
391
392
|
{sessionId && stream && RTCView && size.width > 0 && size.height > 0 ?
|
|
392
393
|
<RTCView pointerEvents="none" streamURL={stream.toURL()} objectFit="contain" style={{ position: 'absolute', left: originX, top: originY, width: pictureWidth, height: pictureHeight }}
|
|
393
394
|
onDimensionsChange={(event) => { if (event.nativeEvent.width > 0 && event.nativeEvent.height > 0) markDesktopPresented(sessionId); }} /> : placeholder}
|
|
394
|
-
{cursor && <
|
|
395
|
+
{cursor && <Image source={pointerArrow} pointerEvents="none" accessible={false} style={[styles.cursor, { left: originX + (cursor.x + 0.5) * scale - CURSOR_HOTSPOT, top: originY + (cursor.y + 0.5) * scale - CURSOR_HOTSPOT }]} />}
|
|
395
396
|
<TextInput ref={input} style={styles.input} value={keyboardText} onChangeText={changeText} autoCorrect={false} autoCapitalize="none" submitBehavior="submit" accessible={false}
|
|
396
397
|
onSubmitEditing={() => {
|
|
397
398
|
// While the app holds a sticky modifier, Enter is the app's to
|
|
@@ -408,6 +409,6 @@ export function DesktopView({ sessionId, style, placeholder, accessibilityLabel,
|
|
|
408
409
|
|
|
409
410
|
const styles = StyleSheet.create({
|
|
410
411
|
surface: { backgroundColor: '#000', overflow: 'hidden' },
|
|
411
|
-
cursor: { position: 'absolute', width:
|
|
412
|
+
cursor: { position: 'absolute', width: 28, height: 28 },
|
|
412
413
|
input: { position: 'absolute', width: 1, height: 1, opacity: 0, left: 0, bottom: 0 },
|
|
413
414
|
});
|
package/src/pointer.png
ADDED
|
Binary file
|
package/src/pointer.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!-- Same outline and (3, 3) tip hotspot as web and Android. Generate the
|
|
2
|
+
iOS density assets with rsvg-convert -w SIZE -h SIZE pointer.svg -o FILE:
|
|
3
|
+
SIZE=28/56/84, FILE=pointer.png/pointer@2x.png/pointer@3x.png. -->
|
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
|
5
|
+
<path d="M3 3 L3 22.25 L7.73 17.96 L10.92 25.22 L14 23.9 L10.92 16.86 L17.08 16.86 Z" fill="none" stroke="#fff" stroke-width="2.2" stroke-linejoin="round"/>
|
|
6
|
+
<path d="M3 3 L3 22.25 L7.73 17.96 L10.92 25.22 L14 23.9 L10.92 16.86 L17.08 16.86 Z" fill="#000"/>
|
|
7
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
package/src/protocol.ts
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
export const PROTOCOL_VERSION = 3;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Pairing is the only trust boundary: a paired session always has
|
|
21
|
+
* view, input and clipboard, so there is nothing to ask for. Kept so 0.3 code
|
|
22
|
+
* compiles; removed at 1.0.
|
|
23
|
+
*/
|
|
19
24
|
export type Permission = 'view' | 'control' | 'clipboard';
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -41,7 +46,9 @@ export interface SurfaceGeometry {
|
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export interface SessionOpenRequest {
|
|
44
|
-
|
|
49
|
+
cursor?: 'embedded' | 'hidden';
|
|
50
|
+
/** @deprecated Ignored: a paired session always has view, input and clipboard. */
|
|
51
|
+
permissions?: Permission[];
|
|
45
52
|
maxWidth?: number;
|
|
46
53
|
maxHeight?: number;
|
|
47
54
|
bitrateKbps?: number;
|
|
@@ -52,6 +59,7 @@ export interface SessionOpenRequest {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export interface SessionOpenResult {
|
|
62
|
+
cursor?: { mode: 'embedded' | 'hidden' | 'metadata' | 'unavailable'; positions: boolean };
|
|
55
63
|
sessionId: string;
|
|
56
64
|
generation: number;
|
|
57
65
|
source: { kind: string; width: number; height: number; origin: { x: number; y: number } };
|
|
@@ -69,6 +77,14 @@ export interface RtcCandidate {
|
|
|
69
77
|
sdpMLineIndex?: number | null;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
export interface CursorSample {
|
|
81
|
+
sessionId: string;
|
|
82
|
+
x: number;
|
|
83
|
+
y: number;
|
|
84
|
+
visible: boolean;
|
|
85
|
+
timestamp_us: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/**
|
|
73
89
|
* Engine → client lifecycle events, already unwrapped from the local protocol.
|
|
74
90
|
*
|
|
@@ -81,6 +97,7 @@ export interface RtcCandidate {
|
|
|
81
97
|
export type SessionEvent =
|
|
82
98
|
| { kind: 'description'; description: RtcDescription; sessionId?: string }
|
|
83
99
|
| { kind: 'candidate'; candidate: RtcCandidate; sessionId?: string }
|
|
100
|
+
| ({ kind: 'cursor' } & CursorSample)
|
|
84
101
|
| { kind: 'state'; capture: string; transport: string; firstFrame: boolean; sessionId?: string }
|
|
85
102
|
| { kind: 'restoreToken'; token: string; sessionId?: string }
|
|
86
103
|
/**
|
|
@@ -116,6 +133,7 @@ export type SessionStatus =
|
|
|
116
133
|
| 'failed';
|
|
117
134
|
|
|
118
135
|
export type SessionFailure =
|
|
136
|
+
/** The app's `authorize()` refused, or the host did: not paired, or the pairing was revoked. */
|
|
119
137
|
| { code: 'permission'; message: string }
|
|
120
138
|
/** Nobody approved the desktop's own screen-sharing prompt in time. */
|
|
121
139
|
| { code: 'consent'; message: string }
|
|
@@ -135,6 +153,12 @@ export interface SessionSnapshot {
|
|
|
135
153
|
geometry: SurfaceGeometry | null;
|
|
136
154
|
/** True once an authorized, current-generation frame has actually rendered. */
|
|
137
155
|
presented: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* `message` is free text from the app's `authorize()`, the host or the
|
|
158
|
+
* engine and may contain a bridge URL or pairing token. Never display it
|
|
159
|
+
* to people; redact sensitive data before logging it. Choose user-facing
|
|
160
|
+
* text from the session state and `code` instead.
|
|
161
|
+
*/
|
|
138
162
|
failure: SessionFailure | null;
|
|
139
163
|
/** Redacted diagnostics: codec, route, counters. Never identities. */
|
|
140
164
|
diagnostics: Record<string, string | number | boolean>;
|
package/src/useDesktopSession.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { AppState } from 'react-native';
|
|
|
4
4
|
import type { NativeDesklinkModule } from './native';
|
|
5
5
|
import type {
|
|
6
6
|
ControlMessage,
|
|
7
|
+
CursorSample,
|
|
7
8
|
IceServerConfig,
|
|
8
9
|
Permission,
|
|
9
10
|
SessionFailure,
|
|
@@ -171,13 +172,14 @@ async function loadPlatform(): Promise<boolean> {
|
|
|
171
172
|
|
|
172
173
|
export interface DesktopSessionOptions {
|
|
173
174
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* The app's pairing check: return the authenticated channel to the host
|
|
176
|
+
* engine of a paired computer, or throw. Pairing is the only trust
|
|
177
|
+
* boundary, so a paired session has view, input and clipboard. Called again
|
|
178
|
+
* on every reconnect, so a revoked pairing cannot reopen.
|
|
178
179
|
*/
|
|
179
180
|
authorize: () => Promise<{ signaling: Signaling; session: SessionOpenRequest }>;
|
|
180
181
|
onStateChange?: (snapshot: SessionSnapshot) => void;
|
|
182
|
+
onCursor?: (sample: CursorSample) => void;
|
|
181
183
|
onError?: (failure: SessionFailure) => void;
|
|
182
184
|
/**
|
|
183
185
|
* The engine refused one input message and the session carries on, such as
|
|
@@ -253,6 +255,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
253
255
|
* before the native session exists waits here and is replayed into it.
|
|
254
256
|
*/
|
|
255
257
|
const pendingOffer = useRef<{ sdp: string; sessionId?: string } | null>(null);
|
|
258
|
+
const pendingCursorEvents = useRef<CursorSample[]>([]);
|
|
256
259
|
const pendingCandidates = useRef<Array<{ candidate: string; sdpMid: string | null; sdpMLineIndex: number | null; sessionId?: string }>>([]);
|
|
257
260
|
const attempts = useRef(0);
|
|
258
261
|
/**
|
|
@@ -572,6 +575,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
572
575
|
pendingClipboard.current.clear();
|
|
573
576
|
pendingOffer.current = null;
|
|
574
577
|
pendingCandidates.current = [];
|
|
578
|
+
pendingCursorEvents.current = [];
|
|
575
579
|
restartGraceUntil.current = 0;
|
|
576
580
|
restartCycles.current = 0;
|
|
577
581
|
opened.current = null;
|
|
@@ -615,6 +619,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
615
619
|
const establish = useCallback(async () => {
|
|
616
620
|
if (nativeRef.current != null) return;
|
|
617
621
|
const token = ++generationToken.current;
|
|
622
|
+
pendingCursorEvents.current = [];
|
|
618
623
|
if (!(await loadPlatform())) {
|
|
619
624
|
if (token === generationToken.current) refuse('this build cannot show a desktop surface');
|
|
620
625
|
return;
|
|
@@ -672,6 +677,13 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
672
677
|
event.candidate.sdpMLineIndex ?? null,
|
|
673
678
|
);
|
|
674
679
|
return;
|
|
680
|
+
case 'cursor':
|
|
681
|
+
if (held == null) {
|
|
682
|
+
pendingCursorEvents.current.push(event);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (current) optionsRef.current.onCursor?.(event);
|
|
686
|
+
return;
|
|
675
687
|
case 'state':
|
|
676
688
|
if (!current) return;
|
|
677
689
|
diagnostics.current.transport = event.transport;
|
|
@@ -705,8 +717,12 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
705
717
|
let openedResult: SessionOpenResult;
|
|
706
718
|
try {
|
|
707
719
|
openedResult = await authorization.signaling.request<SessionOpenResult>('session.open', {
|
|
708
|
-
|
|
720
|
+
// Ignored by 0.4+ engines, whatever the app asked for. A 0.3
|
|
721
|
+
// engine refuses an open without `view`, so the full set stays
|
|
722
|
+
// on the wire until 0.3 hosts leave support.
|
|
723
|
+
permissions: ['view', 'control', 'clipboard'],
|
|
709
724
|
max_width: authorization.session.maxWidth,
|
|
725
|
+
...(authorization.session.cursor === undefined ? {} : { cursor: authorization.session.cursor }),
|
|
710
726
|
max_height: authorization.session.maxHeight,
|
|
711
727
|
bitrate_kbps: authorization.session.bitrateKbps,
|
|
712
728
|
max_fps: authorization.session.maxFps,
|
|
@@ -725,6 +741,9 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
|
|
|
725
741
|
try {
|
|
726
742
|
if (token !== generationToken.current) return;
|
|
727
743
|
opened.current = openedResult;
|
|
744
|
+
for (const sample of pendingCursorEvents.current.splice(0)) {
|
|
745
|
+
if (sample.sessionId === openedResult.sessionId) optionsRef.current.onCursor?.(sample);
|
|
746
|
+
}
|
|
728
747
|
update({ geometry: openedResult.geometry, status: 'connecting' });
|
|
729
748
|
if (token !== generationToken.current) return;
|
|
730
749
|
|
|
@@ -1090,5 +1109,5 @@ function randomId(): string {
|
|
|
1090
1109
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1091
1110
|
}
|
|
1092
1111
|
|
|
1093
|
-
/**
|
|
1112
|
+
/** @deprecated Ignored: a paired session always has view, input and clipboard. Removed at 1.0. */
|
|
1094
1113
|
export const CONTROL_PERMISSIONS: Permission[] = ['view', 'control', 'clipboard'];
|