mirror of
https://github.com/standardnotes/mobile.git
synced 2026-04-24 08:07:00 -04:00
* feat: show listed on mobile * fix: avoid endless loop when fetching listed items * refactor: render side menu items without checking item's index * feat: implement new blog addition * feat: ui improvements, show appropriate message when no action available for a blog * feat: refresh listed menu items when user executes blog-related action * feat: UI improvements - add icons, better distinction between Listed sections * feat: show loading indicators on async actions, near respective items * chore: update dependencies * fix: updates regarding last snjs changes * fix: remove unnecessary html tags from message
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { AppStateType } from '@Lib/application_state';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { ApplicationContext } from '@Root/ApplicationContext';
|
|
import { SCREEN_SETTINGS } from '@Screens/screens';
|
|
import { ContentType, SNTag, SNTheme } from '@standardnotes/snjs';
|
|
import {
|
|
CustomActionSheetOption,
|
|
useCustomActionSheet,
|
|
} from '@Style/custom_action_sheet';
|
|
import { ICON_BRUSH, ICON_SETTINGS } from '@Style/icons';
|
|
import {
|
|
MobileTheme,
|
|
ThemeService,
|
|
ThemeServiceContext,
|
|
} from '@Style/theme_service';
|
|
import React, {
|
|
Fragment,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import { Platform } from 'react-native';
|
|
import FAB from 'react-native-fab';
|
|
import { FlatList } from 'react-native-gesture-handler';
|
|
import DrawerLayout from 'react-native-gesture-handler/DrawerLayout';
|
|
import Icon from 'react-native-vector-icons/Ionicons';
|
|
import { ThemeContext } from 'styled-components/native';
|
|
import {
|
|
FirstSafeAreaView,
|
|
MainSafeAreaView,
|
|
useStyles,
|
|
} from './MainSideMenu.styled';
|
|
import { SideMenuHero } from './SideMenuHero';
|
|
import { SideMenuOptionIconDescriptionType, SideMenuOption, SideMenuSection } from './SideMenuSection';
|
|
import { TagSelectionList } from './TagSelectionList';
|
|
|
|
type Props = {
|
|
drawerRef: DrawerLayout | null;
|
|
};
|
|
|
|
export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
|
|
// Context
|
|
const theme = useContext(ThemeContext);
|
|
const themeService = useContext(ThemeServiceContext);
|
|
const application = useContext(ApplicationContext);
|
|
const navigation = useNavigation();
|
|
const { showActionSheet } = useCustomActionSheet();
|
|
// State
|
|
const [selectedTag, setSelectedTag] = useState(() =>
|
|
application!.getAppState().getSelectedTag()
|
|
);
|
|
const [themes, setThemes] = useState<SNTheme[]>([]);
|
|
const styles = useStyles(theme);
|
|
|
|
useEffect(() => {
|
|
const removeTagChangeObserver = application!
|
|
.getAppState()
|
|
.addStateChangeObserver(state => {
|
|
if (state === AppStateType.TagChanged) {
|
|
setSelectedTag(application!.getAppState().getSelectedTag());
|
|
}
|
|
});
|
|
return removeTagChangeObserver;
|
|
});
|
|
|
|
const onSystemThemeSelect = useCallback(
|
|
async (selectedTheme: MobileTheme) => {
|
|
themeService?.activateSystemTheme(selectedTheme.uuid);
|
|
},
|
|
[themeService]
|
|
);
|
|
|
|
const onThemeSelect = useCallback(
|
|
async (selectedTheme: SNTheme) => {
|
|
themeService?.activateExternalTheme(selectedTheme);
|
|
},
|
|
[themeService]
|
|
);
|
|
|
|
const onThemeLongPress = useCallback(
|
|
async (themeId: string, name: string, snTheme?: SNTheme) => {
|
|
const options: CustomActionSheetOption[] = [];
|
|
/**
|
|
* If this theme is a mobile theme, allow it to be set as the preferred
|
|
* option for light/dark mode.
|
|
*/
|
|
if ((snTheme && !snTheme.getNotAvailOnMobile()) || !snTheme) {
|
|
const activeLightTheme = await themeService?.getThemeForMode('light');
|
|
const lightThemeAction =
|
|
activeLightTheme === themeId ? 'Current' : 'Set as';
|
|
const lightName = ThemeService.doesDeviceSupportDarkMode()
|
|
? 'Light'
|
|
: 'Active';
|
|
const text = `${lightThemeAction} ${lightName} Theme`;
|
|
options.push({
|
|
text,
|
|
callback: () => {
|
|
if (snTheme) {
|
|
themeService?.assignExternalThemeForMode(snTheme, 'light');
|
|
} else {
|
|
themeService?.assignThemeForMode(themeId, 'light');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
/**
|
|
* Only display a dark mode option if this device supports dark mode.
|
|
*/
|
|
if (ThemeService.doesDeviceSupportDarkMode()) {
|
|
const activeDarkTheme = await themeService?.getThemeForMode('dark');
|
|
const darkThemeAction =
|
|
activeDarkTheme === themeId ? 'Current' : 'Set as';
|
|
const text = `${darkThemeAction} Dark Theme`;
|
|
options.push({
|
|
text,
|
|
callback: () => {
|
|
if (snTheme) {
|
|
themeService?.assignExternalThemeForMode(snTheme, 'dark');
|
|
} else {
|
|
themeService?.assignThemeForMode(themeId, 'dark');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
/**
|
|
* System themes cannot be redownloaded.
|
|
*/
|
|
if (snTheme) {
|
|
options.push({
|
|
text: 'Redownload',
|
|
callback: async () => {
|
|
const confirmed = await application?.alertService.confirm(
|
|
'Themes are cached when downloaded. To retrieve the latest version, press Redownload.',
|
|
'Redownload Theme',
|
|
'Redownload'
|
|
);
|
|
if (confirmed) {
|
|
themeService?.downloadThemeAndReload(snTheme);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
showActionSheet(name, options);
|
|
},
|
|
[application?.alertService, showActionSheet, themeService]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const unsubscribeStreamThemes = application?.streamItems(
|
|
ContentType.Theme,
|
|
() => {
|
|
const newItems = application
|
|
.getItems(ContentType.Theme)
|
|
.filter(el => !el.errorDecrypting && !el.deleted);
|
|
setThemes(newItems as SNTheme[]);
|
|
}
|
|
);
|
|
|
|
return unsubscribeStreamThemes;
|
|
}, [application]);
|
|
|
|
const iconDescriptorForTheme = (currentTheme: SNTheme | MobileTheme) => {
|
|
const desc = {
|
|
type: SideMenuOptionIconDescriptionType.Circle,
|
|
side: 'right' as 'right',
|
|
};
|
|
|
|
const dockIcon =
|
|
currentTheme.package_info && currentTheme.package_info.dock_icon;
|
|
|
|
if (dockIcon && dockIcon.type === 'circle') {
|
|
Object.assign(desc, {
|
|
backgroundColor: dockIcon.background_color,
|
|
borderColor: dockIcon.border_color,
|
|
});
|
|
} else {
|
|
Object.assign(desc, {
|
|
backgroundColor: theme.stylekitInfoColor,
|
|
borderColor: theme.stylekitInfoColor,
|
|
});
|
|
}
|
|
|
|
return desc;
|
|
};
|
|
|
|
const themeOptions = useMemo(() => {
|
|
const options: SideMenuOption[] = themeService!
|
|
.systemThemes()
|
|
.map(systemTheme => ({
|
|
text: systemTheme?.name,
|
|
key: systemTheme?.uuid,
|
|
iconDesc: iconDescriptorForTheme(systemTheme),
|
|
dimmed: false,
|
|
onSelect: () => onSystemThemeSelect(systemTheme),
|
|
onLongPress: () =>
|
|
onThemeLongPress(systemTheme?.uuid, systemTheme?.name),
|
|
selected: themeService!.activeThemeId === systemTheme?.uuid,
|
|
}))
|
|
.concat(
|
|
themes
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.map(mapTheme => ({
|
|
text: mapTheme.name,
|
|
key: mapTheme.uuid,
|
|
iconDesc: iconDescriptorForTheme(mapTheme),
|
|
dimmed: mapTheme.getNotAvailOnMobile(),
|
|
onSelect: () => onThemeSelect(mapTheme),
|
|
onLongPress: () =>
|
|
onThemeLongPress(mapTheme?.uuid, mapTheme?.name, mapTheme),
|
|
selected: themeService!.activeThemeId === mapTheme.uuid,
|
|
}))
|
|
);
|
|
|
|
if (options.length === themeService!.systemThemes().length) {
|
|
options.push({
|
|
text: 'Get More Themes',
|
|
key: 'get-theme',
|
|
iconDesc: {
|
|
type: SideMenuOptionIconDescriptionType.Icon,
|
|
name: ThemeService.nameForIcon(ICON_BRUSH),
|
|
side: 'right',
|
|
size: 17,
|
|
},
|
|
onSelect: () => {
|
|
application?.deviceInterface?.openUrl(
|
|
'https://standardnotes.com/plans'
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
return options;
|
|
// We want to also track activeThemeId
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
themeService,
|
|
themeService?.activeThemeId,
|
|
themes,
|
|
onSystemThemeSelect,
|
|
onThemeSelect,
|
|
]);
|
|
|
|
const onTagSelect = useCallback(
|
|
async (tag: SNTag) => {
|
|
if (tag.conflictOf) {
|
|
application!.changeAndSaveItem(tag.uuid, mutator => {
|
|
mutator.conflictOf = undefined;
|
|
});
|
|
}
|
|
application?.getAppState().setSelectedTag(tag, true);
|
|
drawerRef?.closeDrawer();
|
|
},
|
|
[application, drawerRef]
|
|
);
|
|
|
|
const openSettings = () => {
|
|
drawerRef?.closeDrawer();
|
|
navigation?.navigate(SCREEN_SETTINGS);
|
|
};
|
|
|
|
const outOfSyncPressed = async () => {
|
|
const confirmed = await application!.alertService!.confirm(
|
|
"We've detected that the data in the current application session may " +
|
|
'not match the data on the server. This can happen due to poor ' +
|
|
'network conditions, or if a large note fails to download on your ' +
|
|
'device. To resolve this issue, we recommend first creating a backup ' +
|
|
'of your data in the Settings screen, then signing out of your account ' +
|
|
'and signing back in.',
|
|
'Potentially Out of Sync',
|
|
'Open Settings',
|
|
undefined
|
|
);
|
|
if (confirmed) {
|
|
openSettings();
|
|
}
|
|
};
|
|
|
|
const selectedTags = useMemo(() => (selectedTag ? [selectedTag] : []), [
|
|
selectedTag,
|
|
]);
|
|
|
|
return (
|
|
<Fragment>
|
|
<FirstSafeAreaView />
|
|
<MainSafeAreaView>
|
|
<SideMenuHero
|
|
testID="settingsButton"
|
|
onPress={openSettings}
|
|
onOutOfSyncPress={outOfSyncPressed}
|
|
/>
|
|
<FlatList
|
|
style={styles.sections}
|
|
data={['themes-section', 'views-section', 'tags-section'].map(
|
|
key => ({
|
|
key,
|
|
themeOptions,
|
|
onTagSelect,
|
|
selectedTags,
|
|
})
|
|
)}
|
|
renderItem={({ item, index }) => {
|
|
return index === 0 ? (
|
|
<SideMenuSection
|
|
title="Themes"
|
|
options={item.themeOptions}
|
|
collapsed={true}
|
|
/>
|
|
) : index === 1 ? (
|
|
<SideMenuSection title="Views">
|
|
<TagSelectionList
|
|
contentType={ContentType.SmartTag}
|
|
onTagSelect={item.onTagSelect}
|
|
selectedTags={item.selectedTags}
|
|
/>
|
|
</SideMenuSection>
|
|
) : index === 2 ? (
|
|
<SideMenuSection title="Tags">
|
|
<TagSelectionList
|
|
hasBottomPadding={Platform.OS === 'android'}
|
|
emptyPlaceholder={
|
|
'No tags. Create one from the note composer.'
|
|
}
|
|
contentType={ContentType.Tag}
|
|
onTagSelect={item.onTagSelect}
|
|
selectedTags={item.selectedTags}
|
|
/>
|
|
</SideMenuSection>
|
|
) : null;
|
|
}}
|
|
/>
|
|
<FAB
|
|
buttonColor={theme.stylekitInfoColor}
|
|
iconTextColor={theme.stylekitInfoContrastColor}
|
|
onClickAction={openSettings}
|
|
visible={true}
|
|
size={29}
|
|
iconTextComponent={
|
|
<Icon name={ThemeService.nameForIcon(ICON_SETTINGS)} />
|
|
}
|
|
/>
|
|
</MainSafeAreaView>
|
|
</Fragment>
|
|
);
|
|
});
|