@desklink/react-native 0.1.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.
@@ -0,0 +1,20 @@
1
+ package expo.modules.desklink
2
+
3
+ internal fun remoteDeletionCounts(
4
+ beforeLength: Int,
5
+ afterLength: Int,
6
+ selectionStart: Int,
7
+ selectionEnd: Int,
8
+ composingStart: Int,
9
+ composingEnd: Int,
10
+ ): Pair<Int, Int> {
11
+ fun committed(length: Int, from: Long, to: Long): Int {
12
+ val local = if (composingStart >= 0 && composingEnd > composingStart) {
13
+ (minOf(to, composingEnd.toLong()) - maxOf(from, composingStart.toLong())).coerceAtLeast(0)
14
+ } else 0L
15
+ return (length.toLong() - local).coerceIn(0, 64).toInt()
16
+ }
17
+
18
+ return committed(beforeLength, selectionStart.toLong() - beforeLength, selectionStart.toLong()) to
19
+ committed(afterLength, selectionEnd.toLong(), selectionEnd.toLong() + afterLength)
20
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["expo.modules.desklink.DesklinkModule"]
5
+ }
6
+ }
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './src';
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@desklink/react-native",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "Render and drive a desktop session from a React Native app",
6
+ "main": "index.ts",
7
+ "types": "index.ts",
8
+ "exports": {
9
+ ".": "./index.ts",
10
+ "./availability": {
11
+ "browser": "./src/availability.web.ts",
12
+ "default": "./src/availability.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "index.ts",
17
+ "src",
18
+ "!src/**/*.spec.ts",
19
+ "android/build.gradle",
20
+ "android/gradle.properties",
21
+ "android/src/main",
22
+ "expo-module.config.json",
23
+ "NOTICE",
24
+ "README.md"
25
+ ],
26
+ "peerDependencies": {
27
+ "expo": ">=55",
28
+ "expo-modules-core": ">=55",
29
+ "react": ">=19",
30
+ "react-native": ">=0.83",
31
+ "react-native-webrtc": ">=124"
32
+ }
33
+ }
@@ -0,0 +1,61 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+ import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
4
+
5
+ import { desktopAvailable } from './native';
6
+
7
+ export interface DesktopViewProps {
8
+ /** The session handle from `useDesktopSession`. */
9
+ sessionId: string | null;
10
+ style?: StyleProp<ViewStyle>;
11
+ /** Shown until the first desktop frame has actually rendered. */
12
+ placeholder?: React.ReactNode;
13
+ /** What a screen reader calls the surface. */
14
+ accessibilityLabel?: string;
15
+ /**
16
+ * Space the application keeps free above the phone's keyboard for its own
17
+ * controls, in points. While the keyboard is up the picture sits above both,
18
+ * following the keyboard as it moves.
19
+ */
20
+ keyboardClearance?: number;
21
+ }
22
+
23
+ interface NativeSurfaceProps {
24
+ sessionId: string | null;
25
+ style?: StyleProp<ViewStyle>;
26
+ accessible?: boolean;
27
+ accessibilityLabel?: string;
28
+ keyboardClearance?: number;
29
+ }
30
+
31
+ // The Expo view is resolved at module load; on a platform without it the
32
+ // component is absent and `DesktopView` falls back to its placeholder.
33
+ const NativeSurface: React.ComponentType<NativeSurfaceProps> | null = desktopAvailable
34
+ ? (requireNativeView('Desklink', 'DesklinkSurface') as React.ComponentType<NativeSurfaceProps>)
35
+ : null;
36
+
37
+ /**
38
+ * The live desktop surface.
39
+ *
40
+ * It renders the picture and the pointer and owns the gestures, and nothing
41
+ * else: the surrounding chrome, the start state and the return navigation
42
+ * belong to the application, which mounts this wherever it wants the desktop
43
+ * to appear.
44
+ */
45
+ export function DesktopView({ sessionId, style, placeholder, accessibilityLabel, keyboardClearance = 0 }: DesktopViewProps) {
46
+ if (NativeSurface == null || sessionId == null) {
47
+ return (
48
+ <View style={[styles.surface, style]}>
49
+ {placeholder}
50
+ </View>
51
+ );
52
+ }
53
+ return <NativeSurface style={[styles.surface, style]} sessionId={sessionId} accessible={accessibilityLabel !== undefined} accessibilityLabel={accessibilityLabel} keyboardClearance={keyboardClearance} />;
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ surface: {
58
+ backgroundColor: '#000',
59
+ overflow: 'hidden',
60
+ },
61
+ });
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ export interface DesktopViewProps {
5
+ /** The session handle from `useDesktopSession`. */
6
+ sessionId: string | null;
7
+ style?: StyleProp<ViewStyle>;
8
+ /** Shown until the first desktop frame has actually rendered. */
9
+ placeholder?: React.ReactNode;
10
+ /** What a screen reader calls the surface. */
11
+ accessibilityLabel?: string;
12
+ /**
13
+ * Space the application keeps free above the phone's keyboard for its own
14
+ * controls, in points. While the keyboard is up the picture sits above both.
15
+ */
16
+ keyboardClearance?: number;
17
+ }
18
+
19
+ /**
20
+ * The live desktop surface, in a browser.
21
+ *
22
+ * The picture, the gestures and the keyboard belong to the session's own web
23
+ * implementation, so this is a mount point: it hands that session a DOM element
24
+ * and gets out of the way. The platform module is imported when a surface is
25
+ * actually mounted rather than at module load, so an application that never
26
+ * opens a desktop does not carry one in its first paint.
27
+ */
28
+ export function DesktopView({ sessionId, style, placeholder, accessibilityLabel, keyboardClearance = 0 }: DesktopViewProps) {
29
+ const mounted = React.useCallback(
30
+ (node: unknown) => {
31
+ const element = (node as HTMLElement | null) ?? null;
32
+ if (sessionId === null) return;
33
+ void import('./native').then((platform) => {
34
+ if (element === null || !platform.desktopAvailable) return;
35
+ platform.attachSurface(sessionId, element);
36
+ });
37
+ },
38
+ [sessionId],
39
+ );
40
+
41
+ React.useEffect(() => {
42
+ if (sessionId === null) return;
43
+ void import('./native').then((platform) => platform.setKeyboardClearance(sessionId, keyboardClearance));
44
+ }, [sessionId, keyboardClearance]);
45
+
46
+ return (
47
+ <View style={[styles.surface, style]} accessibilityLabel={accessibilityLabel}>
48
+ {sessionId === null ? placeholder : <div ref={mounted} style={styles.dom} />}
49
+ </View>
50
+ );
51
+ }
52
+
53
+ const styles = StyleSheet.create({
54
+ surface: { backgroundColor: '#000', overflow: 'hidden' },
55
+ dom: { position: 'absolute', inset: 0 },
56
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Whether this build can show a desktop surface at all. The browser answers with
3
+ * a constant so its renderer and input bridge stay out of the first paint.
4
+ */
5
+ export { desktopAvailable } from './native';
@@ -0,0 +1,2 @@
1
+ /** A browser always has its own WebRTC stack, so it can always show a surface. */
2
+ export const desktopAvailable = true;
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /** Public API of the desktop client package. Import this, not internals. */
2
+ export * from './protocol';
3
+ /**
4
+ * The platform module is deliberately not re-exported here.
5
+ *
6
+ * It is the bulk of this package — a session, a renderer and an input bridge —
7
+ * and most users of an application never open a desktop. Exporting it from the
8
+ * barrel would put it in the application's first paint; the session loads it
9
+ * when a desktop is actually opened.
10
+ */
11
+ export type { NativeDesklinkModule, NativeSessionEvent } from './native';
12
+ export { desktopAvailable } from './availability';
13
+ export { DesktopView, type DesktopViewProps } from './DesktopView';
14
+ export { observeWebKeyboardMotion } from './webKeyboardMotion';
15
+ export type { StickyModifier, StickyModifiers, StickyState } from './stickyModifiers';
16
+ export {
17
+ useDesktopSession,
18
+ CONTROL_PERMISSIONS,
19
+ type DesktopSession,
20
+ type DesktopSessionOptions,
21
+ } from './useDesktopSession';
package/src/native.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { Platform } from 'react-native';
2
+ import { requireOptionalNativeModule } from 'expo-modules-core';
3
+
4
+
5
+
6
+ /**
7
+ * The native session module.
8
+ *
9
+ * Everywhere but Android the module is absent, `desktopAvailable` is false, and
10
+ * the package still exports its types plus the web surface — an application
11
+ * renders one screen on every platform and branches on capability, not on
12
+ * platform name.
13
+ */
14
+ export type NativeEventName =
15
+ | 'ready'
16
+ | 'answer'
17
+ | 'candidate'
18
+ | 'ice'
19
+ | 'channel'
20
+ | 'track'
21
+ | 'presented'
22
+ | 'control'
23
+ | 'keyboard'
24
+ | 'failure'
25
+ | 'closed';
26
+
27
+ export interface NativeSessionEvent {
28
+ sessionId: string;
29
+ name: NativeEventName;
30
+ payload: Record<string, unknown>;
31
+ }
32
+
33
+ export interface NativeDesklinkModule {
34
+ createSession(iceServersJson: string): string | null;
35
+ setRemoteDescription(id: string, type: string, sdp: string): boolean;
36
+ addRemoteCandidate(id: string, candidate: string, sdpMid: string | null, sdpMLineIndex: number | null): boolean;
37
+ /** Send one control message; the platform stamps it with the session's next sequence. */
38
+ sendControl(id: string, message: string): boolean;
39
+ setInputEnabled(id: string, enabled: boolean): boolean;
40
+ showKeyboard(id: string): boolean;
41
+ hideKeyboard(id: string): boolean;
42
+ /**
43
+ * While captured, what the phone's keyboard types is not sent: it arrives as
44
+ * a `keyboard` event, `{ text }` or `{ key }`, for the session to chord.
45
+ */
46
+ captureKeyboard(id: string, captured: boolean): boolean;
47
+ setSurfaceSize(id: string, width: number, height: number): boolean;
48
+ /** Show the whole desktop again after the user zoomed in. */
49
+ fitToView(id: string): boolean;
50
+ /** Hold the screen in landscape while the desktop is shown, or follow the phone again. */
51
+ setOrientation(mode: 'landscape' | 'auto'): boolean;
52
+ closeSession(id: string): boolean;
53
+ isAvailable(): boolean;
54
+ addListener?(name: 'onSessionEvent', handler: (event: NativeSessionEvent) => void): { remove: () => void };
55
+ }
56
+
57
+ const native = Platform.OS === 'android'
58
+ ? (requireOptionalNativeModule<NativeDesklinkModule>('Desklink') ?? null)
59
+ : null;
60
+
61
+ export const nativeDesklink: NativeDesklinkModule | null = native;
62
+
63
+ /** True when this build can show a live desktop surface at all. */
64
+ export const desktopAvailable = native !== null;
65
+