diff --git a/app/actions/local/file.ts b/app/actions/local/file.ts index bbe95d916..649ef9666 100644 --- a/app/actions/local/file.ts +++ b/app/actions/local/file.ts @@ -33,3 +33,4 @@ export const updateLocalFilePath = async (serverUrl: string, fileId: string, loc return {error}; } }; + diff --git a/app/components/menu_item/index.tsx b/app/components/menu_item/index.tsx index 239027bd0..8f34bffb0 100644 --- a/app/components/menu_item/index.tsx +++ b/app/components/menu_item/index.tsx @@ -10,27 +10,6 @@ import TouchableWithFeedback from '@components/touchable_with_feedback'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export const ITEM_HEIGHT = 50; - -export type MenuItemProps = { - centered?: boolean; - defaultMessage?: string; - i18nId?: string; - iconContainerStyle?: StyleProp; - iconName?: string; - containerStyle?: StyleProp; - isDestructor?: boolean; - labelComponent?: ReactNode; - leftComponent?: ReactNode; - messageValues?: Record; - onPress: () => void; - separator?: boolean; - showArrow?: boolean; - testID: string; - theme: Theme; - labelStyle?: StyleProp; - isLink?: boolean; -}; - const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { container: { @@ -83,25 +62,31 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); +export type MenuItemProps = { + centered?: boolean; + containerStyle?: StyleProp; + defaultMessage?: string; + i18nId?: string; + iconContainerStyle?: StyleProp; + iconName?: string; + isDestructor?: boolean; + isLink?: boolean; + labelComponent?: ReactNode; + labelStyle?: StyleProp; + leftComponent?: ReactNode; + messageValues?: Record; + onPress: () => void; + rightComponent?: ReactNode; + separator?: boolean; + showArrow?: boolean; + testID: string; + theme: Theme; +}; const MenuItem = (props: MenuItemProps) => { const { - centered, - defaultMessage = '', - i18nId, - iconContainerStyle, - iconName, - containerStyle, - isDestructor = false, - isLink = false, - labelComponent, - labelStyle, - leftComponent, - messageValues, - onPress, - separator = true, - showArrow = false, - testID, - theme, + centered, containerStyle, defaultMessage = '', i18nId, iconContainerStyle, iconName, isDestructor = false, + isLink = false, labelComponent, labelStyle, leftComponent, messageValues, onPress, rightComponent, + separator = true, showArrow = false, testID, theme, } = props; const style = getStyleSheet(theme); @@ -165,6 +150,7 @@ const MenuItem = (props: MenuItemProps) => { style={style.chevron} /> )} + {rightComponent} {Boolean(separator) && ()} diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index 7e131c18f..041d32629 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -10,7 +10,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { - action: (value: string | boolean) => void; + action?: (value: string | boolean) => void; description?: string; inline?: boolean; destructive?: boolean; @@ -29,6 +29,7 @@ const OptionType = { DEFAULT: 'default', TOGGLE: 'toggle', SELECT: 'select', + NONE: 'none', } as const; type OptionType = typeof OptionType[keyof typeof OptionType]; @@ -157,7 +158,7 @@ const OptionItem = ({ } const onPress = useCallback(() => { - action(value || ''); + action?.(value || ''); }, [value, action]); const component = ( diff --git a/app/constants/screens.ts b/app/constants/screens.ts index dba5ad729..de537b977 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -41,6 +41,7 @@ export const SEARCH = 'Search'; export const SELECT_TEAM = 'SelectTeam'; export const SERVER = 'Server'; export const SETTINGS = 'Settings'; +export const SETTINGS_ADVANCED = 'SettingsAdvanced'; export const SETTINGS_DISPLAY = 'SettingsDisplay'; export const SETTINGS_DISPLAY_CLOCK = 'SettingsDisplayClock'; export const SETTINGS_DISPLAY_THEME = 'SettingsDisplayTheme'; @@ -99,6 +100,7 @@ export default { SELECT_TEAM, SERVER, SETTINGS, + SETTINGS_ADVANCED, SETTINGS_DISPLAY, SETTINGS_DISPLAY_CLOCK, SETTINGS_DISPLAY_THEME, diff --git a/app/screens/index.tsx b/app/screens/index.tsx index f31bd722a..fd859f323 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -151,6 +151,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { case Screens.SETTINGS: screen = withServerDatabase(require('@screens/settings').default); break; + case Screens.SETTINGS_ADVANCED: + screen = withServerDatabase(require('@screens/settings/advanced').default); + break; case Screens.SETTINGS_DISPLAY: screen = withServerDatabase(require('@screens/settings/display').default); break; diff --git a/app/screens/settings/advanced/index.tsx b/app/screens/settings/advanced/index.tsx new file mode 100644 index 000000000..f56158d29 --- /dev/null +++ b/app/screens/settings/advanced/index.tsx @@ -0,0 +1,110 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useEffect, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {View, TouchableOpacity} from 'react-native'; +import {Edge, SafeAreaView} from 'react-native-safe-area-context'; + +import OptionItem from '@components/option_item'; +import {useTheme} from '@context/theme'; +import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; +import {popTopScreen} from '@screens/navigation'; +import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import type {ReadDirItem} from 'react-native-fs'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + screen: { + flex: 1, + backgroundColor: theme.centerChannelBg, + }, + body: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), + flex: 1, + paddingTop: 35, + }, + itemStyle: { + backgroundColor: theme.centerChannelBg, + paddingHorizontal: 8, + }, + }; +}); + +const EMPTY_FILES: ReadDirItem[] = []; +const EMPTY_SERVERS: string[] = []; +const EDGES: Edge[] = ['left', 'right']; + +type AdvancedSettingsProps = { + componentId: string; +} +const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { + const theme = useTheme(); + const intl = useIntl(); + const [dataSize, setDataSize] = useState(0); + const [files, setFiles] = useState(EMPTY_FILES); + const [serverUrls, setServerUrls] = useState(EMPTY_SERVERS); + const styles = getStyleSheet(theme); + + const getAllCachedFiles = async () => { + const {totalSize, files: cachedFiles, serverUrls: allServerUrls} = await getAllFilesInCachesDirectory(); + setDataSize(totalSize); + setFiles(cachedFiles || EMPTY_FILES); + setServerUrls(allServerUrls || EMPTY_SERVERS); + }; + + const onPressDeleteData = preventDoubleTap(async () => { + try { + if (files.length > 0) { + const deletePromises = []; + for (const server of serverUrls) { + deletePromises.push(deleteFileCache(server)); + } + await Promise.all(deletePromises); + } + await getAllCachedFiles(); + } catch (e) { + //todo: show toast if error https://mattermost.atlassian.net/browse/MM-44926 + } + }); + + useEffect(() => { + getAllCachedFiles(); + }, []); + + const close = () => popTopScreen(componentId); + useAndroidHardwareBackHandler(componentId, close); + + const disabled = Boolean(dataSize && (dataSize > 0)); + + return ( + + + + + + + + ); +}; + +export default AdvancedSettings; diff --git a/app/screens/settings/display_clock/display_clock.tsx b/app/screens/settings/display_clock/display_clock.tsx index 19b1d00a6..66c88913f 100644 --- a/app/screens/settings/display_clock/display_clock.tsx +++ b/app/screens/settings/display_clock/display_clock.tsx @@ -104,12 +104,6 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ setButtons(componentId, buttons); }, [componentId, saveButton, isMilitaryTimeFormat]); - useEffect(() => { - setButtons(componentId, { - rightButtons: [saveButton], - }); - }, []); - useAndroidHardwareBackHandler(componentId, close); useNavButtonPressed(SAVE_CLOCK_BUTTON_ID, componentId, saveClockDisplayPreference, [isMilitaryTimeFormat]); diff --git a/app/screens/settings/index.tsx b/app/screens/settings/index.tsx index bc87a39c9..be37c6c31 100644 --- a/app/screens/settings/index.tsx +++ b/app/screens/settings/index.tsx @@ -15,11 +15,13 @@ import type {WithDatabaseArgs} from '@typings/database/database'; const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { const siteName = observeConfigValue(database, 'SiteName'); - const showHelp = observeConfigValue(database, 'HelpLink').pipe(switchMap((link) => of$(link ? isValidUrl(link) : false))); + const helpLink = observeConfigValue(database, 'HelpLink'); + const showHelp = helpLink.pipe(switchMap((link) => of$(link ? isValidUrl(link) : false))); return { showHelp, siteName, + helpLink, }; }); diff --git a/app/screens/settings/settings.tsx b/app/screens/settings/settings.tsx index df8265db1..749f2ba9c 100644 --- a/app/screens/settings/settings.tsx +++ b/app/screens/settings/settings.tsx @@ -16,6 +16,7 @@ import {dismissModal, goToScreen, setButtons} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import {tryOpenURL} from '@utils/url'; import SettingOption from './setting_option'; @@ -62,13 +63,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { type SettingsProps = { componentId: string; - siteName: string; + helpLink: string; showHelp: boolean; + siteName: string; } //todo: handle display on tablet and Profile the whole feature - https://mattermost.atlassian.net/browse/MM-39711 -const Settings = ({componentId, showHelp, siteName}: SettingsProps) => { +const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) => { const theme = useTheme(); const intl = useIntl(); const styles = getStyleSheet(theme); @@ -97,12 +99,6 @@ const Settings = ({componentId, showHelp, siteName}: SettingsProps) => { useNavButtonPressed(CLOSE_BUTTON_ID, componentId, close, []); - const onPressHandler = () => { - return Alert.alert( - 'The functionality you are trying to use has not yet been implemented.', - ); - }; - const goToNotifications = preventDoubleTap(() => { const screen = Screens.SETTINGS_NOTIFICATION; const title = intl.formatMessage({id: 'settings.notifications', defaultMessage: 'Notifications'}); @@ -124,6 +120,28 @@ const Settings = ({componentId, showHelp, siteName}: SettingsProps) => { goToScreen(screen, title); }); + const goToAdvancedSettings = preventDoubleTap(() => { + const screen = Screens.SETTINGS_ADVANCED; + const title = intl.formatMessage({id: 'settings.advanced_settings', defaultMessage: 'Advanced Settings'}); + + goToScreen(screen, title); + }); + + const openHelp = preventDoubleTap(() => { + const link = helpLink ? helpLink.toLowerCase() : ''; + + if (link) { + const onError = () => { + Alert.alert( + intl.formatMessage({id: 'mobile.link.error.title', defaultMessage: 'Error'}), + intl.formatMessage({id: 'mobile.link.error.text', defaultMessage: 'Unable to open the link.'}), + ); + }; + + tryOpenURL(link, onError); + } + }); + let middleDividerStyle = styles.divider; if (Platform.OS === 'ios') { middleDividerStyle = styles.middleDivider; @@ -153,7 +171,7 @@ const Settings = ({componentId, showHelp, siteName}: SettingsProps) => { /> { {showHelp && diff --git a/app/utils/file/index.ts b/app/utils/file/index.ts index b8d633d5e..516a49908 100644 --- a/app/utils/file/index.ts +++ b/app/utils/file/index.ts @@ -14,6 +14,8 @@ import {Asset} from 'react-native-image-picker'; import Permissions, {PERMISSIONS} from 'react-native-permissions'; import {Files} from '@constants'; +import DatabaseManager from '@database/manager'; +import {queryAllServers} from '@queries/app/servers'; import {generateId} from '@utils/general'; import keyMirror from '@utils/key_mirror'; import {logError} from '@utils/log'; @@ -499,3 +501,34 @@ export const hasWriteStoragePermission = async (intl: IntlShape) => { default: return true; } }; + +export const getAllFilesInCachesDirectory = async () => { + try { + const appDatabase = DatabaseManager.appDatabase; + const servers = await queryAllServers(appDatabase!.database); + if (!servers.length) { + return {error: 'No servers'}; + } + + const serverUrls = []; + const files: FileSystem.ReadDirItem[][] = []; + + for await (const server of servers) { + const directoryFiles = await FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${hashCode(server.url)}`); + files.push(directoryFiles); + serverUrls.push(server.url); + } + + const flattenedFiles = files.flat(); + const totalSize = flattenedFiles.reduce((acc, file) => acc + file.size, 0); + + return { + files: flattenedFiles, + totalSize, + serverUrls, + }; + } catch (error) { + logError('Failed getAllFilesInCachesDirectory', error); + return {error}; + } +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d56a4fa63..de4a1bbbc 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -706,6 +706,7 @@ "settings_display.timezone.manual": "Change timezone", "settings_display.timezone.select": "Select Timezone", "settings.about": "About {appTitle}", + "settings.advanced_settings": "Advanced Settings", "settings.display": "Display", "settings.display.militaryClock.save": "Save", "settings.notifications": "Notifications",