@comergehq/studio 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comergehq/studio",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Comerge studio",
5
5
  "main": "src/index.ts",
6
6
  "module": "dist/index.mjs",
@@ -1,5 +1,7 @@
1
1
  import * as React from 'react';
2
- import { View, type ViewStyle } from 'react-native';
2
+ import { Keyboard, Platform, View, type ViewStyle } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle } from 'react-native-reanimated';
3
5
 
4
6
  import type { ChatMessage } from '../models/types';
5
7
  import { useTheme } from '../../theme';
@@ -37,7 +39,37 @@ export function ChatPage({
37
39
  listRef,
38
40
  }: ChatPageProps) {
39
41
  const theme = useTheme();
42
+ const insets = useSafeAreaInsets();
40
43
  const [composerHeight, setComposerHeight] = React.useState(0);
44
+ const [keyboardVisible, setKeyboardVisible] = React.useState(false);
45
+ const animatedKeyboard = useAnimatedKeyboard();
46
+
47
+ React.useEffect(() => {
48
+ if (Platform.OS !== 'ios') return;
49
+
50
+ const show = Keyboard.addListener('keyboardWillShow', () => setKeyboardVisible(true));
51
+ const hide = Keyboard.addListener('keyboardWillHide', () => setKeyboardVisible(false));
52
+ return () => {
53
+ show.remove();
54
+ hide.remove();
55
+ };
56
+ }, []);
57
+
58
+ const footerBottomPadding = Platform.OS === 'ios' ? (keyboardVisible ? 0 : insets.bottom) : insets.bottom + 10;
59
+ const footerAnimatedStyle = useAnimatedStyle(() => {
60
+ if (Platform.OS !== 'ios') return { paddingBottom: insets.bottom + 10 };
61
+ return { paddingBottom: animatedKeyboard.height.value > 0 ? 0 : insets.bottom };
62
+ });
63
+ const overlayBottom = composerHeight + footerBottomPadding + theme.spacing.lg;
64
+
65
+ const resolvedOverlay = React.useMemo(() => {
66
+ if (!overlay) return null;
67
+ if (!React.isValidElement(overlay)) return overlay;
68
+ const prevStyle = (overlay.props as any)?.style;
69
+ return React.cloneElement(overlay as any, {
70
+ style: [prevStyle, { bottom: overlayBottom }],
71
+ });
72
+ }, [overlay, overlayBottom]);
41
73
  return (
42
74
  <View style={[{ flex: 1 }, style]}>
43
75
  {header ? <View>{header}</View> : null}
@@ -53,15 +85,30 @@ export function ChatPage({
53
85
  showTypingIndicator={showTypingIndicator}
54
86
  renderMessageContent={renderMessageContent}
55
87
  onNearBottomChange={onNearBottomChange}
56
- contentStyle={{ paddingBottom: theme.spacing.xl + composerHeight }}
88
+ contentStyle={{ paddingBottom: theme.spacing.xl + composerHeight + footerBottomPadding }}
57
89
  />
58
- {overlay}
90
+ {resolvedOverlay}
91
+
92
+ <Animated.View
93
+ style={[
94
+ {
95
+ position: 'absolute',
96
+ left: 0,
97
+ right: 0,
98
+ bottom: 0,
99
+ paddingHorizontal: theme.spacing.lg,
100
+ paddingTop: theme.spacing.sm,
101
+ },
102
+ footerAnimatedStyle,
103
+ ]}
104
+ >
105
+ <ChatComposer
106
+ {...composer}
107
+ attachments={composer.attachments ?? []}
108
+ onLayout={({ height }) => setComposerHeight(height)}
109
+ />
110
+ </Animated.View>
59
111
  </View>
60
- <ChatComposer
61
- {...composer}
62
- attachments={composer.attachments ?? []}
63
- onLayout={({ height }) => setComposerHeight(height)}
64
- />
65
112
  </View>
66
113
  );
67
114
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { View } from 'react-native';
2
+ import { Keyboard, Platform, View } from 'react-native';
3
3
  import BottomSheet, { type BottomSheetBackgroundProps, type BottomSheetProps } from '@gorhom/bottom-sheet';
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
5
 
@@ -67,8 +67,17 @@ export function StudioBottomSheet({
67
67
  const internalSheetRef = React.useRef<BottomSheet | null>(null);
68
68
  const resolvedSheetRef = sheetRef ?? internalSheetRef;
69
69
 
70
- // Gorhom BottomSheet `index` is not reliably "fully controlled" across versions.
71
- // Ensure the visual sheet actually opens/closes when `open` changes (e.g. via header X button).
70
+ React.useEffect(() => {
71
+ if (Platform.OS !== 'ios') return;
72
+ const sub = Keyboard.addListener('keyboardDidHide', () => {
73
+ const sheet = resolvedSheetRef.current;
74
+ if (!sheet || !open) return;
75
+ const targetIndex = snapPoints.length - 1;
76
+ setTimeout(() => sheet.snapToIndex(targetIndex), 10);
77
+ });
78
+ return () => sub.remove();
79
+ }, [open, resolvedSheetRef, snapPoints.length]);
80
+
72
81
  React.useEffect(() => {
73
82
  const sheet = resolvedSheetRef.current;
74
83
  if (!sheet) return;
@@ -101,7 +110,7 @@ export function StudioBottomSheet({
101
110
  <StudioSheetBackground {...props} renderBackground={background?.renderBackground} />
102
111
  )}
103
112
  topInset={insets.top}
104
- bottomInset={insets.bottom}
113
+ bottomInset={0}
105
114
  handleIndicatorStyle={{ backgroundColor: theme.colors.handleIndicator }}
106
115
  onChange={handleChange}
107
116
  {...bottomSheetProps}
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { Keyboard, View, useWindowDimensions } from 'react-native';
2
+ import { Keyboard, Platform, View, useWindowDimensions } from 'react-native';
3
3
 
4
4
  import type { App } from '../../data/apps/types';
5
5
  import type { MergeRequest } from '../../data/merge-requests/types';
@@ -117,8 +117,24 @@ export function StudioOverlay({
117
117
  }, [openSheet]);
118
118
 
119
119
  const backToPreview = React.useCallback(() => {
120
+ if (Platform.OS !== 'ios') {
121
+ Keyboard.dismiss();
122
+ setActivePage('preview');
123
+ return;
124
+ }
125
+
126
+ let done = false;
127
+ const finalize = () => {
128
+ if (done) return;
129
+ done = true;
130
+ sub.remove();
131
+ clearTimeout(t);
132
+ setActivePage('preview');
133
+ };
134
+
135
+ const sub = Keyboard.addListener('keyboardDidHide', finalize);
136
+ const t = setTimeout(finalize, 350);
120
137
  Keyboard.dismiss();
121
- setActivePage('preview');
122
138
  }, []);
123
139
 
124
140
  const startDraw = React.useCallback(() => {