Skip to content

Commit bb5bb47

Browse files
authored
Merge pull request #64 from RonasIT/PRD-2450-update-keyboard-lib
Update keyboard controller lib and components
2 parents a2abe94 + 84dd777 commit bb5bb47

12 files changed

Lines changed: 167 additions & 107 deletions

File tree

apps/mobile/app/(main)/chat/[id].tsx

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
AppText,
1515
Icon,
1616
FullScreenSearchModal,
17-
AppKeyboardControllerView,
1817
IconButton,
1918
} from '@open-webui-react-native/mobile/shared/ui/ui-kit';
2019
import {
@@ -65,50 +64,48 @@ export default function ChatScreen(): ReactElement {
6564
);
6665

6766
return (
68-
<AppKeyboardControllerView className='bg-background-primary'>
69-
<AppScreen
70-
className={cn(isOfflineMode && 'pt-20')}
71-
noOutsideSpacing
72-
header={
73-
<AppHeader
74-
title={
75-
isLoading && !modelId ? (
76-
translate('TEXT_LOADING')
77-
) : (
78-
<FullScreenSearchModal
79-
data={models || []}
80-
renderTrigger={renderTrigger}
81-
selectedItemId={modelId}
82-
onSelectItem={onSelectModel}
83-
searchPlaceholder={translate('TEXT_SELECT_A_MODEL')}
84-
/>
85-
)
86-
}
87-
onGoBack={handleGoBackPress}
88-
accessoryRight={
89-
<IconButton
90-
className='p-0'
91-
iconName='moreDots'
92-
onPress={() => {
93-
if (!chat) return;
94-
chatActionsSheetRef.current?.present(chat);
95-
}}
67+
<AppScreen
68+
className={cn(isOfflineMode && 'pt-20')}
69+
noOutsideSpacing
70+
header={
71+
<AppHeader
72+
title={
73+
isLoading && !modelId ? (
74+
translate('TEXT_LOADING')
75+
) : (
76+
<FullScreenSearchModal
77+
data={models || []}
78+
renderTrigger={renderTrigger}
79+
selectedItemId={modelId}
80+
onSelectItem={onSelectModel}
81+
searchPlaceholder={translate('TEXT_SELECT_A_MODEL')}
9682
/>
97-
}
98-
/>
99-
}
100-
scrollDisabled>
101-
<NoConnectionBanner isVisible={isOfflineMode} />
102-
<Chat
103-
chatId={id}
104-
isNewChat={!!isNewChat}
105-
selectedModelId={modelId}
106-
resetToChatsList={handleResetToChatsList} />
107-
<ChatActionsMenuSheet
108-
ref={chatActionsSheetRef}
109-
goToChat={navigateToClonedChat}
110-
isInChat />
111-
</AppScreen>
112-
</AppKeyboardControllerView>
83+
)
84+
}
85+
onGoBack={handleGoBackPress}
86+
accessoryRight={
87+
<IconButton
88+
className='p-0'
89+
iconName='moreDots'
90+
onPress={() => {
91+
if (!chat) return;
92+
chatActionsSheetRef.current?.present(chat);
93+
}}
94+
/>
95+
}
96+
/>
97+
}
98+
scrollDisabled>
99+
<NoConnectionBanner isVisible={isOfflineMode} />
100+
<Chat
101+
chatId={id}
102+
isNewChat={!!isNewChat}
103+
selectedModelId={modelId}
104+
resetToChatsList={handleResetToChatsList} />
105+
<ChatActionsMenuSheet
106+
ref={chatActionsSheetRef}
107+
goToChat={navigateToClonedChat}
108+
isInChat />
109+
</AppScreen>
113110
);
114111
}

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"react-native-compressor": "^1.12.0",
7272
"react-native-extended-stylesheet": "^0.12.0",
7373
"react-native-gesture-handler": "~2.28.0",
74-
"react-native-keyboard-controller": "1.18.5",
74+
"react-native-keyboard-controller": "1.21.8",
7575
"react-native-mmkv": "^3.2.0",
7676
"react-native-modal": "^14.0.0-rc.1",
7777
"react-native-reanimated": "~4.1.1",

libs/mobile/chat/features/chat/src/lib/component.tsx

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useSelector } from '@legendapp/state/react';
2-
import { useKeyboard } from '@react-native-community/hooks';
32
import { useTranslation } from '@ronas-it/react-native-common-modules/i18n';
43
import dayjs from 'dayjs';
54
import { delay } from 'lodash-es';
@@ -14,9 +13,8 @@ import { useSendMessage } from '@open-webui-react-native/mobile/chat/features/us
1413
import { useSuggestChange } from '@open-webui-react-native/mobile/chat/features/use-suggest-change';
1514
import { useAttachedFiles } from '@open-webui-react-native/mobile/shared/features/use-attached-files';
1615
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';
17-
import { AppSpinner, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
16+
import { AppKeyboardStickyView, AppSpinner, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
1817
import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form';
19-
import { useBottomInset } from '@open-webui-react-native/mobile/shared/utils/use-bottom-inset';
2018
import { chatApi, ChatGenerationOption, chatQueriesKeys } from '@open-webui-react-native/shared/data-access/api';
2119
import { Role } from '@open-webui-react-native/shared/data-access/common';
2220
import { useSubscribeToQueryCache } from '@open-webui-react-native/shared/data-access/query-client';
@@ -38,8 +36,6 @@ interface ChatProps {
3836
export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: ChatProps): ReactElement {
3937
const translate = useTranslation('CHAT.CHAT');
4038
const translateRegeneratePrompt = useTranslation('CHAT.AI_MESSAGE_ACTIONS.REGENERATE_MESSAGE_ACTION_SHEET');
41-
const bottomInset = useBottomInset();
42-
const { keyboardShown } = useKeyboard();
4339

4440
const [isInputFocusing, setIsInputFocusing] = useState(false); //NOTE: Needs to avoid ChatBottomButton jumping when auto-scrolling after focus
4541

@@ -225,47 +221,47 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
225221
/>
226222
</React.Suspense>
227223
)}
228-
<View
229-
style={!keyboardShown && { paddingBottom: bottomInset }}
230-
className={cn('pt-8 px-16', shouldHideContent && 'opacity-0')}>
231-
{activeInputMode === ActiveInputMode.EDIT && editingMessageId ? (
232-
<EditMessageInput
233-
control={editMessageControl}
234-
name='editMessageInputValue'
235-
autoFocus={true}
236-
onSave={saveMessage}
237-
onCancel={cancelEditingWrapper}
238-
onSend={sendEditedMessage}
239-
isAiMessage={history?.messages[editingMessageId]?.role === Role.ASSISTANT}
240-
/>
241-
) : activeInputMode === ActiveInputMode.SUGGEST && suggestingMessageId ? (
242-
<SuggestChangeInput
243-
control={suggestMessageControl}
244-
name='suggestionInputValue'
245-
autoFocus
246-
onCancel={cancelSuggestingWrapper}
247-
onSend={submitSuggestion}
248-
/>
249-
) : (
250-
<FormChatInput
251-
placeholder={translate('TEXT_INPUT_PLACEHOLDER')}
252-
control={control}
253-
onFocus={handleInputFocus}
254-
name='inputValue'
255-
onSubmit={onSubmit}
256-
isLoading={isSending || !isSocketConnected || isResponseGenerating}
257-
attachedFiles={attachedFiles}
258-
onFileUploaded={handleFileUploaded}
259-
onDeleteFilePress={handleDeleteFile}
260-
attachedImages={attachedImages}
261-
onImageUploaded={handleImageUploaded}
262-
onDeleteImagePress={handleDeleteImage}
263-
modelId={selectedModelId}
264-
isResponseGenerating={isResponseGenerating}
265-
chat={chat}
266-
/>
267-
)}
268-
</View>
224+
<AppKeyboardStickyView className='bg-background-primary-transparent'>
225+
<View className={cn('pt-8 px-16', shouldHideContent && 'opacity-0')}>
226+
{activeInputMode === ActiveInputMode.EDIT && editingMessageId ? (
227+
<EditMessageInput
228+
control={editMessageControl}
229+
name='editMessageInputValue'
230+
autoFocus={true}
231+
onSave={saveMessage}
232+
onCancel={cancelEditingWrapper}
233+
onSend={sendEditedMessage}
234+
isAiMessage={history?.messages[editingMessageId]?.role === Role.ASSISTANT}
235+
/>
236+
) : activeInputMode === ActiveInputMode.SUGGEST && suggestingMessageId ? (
237+
<SuggestChangeInput
238+
control={suggestMessageControl}
239+
name='suggestionInputValue'
240+
autoFocus
241+
onCancel={cancelSuggestingWrapper}
242+
onSend={submitSuggestion}
243+
/>
244+
) : (
245+
<FormChatInput
246+
placeholder={translate('TEXT_INPUT_PLACEHOLDER')}
247+
control={control}
248+
onFocus={handleInputFocus}
249+
name='inputValue'
250+
onSubmit={onSubmit}
251+
isLoading={isSending || !isSocketConnected || isResponseGenerating}
252+
attachedFiles={attachedFiles}
253+
onFileUploaded={handleFileUploaded}
254+
onDeleteFilePress={handleDeleteFile}
255+
attachedImages={attachedImages}
256+
onImageUploaded={handleImageUploaded}
257+
onDeleteImagePress={handleDeleteImage}
258+
modelId={selectedModelId}
259+
isResponseGenerating={isResponseGenerating}
260+
chat={chat}
261+
/>
262+
)}
263+
</View>
264+
</AppKeyboardStickyView>
269265
</Fragment>
270266
);
271267
}

libs/mobile/chat/features/chat/src/lib/components/chat-bottom-button/component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function ChatBottomButton({ isVisible, onPress }: ChatBottomButto
1313
}));
1414

1515
return (
16-
<AnimatedView style={animatedStyle} className='absolute right-16 bottom-6'>
16+
<AnimatedView style={animatedStyle} className='absolute right-16 bottom-[130]'>
1717
<IconButton
1818
className='rounded-full border border-text-secondary bg-background-primary p-4'
1919
iconName='arrowDown'

libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { FlashList } from '@shopify/flash-list';
22
import { useLocalSearchParams } from 'expo-router';
33
import { delay } from 'lodash-es';
44
import React, { ReactElement, useCallback, useRef } from 'react';
5-
import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
5+
import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from 'react-native';
66
import { useSharedValue, withTiming } from 'react-native-reanimated';
77
import { AiMessageActions } from '@open-webui-react-native/mobile/chat/features/ai-message-actions';
88
import { useManageMessageSiblings } from '@open-webui-react-native/mobile/chat/features/use-manage-messages-siblings';
99
import { UserMessageActions } from '@open-webui-react-native/mobile/chat/features/user-message-actions';
1010
import { useSetSelectedModel } from '@open-webui-react-native/mobile/shared/features/use-set-selected-model';
11-
import { View, AppFlashList } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
11+
import { AppFlashList, AppKeyboardChatScrollView, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
1212
import { ChatScreenParams } from '@open-webui-react-native/mobile/shared/utils/navigation';
1313
import {
1414
Chat,
@@ -70,6 +70,8 @@ export default function ChatMessagesList({
7070
const { id }: ChatScreenParams = useLocalSearchParams();
7171
const { modelId } = useSetSelectedModel(id);
7272

73+
const renderScrollComponent = useCallback((props: ScrollViewProps) => <AppKeyboardChatScrollView {...props} />, []);
74+
7375
const handleContentSizeChange = (): void => {
7476
//NOTE: Needs to wait until the initial scroll to the bottom or content generation finished and not show the ChatBottomButton before
7577
isScrollToBottomAvailable.current = false;
@@ -89,11 +91,7 @@ export default function ChatMessagesList({
8991

9092
if (!isMessagesListLoaded && listRef.current && messages?.length > 0) {
9193
delay(() => {
92-
listRef.current?.scrollToIndex({
93-
index: messages.length - 1,
94-
animated: false,
95-
viewPosition: 1,
96-
});
94+
listRef.current?.scrollToEnd({ animated: false });
9795
delay(onLayout, 125);
9896
}, 125);
9997
}
@@ -261,7 +259,7 @@ export default function ChatMessagesList({
261259
<View className='relative flex-1'>
262260
<AppFlashList<Message>
263261
ref={listRef}
264-
contentContainerClassName='pb-16 px-16'
262+
contentContainerClassName='pb-[135] px-16'
265263
showsVerticalScrollIndicator={false}
266264
drawDistance={1500} //NOTE: Needs to avoid image jumping (while rerendering) when scrolling
267265
keyExtractor={(item) => item.id}
@@ -272,6 +270,7 @@ export default function ChatMessagesList({
272270
maintainVisibleContentPosition={{
273271
startRenderingFromBottom: true,
274272
}}
273+
renderScrollComponent={renderScrollComponent}
275274
onContentSizeChange={handleContentSizeChange}
276275
onScroll={handleScroll}
277276
onTouchStart={handleTouchStart}

libs/mobile/shared/ui/ui-kit/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export * from './header';
3939
export * from './full-screen-modal';
4040
export * from './sheet-header';
4141
export * from './keyboard-aware-scroll-view';
42+
export * from './keyboard-chat-scroll-view';
43+
export * from './keyboard-sticky-view';
4244
export * from './pressable-search-input';
4345
export * from './full-screen-search-modal';
4446
export * from './gesture-pressable-icon-button';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { cssInterop } from 'nativewind';
2+
import { ReactElement } from 'react';
3+
import { KeyboardChatScrollView, KeyboardChatScrollViewProps } from 'react-native-keyboard-controller';
4+
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';
5+
6+
const CustomizedKeyboardChatScrollView = cssInterop(KeyboardChatScrollView, {
7+
className: 'style',
8+
contentContainerClassName: 'contentContainerStyle',
9+
});
10+
11+
type AppKeyboardChatScrollViewProps = KeyboardChatScrollViewProps & {
12+
className?: string;
13+
contentContainerClassName?: string;
14+
};
15+
16+
export function AppKeyboardChatScrollView({
17+
className,
18+
contentContainerClassName,
19+
offset = 20,
20+
automaticallyAdjustContentInsets = false,
21+
contentInsetAdjustmentBehavior = 'never',
22+
...restProps
23+
}: AppKeyboardChatScrollViewProps): ReactElement {
24+
return (
25+
<CustomizedKeyboardChatScrollView
26+
offset={offset}
27+
automaticallyAdjustContentInsets={automaticallyAdjustContentInsets}
28+
contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior}
29+
className={cn(className)}
30+
contentContainerClassName={cn(contentContainerClassName)}
31+
{...restProps}
32+
/>
33+
);
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './component';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AppSafeAreaView } from '@ronas-it/react-native-common-modules/safe-area-view';
2+
import { PropsWithChildren, ReactElement } from 'react';
3+
import { KeyboardStickyView, KeyboardStickyViewProps } from 'react-native-keyboard-controller';
4+
import { Edge } from 'react-native-safe-area-context';
5+
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';
6+
7+
type AppKeyboardStickyViewProps = PropsWithChildren<
8+
KeyboardStickyViewProps & {
9+
className?: string;
10+
safeAreaEdges?: Array<Edge>;
11+
}
12+
>;
13+
14+
export function AppKeyboardStickyView({
15+
className,
16+
offset = { opened: 20, closed: 0 },
17+
style,
18+
children,
19+
safeAreaEdges = ['bottom'],
20+
...restProps
21+
}: AppKeyboardStickyViewProps): ReactElement {
22+
return (
23+
<KeyboardStickyView
24+
offset={offset}
25+
className={cn('absolute bottom-0 left-0 right-0 w-full', className)}
26+
{...restProps}>
27+
<AppSafeAreaView edges={safeAreaEdges}>{children}</AppSafeAreaView>
28+
</KeyboardStickyView>
29+
);
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './component';

0 commit comments

Comments
 (0)