Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 42 additions & 45 deletions apps/mobile/app/(main)/chat/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
AppText,
Icon,
FullScreenSearchModal,
AppKeyboardControllerView,
IconButton,
} from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import {
Expand Down Expand Up @@ -65,50 +64,48 @@ export default function ChatScreen(): ReactElement {
);

return (
<AppKeyboardControllerView className='bg-background-primary'>
<AppScreen
className={cn(isOfflineMode && 'pt-20')}
noOutsideSpacing
header={
<AppHeader
title={
isLoading && !modelId ? (
translate('TEXT_LOADING')
) : (
<FullScreenSearchModal
data={models || []}
renderTrigger={renderTrigger}
selectedItemId={modelId}
onSelectItem={onSelectModel}
searchPlaceholder={translate('TEXT_SELECT_A_MODEL')}
/>
)
}
onGoBack={handleGoBackPress}
accessoryRight={
<IconButton
className='p-0'
iconName='moreDots'
onPress={() => {
if (!chat) return;
chatActionsSheetRef.current?.present(chat);
}}
<AppScreen
className={cn(isOfflineMode && 'pt-20')}
noOutsideSpacing
header={
<AppHeader
title={
isLoading && !modelId ? (
translate('TEXT_LOADING')
) : (
<FullScreenSearchModal
data={models || []}
renderTrigger={renderTrigger}
selectedItemId={modelId}
onSelectItem={onSelectModel}
searchPlaceholder={translate('TEXT_SELECT_A_MODEL')}
/>
}
/>
}
scrollDisabled>
<NoConnectionBanner isVisible={isOfflineMode} />
<Chat
chatId={id}
isNewChat={!!isNewChat}
selectedModelId={modelId}
resetToChatsList={handleResetToChatsList} />
<ChatActionsMenuSheet
ref={chatActionsSheetRef}
goToChat={navigateToClonedChat}
isInChat />
</AppScreen>
</AppKeyboardControllerView>
)
}
onGoBack={handleGoBackPress}
accessoryRight={
<IconButton
className='p-0'
iconName='moreDots'
onPress={() => {
if (!chat) return;
chatActionsSheetRef.current?.present(chat);
}}
/>
}
/>
}
scrollDisabled>
<NoConnectionBanner isVisible={isOfflineMode} />
<Chat
chatId={id}
isNewChat={!!isNewChat}
selectedModelId={modelId}
resetToChatsList={handleResetToChatsList} />
<ChatActionsMenuSheet
ref={chatActionsSheetRef}
goToChat={navigateToClonedChat}
isInChat />
</AppScreen>
);
}
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"react-native-compressor": "^1.12.0",
"react-native-extended-stylesheet": "^0.12.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "1.18.5",
"react-native-keyboard-controller": "1.21.8",
"react-native-mmkv": "^3.2.0",
"react-native-modal": "^14.0.0-rc.1",
"react-native-reanimated": "~4.1.1",
Expand Down
88 changes: 42 additions & 46 deletions libs/mobile/chat/features/chat/src/lib/component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useSelector } from '@legendapp/state/react';
import { useKeyboard } from '@react-native-community/hooks';
import { useTranslation } from '@ronas-it/react-native-common-modules/i18n';
import dayjs from 'dayjs';
import { delay } from 'lodash-es';
Expand All @@ -14,9 +13,8 @@ import { useSendMessage } from '@open-webui-react-native/mobile/chat/features/us
import { useSuggestChange } from '@open-webui-react-native/mobile/chat/features/use-suggest-change';
import { useAttachedFiles } from '@open-webui-react-native/mobile/shared/features/use-attached-files';
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';
import { AppSpinner, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import { AppKeyboardStickyView, AppSpinner, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form';
import { useBottomInset } from '@open-webui-react-native/mobile/shared/utils/use-bottom-inset';
import { chatApi, ChatGenerationOption, chatQueriesKeys } from '@open-webui-react-native/shared/data-access/api';
import { Role } from '@open-webui-react-native/shared/data-access/common';
import { useSubscribeToQueryCache } from '@open-webui-react-native/shared/data-access/query-client';
Expand All @@ -38,8 +36,6 @@ interface ChatProps {
export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: ChatProps): ReactElement {
const translate = useTranslation('CHAT.CHAT');
const translateRegeneratePrompt = useTranslation('CHAT.AI_MESSAGE_ACTIONS.REGENERATE_MESSAGE_ACTION_SHEET');
const bottomInset = useBottomInset();
const { keyboardShown } = useKeyboard();

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

Expand Down Expand Up @@ -225,47 +221,47 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
/>
</React.Suspense>
)}
<View
style={!keyboardShown && { paddingBottom: bottomInset }}
className={cn('pt-8 px-16', shouldHideContent && 'opacity-0')}>
{activeInputMode === ActiveInputMode.EDIT && editingMessageId ? (
<EditMessageInput
control={editMessageControl}
name='editMessageInputValue'
autoFocus={true}
onSave={saveMessage}
onCancel={cancelEditingWrapper}
onSend={sendEditedMessage}
isAiMessage={history?.messages[editingMessageId]?.role === Role.ASSISTANT}
/>
) : activeInputMode === ActiveInputMode.SUGGEST && suggestingMessageId ? (
<SuggestChangeInput
control={suggestMessageControl}
name='suggestionInputValue'
autoFocus
onCancel={cancelSuggestingWrapper}
onSend={submitSuggestion}
/>
) : (
<FormChatInput
placeholder={translate('TEXT_INPUT_PLACEHOLDER')}
control={control}
onFocus={handleInputFocus}
name='inputValue'
onSubmit={onSubmit}
isLoading={isSending || !isSocketConnected || isResponseGenerating}
attachedFiles={attachedFiles}
onFileUploaded={handleFileUploaded}
onDeleteFilePress={handleDeleteFile}
attachedImages={attachedImages}
onImageUploaded={handleImageUploaded}
onDeleteImagePress={handleDeleteImage}
modelId={selectedModelId}
isResponseGenerating={isResponseGenerating}
chat={chat}
/>
)}
</View>
<AppKeyboardStickyView className='bg-background-primary-transparent'>
<View className={cn('pt-8 px-16', shouldHideContent && 'opacity-0')}>
{activeInputMode === ActiveInputMode.EDIT && editingMessageId ? (
<EditMessageInput
control={editMessageControl}
name='editMessageInputValue'
autoFocus={true}
onSave={saveMessage}
onCancel={cancelEditingWrapper}
onSend={sendEditedMessage}
isAiMessage={history?.messages[editingMessageId]?.role === Role.ASSISTANT}
/>
) : activeInputMode === ActiveInputMode.SUGGEST && suggestingMessageId ? (
<SuggestChangeInput
control={suggestMessageControl}
name='suggestionInputValue'
autoFocus
onCancel={cancelSuggestingWrapper}
onSend={submitSuggestion}
/>
) : (
<FormChatInput
placeholder={translate('TEXT_INPUT_PLACEHOLDER')}
control={control}
onFocus={handleInputFocus}
name='inputValue'
onSubmit={onSubmit}
isLoading={isSending || !isSocketConnected || isResponseGenerating}
attachedFiles={attachedFiles}
onFileUploaded={handleFileUploaded}
onDeleteFilePress={handleDeleteFile}
attachedImages={attachedImages}
onImageUploaded={handleImageUploaded}
onDeleteImagePress={handleDeleteImage}
modelId={selectedModelId}
isResponseGenerating={isResponseGenerating}
chat={chat}
/>
)}
</View>
</AppKeyboardStickyView>
</Fragment>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function ChatBottomButton({ isVisible, onPress }: ChatBottomButto
}));

return (
<AnimatedView style={animatedStyle} className='absolute right-16 bottom-6'>
<AnimatedView style={animatedStyle} className='absolute right-16 bottom-[130]'>
<IconButton
className='rounded-full border border-text-secondary bg-background-primary p-4'
iconName='arrowDown'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { FlashList } from '@shopify/flash-list';
import { useLocalSearchParams } from 'expo-router';
import { delay } from 'lodash-es';
import React, { ReactElement, useCallback, useRef } from 'react';
import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from 'react-native';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { AiMessageActions } from '@open-webui-react-native/mobile/chat/features/ai-message-actions';
import { useManageMessageSiblings } from '@open-webui-react-native/mobile/chat/features/use-manage-messages-siblings';
import { UserMessageActions } from '@open-webui-react-native/mobile/chat/features/user-message-actions';
import { useSetSelectedModel } from '@open-webui-react-native/mobile/shared/features/use-set-selected-model';
import { View, AppFlashList } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import { AppFlashList, AppKeyboardChatScrollView, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import { ChatScreenParams } from '@open-webui-react-native/mobile/shared/utils/navigation';
import {
Chat,
Expand Down Expand Up @@ -70,6 +70,8 @@ export default function ChatMessagesList({
const { id }: ChatScreenParams = useLocalSearchParams();
const { modelId } = useSetSelectedModel(id);

const renderScrollComponent = useCallback((props: ScrollViewProps) => <AppKeyboardChatScrollView {...props} />, []);

const handleContentSizeChange = (): void => {
//NOTE: Needs to wait until the initial scroll to the bottom or content generation finished and not show the ChatBottomButton before
isScrollToBottomAvailable.current = false;
Expand All @@ -89,11 +91,7 @@ export default function ChatMessagesList({

if (!isMessagesListLoaded && listRef.current && messages?.length > 0) {
delay(() => {
listRef.current?.scrollToIndex({
index: messages.length - 1,
animated: false,
viewPosition: 1,
});
listRef.current?.scrollToEnd({ animated: false });
delay(onLayout, 125);
}, 125);
}
Expand Down Expand Up @@ -261,7 +259,7 @@ export default function ChatMessagesList({
<View className='relative flex-1'>
<AppFlashList<Message>
ref={listRef}
contentContainerClassName='pb-16 px-16'
contentContainerClassName='pb-[135] px-16'
showsVerticalScrollIndicator={false}
drawDistance={1500} //NOTE: Needs to avoid image jumping (while rerendering) when scrolling
keyExtractor={(item) => item.id}
Expand All @@ -272,6 +270,7 @@ export default function ChatMessagesList({
maintainVisibleContentPosition={{
startRenderingFromBottom: true,
}}
renderScrollComponent={renderScrollComponent}
onContentSizeChange={handleContentSizeChange}
onScroll={handleScroll}
onTouchStart={handleTouchStart}
Expand Down
2 changes: 2 additions & 0 deletions libs/mobile/shared/ui/ui-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export * from './header';
export * from './full-screen-modal';
export * from './sheet-header';
export * from './keyboard-aware-scroll-view';
export * from './keyboard-chat-scroll-view';
export * from './keyboard-sticky-view';
export * from './pressable-search-input';
export * from './full-screen-search-modal';
export * from './gesture-pressable-icon-button';
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { cssInterop } from 'nativewind';
import { ReactElement } from 'react';
import { KeyboardChatScrollView, KeyboardChatScrollViewProps } from 'react-native-keyboard-controller';
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';

const CustomizedKeyboardChatScrollView = cssInterop(KeyboardChatScrollView, {
className: 'style',
contentContainerClassName: 'contentContainerStyle',
});

type AppKeyboardChatScrollViewProps = KeyboardChatScrollViewProps & {
className?: string;
contentContainerClassName?: string;
};

export function AppKeyboardChatScrollView({
className,
contentContainerClassName,
offset = 20,
automaticallyAdjustContentInsets = false,
contentInsetAdjustmentBehavior = 'never',
...restProps
}: AppKeyboardChatScrollViewProps): ReactElement {
return (
<CustomizedKeyboardChatScrollView
offset={offset}
automaticallyAdjustContentInsets={automaticallyAdjustContentInsets}
contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior}
className={cn(className)}
contentContainerClassName={cn(contentContainerClassName)}
{...restProps}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AppSafeAreaView } from '@ronas-it/react-native-common-modules/safe-area-view';
import { PropsWithChildren, ReactElement } from 'react';
import { KeyboardStickyView, KeyboardStickyViewProps } from 'react-native-keyboard-controller';
import { Edge } from 'react-native-safe-area-context';
import { cn } from '@open-webui-react-native/mobile/shared/ui/styles';

type AppKeyboardStickyViewProps = PropsWithChildren<
KeyboardStickyViewProps & {
className?: string;
safeAreaEdges?: Array<Edge>;
}
>;

export function AppKeyboardStickyView({
className,
offset = { opened: 20, closed: 0 },
style,
children,
safeAreaEdges = ['bottom'],
...restProps
}: AppKeyboardStickyViewProps): ReactElement {
return (
<KeyboardStickyView
offset={offset}
className={cn('absolute bottom-0 left-0 right-0 w-full', className)}
{...restProps}>
<AppSafeAreaView edges={safeAreaEdges}>{children}</AppSafeAreaView>
</KeyboardStickyView>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './component';
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading