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
48 changes: 12 additions & 36 deletions src/frontend/features/assistants/AssistantListScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BotIcon, CheckIcon, PlusIcon } from '@cherrystudio/app-icons';
import type { Assistant } from '@cherrystudio/universal/data/types/assistant';
import { Stack, useRouter } from 'expo-router';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'expo-router';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { type AccessibilityActionEvent, Pressable, ScrollView, Text, View } from 'react-native';
import { Pressable as GesturePressable } from 'react-native-gesture-handler';
Expand All @@ -15,20 +15,19 @@ import {
useMessageListBottomInset,
} from '@/frontend/components/messageTabs';
import { SelectionToolbar } from '@/frontend/components/messageTabs/SelectionToolbar/SelectionToolbar';
import {
ContextMenuLink,
type ContextMenuLinkItem,
useSetBottomTabBarHidden,
} from '@/frontend/components/navigation';
import { ContextMenuLink, type ContextMenuLinkItem } from '@/frontend/components/navigation';
import { useAssistantMutations, useAssistantsApi } from '@/frontend/hooks/chat';

import { AssistantListSearchBar } from './AssistantListSearchBar/AssistantListSearchBar';
import { useAssistantListEditing } from './useAssistantListEditing/useAssistantListEditing';

export default function AssistantListScreen() {
const { t } = useTranslation();
const router = useRouter();
const { assistants, isLoading } = useAssistantsApi();
const { deleteAssistant, deleteAssistants } = useAssistantMutations();
const { alert } = useAlert();
const setBottomTabBarHidden = useSetBottomTabBarHidden();
const updatePlatformEditing = useAssistantListEditing();
const bottomInset = useMessageListBottomInset();
const [isEditing, setIsEditing] = useState(false);
const [pendingDeletionIds, setPendingDeletionIds] = useState<ReadonlySet<string>>(
Expand Down Expand Up @@ -59,32 +58,20 @@ export default function AssistantListScreen() {
);
}, [searchText, visibleAssistants]);

useEffect(() => {
if (process.env.EXPO_OS !== 'android') {
return;
}

return () => setBottomTabBarHidden(false);
}, [setBottomTabBarHidden]);

const enterEditing = useCallback(() => {
if (isBatchDeleting) {
return;
}

setSearchText('');
setIsEditing(true);
if (process.env.EXPO_OS === 'android') {
setBottomTabBarHidden(true);
}
}, [isBatchDeleting, setBottomTabBarHidden]);
updatePlatformEditing(true);
}, [isBatchDeleting, updatePlatformEditing]);
const exitEditing = useCallback(() => {
setIsEditing(false);
setSelectedIds(new Set());
if (process.env.EXPO_OS === 'android') {
setBottomTabBarHidden(false);
}
}, [setBottomTabBarHidden]);
updatePlatformEditing(false);
}, [updatePlatformEditing]);
const toggleAssistant = useCallback((assistantId: string) => {
setSelectedIds((current) => toggleSelection(current, assistantId));
}, []);
Expand Down Expand Up @@ -188,18 +175,7 @@ export default function AssistantListScreen() {
rightActions={isEditing ? undefined : rightActions}
title={t('assistant.list.title')}
/>
{process.env.EXPO_OS === 'ios' && !isEditing ? (
<Stack.SearchBar
autoCapitalize="none"
hideNavigationBar={false}
hideWhenScrolling={false}
obscureBackground={false}
placeholder={t('navigation.search')}
placement="stacked"
onCancelButtonPress={() => setSearchText('')}
onChangeText={(event) => setSearchText(event.nativeEvent.text)}
/>
) : null}
<AssistantListSearchBar isEditing={isEditing} setSearchText={setSearchText} />
<ScrollView
alwaysBounceVertical={false}
className="flex-1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { AssistantListSearchBarProps } from './AssistantListSearchBar.types';

export function AssistantListSearchBar(_props: AssistantListSearchBarProps) {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Stack } from 'expo-router';
import { useTranslation } from 'react-i18next';

import type { AssistantListSearchBarProps } from './AssistantListSearchBar.types';

export function AssistantListSearchBar({ isEditing, setSearchText }: AssistantListSearchBarProps) {
const { t } = useTranslation();

return isEditing ? null : (
<Stack.SearchBar
autoCapitalize="none"
hideNavigationBar={false}
hideWhenScrolling={false}
obscureBackground={false}
placeholder={t('navigation.search')}
placement="stacked"
onCancelButtonPress={() => setSearchText('')}
onChangeText={(event) => setSearchText(event.nativeEvent.text)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AssistantListSearchBar } from './AssistantListSearchBar.android';
export type { AssistantListSearchBarProps } from './AssistantListSearchBar.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type AssistantListSearchBarProps = {
isEditing: boolean;
setSearchText: (value: string) => void;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useCallback, useEffect } from 'react';

import { useSetBottomTabBarHidden } from '@/frontend/components/navigation';

export function useAssistantListEditing() {
const setBottomTabBarHidden = useSetBottomTabBarHidden();

useEffect(() => () => setBottomTabBarHidden(false), [setBottomTabBarHidden]);

return useCallback(
(isEditing: boolean) => setBottomTabBarHidden(isEditing),
[setBottomTabBarHidden],
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const updateAssistantListEditing = (_isEditing: boolean) => {};

export function useAssistantListEditing() {
return updateAssistantListEditing;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useAssistantListEditing } from './useAssistantListEditing.android';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FolderOpenIcon, Trash2Icon } from '@cherrystudio/app-icons';
import { Section } from '@cherrystudio/ui/components';
import * as FileSystem from 'expo-file-system/legacy';
import { ActivityAction, startActivityAsync } from 'expo-intent-launcher';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';

import type { DataDirectorySectionProps } from './DataDirectorySection.types';

const ANDROID_GRANT_READ_URI_PERMISSION_FLAG = 1;

export function DataDirectorySection({ onClearCache }: DataDirectorySectionProps) {
const { t } = useTranslation();
const handleAppDataPress = useCallback(() => {
if (!FileSystem.documentDirectory) {
return;
}

FileSystem.getContentUriAsync(FileSystem.documentDirectory)
.then((contentUri) =>
startActivityAsync('android.intent.action.VIEW', {
data: contentUri,
flags: ANDROID_GRANT_READ_URI_PERMISSION_FLAG,
type: 'resource/folder',
}),
)
.catch(() => {
void startActivityAsync(ActivityAction.APP_STORAGE_SETTINGS).catch(() => undefined);
});
}, []);

return (
<Section title={t('settings.data.directory.title')}>
<Section.Item
label={t('settings.data.appData.title')}
leading={<FolderOpenIcon className="size-5 text-foreground" />}
onPress={handleAppDataPress}
showChevron={false}
/>
<Section.Item
label={t('settings.data.clearCache.title')}
leading={<Trash2Icon className="size-5 text-foreground" />}
onPress={onClearCache}
showChevron={false}
/>
</Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Trash2Icon } from '@cherrystudio/app-icons';
import { Section } from '@cherrystudio/ui/components';
import { useTranslation } from 'react-i18next';

import type { DataDirectorySectionProps } from './DataDirectorySection.types';

export function DataDirectorySection({ onClearCache }: DataDirectorySectionProps) {
const { t } = useTranslation();

return (
<Section title={t('settings.data.directory.title')}>
<Section.Item
label={t('settings.data.clearCache.title')}
leading={<Trash2Icon className="size-5 text-foreground" />}
onPress={onClearCache}
showChevron={false}
/>
</Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DataDirectorySection } from './DataDirectorySection.android';
export type { DataDirectorySectionProps } from './DataDirectorySection.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type DataDirectorySectionProps = {
onClearCache: () => void;
};
68 changes: 3 additions & 65 deletions src/frontend/features/settings/DataScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import {
DownloadIcon,
FolderOpenIcon,
MonitorCloudIcon,
RefreshCwIcon,
Trash2Icon,
UploadIcon,
} from '@cherrystudio/app-icons';
import { DownloadIcon, MonitorCloudIcon, RefreshCwIcon, UploadIcon } from '@cherrystudio/app-icons';
import { Section } from '@cherrystudio/ui/components';
import * as FileSystem from 'expo-file-system/legacy';
import { ActivityAction, startActivityAsync } from 'expo-intent-launcher';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, View } from 'react-native';

import { BackHeader } from '@/frontend/components/headers';
import { isAndroid } from '@/frontend/utils/constants';

const ANDROID_GRANT_READ_URI_PERMISSION_FLAG = 1;
import { DataDirectorySection } from './DataDirectorySection/DataDirectorySection';

export default function DataSettingsScreen() {
const { t } = useTranslation();
Expand All @@ -25,48 +15,6 @@ export default function DataSettingsScreen() {
// UI placeholder only. Wire real data actions when those services are ready.
}, []);

const handleAppDataPress = useCallback(() => {
if (!isAndroid || !FileSystem.documentDirectory) {
return;
}

FileSystem.getContentUriAsync(FileSystem.documentDirectory)
.then((contentUri) =>
startActivityAsync('android.intent.action.VIEW', {
data: contentUri,
flags: ANDROID_GRANT_READ_URI_PERMISSION_FLAG,
type: 'resource/folder',
}),
)
.catch(() => {
void startActivityAsync(ActivityAction.APP_STORAGE_SETTINGS).catch(() => undefined);
});
}, []);

const directoryItems = isAndroid
? [
{
hideAccessory: true,
icon: FolderOpenIcon,
title: t('settings.data.appData.title'),
onPress: handleAppDataPress,
},
{
hideAccessory: true,
icon: Trash2Icon,
title: t('settings.data.clearCache.title'),
onPress: handleActionPress,
},
]
: [
{
hideAccessory: true,
icon: Trash2Icon,
title: t('settings.data.clearCache.title'),
onPress: handleActionPress,
},
];

return (
<>
<BackHeader title={t('settings.pages.data.title')} />
Expand Down Expand Up @@ -97,17 +45,7 @@ export default function DataSettingsScreen() {
showChevron={false}
/>
</Section>
<Section title={t('settings.data.directory.title')}>
{directoryItems.map(({ icon: Icon, onPress, title }) => (
<Section.Item
key={title}
label={title}
leading={<Icon className="size-5 text-foreground" />}
onPress={onPress}
showChevron={false}
/>
))}
</Section>
<DataDirectorySection onClearCache={handleActionPress} />
<Section>
<Section.Item
label={t('settings.data.resetData.title')}
Expand Down
17 changes: 5 additions & 12 deletions src/frontend/features/settings/ModelSettingSelectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
useModelSettingSelections,
} from '@/frontend/components/modelPicker';

import { ProviderModelSearchField } from './ProviderScreen/models/components/ProviderModelSearchField';
import { ProviderModelSearchControls } from './ProviderScreen/models/components/ProviderModelSearchControls/ProviderModelSearchControls';
import { ProviderModelTypeFilterBar } from './ProviderScreen/models/components/ProviderModelTypeFilterBar';
import {
getProviderModelTypeCounts,
Expand Down Expand Up @@ -81,24 +81,17 @@ function ModelSettingPicker({ kind }: { kind: ModelSettingKind }) {
return (
<>
<BackHeader title={t(MODEL_SETTING_KIND_TITLE_KEYS[kind])} />
{process.env.EXPO_OS === 'ios' ? (
<ProviderModelSearchField searchText={searchText} setSearchText={setSearchText} />
) : null}
{/* Pinned above the list rather than scrolling away with it: this is every
model on the device in one column, and a filter you have to scroll back
up to reach is one you stop reaching for. iOS pads nothing at the top —
its search field belongs to the navigation bar, which leaves a gap of
its own below itself. */}
<View className={process.env.EXPO_OS === 'ios' ? 'gap-3 px-4 pb-3' : 'gap-3 px-4 py-3'}>
{process.env.EXPO_OS === 'ios' ? null : (
<ProviderModelSearchField searchText={searchText} setSearchText={setSearchText} />
)}
up to reach is one you stop reaching for. The platform controls own
whether search sits in the navigation bar or in this content row. */}
<ProviderModelSearchControls searchText={searchText} setSearchText={setSearchText}>
<ProviderModelTypeFilterBar
counts={typeCounts}
selectedFilter={typeFilter}
onSelect={setTypeFilter}
/>
</View>
</ProviderModelSearchControls>
<View className="flex-1" style={{ paddingBottom: insets.bottom }}>
<ModelPickerList
emptyText={t('settings.provider.models.search.empty')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
CalendarIcon,
HeartPulseIcon,
MapPinIcon,
type AppIconProps,
} from '@cherrystudio/app-icons';
import type { ComponentType } from 'react';

import type { PermissionKind } from '../permissionConfig';

export const visiblePermissionKinds = [
'location',
'calendar',
'health',
] as const satisfies readonly PermissionKind[];

const permissionIcons: Record<PermissionKind, ComponentType<AppIconProps> | undefined> = {
calendar: CalendarIcon,
health: HeartPulseIcon,
location: MapPinIcon,
reminders: undefined,
};

export function PermissionListLeading({ kind }: { kind: PermissionKind }) {
const Icon = permissionIcons[kind];
return Icon ? <Icon className="size-5 text-foreground" /> : null;
}
Loading
Loading