diff --git a/apps/mobile/.env.template b/apps/mobile/.env.template index da8ec8db..6cd64588 100644 --- a/apps/mobile/.env.template +++ b/apps/mobile/.env.template @@ -1,6 +1,7 @@ EXPO_PUBLIC_PROJECT_ID= EXPO_PUBLIC_APP_ID= +EXPO_PUBLIC_IOS_APP_STORE_ID= EXPO_PUBLIC_APP_OWNER= EXPO_PUBLIC_APP_SCHEME= EXPO_PUBLIC_APP_NAME= @@ -9,3 +10,5 @@ EXPO_PUBLIC_IS_INTERNAL_RELEASE= SENTRY_DSN= AMPLITUDE_API_KEY_DEV= AMPLITUDE_API_KEY_PROD= +SUPABASE_URL= +SUPABASE_PUBLISHABLE_KEY= diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 703ab6ec..e268ee77 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -19,6 +19,10 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } apiKeyDev: process.env.AMPLITUDE_API_KEY_DEV, apiKeyProd: process.env.AMPLITUDE_API_KEY_PROD, }, + supabase: { + url: process.env.SUPABASE_URL, + publishableKey: process.env.SUPABASE_PUBLISHABLE_KEY, + }, env: appEnv.current, googleIosClientId: appEnv.select({ default: process.env.GOOGLE_IOS_CLIENT_ID_DEV, @@ -47,6 +51,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } }, ios: { bundleIdentifier: appId, + appStoreUrl: `https://apps.apple.com/app/id${process.env.EXPO_PUBLIC_IOS_APP_STORE_ID}`, supportsTablet: false, buildNumber: appEnv.select({ default: '18', @@ -66,6 +71,7 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } }, android: { package: appId, + playStoreUrl: `https://play.google.com/store/apps/details?id=${appId}`, versionCode: appEnv.select({ default: 15, production: 36, diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 23138d81..34bea50b 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -17,6 +17,7 @@ import { useNetworkConnection } from '@open-webui-react-native/shared/features/n import { analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { constants, LanguageCode } from '@open-webui-react-native/shared/utils/config'; import { setupReactotron } from '@open-webui-react-native/shared/utils/reactotron'; +import { supabaseService } from '@open-webui-react-native/shared/utils/supabase-service'; import { i18n, setLanguage } from '@ronas-it/react-native-common-modules/i18n'; import * as Sentry from '@sentry/react-native'; import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; @@ -32,6 +33,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import '../global.css'; import 'reflect-metadata'; import 'expo-dev-client'; +import 'react-native-url-polyfill/auto'; export { ErrorBoundary } from 'expo-router'; @@ -120,6 +122,7 @@ const useLanguage = setLanguage(translations, constants.defaultLocale); setupReactotron('open-web-ui'); analyticsService.init(); +supabaseService.init(); // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b76a1859..2a1c8ef3 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -42,11 +42,13 @@ "@ronas-it/rtkq-entity-api": "^1.1.1", "@sentry/react-native": "~7.11.0", "@shopify/flash-list": "2.0.2", + "@supabase/supabase-js": "^2.112.3", "@tanstack/react-query": "^5.80.6", "@tanstack/react-query-persist-client": "^5.87.4", "axios-observable": "^2.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "base64-arraybuffer": "^1.0.2", "class-transformer": "^0.5.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -74,6 +76,7 @@ "expo-speech": "~57.0.1", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", + "expo-store-review": "~57.0.1", "expo-updates": "~57.0.10", "html-entities": "^2.6.0", "i18n-js": "^4.5.1", @@ -110,6 +113,7 @@ "react-native-svg": "15.15.4", "react-native-svg-transformer": "~1.5.0", "react-native-toast-message": "^2.3.0", + "react-native-url-polyfill": "^4.0.0", "react-native-uuid": "^2.0.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.1", diff --git a/i18n/mobile/app/de.json b/i18n/mobile/app/de.json index de77e6bc..1daa690c 100644 --- a/i18n/mobile/app/de.json +++ b/i18n/mobile/app/de.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Avatar", "TEXT_CHANGE_PASSWORD": "Passwort ändern", "TEXT_LOGOUT": "Abmelden", - "TEXT_CONTACT_SUPPORT": "Support kontaktieren", + "TEXT_REPORT_A_BUG": "Fehler melden", "TEXT_RATE_APP": "App bewerten", "TEXT_ARCHIVED_CHATS": "Archivierte Chats", "TEXT_EXPORT_ALL_CHATS": "Alle Chats exportieren", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "Es gibt keine Modelle." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Support kontaktieren", - "TEXT_MESSAGE_PLACEHOLDER": "Ihre Nachricht", - "BUTTON_ATTACH": "Screenshots oder Video anhängen" + "TEXT_REPORT_BUG": "Fehler melden", + "TEXT_YOUR_MESSAGE": "Ihre Nachricht", + "TEXT_MESSAGE_SENT": "Vielen Dank für Ihr Feedback! Ihre Nachricht wurde erfolgreich gesendet", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Screenshots oder Video anhängen", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "Sie können maximal {{count}} Dateien anhängen" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Sprache" diff --git a/i18n/mobile/app/en.json b/i18n/mobile/app/en.json index c38e564a..35d7184d 100644 --- a/i18n/mobile/app/en.json +++ b/i18n/mobile/app/en.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Avatar", "TEXT_CHANGE_PASSWORD": "Change password", "TEXT_LOGOUT": "Logout", - "TEXT_CONTACT_SUPPORT": "Contact support", + "TEXT_REPORT_A_BUG": "Report a bug", "TEXT_RATE_APP": "Rate app", "TEXT_ARCHIVED_CHATS": "Archived chats", "TEXT_EXPORT_ALL_CHATS": "Export all chats", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "There are no models." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Contact support", - "TEXT_MESSAGE_PLACEHOLDER": "Your message", - "BUTTON_ATTACH": "Attach screenshots or video" + "TEXT_REPORT_BUG": "Report a bug", + "TEXT_YOUR_MESSAGE": "Your message", + "TEXT_MESSAGE_SENT": "Thank you for your feedback! Your message has been sent successfully", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Attach screenshots or video", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "You can attach up to {{count}} files" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Language" diff --git a/i18n/mobile/app/es.json b/i18n/mobile/app/es.json index 96ff975d..6aa28ef8 100644 --- a/i18n/mobile/app/es.json +++ b/i18n/mobile/app/es.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Avatar", "TEXT_CHANGE_PASSWORD": "Cambiar contraseña", "TEXT_LOGOUT": "Cerrar sesión", - "TEXT_CONTACT_SUPPORT": "Contactar con soporte", + "TEXT_REPORT_A_BUG": "Informar un error", "TEXT_RATE_APP": "Valorar la aplicación", "TEXT_ARCHIVED_CHATS": "Chats archivados", "TEXT_EXPORT_ALL_CHATS": "Exportar todos los chats", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "No hay modelos." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Contactar con soporte", - "TEXT_MESSAGE_PLACEHOLDER": "Tu mensaje", - "BUTTON_ATTACH": "Adjuntar capturas de pantalla o vídeo" + "TEXT_REPORT_BUG": "Informar un error", + "TEXT_YOUR_MESSAGE": "Tu mensaje", + "TEXT_MESSAGE_SENT": "¡Gracias por tus comentarios! Tu mensaje se ha enviado correctamente", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Adjuntar capturas de pantalla o video", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "Puedes adjuntar hasta {{count}} archivos" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Idioma" diff --git a/i18n/mobile/app/fr.json b/i18n/mobile/app/fr.json index 37a21a55..986fd728 100644 --- a/i18n/mobile/app/fr.json +++ b/i18n/mobile/app/fr.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Avatar", "TEXT_CHANGE_PASSWORD": "Modifier le mot de passe", "TEXT_LOGOUT": "Déconnexion", - "TEXT_CONTACT_SUPPORT": "Contacter le support", + "TEXT_REPORT_A_BUG": "Signaler un bug", "TEXT_RATE_APP": "Noter l'application", "TEXT_ARCHIVED_CHATS": "Discussions archivées", "TEXT_EXPORT_ALL_CHATS": "Exporter toutes les discussions", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "Il n'y a aucun modèle." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Contacter le support", - "TEXT_MESSAGE_PLACEHOLDER": "Votre message", - "BUTTON_ATTACH": "Joindre des captures d'écran ou une vidéo" + "TEXT_REPORT_BUG": "Signaler un bug", + "TEXT_YOUR_MESSAGE": "Votre message", + "TEXT_MESSAGE_SENT": "Merci pour votre retour ! Votre message a été envoyé avec succès", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Joindre des captures d'écran ou une vidéo", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "Vous pouvez joindre jusqu'à {{count}} fichiers" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Langue" diff --git a/i18n/mobile/app/ja.json b/i18n/mobile/app/ja.json index 9e9481ea..3b2850a3 100644 --- a/i18n/mobile/app/ja.json +++ b/i18n/mobile/app/ja.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "アバター", "TEXT_CHANGE_PASSWORD": "パスワードを変更", "TEXT_LOGOUT": "ログアウト", - "TEXT_CONTACT_SUPPORT": "サポートに問い合わせる", + "TEXT_REPORT_A_BUG": "不具合を報告", "TEXT_RATE_APP": "アプリを評価", "TEXT_ARCHIVED_CHATS": "アーカイブ済みチャット", "TEXT_EXPORT_ALL_CHATS": "すべてのチャットをエクスポート", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "モデルがありません。" }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "サポートに問い合わせる", - "TEXT_MESSAGE_PLACEHOLDER": "メッセージ", - "BUTTON_ATTACH": "スクリーンショットまたは動画を添付" + "TEXT_REPORT_BUG": "不具合を報告", + "TEXT_YOUR_MESSAGE": "メッセージ", + "TEXT_MESSAGE_SENT": "フィードバックをありがとうございます!メッセージが正常に送信されました", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "スクリーンショットまたは動画を添付", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "添付できるファイルは最大{{count}}件までです" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "言語" diff --git a/i18n/mobile/app/pt.json b/i18n/mobile/app/pt.json index 6e34940c..d0bd68ac 100644 --- a/i18n/mobile/app/pt.json +++ b/i18n/mobile/app/pt.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Avatar", "TEXT_CHANGE_PASSWORD": "Alterar senha", "TEXT_LOGOUT": "Sair", - "TEXT_CONTACT_SUPPORT": "Falar com o suporte", + "TEXT_REPORT_A_BUG": "Reportar um bug", "TEXT_RATE_APP": "Avaliar o aplicativo", "TEXT_ARCHIVED_CHATS": "Chats arquivados", "TEXT_EXPORT_ALL_CHATS": "Exportar todos os chats", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "Não há modelos." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Falar com o suporte", - "TEXT_MESSAGE_PLACEHOLDER": "Sua mensagem", - "BUTTON_ATTACH": "Anexar capturas de tela ou vídeo" + "TEXT_REPORT_BUG": "Reportar um bug", + "TEXT_YOUR_MESSAGE": "Sua mensagem", + "TEXT_MESSAGE_SENT": "Obrigado pelo seu feedback! Sua mensagem foi enviada com sucesso", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Anexar capturas de tela ou vídeo", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "Você pode anexar até {{count}} arquivos" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Idioma" diff --git a/i18n/mobile/app/ru.json b/i18n/mobile/app/ru.json index 71accf4a..ffd562f8 100644 --- a/i18n/mobile/app/ru.json +++ b/i18n/mobile/app/ru.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "Аватар", "TEXT_CHANGE_PASSWORD": "Изменить пароль", "TEXT_LOGOUT": "Выйти", - "TEXT_CONTACT_SUPPORT": "Связаться с поддержкой", + "TEXT_REPORT_A_BUG": "Сообщить об ошибке", "TEXT_RATE_APP": "Оценить приложение", "TEXT_ARCHIVED_CHATS": "Архивные чаты", "TEXT_EXPORT_ALL_CHATS": "Экспортировать все чаты", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "Моделей нет." }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "Связаться с поддержкой", - "TEXT_MESSAGE_PLACEHOLDER": "Ваше сообщение", - "BUTTON_ATTACH": "Прикрепить скриншоты или видео" + "TEXT_REPORT_BUG": "Сообщить об ошибке", + "TEXT_YOUR_MESSAGE": "Введите сообщение", + "TEXT_MESSAGE_SENT": "Спасибо за отзыв! Ваше сообщение успешно отправлено", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "Прикрепить скриншот или видео", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "Можно прикрепить не более {{count}} вложений" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "Язык" diff --git a/i18n/mobile/app/zh.json b/i18n/mobile/app/zh.json index 3d8b9137..a91a16aa 100644 --- a/i18n/mobile/app/zh.json +++ b/i18n/mobile/app/zh.json @@ -16,7 +16,7 @@ "TEXT_AVATAR": "头像", "TEXT_CHANGE_PASSWORD": "修改密码", "TEXT_LOGOUT": "退出登录", - "TEXT_CONTACT_SUPPORT": "联系客服", + "TEXT_REPORT_A_BUG": "报告错误", "TEXT_RATE_APP": "为应用评分", "TEXT_ARCHIVED_CHATS": "已归档聊天", "TEXT_EXPORT_ALL_CHATS": "导出所有聊天", @@ -55,9 +55,11 @@ "TEXT_NO_MODELS": "暂无模型。" }, "CONTACT_SUPPORT_SHEET": { - "TEXT_TITLE": "联系客服", - "TEXT_MESSAGE_PLACEHOLDER": "您的留言", - "BUTTON_ATTACH": "附加截图或视频" + "TEXT_REPORT_BUG": "报告错误", + "TEXT_YOUR_MESSAGE": "您的消息", + "TEXT_MESSAGE_SENT": "感谢您的反馈!您的消息已成功发送", + "BUTTON_ATTACH_SCREENSHOTS_OR_VIDEO": "添加截图或视频", + "TEXT_ATTACHMENTS_LIMIT_REACHED": "最多只能添加 {{count}} 个附件" }, "LANGUAGE_SHEET": { "TEXT_TITLE": "语言" diff --git a/i18n/mobile/shared/de.json b/i18n/mobile/shared/de.json index bdcb2e56..391108ad 100644 --- a/i18n/mobile/shared/de.json +++ b/i18n/mobile/shared/de.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Bitte geben Sie Ihr Passwort ein", "TEXT_INVALID_URL": "Bitte geben Sie eine gültige Server-URL ein, z. B. https://your.domain oder http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Bitte geben Sie einen Ordnernamen ein", + "TEXT_REQUIRED_MESSAGE": "Bitte geben Sie eine Nachricht ein", "TEXT_REQUIRED_PROFILE_NAME": "Bitte geben Sie einen Profilnamen ein", "TEXT_REQUIRED_CURRENT_PASSWORD": "Bitte geben Sie Ihr aktuelles Passwort ein", "TEXT_REQUIRED_NEW_PASSWORD": "Bitte geben Sie ein neues Passwort ein", diff --git a/i18n/mobile/shared/en.json b/i18n/mobile/shared/en.json index e4725cd0..5b0c93b7 100644 --- a/i18n/mobile/shared/en.json +++ b/i18n/mobile/shared/en.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Please enter your password", "TEXT_INVALID_URL": "Please enter a valid server URL, e.g. https://your.domain or http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Please enter a folder name", + "TEXT_REQUIRED_MESSAGE": "Please enter a message", "TEXT_REQUIRED_PROFILE_NAME": "Please enter a profile name", "TEXT_REQUIRED_CURRENT_PASSWORD": "Please enter your current password", "TEXT_REQUIRED_NEW_PASSWORD": "Please enter a new password", diff --git a/i18n/mobile/shared/es.json b/i18n/mobile/shared/es.json index 2a605daf..c559cfff 100644 --- a/i18n/mobile/shared/es.json +++ b/i18n/mobile/shared/es.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Introduce tu contraseña", "TEXT_INVALID_URL": "Introduce una URL de servidor válida, p. ej. https://your.domain o http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Introduce un nombre de carpeta", + "TEXT_REQUIRED_MESSAGE": "Introduce un mensaje", "TEXT_REQUIRED_PROFILE_NAME": "Introduce un nombre de perfil", "TEXT_REQUIRED_CURRENT_PASSWORD": "Introduce tu contraseña actual", "TEXT_REQUIRED_NEW_PASSWORD": "Introduce una nueva contraseña", diff --git a/i18n/mobile/shared/fr.json b/i18n/mobile/shared/fr.json index ef0bc68b..eccfb649 100644 --- a/i18n/mobile/shared/fr.json +++ b/i18n/mobile/shared/fr.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Veuillez saisir votre mot de passe", "TEXT_INVALID_URL": "Veuillez saisir une URL de serveur valide, ex. https://your.domain ou http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Veuillez saisir un nom de dossier", + "TEXT_REQUIRED_MESSAGE": "Veuillez saisir un message", "TEXT_REQUIRED_PROFILE_NAME": "Veuillez saisir un nom de profil", "TEXT_REQUIRED_CURRENT_PASSWORD": "Veuillez saisir votre mot de passe actuel", "TEXT_REQUIRED_NEW_PASSWORD": "Veuillez saisir un nouveau mot de passe", diff --git a/i18n/mobile/shared/ja.json b/i18n/mobile/shared/ja.json index 9b3bba4d..6868521c 100644 --- a/i18n/mobile/shared/ja.json +++ b/i18n/mobile/shared/ja.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "パスワードを入力してください", "TEXT_INVALID_URL": "有効なサーバー URL を入力してください(例: https://your.domain または http://192.168.1.10:3000)", "TEXT_REQUIRED_FOLDER_NAME": "フォルダ名を入力してください", + "TEXT_REQUIRED_MESSAGE": "メッセージを入力してください", "TEXT_REQUIRED_PROFILE_NAME": "プロフィール名を入力してください", "TEXT_REQUIRED_CURRENT_PASSWORD": "現在のパスワードを入力してください", "TEXT_REQUIRED_NEW_PASSWORD": "新しいパスワードを入力してください", diff --git a/i18n/mobile/shared/pt.json b/i18n/mobile/shared/pt.json index 50e0f63b..8f1979ab 100644 --- a/i18n/mobile/shared/pt.json +++ b/i18n/mobile/shared/pt.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Digite sua senha", "TEXT_INVALID_URL": "Digite uma URL de servidor válida, ex.: https://your.domain ou http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Digite um nome de pasta", + "TEXT_REQUIRED_MESSAGE": "Digite uma mensagem", "TEXT_REQUIRED_PROFILE_NAME": "Digite um nome de perfil", "TEXT_REQUIRED_CURRENT_PASSWORD": "Digite sua senha atual", "TEXT_REQUIRED_NEW_PASSWORD": "Digite uma nova senha", diff --git a/i18n/mobile/shared/ru.json b/i18n/mobile/shared/ru.json index caa587d8..f3111f6b 100644 --- a/i18n/mobile/shared/ru.json +++ b/i18n/mobile/shared/ru.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "Введите пароль", "TEXT_INVALID_URL": "Введите корректный URL сервера, напр. https://your.domain или http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Введите название папки", + "TEXT_REQUIRED_MESSAGE": "Введите сообщение", "TEXT_REQUIRED_PROFILE_NAME": "Введите имя профиля", "TEXT_REQUIRED_CURRENT_PASSWORD": "Введите текущий пароль", "TEXT_REQUIRED_NEW_PASSWORD": "Введите новый пароль", diff --git a/i18n/mobile/shared/zh.json b/i18n/mobile/shared/zh.json index 21e8ed16..7718991c 100644 --- a/i18n/mobile/shared/zh.json +++ b/i18n/mobile/shared/zh.json @@ -6,6 +6,7 @@ "TEXT_REQUIRED_PASSWORD": "请输入您的密码", "TEXT_INVALID_URL": "请输入有效的服务器 URL,例如 https://your.domain 或 http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "请输入文件夹名称", + "TEXT_REQUIRED_MESSAGE": "请输入消息内容", "TEXT_REQUIRED_PROFILE_NAME": "请输入个人资料名称", "TEXT_REQUIRED_CURRENT_PASSWORD": "请输入当前密码", "TEXT_REQUIRED_NEW_PASSWORD": "请输入新密码", diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index 432862e2..823cfbdf 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -2,7 +2,7 @@ import { useSelector } from '@legendapp/state/react'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import dayjs from 'dayjs'; import { delay } from 'lodash-es'; -import React, { Fragment, ReactElement, useEffect, useMemo, useState } from 'react'; +import React, { Fragment, ReactElement, useEffect, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { InteractionManager } from 'react-native'; import { EditMessageInput } from '@open-webui-react-native/mobile/chat/features/edit-message-input'; @@ -28,7 +28,7 @@ import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/share import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; import { useAppStateChange } from '@open-webui-react-native/shared/utils/use-app-state-change'; import { ActiveInputMode } from './enums'; -import { patchNewChat } from './utils'; +import { patchNewChat, requestStoreReview } from './utils'; const LazyChatMessagesList = React.lazy(() => import('./components/messages-list/component')); @@ -85,7 +85,11 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const history = chat?.chat.history; // NOTE: chat.messages is a legacy snapshot the backend no longer maintains, so the current branch is derived from history const messages = useMemo(() => (history ? createMessagesList(history, history.currentId) : []), [history]); - const isResponseGenerating = !history?.messages[history.currentId].done; + const currentMessage = history?.messages[history.currentId]; + const isResponseGenerating = !currentMessage?.done; + const isAssistantMessage = currentMessage?.role === Role.ASSISTANT; + + const firstMessageGeneratedRef = useRef(false); const shouldHideContent = isLoading || isRefetching || !isMessagesListLoaded || !selectedModelId; @@ -208,6 +212,13 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C } }, [isNewChat, isSuccess, chatId]); + useEffect(() => { + if (isNewChat && !firstMessageGeneratedRef.current && isAssistantMessage && currentMessage.done) { + firstMessageGeneratedRef.current = true; + requestStoreReview(chatId); + } + }, [isNewChat, chatId, currentMessage?.done, isAssistantMessage]); + return ( {shouldHideContent && ( diff --git a/libs/mobile/chat/features/chat/src/lib/utils/index.ts b/libs/mobile/chat/features/chat/src/lib/utils/index.ts index 047452f8..28df7ca1 100644 --- a/libs/mobile/chat/features/chat/src/lib/utils/index.ts +++ b/libs/mobile/chat/features/chat/src/lib/utils/index.ts @@ -1,3 +1,4 @@ export * from './get-siblings-ids'; export * from './patch-new-chat'; export * from './parse-response-message-content'; +export * from './request-store-review'; diff --git a/libs/mobile/chat/features/chat/src/lib/utils/request-store-review.ts b/libs/mobile/chat/features/chat/src/lib/utils/request-store-review.ts new file mode 100644 index 00000000..acefbe5a --- /dev/null +++ b/libs/mobile/chat/features/chat/src/lib/utils/request-store-review.ts @@ -0,0 +1,28 @@ +import { appStorageService } from '@open-webui-react-native/shared/data-access/storage'; +import { storeReviewService } from '@open-webui-react-native/shared/utils/store-review-service'; + +export const requestStoreReview = async (chatId: string): Promise => { + const createdChatsIdsString = appStorageService.createdChatsIds.get(); + const isRateAppReviewed = appStorageService.isRateAppReviewed.get(); + + if (isRateAppReviewed === 'true') { + return; + } + + const createdChatsIds = createdChatsIdsString ? createdChatsIdsString.split(',') : []; + + if (createdChatsIds.length >= 2) { + const isReviewRequested = await storeReviewService.requestStoreReview(); + + if (isReviewRequested) { + appStorageService.isRateAppReviewed.set('true'); + } + + return; + } + + if (!createdChatsIds.includes(chatId)) { + // If amount of created chats is less than 2, we just add the current chatId to the list of created chats + appStorageService.createdChatsIds.set(`${createdChatsIds.length ? createdChatsIds + ',' : ''}` + chatId); + } +}; diff --git a/libs/mobile/settings/features/contact-support-sheet/.babelrc b/libs/mobile/settings/features/contact-support-sheet/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/mobile/settings/features/contact-support-sheet/README.md b/libs/mobile/settings/features/contact-support-sheet/README.md new file mode 100644 index 00000000..5da6ab0d --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/README.md @@ -0,0 +1,7 @@ +# mobile-settings-features-contact-support-sheet + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test mobile-settings-features-contact-support-sheet` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/mobile/settings/features/contact-support-sheet/eslint.config.cjs b/libs/mobile/settings/features/contact-support-sheet/eslint.config.cjs new file mode 100644 index 00000000..6f47c1a7 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-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/settings/features/contact-support-sheet/project.json b/libs/mobile/settings/features/contact-support-sheet/project.json new file mode 100644 index 00000000..b306d752 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/project.json @@ -0,0 +1,9 @@ +{ + "name": "mobile-settings-features-contact-support-sheet", + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/mobile/settings/features/contact-support-sheet/src", + "projectType": "library", + "tags": ["app:mobile", "type:features", "scope:settings"], + "// targets": "to see all targets run: nx show project mobile-settings-features-contact-support-sheet --web", + "targets": {} +} diff --git a/libs/mobile/settings/features/contact-support-sheet/src/index.ts b/libs/mobile/settings/features/contact-support-sheet/src/index.ts new file mode 100644 index 00000000..f41a696f --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/component.tsx b/libs/mobile/settings/features/contact-support-sheet/src/lib/component.tsx new file mode 100644 index 00000000..cd52914f --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/component.tsx @@ -0,0 +1,167 @@ +import { BottomSheetModal } from '@gorhom/bottom-sheet'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useSelector } from '@legendapp/state/react'; +import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; +import Constants from 'expo-constants'; +import { ForwardedRef, ReactElement, useImperativeHandle, useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import { Platform, TextInput } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + imagePickerService, + ImagePickerSource, +} from '@open-webui-react-native/mobile/shared/data-access/image-picker-service'; +import { useAttachedFiles } from '@open-webui-react-native/mobile/shared/features/use-attached-files'; +import { + AppBottomSheet, + AppBottomSheetKeyboardAwareScrollView, + AppBottomSheetPropsType, + AppButton, + FormFloatedLabelInput, + SheetHeader, + View, +} from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { appConfigurationApi, authApi, supabaseApi } from '@open-webui-react-native/shared/data-access/api'; +import { ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; +import { AttachmentRow } from './components'; +import { contactSupportSheetConfig } from './config'; +import { ContactSupportFormSchema } from './forms'; + +export type ContactSupportSheetMethods = { + present: () => void; +}; + +export type ContactSupportSheetRef = ForwardedRef; + +export type ContactSupportSheetProps = Partial> & { + ref?: ContactSupportSheetRef; +}; + +export function ContactSupportSheet({ ref, ...props }: ContactSupportSheetProps): ReactElement { + const translate = useTranslation('APP.SETTINGS_SCREEN.CONTACT_SUPPORT_SHEET'); + const { bottom } = useSafeAreaInsets(); + + const sheetRef = useRef(null); + const inputRef = useRef(null); + + const { data: profile } = authApi.useGetProfile(); + const { data: configuration } = appConfigurationApi.useGetAppConfiguration(); + const { mutateAsync: submitFeedback, isPending } = supabaseApi.useSubmitFeedback(); + + const { attachedImages, handleImageUploaded, handleDeleteImage, resetAttachments } = useAttachedFiles(); + + const images = useSelector(attachedImages).flatMap((image) => (image ? [image] : [])); + + const { control, handleSubmit, reset } = useForm({ + defaultValues: new ContactSupportFormSchema(), + resolver: yupResolver(ContactSupportFormSchema.validationSchema), + }); + + const closeModal = (): void => { + sheetRef.current?.close(); + reset(new ContactSupportFormSchema()); + resetAttachments(); + }; + + const openModal = (): void => sheetRef.current?.present(); + + const handleOpen = (): void => inputRef.current?.focus(); + + const handlePickImage = async (): Promise => { + if (images.length >= contactSupportSheetConfig.maxAttachmentsCount) { + ToastService.showError( + translate('TEXT_ATTACHMENTS_LIMIT_REACHED', { count: contactSupportSheetConfig.maxAttachmentsCount }), + ); + + return; + } + + const image = await imagePickerService.getImage(ImagePickerSource.GALLERY_WITH_VIDEO); + const asset = image?.assets?.[0]; + + if (!asset) { + return; + } + + handleImageUploaded({ + uri: asset.uri, + base64: asset.base64 ?? '', + mimeType: asset.mimeType, + fileName: asset.fileName || undefined, + }); + }; + + const onSubmit = async ({ message }: ContactSupportFormSchema): Promise => { + try { + await submitFeedback({ + message, + platform: Platform.OS, + appVersion: Constants.expoConfig?.version, + apiVersion: configuration?.version, + userId: profile?.id, + attachments: images, + }); + + closeModal(); + ToastService.showSuccess(translate('TEXT_MESSAGE_SENT')); + } catch { + ToastService.showError(); + } + }; + + useImperativeHandle(ref, () => { + return { + present: openModal, + }; + }, []); + + const renderAttachment = (image: ImageData, index: number): ReactElement => ( + handleDeleteImage(image.uri)} /> + ); + + return ( + + + + + + {images.map(renderAttachment)} + + + + + } + /> + ); +} diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/components/attachment-row/component.tsx b/libs/mobile/settings/features/contact-support-sheet/src/lib/components/attachment-row/component.tsx new file mode 100644 index 00000000..9bf6ef63 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/components/attachment-row/component.tsx @@ -0,0 +1,28 @@ +import { ReactElement } from 'react'; +import { AppPressable, AppText, Icon, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { ImageData } from '@open-webui-react-native/shared/data-access/common'; + +export interface AttachmentRowProps { + attachment: ImageData; + onRemove: () => void; +} + +export function AttachmentRow({ attachment, onRemove }: AttachmentRowProps): ReactElement { + const fileName = attachment.fileName ?? attachment.uri.split('/').pop(); + + return ( + + + + + {fileName} + + + + + + + ); +} diff --git a/libs/mobile/settings/features/settings/src/lib/components/contact-support-sheet/index.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/components/attachment-row/index.ts similarity index 100% rename from libs/mobile/settings/features/settings/src/lib/components/contact-support-sheet/index.ts rename to libs/mobile/settings/features/contact-support-sheet/src/lib/components/attachment-row/index.ts diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/components/index.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/components/index.ts new file mode 100644 index 00000000..e6f87407 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/components/index.ts @@ -0,0 +1 @@ +export * from './attachment-row'; diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/config.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/config.ts new file mode 100644 index 00000000..279b27db --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/config.ts @@ -0,0 +1,3 @@ +export const contactSupportSheetConfig = { + maxAttachmentsCount: 5, +}; diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/contact-support-form.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/contact-support-form.ts new file mode 100644 index 00000000..90589dbc --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/contact-support-form.ts @@ -0,0 +1,16 @@ +import { i18n } from '@ronas-it/react-native-common-modules/i18n'; +import * as Yup from 'yup'; + +export class ContactSupportFormSchema { + public message: string; + + constructor(schema?: Partial) { + this.message = schema?.message || ''; + } + + public static get validationSchema(): Yup.ObjectSchema { + return Yup.object().shape({ + message: Yup.string().required(i18n.t('SHARED.VALIDATION.TEXT_REQUIRED_MESSAGE')), + }); + } +} diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/index.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/index.ts new file mode 100644 index 00000000..fc115377 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/forms/index.ts @@ -0,0 +1 @@ +export * from './contact-support-form'; diff --git a/libs/mobile/settings/features/contact-support-sheet/src/lib/index.ts b/libs/mobile/settings/features/contact-support-sheet/src/lib/index.ts new file mode 100644 index 00000000..bb824842 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-sheet/src/lib/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/libs/mobile/settings/features/contact-support-sheet/tsconfig.json b/libs/mobile/settings/features/contact-support-sheet/tsconfig.json new file mode 100644 index 00000000..ec74bfc3 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-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/settings/features/contact-support-sheet/tsconfig.lib.json b/libs/mobile/settings/features/contact-support-sheet/tsconfig.lib.json new file mode 100644 index 00000000..27f2b917 --- /dev/null +++ b/libs/mobile/settings/features/contact-support-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/settings/features/settings/src/lib/component.tsx b/libs/mobile/settings/features/settings/src/lib/component.tsx index 468ed771..18dca3ed 100644 --- a/libs/mobile/settings/features/settings/src/lib/component.tsx +++ b/libs/mobile/settings/features/settings/src/lib/component.tsx @@ -2,6 +2,10 @@ import { useSelector } from '@legendapp/state/react'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { ReactElement, useRef, useState } from 'react'; import { Alert } from 'react-native'; +import { + ContactSupportSheet, + ContactSupportSheetMethods, +} from '@open-webui-react-native/mobile/settings/features/contact-support-sheet'; import { SettingsSection, SettingsSectionOption } from '@open-webui-react-native/mobile/settings/ui/section'; import { useLogout } from '@open-webui-react-native/mobile/shared/features/use-logout'; import { Avatar, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; @@ -16,12 +20,11 @@ import { } from '@open-webui-react-native/shared/utils/config'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; import { useNavigateOnce } from '@open-webui-react-native/shared/utils/navigation'; +import { storeReviewService } from '@open-webui-react-native/shared/utils/store-review-service'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; import { ChangePasswordSheet, ChangePasswordSheetMethods, - ContactSupportSheet, - ContactSupportSheetMethods, DefaultModelSheet, DefaultModelSheetMethods, ProfileAvatarSheet, @@ -171,13 +174,17 @@ export function Settings(): ReactElement { }, ]; + const handleRateAppPress = (): void => { + storeReviewService.openStoreReview(); + }; + const feedbackOptions: Array = [ { - label: translate('TEXT_CONTACT_SUPPORT'), + label: translate('TEXT_REPORT_A_BUG'), iconName: 'message', onPress: () => contactSupportSheetRef.current?.present(), }, - { label: translate('TEXT_RATE_APP'), iconName: 'star', onPress: ToastService.showFeatureNotImplemented }, + { label: translate('TEXT_RATE_APP'), iconName: 'star', onPress: handleRateAppPress }, ]; const chatsOptions: Array = [ diff --git a/libs/mobile/settings/features/settings/src/lib/components/contact-support-sheet/component.tsx b/libs/mobile/settings/features/settings/src/lib/components/contact-support-sheet/component.tsx deleted file mode 100644 index 1a338f43..00000000 --- a/libs/mobile/settings/features/settings/src/lib/components/contact-support-sheet/component.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { BottomSheetModal, BottomSheetTextInput } from '@gorhom/bottom-sheet'; -import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; -import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - imagePickerService, - ImagePickerSource, -} from '@open-webui-react-native/mobile/shared/data-access/image-picker-service'; -import { colors } from '@open-webui-react-native/mobile/shared/ui/styles'; -import { - AppBottomSheet, - AppBottomSheetPropsType, - AppButton, - AppText, - Icon, - IconButton, - SheetHeader, - View, -} from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { SupportAttachment } from '../../types'; - -export type ContactSupportSheetMethods = { - present: () => void; -}; - -export type ContactSupportSheetRef = ForwardedRef; - -export type ContactSupportSheetProps = Partial> & { - ref?: ContactSupportSheetRef; -}; - -export function ContactSupportSheet({ ref, ...props }: ContactSupportSheetProps): ReactElement { - const translate = useTranslation('APP.SETTINGS_SCREEN.CONTACT_SUPPORT_SHEET'); - const { bottom } = useSafeAreaInsets(); - const sheetRef = useRef(null); - - const [message, setMessage] = useState(''); - const [attachments, setAttachments] = useState>([]); - - const closeSheet = (): void => sheetRef.current?.close(); - - useImperativeHandle(ref, () => ({ present: () => sheetRef.current?.present() }), []); - - const handleAttachPress = async (): Promise => { - const media = await imagePickerService.getImage(ImagePickerSource.GALLERY); - const asset = media?.assets?.[0]; - - if (!asset) { - return; - } - - setAttachments((currentAttachments) => [ - ...currentAttachments, - { uri: asset.uri, name: asset.fileName || asset.uri.split('/').pop() || '' }, - ]); - }; - - const handleRemoveAttachmentPress = (uri: string): void => - setAttachments((currentAttachments) => currentAttachments.filter((attachment) => attachment.uri !== uri)); - - const renderAttachment = ({ uri, name }: SupportAttachment): ReactElement => ( - - - - {name} - - handleRemoveAttachmentPress(uri)} - /> - - ); - - return ( - - - - - {attachments.map(renderAttachment)} - - - - } - /> - ); -} diff --git a/libs/mobile/settings/features/settings/src/lib/components/index.ts b/libs/mobile/settings/features/settings/src/lib/components/index.ts index f49e4050..091310f9 100644 --- a/libs/mobile/settings/features/settings/src/lib/components/index.ts +++ b/libs/mobile/settings/features/settings/src/lib/components/index.ts @@ -1,5 +1,4 @@ export * from './change-password-sheet'; -export * from './contact-support-sheet'; export * from './default-model-sheet'; export * from './profile-avatar-sheet'; export * from './profile-name-sheet'; diff --git a/libs/mobile/shared/data-access/image-picker-service/src/enums/source.ts b/libs/mobile/shared/data-access/image-picker-service/src/enums/source.ts index c4b1451a..cb34c904 100644 --- a/libs/mobile/shared/data-access/image-picker-service/src/enums/source.ts +++ b/libs/mobile/shared/data-access/image-picker-service/src/enums/source.ts @@ -1,4 +1,5 @@ export enum ImagePickerSource { CAMERA = 'camera', GALLERY = 'gallery', + GALLERY_WITH_VIDEO = 'gallery_with_video', } diff --git a/libs/mobile/shared/data-access/image-picker-service/src/service.ts b/libs/mobile/shared/data-access/image-picker-service/src/service.ts index 2ef806d7..9f949920 100644 --- a/libs/mobile/shared/data-access/image-picker-service/src/service.ts +++ b/libs/mobile/shared/data-access/image-picker-service/src/service.ts @@ -5,23 +5,25 @@ import { ToastService } from '@open-webui-react-native/shared/utils/toast-servic import { ImagePickerSource } from './enums'; export class ImagePickerService { - public defaultOptions: ImagePicker.ImagePickerOptions = { - mediaTypes: 'images', - allowsEditing: false, - base64: true, - quality: 0.2, - }; - public getImage(source: ImagePickerSource): Promise { return this.pickImage(source); } + private getDefaultOptions(withVideo = false): ImagePicker.ImagePickerOptions { + return { + mediaTypes: withVideo ? ['images', 'videos'] : 'images', + allowsEditing: false, + base64: true, + quality: 0.2, + }; + } + private requestGalleryAccess(): Promise { return ImagePicker.requestMediaLibraryPermissionsAsync(); } - private launchGallery(): Promise { - return ImagePicker.launchImageLibraryAsync(this.defaultOptions); + private launchGallery(withVideos = false): Promise { + return ImagePicker.launchImageLibraryAsync(this.getDefaultOptions(withVideos)); } private requestCameraAccess(): Promise { @@ -29,7 +31,7 @@ export class ImagePickerService { } private launchCamera(): Promise { - return ImagePicker.launchCameraAsync(this.defaultOptions); + return ImagePicker.launchCameraAsync(this.getDefaultOptions()); } private async pickImage(source: ImagePickerSource): Promise { @@ -43,7 +45,7 @@ export class ImagePickerService { return null; } - return isCamera ? this.launchCamera() : this.launchGallery(); + return isCamera ? this.launchCamera() : this.launchGallery(source === ImagePickerSource.GALLERY_WITH_VIDEO); } } diff --git a/libs/mobile/shared/data-access/speech-streaming-service/src/prepare-speakable-text.ts b/libs/mobile/shared/data-access/speech-streaming-service/src/prepare-speakable-text.ts index 0e4b9bac..993a5ae1 100644 --- a/libs/mobile/shared/data-access/speech-streaming-service/src/prepare-speakable-text.ts +++ b/libs/mobile/shared/data-access/speech-streaming-service/src/prepare-speakable-text.ts @@ -20,10 +20,7 @@ export const prepareSpeakableText = ( return result; }; -const removeDetailsBlocks = ( - text: string, - { holdIncomplete = false }: PrepareSpeakableTextOptions, -): string => { +const removeDetailsBlocks = (text: string, { holdIncomplete = false }: PrepareSpeakableTextOptions): string => { let result = ''; let index = 0; @@ -132,7 +129,9 @@ const stripMarkdownForSpeech = (text: string): string => { // NOTE: Avoid trim() so streaming speakable prefixes stay stable for the spoken cursor const normalizeSpeakableWhitespace = (text: string): string => - text.replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').replace(/ ?\n ?/g, '\n'); + text + .replace(/[^\S\n]+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .replace(/ ?\n ?/g, '\n'); -const appendBoundarySpace = (text: string): string => - text.length > 0 && !/\s$/.test(text) ? `${text} ` : text; +const appendBoundarySpace = (text: string): string => (text.length > 0 && !/\s$/.test(text) ? `${text} ` : text); diff --git a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts index 3edd323c..d9f52179 100644 --- a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts +++ b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts @@ -25,6 +25,13 @@ export function useAttachedFiles(): typeof result { mimeType: ImageMimeType.JPEG, uri: compressed, base64: await fileSystemService.convertToBase64(compressed), + fileName: image.fileName, + }; + } else if (!image.base64) { + // NOTE: expo-image-picker only base64-encodes images, not videos, so videos need to be read from disk + processed = { + ...image, + base64: await fileSystemService.convertToBase64(image.uri), }; } diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment-round.svg b/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment-round.svg new file mode 100644 index 00000000..805b56e9 --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment-round.svg @@ -0,0 +1,4 @@ + + + + diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment.svg b/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment.svg new file mode 100644 index 00000000..167f315c --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts b/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts index fe125baa..f44a0a26 100644 --- a/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts @@ -2,6 +2,8 @@ import alert from './alert.svg'; import archive from './archive.svg'; import arrowDown from './arrow-down.svg'; import arrowUp from './arrow-up.svg'; +import attachmentRound from './attachment-round.svg'; +import attachment from './attachment.svg'; import camera from './camera.svg'; import checkedSmall from './checked-small.svg'; import checked from './checked.svg'; @@ -73,6 +75,8 @@ export const Icons = { checked, search, arrowUp, + attachmentRound, + attachment, arrowDown, moreDots, archive, @@ -99,11 +103,13 @@ export const Icons = { plusInCircle, folderPlus, folder, + message, microphone, unarchive, closeSM, strokeLeft, tick, + star, stop, play, refresh, @@ -112,6 +118,4 @@ export const Icons = { moreText, web, key, - message, - star, }; diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/message.svg b/libs/mobile/shared/ui/ui-kit/src/assets/icons/message.svg index 3ab0d686..4d234519 100644 --- a/libs/mobile/shared/ui/ui-kit/src/assets/icons/message.svg +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/message.svg @@ -1,9 +1,4 @@ - - - + + diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/star.svg b/libs/mobile/shared/ui/ui-kit/src/assets/icons/star.svg index d3031818..453380f9 100644 --- a/libs/mobile/shared/ui/ui-kit/src/assets/icons/star.svg +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/star.svg @@ -1,5 +1,3 @@ - + diff --git a/libs/shared/data-access/api/src/lib/index.ts b/libs/shared/data-access/api/src/lib/index.ts index 4e909381..147558c2 100644 --- a/libs/shared/data-access/api/src/lib/index.ts +++ b/libs/shared/data-access/api/src/lib/index.ts @@ -8,3 +8,4 @@ export * from './audio'; export * from './folders'; export * from './knowledge'; export * from './tasks'; +export * from './supabase'; diff --git a/libs/shared/data-access/api/src/lib/supabase/api.ts b/libs/shared/data-access/api/src/lib/supabase/api.ts new file mode 100644 index 00000000..982a0792 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/supabase/api.ts @@ -0,0 +1,15 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import { SubmitFeedbackRequest, supabaseService } from '@open-webui-react-native/shared/utils/supabase-service'; + +function useSubmitFeedback( + props?: UseMutationOptions, +): UseMutationResult { + return useMutation({ + mutationFn: (request) => supabaseService.submit(request), + ...props, + }); +} + +export const supabaseApi = { + useSubmitFeedback, +}; diff --git a/libs/shared/data-access/api/src/lib/supabase/index.ts b/libs/shared/data-access/api/src/lib/supabase/index.ts new file mode 100644 index 00000000..b1c13e73 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/supabase/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/libs/shared/data-access/common/src/types/image-data.ts b/libs/shared/data-access/common/src/types/image-data.ts index cb830321..12010d4d 100644 --- a/libs/shared/data-access/common/src/types/image-data.ts +++ b/libs/shared/data-access/common/src/types/image-data.ts @@ -2,4 +2,5 @@ export type ImageData = { uri: string; base64: string; mimeType?: string; + fileName?: string; }; diff --git a/libs/shared/data-access/storage/src/service.ts b/libs/shared/data-access/storage/src/service.ts index 8ebe3ad0..1854fda6 100644 --- a/libs/shared/data-access/storage/src/service.ts +++ b/libs/shared/data-access/storage/src/service.ts @@ -13,6 +13,8 @@ class AppStorageService { public apiUrl = new StorageItem('apiUrl', storage); public locale = new StorageItem('locale', storage); public markdownRenderer = new StorageItem('markdownRenderer', storage); + public createdChatsIds = new StorageItem('createdChatsIds', storage); + public isRateAppReviewed = new StorageItem('isRateAppReviewed', storage); } export const appStorageService = new AppStorageService(); diff --git a/libs/shared/utils/store-review-service/.babelrc b/libs/shared/utils/store-review-service/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/shared/utils/store-review-service/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/utils/store-review-service/README.md b/libs/shared/utils/store-review-service/README.md new file mode 100644 index 00000000..d358961f --- /dev/null +++ b/libs/shared/utils/store-review-service/README.md @@ -0,0 +1,7 @@ +# shared/utils/store-review-service + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared/utils/store-review-service` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/utils/store-review-service/eslint.config.cjs b/libs/shared/utils/store-review-service/eslint.config.cjs new file mode 100644 index 00000000..87eef8a2 --- /dev/null +++ b/libs/shared/utils/store-review-service/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/shared/utils/store-review-service/project.json b/libs/shared/utils/store-review-service/project.json new file mode 100644 index 00000000..985f690d --- /dev/null +++ b/libs/shared/utils/store-review-service/project.json @@ -0,0 +1,9 @@ +{ + "name": "shared/utils/store-review-service", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/utils/store-review-service/src", + "projectType": "library", + "tags": ["app:shared", "scope:shared", "type:utils"], + "// targets": "to see all targets run: nx show project shared/utils/store-review-service --web", + "targets": {} +} diff --git a/libs/shared/utils/store-review-service/src/index.ts b/libs/shared/utils/store-review-service/src/index.ts new file mode 100644 index 00000000..f78beabc --- /dev/null +++ b/libs/shared/utils/store-review-service/src/index.ts @@ -0,0 +1 @@ +export * from './service'; diff --git a/libs/shared/utils/store-review-service/src/service.ts b/libs/shared/utils/store-review-service/src/service.ts new file mode 100644 index 00000000..c874b802 --- /dev/null +++ b/libs/shared/utils/store-review-service/src/service.ts @@ -0,0 +1,29 @@ +import { storeUrl, hasAction, requestReview } from 'expo-store-review'; +import { Linking, Platform } from 'react-native'; + +class StoreReviewService { + public async openStoreReview(): Promise { + const isAndroid = Platform.OS === 'android'; + const url = storeUrl(); + + const canOpenUrl = url && (await Linking.canOpenURL(url)); + + if (canOpenUrl) { + await Linking.openURL(`${url}${isAndroid ? '&showAllReviews=true' : `?action=write-review`}`); + } + } + + public async requestStoreReview(): Promise { + const isAvailable = await hasAction(); + + if (isAvailable) { + await requestReview(); + + return true; + } else { + return false; + } + } +} + +export const storeReviewService = new StoreReviewService(); diff --git a/libs/shared/utils/store-review-service/tsconfig.json b/libs/shared/utils/store-review-service/tsconfig.json new file mode 100644 index 00000000..93614c49 --- /dev/null +++ b/libs/shared/utils/store-review-service/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/shared/utils/store-review-service/tsconfig.lib.json b/libs/shared/utils/store-review-service/tsconfig.lib.json new file mode 100644 index 00000000..45b22973 --- /dev/null +++ b/libs/shared/utils/store-review-service/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/utils/supabase-service/.babelrc b/libs/shared/utils/supabase-service/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/shared/utils/supabase-service/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/utils/supabase-service/README.md b/libs/shared/utils/supabase-service/README.md new file mode 100644 index 00000000..40c05ee5 --- /dev/null +++ b/libs/shared/utils/supabase-service/README.md @@ -0,0 +1,7 @@ +# shared-utils-supabase-service + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-utils-supabase-service` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/utils/supabase-service/eslint.config.cjs b/libs/shared/utils/supabase-service/eslint.config.cjs new file mode 100644 index 00000000..87eef8a2 --- /dev/null +++ b/libs/shared/utils/supabase-service/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/shared/utils/supabase-service/project.json b/libs/shared/utils/supabase-service/project.json new file mode 100644 index 00000000..5d5e6087 --- /dev/null +++ b/libs/shared/utils/supabase-service/project.json @@ -0,0 +1,9 @@ +{ + "name": "shared-utils-supabase-service", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/utils/supabase-service/src", + "projectType": "library", + "tags": ["app:shared", "scope:shared", "type:utils"], + "// targets": "to see all targets run: nx show project shared-utils-supabase-service --web", + "targets": {} +} diff --git a/libs/shared/utils/supabase-service/src/enums/index.ts b/libs/shared/utils/supabase-service/src/enums/index.ts new file mode 100644 index 00000000..4961caae --- /dev/null +++ b/libs/shared/utils/supabase-service/src/enums/index.ts @@ -0,0 +1,2 @@ +export * from './supabase-table'; +export * from './supabase-bucket'; diff --git a/libs/shared/utils/supabase-service/src/enums/supabase-bucket.ts b/libs/shared/utils/supabase-service/src/enums/supabase-bucket.ts new file mode 100644 index 00000000..cd379cf0 --- /dev/null +++ b/libs/shared/utils/supabase-service/src/enums/supabase-bucket.ts @@ -0,0 +1,3 @@ +export enum SupabaseBucket { + FEEDBACK_ATTACHMENTS = 'feedback-attachments', +} diff --git a/libs/shared/utils/supabase-service/src/enums/supabase-table.ts b/libs/shared/utils/supabase-service/src/enums/supabase-table.ts new file mode 100644 index 00000000..e01b4234 --- /dev/null +++ b/libs/shared/utils/supabase-service/src/enums/supabase-table.ts @@ -0,0 +1,3 @@ +export enum SupabaseTable { + FEEDBACK = 'feedback', +} diff --git a/libs/shared/utils/supabase-service/src/index.ts b/libs/shared/utils/supabase-service/src/index.ts new file mode 100644 index 00000000..715cab25 --- /dev/null +++ b/libs/shared/utils/supabase-service/src/index.ts @@ -0,0 +1,2 @@ +export * from './service'; +export * from './models'; diff --git a/libs/shared/utils/supabase-service/src/models/index.ts b/libs/shared/utils/supabase-service/src/models/index.ts new file mode 100644 index 00000000..72064800 --- /dev/null +++ b/libs/shared/utils/supabase-service/src/models/index.ts @@ -0,0 +1 @@ +export * from './submit-feedback-request'; diff --git a/libs/shared/utils/supabase-service/src/models/submit-feedback-request.ts b/libs/shared/utils/supabase-service/src/models/submit-feedback-request.ts new file mode 100644 index 00000000..85939c7e --- /dev/null +++ b/libs/shared/utils/supabase-service/src/models/submit-feedback-request.ts @@ -0,0 +1,14 @@ +export interface SubmitFeedbackAttachment { + base64: string; + mimeType?: string; + fileName?: string; +} + +export interface SubmitFeedbackRequest { + message: string; + platform: string; + appVersion?: string; + apiVersion?: string; + userId?: string; + attachments?: Array; +} diff --git a/libs/shared/utils/supabase-service/src/service.ts b/libs/shared/utils/supabase-service/src/service.ts new file mode 100644 index 00000000..436b22a7 --- /dev/null +++ b/libs/shared/utils/supabase-service/src/service.ts @@ -0,0 +1,59 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { decode } from 'base64-arraybuffer'; +import Constants from 'expo-constants'; +import { SupabaseBucket, SupabaseTable } from './enums'; +import { SubmitFeedbackAttachment, SubmitFeedbackRequest } from './models'; + +class SupabaseService { + private client: SupabaseClient; + + public init(): void { + const supabaseConfig = Constants.expoConfig?.extra?.supabase; + + this.client = createClient(supabaseConfig?.url ?? '', supabaseConfig?.publishableKey ?? '', { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, + }); + } + + public async submit(request: SubmitFeedbackRequest): Promise { + const attachmentPaths = request.attachments?.length + ? await Promise.all(request.attachments.map((attachment, index) => this.uploadAttachment(attachment, index))) + : []; + + const { error } = await this.client.from(SupabaseTable.FEEDBACK).insert({ + message: request.message, + platform: request.platform, + app_version: request.appVersion, + api_version: request.apiVersion, + app_user_id: request.userId, + attachment_paths: attachmentPaths, + }); + + if (error) { + throw error; + } + } + + private async uploadAttachment(attachment: SubmitFeedbackAttachment, index: number): Promise { + const fallbackFileName = attachment.mimeType?.startsWith('video/') ? 'video.mp4' : 'image.jpg'; + const path = `${Date.now()}-${index}-${attachment.fileName ?? fallbackFileName}`; + + const { error } = await this.client.storage + .from(SupabaseBucket.FEEDBACK_ATTACHMENTS) + .upload(path, decode(attachment.base64), { + contentType: attachment.mimeType, + }); + + if (error) { + throw error; + } + + return path; + } +} + +export const supabaseService = new SupabaseService(); diff --git a/libs/shared/utils/supabase-service/tsconfig.json b/libs/shared/utils/supabase-service/tsconfig.json new file mode 100644 index 00000000..93614c49 --- /dev/null +++ b/libs/shared/utils/supabase-service/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/shared/utils/supabase-service/tsconfig.lib.json b/libs/shared/utils/supabase-service/tsconfig.lib.json new file mode 100644 index 00000000..45b22973 --- /dev/null +++ b/libs/shared/utils/supabase-service/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/package-lock.json b/package-lock.json index 2bef0577..0ea5079f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,13 @@ "@ronas-it/rtkq-entity-api": "^1.1.1", "@sentry/react-native": "~7.11.0", "@shopify/flash-list": "2.0.2", + "@supabase/supabase-js": "^2.112.3", "@tanstack/react-query": "^5.80.6", "@tanstack/react-query-persist-client": "^5.87.4", "axios-observable": "^2.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "base64-arraybuffer": "^1.0.2", "class-transformer": "^0.5.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -63,6 +65,7 @@ "expo-speech": "~57.0.1", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", + "expo-store-review": "~57.0.1", "expo-updates": "~57.0.10", "html-entities": "^2.6.0", "i18n-js": "^4.5.1", @@ -99,6 +102,7 @@ "react-native-svg": "15.15.4", "react-native-svg-transformer": "~1.5.0", "react-native-toast-message": "^2.3.0", + "react-native-url-polyfill": "^4.0.0", "react-native-uuid": "^2.0.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.1", @@ -192,11 +196,13 @@ "@ronas-it/rtkq-entity-api": "^1.1.1", "@sentry/react-native": "~7.11.0", "@shopify/flash-list": "2.0.2", + "@supabase/supabase-js": "^2.112.3", "@tanstack/react-query": "^5.80.6", "@tanstack/react-query-persist-client": "^5.87.4", "axios-observable": "^2.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "base64-arraybuffer": "^1.0.2", "class-transformer": "^0.5.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -224,6 +230,7 @@ "expo-speech": "~57.0.1", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", + "expo-store-review": "~57.0.1", "expo-updates": "~57.0.10", "html-entities": "^2.6.0", "i18n-js": "^4.5.1", @@ -235,6 +242,7 @@ "nativewind": "^4.1.23", "patch-package": "^8.0.0", "pusher-js": "^8.4.0", + "ratex-react-native": "^0.1.14", "react": "19.2.3", "react-compiler-runtime": "^19.0.0-beta-ebf51a3-20250411", "react-content-loader": "^7.0.2", @@ -251,6 +259,7 @@ "react-native-mmkv": "^3.2.0", "react-native-modal": "^14.0.0-rc.1", "react-native-nitro-cookies": "^1.1.0", + "react-native-nitro-markdown": "^0.10.0", "react-native-nitro-modules": "^0.35.9", "react-native-reanimated": "4.5.0", "react-native-safe-area-context": "~5.7.0", @@ -258,6 +267,7 @@ "react-native-svg": "15.15.4", "react-native-svg-transformer": "~1.5.0", "react-native-toast-message": "^2.3.0", + "react-native-url-polyfill": "^4.0.0", "react-native-uuid": "^2.0.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.1", @@ -9850,6 +9860,98 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@supabase/auth-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.112.3.tgz", + "integrity": "sha512-NA0rsgAlWZPvbhw8aUdmgfpHVgUAcd8zK5ov43l++o1bLIPXZhRiAlRobhwF5AatQuovpqxsMH50F4oyyV4XZw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.112.3.tgz", + "integrity": "sha512-gfv481mTOVWtZIJgXupxZpni2V2UWPf6jeF/jOK7HdMHdH+mt6sU0sHHwf0POsPip8ltlulu9OUHgwVzl5ddRw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.112.3.tgz", + "integrity": "sha512-+Mf6uCpzr00bqxwX8hTK2X2L9eAL/1vuOjdEjx6upz9ulb0RmQT16XeU/JkMUlVHw/B46ZnPa2busY4Kd9YCzw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.112.3.tgz", + "integrity": "sha512-E6wljXWs7DUOloyIB69i3YFInWE6IyCvgTAbQ0KYxOHv26FdA1KzEXTuzxrYEdf70t406Z9BRwUlGyclGF2FXA==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "0.4.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.112.3.tgz", + "integrity": "sha512-oSK61tzlUvg+BWPqpKQCu9qqonsO26btaoAR9D6Gest2aj7xUqToj9rKyaoYOJczkhg9BjqA1REbYy9tPI4bDA==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.112.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.3.tgz", + "integrity": "sha512-Jv1bxVQmEJNkjvPEhFaKjPzsh+Ozyew6lWGD+SoYcsclDEP1z7yEvKvfUQfzy0DkxRIQnZNxmmWtAzw5XLTQoA==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.112.3", + "@supabase/functions-js": "2.112.3", + "@supabase/postgrest-js": "2.112.3", + "@supabase/realtime-js": "2.112.3", + "@supabase/storage-js": "2.112.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -13286,6 +13388,15 @@ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -17942,6 +18053,16 @@ "react-native": "*" } }, + "node_modules/expo-store-review": { + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-57.0.1.tgz", + "integrity": "sha512-OktDBfIEe4DQXVxz7umFyg+slkZo/nrr4GfwiFJlNjOXl+F+vNsaGBdEn1aE4W5DAnSVpLQsjaZt1dts2HJXlQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-structured-headers": { "version": "57.0.0", "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-57.0.0.tgz", @@ -20542,6 +20663,15 @@ "make-plural": "7.5.0" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -28803,6 +28933,15 @@ "react-native": "*" } }, + "node_modules/react-native-url-polyfill": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-4.0.0.tgz", + "integrity": "sha512-eqYM3wBAA0eL1sPYbBAoNfbES3+NkgcxUdelQ7QzmoVtqKB5qGG0U13MPTRUroAWK+y2EoJFS3MZUK0fwTf0pA==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/react-native-uuid": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.4.tgz", diff --git a/package.json b/package.json index 1e966340..9bf9ea5d 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,13 @@ "@ronas-it/rtkq-entity-api": "^1.1.1", "@sentry/react-native": "~7.11.0", "@shopify/flash-list": "2.0.2", + "@supabase/supabase-js": "^2.112.3", "@tanstack/react-query": "^5.80.6", "@tanstack/react-query-persist-client": "^5.87.4", "axios-observable": "^2.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "base64-arraybuffer": "^1.0.2", "class-transformer": "^0.5.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -61,6 +63,7 @@ "expo-speech": "~57.0.1", "expo-splash-screen": "~57.0.5", "expo-status-bar": "~57.0.1", + "expo-store-review": "~57.0.1", "expo-updates": "~57.0.10", "html-entities": "^2.6.0", "i18n-js": "^4.5.1", @@ -97,6 +100,7 @@ "react-native-svg": "15.15.4", "react-native-svg-transformer": "~1.5.0", "react-native-toast-message": "^2.3.0", + "react-native-url-polyfill": "^4.0.0", "react-native-uuid": "^2.0.3", "react-native-web": "^0.21.0", "react-native-webview": "13.16.1", diff --git a/tsconfig.base.json b/tsconfig.base.json index 6ddaff11..b00302a1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -152,6 +152,9 @@ "@open-webui-react-native/mobile/profile/features/menu-sheet": [ "libs/mobile/profile/features/menu-sheet/src/index.ts" ], + "@open-webui-react-native/mobile/settings/features/contact-support-sheet": [ + "libs/mobile/settings/features/contact-support-sheet/src/index.ts" + ], "@open-webui-react-native/mobile/settings/features/settings": [ "libs/mobile/settings/features/settings/src/index.ts" ], @@ -271,7 +274,11 @@ "@open-webui-react-native/shared/utils/objects": ["libs/shared/utils/objects/src/index.ts"], "@open-webui-react-native/shared/utils/permission-alert": ["libs/shared/utils/permission-alert/src/index.ts"], "@open-webui-react-native/shared/utils/reactotron": ["libs/shared/utils/reactotron/src/index.ts"], + "@open-webui-react-native/shared/utils/store-review-service": [ + "libs/shared/utils/store-review-service/src/index.ts" + ], "@open-webui-react-native/shared/utils/strings": ["libs/shared/utils/strings/src/index.ts"], + "@open-webui-react-native/shared/utils/supabase-service": ["libs/shared/utils/supabase-service/src/index.ts"], "@open-webui-react-native/shared/utils/toast-service": ["libs/shared/utils/toast-service/src/index.ts"], "@open-webui-react-native/shared/utils/use-app-state-change": [ "libs/shared/utils/use-app-state-change/src/index.ts" diff --git a/types.d.ts b/types.d.ts index 771a040e..fab02008 100644 --- a/types.d.ts +++ b/types.d.ts @@ -2,3 +2,4 @@ // Issue: https://github.com/gustavopch/tsc-files/issues/20 // TODO: Uncomment the line below if you have an Expo app import 'node_modules/@nx/expo/typings/svg.d.ts'; +import 'apps/mobile/nativewind-env.d.ts';