From fb4c0db9360ea604638608b07d706d23d016a8b9 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Wed, 28 Jan 2026 00:30:25 +0800 Subject: [PATCH 01/12] fix: disable ScreenWrapper scroll when isKeyboardAvoiding is true, custom bottom offset added --- apps/mobile/app/(main)/chat/create.tsx | 2 +- .../ui/keyboard-avoiding-view/src/lib/component.tsx | 7 ++++--- .../shared/ui/screen-wrapper/src/lib/component.tsx | 13 ++++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/mobile/app/(main)/chat/create.tsx b/apps/mobile/app/(main)/chat/create.tsx index 0f679f4..6b62115 100644 --- a/apps/mobile/app/(main)/chat/create.tsx +++ b/apps/mobile/app/(main)/chat/create.tsx @@ -47,7 +47,7 @@ export default function CreateChatScreen(): ReactElement { header: , }} safeAreaProps={{ edges: [] }} - keyBoardAvoidingProps={{ enabled: !isBottomSheetInputFocused }}> + keyBoardAvoidingProps={{ enabled: !isBottomSheetInputFocused, bottomOffset: 60 }}> & { onFocus?: ((event: any) => void) | null; contentContainerStyleKeyboardShown?: StyleProp; enabled?: boolean; + bottomOffset?: number; }; // TODO: Research how use nativewind here export const AppKeyboardAvoidingView: ComponentType = ({ children, ...props }) => { - const { onFocus, ...restProps } = props; + const { onFocus, contentContainerStyle, bottomOffset, ...restProps } = props; return ( {children} diff --git a/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx b/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx index 39dab46..2be92ca 100644 --- a/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx +++ b/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx @@ -25,13 +25,20 @@ export function ScreenWrapper({ isKeyboardAvoiding, keyBoardAvoidingProps, }: PropsWithChildren): ReactElement { - const { header, ...restScreenProps } = screenProps || {}; + const { header, scrollDisabled, ...restScreenProps } = screenProps || {}; + + const effectiveScrollDisabled = isKeyboardAvoiding ? true : scrollDisabled; + const content = isKeyboardAvoiding ? ( - {children} + + {children} + ) : ( - {children} + + {children} + ); return ( From 6ae9ce7fdbf429992e917a262ed9e7f7fae55903 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Wed, 28 Jan 2026 00:33:30 +0800 Subject: [PATCH 02/12] fix: KnowledgeResponse items transform fix --- .../api/src/lib/knowledge/models/knowledge-response.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-response.ts b/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-response.ts index 387f00f..5851bed 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-response.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-response.ts @@ -1,8 +1,9 @@ -import { Expose } from 'class-transformer'; +import { Expose, Type } from 'class-transformer'; import { Knowledge } from './knowledge'; export class KnowledgeResponse { @Expose() + @Type(() => Knowledge) public items: Array; @Expose() From 67d80267ca5640b60bdd86efc27d642ca3dc3a30 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Fri, 30 Jan 2026 13:56:00 +0800 Subject: [PATCH 03/12] chore: props simplified --- .../shared/ui/screen-wrapper/src/lib/component.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx b/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx index 2be92ca..ba54d59 100644 --- a/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx +++ b/libs/mobile/shared/ui/screen-wrapper/src/lib/component.tsx @@ -25,20 +25,16 @@ export function ScreenWrapper({ isKeyboardAvoiding, keyBoardAvoidingProps, }: PropsWithChildren): ReactElement { - const { header, scrollDisabled, ...restScreenProps } = screenProps || {}; - - const effectiveScrollDisabled = isKeyboardAvoiding ? true : scrollDisabled; + const { header, ...restScreenProps } = screenProps || {}; const content = isKeyboardAvoiding ? ( - + {children} ) : ( - - {children} - + {children} ); return ( From 3f707924d29e1cf665c181e08ca568858122430c Mon Sep 17 00:00:00 2001 From: tfomkin Date: Fri, 30 Jan 2026 14:46:39 +0800 Subject: [PATCH 04/12] chore: replaced styles for Markdown code block with classnames --- .../shared/features/markdown-view/src/lib/component.tsx | 7 +------ libs/mobile/shared/ui/code-block/src/lib/component.tsx | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/libs/mobile/shared/features/markdown-view/src/lib/component.tsx b/libs/mobile/shared/features/markdown-view/src/lib/component.tsx index 3a4e41e..50c3d65 100644 --- a/libs/mobile/shared/features/markdown-view/src/lib/component.tsx +++ b/libs/mobile/shared/features/markdown-view/src/lib/component.tsx @@ -54,7 +54,7 @@ export function AppMarkdownView({ ) => { return ( {copyButton} - {formattedContent} + + {formattedContent} + ); } From 33ed9064b8ec45ef12011551a9e1ebb48886a8a7 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Sat, 7 Feb 2026 01:47:05 +0800 Subject: [PATCH 05/12] fix: allow user to scroll up while response is generating --- .../components/messages-list/component.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx index 31b3eaf..0932ad7 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx @@ -1,7 +1,7 @@ import { FlashList } from '@shopify/flash-list'; import { useLocalSearchParams } from 'expo-router'; import { delay } from 'lodash-es'; -import React, { ReactElement, useCallback, useRef, useState } from 'react'; +import React, { ReactElement, useCallback, useRef } from 'react'; import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; import { AiMessageActions } from '@open-webui-react-native/mobile/chat/features/ai-message-actions'; @@ -62,7 +62,7 @@ export default function ChatMessagesList({ const isScrollToBottomAvailableTimeout = useRef(null); //NOTE: number needs to fix pipeline lint error const isScrollToBottomVisible = useSharedValue(0); const previousScrollY = useRef(0); - const [autoscrollToBottomThreshold, setAutoscrollToBottomThreshold] = useState(1); + const isNearBottomRef = useRef(true); const { showPreviousSibling, showNextSibling, getSiblingsInfo } = useManageMessageSiblings(chatId, history); const { mutate: completeChat } = chatApi.useCompleteChat(); @@ -80,6 +80,12 @@ export default function ChatMessagesList({ isScrollToBottomAvailable.current = true; }, 500); + if (isNearBottomRef.current && listRef.current && messages?.length > 0) { + requestAnimationFrame(() => { + listRef.current?.scrollToEnd({ animated: true }); + }); + } + if (!isMessagesListLoaded && listRef.current && messages?.length > 0) { delay(() => { listRef.current?.scrollToIndex({ @@ -108,6 +114,7 @@ export default function ChatMessagesList({ //NOTE: The indent of 100 is needed to display the button not immediately when we start scrolling, //but when a small distance has been scrolled. const isNearBottom = scrollY + containerHeight >= contentHeight - 100; + isNearBottomRef.current = isNearBottom; if (isNearBottom || isScrollingUp) { animateScrollToBottom(0); @@ -120,6 +127,8 @@ export default function ChatMessagesList({ //NOTE: Needs to hide scroll to bottom button to avoid its jumping while scrolling to bottom animateScrollToBottom(0); isScrollToBottomAvailable.current = false; + isNearBottomRef.current = true; + delay(() => { isScrollToBottomAvailable.current = true; }, 1000); @@ -128,7 +137,6 @@ export default function ChatMessagesList({ }; const handleEditPress = (index: number, messageId: string, content: string): void => { - setAutoscrollToBottomThreshold(undefined); onEditPress(messageId, content); delay(() => { listRef.current?.scrollToIndex({ @@ -137,9 +145,6 @@ export default function ChatMessagesList({ animated: true, }); }, 500); - delay(() => { - setAutoscrollToBottomThreshold(1); - }, 1000); }; const handleContinueResponsePress = (messageId: string): void => { @@ -243,12 +248,6 @@ export default function ChatMessagesList({ ItemSeparatorComponent={() => } data={messages} renderItem={renderItem} - // TODO: Add autoscrollToBottom logic when it implemented in lib - maintainVisibleContentPosition={{ - startRenderingFromBottom: true, - animateAutoScrollToBottom: true, - autoscrollToBottomThreshold, - }} onContentSizeChange={handleContentSizeChange} onScroll={handleScroll} scrollEventThrottle={16} From 2b0e6ace561a020bbd81fc41c38544d8baff2e01 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Sun, 8 Feb 2026 17:46:10 +0800 Subject: [PATCH 06/12] fix: voice mode modal layout fix --- .../voice-mode-modal/src/lib/component.tsx | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx b/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx index 517fea6..e0573d1 100644 --- a/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx +++ b/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx @@ -184,31 +184,33 @@ export function VoiceModeModal({ onChatCreated, ref, ...props }: VoiceModeModalP backdropTransitionOutTiming={1} animationOutTiming={1} animationIn='fadeIn' - style={{ overflow: 'hidden' }} + style={{ overflow: 'hidden', margin: 0 }} {...props}> - - - {isThinking || isAiSpeaking ? : } - - - - - {isAiSpeaking - ? translate('TEXT_TALKING') - : isThinking - ? translate('TEXT_THINKING') - : translate('TEXT_LISTENING')} - - - - + + + + {isThinking || isAiSpeaking ? : } + + + + + {isAiSpeaking + ? translate('TEXT_TALKING') + : isThinking + ? translate('TEXT_THINKING') + : translate('TEXT_LISTENING')} + + + + + ); From 2a9bca9c6dce5a9ed945f72295f5a67be87bd54d Mon Sep 17 00:00:00 2001 From: tfomkin Date: Wed, 11 Feb 2026 13:12:21 +0800 Subject: [PATCH 07/12] fix: enable maintainVisibleContentPosition startRenderingFromBottom --- .../chat/src/lib/components/messages-list/component.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx index 0932ad7..d11f70c 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx @@ -248,6 +248,9 @@ export default function ChatMessagesList({ ItemSeparatorComponent={() => } data={messages} renderItem={renderItem} + maintainVisibleContentPosition={{ + startRenderingFromBottom: true, + }} onContentSizeChange={handleContentSizeChange} onScroll={handleScroll} scrollEventThrottle={16} From 3486abf2baaeed075b35227ad4a20032066ed841 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Fri, 13 Feb 2026 16:39:52 +0800 Subject: [PATCH 08/12] fix: disable AI message actions long press while response is generating --- .../chat/features/ai-message-actions/src/lib/component.tsx | 7 ++++++- .../chat/src/lib/components/messages-list/component.tsx | 1 + .../chat/ui/message-actions-wrapper/src/lib/component.tsx | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx b/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx index a7a00ff..9bff564 100644 --- a/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx +++ b/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx @@ -19,6 +19,7 @@ interface AiMessageActionsProps { onAddDetails: (messageId: string) => void; onMoreConcise: (messageId: string) => void; isLast: boolean; + isResponseGenerating: boolean; } //TODO Extend with more actions - https://www.figma.com/design/YPCZjyVlD86psDwUxvMVBc/OpenWebUI-Redesign-React-Native?node-id=27540-25291&t=kg2yUIDp3UQDStLf-0 @@ -31,6 +32,7 @@ export function AiMessageActions({ onAddDetails, onMoreConcise, isLast, + isResponseGenerating, children, }: PropsWithChildren): ReactElement { const translate = useTranslation('CHAT.AI_MESSAGE_ACTIONS'); @@ -134,7 +136,10 @@ export function AiMessageActions({ return ( - + {children} ; actions: Array; + isResponseGenerating?: boolean; + sheetRef?: React.RefObject; } export function MessageActionsSheetWrapper({ sheetRef, actions, + isResponseGenerating, children, }: PropsWithChildren): ReactElement { const actionsSheetRef = useRef(null); const { animatedStyle, startAnimation, stopAnimation } = useAnimateMessage(); const handleLongPress = (): void => { + if (isResponseGenerating) return; + startAnimation(); actionsSheetRef.current?.present(); }; From ea1a8087fb657988c4cf5bb4ac026e38a4295a4b Mon Sep 17 00:00:00 2001 From: tfomkin Date: Sun, 15 Feb 2026 23:56:06 +0800 Subject: [PATCH 09/12] feat: export archived chat --- .../archived-chat-item/src/lib/component.tsx | 7 ++- .../component.tsx | 8 +-- .../src/lib/component.tsx | 2 +- .../src/lib/components/index.ts | 1 - .../download-chat-options-sheet/.babelrc | 12 ++++ .../download-chat-options-sheet/README.md | 7 +++ .../eslint.config.cjs | 12 ++++ .../download-chat-options-sheet/project.json | 9 +++ .../download-chat-options-sheet/src/index.ts | 1 + .../src/lib}/component.tsx | 35 +++--------- .../src/lib}/index.ts | 0 .../download-chat-options-sheet/tsconfig.json | 17 ++++++ .../tsconfig.lib.json | 19 +++++++ .../shared/features/download-chat/.babelrc | 12 ++++ .../shared/features/download-chat/README.md | 7 +++ .../features/download-chat/eslint.config.cjs | 12 ++++ .../features/download-chat/project.json | 9 +++ .../download-chat/src/download-chat.ts | 55 +++++++++++++++++++ .../features/download-chat/src/index.ts | 1 + .../features/download-chat/tsconfig.json | 17 ++++++ .../features/download-chat/tsconfig.lib.json | 19 +++++++ .../data-access/api/src/lib/chats/api.ts | 41 +++++++------- tsconfig.base.json | 6 ++ 23 files changed, 254 insertions(+), 55 deletions(-) delete mode 100644 libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/index.ts create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/.babelrc create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/README.md create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/eslint.config.cjs create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/project.json create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/src/index.ts rename libs/mobile/shared/features/{chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet => download-chat-options-sheet/src/lib}/component.tsx (50%) rename libs/mobile/shared/features/{chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet => download-chat-options-sheet/src/lib}/index.ts (100%) create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/tsconfig.json create mode 100644 libs/mobile/shared/features/download-chat-options-sheet/tsconfig.lib.json create mode 100644 libs/mobile/shared/features/download-chat/.babelrc create mode 100644 libs/mobile/shared/features/download-chat/README.md create mode 100644 libs/mobile/shared/features/download-chat/eslint.config.cjs create mode 100644 libs/mobile/shared/features/download-chat/project.json create mode 100644 libs/mobile/shared/features/download-chat/src/download-chat.ts create mode 100644 libs/mobile/shared/features/download-chat/src/index.ts create mode 100644 libs/mobile/shared/features/download-chat/tsconfig.json create mode 100644 libs/mobile/shared/features/download-chat/tsconfig.lib.json diff --git a/libs/mobile/chat/features/archived-chat-item/src/lib/component.tsx b/libs/mobile/chat/features/archived-chat-item/src/lib/component.tsx index ba07e95..ae427d5 100644 --- a/libs/mobile/chat/features/archived-chat-item/src/lib/component.tsx +++ b/libs/mobile/chat/features/archived-chat-item/src/lib/component.tsx @@ -2,13 +2,13 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { compact } from 'lodash-es'; import { Fragment, ReactElement, useRef } from 'react'; +import { DownloadChatOptionsSheet } from '@open-webui-react-native/mobile/shared/features/download-chat-options-sheet'; import { ChatListRow, ChatListRowProps } from '@open-webui-react-native/mobile/shared/ui/chat-list-row'; import { ActionsBottomSheet, ActionSheetItemProps } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { chatApi, ChatListItem } from '@open-webui-react-native/shared/data-access/api'; import { withOfflineGuard } from '@open-webui-react-native/shared/features/network'; import { alertService } from '@open-webui-react-native/shared/utils/alert-service'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; -import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; interface ArchivedChatItemProps extends Partial { onItemPress: (id: string) => void; @@ -19,6 +19,7 @@ export function ArchivedChatItem({ item, onItemPress, ...restProps }: ArchivedCh const translate = useTranslation('CHAT.ARCHIVED_CHATS_LIST.CHAT_ITEM'); const actionsSheetRef = useRef(null); + const downloadOptionsModalRef = useRef(null); const { mutateAsync: deleteChat, isPending: isDeleting } = chatApi.useDelete(); const { mutateAsync: unarchiveChat, isPending: isUnarchiving } = chatApi.useUnarchiveChat(); @@ -46,7 +47,8 @@ export function ArchivedChatItem({ item, onItemPress, ...restProps }: ArchivedCh await unarchiveChat(item.id); actionsSheetRef.current?.close(); }; - const handleExportChatPress = (): void => ToastService.showFeatureNotImplemented(); + + const handleExportChatPress = (): void => downloadOptionsModalRef.current?.present(); const actions: Array = compact([ { @@ -80,6 +82,7 @@ export function ArchivedChatItem({ item, onItemPress, ...restProps }: ArchivedCh {...restProps} /> + ); } diff --git a/libs/mobile/chat/features/archived-chats-list/src/lib/components/archived-chats-actions-sheet/component.tsx b/libs/mobile/chat/features/archived-chats-list/src/lib/components/archived-chats-actions-sheet/component.tsx index cd86ed6..48de04e 100644 --- a/libs/mobile/chat/features/archived-chats-list/src/lib/components/archived-chats-actions-sheet/component.tsx +++ b/libs/mobile/chat/features/archived-chats-list/src/lib/components/archived-chats-actions-sheet/component.tsx @@ -19,7 +19,7 @@ interface ArchivedChatsActionsSheetProps { export function ArchivedChatsActionsSheet({ renderTrigger }: ArchivedChatsActionsSheetProps): ReactElement { const translate = useTranslation('CHAT.ARCHIVED_CHATS_LIST.ARCHIVED_CHATS_ACTIONS_SHEET'); - const actionsHSeetRef = useRef(null); + const actionsSheetRef = useRef(null); const { unarchiveAllChats, isUnarchiving: isUnarchivingAllChats } = useUnarchiveChats(); @@ -35,12 +35,12 @@ export function ArchivedChatsActionsSheet({ renderTrigger }: ArchivedChatsAction const handleConfirmUnarchiveAll = async (): Promise => { await unarchiveAllChats(); - actionsHSeetRef.current?.close(); + actionsSheetRef.current?.close(); }; const handleExportArchivedChats = async (): Promise => { await exportArchivedChats(); - actionsHSeetRef.current?.close(); + actionsSheetRef.current?.close(); }; const actions: Array = compact([ @@ -61,5 +61,5 @@ export function ArchivedChatsActionsSheet({ renderTrigger }: ArchivedChatsAction return ; + ref={actionsSheetRef} />; } diff --git a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/component.tsx b/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/component.tsx index 050bdcb..7394148 100644 --- a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/component.tsx +++ b/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/component.tsx @@ -11,6 +11,7 @@ import { UpsertFolderSheet, UpsertFolderSheetMethods, } from '@open-webui-react-native/mobile/folder/features/upsert-folder-sheet'; +import { DownloadChatOptionsSheet } from '@open-webui-react-native/mobile/shared/features/download-chat-options-sheet'; import { ActionButtonsModal, ActionButtonsModalMethods, @@ -31,7 +32,6 @@ import { import { withOfflineGuard } from '@open-webui-react-native/shared/features/network'; import { alertService } from '@open-webui-react-native/shared/utils/alert-service'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; -import { DownloadChatOptionsSheet } from './components'; import { ChatAction } from './enums'; export type ChatActionsMenuSheetMethods = { diff --git a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/index.ts b/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/index.ts deleted file mode 100644 index 5cde4b2..0000000 --- a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './download-chat-options-sheet'; diff --git a/libs/mobile/shared/features/download-chat-options-sheet/.babelrc b/libs/mobile/shared/features/download-chat-options-sheet/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/mobile/shared/features/download-chat-options-sheet/README.md b/libs/mobile/shared/features/download-chat-options-sheet/README.md new file mode 100644 index 0000000..4b8f0e5 --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/README.md @@ -0,0 +1,7 @@ +# mobile/shared/features/download-chat-options-sheet + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test mobile/shared/features/download-chat-options-sheet` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/mobile/shared/features/download-chat-options-sheet/eslint.config.cjs b/libs/mobile/shared/features/download-chat-options-sheet/eslint.config.cjs new file mode 100644 index 0000000..6f47c1a --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/eslint.config.cjs @@ -0,0 +1,12 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/mobile/shared/features/download-chat-options-sheet/project.json b/libs/mobile/shared/features/download-chat-options-sheet/project.json new file mode 100644 index 0000000..266ea3c --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/project.json @@ -0,0 +1,9 @@ +{ + "name": "mobile/shared/features/download-chat-options-sheet", + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/mobile/shared/features/download-chat-options-sheet/src", + "projectType": "library", + "tags": ["app:mobile", "scope:shared", "type:features"], + "// targets": "to see all targets run: nx show project mobile/shared/features/download-chat-options-sheet --web", + "targets": {} +} diff --git a/libs/mobile/shared/features/download-chat-options-sheet/src/index.ts b/libs/mobile/shared/features/download-chat-options-sheet/src/index.ts new file mode 100644 index 0000000..f41a696 --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/src/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet/component.tsx b/libs/mobile/shared/features/download-chat-options-sheet/src/lib/component.tsx similarity index 50% rename from libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet/component.tsx rename to libs/mobile/shared/features/download-chat-options-sheet/src/lib/component.tsx index 295cd1b..fbd5146 100644 --- a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet/component.tsx +++ b/libs/mobile/shared/features/download-chat-options-sheet/src/lib/component.tsx @@ -1,14 +1,9 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { ReactElement, useState } from 'react'; -import { - FileExtension, - fileSystemService, -} from '@open-webui-react-native/mobile/shared/data-access/file-system-service'; +import { FileExtension } from '@open-webui-react-native/mobile/shared/data-access/file-system-service'; +import { createChatDownloadHandlers } from '@open-webui-react-native/mobile/shared/features/download-chat'; import { ActionsBottomSheet, ActionSheetItemProps } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { chatQueriesKeys, ChatResponse } from '@open-webui-react-native/shared/data-access/api'; -import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; -import { getChatAsText } from '@open-webui-react-native/shared/features/get-chat-as-text'; export interface DownloadChatOptionsSheetProps { chatId: string; @@ -22,37 +17,23 @@ export function DownloadChatOptionsSheet({ chatId, ref }: DownloadChatOptionsShe FileExtension.JSON | FileExtension.TXT | FileExtension.PDF | null >(null); - const getChat = async (): Promise => - await queryClient.fetchQuery({ queryKey: chatQueriesKeys.get(chatId).queryKey }); - - const onDownloadText = async (): Promise => { - setFileTypeLoading(FileExtension.TXT); - const { chat } = await getChat(); - const text = getChatAsText(chat); - await fileSystemService.shareTextFile(`chat-${chat.title}`, text); - setFileTypeLoading(null); - }; - - const onDownloadJson = async (): Promise => { - setFileTypeLoading(FileExtension.JSON); - const { chat } = await getChat(); - const jsonData = JSON.stringify([chat], null, 2); - await fileSystemService.shareJsonFile(`chat-export-${Date.now()}`, jsonData); - setFileTypeLoading(null); - }; + const { downloadJson, downloadText } = createChatDownloadHandlers({ + chatId, + setFileTypeLoading, + }); const actions: Array = [ { title: translate('TEXT_EXPORT_CHAT_JSON'), iconName: 'jsonFile', - onPress: onDownloadJson, + onPress: downloadJson, disabled: !!fileTypeLoading, isLoading: fileTypeLoading === FileExtension.JSON, }, { title: translate('TEXT_PLAIN_TEXT'), iconName: 'txtFile', - onPress: onDownloadText, + onPress: downloadText, disabled: !!fileTypeLoading, isLoading: fileTypeLoading === FileExtension.TXT, }, diff --git a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet/index.ts b/libs/mobile/shared/features/download-chat-options-sheet/src/lib/index.ts similarity index 100% rename from libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/components/download-chat-options-sheet/index.ts rename to libs/mobile/shared/features/download-chat-options-sheet/src/lib/index.ts diff --git a/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.json b/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.json new file mode 100644 index 0000000..ec74bfc --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../../tsconfig.base.json" +} diff --git a/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.lib.json b/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.lib.json new file mode 100644 index 0000000..27f2b91 --- /dev/null +++ b/libs/mobile/shared/features/download-chat-options-sheet/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/mobile/shared/features/download-chat/.babelrc b/libs/mobile/shared/features/download-chat/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/mobile/shared/features/download-chat/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/mobile/shared/features/download-chat/README.md b/libs/mobile/shared/features/download-chat/README.md new file mode 100644 index 0000000..8dee320 --- /dev/null +++ b/libs/mobile/shared/features/download-chat/README.md @@ -0,0 +1,7 @@ +# mobile/shared/features/download-chat + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test mobile/shared/features/download-chat` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/mobile/shared/features/download-chat/eslint.config.cjs b/libs/mobile/shared/features/download-chat/eslint.config.cjs new file mode 100644 index 0000000..6f47c1a --- /dev/null +++ b/libs/mobile/shared/features/download-chat/eslint.config.cjs @@ -0,0 +1,12 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/mobile/shared/features/download-chat/project.json b/libs/mobile/shared/features/download-chat/project.json new file mode 100644 index 0000000..f60bd21 --- /dev/null +++ b/libs/mobile/shared/features/download-chat/project.json @@ -0,0 +1,9 @@ +{ + "name": "mobile/shared/features/download-chat", + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/mobile/shared/features/download-chat/src", + "projectType": "library", + "tags": ["app:mobile", "scope:shared", "type:features"], + "// targets": "to see all targets run: nx show project mobile/shared/features/download-chat --web", + "targets": {} +} diff --git a/libs/mobile/shared/features/download-chat/src/download-chat.ts b/libs/mobile/shared/features/download-chat/src/download-chat.ts new file mode 100644 index 0000000..4393a73 --- /dev/null +++ b/libs/mobile/shared/features/download-chat/src/download-chat.ts @@ -0,0 +1,55 @@ +import { + FileExtension, + fileSystemService, +} from '@open-webui-react-native/mobile/shared/data-access/file-system-service'; +import { ChatResponse, getChatQueryOptions } from '@open-webui-react-native/shared/data-access/api'; +import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; +import { getChatAsText } from '@open-webui-react-native/shared/features/get-chat-as-text'; + +export interface CreateChatDownloadHandlersParams { + chatId: string; + setFileTypeLoading: (type: FileExtension | null) => void; +} + +export interface ChatDownloadHandlers { + downloadJson: () => Promise; + downloadText: () => Promise; +} + +export const createChatDownloadHandlers = ({ + chatId, + setFileTypeLoading, +}: CreateChatDownloadHandlersParams): ChatDownloadHandlers => { + const getChat = async (): Promise => queryClient.ensureQueryData(getChatQueryOptions(chatId)); + + const downloadJson = async (): Promise => { + try { + setFileTypeLoading(FileExtension.JSON); + + const { chat } = await getChat(); + const jsonData = JSON.stringify([chat], null, 2); + + await fileSystemService.shareJsonFile(`chat-export-${Date.now()}`, jsonData); + } finally { + setFileTypeLoading(null); + } + }; + + const downloadText = async (): Promise => { + try { + setFileTypeLoading(FileExtension.TXT); + + const { chat } = await getChat(); + const text = getChatAsText(chat); + + await fileSystemService.shareTextFile(`chat-${chat.title}`, text); + } finally { + setFileTypeLoading(null); + } + }; + + return { + downloadJson, + downloadText, + }; +}; diff --git a/libs/mobile/shared/features/download-chat/src/index.ts b/libs/mobile/shared/features/download-chat/src/index.ts new file mode 100644 index 0000000..00930d3 --- /dev/null +++ b/libs/mobile/shared/features/download-chat/src/index.ts @@ -0,0 +1 @@ +export * from './download-chat'; diff --git a/libs/mobile/shared/features/download-chat/tsconfig.json b/libs/mobile/shared/features/download-chat/tsconfig.json new file mode 100644 index 0000000..ec74bfc --- /dev/null +++ b/libs/mobile/shared/features/download-chat/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../../tsconfig.base.json" +} diff --git a/libs/mobile/shared/features/download-chat/tsconfig.lib.json b/libs/mobile/shared/features/download-chat/tsconfig.lib.json new file mode 100644 index 0000000..27f2b91 --- /dev/null +++ b/libs/mobile/shared/features/download-chat/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/shared/data-access/api/src/lib/chats/api.ts b/libs/shared/data-access/api/src/lib/chats/api.ts index 7215c3c..d008f0b 100644 --- a/libs/shared/data-access/api/src/lib/chats/api.ts +++ b/libs/shared/data-access/api/src/lib/chats/api.ts @@ -91,31 +91,32 @@ function useSearchInfinite(text: string): UseInfiniteQueryResult>, 'queryKey' | 'queryFn'>, -): UseQueryResult> { - const queryKey = chatQueriesKeys.get(id).queryKey; - - const result = useQuery({ - queryKey, - queryFn: async () => { - const result = await chatService.get(id); - const messages = result.chat.history.messages; - - for (const message of Object.values(messages)) { - if (message.role === Role.ASSISTANT) { - message.done = true; - } +) => ({ + queryKey: chatQueriesKeys.get(id).queryKey, + queryFn: async (): Promise => { + const result = await chatService.get(id); + const messages = result.chat.history.messages; + + for (const message of Object.values(messages)) { + if (message.role === Role.ASSISTANT) { + message.done = true; } + } - return result; - }, - staleTime: 5000, //NOTE Needs to avoid simultaneous requests for the same chat - ...options, - }); + return result; + }, + staleTime: 5000, + ...options, +}); - return result; +function useGet( + id: string, + options?: Omit>, 'queryKey' | 'queryFn'>, +): UseQueryResult> { + return useQuery(getChatQueryOptions(id, options)); } export function useUpdate( diff --git a/tsconfig.base.json b/tsconfig.base.json index 5fe8ce7..5f7aff2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -168,6 +168,12 @@ "@open-webui-react-native/mobile/shared/features/chat-actions-menu-sheet": [ "libs/mobile/shared/features/chat-actions-menu-sheet/src/index.ts" ], + "@open-webui-react-native/mobile/shared/features/download-chat": [ + "libs/mobile/shared/features/download-chat/src/index.ts" + ], + "@open-webui-react-native/mobile/shared/features/download-chat-options-sheet": [ + "libs/mobile/shared/features/download-chat-options-sheet/src/index.ts" + ], "@open-webui-react-native/mobile/shared/features/image-preview-modal": [ "libs/mobile/shared/features/image-preview-modal/src/index.ts" ], From e91293dd91182f3881e9c18bacfcca475b9b19f7 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Thu, 19 Feb 2026 17:59:20 +0800 Subject: [PATCH 10/12] fix: improved auto scroll behavior while response is generating --- .../components/messages-list/component.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx index d11f70c..88ddbb4 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx @@ -2,7 +2,7 @@ import { FlashList } from '@shopify/flash-list'; import { useLocalSearchParams } from 'expo-router'; import { delay } from 'lodash-es'; import React, { ReactElement, useCallback, useRef } from 'react'; -import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; +import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent } 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'; @@ -62,7 +62,8 @@ export default function ChatMessagesList({ const isScrollToBottomAvailableTimeout = useRef(null); //NOTE: number needs to fix pipeline lint error const isScrollToBottomVisible = useSharedValue(0); const previousScrollY = useRef(0); - const isNearBottomRef = useRef(true); + const shouldAutoscrollToBottomRef = useRef(true); + const previousTouchY = useRef(0); const { showPreviousSibling, showNextSibling, getSiblingsInfo } = useManageMessageSiblings(chatId, history); const { mutate: completeChat } = chatApi.useCompleteChat(); @@ -80,7 +81,7 @@ export default function ChatMessagesList({ isScrollToBottomAvailable.current = true; }, 500); - if (isNearBottomRef.current && listRef.current && messages?.length > 0) { + if (shouldAutoscrollToBottomRef.current) { requestAnimationFrame(() => { listRef.current?.scrollToEnd({ animated: true }); }); @@ -114,7 +115,6 @@ export default function ChatMessagesList({ //NOTE: The indent of 100 is needed to display the button not immediately when we start scrolling, //but when a small distance has been scrolled. const isNearBottom = scrollY + containerHeight >= contentHeight - 100; - isNearBottomRef.current = isNearBottom; if (isNearBottom || isScrollingUp) { animateScrollToBottom(0); @@ -127,7 +127,6 @@ export default function ChatMessagesList({ //NOTE: Needs to hide scroll to bottom button to avoid its jumping while scrolling to bottom animateScrollToBottom(0); isScrollToBottomAvailable.current = false; - isNearBottomRef.current = true; delay(() => { isScrollToBottomAvailable.current = true; @@ -176,6 +175,23 @@ export default function ChatMessagesList({ onFollowUpPress(text); }; + const handleTouchStart = (e: GestureResponderEvent): void => { + if (!isResponseGenerating) return; + + shouldAutoscrollToBottomRef.current = false; + previousTouchY.current = e.nativeEvent.pageY; + }; + + const handleTouchMove = (e: GestureResponderEvent): void => { + if (!isResponseGenerating) return; + + const { pageY } = e.nativeEvent; + const deltaY = pageY - previousTouchY.current; + + previousTouchY.current = pageY; + shouldAutoscrollToBottomRef.current = deltaY < 0; + }; + const renderItem = useCallback( ({ item, index }: { item: Message; index: number }) => { const message = history?.messages[item.id]; @@ -253,6 +269,8 @@ export default function ChatMessagesList({ }} onContentSizeChange={handleContentSizeChange} onScroll={handleScroll} + onTouchStart={handleTouchStart} + onTouchMove={handleTouchMove} scrollEventThrottle={16} /> From 6bfc86c27f8226222bcd2d357bf2c64b85b6b977 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Fri, 20 Feb 2026 21:06:50 +0800 Subject: [PATCH 11/12] fix: show version controls for ai message only if it has a parentId --- .../src/lib/components/ai-message/component.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/libs/mobile/chat/features/chat/src/lib/components/ai-message/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/ai-message/component.tsx index 974e252..a4e8540 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/ai-message/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/ai-message/component.tsx @@ -118,12 +118,14 @@ export function ChatAiMessage({ visible={isPreviewVisible} onClosePress={handleCloseImagePress} /> - + {message.parentId && ( + + )} ) : ( From 384fd1983f1b036cc8d2e9025423e80437cb3a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Fri, 3 Apr 2026 18:53:51 +0600 Subject: [PATCH 12/12] chore: release 1.3.0 --- apps/mobile/app.config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 1839f1b..3215f94 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -27,10 +27,10 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } slug: process.env.EXPO_PUBLIC_APP_SLUG as string, scheme: process.env.EXPO_PUBLIC_APP_SCHEME as string, owner: process.env.EXPO_PUBLIC_APP_OWNER as string, - version: '1.2.0', + version: '1.3.0', orientation: 'portrait', icon: './assets/icon.png', - runtimeVersion: '1.2.0', + runtimeVersion: '1.3.0', experiments: { reactCompiler: true, }, @@ -42,7 +42,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } supportsTablet: false, buildNumber: appEnv.select({ default: '18', - production: '8', + production: '10', }), config: { usesNonExemptEncryption: false, @@ -52,7 +52,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } package: appId, versionCode: appEnv.select({ default: 15, - production: 8, + production: 10, }), adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png',