@q9labsai/chalk-react-native 4.1.15 → 4.1.17
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 +21 -0
- package/README.md +11 -0
- package/dist/android.js +5 -2
- package/dist/{chunk-MK6OO4V7.js → chunk-2OS7SSXE.js} +6 -1
- package/dist/{chunk-4NLA35HE.js → chunk-ITLUHTVS.js} +1126 -644
- package/dist/client.js +1 -1
- package/dist/components/Chalk.d.ts +2 -1
- package/dist/components/Chalk.d.ts.map +1 -1
- package/dist/components/EmbeddedWhiteboard.d.ts.map +1 -1
- package/dist/components/SpaceView.d.ts +2 -1
- package/dist/components/SpaceView.d.ts.map +1 -1
- package/dist/components/SpaceView.shared.d.ts.map +1 -1
- package/dist/components/native-space-view/FeedbackSheet.d.ts +12 -0
- package/dist/components/native-space-view/FeedbackSheet.d.ts.map +1 -0
- package/dist/components/native-space-view/SpaceActionMenu.d.ts +2 -1
- package/dist/components/native-space-view/SpaceActionMenu.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts +8 -0
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts.map +1 -0
- package/dist/components/native-space-view/SpaceChatSheet.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts +2 -1
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts +3 -1
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +1 -1
- package/dist/components/native-space-view/screen-share-availability.d.ts +4 -1
- package/dist/components/native-space-view/screen-share-availability.d.ts.map +1 -1
- package/dist/components/native-space-view/space-chat-attachments.d.ts.map +1 -1
- package/dist/components/native-space-view/useSpaceViewController.d.ts +8 -2
- package/dist/components/native-space-view/useSpaceViewController.d.ts.map +1 -1
- package/dist/feedback/native-capture.d.ts +9 -0
- package/dist/feedback/native-capture.d.ts.map +1 -0
- package/dist/feedback/native-evidence.d.ts +3 -0
- package/dist/feedback/native-evidence.d.ts.map +1 -0
- package/dist/hooks/space-hooks.d.ts +1 -0
- package/dist/hooks/space-hooks.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/ios-pad.js +5 -2
- package/dist/ios-phone.js +5 -2
- package/dist/ios.js +5 -2
- package/dist/macos.js +5 -2
- package/dist/space-client/create-native-space-client.d.ts +2 -0
- package/dist/space-client/create-native-space-client.d.ts.map +1 -1
- package/dist/whiteboard/embedded-whiteboard-viewport.d.ts +3 -0
- package/dist/whiteboard/embedded-whiteboard-viewport.d.ts.map +1 -0
- package/embedded/chalk-whiteboard/asset-manifest.json +2 -2
- package/embedded/chalk-whiteboard/renderer.js +581 -581
- package/package.json +8 -6
- package/src/components/Chalk.tsx +6 -2
- package/src/components/EmbeddedWhiteboard.tsx +14 -5
- package/src/components/SpaceView.shared.tsx +58 -47
- package/src/components/SpaceView.tsx +2 -1
- package/src/components/native-space-view/FeedbackSheet.tsx +296 -0
- package/src/components/native-space-view/SpaceActionMenu.tsx +23 -3
- package/src/components/native-space-view/SpaceChatAttachment.tsx +105 -0
- package/src/components/native-space-view/SpaceChatSheet.tsx +14 -9
- package/src/components/native-space-view/SpaceMoreSheet.tsx +35 -6
- package/src/components/native-space-view/SpaceTopBar.android.tsx +16 -1
- package/src/components/native-space-view/screen-share-availability.test.ts +16 -0
- package/src/components/native-space-view/screen-share-availability.ts +4 -1
- package/src/components/native-space-view/space-chat-attachments.test.ts +26 -0
- package/src/components/native-space-view/space-chat-attachments.ts +2 -4
- package/src/components/native-space-view/useSpaceViewController.ts +28 -8
- package/src/feedback/native-capture.ts +62 -0
- package/src/feedback/native-evidence.ts +37 -0
- package/src/hooks/space-hooks.ts +4 -0
- package/src/index.ts +1 -1
- package/src/space-client/create-native-space-client.ts +7 -0
- package/src/whiteboard/embedded-whiteboard-viewport.test.ts +13 -0
- package/src/whiteboard/embedded-whiteboard-viewport.ts +6 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ChatAttachment } from "@q9labsai/chalk-client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Image, Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
|
|
5
|
+
import { Theme } from "../../ui/theme";
|
|
6
|
+
|
|
7
|
+
export function SpaceChatAttachment({ attachment, isLocal, onOpen, resolveUrl }: { readonly attachment: ChatAttachment; readonly isLocal: boolean; readonly onOpen: (attachmentId: string) => void; readonly resolveUrl: (attachmentId: string) => Promise<string | null> }): React.JSX.Element {
|
|
8
|
+
const isImage = attachment.mimeType.startsWith("image/");
|
|
9
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
10
|
+
const [previewFailed, setPreviewFailed] = useState(false);
|
|
11
|
+
const [retryGeneration, setRetryGeneration] = useState(0);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!isImage || previewUrl || previewFailed) return;
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
void resolveUrl(attachment.attachmentId).then(
|
|
17
|
+
(url) => {
|
|
18
|
+
if (cancelled) return;
|
|
19
|
+
if (!url) {
|
|
20
|
+
reportAttachmentPreviewFailure(attachment.attachmentId, "resolve");
|
|
21
|
+
setPreviewFailed(true);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setPreviewUrl(url);
|
|
25
|
+
setPreviewFailed(false);
|
|
26
|
+
},
|
|
27
|
+
() => {
|
|
28
|
+
if (cancelled) return;
|
|
29
|
+
reportAttachmentPreviewFailure(attachment.attachmentId, "resolve");
|
|
30
|
+
setPreviewFailed(true);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
return () => {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
};
|
|
36
|
+
}, [attachment.attachmentId, isImage, previewFailed, previewUrl, resolveUrl, retryGeneration]);
|
|
37
|
+
|
|
38
|
+
if (!isImage) {
|
|
39
|
+
return (
|
|
40
|
+
<Pressable accessibilityRole="link" onPress={() => onOpen(attachment.attachmentId)} style={({ pressed }) => [styles.attachmentLink, pressed && styles.pressed]}>
|
|
41
|
+
<Text numberOfLines={1} style={[styles.attachment, isLocal && styles.localText]}>
|
|
42
|
+
{attachment.fileName}
|
|
43
|
+
</Text>
|
|
44
|
+
</Pressable>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (previewFailed) {
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.previewError}>
|
|
51
|
+
<Text accessibilityLiveRegion="polite" style={[styles.previewErrorText, isLocal && styles.localText]}>
|
|
52
|
+
Image preview unavailable
|
|
53
|
+
</Text>
|
|
54
|
+
<Pressable
|
|
55
|
+
accessibilityLabel={`Retry preview for ${attachment.fileName}`}
|
|
56
|
+
accessibilityRole="button"
|
|
57
|
+
style={styles.retryButton}
|
|
58
|
+
onPress={() => {
|
|
59
|
+
setPreviewFailed(false);
|
|
60
|
+
setPreviewUrl(null);
|
|
61
|
+
setRetryGeneration((current) => current + 1);
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<Text style={[styles.retry, isLocal && styles.localText]}>Retry preview</Text>
|
|
65
|
+
</Pressable>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!previewUrl) return <Text style={[styles.previewLoading, isLocal && styles.localText]}>Loading {attachment.fileName}…</Text>;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Pressable accessibilityLabel={`Open ${attachment.fileName}`} accessibilityRole="imagebutton" onPress={() => onOpen(attachment.attachmentId)} style={({ pressed }) => [styles.imagePreviewButton, pressed && styles.pressed]}>
|
|
74
|
+
<Image
|
|
75
|
+
accessibilityLabel={attachment.fileName}
|
|
76
|
+
onError={() => {
|
|
77
|
+
reportAttachmentPreviewFailure(attachment.attachmentId, "render");
|
|
78
|
+
setPreviewFailed(true);
|
|
79
|
+
setPreviewUrl(null);
|
|
80
|
+
}}
|
|
81
|
+
resizeMode="cover"
|
|
82
|
+
source={{ uri: previewUrl }}
|
|
83
|
+
style={styles.imagePreview}
|
|
84
|
+
/>
|
|
85
|
+
</Pressable>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reportAttachmentPreviewFailure(attachmentId: string, operation: "render" | "resolve"): void {
|
|
90
|
+
console.warn("Chat attachment preview unavailable.", { attachmentId, operation });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const styles = StyleSheet.create({
|
|
94
|
+
attachmentLink: { justifyContent: "center", marginTop: 4, maxWidth: "100%", minHeight: 32 },
|
|
95
|
+
attachment: { color: Theme.colors.information, fontSize: 14 },
|
|
96
|
+
imagePreviewButton: { borderRadius: Theme.radius.sm, marginTop: Theme.spacing.sm, maxWidth: "100%", overflow: "hidden" },
|
|
97
|
+
imagePreview: { height: 160, width: 240 },
|
|
98
|
+
previewLoading: { color: Theme.colors.ink2, fontSize: 13, marginTop: Theme.spacing.sm },
|
|
99
|
+
previewError: { borderColor: Theme.colors.line, borderRadius: Theme.radius.sm, borderWidth: 1, marginTop: Theme.spacing.sm, width: 240, maxWidth: "100%", minHeight: 160, justifyContent: "center", alignItems: "center", padding: Theme.spacing.md },
|
|
100
|
+
previewErrorText: { color: Theme.colors.ink2, fontSize: 13 },
|
|
101
|
+
retryButton: { minHeight: 44, justifyContent: "center", paddingHorizontal: Theme.spacing.sm },
|
|
102
|
+
retry: { color: Theme.colors.information, fontSize: 13, fontWeight: "700", marginTop: 6 },
|
|
103
|
+
localText: { color: Theme.colors.surface },
|
|
104
|
+
pressed: { opacity: 0.72, transform: [{ scale: 0.98 }] },
|
|
105
|
+
});
|
|
@@ -6,6 +6,7 @@ import { FlatList, KeyboardAvoidingView, Platform, Pressable, StyleSheet, Text,
|
|
|
6
6
|
|
|
7
7
|
import { Theme } from "../../ui/theme";
|
|
8
8
|
import { formatChatTimestamp } from "./space-progressive-surface-helpers";
|
|
9
|
+
import { SpaceChatAttachment } from "./SpaceChatAttachment";
|
|
9
10
|
import { InitialsAvatar } from "./SpaceSurfacePrimitives";
|
|
10
11
|
import type { SpaceController } from "./space-progressive-surface-types";
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ export function SpaceChatSheet({ controller }: { readonly controller: SpaceContr
|
|
|
31
32
|
const sequence = viewableItems.at(-1)?.item.sequence;
|
|
32
33
|
if (sequence) controller.markChatMessageVisible(sequence);
|
|
33
34
|
}}
|
|
34
|
-
renderItem={({ item }) => <ChatMessage isLocal={item.senderId === localParticipantId} message={item} onOpenAttachment={controller.openChatAttachment} />}
|
|
35
|
+
renderItem={({ item }) => <ChatMessage isLocal={item.senderId === localParticipantId} message={item} onOpenAttachment={controller.openChatAttachment} resolveAttachmentUrl={controller.resolveChatAttachmentUrl} />}
|
|
35
36
|
showsVerticalScrollIndicator={false}
|
|
36
37
|
/>
|
|
37
38
|
{controller.chat.pendingMessages.map((message) => (
|
|
@@ -97,7 +98,17 @@ export function SpaceChatSheet({ controller }: { readonly controller: SpaceContr
|
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function ChatMessage({
|
|
101
|
+
function ChatMessage({
|
|
102
|
+
isLocal,
|
|
103
|
+
message,
|
|
104
|
+
onOpenAttachment,
|
|
105
|
+
resolveAttachmentUrl,
|
|
106
|
+
}: {
|
|
107
|
+
readonly isLocal: boolean;
|
|
108
|
+
readonly message: SpaceController["chat"]["messages"][number];
|
|
109
|
+
readonly onOpenAttachment: (attachmentId: string) => void;
|
|
110
|
+
readonly resolveAttachmentUrl: (attachmentId: string) => Promise<string | null>;
|
|
111
|
+
}): React.JSX.Element {
|
|
101
112
|
const timestamp = formatChatTimestamp(message.timestamp);
|
|
102
113
|
return (
|
|
103
114
|
<View style={[styles.messageRow, isLocal && styles.messageRowLocal]}>
|
|
@@ -107,11 +118,7 @@ function ChatMessage({ isLocal, message, onOpenAttachment }: { readonly isLocal:
|
|
|
107
118
|
<View style={[styles.bubble, isLocal ? styles.localBubble : styles.remoteBubble]}>
|
|
108
119
|
{message.text ? <Text style={[styles.messageText, isLocal && styles.localMessageText]}>{message.text}</Text> : null}
|
|
109
120
|
{message.attachments.map((attachment) => (
|
|
110
|
-
<
|
|
111
|
-
<Text numberOfLines={1} style={[styles.attachment, isLocal && styles.localMessageText]}>
|
|
112
|
-
{attachment.fileName}
|
|
113
|
-
</Text>
|
|
114
|
-
</Pressable>
|
|
121
|
+
<SpaceChatAttachment attachment={attachment} isLocal={isLocal} key={attachment.attachmentId} onOpen={onOpenAttachment} resolveUrl={resolveAttachmentUrl} />
|
|
115
122
|
))}
|
|
116
123
|
</View>
|
|
117
124
|
<Text style={[styles.timestamp, isLocal && styles.timestampLocal]}>{timestamp}</Text>
|
|
@@ -150,8 +157,6 @@ const styles = StyleSheet.create({
|
|
|
150
157
|
localBubble: { backgroundColor: Theme.colors.ink, borderColor: Theme.colors.ink, borderTopRightRadius: 5 },
|
|
151
158
|
messageText: { color: Theme.colors.ink, fontSize: 17, lineHeight: 24 },
|
|
152
159
|
localMessageText: { color: Theme.colors.surface },
|
|
153
|
-
attachmentLink: { justifyContent: "center", marginTop: 4, maxWidth: "100%", minHeight: 32 },
|
|
154
|
-
attachment: { color: Theme.colors.information, fontSize: 14 },
|
|
155
160
|
timestamp: { color: Theme.colors.ink3, fontSize: 12, marginTop: 5, paddingHorizontal: 4 },
|
|
156
161
|
timestampLocal: { textAlign: "right" },
|
|
157
162
|
readBy: { color: Theme.colors.ink3, fontSize: 11, marginTop: 3, paddingHorizontal: 4 },
|
|
@@ -19,10 +19,11 @@ interface MoreAction {
|
|
|
19
19
|
readonly icon?: SpaceIcon;
|
|
20
20
|
readonly symbol?: string;
|
|
21
21
|
readonly wash?: string;
|
|
22
|
+
readonly unavailableReason?: string;
|
|
22
23
|
readonly onPress: () => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function SpaceMoreSheet({ controller, onOpenSettings }: { readonly controller: SpaceController; readonly onOpenSettings: () => void }): React.JSX.Element {
|
|
26
|
+
export function SpaceMoreSheet({ controller, onOpenSettings, onOpenFeedback }: { readonly controller: SpaceController; readonly onOpenSettings: () => void; readonly onOpenFeedback?: () => void }): React.JSX.Element {
|
|
26
27
|
const close = () => controller.setActionsOpen(false);
|
|
27
28
|
const actions: MoreAction[] = [
|
|
28
29
|
{
|
|
@@ -77,12 +78,13 @@ export function SpaceMoreSheet({ controller, onOpenSettings }: { readonly contro
|
|
|
77
78
|
: []),
|
|
78
79
|
...(controller.canParticipants ? [{ id: "people", label: "People", icon: UserGroupIcon, onPress: () => controller.openPanel("participants") }] : []),
|
|
79
80
|
...(controller.canChat ? [{ id: "chat", label: "Chat", icon: Chat01Icon, onPress: () => controller.openPanel("chat") }] : []),
|
|
80
|
-
...(controller.canScreenShare
|
|
81
|
+
...(controller.canScreenShare || controller.screenShare.isLocalSharing || controller.screenShareUnavailableReason
|
|
81
82
|
? [
|
|
82
83
|
{
|
|
83
84
|
id: "screen-share",
|
|
84
85
|
label: controller.screenShare.isLocalSharing ? "Stop sharing" : "Present screen",
|
|
85
86
|
icon: ComputerScreenShareIcon,
|
|
87
|
+
unavailableReason: controller.screenShare.isLocalSharing ? undefined : (controller.screenShareUnavailableReason ?? undefined),
|
|
86
88
|
onPress: () => {
|
|
87
89
|
close();
|
|
88
90
|
controller.toggleScreenShare();
|
|
@@ -99,6 +101,19 @@ export function SpaceMoreSheet({ controller, onOpenSettings }: { readonly contro
|
|
|
99
101
|
onOpenSettings();
|
|
100
102
|
},
|
|
101
103
|
},
|
|
104
|
+
...(onOpenFeedback
|
|
105
|
+
? [
|
|
106
|
+
{
|
|
107
|
+
id: "feedback",
|
|
108
|
+
label: "Feedback",
|
|
109
|
+
symbol: "✉",
|
|
110
|
+
onPress: () => {
|
|
111
|
+
close();
|
|
112
|
+
onOpenFeedback();
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
: []),
|
|
102
117
|
];
|
|
103
118
|
|
|
104
119
|
return (
|
|
@@ -110,11 +125,23 @@ export function SpaceMoreSheet({ controller, onOpenSettings }: { readonly contro
|
|
|
110
125
|
<Text style={styles.title}>More</Text>
|
|
111
126
|
<ScrollView contentContainerStyle={styles.actionGrid} showsVerticalScrollIndicator={false}>
|
|
112
127
|
{actions.map((action) => (
|
|
113
|
-
<Pressable
|
|
128
|
+
<Pressable
|
|
129
|
+
accessibilityLabel={action.label}
|
|
130
|
+
accessibilityHint={action.unavailableReason}
|
|
131
|
+
accessibilityRole="button"
|
|
132
|
+
accessibilityState={{ disabled: Boolean(action.unavailableReason) }}
|
|
133
|
+
disabled={Boolean(action.unavailableReason)}
|
|
134
|
+
key={action.id}
|
|
135
|
+
onPress={action.onPress}
|
|
136
|
+
style={({ pressed }) => [styles.action, pressed && styles.actionPressed]}
|
|
137
|
+
>
|
|
114
138
|
<IconTile icon={action.icon} symbol={action.symbol} wash={action.wash ?? Theme.colors.surfaceMuted} />
|
|
115
|
-
<
|
|
116
|
-
{
|
|
117
|
-
|
|
139
|
+
<View style={styles.actionCopy}>
|
|
140
|
+
<Text numberOfLines={2} style={styles.actionLabel}>
|
|
141
|
+
{action.label}
|
|
142
|
+
</Text>
|
|
143
|
+
{action.unavailableReason ? <Text style={styles.actionDetail}>{action.unavailableReason}</Text> : null}
|
|
144
|
+
</View>
|
|
118
145
|
</Pressable>
|
|
119
146
|
))}
|
|
120
147
|
</ScrollView>
|
|
@@ -155,6 +182,8 @@ const styles = StyleSheet.create({
|
|
|
155
182
|
actionGrid: { flexDirection: "row", flexWrap: "wrap", gap: Theme.spacing.md, padding: Theme.spacing.lg, paddingBottom: Theme.spacing.md },
|
|
156
183
|
action: { alignItems: "center", backgroundColor: Theme.colors.surfaceMuted, borderColor: Theme.colors.line, borderRadius: Theme.radius.md, borderWidth: 1, flexDirection: "row", gap: Theme.spacing.md, minHeight: 76, paddingHorizontal: Theme.spacing.md, width: "47.8%" },
|
|
157
184
|
actionLabel: { color: Theme.colors.ink, flex: 1, fontSize: 15, fontWeight: "600", lineHeight: 20 },
|
|
185
|
+
actionCopy: { flex: 1, gap: Theme.spacing.xs },
|
|
186
|
+
actionDetail: { color: Theme.colors.ink2, fontSize: 12, lineHeight: 17 },
|
|
158
187
|
actionPressed: { opacity: 0.72, transform: [{ scale: 0.985 }] },
|
|
159
188
|
leaveButton: { alignItems: "center", borderColor: Theme.colors.dangerBackground, borderRadius: Theme.radius.md, borderWidth: 1, flexDirection: "row", gap: Theme.spacing.sm, justifyContent: "center", marginHorizontal: Theme.spacing.lg, minHeight: 52, paddingHorizontal: Theme.spacing.lg },
|
|
160
189
|
leaveText: { color: Theme.colors.error, fontSize: 16, fontWeight: "700" },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Recording } from "@q9labsai/chalk-client";
|
|
1
2
|
import UserGroupIcon from "@hugeicons/core-free-icons/dist/esm/UserGroupIcon";
|
|
2
3
|
import { HugeiconsIcon } from "@hugeicons/react-native";
|
|
3
4
|
import { Image, Platform, StyleSheet, Text, View } from "react-native";
|
|
@@ -10,9 +11,10 @@ export interface SpaceTopBarProps {
|
|
|
10
11
|
participantCount: number;
|
|
11
12
|
formattedDuration: string;
|
|
12
13
|
logoUrl?: string;
|
|
14
|
+
recording?: Recording | null;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDuration, logoUrl }: SpaceTopBarProps): React.JSX.Element {
|
|
17
|
+
export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDuration, logoUrl, recording }: SpaceTopBarProps): React.JSX.Element {
|
|
16
18
|
const theme = useNativeTheme();
|
|
17
19
|
return (
|
|
18
20
|
<View style={[styles.topBar, { backgroundColor: theme.colors.darkCanvas, borderColor: theme.colors.border }]}>
|
|
@@ -26,6 +28,11 @@ export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDurat
|
|
|
26
28
|
<Text style={[styles.timerText, { color: theme.colors.mutedForeground }]}>{formattedDuration}</Text>
|
|
27
29
|
</View>
|
|
28
30
|
</View>
|
|
31
|
+
{recording && recording.status !== "stopped" ? (
|
|
32
|
+
<Text accessibilityLiveRegion="polite" style={[styles.timerText, { color: theme.colors.error }]}>
|
|
33
|
+
{recordingLabels[recording.status]}
|
|
34
|
+
</Text>
|
|
35
|
+
) : null}
|
|
29
36
|
<View style={[styles.topBarRight, { backgroundColor: theme.colors.surface }]}>
|
|
30
37
|
<HugeiconsIcon icon={UserGroupIcon} size={14} color={theme.colors.foreground} />
|
|
31
38
|
<Text style={[styles.topBarCount, { color: theme.colors.foreground }]}>{participantCount}</Text>
|
|
@@ -34,6 +41,14 @@ export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDurat
|
|
|
34
41
|
);
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
const recordingLabels = {
|
|
45
|
+
starting: "Starting recording…",
|
|
46
|
+
recording: "Recording",
|
|
47
|
+
stopping: "Stopping recording…",
|
|
48
|
+
stopped: "Recording stopped",
|
|
49
|
+
failed: "Recording failed",
|
|
50
|
+
};
|
|
51
|
+
|
|
37
52
|
export { SpaceTopBarAndroid as SpaceTopBar };
|
|
38
53
|
|
|
39
54
|
const styles = StyleSheet.create({
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveNativeScreenShareAvailability } from "./screen-share-availability";
|
|
3
|
+
|
|
4
|
+
describe("native screen sharing availability", () => {
|
|
5
|
+
it.each(["ios", "android"])("allows capture on a supported physical %s runtime", (platform) => {
|
|
6
|
+
expect(resolveNativeScreenShareAvailability({ featureEnabled: true, platform, simulator: false, captureAvailable: true }).enabled).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
it.each([
|
|
9
|
+
{ featureEnabled: false, platform: "ios", simulator: false, captureAvailable: true },
|
|
10
|
+
{ featureEnabled: true, platform: "ios", simulator: true, captureAvailable: true },
|
|
11
|
+
{ featureEnabled: true, platform: "android", simulator: false, captureAvailable: false },
|
|
12
|
+
{ featureEnabled: true, platform: "windows", simulator: false, captureAvailable: true },
|
|
13
|
+
])("explains an unavailable runtime: $platform", (input) => {
|
|
14
|
+
expect(resolveNativeScreenShareAvailability(input)).toMatchObject({ enabled: false, detail: expect.any(String), reason: expect.any(String) });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -4,7 +4,7 @@ export interface NativeScreenShareAvailability {
|
|
|
4
4
|
detail: string | null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function resolveNativeScreenShareAvailability({ featureEnabled }: { featureEnabled: boolean }): NativeScreenShareAvailability {
|
|
7
|
+
export function resolveNativeScreenShareAvailability({ featureEnabled, platform, simulator, captureAvailable }: { featureEnabled: boolean; platform: string; simulator: boolean; captureAvailable: boolean }): NativeScreenShareAvailability {
|
|
8
8
|
if (!featureEnabled) {
|
|
9
9
|
return {
|
|
10
10
|
enabled: false,
|
|
@@ -13,6 +13,9 @@ export function resolveNativeScreenShareAvailability({ featureEnabled }: { featu
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
if (simulator) return { enabled: false, reason: "simulator", detail: "Screen sharing requires a physical device." };
|
|
17
|
+
if ((platform !== "ios" && platform !== "android") || !captureAvailable) return { enabled: false, reason: "runtime-unsupported", detail: "Screen sharing is unavailable in this app runtime. Use a supported native build or share from the web app." };
|
|
18
|
+
|
|
16
19
|
return {
|
|
17
20
|
enabled: true,
|
|
18
21
|
reason: null,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChatUploadFile } from "@q9labsai/chalk-client";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { describeChatUploadFile } from "./space-chat-attachments";
|
|
5
|
+
|
|
6
|
+
describe("describeChatUploadFile", () => {
|
|
7
|
+
it("describes a lazy native picker file that also exposes bytes", () => {
|
|
8
|
+
const file: ChatUploadFile = new LazyNativeFileWithBytes();
|
|
9
|
+
|
|
10
|
+
expect(describeChatUploadFile(file)).toMatchObject({ fileName: "mobile-photo.png", mimeType: "image/png", byteLength: 7 });
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
class LazyNativeFileWithBytes {
|
|
15
|
+
readonly name = "mobile-photo.png";
|
|
16
|
+
readonly type = "image/png";
|
|
17
|
+
readonly size = 7;
|
|
18
|
+
|
|
19
|
+
arrayBuffer(): Promise<ArrayBuffer> {
|
|
20
|
+
return Promise.resolve(new ArrayBuffer(this.size));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
bytes(): Promise<Uint8Array> {
|
|
24
|
+
return Promise.resolve(new Uint8Array(this.size));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -11,10 +11,8 @@ export interface NativeChatFileDraft {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function describeChatUploadFile(file: ChatUploadFile): NativeChatFileDraft {
|
|
14
|
-
if ("
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return { file, fileName: file.name, mimeType: file.type, byteLength: file.size };
|
|
14
|
+
if ("arrayBuffer" in file) return { file, fileName: file.name, mimeType: file.type, byteLength: file.size };
|
|
15
|
+
return { file, fileName: file.fileName, mimeType: file.mimeType, byteLength: file.bytes.byteLength };
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function validateChatFileDraft(draft: NativeChatFileDraft): string | null {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ChatSendInput, Reaction } from "@q9labsai/chalk-client";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Alert, Linking } from "react-native";
|
|
3
|
+
import { Alert, Linking, Platform } from "react-native";
|
|
4
|
+
import { mediaDevices } from "@cloudflare/react-native-webrtc";
|
|
4
5
|
|
|
5
6
|
import { useSpaceClient } from "../../context/space-client-context";
|
|
6
|
-
import { useChat as useSpaceChat, useMedia as useSpaceMedia, useParticipants as useSpaceParticipants, useReactions as useSpaceReactions, useSelf, useWhiteboard } from "../../hooks/space-hooks";
|
|
7
|
+
import { useChat as useSpaceChat, useMedia as useSpaceMedia, useParticipants as useSpaceParticipants, useReactions as useSpaceReactions, useSelf, useWhiteboard, useRecording, useConnection } from "../../hooks/space-hooks";
|
|
7
8
|
import { getNativeJourneyContext } from "../../telemetry";
|
|
8
9
|
import type { NativeChatMessage, NativeParticipant, NativeReaction } from "../../ui/native-types";
|
|
9
10
|
import { isIosSimulator } from "../../utils/ios-simulator";
|
|
@@ -29,6 +30,9 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
29
30
|
const chatSlice = useSpaceChat();
|
|
30
31
|
const reactionSlice = useSpaceReactions();
|
|
31
32
|
const whiteboardSlice = useWhiteboard();
|
|
33
|
+
const recording = useRecording().current;
|
|
34
|
+
const connection = useConnection();
|
|
35
|
+
|
|
32
36
|
const journeyContext = getNativeJourneyContext(client);
|
|
33
37
|
const [actionsOpen, setActionsOpen] = useState(false);
|
|
34
38
|
const [reactionPickerOpen, setReactionPickerOpen] = useState(false);
|
|
@@ -55,7 +59,7 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
55
59
|
return {
|
|
56
60
|
...participant,
|
|
57
61
|
id: participant.participantId,
|
|
58
|
-
audioEnabled: local ? media.local.microphone.state === "enabled"
|
|
62
|
+
audioEnabled: local ? media.local.microphone.state === "enabled" : participant.media.microphone === "active",
|
|
59
63
|
videoEnabled: local ? media.local.camera.state === "enabled" || media.local.camera.state === "requesting" : participant.media.camera === "active",
|
|
60
64
|
audioTrack: local ? media.local.microphone.track : (remoteMedia.find((publication) => publication.source === "microphone")?.track ?? null),
|
|
61
65
|
videoTrack: local ? media.local.camera.track : (remoteMedia.find((publication) => publication.source === "camera")?.track ?? null),
|
|
@@ -175,7 +179,7 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
175
179
|
const canClear = canWhiteboard && self.can("manageWhiteboard");
|
|
176
180
|
const whiteboardTransport = useMemo(() => client.whiteboard.transport(), [client.whiteboard, whiteboardSlice.engine.status]);
|
|
177
181
|
const canEndEpisode = self.can("endEpisode");
|
|
178
|
-
const screenShareAvailability = useMemo(() => resolveNativeScreenShareAvailability({ featureEnabled: features?.screenShare !== false }), [features?.screenShare]);
|
|
182
|
+
const screenShareAvailability = useMemo(() => resolveNativeScreenShareAvailability({ featureEnabled: features?.screenShare !== false, platform: Platform.OS, simulator: isIosSimulator(), captureAvailable: typeof mediaDevices.getDisplayMedia === "function" }), [features?.screenShare]);
|
|
179
183
|
const capabilities = useMemo(
|
|
180
184
|
() => ({
|
|
181
185
|
canEndEpisode,
|
|
@@ -297,12 +301,24 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
297
301
|
});
|
|
298
302
|
}, [chat, chatAttachments, chatDraft, client.chat.files, run]);
|
|
299
303
|
|
|
304
|
+
const resolveChatAttachmentUrl = useCallback(
|
|
305
|
+
async (attachmentId: string) => {
|
|
306
|
+
const attachment = chatSlice.messages.flatMap((message) => message.attachments).find((candidate) => candidate.attachmentId === attachmentId);
|
|
307
|
+
return attachment ? client.chat.files.resolveUrl(attachment) : null;
|
|
308
|
+
},
|
|
309
|
+
[chatSlice.messages, client.chat.files],
|
|
310
|
+
);
|
|
311
|
+
|
|
300
312
|
return {
|
|
313
|
+
recording,
|
|
314
|
+
canManageRecording: self.can("manageRecording"),
|
|
315
|
+
canToggleRecording: connection.status === "live" && recording?.status !== "starting" && recording?.status !== "stopping",
|
|
316
|
+
toggleRecording: () => void run(() => (recording?.status === "recording" ? client.recording.stop() : client.recording.start())),
|
|
301
317
|
simulatorMediaDisabled: isIosSimulator(),
|
|
302
318
|
spaceName: spaceName || "Space",
|
|
303
319
|
panel: spacePanels.panel,
|
|
304
320
|
selfName: self.displayName || "Participant",
|
|
305
|
-
isMuted:
|
|
321
|
+
isMuted: media.local.microphone.state !== "enabled",
|
|
306
322
|
isCameraOff: !(media.local.camera.state === "enabled" || media.local.camera.state === "requesting"),
|
|
307
323
|
handRaised: interactionState.isHandRaised,
|
|
308
324
|
raisedHandCount: interactionState.raisedHandCount,
|
|
@@ -321,6 +337,7 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
321
337
|
canChat: capabilities.canChat,
|
|
322
338
|
canParticipants: capabilities.canParticipants,
|
|
323
339
|
canScreenShare: capabilities.canScreenShare,
|
|
340
|
+
screenShareUnavailableReason: features?.screenShare !== false && self.can("publishScreen") ? screenShareAvailability.detail : null,
|
|
324
341
|
canReactions: capabilities.canReactions,
|
|
325
342
|
canHandRaise: capabilities.canHandRaise,
|
|
326
343
|
canInvite: capabilities.canInvite,
|
|
@@ -337,7 +354,7 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
337
354
|
participants: spaceParticipants,
|
|
338
355
|
settings: {
|
|
339
356
|
displayName: self.displayName ?? "",
|
|
340
|
-
microphoneEnabled: media.local.microphone.state === "enabled"
|
|
357
|
+
microphoneEnabled: media.local.microphone.state === "enabled",
|
|
341
358
|
cameraEnabled: media.local.camera.state === "enabled" || media.local.camera.state === "requesting",
|
|
342
359
|
devices: media.devices,
|
|
343
360
|
selection: media.selection,
|
|
@@ -373,9 +390,12 @@ export function useSpaceViewController({ spaceName, inviteLink, layout: controll
|
|
|
373
390
|
void run(() => interactionState.sendReaction(reaction));
|
|
374
391
|
},
|
|
375
392
|
sendChatMessage,
|
|
393
|
+
resolveChatAttachmentUrl,
|
|
376
394
|
openChatAttachment: (attachmentId: string) => {
|
|
377
|
-
|
|
378
|
-
|
|
395
|
+
void run(async () => {
|
|
396
|
+
const url = await resolveChatAttachmentUrl(attachmentId);
|
|
397
|
+
if (url) await Linking.openURL(url);
|
|
398
|
+
});
|
|
379
399
|
},
|
|
380
400
|
markChatMessageVisible: (sequence: string) => void run(() => chat.markAsRead(sequence)),
|
|
381
401
|
admitParticipant: (requestId: string) => void run(() => client.participants.admit(requestId)),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FeedbackScreenshotCapture, FeedbackScreenshotUnavailable } from "@q9labsai/chalk-client";
|
|
2
|
+
import { Platform, type View } from "react-native";
|
|
3
|
+
import { captureRef } from "react-native-view-shot";
|
|
4
|
+
|
|
5
|
+
export type FeedbackCaptureTarget = React.RefObject<View | null>;
|
|
6
|
+
|
|
7
|
+
export type FeedbackCaptureSize = Readonly<{
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
const MAX_SCREENSHOT_BYTES = 450 * 1024;
|
|
13
|
+
const MAX_SCREENSHOT_WIDTH = 1_920;
|
|
14
|
+
const MAX_SCREENSHOT_HEIGHT = 1_080;
|
|
15
|
+
|
|
16
|
+
export async function captureNativeFeedbackView(target: FeedbackCaptureTarget, size: FeedbackCaptureSize): Promise<FeedbackScreenshotCapture | FeedbackScreenshotUnavailable> {
|
|
17
|
+
if (Platform.OS === "macos") return { state: "unavailable", failure_code: "unsupported" };
|
|
18
|
+
if (!target.current) return { state: "unavailable", failure_code: "capture_failed" };
|
|
19
|
+
|
|
20
|
+
const outputSize = boundedOutputSize(size);
|
|
21
|
+
if (!outputSize) return { state: "unavailable", failure_code: "capture_failed" };
|
|
22
|
+
try {
|
|
23
|
+
const captured = await captureRef(target, {
|
|
24
|
+
format: "jpg",
|
|
25
|
+
quality: 0.72,
|
|
26
|
+
result: "base64",
|
|
27
|
+
...outputSize,
|
|
28
|
+
});
|
|
29
|
+
const dataBase64 = normalizeBase64(captured);
|
|
30
|
+
if (!dataBase64) return { state: "unavailable", failure_code: "capture_failed" };
|
|
31
|
+
if (base64ByteLength(dataBase64) > MAX_SCREENSHOT_BYTES) return { state: "unavailable", failure_code: "too_large" };
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
state: "captured",
|
|
35
|
+
mime_type: "image/jpeg",
|
|
36
|
+
width: outputSize.width,
|
|
37
|
+
height: outputSize.height,
|
|
38
|
+
captured_at: new Date().toISOString(),
|
|
39
|
+
data_base64: dataBase64,
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return { state: "unavailable", failure_code: "capture_failed" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedOutputSize(size: FeedbackCaptureSize): Readonly<{ width: number; height: number }> | undefined {
|
|
47
|
+
if (!Number.isFinite(size.width) || !Number.isFinite(size.height) || size.width <= 0 || size.height <= 0) return undefined;
|
|
48
|
+
const scale = Math.min(1, MAX_SCREENSHOT_WIDTH / size.width, MAX_SCREENSHOT_HEIGHT / size.height);
|
|
49
|
+
return {
|
|
50
|
+
width: Math.max(1, Math.floor(size.width * scale)),
|
|
51
|
+
height: Math.max(1, Math.floor(size.height * scale)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeBase64(value: string): string {
|
|
56
|
+
return value.replace(/^data:[^;]+;base64,/u, "").replaceAll(/\s/gu, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function base64ByteLength(value: string): number {
|
|
60
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
61
|
+
return Math.floor((value.length * 3) / 4) - padding;
|
|
62
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FeedbackEvidenceInput, FeedbackPlatformKind } from "@q9labsai/chalk-client";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { getDeviceInfo } from "../runtime";
|
|
5
|
+
|
|
6
|
+
export function createNativeFeedbackEvidence(): FeedbackEvidenceInput {
|
|
7
|
+
const device = getDeviceInfo({ scriptUrl: null });
|
|
8
|
+
const kind = feedbackPlatformKind(Platform.OS);
|
|
9
|
+
const osName = boundedString(device.systemName ?? device.platform ?? kind);
|
|
10
|
+
const osVersion = boundedString(device.osVersion);
|
|
11
|
+
const deviceModel = boundedString(device.model);
|
|
12
|
+
const reactNativeVersion = boundedString(device.reactNativeVersion);
|
|
13
|
+
const deviceClass = kind === "macos" ? "desktop" : device.interfaceIdiom === "pad" ? "tablet" : "phone";
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
sdk: { client: "@q9labsai/chalk-client", ...(reactNativeVersion ? { react_native: reactNativeVersion } : {}) },
|
|
17
|
+
platform: {
|
|
18
|
+
kind,
|
|
19
|
+
...(osName ? { os_name: osName } : {}),
|
|
20
|
+
...(osVersion ? { os_version: osVersion } : {}),
|
|
21
|
+
device_class: deviceClass,
|
|
22
|
+
...(deviceModel ? { device_model: deviceModel } : {}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function feedbackPlatformKind(platform: string): FeedbackPlatformKind {
|
|
28
|
+
if (platform === "ios") return "ios";
|
|
29
|
+
if (platform === "macos") return "macos";
|
|
30
|
+
return "android";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function boundedString(value: string | null): string | undefined {
|
|
34
|
+
const normalized = value?.trim();
|
|
35
|
+
if (!normalized) return undefined;
|
|
36
|
+
return normalized.slice(0, 64);
|
|
37
|
+
}
|
package/src/hooks/space-hooks.ts
CHANGED
|
@@ -34,6 +34,10 @@ export function useReactions(): SpaceSnapshot["reactions"] {
|
|
|
34
34
|
return useSpaceSlice("reactions");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function useRecording(): SpaceSnapshot["recording"] {
|
|
38
|
+
return useSpaceSlice("recording");
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
export function useWhiteboard(): SpaceSnapshot["whiteboard"] {
|
|
38
42
|
return useSpaceSlice("whiteboard");
|
|
39
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { Logo } from "./components/logo/Logo";
|
|
|
6
6
|
export type { LogoMotion, LogoProps, LogoVariant } from "./components/logo/Logo";
|
|
7
7
|
export { ChalkProvider, useSpaceClient } from "./context/space-client-context";
|
|
8
8
|
export type { ChalkProviderProps } from "./context/space-client-context";
|
|
9
|
-
export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useSelf, useWhiteboard } from "./hooks/space-hooks";
|
|
9
|
+
export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useRecording, useSelf, useWhiteboard } from "./hooks/space-hooks";
|
|
10
10
|
export { Theme, createNativeTheme } from "./ui/theme";
|
|
11
11
|
export { extractSpaceInviteFromLink, getClipboardInviteSuggestion, parseSpaceInviteLink } from "./invites";
|
|
12
12
|
export type { SpaceInviteLink } from "./invites";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ReactNativeAsyncStorage,
|
|
17
17
|
} from "@q9labsai/chalk-client";
|
|
18
18
|
import { createSpaceClientForPlatform, type SpaceClientPlatform } from "@q9labsai/chalk-client/effect";
|
|
19
|
+
import type { FeedbackSource } from "@q9labsai/chalk-client";
|
|
19
20
|
import type { SpaceClient } from "@q9labsai/chalk-client";
|
|
20
21
|
import { RTCPeerConnection, mediaDevices } from "@cloudflare/react-native-webrtc";
|
|
21
22
|
import { AppState } from "react-native";
|
|
@@ -41,6 +42,7 @@ export type NativeSpaceClientOptions = {
|
|
|
41
42
|
readonly syncStartupTimeoutMs?: number;
|
|
42
43
|
readonly storage?: ReactNativeAsyncStorage;
|
|
43
44
|
readonly telemetry?: TelemetryJourney;
|
|
45
|
+
readonly feedbackSource?: FeedbackSource;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
export function createNativeSpaceClient(options: NativeSpaceClientOptions): SpaceClient {
|
|
@@ -86,6 +88,8 @@ export function createNativeSpaceClient(options: NativeSpaceClientOptions): Spac
|
|
|
86
88
|
initialMicrophoneEnabled: options.microphone,
|
|
87
89
|
initialCameraEnabled: options.camera,
|
|
88
90
|
onConnectionDiagnostic: (event) => journey?.recordDiagnostic(connectionDiagnostic(event)),
|
|
91
|
+
...(journey ? { recordReconnect: (observation) => journey.recordDiagnostic(observation) } : {}),
|
|
92
|
+
feedbackSource: options.feedbackSource ?? "embedded",
|
|
89
93
|
},
|
|
90
94
|
);
|
|
91
95
|
bindNativeAuthenticatedTelemetry(client, journey);
|
|
@@ -150,6 +154,7 @@ function createMediaClient(apiBaseURL: string, input: MediaFactoryInput, fetch:
|
|
|
150
154
|
participantId: subject.participantId,
|
|
151
155
|
transport: createCloudflareSFUHTTPTransport({
|
|
152
156
|
apiBaseURL,
|
|
157
|
+
telemetry: input.telemetry,
|
|
153
158
|
credential: () => authenticatedNativeCredential(input.credential, journey),
|
|
154
159
|
tenantId: subject.tenantId,
|
|
155
160
|
spaceId: subject.spaceId,
|
|
@@ -169,6 +174,7 @@ function createMediaClient(apiBaseURL: string, input: MediaFactoryInput, fetch:
|
|
|
169
174
|
observePeerConnection?.(connection as unknown as RtcPeerConnection);
|
|
170
175
|
return connection as unknown as globalThis.RTCPeerConnection;
|
|
171
176
|
},
|
|
177
|
+
recordReconnect: input.recordReconnect,
|
|
172
178
|
onError: input.onFailure,
|
|
173
179
|
onScreenEnded: input.onScreenEnded,
|
|
174
180
|
});
|
|
@@ -224,6 +230,7 @@ function createSyncClient(syncURL: string, input: SyncFactoryInput, lifecycle: R
|
|
|
224
230
|
lifecycle,
|
|
225
231
|
webSocket,
|
|
226
232
|
telemetry: input.telemetry,
|
|
233
|
+
recordReconnect: input.recordReconnect,
|
|
227
234
|
...(storage ? { pendingStore: new AsyncStorageV1PendingTargetStore({ scope, storage }) } : {}),
|
|
228
235
|
});
|
|
229
236
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createEmbeddedWhiteboardViewport } from "./embedded-whiteboard-viewport";
|
|
4
|
+
|
|
5
|
+
describe("native embedded whiteboard viewport", () => {
|
|
6
|
+
it("uses the measured Board rectangle rather than the device window", () => {
|
|
7
|
+
expect(createEmbeddedWhiteboardViewport(640, 420, 2)).toEqual({ width: 640, height: 420, scale: 2 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("waits for a usable layout", () => {
|
|
11
|
+
expect(createEmbeddedWhiteboardViewport(0, 420, 2)).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
});
|