@comergehq/studio 0.1.9 → 0.1.11
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/dist/index.js +1120 -863
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +911 -654
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatComposer.tsx +9 -7
- package/src/components/chat/ChatMessageList.tsx +21 -35
- package/src/components/chat/ChatPage.tsx +3 -1
- package/src/components/comments/AppCommentsSheet.tsx +6 -6
- package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +12 -27
- package/src/components/preview/StatsBar.tsx +4 -3
- package/src/components/studio-sheet/StudioBottomSheet.tsx +3 -3
- package/src/components/studio-sheet/StudioSheetBackground.tsx +3 -2
- package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +7 -5
- package/src/components/utils/ResettableLiquidGlassView.tsx +28 -0
- package/src/components/utils/liquidGlassReset.tsx +36 -0
- package/src/studio/ComergeStudio.tsx +51 -15
- package/src/studio/hooks/useAttachmentUpload.ts +51 -5
- package/src/studio/hooks/useBundleManager.ts +91 -17
- package/src/studio/hooks/useOptimisticChatMessages.ts +128 -0
- package/src/studio/ui/ChatPanel.tsx +1 -0
- package/src/studio/ui/RuntimeRenderer.tsx +7 -2
- package/src/studio/ui/StudioOverlay.tsx +11 -2
package/package.json
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
View,
|
|
10
10
|
type ViewStyle,
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import {
|
|
12
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
13
13
|
import { Plus } from 'lucide-react-native';
|
|
14
14
|
|
|
15
15
|
import { useTheme } from '../../theme';
|
|
16
16
|
import { MultilineTextInput } from './MultilineTextInput';
|
|
17
17
|
import { IconChevronRight, IconClose } from '../icons/StudioIcons';
|
|
18
|
+
import { withAlpha } from '../utils/color';
|
|
19
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
18
20
|
|
|
19
21
|
export type ChatComposerProps = {
|
|
20
22
|
value?: string;
|
|
@@ -157,6 +159,7 @@ export function ChatComposer({
|
|
|
157
159
|
|
|
158
160
|
const textareaBgColor = theme.scheme === 'dark' ? '#18181B' : '#F6F6F6';
|
|
159
161
|
const placeholderTextColor = theme.scheme === 'dark' ? '#A1A1AA' : '#71717A';
|
|
162
|
+
const sendBg = withAlpha(theme.colors.primary, isButtonDisabled ? 0.6 : sendPressed ? 0.9 : 1);
|
|
160
163
|
|
|
161
164
|
return (
|
|
162
165
|
<View
|
|
@@ -165,7 +168,7 @@ export function ChatComposer({
|
|
|
165
168
|
>
|
|
166
169
|
<View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 8 }}>
|
|
167
170
|
<Animated.View style={{ flex: 1, transform: [{ translateX: shakeAnim }] }}>
|
|
168
|
-
<
|
|
171
|
+
<ResettableLiquidGlassView
|
|
169
172
|
style={[
|
|
170
173
|
// LiquidGlassView doesn't reliably auto-size to children; ensure enough height for the
|
|
171
174
|
// thumbnail strip when attachments are present.
|
|
@@ -234,10 +237,10 @@ export function ChatComposer({
|
|
|
234
237
|
lineHeight: 20,
|
|
235
238
|
}}
|
|
236
239
|
/>
|
|
237
|
-
</
|
|
240
|
+
</ResettableLiquidGlassView>
|
|
238
241
|
</Animated.View>
|
|
239
242
|
|
|
240
|
-
<
|
|
243
|
+
<ResettableLiquidGlassView
|
|
241
244
|
style={[{ borderRadius: 100 }, !isLiquidGlassSupported && { backgroundColor: textareaBgColor }]}
|
|
242
245
|
interactive
|
|
243
246
|
effect="clear"
|
|
@@ -248,8 +251,7 @@ export function ChatComposer({
|
|
|
248
251
|
height: 44,
|
|
249
252
|
borderRadius: 22,
|
|
250
253
|
overflow: 'hidden',
|
|
251
|
-
backgroundColor:
|
|
252
|
-
opacity: isButtonDisabled ? 0.6 : sendPressed ? 0.9 : 1,
|
|
254
|
+
backgroundColor: sendBg,
|
|
253
255
|
}}
|
|
254
256
|
>
|
|
255
257
|
<Pressable
|
|
@@ -270,7 +272,7 @@ export function ChatComposer({
|
|
|
270
272
|
)}
|
|
271
273
|
</Pressable>
|
|
272
274
|
</View>
|
|
273
|
-
</
|
|
275
|
+
</ResettableLiquidGlassView>
|
|
274
276
|
</View>
|
|
275
277
|
</View>
|
|
276
278
|
);
|
|
@@ -46,10 +46,13 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
|
|
|
46
46
|
const initialScrollDoneRef = React.useRef(false);
|
|
47
47
|
const lastMessageIdRef = React.useRef<string | null>(null);
|
|
48
48
|
|
|
49
|
+
const data = React.useMemo(() => {
|
|
50
|
+
return [...messages].reverse();
|
|
51
|
+
}, [messages]);
|
|
52
|
+
|
|
49
53
|
const scrollToBottom = React.useCallback((options?: { animated?: boolean }) => {
|
|
50
54
|
const animated = options?.animated ?? true;
|
|
51
|
-
|
|
52
|
-
listRef.current?.scrollToEnd({ animated });
|
|
55
|
+
listRef.current?.scrollToOffset({ offset: 0, animated });
|
|
53
56
|
}, []);
|
|
54
57
|
|
|
55
58
|
React.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
|
|
@@ -57,12 +60,7 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
|
|
|
57
60
|
const handleScroll = React.useCallback(
|
|
58
61
|
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
59
62
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
60
|
-
|
|
61
|
-
// so "near bottom" still means "near the latest message", not "deep into empty space".
|
|
62
|
-
const distanceFromBottom = Math.max(
|
|
63
|
-
contentSize.height - Math.max(bottomInset, 0) - (contentOffset.y + layoutMeasurement.height),
|
|
64
|
-
0
|
|
65
|
-
);
|
|
63
|
+
const distanceFromBottom = Math.max(contentOffset.y - Math.max(bottomInset, 0), 0);
|
|
66
64
|
const isNear = distanceFromBottom <= nearBottomThreshold;
|
|
67
65
|
|
|
68
66
|
if (nearBottomRef.current !== isNear) {
|
|
@@ -73,16 +71,6 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
|
|
|
73
71
|
[bottomInset, nearBottomThreshold, onNearBottomChange]
|
|
74
72
|
);
|
|
75
73
|
|
|
76
|
-
// On first load, start at the bottom
|
|
77
|
-
React.useEffect(() => {
|
|
78
|
-
if (initialScrollDoneRef.current) return;
|
|
79
|
-
if (messages.length === 0) return;
|
|
80
|
-
|
|
81
|
-
initialScrollDoneRef.current = true;
|
|
82
|
-
lastMessageIdRef.current = messages[messages.length - 1]?.id ?? null;
|
|
83
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
84
|
-
return () => cancelAnimationFrame(id);
|
|
85
|
-
}, [messages, scrollToBottom]);
|
|
86
74
|
|
|
87
75
|
// When new messages arrive, keep the user pinned to the bottom only if they already were near it.
|
|
88
76
|
React.useEffect(() => {
|
|
@@ -106,38 +94,36 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
|
|
|
106
94
|
return undefined;
|
|
107
95
|
}, [showTypingIndicator, scrollToBottom]);
|
|
108
96
|
|
|
109
|
-
// When the bottom inset grows/shrinks (e.g. composer height changes), keep pinned users at bottom.
|
|
110
|
-
React.useEffect(() => {
|
|
111
|
-
if (!initialScrollDoneRef.current) return;
|
|
112
|
-
if (!nearBottomRef.current) return;
|
|
113
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
114
|
-
return () => cancelAnimationFrame(id);
|
|
115
|
-
}, [bottomInset, scrollToBottom]);
|
|
116
|
-
|
|
117
97
|
return (
|
|
118
98
|
<BottomSheetFlatList
|
|
119
99
|
ref={listRef}
|
|
120
|
-
|
|
100
|
+
inverted
|
|
101
|
+
data={data}
|
|
121
102
|
keyExtractor={(m: ChatMessage) => m.id}
|
|
122
|
-
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
|
|
123
103
|
keyboardShouldPersistTaps="handled"
|
|
124
104
|
onScroll={handleScroll}
|
|
125
105
|
scrollEventThrottle={16}
|
|
126
106
|
showsVerticalScrollIndicator={false}
|
|
107
|
+
onContentSizeChange={() => {
|
|
108
|
+
if (initialScrollDoneRef.current) return;
|
|
109
|
+
initialScrollDoneRef.current = true;
|
|
110
|
+
lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1]!.id : null;
|
|
111
|
+
nearBottomRef.current = true;
|
|
112
|
+
onNearBottomChange?.(true);
|
|
113
|
+
requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
114
|
+
}}
|
|
127
115
|
contentContainerStyle={[
|
|
128
116
|
{
|
|
129
117
|
paddingHorizontal: theme.spacing.lg,
|
|
130
|
-
|
|
131
|
-
paddingBottom: theme.spacing.sm,
|
|
118
|
+
paddingVertical: theme.spacing.sm,
|
|
132
119
|
},
|
|
133
120
|
contentStyle,
|
|
134
121
|
]}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</View>
|
|
122
|
+
ItemSeparatorComponent={() => <View style={{ height: theme.spacing.sm }} />}
|
|
123
|
+
renderItem={({ item }: { item: ChatMessage }) => (
|
|
124
|
+
<ChatMessageBubble message={item} renderContent={renderMessageContent} />
|
|
139
125
|
)}
|
|
140
|
-
|
|
126
|
+
ListHeaderComponent={
|
|
141
127
|
<View>
|
|
142
128
|
{showTypingIndicator ? (
|
|
143
129
|
<View style={{ marginTop: theme.spacing.sm, alignSelf: 'flex-start', paddingHorizontal: theme.spacing.lg }}>
|
|
@@ -21,6 +21,7 @@ export type ChatPageProps = {
|
|
|
21
21
|
*/
|
|
22
22
|
overlay?: React.ReactNode;
|
|
23
23
|
style?: ViewStyle;
|
|
24
|
+
composerHorizontalPadding?: number;
|
|
24
25
|
onNearBottomChange?: ChatMessageListProps['onNearBottomChange'];
|
|
25
26
|
listRef?: React.RefObject<ChatMessageListRef | null>;
|
|
26
27
|
};
|
|
@@ -34,6 +35,7 @@ export function ChatPage({
|
|
|
34
35
|
composer,
|
|
35
36
|
overlay,
|
|
36
37
|
style,
|
|
38
|
+
composerHorizontalPadding,
|
|
37
39
|
onNearBottomChange,
|
|
38
40
|
listRef,
|
|
39
41
|
}: ChatPageProps) {
|
|
@@ -92,7 +94,7 @@ export function ChatPage({
|
|
|
92
94
|
left: 0,
|
|
93
95
|
right: 0,
|
|
94
96
|
bottom: 0,
|
|
95
|
-
paddingHorizontal: theme.spacing.
|
|
97
|
+
paddingHorizontal: composerHorizontalPadding ?? theme.spacing.md,
|
|
96
98
|
paddingTop: theme.spacing.sm,
|
|
97
99
|
paddingBottom: footerBottomPadding,
|
|
98
100
|
}}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
BottomSheetScrollView,
|
|
7
7
|
} from '@gorhom/bottom-sheet';
|
|
8
8
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
|
-
import {
|
|
9
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
10
10
|
import { Play } from 'lucide-react-native';
|
|
11
11
|
|
|
12
12
|
import { useTheme } from '../../theme';
|
|
@@ -17,6 +17,7 @@ import { CommentRow } from './CommentRow';
|
|
|
17
17
|
import { useAppComments } from './useAppComments';
|
|
18
18
|
import { useAppDetails } from './useAppDetails';
|
|
19
19
|
import { useIosKeyboardSnapFix } from './useIosKeyboardSnapFix';
|
|
20
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
20
21
|
|
|
21
22
|
export type AppCommentsSheetProps = {
|
|
22
23
|
appId: string | null;
|
|
@@ -117,7 +118,7 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
|
|
|
117
118
|
{loadingApp ? 'Loading...' : app?.name || 'Comments'}
|
|
118
119
|
</Text>
|
|
119
120
|
|
|
120
|
-
<
|
|
121
|
+
<ResettableLiquidGlassView
|
|
121
122
|
style={[
|
|
122
123
|
{ borderRadius: 24 },
|
|
123
124
|
!isLiquidGlassSupported && { backgroundColor: theme.scheme === 'dark' ? '#18181B' : '#F6F6F6' },
|
|
@@ -130,10 +131,9 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
|
|
|
130
131
|
width: 32,
|
|
131
132
|
height: 32,
|
|
132
133
|
borderRadius: 999,
|
|
133
|
-
backgroundColor: theme.colors.primary,
|
|
134
|
+
backgroundColor: withAlpha(theme.colors.primary, appId ? 1 : 0.5),
|
|
134
135
|
alignItems: 'center',
|
|
135
136
|
justifyContent: 'center',
|
|
136
|
-
opacity: appId ? 1 : 0.5,
|
|
137
137
|
}}
|
|
138
138
|
>
|
|
139
139
|
<Pressable
|
|
@@ -147,13 +147,13 @@ export function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }: A
|
|
|
147
147
|
alignItems: 'center',
|
|
148
148
|
justifyContent: 'center',
|
|
149
149
|
},
|
|
150
|
-
pressed ? {
|
|
150
|
+
pressed ? { transform: [{ scale: 0.96 }] } : null,
|
|
151
151
|
]}
|
|
152
152
|
>
|
|
153
153
|
<Play size={16} color={theme.colors.onPrimary} />
|
|
154
154
|
</Pressable>
|
|
155
155
|
</View>
|
|
156
|
-
</
|
|
156
|
+
</ResettableLiquidGlassView>
|
|
157
157
|
</View>
|
|
158
158
|
|
|
159
159
|
<BottomSheetScrollView
|
|
@@ -20,11 +20,12 @@ import Animated, {
|
|
|
20
20
|
withSpring,
|
|
21
21
|
withTiming,
|
|
22
22
|
} from 'react-native-reanimated';
|
|
23
|
-
import {
|
|
23
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
24
24
|
|
|
25
|
-
import { DEFAULT_EDGE_PADDING, DEFAULT_OFFSET, DEFAULT_SIZE, ENTER_ROTATION_FROM_DEG, ENTER_SCALE_FROM,
|
|
25
|
+
import { DEFAULT_EDGE_PADDING, DEFAULT_OFFSET, DEFAULT_SIZE, ENTER_ROTATION_FROM_DEG, ENTER_SCALE_FROM, PULSE_DURATION_MS } from './constants';
|
|
26
26
|
import type { FloatingDraggableButtonProps } from './types';
|
|
27
27
|
import { useTheme } from '../../theme';
|
|
28
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
28
29
|
|
|
29
30
|
const HIDDEN_OFFSET_X = 20;
|
|
30
31
|
|
|
@@ -35,9 +36,6 @@ const SPRING_ROTATION_IN = { damping: 15, stiffness: 80 } as const;
|
|
|
35
36
|
const SPRING_ROTATION_GRAB = { damping: 20 } as const;
|
|
36
37
|
const SPRING_SCALE_GRAB = { damping: 15, stiffness: 200 } as const;
|
|
37
38
|
|
|
38
|
-
const TIMING_OPACITY_IN = { duration: 300, easing: Easing.out(Easing.ease) } as const;
|
|
39
|
-
const TIMING_OPACITY_OUT = { duration: 250, easing: Easing.in(Easing.ease) } as const;
|
|
40
|
-
|
|
41
39
|
function clamp(value: number, min: number, max: number) {
|
|
42
40
|
'worklet';
|
|
43
41
|
return Math.max(min, Math.min(max, value));
|
|
@@ -91,7 +89,6 @@ export function FloatingDraggableButton({
|
|
|
91
89
|
const translateY = useSharedValue(getHiddenTranslateY(height));
|
|
92
90
|
const scale = useSharedValue(ENTER_SCALE_FROM);
|
|
93
91
|
const rotation = useSharedValue(ENTER_ROTATION_FROM_DEG);
|
|
94
|
-
const opacity = useSharedValue(1);
|
|
95
92
|
const borderPulse = useSharedValue(0);
|
|
96
93
|
const startPos = useRef({ x: 0, y: 0 });
|
|
97
94
|
const isAnimatingOut = useRef(false);
|
|
@@ -99,26 +96,16 @@ export function FloatingDraggableButton({
|
|
|
99
96
|
const animateToHidden = useCallback(
|
|
100
97
|
(options?: { onFinish?: () => void }) => {
|
|
101
98
|
// Animate back to starting position (reverse of entrance)
|
|
102
|
-
|
|
99
|
+
const finish = options?.onFinish;
|
|
100
|
+
|
|
101
|
+
translateX.value = withSpring(getHiddenTranslateX(size), SPRING_POSITION, (finished?: boolean) => {
|
|
102
|
+
if (finished && finish) runOnJS(finish)();
|
|
103
|
+
});
|
|
103
104
|
translateY.value = withSpring(getHiddenTranslateY(height), SPRING_POSITION);
|
|
104
105
|
scale.value = withSpring(ENTER_SCALE_FROM, SPRING_SCALE_IN);
|
|
105
106
|
rotation.value = withSpring(ENTER_ROTATION_FROM_DEG, SPRING_ROTATION_IN);
|
|
106
|
-
|
|
107
|
-
const finish = options?.onFinish;
|
|
108
|
-
if (!finish) {
|
|
109
|
-
opacity.value = withTiming(HIDDEN_OPACITY, TIMING_OPACITY_OUT);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
opacity.value = withTiming(
|
|
114
|
-
HIDDEN_OPACITY as unknown as number,
|
|
115
|
-
TIMING_OPACITY_OUT,
|
|
116
|
-
(finished?: boolean) => {
|
|
117
|
-
if (finished) runOnJS(finish)();
|
|
118
|
-
}
|
|
119
|
-
);
|
|
120
107
|
},
|
|
121
|
-
[height,
|
|
108
|
+
[height, rotation, scale, size, translateX, translateY]
|
|
122
109
|
);
|
|
123
110
|
|
|
124
111
|
const animateOut = useCallback(() => {
|
|
@@ -163,8 +150,7 @@ export function FloatingDraggableButton({
|
|
|
163
150
|
withSpring(1, SPRING_SCALE_OUT)
|
|
164
151
|
);
|
|
165
152
|
rotation.value = withSpring(0, SPRING_ROTATION_IN);
|
|
166
|
-
|
|
167
|
-
}, [height, offset.bottom, offset.left, opacity, rotation, scale, size, translateX, translateY]);
|
|
153
|
+
}, [height, offset.bottom, offset.left, rotation, scale, size, translateX, translateY]);
|
|
168
154
|
|
|
169
155
|
// Initial animation on mount
|
|
170
156
|
useEffect(() => {
|
|
@@ -228,7 +214,6 @@ export function FloatingDraggableButton({
|
|
|
228
214
|
{ scale: scale.value },
|
|
229
215
|
{ rotate: `${rotation.value}deg` },
|
|
230
216
|
],
|
|
231
|
-
opacity: opacity.value,
|
|
232
217
|
}));
|
|
233
218
|
|
|
234
219
|
const borderAnimatedStyle = useAnimatedStyle(() => {
|
|
@@ -257,7 +242,7 @@ export function FloatingDraggableButton({
|
|
|
257
242
|
accessibilityLabel={ariaLabel}
|
|
258
243
|
>
|
|
259
244
|
<Animated.View style={[{ width: size, height: size, borderRadius: size / 2 }, borderAnimatedStyle]}>
|
|
260
|
-
<
|
|
245
|
+
<ResettableLiquidGlassView
|
|
261
246
|
style={[{ flex: 1, borderRadius: size / 2 }, !isLiquidGlassSupported && { backgroundColor: fallbackBgColor }]}
|
|
262
247
|
interactive
|
|
263
248
|
effect="clear"
|
|
@@ -271,7 +256,7 @@ export function FloatingDraggableButton({
|
|
|
271
256
|
>
|
|
272
257
|
{children ?? <View />}
|
|
273
258
|
</Pressable>
|
|
274
|
-
</
|
|
259
|
+
</ResettableLiquidGlassView>
|
|
275
260
|
</Animated.View>
|
|
276
261
|
|
|
277
262
|
{badgeCount > 0 && (
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Pressable, View, type ViewStyle } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
4
4
|
import { Heart, MessageCircle } from 'lucide-react-native';
|
|
5
5
|
|
|
6
6
|
import { useTheme } from '../../theme';
|
|
7
7
|
import { Text } from '../primitives/Text';
|
|
8
8
|
import { MergeIcon } from '../icons/MergeIcon';
|
|
9
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
9
10
|
|
|
10
11
|
export type StatsBarProps = {
|
|
11
12
|
likeCount: number;
|
|
@@ -41,7 +42,7 @@ export function StatsBar({
|
|
|
41
42
|
style,
|
|
42
43
|
]}
|
|
43
44
|
>
|
|
44
|
-
<
|
|
45
|
+
<ResettableLiquidGlassView
|
|
45
46
|
style={[
|
|
46
47
|
{ borderRadius: 100, overflow: 'hidden' },
|
|
47
48
|
fixedWidth ? { width: fixedWidth } : undefined,
|
|
@@ -101,7 +102,7 @@ export function StatsBar({
|
|
|
101
102
|
</Text>
|
|
102
103
|
</View>
|
|
103
104
|
</View>
|
|
104
|
-
</
|
|
105
|
+
</ResettableLiquidGlassView>
|
|
105
106
|
</View>
|
|
106
107
|
);
|
|
107
108
|
}
|
|
@@ -55,7 +55,7 @@ export type StudioBottomSheetProps = {
|
|
|
55
55
|
export function StudioBottomSheet({
|
|
56
56
|
open,
|
|
57
57
|
onOpenChange,
|
|
58
|
-
snapPoints = ['
|
|
58
|
+
snapPoints = ['100%'],
|
|
59
59
|
sheetRef,
|
|
60
60
|
background,
|
|
61
61
|
children,
|
|
@@ -117,9 +117,9 @@ export function StudioBottomSheet({
|
|
|
117
117
|
ref={resolvedSheetRef}
|
|
118
118
|
index={open ? snapPoints.length - 1 : -1}
|
|
119
119
|
snapPoints={snapPoints}
|
|
120
|
+
enableDynamicSizing={false}
|
|
120
121
|
enablePanDownToClose
|
|
121
|
-
|
|
122
|
-
keyboardBlurBehavior="restore"
|
|
122
|
+
enableContentPanningGesture={false}
|
|
123
123
|
android_keyboardInputMode="adjustResize"
|
|
124
124
|
backgroundComponent={(props: BottomSheetBackgroundProps) => (
|
|
125
125
|
<StudioSheetBackground {...props} renderBackground={background?.renderBackground} />
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Platform, View, type ViewStyle } from 'react-native';
|
|
3
3
|
import type { BottomSheetBackgroundProps } from '@gorhom/bottom-sheet';
|
|
4
|
-
import {
|
|
4
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
5
5
|
|
|
6
6
|
import { useTheme } from '../../theme';
|
|
7
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
7
8
|
|
|
8
9
|
export type StudioSheetBackgroundProps = BottomSheetBackgroundProps & {
|
|
9
10
|
/**
|
|
@@ -35,7 +36,7 @@ export function StudioSheetBackground({
|
|
|
35
36
|
|
|
36
37
|
return (
|
|
37
38
|
<>
|
|
38
|
-
<
|
|
39
|
+
<ResettableLiquidGlassView
|
|
39
40
|
style={[containerStyle, !isLiquidGlassSupported && { backgroundColor: fallbackBgColor }]}
|
|
40
41
|
effect="regular"
|
|
41
42
|
/>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Pressable, View, type ViewStyle } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import { isLiquidGlassSupported } from '@callstack/liquid-glass';
|
|
4
4
|
|
|
5
5
|
import { useTheme } from '../../theme';
|
|
6
|
+
import { withAlpha } from '../utils/color';
|
|
7
|
+
import { ResettableLiquidGlassView } from '../utils/ResettableLiquidGlassView';
|
|
6
8
|
|
|
7
9
|
export type StudioSheetHeaderIconButtonProps = {
|
|
8
10
|
onPress: () => void;
|
|
@@ -38,11 +40,12 @@ export function StudioSheetHeaderIconButton({
|
|
|
38
40
|
const glassInnerBg = intent === 'danger' ? theme.colors.danger : theme.colors.primary;
|
|
39
41
|
|
|
40
42
|
const resolvedOpacity = disabled ? 0.6 : pressed ? 0.9 : 1;
|
|
43
|
+
const glassBg = withAlpha(glassInnerBg, resolvedOpacity);
|
|
41
44
|
|
|
42
45
|
return (
|
|
43
46
|
<View style={style}>
|
|
44
47
|
{appearance === 'glass' ? (
|
|
45
|
-
<
|
|
48
|
+
<ResettableLiquidGlassView
|
|
46
49
|
style={[{ borderRadius: 100 }, !isLiquidGlassSupported && { backgroundColor: glassFallbackBg }]}
|
|
47
50
|
interactive
|
|
48
51
|
effect="clear"
|
|
@@ -54,8 +57,7 @@ export function StudioSheetHeaderIconButton({
|
|
|
54
57
|
borderRadius: 100,
|
|
55
58
|
alignItems: 'center',
|
|
56
59
|
justifyContent: 'center',
|
|
57
|
-
backgroundColor:
|
|
58
|
-
opacity: resolvedOpacity,
|
|
60
|
+
backgroundColor: glassBg,
|
|
59
61
|
}}
|
|
60
62
|
>
|
|
61
63
|
<Pressable
|
|
@@ -73,7 +75,7 @@ export function StudioSheetHeaderIconButton({
|
|
|
73
75
|
{children}
|
|
74
76
|
</Pressable>
|
|
75
77
|
</View>
|
|
76
|
-
</
|
|
78
|
+
</ResettableLiquidGlassView>
|
|
77
79
|
) : (
|
|
78
80
|
<View
|
|
79
81
|
style={{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { LiquidGlassView } from '@callstack/liquid-glass';
|
|
3
|
+
|
|
4
|
+
import { useLiquidGlassResetToken } from './liquidGlassReset';
|
|
5
|
+
|
|
6
|
+
export type ResettableLiquidGlassViewProps = React.ComponentProps<typeof LiquidGlassView>;
|
|
7
|
+
|
|
8
|
+
export function ResettableLiquidGlassView({ children, ...props }: ResettableLiquidGlassViewProps) {
|
|
9
|
+
const token = useLiquidGlassResetToken();
|
|
10
|
+
const [layoutBootKey, setLayoutBootKey] = React.useState(0);
|
|
11
|
+
const sawNonZeroLayoutRef = React.useRef(false);
|
|
12
|
+
|
|
13
|
+
const onLayout: NonNullable<ResettableLiquidGlassViewProps['onLayout']> = (e) => {
|
|
14
|
+
props.onLayout?.(e);
|
|
15
|
+
const { width, height } = e.nativeEvent.layout;
|
|
16
|
+
if (width > 0 && height > 0 && !sawNonZeroLayoutRef.current) {
|
|
17
|
+
sawNonZeroLayoutRef.current = true;
|
|
18
|
+
setLayoutBootKey((k) => k + 1);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<LiquidGlassView key={`liquid-glass-${token}-${layoutBootKey}`} {...props} onLayout={onLayout}>
|
|
24
|
+
{children}
|
|
25
|
+
</LiquidGlassView>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { AppState, type AppStateStatus, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const LiquidGlassResetContext = React.createContext(0);
|
|
5
|
+
|
|
6
|
+
export function LiquidGlassResetProvider({
|
|
7
|
+
children,
|
|
8
|
+
resetTriggers = [],
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
resetTriggers?: React.DependencyList;
|
|
12
|
+
}) {
|
|
13
|
+
const [token, setToken] = React.useState(0);
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (Platform.OS !== 'ios') return;
|
|
17
|
+
|
|
18
|
+
const onChange = (state: AppStateStatus) => {
|
|
19
|
+
if (state === 'active') setToken((t) => t + 1);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const sub = AppState.addEventListener('change', onChange);
|
|
23
|
+
return () => sub.remove();
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
setToken((t) => t + 1);
|
|
28
|
+
}, resetTriggers);
|
|
29
|
+
|
|
30
|
+
return <LiquidGlassResetContext.Provider value={token}>{children}</LiquidGlassResetContext.Provider>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useLiquidGlassResetToken() {
|
|
34
|
+
return React.useContext(LiquidGlassResetContext);
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -13,6 +13,7 @@ import { useStudioActions } from './hooks/useStudioActions';
|
|
|
13
13
|
import { hasNoOutcomeAfterLastHuman } from './lib/chat';
|
|
14
14
|
import { RuntimeRenderer } from './ui/RuntimeRenderer';
|
|
15
15
|
import { StudioOverlay } from './ui/StudioOverlay';
|
|
16
|
+
import { LiquidGlassResetProvider } from '../components/utils/liquidGlassReset';
|
|
16
17
|
|
|
17
18
|
export type ComergeStudioProps = {
|
|
18
19
|
appId: string;
|
|
@@ -46,20 +47,22 @@ export function ComergeStudio({
|
|
|
46
47
|
<StudioBootstrap apiKey={apiKey}>
|
|
47
48
|
{({ userId }) => (
|
|
48
49
|
<BottomSheetModalProvider>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
<LiquidGlassResetProvider resetTriggers={[appId, activeAppId, runtimeAppId]}>
|
|
51
|
+
<ComergeStudioInner
|
|
52
|
+
userId={userId}
|
|
53
|
+
activeAppId={activeAppId}
|
|
54
|
+
setActiveAppId={setActiveAppId}
|
|
55
|
+
runtimeAppId={runtimeAppId}
|
|
56
|
+
setRuntimeAppId={setRuntimeAppId}
|
|
57
|
+
pendingRuntimeTargetAppId={pendingRuntimeTargetAppId}
|
|
58
|
+
setPendingRuntimeTargetAppId={setPendingRuntimeTargetAppId}
|
|
59
|
+
appKey={appKey}
|
|
60
|
+
platform={platform}
|
|
61
|
+
onNavigateHome={onNavigateHome}
|
|
62
|
+
captureTargetRef={captureTargetRef}
|
|
63
|
+
style={style}
|
|
64
|
+
/>
|
|
65
|
+
</LiquidGlassResetProvider>
|
|
63
66
|
</BottomSheetModalProvider>
|
|
64
67
|
)}
|
|
65
68
|
</StudioBootstrap>
|
|
@@ -125,6 +128,34 @@ function ComergeStudioInner({
|
|
|
125
128
|
canRequestLatest: runtimeApp?.status === 'ready',
|
|
126
129
|
});
|
|
127
130
|
|
|
131
|
+
const sawEditingOnActiveAppRef = React.useRef(false);
|
|
132
|
+
const [showPostEditPreparing, setShowPostEditPreparing] = React.useState(false);
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
sawEditingOnActiveAppRef.current = false;
|
|
135
|
+
setShowPostEditPreparing(false);
|
|
136
|
+
}, [activeAppId]);
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
if (!app?.id) return;
|
|
140
|
+
if (app.status === 'editing') {
|
|
141
|
+
sawEditingOnActiveAppRef.current = true;
|
|
142
|
+
setShowPostEditPreparing(false);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (app.status === 'ready' && sawEditingOnActiveAppRef.current) {
|
|
146
|
+
setShowPostEditPreparing(true);
|
|
147
|
+
sawEditingOnActiveAppRef.current = false;
|
|
148
|
+
}
|
|
149
|
+
}, [app?.id, app?.status]);
|
|
150
|
+
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (!showPostEditPreparing) return;
|
|
153
|
+
const stillProcessingBaseBundle = bundle.loading && bundle.loadingMode === 'base' && !bundle.isTesting;
|
|
154
|
+
if (!stillProcessingBaseBundle) {
|
|
155
|
+
setShowPostEditPreparing(false);
|
|
156
|
+
}
|
|
157
|
+
}, [showPostEditPreparing, bundle.loading, bundle.loadingMode, bundle.isTesting]);
|
|
158
|
+
|
|
128
159
|
const threadId = app?.threadId ?? '';
|
|
129
160
|
const thread = useThreadMessages(threadId);
|
|
130
161
|
|
|
@@ -173,7 +204,12 @@ function ComergeStudioInner({
|
|
|
173
204
|
return (
|
|
174
205
|
<View style={[{ flex: 1 }, style]}>
|
|
175
206
|
<View ref={captureTargetRef} style={{ flex: 1 }} collapsable={false}>
|
|
176
|
-
<RuntimeRenderer
|
|
207
|
+
<RuntimeRenderer
|
|
208
|
+
appKey={appKey}
|
|
209
|
+
bundlePath={bundle.bundlePath}
|
|
210
|
+
forcePreparing={showPostEditPreparing}
|
|
211
|
+
renderToken={bundle.renderToken}
|
|
212
|
+
/>
|
|
177
213
|
|
|
178
214
|
<StudioOverlay
|
|
179
215
|
captureTargetRef={captureTargetRef}
|