feat: mobile app package (#1075)
This commit is contained in:
79
packages/mobile/src/Screens/Notes/NoteCell.styled.ts
Normal file
79
packages/mobile/src/Screens/Notes/NoteCell.styled.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { hexToRGBA } from '@Style/Utils'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const TouchableContainer = styled.TouchableWithoutFeedback``
|
||||
export const Container = styled.View<{ selected: boolean; distance: number }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: ${props => props.distance}px 0 0 ${props => props.distance}px;
|
||||
background-color: ${({ theme, selected }) => {
|
||||
return selected ? theme.stylekitInfoColor : theme.stylekitBackgroundColor
|
||||
}};
|
||||
`
|
||||
export const NoteDataContainer = styled.View<{ distance: number }>`
|
||||
border-bottom-color: ${({ theme }) => hexToRGBA(theme.stylekitBorderColor, 0.75)};
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: ${props => props.distance}px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
padding-right: ${props => props.distance}px;
|
||||
`
|
||||
export const DeletedText = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitInfoColor};
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
export const NoteText = styled.Text<{ selected: boolean }>`
|
||||
font-size: 15px;
|
||||
color: ${({ theme, selected }) => {
|
||||
return selected ? theme.stylekitInfoContrastColor : theme.stylekitForegroundColor
|
||||
}};
|
||||
opacity: 0.8;
|
||||
line-height: 19px;
|
||||
`
|
||||
export const TitleText = styled.Text<{ selected: boolean }>`
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: ${({ theme, selected }) => {
|
||||
return selected ? theme.stylekitInfoContrastColor : theme.stylekitForegroundColor
|
||||
}};
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-bottom: 4px;
|
||||
`
|
||||
export const TagsContainter = styled.View`
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
margin-top: 7px;
|
||||
`
|
||||
export const TagText = styled.Text<{ selected: boolean }>`
|
||||
margin-right: 2px;
|
||||
font-size: 12px;
|
||||
color: ${({ theme, selected }) => {
|
||||
return selected ? theme.stylekitInfoContrastColor : theme.stylekitForegroundColor
|
||||
}};
|
||||
opacity: ${props => (props.selected ? 0.8 : 0.5)};
|
||||
`
|
||||
export const DetailsText = styled(TagText)`
|
||||
margin-right: 0;
|
||||
margin-top: 5px;
|
||||
`
|
||||
export const FlexContainer = styled.View`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
`
|
||||
export const NoteContentsContainer = styled.View`
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
`
|
||||
export const styles = StyleSheet.create({
|
||||
editorIcon: {
|
||||
marginTop: 2,
|
||||
marginRight: 10,
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
})
|
||||
241
packages/mobile/src/Screens/Notes/NoteCell.tsx
Normal file
241
packages/mobile/src/Screens/Notes/NoteCell.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { useChangeNote, useDeleteNoteWithPrivileges, useProtectOrUnprotectNote } from '@Lib/SnjsHelperHooks'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { SnIcon } from '@Root/Components/SnIcon'
|
||||
import { NoteCellIconFlags } from '@Root/Screens/Notes/NoteCellIconFlags'
|
||||
import { CollectionSort, CollectionSortProperty, IconType, isNullOrUndefined, SNNote } from '@standardnotes/snjs'
|
||||
import { CustomActionSheetOption, useCustomActionSheet } from '@Style/CustomActionSheet'
|
||||
import { getTintColorForEditor } from '@Style/Utils'
|
||||
import React, { useContext, useRef, useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import {
|
||||
Container,
|
||||
DetailsText,
|
||||
FlexContainer,
|
||||
NoteContentsContainer,
|
||||
NoteDataContainer,
|
||||
NoteText,
|
||||
styles,
|
||||
TitleText,
|
||||
TouchableContainer,
|
||||
} from './NoteCell.styled'
|
||||
import { NoteCellFlags } from './NoteCellFlags'
|
||||
|
||||
type Props = {
|
||||
note: SNNote
|
||||
highlighted?: boolean
|
||||
onPressItem: (noteUuid: SNNote['uuid']) => void
|
||||
hideDates: boolean
|
||||
hidePreviews: boolean
|
||||
hideEditorIcon: boolean
|
||||
sortType: CollectionSortProperty
|
||||
}
|
||||
|
||||
export const NoteCell = ({
|
||||
note,
|
||||
onPressItem,
|
||||
highlighted,
|
||||
sortType,
|
||||
hideDates,
|
||||
hidePreviews,
|
||||
hideEditorIcon,
|
||||
}: Props) => {
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [changeNote] = useChangeNote(note)
|
||||
const [protectOrUnprotectNote] = useProtectOrUnprotectNote(note)
|
||||
|
||||
// State
|
||||
const [selected, setSelected] = useState(false)
|
||||
|
||||
// Ref
|
||||
const selectionTimeout = useRef<ReturnType<typeof setTimeout>>()
|
||||
const elementRef = useRef<View>(null)
|
||||
|
||||
const { showActionSheet } = useCustomActionSheet()
|
||||
|
||||
const [deleteNote] = useDeleteNoteWithPrivileges(
|
||||
note,
|
||||
async () => {
|
||||
await application?.mutator.deleteItem(note)
|
||||
},
|
||||
() => {
|
||||
void changeNote(mutator => {
|
||||
mutator.trashed = true
|
||||
}, false)
|
||||
},
|
||||
undefined,
|
||||
)
|
||||
|
||||
const highlight = Boolean(selected || highlighted)
|
||||
|
||||
const _onPress = () => {
|
||||
setSelected(true)
|
||||
selectionTimeout.current = setTimeout(() => {
|
||||
setSelected(false)
|
||||
onPressItem(note.uuid)
|
||||
}, 25)
|
||||
}
|
||||
|
||||
const _onPressIn = () => {
|
||||
setSelected(true)
|
||||
}
|
||||
|
||||
const _onPressOut = () => {
|
||||
setSelected(false)
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (note.protected) {
|
||||
showActionSheet({
|
||||
title: note.title,
|
||||
options: [
|
||||
{
|
||||
text: 'Note Protected',
|
||||
},
|
||||
],
|
||||
anchor: elementRef.current ?? undefined,
|
||||
})
|
||||
} else {
|
||||
let options: CustomActionSheetOption[] = []
|
||||
|
||||
options.push({
|
||||
text: note.pinned ? 'Unpin' : 'Pin',
|
||||
key: 'pin',
|
||||
callback: () =>
|
||||
changeNote(mutator => {
|
||||
mutator.pinned = !note.pinned
|
||||
}, false),
|
||||
})
|
||||
|
||||
options.push({
|
||||
text: note.archived ? 'Unarchive' : 'Archive',
|
||||
key: 'archive',
|
||||
callback: () => {
|
||||
if (note.locked) {
|
||||
void application?.alertService.alert(
|
||||
`This note has editing disabled. If you'd like to ${
|
||||
note.archived ? 'unarchive' : 'archive'
|
||||
} it, enable editing on it, and try again.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
void changeNote(mutator => {
|
||||
mutator.archived = !note.archived
|
||||
}, false)
|
||||
},
|
||||
})
|
||||
|
||||
options.push({
|
||||
text: note.locked ? 'Enable editing' : 'Prevent editing',
|
||||
key: 'lock',
|
||||
callback: () =>
|
||||
changeNote(mutator => {
|
||||
mutator.locked = !note.locked
|
||||
}, false),
|
||||
})
|
||||
|
||||
options.push({
|
||||
text: note.protected ? 'Unprotect' : 'Protect',
|
||||
key: 'protect',
|
||||
callback: async () => await protectOrUnprotectNote(),
|
||||
})
|
||||
|
||||
if (!note.trashed) {
|
||||
options.push({
|
||||
text: 'Move to Trash',
|
||||
key: 'trash',
|
||||
destructive: true,
|
||||
callback: async () => deleteNote(false),
|
||||
})
|
||||
} else {
|
||||
options = options.concat([
|
||||
{
|
||||
text: 'Restore',
|
||||
key: 'restore-note',
|
||||
callback: () => {
|
||||
void changeNote(mutator => {
|
||||
mutator.trashed = false
|
||||
}, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Delete permanently',
|
||||
key: 'delete-forever',
|
||||
destructive: true,
|
||||
callback: async () => deleteNote(true),
|
||||
},
|
||||
])
|
||||
}
|
||||
showActionSheet({
|
||||
title: note.title,
|
||||
options,
|
||||
anchor: elementRef.current ?? undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const padding = 14
|
||||
const showPreview = !hidePreviews && !note.protected && !note.hidePreview
|
||||
const hasPlainPreview = !isNullOrUndefined(note.preview_plain) && note.preview_plain.length > 0
|
||||
const showDetails = !hideDates || note.protected
|
||||
|
||||
const editorForNote = application?.componentManager.editorForNote(note)
|
||||
const [icon, tint] = application?.iconsController.getIconAndTintForNoteType(
|
||||
editorForNote?.package_info.note_type,
|
||||
) as [IconType, number]
|
||||
|
||||
return (
|
||||
<TouchableContainer
|
||||
onPress={_onPress}
|
||||
onPressIn={_onPressIn}
|
||||
onPressOut={_onPressOut}
|
||||
onLongPress={onLongPress}
|
||||
delayPressIn={150}
|
||||
>
|
||||
<Container ref={elementRef as any} selected={highlight} distance={padding}>
|
||||
{!hideEditorIcon && <SnIcon type={icon} fill={getTintColorForEditor(theme, tint)} style={styles.editorIcon} />}
|
||||
<NoteDataContainer distance={padding}>
|
||||
<NoteCellFlags note={note} highlight={highlight} />
|
||||
|
||||
<FlexContainer>
|
||||
<NoteContentsContainer>
|
||||
{note.title.length > 0 ? <TitleText selected={highlight}>{note.title}</TitleText> : <View />}
|
||||
{hasPlainPreview && showPreview && (
|
||||
<NoteText selected={highlight} numberOfLines={2}>
|
||||
{note.preview_plain}
|
||||
</NoteText>
|
||||
)}
|
||||
|
||||
{!hasPlainPreview && showPreview && note.text.length > 0 && (
|
||||
<NoteText selected={highlight} numberOfLines={2}>
|
||||
{note.text}
|
||||
</NoteText>
|
||||
)}
|
||||
</NoteContentsContainer>
|
||||
<NoteCellIconFlags note={note} />
|
||||
</FlexContainer>
|
||||
|
||||
{showDetails && (
|
||||
<DetailsText numberOfLines={1} selected={highlight}>
|
||||
{note.protected && (
|
||||
<Text>
|
||||
Protected
|
||||
{!hideDates && ' • '}
|
||||
</Text>
|
||||
)}
|
||||
{!hideDates && (
|
||||
<Text>
|
||||
{sortType === CollectionSort.UpdatedAt ? 'Modified ' + note.updatedAtString : note.createdAtString}
|
||||
</Text>
|
||||
)}
|
||||
</DetailsText>
|
||||
)}
|
||||
</NoteDataContainer>
|
||||
</Container>
|
||||
</TouchableContainer>
|
||||
)
|
||||
}
|
||||
56
packages/mobile/src/Screens/Notes/NoteCellFlags.tsx
Normal file
56
packages/mobile/src/Screens/Notes/NoteCellFlags.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import React, { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
type NoteFlag = {
|
||||
text: string
|
||||
color: string
|
||||
}
|
||||
|
||||
const FlagsContainer = styled.View`
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
const FlagContainer = styled.View<{ color: string; selected: boolean }>`
|
||||
background-color: ${({ theme, selected, color }) => {
|
||||
return selected ? theme.stylekitInfoContrastColor : color
|
||||
}};
|
||||
padding: 4px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 4px;
|
||||
`
|
||||
const FlagLabel = styled.Text<{ selected: boolean }>`
|
||||
color: ${({ theme, selected }) => {
|
||||
return selected ? theme.stylekitInfoColor : theme.stylekitInfoContrastColor
|
||||
}};
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
export const NoteCellFlags = ({ note, highlight }: { note: SNNote; highlight: boolean }) => {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const flags: NoteFlag[] = []
|
||||
|
||||
if (note.conflictOf) {
|
||||
flags.push({
|
||||
text: 'Conflicted Copy',
|
||||
color: theme.stylekitDangerColor,
|
||||
})
|
||||
}
|
||||
|
||||
return flags.length > 0 ? (
|
||||
<FlagsContainer>
|
||||
{flags.map(flag => (
|
||||
<FlagContainer key={flag.text.concat(flag.color)} color={flag.color} selected={highlight}>
|
||||
<FlagLabel selected={highlight}>{flag.text}</FlagLabel>
|
||||
</FlagContainer>
|
||||
))}
|
||||
</FlagsContainer>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
59
packages/mobile/src/Screens/Notes/NoteCellIconFlags.tsx
Normal file
59
packages/mobile/src/Screens/Notes/NoteCellIconFlags.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { SnIcon } from '@Root/Components/SnIcon'
|
||||
import { IconType, SNNote } from '@standardnotes/snjs'
|
||||
import React, { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
const FlagIconsContainer = styled.View`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-top: 2px;
|
||||
`
|
||||
type Props = {
|
||||
note: SNNote
|
||||
}
|
||||
|
||||
type TFlagIcon = {
|
||||
icon: IconType
|
||||
fillColor?: string
|
||||
}
|
||||
|
||||
export const NoteCellIconFlags = ({ note }: Props) => {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { stylekitCorn, stylekitDangerColor, stylekitInfoColor } = theme
|
||||
|
||||
const flagIcons = [] as TFlagIcon[]
|
||||
|
||||
if (note.archived) {
|
||||
flagIcons.push({
|
||||
icon: 'archive',
|
||||
fillColor: stylekitCorn,
|
||||
})
|
||||
}
|
||||
if (note.locked) {
|
||||
flagIcons.push({
|
||||
icon: 'pencil-off',
|
||||
fillColor: stylekitInfoColor,
|
||||
})
|
||||
}
|
||||
if (note.trashed) {
|
||||
flagIcons.push({
|
||||
icon: 'trash-filled',
|
||||
fillColor: stylekitDangerColor,
|
||||
})
|
||||
}
|
||||
if (note.pinned) {
|
||||
flagIcons.push({
|
||||
icon: 'pin-filled',
|
||||
fillColor: stylekitInfoColor,
|
||||
})
|
||||
}
|
||||
return flagIcons.length ? (
|
||||
<FlagIconsContainer>
|
||||
{flagIcons.map((flagIcon, index) => (
|
||||
<SnIcon key={index} type={flagIcon.icon} fill={flagIcon.fillColor} />
|
||||
))}
|
||||
</FlagIconsContainer>
|
||||
) : null
|
||||
}
|
||||
60
packages/mobile/src/Screens/Notes/NoteList.styled.ts
Normal file
60
packages/mobile/src/Screens/Notes/NoteList.styled.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Platform, StyleSheet } from 'react-native'
|
||||
import styled, { css } from 'styled-components/native'
|
||||
|
||||
// no support for generic types in Flatlist
|
||||
export const styles = StyleSheet.create({
|
||||
list: {
|
||||
height: '100%',
|
||||
},
|
||||
inputStyle: {
|
||||
height: 30,
|
||||
},
|
||||
androidSearch: {
|
||||
height: 30,
|
||||
},
|
||||
})
|
||||
|
||||
export const Container = styled.View`
|
||||
background-color: ${props => props.theme.stylekitBackgroundColor};
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
export const LoadingContainer = styled.View`
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
interface LoadingTextProps {
|
||||
textAlign?: 'left' | 'center' | 'right' | 'justify'
|
||||
}
|
||||
|
||||
export const LoadingText = styled.Text<LoadingTextProps>`
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
color: ${props => props.theme.stylekitForegroundColor};
|
||||
text-align: ${props => props.textAlign ?? 'left'};
|
||||
`
|
||||
|
||||
export const HeaderContainer = styled.View`
|
||||
padding-top: 3px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
`
|
||||
|
||||
export const SearchBarContainer = styled.View`
|
||||
background-color: ${props => props.theme.stylekitBackgroundColor};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
export const SearchOptionsContainer = styled.ScrollView`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 12px;
|
||||
${() =>
|
||||
Platform.OS === 'android' &&
|
||||
css`
|
||||
padding-top: 4px;
|
||||
`}
|
||||
`
|
||||
257
packages/mobile/src/Screens/Notes/NoteList.tsx
Normal file
257
packages/mobile/src/Screens/Notes/NoteList.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import { AppStateEventType, AppStateType } from '@Lib/ApplicationState'
|
||||
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { AppStackNavigationProp } from '@Root/AppStack'
|
||||
import { Chip } from '@Root/Components/Chip'
|
||||
import { SearchBar } from '@Root/Components/SearchBar'
|
||||
import { SCREEN_NOTES } from '@Root/Screens/screens'
|
||||
import { CollectionSortProperty, SNNote } from '@standardnotes/snjs'
|
||||
import React, { Dispatch, SetStateAction, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { Animated, FlatList, ListRenderItem, RefreshControl } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import IosSearchBar from 'react-native-search-bar'
|
||||
import AndroidSearchBar from 'react-native-search-box'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { NoteCell } from './NoteCell'
|
||||
import {
|
||||
Container,
|
||||
HeaderContainer,
|
||||
LoadingContainer,
|
||||
LoadingText,
|
||||
SearchBarContainer,
|
||||
SearchOptionsContainer,
|
||||
styles,
|
||||
} from './NoteList.styled'
|
||||
import { OfflineBanner } from './OfflineBanner'
|
||||
|
||||
type Props = {
|
||||
onSearchChange: (text: string) => void
|
||||
onSearchCancel: () => void
|
||||
searchText: string
|
||||
searchOptions: {
|
||||
selected: boolean
|
||||
onPress: () => void
|
||||
label: string
|
||||
}[]
|
||||
onPressItem: (noteUuid: SNNote['uuid']) => void
|
||||
selectedNoteId: string | undefined
|
||||
sortType: CollectionSortProperty
|
||||
hideDates: boolean
|
||||
hidePreviews: boolean
|
||||
hideEditorIcon: boolean
|
||||
decrypting: boolean
|
||||
loading: boolean
|
||||
hasRefreshControl: boolean
|
||||
notes: SNNote[]
|
||||
refreshing: boolean
|
||||
onRefresh: () => void
|
||||
shouldFocusSearch: boolean
|
||||
setShouldFocusSearch: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const NoteList = (props: Props) => {
|
||||
// Context
|
||||
const [signedIn] = useSignedIn()
|
||||
const application = useContext(ApplicationContext)
|
||||
const theme = useContext(ThemeContext)
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
const [collapseSearchBarOnBlur, setCollapseSearchBarOnBlur] = useState(true)
|
||||
const [noteListScrolled, setNoteListScrolled] = useState(false)
|
||||
|
||||
// Ref
|
||||
const opacityAnimationValue = useRef(new Animated.Value(0)).current
|
||||
const marginTopAnimationValue = useRef(new Animated.Value(-40)).current
|
||||
const iosSearchBarInputRef = useRef<IosSearchBar>(null)
|
||||
const androidSearchBarInputRef = useRef<typeof AndroidSearchBar>(null)
|
||||
const noteListRef = useRef<FlatList>(null)
|
||||
|
||||
const navigation = useNavigation<AppStackNavigationProp<typeof SCREEN_NOTES>['navigation']>()
|
||||
|
||||
const dismissKeyboard = () => {
|
||||
iosSearchBarInputRef.current?.blur()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const removeBlurScreenListener = navigation.addListener('blur', () => {
|
||||
setCollapseSearchBarOnBlur(false)
|
||||
})
|
||||
|
||||
return removeBlurScreenListener
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeStateEventObserver = application?.getAppState().addStateEventObserver(state => {
|
||||
if (state === AppStateEventType.DrawerOpen) {
|
||||
dismissKeyboard()
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribeStateEventObserver
|
||||
}, [application])
|
||||
|
||||
const scrollListToTop = useCallback(() => {
|
||||
if (noteListScrolled && props.notes && props.notes.length > 0) {
|
||||
noteListRef.current?.scrollToIndex({ animated: false, index: 0 })
|
||||
setNoteListScrolled(false)
|
||||
}
|
||||
}, [noteListScrolled, props.notes])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeTagChangedEventObserver = application?.getAppState().addStateChangeObserver(event => {
|
||||
if (event === AppStateType.TagChanged) {
|
||||
scrollListToTop()
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribeTagChangedEventObserver
|
||||
}, [application, scrollListToTop])
|
||||
|
||||
const { shouldFocusSearch, searchText } = props
|
||||
|
||||
const focusSearch = useCallback(() => {
|
||||
setCollapseSearchBarOnBlur(true)
|
||||
|
||||
if (shouldFocusSearch) {
|
||||
iosSearchBarInputRef.current?.focus()
|
||||
androidSearchBarInputRef.current?.focus(searchText)
|
||||
}
|
||||
}, [shouldFocusSearch, searchText])
|
||||
|
||||
useFocusEffect(focusSearch)
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return dismissKeyboard
|
||||
}, []),
|
||||
)
|
||||
|
||||
const onChangeSearchText = (text: string) => {
|
||||
props.onSearchChange(text)
|
||||
scrollListToTop()
|
||||
}
|
||||
|
||||
const toggleSearchOptions = (showOptions: boolean) => {
|
||||
Animated.parallel([
|
||||
Animated.timing(opacityAnimationValue, {
|
||||
toValue: showOptions ? 1 : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(marginTopAnimationValue, {
|
||||
toValue: showOptions ? 0 : -40,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]).start()
|
||||
}
|
||||
|
||||
const onSearchFocus = () => {
|
||||
toggleSearchOptions(true)
|
||||
props.setShouldFocusSearch(false)
|
||||
}
|
||||
|
||||
const onSearchBlur = () => {
|
||||
toggleSearchOptions(false)
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
setNoteListScrolled(true)
|
||||
}
|
||||
|
||||
const renderItem: ListRenderItem<SNNote> | null | undefined = ({ item }) => {
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<NoteCell
|
||||
note={item}
|
||||
onPressItem={props.onPressItem}
|
||||
sortType={props.sortType}
|
||||
hideDates={props.hideDates}
|
||||
hidePreviews={props.hidePreviews}
|
||||
hideEditorIcon={props.hideEditorIcon}
|
||||
highlighted={item.uuid === props.selectedNoteId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
let placeholderText = ''
|
||||
if (props.decrypting) {
|
||||
placeholderText = 'Decrypting notes...'
|
||||
} else if (props.loading) {
|
||||
placeholderText = 'Loading notes...'
|
||||
} else if (props.notes.length === 0) {
|
||||
placeholderText = 'No notes.'
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<SearchBarContainer>
|
||||
<SearchBar
|
||||
onChangeText={onChangeSearchText}
|
||||
onSearchCancel={props.onSearchCancel}
|
||||
onSearchFocusCallback={onSearchFocus}
|
||||
onSearchBlurCallback={onSearchBlur}
|
||||
iosSearchBarInputRef={iosSearchBarInputRef}
|
||||
androidSearchBarInputRef={androidSearchBarInputRef}
|
||||
collapseSearchBarOnBlur={collapseSearchBarOnBlur}
|
||||
/>
|
||||
</SearchBarContainer>
|
||||
<SearchOptionsContainer
|
||||
as={Animated.ScrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
style={{
|
||||
opacity: opacityAnimationValue,
|
||||
marginTop: marginTopAnimationValue,
|
||||
}}
|
||||
>
|
||||
{props.searchOptions.map(({ selected, onPress, label }, index) => (
|
||||
<Chip
|
||||
key={label}
|
||||
selected={selected}
|
||||
onPress={onPress}
|
||||
label={label}
|
||||
last={index === props.searchOptions.length - 1}
|
||||
/>
|
||||
))}
|
||||
</SearchOptionsContainer>
|
||||
</HeaderContainer>
|
||||
<FlatList
|
||||
ref={noteListRef}
|
||||
style={styles.list}
|
||||
keyExtractor={item => item?.uuid}
|
||||
contentContainerStyle={[{ paddingBottom: insets.bottom }, props.notes.length > 0 ? {} : { height: '100%' }]}
|
||||
initialNumToRender={6}
|
||||
windowSize={6}
|
||||
maxToRenderPerBatch={6}
|
||||
ListEmptyComponent={() => {
|
||||
return placeholderText.length > 0 ? (
|
||||
<LoadingContainer>
|
||||
<LoadingText>{placeholderText}</LoadingText>
|
||||
</LoadingContainer>
|
||||
) : null
|
||||
}}
|
||||
keyboardDismissMode={'interactive'}
|
||||
keyboardShouldPersistTaps={'never'}
|
||||
refreshControl={
|
||||
!props.hasRefreshControl ? undefined : (
|
||||
<RefreshControl
|
||||
tintColor={theme.stylekitContrastForegroundColor}
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
/>
|
||||
)
|
||||
}
|
||||
data={props.notes}
|
||||
renderItem={renderItem}
|
||||
extraData={signedIn}
|
||||
ListHeaderComponent={() => <HeaderContainer>{!signedIn && <OfflineBanner />}</HeaderContainer>}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
7
packages/mobile/src/Screens/Notes/Notes.styled.ts
Normal file
7
packages/mobile/src/Screens/Notes/Notes.styled.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Icon from 'react-native-vector-icons/Ionicons'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const StyledIcon = styled(Icon)`
|
||||
text-align-vertical: center;
|
||||
margin-left: 2px;
|
||||
`
|
||||
543
packages/mobile/src/Screens/Notes/Notes.tsx
Normal file
543
packages/mobile/src/Screens/Notes/Notes.tsx
Normal file
@@ -0,0 +1,543 @@
|
||||
import { AppStateType } from '@Lib/ApplicationState'
|
||||
import { useSignedIn, useSyncStatus } from '@Lib/SnjsHelperHooks'
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
||||
import { AppStackNavigationProp } from '@Root/AppStack'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { SCREEN_COMPOSE, SCREEN_NOTES, SCREEN_VIEW_PROTECTED_NOTE } from '@Root/Screens/screens'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
CollectionSort,
|
||||
CollectionSortProperty,
|
||||
ContentType,
|
||||
PrefKey,
|
||||
SmartView,
|
||||
SNNote,
|
||||
SNTag,
|
||||
SystemViewId,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs'
|
||||
import { ICON_ADD } from '@Style/Icons'
|
||||
import { ThemeService } from '@Style/ThemeService'
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import FAB from 'react-native-fab'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { NoteList } from './NoteList'
|
||||
import { StyledIcon } from './Notes.styled'
|
||||
|
||||
type SearchOptions = {
|
||||
selected: boolean
|
||||
onPress: () => void
|
||||
label: string
|
||||
}[]
|
||||
|
||||
export const Notes = React.memo(
|
||||
({ isInTabletMode, keyboardHeight }: { isInTabletMode: boolean | undefined; keyboardHeight: number | undefined }) => {
|
||||
const application = useSafeApplicationContext()
|
||||
const theme = useContext(ThemeContext)
|
||||
const navigation = useNavigation<AppStackNavigationProp<typeof SCREEN_NOTES>['navigation']>()
|
||||
|
||||
const [loading, decrypting, refreshing, startRefreshing] = useSyncStatus()
|
||||
const [signedIn] = useSignedIn()
|
||||
|
||||
const [sortBy, setSortBy] = useState<CollectionSortProperty>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileSortNotesBy, CollectionSort.CreatedAt),
|
||||
)
|
||||
const [sortReverse, setSortReverse] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileSortNotesReverse, false),
|
||||
)
|
||||
const [hideDates, setHideDates] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideDate, false),
|
||||
)
|
||||
const [hidePreviews, setHidePreviews] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideNotePreview, false),
|
||||
)
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideEditorIcon, false),
|
||||
)
|
||||
const [notes, setNotes] = useState<SNNote[]>([])
|
||||
const [selectedNoteId, setSelectedNoteId] = useState<SNNote['uuid']>()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [searchOptions, setSearchOptions] = useState<SearchOptions>([])
|
||||
const [includeProtectedNoteText, setIncludeProtectedNoteText] = useState<boolean>(
|
||||
() => !(application.hasProtectionSources() && !application.hasUnprotectedAccessSession()),
|
||||
)
|
||||
const [includeArchivedNotes, setIncludeArchivedNotes] = useState<boolean>(false)
|
||||
const [includeTrashedNotes, setIncludeTrashedNotes] = useState<boolean>(false)
|
||||
const [includeProtectedStarted, setIncludeProtectedStarted] = useState<boolean>(false)
|
||||
const [shouldFocusSearch, setShouldFocusSearch] = useState<boolean>(false)
|
||||
|
||||
const haveDisplayOptions = useRef(false)
|
||||
const protectionsEnabled = useRef(application.hasProtectionSources() && !application.hasUnprotectedAccessSession())
|
||||
|
||||
const reloadTitle = useCallback(
|
||||
(newNotes?: SNNote[], newFilter?: string) => {
|
||||
let title = ''
|
||||
let subTitle: string | undefined
|
||||
|
||||
const selectedTag = application.getAppState().selectedTag
|
||||
|
||||
if (newNotes && (newFilter ?? searchText).length > 0) {
|
||||
const resultCount = newNotes.length
|
||||
title = resultCount === 1 ? `${resultCount} search result` : `${resultCount} search results`
|
||||
} else if (selectedTag) {
|
||||
title = selectedTag.title
|
||||
if (selectedTag instanceof SNTag && selectedTag.parentId) {
|
||||
const parents = application.items.getTagParentChain(selectedTag)
|
||||
const hierarchy = parents.map(tag => tag.title).join(' ⫽ ')
|
||||
subTitle = hierarchy.length > 0 ? `in ${hierarchy}` : undefined
|
||||
}
|
||||
}
|
||||
|
||||
navigation.setParams({
|
||||
title,
|
||||
subTitle,
|
||||
})
|
||||
},
|
||||
[application, navigation, searchText],
|
||||
)
|
||||
|
||||
const openCompose = useCallback(
|
||||
(newNote: boolean, noteUuid: UuidString, replaceScreen = false) => {
|
||||
if (!isInTabletMode) {
|
||||
if (replaceScreen) {
|
||||
navigation.replace(SCREEN_COMPOSE, {
|
||||
title: newNote ? 'Compose' : 'Note',
|
||||
noteUuid,
|
||||
})
|
||||
} else {
|
||||
navigation.navigate(SCREEN_COMPOSE, {
|
||||
title: newNote ? 'Compose' : 'Note',
|
||||
noteUuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[navigation, isInTabletMode],
|
||||
)
|
||||
|
||||
const openNote = useCallback(
|
||||
async (noteUuid: SNNote['uuid'], replaceScreen = false) => {
|
||||
await application.getAppState().openEditor(noteUuid)
|
||||
openCompose(false, noteUuid, replaceScreen)
|
||||
},
|
||||
[application, openCompose],
|
||||
)
|
||||
|
||||
const onNoteSelect = useCallback(
|
||||
async (noteUuid: SNNote['uuid']) => {
|
||||
const note = application.items.findItem<SNNote>(noteUuid)
|
||||
if (note) {
|
||||
if (note.protected && !application.hasProtectionSources()) {
|
||||
return navigation.navigate(SCREEN_VIEW_PROTECTED_NOTE, {
|
||||
onPressView: () => openNote(noteUuid, true),
|
||||
})
|
||||
}
|
||||
if (await application.authorizeNoteAccess(note)) {
|
||||
if (!isInTabletMode) {
|
||||
await openNote(noteUuid)
|
||||
} else {
|
||||
/**
|
||||
* @TODO: remove setTimeout after SNJS navigation feature
|
||||
* https://app.asana.com/0/1201653402817596/1202360754617865
|
||||
*/
|
||||
setTimeout(async () => {
|
||||
await openNote(noteUuid)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[application, isInTabletMode, navigation, openNote],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeBlurScreenListener = navigation.addListener('blur', () => {
|
||||
if (includeProtectedStarted) {
|
||||
setIncludeProtectedStarted(false)
|
||||
setShouldFocusSearch(true)
|
||||
}
|
||||
})
|
||||
|
||||
return removeBlurScreenListener
|
||||
}, [navigation, includeProtectedStarted])
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
const removeEditorObserver = application.editorGroup.addActiveControllerChangeObserver(activeEditor => {
|
||||
if (mounted) {
|
||||
setSelectedNoteId(activeEditor?.note?.uuid)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
removeEditorObserver && removeEditorObserver()
|
||||
}
|
||||
}, [application])
|
||||
|
||||
/**
|
||||
* Note that reloading display options destroys the current index and rebuilds it,
|
||||
* so call sparingly. The runtime complexity of destroying and building
|
||||
* an index is roughly O(n^2).
|
||||
* There are optional parameters to force using the new values,
|
||||
* use when React is too slow when updating the state.
|
||||
*/
|
||||
const reloadNotesDisplayOptions = useCallback(
|
||||
(
|
||||
searchFilter?: string,
|
||||
sortOptions?: {
|
||||
sortBy?: CollectionSortProperty
|
||||
sortReverse: boolean
|
||||
},
|
||||
includeProtected?: boolean,
|
||||
includeArchived?: boolean,
|
||||
includeTrashed?: boolean,
|
||||
) => {
|
||||
const tag = application.getAppState().selectedTag
|
||||
const searchQuery =
|
||||
searchText || searchFilter
|
||||
? {
|
||||
query: searchFilter?.toLowerCase() ?? searchText.toLowerCase(),
|
||||
includeProtectedNoteText: includeProtected ?? includeProtectedNoteText,
|
||||
}
|
||||
: undefined
|
||||
|
||||
let applyFilters = false
|
||||
if (typeof searchFilter !== 'undefined') {
|
||||
applyFilters = searchFilter !== ''
|
||||
} else if (typeof searchText !== 'undefined') {
|
||||
applyFilters = searchText !== ''
|
||||
}
|
||||
|
||||
application.items.setPrimaryItemDisplayOptions({
|
||||
sortBy: sortOptions?.sortBy ?? sortBy,
|
||||
sortDirection: sortOptions?.sortReverse ?? sortReverse ? 'asc' : 'dsc',
|
||||
tags: tag instanceof SNTag ? [tag] : [],
|
||||
views: tag instanceof SmartView ? [tag] : [],
|
||||
searchQuery: searchQuery,
|
||||
includeArchived: applyFilters && (includeArchived ?? includeArchivedNotes),
|
||||
includeTrashed: applyFilters && (includeTrashed ?? includeTrashedNotes),
|
||||
})
|
||||
},
|
||||
[
|
||||
application,
|
||||
includeArchivedNotes,
|
||||
includeProtectedNoteText,
|
||||
includeTrashedNotes,
|
||||
sortBy,
|
||||
sortReverse,
|
||||
searchText,
|
||||
],
|
||||
)
|
||||
|
||||
const toggleIncludeProtected = useCallback(async () => {
|
||||
const includeProtected = !includeProtectedNoteText
|
||||
let allowToggling: boolean | undefined = true
|
||||
|
||||
if (includeProtected) {
|
||||
setIncludeProtectedStarted(true)
|
||||
allowToggling = await application.authorizeSearchingProtectedNotesText()
|
||||
}
|
||||
|
||||
setIncludeProtectedStarted(false)
|
||||
|
||||
if (allowToggling) {
|
||||
reloadNotesDisplayOptions(undefined, undefined, includeProtected)
|
||||
setIncludeProtectedNoteText(includeProtected)
|
||||
}
|
||||
}, [application, includeProtectedNoteText, reloadNotesDisplayOptions])
|
||||
|
||||
const toggleIncludeArchived = useCallback(() => {
|
||||
const includeArchived = !includeArchivedNotes
|
||||
reloadNotesDisplayOptions(undefined, undefined, undefined, includeArchived)
|
||||
setIncludeArchivedNotes(includeArchived)
|
||||
}, [includeArchivedNotes, reloadNotesDisplayOptions])
|
||||
|
||||
const toggleIncludeTrashed = useCallback(() => {
|
||||
const includeTrashed = !includeTrashedNotes
|
||||
reloadNotesDisplayOptions(undefined, undefined, undefined, undefined, includeTrashed)
|
||||
setIncludeTrashedNotes(includeTrashed)
|
||||
}, [includeTrashedNotes, reloadNotesDisplayOptions])
|
||||
|
||||
const reloadSearchOptions = useCallback(() => {
|
||||
const protections = application.hasProtectionSources() && !application.hasUnprotectedAccessSession()
|
||||
|
||||
if (protections !== protectionsEnabled.current) {
|
||||
protectionsEnabled.current = !!protections
|
||||
setIncludeProtectedNoteText(!protections)
|
||||
}
|
||||
|
||||
const selectedTag = application.getAppState().selectedTag
|
||||
const options = [
|
||||
{
|
||||
label: 'Include Protected Contents',
|
||||
selected: includeProtectedNoteText,
|
||||
onPress: toggleIncludeProtected,
|
||||
},
|
||||
]
|
||||
|
||||
const isArchiveView = selectedTag instanceof SmartView && selectedTag.uuid === SystemViewId.ArchivedNotes
|
||||
const isTrashView = selectedTag instanceof SmartView && selectedTag.uuid === SystemViewId.TrashedNotes
|
||||
if (!isArchiveView && !isTrashView) {
|
||||
setSearchOptions([
|
||||
...options,
|
||||
{
|
||||
label: 'Archived',
|
||||
selected: includeArchivedNotes,
|
||||
onPress: toggleIncludeArchived,
|
||||
},
|
||||
{
|
||||
label: 'Trashed',
|
||||
selected: includeTrashedNotes,
|
||||
onPress: toggleIncludeTrashed,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
setSearchOptions(options)
|
||||
}
|
||||
}, [
|
||||
application,
|
||||
includeProtectedNoteText,
|
||||
includeArchivedNotes,
|
||||
includeTrashedNotes,
|
||||
toggleIncludeProtected,
|
||||
toggleIncludeArchived,
|
||||
toggleIncludeTrashed,
|
||||
])
|
||||
|
||||
const getFirstSelectableNote = useCallback((newNotes: SNNote[]) => newNotes.find(note => !note.protected), [])
|
||||
|
||||
const selectFirstNote = useCallback(
|
||||
(newNotes: SNNote[]) => {
|
||||
const note = getFirstSelectableNote(newNotes)
|
||||
if (note && !loading && !decrypting) {
|
||||
void onNoteSelect(note.uuid)
|
||||
}
|
||||
},
|
||||
[decrypting, getFirstSelectableNote, loading, onNoteSelect],
|
||||
)
|
||||
|
||||
const selectNextOrCreateNew = useCallback(
|
||||
(newNotes: SNNote[]) => {
|
||||
const note = getFirstSelectableNote(newNotes)
|
||||
if (note) {
|
||||
void onNoteSelect(note.uuid)
|
||||
} else {
|
||||
application.getAppState().closeActiveEditor()
|
||||
}
|
||||
},
|
||||
[application, getFirstSelectableNote, onNoteSelect],
|
||||
)
|
||||
|
||||
const reloadNotes = useCallback(
|
||||
(reselectNote?: boolean, tagChanged?: boolean, searchFilter?: string) => {
|
||||
const tag = application.getAppState().selectedTag
|
||||
|
||||
if (!tag) {
|
||||
return
|
||||
}
|
||||
|
||||
reloadSearchOptions()
|
||||
|
||||
if (!haveDisplayOptions.current) {
|
||||
haveDisplayOptions.current = true
|
||||
reloadNotesDisplayOptions()
|
||||
}
|
||||
|
||||
const newNotes = application.items.getDisplayableNotes()
|
||||
const renderedNotes: SNNote[] = newNotes
|
||||
|
||||
setNotes(renderedNotes)
|
||||
reloadTitle(renderedNotes, searchFilter)
|
||||
|
||||
if (!application.getAppState().isTabletDevice || !reselectNote) {
|
||||
return
|
||||
}
|
||||
|
||||
if (tagChanged) {
|
||||
if (renderedNotes.length > 0) {
|
||||
selectFirstNote(renderedNotes)
|
||||
} else {
|
||||
application.getAppState().closeActiveEditor()
|
||||
}
|
||||
} else {
|
||||
const activeNote = application.getAppState().getActiveNoteController()?.note
|
||||
|
||||
if (activeNote) {
|
||||
const isTrashView =
|
||||
application.getAppState().selectedTag instanceof SmartView &&
|
||||
application.getAppState().selectedTag.uuid === SystemViewId.TrashedNotes
|
||||
|
||||
if (activeNote.trashed && !isTrashView) {
|
||||
selectNextOrCreateNew(renderedNotes)
|
||||
}
|
||||
} else {
|
||||
selectFirstNote(renderedNotes)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
application,
|
||||
reloadNotesDisplayOptions,
|
||||
reloadSearchOptions,
|
||||
reloadTitle,
|
||||
selectFirstNote,
|
||||
selectNextOrCreateNew,
|
||||
],
|
||||
)
|
||||
|
||||
const onNoteCreate = useCallback(async () => {
|
||||
const title = application.getAppState().isTabletDevice ? `Note ${notes.length + 1}` : undefined
|
||||
const noteView = await application.getAppState().createEditor(title)
|
||||
openCompose(true, noteView.note.uuid)
|
||||
reloadNotes(true)
|
||||
}, [application, notes.length, openCompose, reloadNotes])
|
||||
|
||||
const reloadPreferences = useCallback(async () => {
|
||||
let newSortBy = application.getLocalPreferences().getValue(PrefKey.MobileSortNotesBy, CollectionSort.CreatedAt)
|
||||
|
||||
if (newSortBy === CollectionSort.UpdatedAt || (newSortBy as string) === 'client_updated_at') {
|
||||
newSortBy = CollectionSort.UpdatedAt
|
||||
}
|
||||
let displayOptionsChanged = false
|
||||
const newSortReverse = application.getLocalPreferences().getValue(PrefKey.MobileSortNotesReverse, false)
|
||||
const newHidePreview = application.getLocalPreferences().getValue(PrefKey.MobileNotesHideNotePreview, false)
|
||||
const newHideDate = application.getLocalPreferences().getValue(PrefKey.MobileNotesHideDate, false)
|
||||
const newHideEditorIcon = application.getLocalPreferences().getValue(PrefKey.MobileNotesHideEditorIcon, false)
|
||||
|
||||
if (sortBy !== newSortBy) {
|
||||
setSortBy(newSortBy)
|
||||
displayOptionsChanged = true
|
||||
}
|
||||
if (sortReverse !== newSortReverse) {
|
||||
setSortReverse(newSortReverse)
|
||||
displayOptionsChanged = true
|
||||
}
|
||||
if (hidePreviews !== newHidePreview) {
|
||||
setHidePreviews(newHidePreview)
|
||||
displayOptionsChanged = true
|
||||
}
|
||||
if (hideDates !== newHideDate) {
|
||||
setHideDates(newHideDate)
|
||||
displayOptionsChanged = true
|
||||
}
|
||||
if (hideEditorIcon !== newHideEditorIcon) {
|
||||
setHideEditorIcon(newHideEditorIcon)
|
||||
displayOptionsChanged = true
|
||||
}
|
||||
|
||||
if (displayOptionsChanged) {
|
||||
reloadNotesDisplayOptions(undefined, {
|
||||
sortBy: newSortBy,
|
||||
sortReverse: newSortReverse,
|
||||
})
|
||||
}
|
||||
reloadNotes()
|
||||
}, [
|
||||
application,
|
||||
sortBy,
|
||||
sortReverse,
|
||||
hidePreviews,
|
||||
hideDates,
|
||||
hideEditorIcon,
|
||||
reloadNotes,
|
||||
reloadNotesDisplayOptions,
|
||||
])
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
startRefreshing()
|
||||
void application.sync.sync()
|
||||
}, [application, startRefreshing])
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(filter: string) => {
|
||||
reloadNotesDisplayOptions(filter)
|
||||
setSearchText(filter)
|
||||
reloadNotes(undefined, undefined, filter)
|
||||
},
|
||||
[reloadNotes, reloadNotesDisplayOptions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeEventObserver = application?.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||
await reloadPreferences()
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeEventObserver?.()
|
||||
}
|
||||
}, [application, reloadPreferences])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
void reloadPreferences()
|
||||
}, [reloadPreferences]),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeAppStateChangeHandler = application.getAppState().addStateChangeObserver(state => {
|
||||
if (state === AppStateType.TagChanged) {
|
||||
reloadNotesDisplayOptions()
|
||||
reloadNotes(true, true)
|
||||
}
|
||||
if (state === AppStateType.PreferencesChanged) {
|
||||
void reloadPreferences()
|
||||
}
|
||||
})
|
||||
|
||||
const removeStreamNotes = application.streamItems([ContentType.Note], async () => {
|
||||
/** If a note changes, it will be queried against the existing filter;
|
||||
* we dont need to reload display options */
|
||||
reloadNotes(true)
|
||||
})
|
||||
|
||||
const removeStreamTags = application.streamItems([ContentType.Tag], async () => {
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
reloadNotesDisplayOptions()
|
||||
reloadNotes()
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeStreamNotes()
|
||||
removeStreamTags()
|
||||
removeAppStateChangeHandler()
|
||||
}
|
||||
}, [application, reloadNotes, reloadNotesDisplayOptions, reloadPreferences])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NoteList
|
||||
onRefresh={onRefresh}
|
||||
hasRefreshControl={signedIn}
|
||||
onPressItem={onNoteSelect}
|
||||
refreshing={refreshing}
|
||||
searchText={searchText}
|
||||
onSearchChange={onSearchChange}
|
||||
onSearchCancel={() => onSearchChange('')}
|
||||
notes={notes}
|
||||
sortType={sortBy}
|
||||
decrypting={decrypting}
|
||||
loading={loading}
|
||||
hidePreviews={hidePreviews}
|
||||
hideDates={hideDates}
|
||||
hideEditorIcon={hideEditorIcon}
|
||||
selectedNoteId={application.getAppState().isInTabletMode ? selectedNoteId : undefined}
|
||||
searchOptions={searchOptions}
|
||||
shouldFocusSearch={shouldFocusSearch}
|
||||
setShouldFocusSearch={setShouldFocusSearch}
|
||||
/>
|
||||
<FAB
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore style prop does not exist in types
|
||||
style={application.getAppState().isInTabletMode ? { bottom: keyboardHeight } : undefined}
|
||||
buttonColor={theme.stylekitInfoColor}
|
||||
iconTextColor={theme.stylekitInfoContrastColor}
|
||||
onClickAction={onNoteCreate}
|
||||
visible={true}
|
||||
size={30}
|
||||
iconTextComponent={<StyledIcon testID="newNoteButton" name={ThemeService.nameForIcon(ICON_ADD)} />}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
41
packages/mobile/src/Screens/Notes/OfflineBanner.styled.ts
Normal file
41
packages/mobile/src/Screens/Notes/OfflineBanner.styled.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import Icon from 'react-native-vector-icons/Ionicons'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
const MARGIN = 4
|
||||
const PADDING = 12
|
||||
|
||||
const Touchable = styled.TouchableWithoutFeedback``
|
||||
const Container = styled.View`
|
||||
flex-direction: row;
|
||||
margin: ${MARGIN}px;
|
||||
padding: ${PADDING}px;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: ${props => props.theme.stylekitBorderColor};
|
||||
`
|
||||
const CenterContainer = styled.View`
|
||||
justify-content: center;
|
||||
`
|
||||
const UserIcon = styled(Icon)`
|
||||
font-size: 24px;
|
||||
color: ${props => props.theme.stylekitInfoColor};
|
||||
`
|
||||
const ForwardIcon = styled(UserIcon)`
|
||||
color: ${props => props.theme.stylekitNeutralColor};
|
||||
`
|
||||
const TextContainer = styled.View`
|
||||
flex: 1;
|
||||
padding-left: ${PADDING}px;
|
||||
`
|
||||
const BoldText = styled.Text`
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${props => props.theme.stylekitForegroundColor};
|
||||
`
|
||||
const SubText = styled.Text`
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: ${props => props.theme.stylekitNeutralColor};
|
||||
`
|
||||
|
||||
export { Touchable, Container, CenterContainer, UserIcon, ForwardIcon, TextContainer, BoldText, SubText }
|
||||
42
packages/mobile/src/Screens/Notes/OfflineBanner.tsx
Normal file
42
packages/mobile/src/Screens/Notes/OfflineBanner.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { ICON_FORWARD, ICON_USER } from '@Style/Icons'
|
||||
import { ThemeService } from '@Style/ThemeService'
|
||||
import React from 'react'
|
||||
import {
|
||||
BoldText,
|
||||
CenterContainer,
|
||||
Container,
|
||||
ForwardIcon,
|
||||
SubText,
|
||||
TextContainer,
|
||||
Touchable,
|
||||
UserIcon,
|
||||
} from './OfflineBanner.styled'
|
||||
|
||||
const NOT_BACKED_UP_TEXT = 'Data not backed up'
|
||||
const SIGN_IN_TEXT = 'Sign in or register to backup your notes'
|
||||
|
||||
export const OfflineBanner: React.FC = () => {
|
||||
const navigation = useNavigation()
|
||||
const onPress = () => {
|
||||
navigation.navigate(SCREEN_SETTINGS as never)
|
||||
}
|
||||
|
||||
return (
|
||||
<Touchable onPress={onPress}>
|
||||
<Container>
|
||||
<CenterContainer>
|
||||
<UserIcon name={ThemeService.nameForIcon(ICON_USER)} />
|
||||
</CenterContainer>
|
||||
<TextContainer>
|
||||
<BoldText>{NOT_BACKED_UP_TEXT}</BoldText>
|
||||
<SubText>{SIGN_IN_TEXT}</SubText>
|
||||
</TextContainer>
|
||||
<CenterContainer>
|
||||
<ForwardIcon name={ThemeService.nameForIcon(ICON_FORWARD)} />
|
||||
</CenterContainer>
|
||||
</Container>
|
||||
</Touchable>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user