diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index dcf1903..e78cd76 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -30,11 +30,11 @@ 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.5.0', + version: '1.6.4', userInterfaceStyle: 'automatic', orientation: 'portrait', icon: './assets/icon.png', - runtimeVersion: '1.5.0', + runtimeVersion: '1.6.4', experiments: { reactCompiler: true, }, @@ -46,7 +46,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } supportsTablet: false, buildNumber: appEnv.select({ default: '18', - production: '24', + production: '29', }), config: { usesNonExemptEncryption: false, @@ -56,7 +56,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } package: appId, versionCode: appEnv.select({ default: 15, - production: 24, + production: 29, }), adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png', diff --git a/i18n/mobile/shared/en.json b/i18n/mobile/shared/en.json index 78de59a..8221ea9 100644 --- a/i18n/mobile/shared/en.json +++ b/i18n/mobile/shared/en.json @@ -77,6 +77,7 @@ "TEXT_RENAME": "Rename", "TEXT_CLONE": "Clone", "TEXT_ARCHIVE": "Archive", + "TEXT_RESTORE": "Restore", "TEXT_SHARE": "Share", "TEXT_DOWNLOAD": "Download", "TEXT_DELETE": "Delete", diff --git a/libs/mobile/auth/features/email-sign-in-form/src/lib/component.tsx b/libs/mobile/auth/features/email-sign-in-form/src/lib/component.tsx index 485a95d..cdb9521 100644 --- a/libs/mobile/auth/features/email-sign-in-form/src/lib/component.tsx +++ b/libs/mobile/auth/features/email-sign-in-form/src/lib/component.tsx @@ -16,10 +16,11 @@ import { EmailFormSchema } from './forms'; interface EmailSignInFormProps { onSuccess: () => void; + onApiUrlChange?: (url: string) => void; setOauthProviders: (providers: Array) => void; } -export function EmailSignInForm({ onSuccess, setOauthProviders }: EmailSignInFormProps): ReactElement { +export function EmailSignInForm({ onSuccess, onApiUrlChange, setOauthProviders }: EmailSignInFormProps): ReactElement { const translate = useTranslation('AUTH.SIGN_IN.EMAIL_FORM'); const emailRef = useRef(null); const passwordRef = useRef(null); @@ -59,6 +60,7 @@ export function EmailSignInForm({ onSuccess, setOauthProviders }: EmailSignInFor useEffect(() => { setValue('url', query, { shouldValidate: true }); + onApiUrlChange?.(query); setOauthProviders([]); }, [query]); diff --git a/libs/mobile/auth/features/google-sign-in-form/src/lib/components/oauth-web-view-modal/component.tsx b/libs/mobile/auth/features/google-sign-in-form/src/lib/components/oauth-web-view-modal/component.tsx index b8317b8..a9134a2 100644 --- a/libs/mobile/auth/features/google-sign-in-form/src/lib/components/oauth-web-view-modal/component.tsx +++ b/libs/mobile/auth/features/google-sign-in-form/src/lib/components/oauth-web-view-modal/component.tsx @@ -79,7 +79,7 @@ export function OauthWebViewModal({ isVisible, onClose, onGetToken }: OauthWebVi - {translate('BUTTON_CLOSE')} + {translate('BUTTON_CLOSE')} diff --git a/libs/mobile/auth/features/sign-in/src/lib/component.tsx b/libs/mobile/auth/features/sign-in/src/lib/component.tsx index 4a7d946..88d132d 100644 --- a/libs/mobile/auth/features/sign-in/src/lib/component.tsx +++ b/libs/mobile/auth/features/sign-in/src/lib/component.tsx @@ -4,6 +4,7 @@ import { EmailSignInForm } from '@open-webui-react-native/mobile/auth/features/e import { GoogleSignInForm } from '@open-webui-react-native/mobile/auth/features/google-sign-in-form'; import { AppText, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Provider } from '@open-webui-react-native/shared/data-access/api'; +import { isTestApiUrl } from '@open-webui-react-native/shared/utils/config'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; export interface SignInProps { @@ -13,9 +14,10 @@ export interface SignInProps { export function SignIn(props: SignInProps): ReactElement { const { onSuccess } = props; const translate = useTranslation('AUTH.SIGN_IN'); + const [apiUrlInput, setApiUrlInput] = useState(); const [providers, setProviders] = useState>([]); - const showGoogleSignIn = providers.includes(Provider.GOOGLE); + const showGoogleSignIn = !isTestApiUrl(apiUrlInput) && providers.includes(Provider.GOOGLE); const handleSuccess = (): void => { onSuccess(); @@ -29,7 +31,11 @@ export function SignIn(props: SignInProps): ReactElement { {translate('TEXT_TITLE_EXTERNAL')} - + setApiUrlInput(url)} + setOauthProviders={setProviders} + /> {showGoogleSignIn && ( diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index 1c885f5..d4adfc9 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -44,6 +44,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const [isMessagesListLoaded, setIsMessagesListLoaded] = useState(false); const [isChatVisible, setIsChatVisible] = useState(false); const [activeInputMode, setActiveInputMode] = useState(null); + const [inputRerenderKey, setInputRerenderKey] = useState(0); const { attachedFiles, @@ -164,6 +165,8 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); reset(); resetAttachments(); + // NOTE: Forces input rerender to reset it to its initial height after submit + setInputRerenderKey((key) => key + 1); })(); const handleFollowUpPress = (text: string): void => { @@ -257,6 +260,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C onDeleteImagePress={handleDeleteImage} modelId={selectedModelId} isResponseGenerating={isResponseGenerating} + inputRerenderKey={inputRerenderKey} chat={chat} /> )} diff --git a/libs/mobile/chat/features/chat/src/lib/components/chat-bottom-button/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/chat-bottom-button/component.tsx index f56fcf3..076faa4 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/chat-bottom-button/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/chat-bottom-button/component.tsx @@ -14,7 +14,7 @@ export default function ChatBottomButton({ isVisible, onPress }: ChatBottomButto const animatedStyle = useAnimatedStyle(() => ({ transform: [ - { translateY: keyboardHeight.value ? keyboardHeight.value + 15 : 0 }, + { translateY: keyboardHeight.value ? keyboardHeight.value + 4 : -15 }, { scale: interpolate(isVisible.value, [0, 1], [0, 1]) }, ], })); 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 deba083..da3b8a6 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 @@ -4,6 +4,7 @@ import { delay } from 'lodash-es'; import React, { ReactElement, useCallback, useRef } from 'react'; import { GestureResponderEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; 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'; @@ -65,6 +66,8 @@ export default function ChatMessagesList({ const shouldAutoscrollToBottomRef = useRef(true); const previousTouchY = useRef(0); + const { bottom } = useSafeAreaInsets(); + const { showPreviousSibling, showNextSibling, getSiblingsInfo } = useManageMessageSiblings(chatId, history); const { mutate: completeChat } = chatApi.useCompleteChat(); const { id }: ChatScreenParams = useLocalSearchParams(); @@ -259,7 +262,8 @@ export default function ChatMessagesList({ ref={listRef} - contentContainerClassName='pb-[135] px-16' + contentContainerClassName='px-16' + contentContainerStyle={{ paddingBottom: bottom + 120 }} showsVerticalScrollIndicator={false} drawDistance={1500} //NOTE: Needs to avoid image jumping (while rerendering) when scrolling keyExtractor={(item) => item.id} diff --git a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx index 90933b5..6223b29 100644 --- a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx +++ b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx @@ -43,6 +43,7 @@ interface FormChatInputProps extends AppInputProps { isLoading?: boolean; isSuggestionShown?: boolean; isResponseGenerating?: boolean; + inputRerenderKey?: number; } export interface FormChatInputSchema { @@ -65,6 +66,7 @@ export function FormChatInput({ isLoading, isSuggestionShown, isResponseGenerating, + inputRerenderKey, ...restProps }: FormChatInputProps): ReactElement { const translate = useTranslation('CHAT.FORM_CHAT_INPUT'); @@ -150,6 +152,7 @@ export function FormChatInput({ /> ) : ( (null); @@ -94,6 +95,8 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat const chatTitle = activeChat?.title ?? ''; const { data: chatFullData } = chatApi.useGet(chatId, { enabled: !!chatId }); + const isArchived = chatFullData?.archived; + const openCreateFolderModal = (): void => { upsertFolderSheetRef.current?.present(); }; @@ -126,6 +129,9 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat case ChatAction.ARCHIVE: await archiveChatHandler(); break; + case ChatAction.RESTORE: + await restoreChatHandler(); + break; case ChatAction.DELETE: await onDeleteConfirm(); break; @@ -215,6 +221,11 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat closeActionsModal(); }; + const restoreChatHandler = async (): Promise => { + await unarchiveChat(chatId); + closeActionsModal(); + }; + const onFolderSelectedHandler = async (id: string | null): Promise => { await updateChatFolder({ id: chatId, @@ -244,10 +255,10 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat onPress: () => handleAction(ChatAction.CLONE), }, isFeatureEnabled(FeatureID.ARCHIVE_CHAT) && { - title: translate('TEXT_ARCHIVE'), - iconName: 'archive', - isLoading: isArchiving, - onPress: () => handleAction(ChatAction.ARCHIVE), + title: isArchived ? translate('TEXT_RESTORE') : translate('TEXT_ARCHIVE'), + iconName: isArchived ? 'unarchive' : 'archive', + isLoading: isArchived ? isUnarchiving : isArchiving, + onPress: () => handleAction(isArchived ? ChatAction.RESTORE : ChatAction.ARCHIVE), }, { title: translate('TEXT_SHARE'), iconName: 'exportIcon', onPress: openShareChatModal }, { title: translate('TEXT_DOWNLOAD'), iconName: 'download', onPress: openDownloadOptionsModal }, diff --git a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/enums/chat-action.ts b/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/enums/chat-action.ts index d4743d5..cfda260 100644 --- a/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/enums/chat-action.ts +++ b/libs/mobile/shared/features/chat-actions-menu-sheet/src/lib/enums/chat-action.ts @@ -4,6 +4,7 @@ export enum ChatAction { RENAME = 'rename', CLONE = 'clone', ARCHIVE = 'archive', + RESTORE = 'restore', DELETE = 'delete', CLOSE = 'close', } diff --git a/libs/shared/data-access/api/src/lib/chats/utils/get-share-chat-link.ts b/libs/shared/data-access/api/src/lib/chats/utils/get-share-chat-link.ts index 0168c49..f53d196 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/get-share-chat-link.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/get-share-chat-link.ts @@ -1,5 +1,5 @@ -import { getApiUrl } from '@open-webui-react-native/shared/utils/config'; +import { getDisplayApiUrl } from '@open-webui-react-native/shared/utils/config'; export function getShareChatLink(shareId: string): string { - return `${getApiUrl()}/s/${shareId}`; + return `${getDisplayApiUrl()}/s/${shareId}`; } diff --git a/libs/shared/utils/config/src/get-api-url.ts b/libs/shared/utils/config/src/get-api-url.ts index 5c94212..b4be194 100644 --- a/libs/shared/utils/config/src/get-api-url.ts +++ b/libs/shared/utils/config/src/get-api-url.ts @@ -1,4 +1,4 @@ -// eslint-disable-next-line @nx/enforce-module-boundaries + import { appStorageService } from '@open-webui-react-native/shared/data-access/storage'; import { appEnv } from '@open-webui-react-native/shared/utils/app-env'; @@ -8,7 +8,7 @@ export const ronasApiUrl = appEnv.select({ production: 'https://ai.ronas.online', }); -const testApiUrl = 'https://ai.test-api.online'; +export const testApiUrl = 'https://ai.test-api.online'; const normalizeUrl = (url?: string): string => (url ?? '').trim().replace(/\/+$/, ''); @@ -16,11 +16,18 @@ export const resolveApiUrl = (url: string): string => { return normalizeUrl(url) === normalizeUrl(testApiUrl) ? normalizeUrl(ronasApiUrl) : normalizeUrl(url); }; -export const getApiUrl = (): string => { +export const isTestApiUrl = (url?: string): boolean => + normalizeUrl(url ?? appStorageService.apiUrl.get()) === normalizeUrl(testApiUrl); + +export const getDisplayApiUrl = (): string => { const stored = normalizeUrl(appStorageService.apiUrl.get()); const fallback = normalizeUrl(ronasApiUrl); - return resolveApiUrl(stored || fallback); + return stored || fallback; +}; + +export const getApiUrl = (): string => { + return resolveApiUrl(getDisplayApiUrl()); }; export const getHost = (url: string): string =>