@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.
- package/LICENSE +202 -0
- package/NOTICE +18 -0
- package/README.md +131 -0
- package/android/build.gradle +29 -0
- package/android/gradle.properties +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/expo/modules/desklink/DesklinkModule.kt +199 -0
- package/android/src/main/java/expo/modules/desklink/DesktopRenderer.kt +284 -0
- package/android/src/main/java/expo/modules/desklink/DesktopSession.kt +478 -0
- package/android/src/main/java/expo/modules/desklink/DesktopView.kt +1019 -0
- package/android/src/main/java/expo/modules/desklink/ImeDeletion.kt +20 -0
- package/expo-module.config.json +6 -0
- package/index.ts +1 -0
- package/package.json +33 -0
- package/src/DesktopView.tsx +61 -0
- package/src/DesktopView.web.tsx +56 -0
- package/src/availability.ts +5 -0
- package/src/availability.web.ts +2 -0
- package/src/index.ts +21 -0
- package/src/native.ts +65 -0
- package/src/native.web.ts +1164 -0
- package/src/protocol.ts +151 -0
- package/src/stickyModifiers.ts +66 -0
- package/src/useDesktopSession.ts +661 -0
- package/src/webKeyboardMotion.ts +57 -0
|
@@ -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
|
+
}
|
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
|
+
});
|
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
|
+
|