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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/mobile/app/(main)/chat/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UpsertFolderSheet,
UpsertFolderSheetMethods,
} from '@open-webui-react-native/mobile/folder/features/upsert-folder-sheet';
import { useFoldersEnabled } from '@open-webui-react-native/mobile/shared/features/use-folders-enabled';
import { ScreenWrapper } from '@open-webui-react-native/mobile/shared/ui/screen-wrapper';
import { AppHeader, AppPressable, Avatar, IconButton, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit';
import { navigationConfig } from '@open-webui-react-native/mobile/shared/utils/navigation';
Expand All @@ -29,15 +30,13 @@ export default function ChatListScreen(): ReactElement {
const upsertFolderSheetRef = useRef<UpsertFolderSheetMethods>(null);
const shareFolderSheetRef = useRef<ShareFolderSheetMethods>(null);
const { data: profile } = authApi.useGetProfile();
const canUseFolders = useFoldersEnabled();

const handleChatPress = (id: string): void => navigateOnce(navigationConfig.main.chat.view({ id }));

const handleNewChatPress = (): void =>
navigateOnce(`${navigationConfig.main.chat.index}/${navigationConfig.main.chat.create}`);

const handleArchivedChatsPress = (): void =>
navigateOnce(`${navigationConfig.main.chat.index}/${navigationConfig.main.chat.archivedChats}`);

const handleSettingsPress = (): void => navigateOnce(navigationConfig.main.settings);

const handleFolderPress = (id: string, title: string): void =>
Expand Down Expand Up @@ -68,7 +67,7 @@ export default function ChatListScreen(): ReactElement {
}
accessoryRight={
<View className='flex-row gap-12'>
{isFeatureEnabled(FeatureID.CHAT_FOLDERS) && (
{isFeatureEnabled(FeatureID.CHAT_FOLDERS) && canUseFolders && (
<UpsertFolderSheet
renderTrigger={({ onPress }) => (
<IconButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FolderSearchItem,
useFolderSearchList,
} from '@open-webui-react-native/mobile/chat/utils/use-folder-search-list';
import { useFoldersEnabled } from '@open-webui-react-native/mobile/shared/features/use-folders-enabled';
import { useColorScheme } from '@open-webui-react-native/mobile/shared/ui/styles';
import {
AppPressable,
Expand Down Expand Up @@ -32,14 +33,16 @@ export function SearchFolderView({
const translate = useTranslation('CHAT.CREATE_CHAT.SEARCH_FOLDER_VIEW');
const { isDarkColorScheme } = useColorScheme();

const canUseFolders = useFoldersEnabled();
const { emptyFolders } = useFolderSearchList({
noFolderText: translate('TEXT_NO_FOLDER'),
createFolderText: translate('TEXT_CREATE_NEW_FOLDER'),
onCreateFolderPress,
canCreateFolder: canUseFolders,
});

const { data: folders } = foldersApi.useGetFolders();
const { data: sharedFolders } = foldersApi.useGetSharedFolders();
const { data: folders } = foldersApi.useGetFolders({ enabled: canUseFolders });
const { data: sharedFolders } = foldersApi.useGetSharedFolders({ enabled: canUseFolders });

// NOTE: A chat can be created in a folder shared by somebody else only with a write grant, so
// read-only ones are left out. Without them the folder opened from its own screen resolved to no
Expand Down Expand Up @@ -81,14 +84,16 @@ export function SearchFolderView({
width={60}
height={60} />
)}
<FullScreenSearchModal
data={foldersWithIcon || []}
unfilteredData={emptyFolders as Array<FolderSearchItem>}
selectedItemId={selectedItemId}
renderTrigger={renderTrigger}
searchPlaceholder={translate('TEXT_SEARCH_FOLDER')}
{...props}
/>
{canUseFolders && (
<FullScreenSearchModal
data={foldersWithIcon || []}
unfilteredData={emptyFolders as Array<FolderSearchItem>}
selectedItemId={selectedItemId}
renderTrigger={renderTrigger}
searchPlaceholder={translate('TEXT_SEARCH_FOLDER')}
{...props}
/>
)}
</View>
);
}
17 changes: 13 additions & 4 deletions libs/mobile/chat/features/menu-list/src/lib/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChatActionsMenuSheet,
ChatActionsMenuSheetMethods,
} from '@open-webui-react-native/mobile/shared/features/chat-actions-menu-sheet';
import { useFoldersEnabled } from '@open-webui-react-native/mobile/shared/features/use-folders-enabled';
import { ChatListRow } from '@open-webui-react-native/mobile/shared/ui/chat-list-row';
import { DateSectionList } from '@open-webui-react-native/mobile/shared/ui/date-section-list';
import {
Expand Down Expand Up @@ -44,6 +45,8 @@ export function ChatMenuList({

const [isFirstLoading, setIsFirstLoading] = useState<boolean>(true);

const canUseFolders = useFoldersEnabled();

const {
data: chats,
isFetchingNextPage,
Expand All @@ -63,20 +66,26 @@ export function ChatMenuList({
isLoading: isFoldersLoading,
isRefetching: isFoldersRefetching,
refetch: refetchFolders,
} = foldersApi.useGetFolders();
} = foldersApi.useGetFolders({ enabled: canUseFolders });
const {
data: sharedFolders,
isLoading: isSharedFoldersLoading,
isRefetching: isSharedFoldersRefetching,
refetch: refetchSharedFolders,
} = foldersApi.useGetSharedFolders();
} = foldersApi.useGetSharedFolders({ enabled: canUseFolders });

const isLoading =
isChatsLoading || isPinnedChatsLoading || isFoldersLoading || isSharedFoldersLoading || isFirstLoading;
const isRefetching = isChatsRefetching || isPinnedChatsRefetching || isFoldersRefetching || isSharedFoldersRefetching;

const refetch = (): void => {
Promise.all([refetchChats(), refetchPinnedChats(), refetchFolders(), refetchSharedFolders()]);
// NOTE: react-query's refetch() runs even for a disabled query, so the folders feature gate has
// to be re-checked here too, or a pull-to-refresh would still hit the forbidden endpoint.
Promise.all([
refetchChats(),
refetchPinnedChats(),
...(canUseFolders ? [refetchFolders(), refetchSharedFolders()] : []),
]);
};

useEffect(() => {
Expand Down Expand Up @@ -113,7 +122,7 @@ export function ChatMenuList({
refreshControl={<AppRefreshControl onRefresh={refetch} refreshing={isFocused && isRefetching} />}
ListHeaderComponent={
<View>
{isFeatureEnabled(FeatureID.CHAT_FOLDERS) && (
{isFeatureEnabled(FeatureID.CHAT_FOLDERS) && canUseFolders && (
<Fragment>
{/* NOTE: A folder owned by somebody else cannot be renamed or deleted by the
recipient, so its row offers no actions on long press. */}
Expand Down
26 changes: 16 additions & 10 deletions libs/mobile/chat/utils/use-folder-search-list/src/lib/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface UseFolderSearchListParams {
noFolderText: string;
createFolderText: string;
onCreateFolderPress: () => void;
canCreateFolder?: boolean;
}

interface UseFolderSearchListResult {
Expand All @@ -22,6 +23,7 @@ export function useFolderSearchList({
noFolderText,
createFolderText,
onCreateFolderPress,
canCreateFolder = true,
}: UseFolderSearchListParams): UseFolderSearchListResult {
const { isDarkColorScheme } = useColorScheme();

Expand All @@ -32,17 +34,21 @@ export function useFolderSearchList({
name: noFolderText,
iconName: isDarkColorScheme ? ('logoSmallDark' as IconName) : ('logoSmallLight' as IconName),
},
{
id: MockFolderItemIds.CREATE_FOLDER_ID,
name: createFolderText,
onPress: onCreateFolderPress,
iconName: 'folderPlus' as IconName,
containerClassName: 'mb-24',
textClassName: 'text-brand-primary',
iconClassName: 'color-brand-primary',
},
...(canCreateFolder
? [
{
id: MockFolderItemIds.CREATE_FOLDER_ID,
name: createFolderText,
onPress: onCreateFolderPress,
iconName: 'folderPlus' as IconName,
containerClassName: 'mb-24',
textClassName: 'text-brand-primary',
iconClassName: 'color-brand-primary',
},
]
: []),
],
[isDarkColorScheme, noFolderText, createFolderText, onCreateFolderPress],
[isDarkColorScheme, canCreateFolder, noFolderText, createFolderText, onCreateFolderPress],
);

return { emptyFolders, createFolderId: MockFolderItemIds.CREATE_FOLDER_ID };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet';
import { useTranslation } from '@ronas-it/react-native-common-modules/i18n';
import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react';
import { fileSystemService } from '@open-webui-react-native/mobile/shared/data-access/file-system-service';
import { useFoldersEnabled } from '@open-webui-react-native/mobile/shared/features/use-folders-enabled';
import {
ActionsBottomSheet,
ActionsBottomSheetProps,
Expand Down Expand Up @@ -39,7 +40,8 @@ export function FolderActionsSheet({ onEditPress, onSharePress, ref }: FolderAct
const [isExportLoading, setIsExportLoading] = useState<boolean>(false);

const { data: profile } = authApi.useGetProfile();
const { data: sharedFolders } = foldersApi.useGetSharedFolders();
const canUseFolders = useFoldersEnabled();
const { data: sharedFolders } = foldersApi.useGetSharedFolders({ enabled: canUseFolders });
const { mutateAsync: deleteFolder, isPending: isDeleting } = foldersApi.useDeleteFolder();

// NOTE: The shared list holds exactly the folders owned by somebody else, so a folder missing from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
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 { useFoldersEnabled } from '@open-webui-react-native/mobile/shared/features/use-folders-enabled';
import {
ActionButtonsModal,
ActionButtonsModalMethods,
Expand Down Expand Up @@ -64,7 +65,8 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat
const { mutateAsync: cloneChat, isPending: isCloning } = chatApi.useCloneChat();
const { mutateAsync: archiveChat, isPending: isArchiving } = chatApi.useArchiveChat();
const { mutateAsync: unarchiveChat, isPending: isUnarchiving } = chatApi.useUnarchiveChat();
const { data: folders } = foldersApi.useGetFolders();
const canUseFolders = useFoldersEnabled();
const { data: folders } = foldersApi.useGetFolders({ enabled: canUseFolders });

const [activeChat, setActiveChat] = useState<ChatListItem | null>(null);
const [folderId, setFolderId] = useState<string | undefined>(undefined);
Expand Down Expand Up @@ -105,6 +107,7 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat
noFolderText: translate('MOVE_CHAT_TO_FOLDER_MODAL.TEXT_NO_FOLDER'),
createFolderText: translate('MOVE_CHAT_TO_FOLDER_MODAL.TEXT_CREATE_NEW_FOLDER'),
onCreateFolderPress: openCreateFolderModal,
canCreateFolder: canUseFolders,
});

const handleAction = useMemo(
Expand Down Expand Up @@ -236,7 +239,7 @@ export function ChatActionsMenuSheet({ goToChat, isPinned, ref, isInChat }: Chat
};

const actions: Array<ActionSheetItemProps> = compact([
{
canUseFolders && {
title: translate('TEXT_MOVE_TO_FOLDER'),
iconName: 'folderPlus',
onPress: () => handleAction(ChatAction.MOVE_TO_FOLDER),
Expand Down
12 changes: 12 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
12 changes: 12 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/eslint.config.cjs
Original file line number Diff line number Diff line change
@@ -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: {},
},
];
9 changes: 9 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "mobile/shared/features/use-folders-enabled",
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/mobile/shared/features/use-folders-enabled/src",
"projectType": "library",
"tags": ["app:mobile", "scope:shared", "type:features"],
"// targets": "to see all targets run: nx show project mobile/shared/features/use-folders-enabled --web",
"targets": {}
}
15 changes: 15 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/src/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { appConfigurationApi, authApi } from '@open-webui-react-native/shared/data-access/api';
import { UserRole } from '@open-webui-react-native/shared/data-access/common';

export function useFoldersEnabled(): boolean {
const { data: config } = appConfigurationApi.useGetAppConfiguration();
const { data: profile } = authApi.useGetProfile();

// NOTE: matches the web app's gate (Sidebar.svelte) and the backend's check_folders_permission,
// which guards every folders route (read and write alike) — the instance-wide toggle must be on,
// and a non-admin additionally needs the per-user permission, defaulting to allowed when unset.
return Boolean(
config?.features.enableFolders &&
(profile?.role === UserRole.ADMIN || (profile?.permissions.features.folders ?? true)),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hook';
17 changes: 17 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
19 changes: 19 additions & 0 deletions libs/mobile/shared/features/use-folders-enabled/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class Features {
@Expose({ name: 'enable_direct_connections' })
public enableDirectConnections: boolean;

@Expose({ name: 'enable_folders' })
public enableFolders: boolean;

@Expose({ name: 'enable_channels' })
public enableChannels: boolean;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class FeaturesPermissions {
@Expose()
public notes: boolean;

@Expose()
public folders: boolean;

constructor(model: Partial<FeaturesPermissions>) {
Object.assign(this, model);
}
Expand Down
2 changes: 1 addition & 1 deletion libs/shared/data-access/api/src/lib/folders/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function useDeleteFolder(
}

function useGetFolders(
props?: UseQueryOptions<Array<FolderListItem>, AxiosError<ApiErrorData>>,
props?: Omit<UseQueryOptions<Array<FolderListItem>, AxiosError<ApiErrorData>>, 'queryKey' | 'queryFn'>,
): UseQueryResult<Array<FolderListItem>, AxiosError<ApiErrorData>> {
return useQuery<Array<FolderListItem>, AxiosError<ApiErrorData>>({
queryFn: foldersService.getFolders,
Expand Down
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@
"@open-webui-react-native/mobile/shared/features/use-audio-recorder": [
"libs/mobile/shared/features/use-audio-recorder/src/index.ts"
],
"@open-webui-react-native/mobile/shared/features/use-folders-enabled": [
"libs/mobile/shared/features/use-folders-enabled/src/index.ts"
],
"@open-webui-react-native/mobile/shared/features/use-dictate-mode": [
"libs/mobile/shared/features/use-dictate-mode/src/index.ts"
],
Expand Down
Loading