feat: screen presentation and dismiss animations for mobile (#2073)

This commit is contained in:
Mo
2022-11-30 14:37:36 -06:00
committed by GitHub
parent 0e95b451d6
commit 7f2074a6ec
79 changed files with 1338 additions and 878 deletions

View File

@@ -9,12 +9,11 @@ import {
} from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { FileItem, PrefKey } from '@standardnotes/snjs'
import { FileItem, PrefKey, WebAppEvent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
import ContentList from '@/Components/ContentListView/ContentList'
import NoAccountWarning from '@/Components/NoAccountWarning/NoAccountWarning'
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
@@ -24,19 +23,19 @@ import { NotesController } from '@/Controllers/NotesController/NotesController'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { ElementIds } from '@/Constants/ElementIDs'
import ContentListHeader from './Header/ContentListHeader'
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { StreamingFileReader } from '@standardnotes/filepicker'
import SearchBar from '../SearchBar/SearchBar'
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
import { classNames } from '@standardnotes/utils'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
import { LinkingController } from '@/Controllers/LinkingController'
import DailyContentList from './Daily/DailyContentList'
import { ListableContentItem } from './Types/ListableContentItem'
import { FeatureName } from '@/Controllers/FeatureName'
import { PanelResizedData } from '@/Types/PanelResizedData'
import { useForwardedRef } from '@/Hooks/useForwardedRef'
type Props = {
accountMenuController: AccountMenuController
@@ -49,235 +48,252 @@ type Props = {
selectionController: SelectedItemsController
searchOptionsController: SearchOptionsController
linkingController: LinkingController
className?: string
id: string
children?: React.ReactNode
onPanelWidthLoad: (width: number) => void
}
const ContentListView: FunctionComponent<Props> = ({
accountMenuController,
application,
filesController,
itemListController,
navigationController,
noAccountWarningController,
notesController,
selectionController,
searchOptionsController,
linkingController,
}) => {
const { isNotesListVisibleOnTablets, toggleAppPane } = useResponsiveAppPane()
const ContentListView = forwardRef<HTMLDivElement, Props>(
(
{
accountMenuController,
application,
filesController,
itemListController,
navigationController,
noAccountWarningController,
notesController,
selectionController,
searchOptionsController,
linkingController,
className,
id,
children,
onPanelWidthLoad,
},
ref,
) => {
const { toggleAppPane, panes } = useResponsiveAppPane()
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
const { selected: selectedTag, selectedAsTag } = navigationController
const {
completedFullSync,
createNewNote,
optionsSubtitle,
paginate,
panelTitle,
renderedItems,
items,
isCurrentNoteTemplate,
} = itemListController
const fileInputRef = useRef<HTMLInputElement>(null)
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const innerRef = useForwardedRef(ref)
const { addDragTarget, removeDragTarget } = useFileDragNDrop()
const { addDragTarget, removeDragTarget } = useFileDragNDrop()
const fileDropCallback = useCallback(
async (files: FileItem[]) => {
useEffect(() => {
return application.addWebEventObserver((event, data) => {
if (event === WebAppEvent.PanelResized) {
const { panel, width } = data as PanelResizedData
if (panel === PANEL_NAME_NOTES) {
if (selectedAsTag) {
void navigationController.setPanelWidthForTag(selectedAsTag, width)
} else {
void application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
}
}
}
})
}, [application, navigationController, selectedAsTag])
useEffect(() => {
const panelWidth = selectedTag?.preferences?.panelWidth || application.getPreference(PrefKey.NotesPanelWidth)
if (panelWidth) {
onPanelWidthLoad(panelWidth)
}
}, [selectedTag, application, onPanelWidthLoad])
const fileDropCallback = useCallback(
async (files: FileItem[]) => {
const currentTag = navigationController.selected
if (!currentTag) {
return
}
if (navigationController.isInAnySystemView() || navigationController.isInSmartView()) {
console.error('Trying to link uploaded files to smart view')
return
}
files.forEach(async (file) => {
await linkingController.linkItems(file, currentTag)
})
},
[navigationController, linkingController],
)
useEffect(() => {
const target = innerRef.current
const currentTag = navigationController.selected
const shouldAddDropTarget = !navigationController.isInAnySystemView() && !navigationController.isInSmartView()
if (!currentTag) {
return
if (target && shouldAddDropTarget && currentTag) {
addDragTarget(target, {
tooltipText: `Drop your files to upload and link them to tag "${currentTag.title}"`,
callback: fileDropCallback,
})
}
if (navigationController.isInAnySystemView() || navigationController.isInSmartView()) {
console.error('Trying to link uploaded files to smart view')
return
return () => {
if (target) {
removeDragTarget(target)
}
}
files.forEach(async (file) => {
await linkingController.linkItems(file, currentTag)
})
},
[navigationController, linkingController],
)
useEffect(() => {
const target = itemsViewPanelRef.current
const currentTag = navigationController.selected
const shouldAddDropTarget = !navigationController.isInAnySystemView() && !navigationController.isInSmartView()
if (target && shouldAddDropTarget && currentTag) {
addDragTarget(target, {
tooltipText: `Drop your files to upload and link them to tag "${currentTag.title}"`,
callback: fileDropCallback,
})
}
return () => {
if (target) {
removeDragTarget(target)
}
}
}, [addDragTarget, fileDropCallback, navigationController, navigationController.selected, removeDragTarget])
const {
completedFullSync,
createNewNote,
optionsSubtitle,
paginate,
panelTitle,
panelWidth,
renderedItems,
items,
isCurrentNoteTemplate,
} = itemListController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
const { selected: selectedTag, selectedAsTag } = navigationController
const icon = selectedTag?.iconString
const isFilesSmartView = useMemo(() => navigationController.isInFilesView, [navigationController.isInFilesView])
const addNewItem = useCallback(async () => {
if (isFilesSmartView) {
if (!application.entitledToFiles) {
application.showPremiumModal(FeatureName.Files)
return
}
if (StreamingFileReader.available()) {
void filesController.uploadNewFile()
return
}
fileInputRef.current?.click()
} else {
await createNewNote()
toggleAppPane(AppPaneId.Editor)
}
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
useEffect(() => {
const searchBarElement = document.getElementById(ElementIds.SearchBar)
/**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
* use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent.
*/
return application.keyboardService.addCommandHandlers([
{
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
void addNewItem()
},
},
{
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => {
if (searchBarElement === document.activeElement) {
searchBarElement?.blur()
}
selectNextItem()
},
},
{
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
element: document.body,
onKeyDown: () => {
selectPreviousItem()
},
},
{
command: SEARCH_KEYBOARD_COMMAND,
onKeyDown: (event) => {
if (searchBarElement) {
event.preventDefault()
searchBarElement.focus()
}
},
},
{
command: CANCEL_SEARCH_COMMAND,
onKeyDown: () => {
if (searchBarElement) {
searchBarElement.blur()
}
},
},
{
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
onKeyDown: (event) => {
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
if (!isTargetInsideContentList) {
return
}
event.preventDefault()
selectionController.selectAll()
},
},
}, [
addDragTarget,
fileDropCallback,
navigationController,
navigationController.selected,
removeDragTarget,
innerRef,
])
}, [addNewItem, application.keyboardService, createNewNote, selectNextItem, selectPreviousItem, selectionController])
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
if (selectedAsTag) {
void navigationController.setPanelWidthForTag(selectedAsTag, width)
const icon = selectedTag?.iconString
const isFilesSmartView = useMemo(() => navigationController.isInFilesView, [navigationController.isInFilesView])
const addNewItem = useCallback(async () => {
if (isFilesSmartView) {
if (!application.entitledToFiles) {
application.showPremiumModal(FeatureName.Files)
return
}
if (StreamingFileReader.available()) {
void filesController.uploadNewFile()
return
}
fileInputRef.current?.click()
} else {
void application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
await createNewNote()
toggleAppPane(AppPaneId.Editor)
}
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
},
[application, selectedAsTag, navigationController],
)
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
const shortcutForCreate = useMemo(
() => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND),
[application],
)
useEffect(() => {
const searchBarElement = document.getElementById(ElementIds.SearchBar)
/**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
* use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent.
*/
return application.keyboardService.addCommandHandlers([
{
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
void addNewItem()
},
},
{
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => {
if (searchBarElement === document.activeElement) {
searchBarElement?.blur()
}
selectNextItem()
},
},
{
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
element: document.body,
onKeyDown: () => {
selectPreviousItem()
},
},
{
command: SEARCH_KEYBOARD_COMMAND,
onKeyDown: (event) => {
if (searchBarElement) {
event.preventDefault()
searchBarElement.focus()
}
},
},
{
command: CANCEL_SEARCH_COMMAND,
onKeyDown: () => {
if (searchBarElement) {
searchBarElement.blur()
}
},
},
{
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
onKeyDown: (event) => {
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
const addButtonLabel = useMemo(() => {
return isFilesSmartView
? 'Upload file'
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
}, [isFilesSmartView, shortcutForCreate])
if (!isTargetInsideContentList) {
return
}
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
event.preventDefault()
selectionController.selectAll()
},
},
])
}, [
addNewItem,
application.keyboardService,
createNewNote,
selectNextItem,
selectPreviousItem,
selectionController,
])
const dailyMode = selectedAsTag?.isDailyEntry
const shortcutForCreate = useMemo(
() => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND),
[application],
)
const handleDailyListSelection = useCallback(
async (item: ListableContentItem, userTriggered: boolean) => {
await selectionController.selectItemWithScrollHandling(item, {
userTriggered: true,
scrollIntoView: userTriggered === false,
animated: false,
})
},
[selectionController],
)
const addButtonLabel = useMemo(() => {
return isFilesSmartView
? 'Upload file'
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
}, [isFilesSmartView, shortcutForCreate])
useEffect(() => {
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
if (!hasEditorPane) {
itemsViewPanelRef.current?.style.removeProperty('width')
}
}, [selectedUuids, itemsViewPanelRef, isCurrentNoteTemplate, renderedItems])
const dailyMode = selectedAsTag?.isDailyEntry
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
const handleDailyListSelection = useCallback(
async (item: ListableContentItem, userTriggered: boolean) => {
await selectionController.selectItemWithScrollHandling(item, {
userTriggered: true,
scrollIntoView: userTriggered === false,
animated: false,
})
},
[selectionController],
)
return (
<div
id="items-column"
className={classNames(
'sn-component section app-column flex h-full flex-col overflow-hidden pt-safe-top',
hasEditorPane ? 'xl:w-[24rem] xsm-only:!w-full sm-only:!w-full' : 'w-full md:min-w-[400px]',
hasEditorPane
? isTabletScreenSize && !isNotesListVisibleOnTablets
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
: 'pointer-coarse:md-only:!w-60 pointer-coarse:lg-only:!w-60'
: '',
)}
aria-label={'Notes & Files'}
ref={itemsViewPanelRef}
>
<ResponsivePaneContent className="overflow-hidden" paneId={AppPaneId.Items}>
useEffect(() => {
const hasEditorPane = panes.includes(AppPaneId.Editor)
if (!hasEditorPane) {
innerRef.current?.style.removeProperty('width')
}
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
return (
<div
id={id}
className={classNames(className, 'sn-component section h-full overflow-hidden pt-safe-top')}
aria-label={'Notes & Files'}
ref={innerRef}
>
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
<div id="items-title-bar-container">
<input
@@ -325,14 +341,16 @@ const ContentListView: FunctionComponent<Props> = ({
onSelect={handleDailyListSelection}
/>
)}
{!dailyMode && completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">No items.</p>
) : null}
{!dailyMode && !completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">Loading...</p>
) : null}
{!dailyMode && renderedItems.length ? (
<>
{completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">No items.</p>
) : null}
{!completedFullSync && !renderedItems.length ? (
<p className="empty-items-list opacity-50">Loading...</p>
) : null}
<ContentList
items={renderedItems}
selectedUuids={selectedUuids}
@@ -347,22 +365,10 @@ const ContentListView: FunctionComponent<Props> = ({
</>
) : null}
<div className="absolute bottom-0 h-safe-bottom w-full" />
</ResponsivePaneContent>
{hasEditorPane && itemsViewPanelRef.current && (
<PanelResizer
collapsable={true}
hoverable={true}
defaultWidth={300}
panel={itemsViewPanelRef.current}
side={PanelSide.Right}
type={PanelResizeType.WidthOnly}
resizeFinishCallback={panelResizeFinishCallback}
width={panelWidth}
left={0}
/>
)}
</div>
)
}
{children}
</div>
)
},
)
export default observer(ContentListView)

View File

@@ -2,8 +2,8 @@ import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, us
import { ListableContentItem } from '../Types/ListableContentItem'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../../Panes/AppPaneMetadata'
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
import { DailyItemsDay } from './DailyItemsDaySection'
import { DailyItemCell } from './DailyItemCell'

View File

@@ -6,13 +6,13 @@ import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemTags from './ListItemTags'
import ListItemMetadata from './ListItemMetadata'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import { classNames } from '@standardnotes/utils'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({

View File

@@ -7,8 +7,6 @@ import ListItemFlagIcons from './ListItemFlagIcons'
import ListItemTags from './ListItemTags'
import ListItemMetadata from './ListItemMetadata'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import ListItemNotePreviewText from './ListItemNotePreviewText'
import { ListItemTitle } from './ListItemTitle'
@@ -31,8 +29,6 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
isPreviousItemTiled,
isNextItemTiled,
}) => {
const { toggleAppPane } = useResponsiveAppPane()
const listItemRef = useRef<HTMLDivElement>(null)
const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
@@ -65,11 +61,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
}
const onClick = useCallback(async () => {
const { didSelect } = await onSelect(item, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item, onSelect, toggleAppPane])
await onSelect(item, true)
}, [item, onSelect])
useContextMenuEvent(listItemRef, openContextMenu)