2024-10-26 13:10:01 -07:00
import * as React from 'react' ;
2024-08-02 06:51:49 -07:00
import { useMemo , useEffect , useCallback , useContext } from 'react' ;
2024-10-26 13:10:01 -07:00
import { Easing , Animated , TouchableOpacity , Text , StyleSheet , ScrollView , View , Image , ImageStyle } from 'react-native' ;
import { Dispatch } from 'redux' ;
import { connect } from 'react-redux' ;
const IonIcon = require ( 'react-native-vector-icons/Ionicons' ) . default ;
import Icon from './Icon' ;
2022-10-11 12:31:09 +01:00
import Folder from '@joplin/lib/models/Folder' ;
import Synchronizer from '@joplin/lib/Synchronizer' ;
import NavService from '@joplin/lib/services/NavService' ;
import { _ } from '@joplin/lib/locale' ;
2024-10-26 13:10:01 -07:00
import { themeStyle } from './global-style' ;
2024-07-04 05:56:57 -07:00
import { buildFolderTree , isFolderSelected , renderFolders } from '@joplin/lib/components/shared/side-menu-shared' ;
2024-03-02 14:25:27 +00:00
import { FolderEntity , FolderIcon , FolderIconType } from '@joplin/lib/services/database/types' ;
2022-10-11 12:31:09 +01:00
import { AppState } from '../utils/types' ;
2022-10-11 12:46:40 +01:00
import Setting from '@joplin/lib/models/Setting' ;
import { reg } from '@joplin/lib/registry' ;
2023-01-10 12:08:13 +00:00
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types' ;
2024-03-02 14:25:27 +00:00
import { getTrashFolderIcon , getTrashFolderId } from '@joplin/lib/services/trash' ;
import restoreItems from '@joplin/lib/services/trash/restoreItems' ;
2024-03-15 00:12:58 +05:30
import emptyTrash from '@joplin/lib/services/trash/emptyTrash' ;
2024-03-02 14:25:27 +00:00
import { ModelType } from '@joplin/lib/BaseModel' ;
2024-08-02 06:51:49 -07:00
import { DialogContext } from './DialogManager' ;
2024-10-26 13:10:01 -07:00
import { TextStyle , ViewStyle } from 'react-native' ;
import { StateDecryptionWorker , StateResourceFetcher } from '@joplin/lib/reducer' ;
2025-01-06 02:52:59 -08:00
import useOnLongPressProps from '../utils/hooks/useOnLongPressProps' ;
import { TouchableRipple } from 'react-native-paper' ;
import shim from '@joplin/lib/shim' ;
2024-03-02 14:25:27 +00:00
const { substrWithEllipsis } = require ( '@joplin/lib/string-utils' ) ;
2017-05-24 19:27:13 +00:00
2022-10-11 12:31:09 +01:00
interface Props {
syncStarted : boolean ;
themeId : number ;
2024-10-26 13:10:01 -07:00
dispatch : Dispatch ;
2022-10-11 12:31:09 +01:00
collapsedFolderIds : string [ ] ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2022-10-11 12:31:09 +01:00
syncReport : any ;
2024-10-26 13:10:01 -07:00
decryptionWorker : StateDecryptionWorker ;
resourceFetcher : StateResourceFetcher ;
2022-10-11 12:31:09 +01:00
syncOnlyOverWifi : boolean ;
isOnMobileData : boolean ;
notesParentType : string ;
folders : FolderEntity [ ] ;
2023-01-10 12:08:13 +00:00
profileConfig : ProfileConfig ;
2023-07-18 16:15:45 -03:00
inboxJopId : string ;
2024-01-09 16:22:06 +00:00
selectedFolderId : string ;
selectedTagId : string ;
2022-10-11 12:31:09 +01:00
}
2017-08-01 17:59:01 +00:00
2022-10-11 12:31:09 +01:00
const syncIconRotationValue = new Animated . Value ( 0 ) ;
2017-08-01 17:59:01 +00:00
2022-10-11 12:31:09 +01:00
const syncIconRotation = syncIconRotationValue . interpolate ( {
inputRange : [ 0 , 1 ] ,
outputRange : [ '0deg' , '360deg' ] ,
} ) ;
2017-08-01 17:59:01 +00:00
2022-10-12 16:03:51 +01:00
const folderIconRightMargin = 10 ;
2024-10-26 13:10:01 -07:00
let syncIconAnimation : Animated.CompositeAnimation | null = null ;
2022-10-11 12:31:09 +01:00
2025-01-06 02:52:59 -08:00
const useStyles = ( themeId : number ) = > {
return useMemo ( ( ) = > {
const theme = themeStyle ( themeId ) ;
2022-10-11 12:31:09 +01:00
2024-10-26 13:10:01 -07:00
const buttonStyle : ViewStyle = {
flex : 1 ,
flexDirection : 'row' ,
flexBasis : 'auto' ,
height : 36 ,
alignItems : 'center' ,
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
} ;
const buttonTextStyle : TextStyle = {
flex : 1 ,
color : theme.color ,
paddingLeft : 10 ,
fontSize : theme.fontSize ,
} ;
const sidebarIconStyle : TextStyle = {
fontSize : 22 ,
color : theme.color ,
width : 26 ,
textAlign : 'center' ,
textAlignVertical : 'center' ,
} ;
const folderIconBase : ViewStyle & ImageStyle = {
marginRight : folderIconRightMargin ,
width : 27 ,
} ;
const folderButtonStyle : ViewStyle = {
. . . buttonStyle ,
paddingLeft : 0 ,
} ;
const sideButtonStyle : ViewStyle = {
. . . buttonStyle ,
flex : 0 ,
} ;
2025-10-03 14:41:55 +01:00
const folderButtonTextStyle : ViewStyle = {
. . . buttonTextStyle ,
paddingLeft : 0 ,
} ;
2024-10-26 13:10:01 -07:00
const styles = StyleSheet . create ( {
2017-08-01 17:59:01 +00:00
menu : {
flex : 1 ,
2019-07-29 15:43:53 +02:00
backgroundColor : theme.backgroundColor ,
2017-08-01 17:59:01 +00:00
} ,
2024-10-26 13:10:01 -07:00
button : buttonStyle ,
buttonText : buttonTextStyle ,
2017-08-01 17:59:01 +00:00
syncStatus : {
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
color : theme.colorFaded ,
fontSize : theme.fontSizeSmaller ,
2019-06-26 18:05:37 +00:00
flex : 0 ,
2017-08-01 17:59:01 +00:00
} ,
2024-10-26 13:10:01 -07:00
sidebarIcon : sidebarIconStyle ,
folderButton : folderButtonStyle ,
2025-10-03 14:41:55 +01:00
folderButtonText : folderButtonTextStyle ,
conflictFolderButtonText : {
. . . folderButtonTextStyle ,
color : theme.colorError ,
2019-06-26 01:10:15 +01:00
} ,
2024-10-26 13:10:01 -07:00
folderButtonSelected : {
. . . folderButtonStyle ,
backgroundColor : theme.selectedColor ,
} ,
folderToggleIcon : {
. . . theme . icon ,
color : theme.colorFaded ,
paddingTop : 3 ,
} ,
sideButton : sideButtonStyle ,
sideButtonSelected : {
. . . sideButtonStyle ,
} ,
sideButtonText : {
. . . buttonTextStyle ,
} ,
folderBaseIcon : {
. . . sidebarIconStyle ,
. . . folderIconBase ,
} ,
folderEmojiIcon : {
. . . sidebarIconStyle ,
. . . folderIconBase ,
fontSize : theme.fontSize ,
} ,
folderImageIcon : {
. . . folderIconBase ,
height : 20 ,
resizeMode : 'contain' ,
} ,
} ) ;
2017-07-06 19:48:17 +00:00
2024-10-26 13:10:01 -07:00
return styles ;
2025-01-06 02:52:59 -08:00
} , [ themeId ] ) ;
} ;
type Styles = ReturnType < typeof useStyles > ;
type FolderEventHandler = ( folder : FolderEntity ) = > void ;
interface FolderItemProps {
themeId : number ;
hasChildren : boolean ;
collapsed : boolean ;
folder : FolderEntity ;
selected : boolean ;
depth : number ;
styles : Styles ;
alwaysShowFolderIcons : boolean ;
onPress : FolderEventHandler ;
onTogglePress : FolderEventHandler ;
onLongPress : FolderEventHandler ;
}
const FolderItem : React.FC < FolderItemProps > = props = > {
const styles = useMemo ( ( ) = > {
const theme = themeStyle ( props . themeId ) ;
return StyleSheet . create ( {
buttonWrapper : { flex : 1 , flexDirection : 'row' } ,
folderButton : {
flex : 1 ,
flexDirection : 'row' ,
flexBasis : 'auto' ,
height : 36 ,
alignItems : 'center' ,
paddingRight : theme.marginRight ,
backgroundColor : props.selected ? theme.selectedColor : undefined ,
paddingLeft : props.depth * 10 + theme . marginLeft ,
} ,
iconWrapper : {
paddingLeft : 10 ,
paddingRight : 10 ,
backgroundColor : props.selected ? theme.selectedColor : undefined ,
} ,
} ) ;
} , [ props . selected , props . depth , props . themeId ] ) ;
const baseStyles = props . styles ;
const collapsed = props . collapsed ;
const iconName = collapsed ? 'chevron-down' : 'chevron-up' ;
const iconComp = < IonIcon name = { iconName } style = { baseStyles . folderToggleIcon } / > ;
const onTogglePress = useCallback ( ( ) = > {
props . onTogglePress ( props . folder ) ;
} , [ props . folder , props . onTogglePress ] ) ;
const iconWrapper = ! props . hasChildren ? null : (
< TouchableOpacity
style = { styles . iconWrapper }
onPress = { onTogglePress }
accessibilityLabel = { _ ( 'Expand %s' , props . folder . title ) }
aria - pressed = { ! collapsed }
accessibilityState = { { checked : ! collapsed } }
// The togglebutton role is only supported on Android and iOS.
// On web, the button role with aria-pressed creates a togglebutton.
accessibilityRole = { shim . mobilePlatform ( ) === 'web' ? 'button' : 'togglebutton' }
>
{ iconComp }
< / TouchableOpacity >
) ;
const folderIcon = Folder . unserializeIcon ( props . folder . icon ) ;
const renderFolderIcon = ( folderId : string , folderIcon : FolderIcon ) = > {
if ( ! folderIcon ) {
if ( folderId === getTrashFolderId ( ) ) {
folderIcon = getTrashFolderIcon ( FolderIconType . FontAwesome ) ;
} else if ( props . alwaysShowFolderIcons ) {
return < IonIcon name = "folder-outline" style = { baseStyles . folderBaseIcon } / > ;
} else {
return null ;
}
}
if ( folderIcon . type === FolderIconType . Emoji ) {
return < Text style = { baseStyles . folderEmojiIcon } > { folderIcon . emoji } < / Text > ;
} else if ( folderIcon . type === FolderIconType . DataUrl ) {
return < Image style = { baseStyles . folderImageIcon } source = { { uri : folderIcon.dataUrl } } / > ;
} else if ( folderIcon . type === FolderIconType . FontAwesome ) {
return < Icon style = { baseStyles . folderBaseIcon } name = { folderIcon . name } accessibilityLabel = { null } / > ;
} else {
throw new Error ( ` Unsupported folder icon type: ${ folderIcon . type } ` ) ;
}
} ;
const onPress = useCallback ( ( ) = > {
props . onPress ( props . folder ) ;
} , [ props . folder , props . onPress ] ) ;
const onLongPress = useCallback ( ( ) = > {
props . onLongPress ( props . folder ) ;
} , [ props . folder , props . onLongPress ] ) ;
const longPressProps = useOnLongPressProps ( {
onLongPress ,
actionDescription : _ ( 'Show notebook options' ) ,
} ) ;
const folderTitle = Folder . displayTitle ( props . folder ) ;
// React Native doesn't seem to include an equivalent to web's aria-level.
// To allow screen reader users to determine whether a notebook is a subnotebook or not,
// depth is specified with an accessibilityLabel:
const folderDepthDescription = props . depth > 0 ? _ ( '(level %d)' , props . depth ) : '' ;
const accessibilityLabel = ` ${ folderTitle } ${ folderDepthDescription } ` . trim ( ) ;
2025-10-03 14:41:55 +01:00
const folderButtonTextStyle = props . folder . id === Folder . conflictFolderId ( ) ? baseStyles.conflictFolderButtonText : baseStyles.folderButtonText ;
2025-01-06 02:52:59 -08:00
return (
< View key = { props . folder . id } style = { styles . buttonWrapper } >
< TouchableRipple
style = { { flex : 1 , flexBasis : 'auto' } }
onPress = { onPress }
{ . . . longPressProps }
accessibilityHint = { _ ( 'Opens notebook' ) }
accessibilityState = { { selected : props.selected } }
aria - current = { props . selected }
role = 'button'
>
< View style = { styles . folderButton } >
{ renderFolderIcon ( props . folder . id , folderIcon ) }
< Text
numberOfLines = { 1 }
2025-10-03 14:41:55 +01:00
style = { folderButtonTextStyle }
2025-01-06 02:52:59 -08:00
accessibilityLabel = { accessibilityLabel }
>
{ folderTitle }
< / Text >
< / View >
< / TouchableRipple >
{ iconWrapper }
< / View >
) ;
} ;
const SideMenuContentComponent = ( props : Props ) = > {
const alwaysShowFolderIcons = useMemo ( ( ) = > Folder . shouldShowFolderIcons ( props . folders ) , [ props . folders ] ) ;
const styles_ = useStyles ( props . themeId ) ;
2022-10-11 12:31:09 +01:00
useEffect ( ( ) = > {
if ( props . syncStarted ) {
syncIconAnimation = Animated . loop (
Animated . timing ( syncIconRotationValue , {
toValue : 1 ,
duration : 3000 ,
easing : Easing.linear ,
2023-12-06 02:15:33 -08:00
useNativeDriver : false ,
2023-08-22 11:58:53 +01:00
} ) ,
2022-10-11 12:31:09 +01:00
) ;
syncIconAnimation . start ( ) ;
} else {
if ( syncIconAnimation ) syncIconAnimation . stop ( ) ;
syncIconAnimation = null ;
2019-07-29 15:43:53 +02:00
}
2022-10-11 12:31:09 +01:00
} , [ props . syncStarted ] ) ;
2019-07-12 18:07:47 +01:00
2025-01-06 02:52:59 -08:00
const dialogs = useContext ( DialogContext ) ;
2022-10-11 12:31:09 +01:00
const folder_press = ( folder : FolderEntity ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2019-07-11 18:44:26 +01:00
2022-10-11 12:31:09 +01:00
props . dispatch ( {
2019-07-11 18:44:26 +01:00
type : 'NAV_GO' ,
routeName : 'Notes' ,
folderId : folder.id ,
} ) ;
2022-10-11 12:31:09 +01:00
} ;
2017-05-24 19:27:13 +00:00
2022-11-17 16:34:16 +00:00
const folder_longPress = async ( folderOrAll : FolderEntity | string ) = > {
if ( folderOrAll === 'all' ) return ;
const folder = folderOrAll as FolderEntity ;
2019-06-28 00:51:02 +01:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-03-02 14:25:27 +00:00
const menuItems : any [ ] = [ ] ;
2024-03-15 00:12:58 +05:30
if ( folder && folder . id === getTrashFolderId ( ) ) {
menuItems . push ( {
text : _ ( 'Empty trash' ) ,
onPress : async ( ) = > {
2024-08-02 06:51:49 -07:00
dialogs . prompt ( '' , _ ( 'This will permanently delete all items in the trash. Continue?' ) , [
2024-03-15 00:12:58 +05:30
{
text : _ ( 'Empty trash' ) ,
onPress : async ( ) = > {
await emptyTrash ( ) ;
} ,
} ,
{
text : _ ( 'Cancel' ) ,
onPress : ( ) = > { } ,
style : 'cancel' ,
} ,
] ) ;
} ,
style : 'destructive' ,
} ) ;
} else if ( folder && ! ! folder . deleted_time ) {
2024-03-02 14:25:27 +00:00
menuItems . push ( {
text : _ ( 'Restore' ) ,
onPress : async ( ) = > {
await restoreItems ( ModelType . Folder , [ folder . id ] ) ;
} ,
style : 'destructive' ,
} ) ;
// Alert.alert(
// '',
// _('Notebook: %s', folder.title),
// [
// {
// text: _('Restore'),
// onPress: async () => {
// await restoreItems(ModelType.Folder, [folder.id]);
// },
// style: 'destructive',
// },
// {
// text: _('Cancel'),
// onPress: () => {},
// style: 'cancel',
// },
// ],
// {
// cancelable: false,
// },
// );
} else {
const generateFolderDeletion = ( ) = > {
const folderDeletion = ( message : string ) = > {
2024-08-02 06:51:49 -07:00
dialogs . prompt ( '' , message , [
2024-03-02 14:25:27 +00:00
{
text : _ ( 'OK' ) ,
onPress : ( ) = > {
2024-03-09 02:33:05 -08:00
void Folder . delete ( folder . id , { toTrash : true , sourceDescription : 'side-menu-content (long-press)' } ) ;
2024-03-02 14:25:27 +00:00
} ,
} ,
{
text : _ ( 'Cancel' ) ,
onPress : ( ) = > { } ,
style : 'cancel' ,
2023-07-18 16:15:45 -03:00
} ,
2024-03-02 14:25:27 +00:00
] ) ;
} ;
if ( folder . id === props . inboxJopId ) {
return folderDeletion (
_ ( 'Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.' ) ,
) ;
}
return folderDeletion ( _ ( 'Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.' , substrWithEllipsis ( folder . title , 0 , 32 ) ) ) ;
2023-07-18 16:15:45 -03:00
} ;
2024-03-02 14:25:27 +00:00
menuItems . push ( {
text : _ ( 'Edit' ) ,
onPress : ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Folder' ,
folderId : folder.id ,
} ) ;
} ,
} ) ;
menuItems . push ( {
text : _ ( 'Delete' ) ,
onPress : generateFolderDeletion ,
style : 'destructive' ,
} ) ;
}
menuItems . push ( {
text : _ ( 'Cancel' ) ,
onPress : ( ) = > { } ,
style : 'cancel' ,
} ) ;
2023-07-18 16:15:45 -03:00
2024-08-02 06:51:49 -07:00
dialogs . prompt (
2019-06-26 18:28:09 +01:00
'' ,
2019-07-29 15:43:53 +02:00
_ ( 'Notebook: %s' , folder . title ) ,
2024-03-02 14:25:27 +00:00
menuItems ,
2019-06-26 18:28:09 +01:00
) ;
2022-10-11 12:31:09 +01:00
} ;
2019-06-26 18:28:09 +01:00
2022-10-11 12:31:09 +01:00
const folder_togglePress = ( folder : FolderEntity ) = > {
props . dispatch ( {
2018-05-09 12:39:17 +01:00
type : 'FOLDER_TOGGLE' ,
id : folder.id ,
} ) ;
2022-10-11 12:31:09 +01:00
} ;
2018-05-09 12:39:17 +01:00
2022-10-11 12:31:09 +01:00
const tagButton_press = ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2017-07-25 19:36:52 +01:00
2022-10-11 12:31:09 +01:00
props . dispatch ( {
2018-03-09 20:59:12 +00:00
type : 'NAV_GO' ,
2019-03-13 22:42:16 +00:00
routeName : 'Tags' ,
2017-07-25 19:36:52 +01:00
} ) ;
2022-10-11 12:31:09 +01:00
} ;
2017-07-25 19:36:52 +01:00
2023-01-10 12:08:13 +00:00
const switchProfileButton_press = ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'ProfileSwitcher' ,
} ) ;
} ;
2022-10-11 12:31:09 +01:00
const configButton_press = ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
void NavService . go ( 'Config' ) ;
} ;
2019-06-26 18:05:37 +00:00
2022-10-11 12:31:09 +01:00
const allNotesButton_press = ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2019-07-11 18:44:26 +01:00
2022-10-11 12:31:09 +01:00
props . dispatch ( {
2019-07-11 18:44:26 +01:00
type : 'NAV_GO' ,
routeName : 'Notes' ,
smartFilterId : 'c3176726992c11e9ac940492261af972' ,
} ) ;
2022-10-11 12:31:09 +01:00
} ;
2019-07-11 18:44:26 +01:00
2022-10-11 12:31:09 +01:00
const newFolderButton_press = ( ) = > {
props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2019-06-26 01:10:15 +01:00
2022-10-11 12:31:09 +01:00
props . dispatch ( {
2019-06-26 01:10:15 +01:00
type : 'NAV_GO' ,
routeName : 'Folder' ,
folderId : null ,
} ) ;
2022-10-11 12:31:09 +01:00
} ;
2019-06-26 01:10:15 +01:00
2022-10-11 12:46:40 +01:00
const performSync = useCallback ( async ( ) = > {
const action = props . syncStarted ? 'cancel' : 'start' ;
if ( ! Setting . value ( 'sync.target' ) ) {
props . dispatch ( {
type : 'SIDE_MENU_CLOSE' ,
} ) ;
props . dispatch ( {
2025-10-01 01:34:18 -07:00
type : 'SYNC_WIZARD_VISIBLE_CHANGE' ,
visible : true ,
2022-10-11 12:46:40 +01:00
} ) ;
return 'init' ;
}
if ( ! ( await reg . syncTarget ( ) . isAuthenticated ( ) ) ) {
if ( reg . syncTarget ( ) . authRouteName ( ) ) {
props . dispatch ( {
type : 'NAV_GO' ,
routeName : reg.syncTarget ( ) . authRouteName ( ) ,
} ) ;
return 'auth' ;
}
reg . logger ( ) . error ( 'Not authenticated with sync target - please check your credentials.' ) ;
return 'error' ;
}
let sync = null ;
try {
sync = await reg . syncTarget ( ) . synchronizer ( ) ;
} catch ( error ) {
reg . logger ( ) . error ( 'Could not initialise synchroniser: ' ) ;
reg . logger ( ) . error ( error ) ;
error . message = ` Could not initialise synchroniser: ${ error . message } ` ;
props . dispatch ( {
type : 'SYNC_REPORT_UPDATE' ,
report : { errors : [ error ] } ,
} ) ;
return 'error' ;
}
if ( action === 'cancel' ) {
void sync . cancel ( ) ;
return 'cancel' ;
} else {
void reg . scheduleSync ( 0 ) ;
return 'sync' ;
}
} , [ props . syncStarted , props . dispatch ] ) ;
const synchronize_press = useCallback ( async ( ) = > {
const actionDone = await performSync ( ) ;
2022-10-11 12:31:09 +01:00
if ( actionDone === 'auth' ) props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2022-10-11 12:46:40 +01:00
} , [ performSync , props . dispatch ] ) ;
2017-07-06 19:48:17 +00:00
2022-02-06 16:42:00 +00:00
2024-04-25 07:31:18 -07:00
const renderFolderItem = ( folder : FolderEntity , hasChildren : boolean , depth : number ) = > {
2025-01-06 02:52:59 -08:00
return < FolderItem
key = { ` folder-item- ${ folder . id } ` }
themeId = { props . themeId }
hasChildren = { hasChildren }
depth = { depth }
collapsed = { props . collapsedFolderIds . includes ( folder . id ) }
selected = { isFolderSelected ( folder , { selectedFolderId : props.selectedFolderId , notesParentType : props.notesParentType } ) }
styles = { styles_ }
folder = { folder }
alwaysShowFolderIcons = { alwaysShowFolderIcons }
onPress = { folder_press }
onLongPress = { folder_longPress }
onTogglePress = { folder_togglePress }
/ > ;
2022-10-11 12:31:09 +01:00
} ;
2017-07-22 16:55:09 +01:00
2025-01-06 02:52:59 -08:00
type SidebarButtonOptions = {
onPress ? : ( ) = > void ;
selected? : boolean ;
isHeader? : boolean ;
} ;
const renderSidebarButton = (
key : string ,
title : string ,
iconName : string ,
{ onPress = null , selected = false , isHeader = false } : SidebarButtonOptions = { } ,
) = > {
2024-11-22 02:47:31 -08:00
let icon = < Icon name = { ` ionicon ${ iconName } ` } style = { styles_ . sidebarIcon } accessibilityLabel = { null } / > ;
2019-07-12 18:07:47 +01:00
if ( key === 'synchronize_button' ) {
2022-10-11 12:31:09 +01:00
icon = < Animated.View style = { { transform : [ { rotate : syncIconRotation } ] } } > { icon } < / Animated.View > ;
2019-07-12 18:07:47 +01:00
}
2019-07-11 18:44:26 +01:00
const content = (
2022-10-11 12:31:09 +01:00
< View key = { key } style = { selected ? styles_.sideButtonSelected : styles_.sideButton } >
2019-07-12 18:07:47 +01:00
{ icon }
2025-01-06 02:52:59 -08:00
< Text
style = { styles_ . sideButtonText }
accessibilityRole = { isHeader ? 'header' : undefined }
> { title } < / Text >
2019-07-11 18:44:26 +01:00
< / View >
) ;
2025-01-06 02:52:59 -08:00
if ( ! onPress ) return content ;
2019-07-11 18:44:26 +01:00
2017-07-22 16:55:09 +01:00
return (
2025-01-06 02:52:59 -08:00
< TouchableOpacity key = { key } onPress = { onPress } accessibilityRole = 'button' >
2019-07-11 18:44:26 +01:00
{ content }
2017-07-22 16:55:09 +01:00
< / TouchableOpacity >
) ;
2022-10-11 12:31:09 +01:00
} ;
2017-07-22 16:55:09 +01:00
2022-10-11 12:31:09 +01:00
const makeDivider = ( key : string ) = > {
const theme = themeStyle ( props . themeId ) ;
2025-01-06 02:52:59 -08:00
return < View role = 'separator' style = { { marginTop : 15 , marginBottom : 15 , flex : - 1 , borderBottomWidth : 1 , borderBottomColor : theme.dividerColor } } key = { key } > < / View > ;
2022-10-11 12:31:09 +01:00
} ;
2017-07-25 19:36:52 +01:00
2022-10-11 12:31:09 +01:00
const renderBottomPanel = ( ) = > {
const theme = themeStyle ( props . themeId ) ;
2017-10-30 18:27:51 +00:00
2019-06-26 18:05:37 +00:00
const items = [ ] ;
2017-05-24 19:27:13 +00:00
2022-10-11 12:31:09 +01:00
items . push ( makeDivider ( 'divider_1' ) ) ;
2017-07-28 17:57:01 +00:00
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'newFolder_button' , _ ( 'New Notebook' ) , 'folder-open' , { onPress : newFolderButton_press } ) ) ;
2019-06-26 01:10:15 +01:00
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'tag_button' , _ ( 'Tags' ) , 'pricetag' , { onPress : tagButton_press } ) ) ;
2017-07-06 19:48:17 +00:00
2023-01-10 12:08:13 +00:00
if ( props . profileConfig && props . profileConfig . profiles . length > 1 ) {
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'switchProfile_button' , _ ( 'Switch profile' ) , 'people-circle-outline' , { onPress : switchProfileButton_press } ) ) ;
2023-01-10 12:08:13 +00:00
}
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'config_button' , _ ( 'Configuration' ) , 'settings' , { onPress : configButton_press } ) ) ;
2019-06-26 18:05:37 +00:00
2022-10-11 12:31:09 +01:00
items . push ( makeDivider ( 'divider_2' ) ) ;
2019-06-26 18:05:37 +00:00
2022-10-11 12:31:09 +01:00
const lines = Synchronizer . reportToLines ( props . syncReport ) ;
2019-07-29 15:43:53 +02:00
const syncReportText = lines . join ( '\n' ) ;
2017-07-17 23:43:29 +01:00
2018-06-10 17:43:24 +01:00
let decryptionReportText = '' ;
2022-10-11 12:31:09 +01:00
if ( props . decryptionWorker && props . decryptionWorker . state !== 'idle' && props . decryptionWorker . itemCount ) {
decryptionReportText = _ ( 'Decrypting items: %d/%d' , props . decryptionWorker . itemIndex + 1 , props . decryptionWorker . itemCount ) ;
2018-06-10 17:43:24 +01:00
}
2018-11-13 22:27:58 +00:00
let resourceFetcherText = '' ;
2022-10-11 12:31:09 +01:00
if ( props . resourceFetcher && props . resourceFetcher . toFetchCount ) {
resourceFetcherText = _ ( 'Fetching resources: %d/%d' , props . resourceFetcher . fetchingCount , props . resourceFetcher . toFetchCount ) ;
2018-11-13 22:27:58 +00:00
}
2020-03-13 23:46:14 +00:00
const fullReport = [ ] ;
2018-06-10 17:43:24 +01:00
if ( syncReportText ) fullReport . push ( syncReportText ) ;
2019-05-22 15:56:07 +01:00
if ( resourceFetcherText ) fullReport . push ( resourceFetcherText ) ;
2018-06-10 17:43:24 +01:00
if ( decryptionReportText ) fullReport . push ( decryptionReportText ) ;
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'synchronize_button' , ! props . syncStarted ? _ ( 'Synchronise' ) : _ ( 'Cancel' ) , 'sync' , { onPress : synchronize_press } ) ) ;
2019-06-26 18:05:37 +00:00
2020-03-13 23:57:34 +00:00
if ( fullReport . length ) {
2019-07-29 15:43:53 +02:00
items . push (
2022-10-11 12:31:09 +01:00
< Text key = "sync_report" style = { styles_ . syncStatus } >
2019-07-29 15:43:53 +02:00
{ fullReport . join ( '\n' ) }
2023-08-22 11:58:53 +01:00
< / Text > ,
2019-07-29 15:43:53 +02:00
) ;
2020-03-13 23:57:34 +00:00
}
2019-06-26 18:05:37 +00:00
2022-10-11 12:31:09 +01:00
if ( props . syncOnlyOverWifi && props . isOnMobileData ) {
2021-03-29 10:35:39 +02:00
items . push (
2022-10-11 12:31:09 +01:00
< Text key = "net_info" style = { styles_ . syncStatus } >
2021-03-29 10:35:39 +02:00
{ _ ( 'Mobile data - auto-sync disabled' ) }
2023-08-22 11:58:53 +01:00
< / Text > ,
2021-03-29 10:35:39 +02:00
) ;
}
2024-08-02 06:51:49 -07:00
return < View style = { { flex : 0 , flexDirection : 'column' , flexBasis : 'auto' , paddingBottom : theme.marginBottom } } > { items } < / View > ;
2022-10-11 12:31:09 +01:00
} ;
2019-06-26 18:05:37 +00:00
2022-10-11 12:31:09 +01:00
let items = [ ] ;
2018-06-10 17:43:24 +01:00
2022-10-11 12:31:09 +01:00
const theme = themeStyle ( props . themeId ) ;
2017-07-22 16:55:09 +01:00
2022-10-11 12:31:09 +01:00
// HACK: inner height of ScrollView doesn't appear to be calculated correctly when
// using padding. So instead creating blank elements for padding bottom and top.
items . push ( < View style = { { height : theme.marginTop } } key = "bottom_top_hack" / > ) ;
2019-06-28 00:51:02 +01:00
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'all_notes' , _ ( 'All notes' ) , 'document' , {
onPress : allNotesButton_press ,
selected : props.notesParentType === 'SmartFilter' ,
} ) ) ;
2019-06-28 00:51:02 +01:00
2022-10-11 12:31:09 +01:00
items . push ( makeDivider ( 'divider_all' ) ) ;
2019-07-11 18:44:26 +01:00
2025-01-06 02:52:59 -08:00
items . push ( renderSidebarButton ( 'folder_header' , _ ( 'Notebooks' ) , 'folder' , {
isHeader : true ,
} ) ) ;
2017-07-06 19:48:17 +00:00
2024-07-04 05:56:57 -07:00
const folderTree = useMemo ( ( ) = > {
return buildFolderTree ( props . folders ) ;
} , [ props . folders ] ) ;
2022-10-11 12:31:09 +01:00
if ( props . folders . length ) {
2024-07-04 05:56:57 -07:00
const result = renderFolders ( {
folderTree ,
collapsedFolderIds : props.collapsedFolderIds ,
} , renderFolderItem ) ;
2022-10-11 12:31:09 +01:00
const folderItems = result . items ;
items = items . concat ( folderItems ) ;
}
2017-10-30 18:27:51 +00:00
2022-10-11 12:31:09 +01:00
const style = {
flex : 1 ,
borderRightWidth : 1 ,
borderRightColor : theme.dividerColor ,
backgroundColor : theme.backgroundColor ,
} ;
return (
2024-09-16 14:17:12 -07:00
< View style = { style } >
2024-10-26 13:10:01 -07:00
< View style = { { flex : 1 } } >
2022-10-11 12:31:09 +01:00
< ScrollView scrollsToTop = { false } style = { styles_ . menu } >
{ items }
< / ScrollView >
{ renderBottomPanel ( ) }
2017-07-22 16:55:09 +01:00
< / View >
2024-09-16 14:17:12 -07:00
< / View >
2022-10-11 12:31:09 +01:00
) ;
} ;
2019-07-29 15:43:53 +02:00
2022-10-11 12:31:09 +01:00
export default connect ( ( state : AppState ) = > {
2019-07-29 15:43:53 +02:00
return {
folders : state.folders ,
syncStarted : state.syncStarted ,
syncReport : state.syncReport ,
selectedFolderId : state.selectedFolderId ,
selectedTagId : state.selectedTagId ,
notesParentType : state.notesParentType ,
locale : state.settings.locale ,
2020-09-15 14:01:07 +01:00
themeId : state.settings.theme ,
2019-07-29 15:43:53 +02:00
collapsedFolderIds : state.collapsedFolderIds ,
decryptionWorker : state.decryptionWorker ,
resourceFetcher : state.resourceFetcher ,
2021-03-29 10:35:39 +02:00
isOnMobileData : state.isOnMobileData ,
syncOnlyOverWifi : state.settings [ 'sync.mobileWifiOnly' ] ,
2023-01-10 12:08:13 +00:00
profileConfig : state.profileConfig ,
2023-07-23 15:57:55 +01:00
inboxJopId : state.settings [ 'sync.10.inboxId' ] ,
2019-07-29 15:43:53 +02:00
} ;
} ) ( SideMenuContentComponent ) ;