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

@@ -22,7 +22,7 @@ declare global {
plansUrl: string
purchaseUrl: string
startApplication: StartApplication
zip: any
zip: unknown
electronMainEvents: any
}
}
@@ -88,15 +88,15 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
/*
Title bar events
*/
document.getElementById('menu-btn')!.addEventListener('click', () => {
document.getElementById('menu-btn')?.addEventListener('click', () => {
remoteBridge.displayAppMenu()
})
document.getElementById('min-btn')!.addEventListener('click', () => {
document.getElementById('min-btn')?.addEventListener('click', () => {
remoteBridge.minimizeWindow()
})
document.getElementById('max-btn')!.addEventListener('click', async () => {
document.getElementById('max-btn')?.addEventListener('click', async () => {
if (remoteBridge.isWindowMaximized()) {
remoteBridge.unmaximizeWindow()
} else {
@@ -104,15 +104,12 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
}
})
document.getElementById('close-btn')!.addEventListener('click', () => {
document.getElementById('close-btn')?.addEventListener('click', () => {
remoteBridge.closeWindow()
})
// For Mac inset window
const sheet = document.styleSheets[0]
if (isMacOS) {
sheet.insertRule('#navigation-content { padding-top: 25px !important; }', sheet.cssRules.length)
}
if (isMacOS || useSystemMenuBar) {
// !important is important here because #desktop-title-bar has display: flex.
@@ -141,7 +138,7 @@ window.electronMainEvents.handlePerformAutomatedBackup(() => {
void window.device.downloadBackup()
})
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: any) => {
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: { success: boolean }) => {
window.webClient.didFinishBackup(data.success)
})

View File

@@ -10,18 +10,14 @@
}
@media screen and (max-width: 768px) {
.mac-desktop .app-column.selected {
padding-top: 18px;
.mac-desktop .app-pane {
padding-top: 25px;
}
}
@media screen and (min-width: 768px) {
.mac-desktop #app.collapsed-notes.collapsed-navigation #editor-column {
padding-top: 18px;
}
.mac-desktop #app.collapsed-navigation #items-column {
padding-top: 18px;
.mac-desktop .app-pane-1 {
padding-top: 25px;
}
}

View File

@@ -12,8 +12,8 @@
"build": "yarn clean && yarn copy:components && webpack --config web.webpack.prod.js && yarn tsc",
"clean": "rm -fr dist",
"format": "prettier --write src/javascripts",
"lint": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts",
"lint:fix": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts --fix",
"lint": "eslint src/javascripts && yarn tsc",
"lint:fix": "eslint src/javascripts --fix",
"start": "webpack-dev-server --config web.webpack.dev.js",
"start-secure": "yarn start --server-type https",
"test": "jest --config jest.config.js --coverage",
@@ -81,7 +81,6 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",

View File

@@ -166,11 +166,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
}
publishPanelDidResizeEvent(name: string, collapsed: boolean) {
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
const data: PanelResizedData = {
panel: name,
collapsed: collapsed,
collapsed,
width,
}
this.notifyWebEvent(WebAppEvent.PanelResized, data)
}

View File

@@ -1,36 +1,31 @@
import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { getPlatformString } from '@/Utils'
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
import { alertDialog, RouteType } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import Navigation from '@/Components/Tags/Navigation'
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
import Footer from '@/Components/Footer/Footer'
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrapper'
import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
import PremiumModalProvider from '@/Hooks/usePremiumModal'
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
import { ToastContainer } from '@standardnotes/toast'
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
import ContentListView from '@/Components/ContentListView/ContentListView'
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
import { PanelResizedData } from '@/Types/PanelResizedData'
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
import FileDragNDropProvider from '../FileDragNDropProvider'
import ResponsivePaneProvider from '../Panes/ResponsivePaneProvider'
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
import ApplicationProvider from './ApplicationProvider'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import CommandProvider from './CommandProvider'
import ApplicationProvider from '../ApplicationProvider'
import CommandProvider from '../CommandProvider'
import PanesSystemComponent from '../Panes/PanesSystemComponent'
type Props = {
application: WebApplication
@@ -45,8 +40,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
const viewControllerManager = application.getViewControllerManager()
const appColumnContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const desktopService = application.getDesktopService()
@@ -137,30 +130,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
}, [application, onAppLaunch, onAppStart])
useEffect(() => {
const removeObserver = application.addWebEventObserver(async (eventName, data) => {
if (eventName === WebAppEvent.PanelResized) {
if (!appColumnContainerRef.current) {
return
}
const { panel, collapsed } = data as PanelResizedData
if (panel === PANEL_NAME_NOTES) {
if (collapsed) {
appColumnContainerRef.current.classList.add('collapsed-notes')
} else {
appColumnContainerRef.current.classList.remove('collapsed-notes')
}
}
if (panel === PANEL_NAME_NAVIGATION) {
if (collapsed) {
appColumnContainerRef.current.classList.add('collapsed-navigation')
} else {
appColumnContainerRef.current.classList.remove('collapsed-navigation')
}
}
} else if (eventName === WebAppEvent.WindowDidFocus) {
const removeObserver = application.addWebEventObserver(async (eventName) => {
if (eventName === WebAppEvent.WindowDidFocus) {
if (!(await application.isLocked())) {
application.sync.sync().catch(console.error)
}
@@ -206,30 +177,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
featuresController={viewControllerManager.featuresController}
>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<Navigation application={application} />
<ContentListView
application={application}
accountMenuController={viewControllerManager.accountMenuController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
/>
<ErrorBoundary>
<NoteGroupView application={application} />
</ErrorBoundary>
</FileDragNDropProvider>
</div>
<FileDragNDropProvider
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<PanesSystemComponent />
</FileDragNDropProvider>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />

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)

View File

@@ -1,6 +1,6 @@
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import MenuItem from '../Menu/MenuItem'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import { FileBackupRecord, FileItem } from '@standardnotes/snjs'
import { dateToStringStyle1 } from '@/Utils/DateUtils'

View File

@@ -6,8 +6,8 @@ import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import MenuItem from '../Menu/MenuItem'
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'

View File

@@ -7,7 +7,7 @@ import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
import { StreamingFileReader } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext, memo } from 'react'
import Portal from '../Portal/Portal'
import Portal from './Portal/Portal'
type FileDragTargetData = {
tooltipText: string

View File

@@ -3,8 +3,8 @@ import { getBase64FromBlob } from '@/Utils'
import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
import Button from '../Button/Button'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
import ImagePreview from './ImagePreview'
import { ImageZoomLevelProps } from './ImageZoomLevelProps'

View File

@@ -9,7 +9,7 @@ import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
import Popover from '../Popover/Popover'
import FilePreviewInfoPanel from '../FilePreview/FilePreviewInfoPanel'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
import RoundIconButton from '../Button/RoundIconButton'
const SyncTimeoutNoDebounceMs = 100

View File

@@ -19,7 +19,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
import { ElementIds } from '@/Constants/ElementIDs'
import Menu from '../Menu/Menu'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
type Props = {
linkingController: LinkingController

View File

@@ -8,7 +8,7 @@ import Icon from '../Icon/Icon'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
type Props = {

View File

@@ -3,14 +3,14 @@ import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
import { LinkingController } from '@/Controllers/LinkingController'
import LinkedItemBubble from './LinkedItemBubble'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@standardnotes/utils'
import { ContentType } from '@standardnotes/snjs'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useCommandService } from '../CommandProvider'
type Props = {
linkingController: LinkingController

View File

@@ -4,7 +4,7 @@ import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { observer } from 'mobx-react-lite'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
type Props = {

View File

@@ -6,7 +6,7 @@ import { classNames } from '@standardnotes/utils'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import ClearInputButton from '../ClearInputButton/ClearInputButton'
import Icon from '../Icon/Icon'
import DecoratedInput from '../Input/DecoratedInput'

View File

@@ -8,7 +8,7 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { KeyboardKey } from '@standardnotes/ui-services'
import { useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import Icon from '../Icon/Icon'
import MenuItem from '../Menu/MenuItem'

View File

@@ -1,19 +1,19 @@
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
import { classNames } from '@standardnotes/snjs'
import RoundIconButton from '../Button/RoundIconButton'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
/** This button is displayed in the items list header */
export const NavigationMenuButton = () => {
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
const { setPaneLayout } = useResponsiveAppPane()
const { isTabletOrMobile } = useIsTabletOrMobileScreen()
return (
<RoundIconButton
className="mr-3 md:hidden pointer-coarse:md-only:flex pointer-coarse:lg-only:flex"
className={classNames(isTabletOrMobile ? 'flex' : 'hidden', 'mr-3')}
onClick={() => {
if (selectedPane === AppPaneId.Items || selectedPane === AppPaneId.Editor) {
toggleAppPane(AppPaneId.Navigation)
} else {
toggleAppPane(AppPaneId.Items)
}
setPaneLayout(PaneLayout.TagSelection)
}}
label="Open navigation menu"
icon="menu-variant"

View File

@@ -1,30 +1,33 @@
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { useMediaQuery, MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { IconType } from '@standardnotes/snjs'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { classNames, IconType } from '@standardnotes/snjs'
import RoundIconButton from '../Button/RoundIconButton'
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
const MobileItemsListButton = () => {
const { toggleAppPane, isNotesListVisibleOnTablets, toggleNotesListOnTablets } = useResponsiveAppPane()
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
const { panes, replacePanes, setPaneLayout } = useResponsiveAppPane()
const iconType: IconType = isTabletScreenSize && !isNotesListVisibleOnTablets ? 'chevron-right' : 'chevron-left'
const label = isTabletScreenSize
? isNotesListVisibleOnTablets
? 'Hide items list'
: 'Show items list'
: 'Go to items list'
const { isTablet, isTabletOrMobile, isMobile } = useIsTabletOrMobileScreen()
const itemsShown = panes.includes(AppPaneId.Items)
const iconType: IconType = isTablet && !itemsShown ? 'chevron-right' : 'chevron-left'
const label = isTablet ? (itemsShown ? 'Hide items list' : 'Show items list') : 'Go to items list'
return (
<RoundIconButton
className="mr-3 md:hidden pointer-coarse:md-only:flex pointer-coarse:lg-only:flex"
className={classNames(isTabletOrMobile ? 'flex' : 'hidden', 'mr-3')}
onClick={() => {
if (isTabletScreenSize) {
toggleNotesListOnTablets()
if (isMobile) {
void setPaneLayout(PaneLayout.ItemSelection)
} else {
toggleAppPane(AppPaneId.Items)
if (itemsShown) {
void replacePanes([AppPaneId.Editor])
} else {
void setPaneLayout(PaneLayout.ItemSelection)
}
}
}}
label={label}

View File

@@ -3,9 +3,7 @@ import { AbstractComponent } from '@/Components/Abstract/PureComponent'
import { WebApplication } from '@/Application/Application'
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
import { ElementIds } from '@/Constants/ElementIDs'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import FileView from '../FileView/FileView'
import NoteView from '../NoteView/NoteView'
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
@@ -22,6 +20,9 @@ type State = {
type Props = {
application: WebApplication
className?: string
innerRef: (ref: HTMLDivElement) => void
id: string
}
class NoteGroupView extends AbstractComponent<Props, State> {
@@ -97,44 +98,44 @@ class NoteGroupView extends AbstractComponent<Props, State> {
const hasControllers = this.state.controllers.length > 0
const canRenderEditorView = this.state.selectedPane === AppPaneId.Editor || !this.state.isInMobileView
return (
<div id={ElementIds.EditorColumn} className="app-column app-column-third flex h-full flex-col pt-safe-top">
<ResponsivePaneContent paneId={AppPaneId.Editor} className="flex-grow">
{this.state.showMultipleSelectedNotes && (
<MultipleSelectedNotes
application={this.application}
selectionController={this.viewControllerManager.selectionController}
navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController}
linkingController={this.viewControllerManager.linkingController}
historyModalController={this.viewControllerManager.historyModalController}
/>
)}
{this.state.showMultipleSelectedFiles && (
<MultipleSelectedFiles
filesController={this.viewControllerManager.filesController}
selectionController={this.viewControllerManager.selectionController}
/>
)}
{shouldNotShowMultipleSelectedItems && hasControllers && canRenderEditorView && (
<>
{this.state.controllers.map((controller) => {
return controller instanceof NoteViewController ? (
<NoteView key={controller.runtimeId} application={this.application} controller={controller} />
) : (
<FileView
key={controller.runtimeId}
application={this.application}
viewControllerManager={this.viewControllerManager}
file={controller.item}
/>
)
})}
</>
)}
</ResponsivePaneContent>
<div
id={this.props.id}
className={`flex h-full flex-grow flex-col pt-safe-top ${this.props.className}`}
ref={this.props.innerRef}
>
{this.state.showMultipleSelectedNotes && (
<MultipleSelectedNotes
application={this.application}
selectionController={this.viewControllerManager.selectionController}
navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController}
linkingController={this.viewControllerManager.linkingController}
historyModalController={this.viewControllerManager.historyModalController}
/>
)}
{this.state.showMultipleSelectedFiles && (
<MultipleSelectedFiles
filesController={this.viewControllerManager.filesController}
selectionController={this.viewControllerManager.selectionController}
/>
)}
{shouldNotShowMultipleSelectedItems && hasControllers && (
<>
{this.state.controllers.map((controller) => {
return controller instanceof NoteViewController ? (
<NoteView key={controller.runtimeId} application={this.application} controller={controller} />
) : (
<FileView
key={controller.runtimeId}
application={this.application}
viewControllerManager={this.viewControllerManager}
file={controller.item}
/>
)
})}
</>
)}
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { classNames } from '@standardnotes/utils'
import { ReactNode, useCallback, useState } from 'react'
import { IconType, PrefKey } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
export type NoteStatus = {
type: 'saving' | 'saved' | 'error'

View File

@@ -9,7 +9,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { debounce, isDesktopApplication, isMobileScreen, isTabletOrMobileScreen } from '@/Utils'
import { classNames } from '@standardnotes/utils'
import {
ApplicationEvent,
@@ -672,10 +672,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
PrefDefaults[PrefKey.EditorMonospaceEnabled],
)
const marginResizersEnabled = this.application.getPreference(
PrefKey.EditorResizersEnabled,
PrefDefaults[PrefKey.EditorResizersEnabled],
)
const marginResizersEnabled =
!isTabletOrMobileScreen() &&
this.application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled])
const updateSavingIndicator = this.application.getPreference(
PrefKey.UpdateSavingStatusIndicator,
@@ -687,7 +686,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.setState({
monospaceFont,
marginResizersEnabled,
updateSavingIndicator,
})
@@ -929,6 +927,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
left={this.state.leftResizerOffset}
width={this.state.leftResizerWidth}
resizeFinishCallback={this.onPanelResizeFinish}
modifyElementWidth={true}
/>
) : null}
@@ -980,6 +979,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
left={this.state.rightResizerOffset}
width={this.state.rightResizerWidth}
resizeFinishCallback={this.onPanelResizeFinish}
modifyElementWidth={true}
/>
) : null}
</div>

View File

@@ -2,7 +2,7 @@ import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { SNNote } from '@standardnotes/snjs'
import { useEffect } from 'react'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
type Props = {
note: SNNote

View File

@@ -1,7 +1,7 @@
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ElementFormatType, NodeKey } from 'lexical'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import FilePreview from '@/Components/FilePreview/FilePreview'
import { FileItem } from '@standardnotes/snjs'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'

View File

@@ -1,4 +1,4 @@
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { downloadBlobOnAndroid } from '@/NativeMobileWeb/DownloadBlobOnAndroid'
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
@@ -13,7 +13,7 @@ import { useCallback, useEffect } from 'react'
import { $convertToMarkdownString } from '@lexical/markdown'
import { MarkdownTransformers } from '@standardnotes/blocks-editor'
import { $generateHtmlFromNodes } from '@lexical/html'
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
import { useCommandService } from '@/Components/CommandProvider'
export const ExportPlugin = () => {
const application = useApplication()

View File

@@ -1,10 +1,10 @@
import { useCallback, useMemo } from 'react'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import LinkedItemBubble from '@/Components/LinkedItems/LinkedItemBubble'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { useResponsiveAppPane } from '@/Components/ResponsivePane/ResponsivePaneProvider'
import { useResponsiveAppPane } from '@/Components/Panes/ResponsivePaneProvider'
import { LexicalNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'

View File

@@ -4,7 +4,7 @@ import { TextNode } from 'lexical'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
import { ItemOption } from './ItemOption'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { ContentType, SNNote } from '@standardnotes/snjs'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import Popover from '@/Components/Popover/Popover'

View File

@@ -1,4 +1,4 @@
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { SNNote, ContentType } from '@standardnotes/snjs'
import { useState, useEffect } from 'react'

View File

@@ -30,7 +30,7 @@ import {
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { useCommandService } from '@/Components/ApplicationView/CommandProvider'
import { useCommandService } from '@/Components/CommandProvider'
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
import { ExportPlugin } from './Plugins/ExportPlugin/ExportPlugin'

View File

@@ -7,7 +7,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
import Popover from '../Popover/Popover'
import { IconType } from '@standardnotes/snjs'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import MenuItem from '../Menu/MenuItem'
import Menu from '../Menu/Menu'

View File

@@ -15,8 +15,8 @@ import AddTagOption from './AddTagOption'
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { NotesOptionsProps } from './NotesOptionsProps'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes'
import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid'
@@ -27,7 +27,7 @@ import { NoteAttributes } from './NoteAttributes'
import { SpellcheckOptions } from './SpellcheckOptions'
import { NoteSizeWarning } from './NoteSizeWarning'
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useCommandService } from '../CommandProvider'
import { iconClass } from './ClassNames'
import SuperNoteOptions from './SuperNoteOptions'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
@@ -160,6 +160,10 @@ const NotesOptions = ({
return <ProtectedUnauthorizedLabel />
}
if (notes.length === 0) {
return null
}
return (
<>
{notes.length === 1 && (

View File

@@ -6,7 +6,7 @@ import {
SUPER_EXPORT_MARKDOWN,
} from '@standardnotes/ui-services'
import { useRef, useState } from 'react'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useCommandService } from '../CommandProvider'
import Icon from '../Icon/Icon'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
import Menu from '../Menu/Menu'

View File

@@ -31,7 +31,8 @@ type Props = {
side: PanelSide
type: PanelResizeType
resizeFinishCallback?: ResizeFinishCallback
widthEventCallback?: () => void
widthEventCallback?: (width: number) => void
modifyElementWidth: boolean
}
type State = {
@@ -82,9 +83,12 @@ class PanelResizer extends Component<Props, State> {
}
override componentDidUpdate(prevProps: Props) {
this.lastWidth = this.props.panel.scrollWidth
if (this.props.width != prevProps.width) {
this.setWidth(this.props.width)
}
if (this.props.left !== prevProps.left) {
this.setLeft(this.props.left)
this.setWidth(this.props.width)
@@ -108,6 +112,10 @@ class PanelResizer extends Component<Props, State> {
}
getParentRect() {
if (!this.props.panel.parentNode) {
return new DOMRect()
}
return (this.props.panel.parentNode as HTMLElement).getBoundingClientRect()
}
@@ -131,7 +139,7 @@ class PanelResizer extends Component<Props, State> {
})
}
setWidth = (width: number, finish = false): void => {
setWidth = (width: number, finish = false): number => {
if (width === 0) {
width = this.computeMaxWidth()
}
@@ -150,22 +158,29 @@ class PanelResizer extends Component<Props, State> {
}
const isFullWidth = Math.round(width + this.lastLeft) === Math.round(parentRect.width)
if (isFullWidth) {
if (this.props.type === PanelResizeType.WidthOnly) {
this.props.panel.style.removeProperty('width')
if (this.props.modifyElementWidth) {
if (isFullWidth) {
if (this.props.type === PanelResizeType.WidthOnly) {
this.props.panel.style.removeProperty('width')
} else {
this.props.panel.style.width = `calc(100% - ${this.lastLeft}px)`
}
} else {
this.props.panel.style.width = `calc(100% - ${this.lastLeft}px)`
this.props.panel.style.width = width + 'px'
}
} else {
this.props.panel.style.width = width + 'px'
}
this.lastWidth = width
if (finish) {
this.finishSettingWidth()
if (this.props.resizeFinishCallback) {
this.props.resizeFinishCallback(this.lastWidth, this.lastLeft, this.isAtMaxWidth(), this.isCollapsed())
}
}
return width
}
setLeft = (left: number) => {
@@ -187,10 +202,6 @@ class PanelResizer extends Component<Props, State> {
}
handleWidthEvent(event?: MouseEvent) {
if (this.props.widthEventCallback) {
this.props.widthEventCallback()
}
let x
if (event) {
x = event.clientX
@@ -201,7 +212,11 @@ class PanelResizer extends Component<Props, State> {
}
const deltaX = x - this.lastDownX
const newWidth = this.startWidth + deltaX
this.setWidth(newWidth, false)
const adjustedWidth = this.setWidth(newWidth, false)
if (this.props.widthEventCallback) {
this.props.widthEventCallback(adjustedWidth)
}
}
handleLeftEvent(event: MouseEvent) {
@@ -309,6 +324,7 @@ class PanelResizer extends Component<Props, State> {
return (
<div
className={classNames(
'panel-resizer',
'absolute right-0 top-0 z-panel-resizer',
'hidden h-full w-[4px] cursor-col-resize border-y-0 bg-[color:var(--panel-resizer-background-color)] md:block',
this.props.alwaysVisible || this.state.collapsed || this.state.pressed ? ' opacity-100' : 'opacity-0',

View File

@@ -0,0 +1,13 @@
import { ElementIds } from '@/Constants/ElementIDs'
export enum AppPaneId {
Navigation = 'NavigationColumn',
Items = 'ItemsColumn',
Editor = 'EditorColumn',
}
export const AppPaneIdToDivId = {
[AppPaneId.Navigation]: ElementIds.NavigationColumn,
[AppPaneId.Items]: ElementIds.ItemsColumn,
[AppPaneId.Editor]: ElementIds.EditorColumn,
}

View File

@@ -0,0 +1,60 @@
import { log, LoggingDomain } from '@/Logging'
const ENTRANCE_DURATION = 200
const EXIT_DURATION = 200
export async function animatePaneEntranceTransitionFromOffscreenToTheRight(elementId: string): Promise<void> {
log(LoggingDomain.Panes, 'Animating pane entrance transition from offscreen to the right', elementId)
const element = document.getElementById(elementId)
if (!element) {
return
}
const animation = element.animate(
[
{
transform: 'translateX(100%)',
},
{
transform: 'translateX(0)',
},
],
{
duration: ENTRANCE_DURATION,
easing: 'ease-in-out',
fill: 'forwards',
},
)
await animation.finished
animation.finish()
}
export async function animatePaneExitTransitionOffscreenToTheRight(elementId: string): Promise<void> {
log(LoggingDomain.Panes, 'Animating pane exit transition offscreen to the right', elementId)
const element = document.getElementById(elementId)
if (!element) {
return
}
const animation = element.animate(
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(100%)',
},
],
{
duration: EXIT_DURATION,
easing: 'ease-in-out',
fill: 'forwards',
},
)
await animation.finished
animation.finish()
}

View File

@@ -0,0 +1,342 @@
import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants'
import { ElementIds } from '@/Constants/ElementIDs'
import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { ApplicationEvent, classNames, PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { useCallback, useEffect, useState } from 'react'
import { usePrevious } from '../ContentListView/Calendar/usePrevious'
import ContentListView from '../ContentListView/ContentListView'
import NoteGroupView from '../NoteGroupView/NoteGroupView'
import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer'
import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata'
import { useResponsiveAppPane } from './ResponsivePaneProvider'
import Navigation from '../Tags/Navigation'
import { useApplication } from '../ApplicationProvider'
import {
animatePaneEntranceTransitionFromOffscreenToTheRight,
animatePaneExitTransitionOffscreenToTheRight,
} from '@/Components/Panes/PaneAnimator'
import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout'
import { log, LoggingDomain } from '@/Logging'
const NAVIGATION_PANEL_MIN_WIDTH = 48
const ITEMS_PANEL_MIN_WIDTH = 200
const PLACEHOLDER_NAVIGATION_PANEL_WIDTH = 220
const PLACEHOLDER_NOTES_PANEL_WIDTH = 400
const PanesSystemComponent = () => {
const application = useApplication()
const isTabletOrMobileScreenWrapped = useIsTabletOrMobileScreen()
const { isTabletOrMobile, isTablet, isMobile } = isTabletOrMobileScreenWrapped
const previousIsTabletOrMobileWrapped = usePrevious(isTabletOrMobileScreenWrapped)
const paneController = useResponsiveAppPane()
const previousPaneController = usePrevious(paneController)
const [renderPanes, setRenderPanes] = useState<AppPaneId[]>([])
const [panesPendingEntrance, setPanesPendingEntrance] = useState<AppPaneId[]>([])
const [panesPendingExit, setPanesPendingExit] = useState<AppPaneId[]>([])
const viewControllerManager = application.getViewControllerManager()
const [navigationPanelWidth, setNavigationPanelWidth] = useState<number>(
application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH),
)
const [navigationRef, setNavigationRef] = useState<HTMLDivElement | null>(null)
const [itemsPanelWidth, setItemsPanelWidth] = useState<number>(
application.getPreference(PrefKey.NotesPanelWidth, PLACEHOLDER_NOTES_PANEL_WIDTH),
)
const [listRef, setListRef] = useState<HTMLDivElement | null>(null)
const showPanelResizers = !isTabletOrMobile
const [_editorRef, setEditorRef] = useState<HTMLDivElement | null>(null)
const animationsSupported = isMobile
useEffect(() => {
if (!animationsSupported) {
return
}
const panes = paneController.panes
const previousPanes = previousPaneController?.panes
if (!previousPanes) {
setPanesPendingEntrance([])
return
}
const isPush = isPanesChangePush(previousPanes, panes)
if (isPush) {
setPanesPendingEntrance([panes[panes.length - 1]])
}
}, [paneController.panes, previousPaneController?.panes, animationsSupported])
useEffect(() => {
if (!animationsSupported) {
return
}
const panes = paneController.panes
const previousPanes = previousPaneController?.panes
if (!previousPanes) {
setPanesPendingExit([])
return
}
const isExit = isPanesChangeLeafDismiss(previousPanes, panes)
if (isExit) {
setPanesPendingExit([previousPanes[previousPanes.length - 1]])
}
}, [paneController.panes, previousPaneController?.panes, animationsSupported])
useEffect(() => {
setRenderPanes(paneController.panes)
}, [paneController.panes])
useEffect(() => {
if (!panesPendingEntrance || panesPendingEntrance?.length === 0) {
return
}
if (panesPendingEntrance.length > 1) {
console.warn('More than one pane pending entrance. This is not supported.')
return
}
void animatePaneEntranceTransitionFromOffscreenToTheRight(AppPaneIdToDivId[panesPendingEntrance[0]]).then(() => {
setPanesPendingEntrance([])
})
}, [panesPendingEntrance])
useEffect(() => {
if (!panesPendingExit || panesPendingExit?.length === 0) {
return
}
if (panesPendingExit.length > 1) {
console.warn('More than one pane pending exit. This is not supported.')
return
}
void animatePaneExitTransitionOffscreenToTheRight(AppPaneIdToDivId[panesPendingExit[0]]).then(() => {
setPanesPendingExit([])
})
}, [panesPendingExit])
useEffect(() => {
const removeObserver = application.addEventObserver(async () => {
const width = application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH)
setNavigationPanelWidth(width)
}, ApplicationEvent.PreferencesChanged)
return () => {
removeObserver()
}
}, [application])
const navigationPanelResizeWidthChangeCallback = useCallback((width: number) => {
setNavigationPanelWidth(width)
}, [])
const itemsPanelResizeWidthChangeCallback = useCallback((width: number) => {
setItemsPanelWidth(width)
}, [])
const handleInitialItemsListPanelWidthLoad = useCallback((width: number) => {
setItemsPanelWidth(width)
}, [])
const navigationPanelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, width, isCollapsed)
},
[application],
)
const itemsPanelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, width, isCollapsed)
},
[application],
)
useEffect(() => {
if (isTablet && !previousIsTabletOrMobileWrapped?.isTablet) {
if (paneController.selectedPane !== AppPaneId.Navigation) {
paneController.removePane(AppPaneId.Navigation)
}
} else if (
!isTablet &&
previousIsTabletOrMobileWrapped?.isTablet &&
!paneController.panes.includes(AppPaneId.Navigation)
) {
paneController.insertPaneAtIndex(AppPaneId.Navigation, 0)
}
}, [isTablet, paneController, previousIsTabletOrMobileWrapped])
const computeStylesForContainer = (): React.CSSProperties => {
const panes = paneController.panes
const numPanes = panes.length
if (isMobile) {
return {}
}
switch (numPanes) {
case 1: {
return {
gridTemplateColumns: 'auto',
}
}
case 2: {
if (paneController.focusModeEnabled) {
return {
gridTemplateColumns: '0 1fr',
}
}
if (isTablet) {
return {
gridTemplateColumns: '1fr 2fr',
}
} else {
if (panes[0] === AppPaneId.Navigation) {
return {
gridTemplateColumns: `${navigationPanelWidth}px auto`,
}
} else {
return {
gridTemplateColumns: `${itemsPanelWidth}px auto`,
}
}
}
}
case 3: {
if (paneController.focusModeEnabled) {
return {
gridTemplateColumns: '0 0 1fr',
}
}
return {
gridTemplateColumns: `${navigationPanelWidth}px ${itemsPanelWidth}px 2fr`,
}
}
default:
return {}
}
}
const computeClassesForPane = (_paneId: AppPaneId, isPendingEntrance: boolean, index: number): string => {
const common = `app-pane app-pane-${index + 1} h-full content`
if (isMobile) {
return `absolute top-0 left-0 w-full flex flex-col ${common} ${
isPendingEntrance ? 'translate-x-[100%]' : 'translate-x-0 '
}`
} else {
return `flex flex-col relative overflow-hidden ${common}`
}
}
const computeClassesForContainer = (): string => {
if (isMobile) {
return 'w-full'
}
return 'grid'
}
const renderPanesWithPendingExit = [...renderPanes, ...panesPendingExit]
log(LoggingDomain.Panes, 'Rendering panes', renderPanesWithPendingExit)
return (
<div id="app" className={`app ${computeClassesForContainer()}`} style={{ ...computeStylesForContainer() }}>
{renderPanesWithPendingExit.map((pane, index) => {
const isPendingEntrance = panesPendingEntrance?.includes(pane)
const className = computeClassesForPane(pane, isPendingEntrance ?? false, index)
if (pane === AppPaneId.Navigation) {
return (
<Navigation
id={ElementIds.NavigationColumn}
ref={setNavigationRef}
className={classNames(className, isTabletOrMobile ? 'w-full' : '')}
key="navigation-pane"
application={application}
>
{showPanelResizers && navigationRef && (
<PanelResizer
collapsable={true}
defaultWidth={navigationPanelWidth}
hoverable={true}
left={0}
minWidth={NAVIGATION_PANEL_MIN_WIDTH}
modifyElementWidth={false}
panel={navigationRef}
resizeFinishCallback={navigationPanelResizeFinishCallback}
side={PanelSide.Right}
type={PanelResizeType.WidthOnly}
width={navigationPanelWidth}
widthEventCallback={navigationPanelResizeWidthChangeCallback}
/>
)}
</Navigation>
)
} else if (pane === AppPaneId.Items) {
return (
<ContentListView
id={ElementIds.ItemsColumn}
className={className}
ref={setListRef}
key={'content-list-view'}
application={application}
onPanelWidthLoad={handleInitialItemsListPanelWidthLoad}
accountMenuController={viewControllerManager.accountMenuController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
>
{showPanelResizers && listRef && (
<PanelResizer
collapsable={true}
defaultWidth={itemsPanelWidth}
hoverable={true}
left={0}
minWidth={ITEMS_PANEL_MIN_WIDTH}
modifyElementWidth={false}
panel={listRef}
resizeFinishCallback={itemsPanelResizeFinishCallback}
side={PanelSide.Right}
type={PanelResizeType.WidthOnly}
width={itemsPanelWidth}
widthEventCallback={itemsPanelResizeWidthChangeCallback}
/>
)}
</ContentListView>
)
} else if (pane === AppPaneId.Editor) {
return (
<ErrorBoundary key="editor-pane">
<NoteGroupView
id={ElementIds.EditorColumn}
innerRef={(ref) => setEditorRef(ref)}
className={className}
application={application}
/>
</ErrorBoundary>
)
}
})}
</div>
)
}
export default observer(PanesSystemComponent)

View File

@@ -1,4 +1,3 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
import {
useEffect,
@@ -7,25 +6,31 @@ import {
createContext,
useCallback,
useContext,
useState,
memo,
useRef,
useLayoutEffect,
MutableRefObject,
} from 'react'
import { AppPaneId } from './AppPaneMetadata'
import { PaneController } from '../../Controllers/PaneController'
import { PaneController } from '../../Controllers/PaneController/PaneController'
import { observer } from 'mobx-react-lite'
type ResponsivePaneData = {
selectedPane: AppPaneId
toggleAppPane: (paneId: AppPaneId) => void
toggleNotesListOnTablets: () => void
toggleListPane: () => void
toggleNavigationPane: () => void
isNotesListVisibleOnTablets: boolean
isListPaneCollapsed: boolean
isNavigationPaneCollapsed: boolean
panes: PaneController['panes']
toggleAppPane: (paneId: AppPaneId) => void
presentPane: PaneController['presentPane']
popToPane: PaneController['popToPane']
dismissLastPane: PaneController['dismissLastPane']
replacePanes: PaneController['replacePanes']
removePane: PaneController['removePane']
insertPaneAtIndex: PaneController['insertPaneAtIndex']
setPaneLayout: PaneController['setPaneLayout']
focusModeEnabled: PaneController['focusModeEnabled']
}
const ResponsivePaneContext = createContext<ResponsivePaneData | undefined>(undefined)
@@ -62,27 +67,15 @@ const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) => {
const currentSelectedPane = paneController.currentPane
const previousSelectedPane = paneController.previousPane
const currentSelectedPaneRef = useStateRef<AppPaneId>(currentSelectedPane)
const toggleAppPane = useCallback(
(paneId: AppPaneId) => {
paneController.setPreviousPane(currentSelectedPane)
paneController.setCurrentPane(paneId)
paneController.presentPane(paneId)
},
[paneController, currentSelectedPane],
[paneController],
)
useEffect(() => {
if (previousSelectedPane) {
const previousPaneElement = document.getElementById(ElementIds[previousSelectedPane])
previousPaneElement?.removeAttribute('data-selected-pane')
}
const currentPaneElement = document.getElementById(ElementIds[currentSelectedPane])
currentPaneElement?.setAttribute('data-selected-pane', '')
}, [currentSelectedPane, previousSelectedPane])
const addAndroidBackHandler = useAndroidBackHandler()
useEffect(() => {
@@ -104,32 +97,40 @@ const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) =>
}
}, [addAndroidBackHandler, currentSelectedPaneRef, toggleAppPane])
const [isNotesListVisibleOnTablets, setNotesListVisibleOnTablets] = useState(true)
const toggleNotesListOnTablets = useCallback(() => {
setNotesListVisibleOnTablets((visible) => !visible)
}, [])
const contextValue = useMemo(
() => ({
(): ResponsivePaneData => ({
selectedPane: currentSelectedPane,
toggleAppPane,
isNotesListVisibleOnTablets,
toggleNotesListOnTablets,
presentPane: paneController.presentPane,
isListPaneCollapsed: paneController.isListPaneCollapsed,
isNavigationPaneCollapsed: paneController.isNavigationPaneCollapsed,
toggleListPane: paneController.toggleListPane,
toggleNavigationPane: paneController.toggleNavigationPane,
panes: paneController.panes,
popToPane: paneController.popToPane,
dismissLastPane: paneController.dismissLastPane,
replacePanes: paneController.replacePanes,
removePane: paneController.removePane,
insertPaneAtIndex: paneController.insertPaneAtIndex,
setPaneLayout: paneController.setPaneLayout,
focusModeEnabled: paneController.focusModeEnabled,
}),
[
currentSelectedPane,
isNotesListVisibleOnTablets,
toggleAppPane,
toggleNotesListOnTablets,
paneController.toggleListPane,
paneController.toggleNavigationPane,
paneController.panes,
paneController.isListPaneCollapsed,
paneController.isNavigationPaneCollapsed,
paneController.toggleListPane,
paneController.toggleNavigationPane,
paneController.presentPane,
paneController.popToPane,
paneController.dismissLastPane,
paneController.replacePanes,
paneController.removePane,
paneController.insertPaneAtIndex,
paneController.setPaneLayout,
paneController.focusModeEnabled,
],
)

View File

@@ -5,7 +5,7 @@ import Icon from '@/Components/Icon/Icon'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { classNames } from '@standardnotes/utils'
import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useCommandService } from '../CommandProvider'
type Props = {
className?: string

View File

@@ -1,4 +1,4 @@
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import Icon from '@/Components/Icon/Icon'
import { ContentType, ItemCounter } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'

View File

@@ -1,8 +1,8 @@
import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services'
import { useMemo } from 'react'
import { observer } from 'mobx-react-lite'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { useCommandService } from '../CommandProvider'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
const PanelSettingsSection = () => {
@@ -25,7 +25,7 @@ const PanelSettingsSection = () => {
<div className="hidden md:block pointer-coarse:md-only:hidden pointer-coarse:lg-only:hidden">
<MenuSwitchButtonItem
className="items-center"
checked={isNavigationPaneCollapsed}
checked={!isNavigationPaneCollapsed}
onChange={toggleNavigationPane}
shortcut={navigationShortcut}
>
@@ -33,7 +33,7 @@ const PanelSettingsSection = () => {
</MenuSwitchButtonItem>
<MenuSwitchButtonItem
className="items-center"
checked={isListPaneCollapsed}
checked={!isListPaneCollapsed}
onChange={toggleListPane}
shortcut={listShortcut}
>

View File

@@ -23,15 +23,14 @@ import Menu from '../Menu/Menu'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
export const focusModeAnimationDuration = 1255
type MenuProps = {
quickSettingsMenuController: QuickSettingsController
application: WebApplication
}
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSettingsMenuController }) => {
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
const { closeQuickSettingsMenu } = quickSettingsMenuController
const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])

View File

@@ -1,5 +0,0 @@
export enum AppPaneId {
Navigation = 'NavigationColumn',
Items = 'ItemsColumn',
Editor = 'EditorColumn',
}

View File

@@ -1,28 +0,0 @@
import { useMemo, ReactNode } from 'react'
import { AppPaneId } from './AppPaneMetadata'
import { classNames } from '@standardnotes/utils'
import { useResponsiveAppPane } from './ResponsivePaneProvider'
type Props = {
children: ReactNode
className?: string
contentElementId?: string
paneId: AppPaneId
}
const ResponsivePaneContent = ({ children, className, contentElementId, paneId }: Props) => {
const { selectedPane } = useResponsiveAppPane()
const isSelectedPane = useMemo(() => selectedPane === paneId, [paneId, selectedPane])
return (
<div
id={contentElementId}
className={classNames('content flex flex-col', isSelectedPane ? 'h-full' : 'hidden md:flex', className)}
>
{children}
</div>
)
}
export default ResponsivePaneContent

View File

@@ -1,6 +1,6 @@
import { getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors'
import { NoteType } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { useApplication } from '../ApplicationProvider'
import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths'
type Props = {

View File

@@ -1,28 +1,28 @@
import SmartViewsSection from '@/Components/Tags/SmartViewsSection'
import TagsSection from '@/Components/Tags/TagsSection'
import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
import { ApplicationEvent, PrefKey, WebAppEvent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
import { forwardRef, useEffect, useMemo, useState } from 'react'
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
import { classNames } from '@standardnotes/utils'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import UpgradeNow from '../Footer/UpgradeNow'
import RoundIconButton from '../Button/RoundIconButton'
import { isIOS } from '@/Utils'
import { PanelResizedData } from '@/Types/PanelResizedData'
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
type Props = {
application: WebApplication
className?: string
children?: React.ReactNode
id: string
}
const Navigation: FunctionComponent<Props> = ({ application }) => {
const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className, children, id }, ref) => {
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
const [panelElement, setPanelElement] = useState<HTMLDivElement>()
const [panelWidth, setPanelWidth] = useState<number>(0)
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
const { toggleAppPane } = useResponsiveAppPane()
const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode())
useEffect(() => {
@@ -34,26 +34,16 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
}, [application])
useEffect(() => {
const removeObserver = application.addEventObserver(async () => {
const width = application.getPreference(PrefKey.TagsPanelWidth)
if (width) {
setPanelWidth(width)
return application.addWebEventObserver((event, data) => {
if (event === WebAppEvent.PanelResized) {
const { panel, width } = data as PanelResizedData
if (panel === PANEL_NAME_NAVIGATION) {
application.setPreference(PrefKey.TagsPanelWidth, width).catch(console.error)
}
}
}, ApplicationEvent.PreferencesChanged)
return () => {
removeObserver()
}
})
}, [application])
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.TagsPanelWidth, width).catch(console.error)
application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, isCollapsed)
},
[application],
)
const NavigationFooter = useMemo(() => {
return (
<div
@@ -115,52 +105,33 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
return (
<div
id="navigation"
id={id}
className={classNames(
'pb-[50px] md:pb-0',
'sn-component section app-column h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
'w-[220px] xl:w-[220px] xsm-only:!w-full sm-only:!w-full',
selectedPane === AppPaneId.Navigation
? 'pointer-coarse:md-only:!w-48 pointer-coarse:lg-only:!w-48'
: 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0',
className,
'sn-component section pb-[50px] md:pb-0',
'h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0',
)}
ref={(element) => {
if (element) {
setPanelElement(element)
}
}}
ref={ref}
>
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
<div
className={classNames(
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
'md:hover:[overflow-y:_overlay]',
)}
>
<SmartViewsSection
application={application}
featuresController={viewControllerManager.featuresController}
navigationController={viewControllerManager.navigationController}
/>
<TagsSection viewControllerManager={viewControllerManager} />
</div>
{NavigationFooter}
</ResponsivePaneContent>
{panelElement && (
<PanelResizer
collapsable={true}
defaultWidth={150}
panel={panelElement}
hoverable={true}
side={PanelSide.Right}
type={PanelResizeType.WidthOnly}
resizeFinishCallback={panelResizeFinishCallback}
width={panelWidth}
left={0}
<div
id="navigation-content"
className={classNames(
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto',
'md:hover:[overflow-y:_overlay] pointer-coarse:md:overflow-y-auto',
)}
>
<SmartViewsSection
application={application}
featuresController={viewControllerManager.featuresController}
navigationController={viewControllerManager.navigationController}
/>
)}
<TagsSection viewControllerManager={viewControllerManager} />
</div>
{NavigationFooter}
{children}
</div>
)
}
})
export default observer(Navigation)

View File

@@ -13,8 +13,6 @@ import {
useRef,
useState,
} from 'react'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { classNames } from '@standardnotes/utils'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
@@ -37,8 +35,6 @@ const getIconClass = (view: SmartView, isSelected: boolean): string => {
}
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEditingSmartView }) => {
const { toggleAppPane } = useResponsiveAppPane()
const [title, setTitle] = useState(view.title || '')
const inputRef = useRef<HTMLInputElement>(null)
@@ -54,8 +50,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
await tagsState.setSelectedTag(view, 'views', {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [tagsState, toggleAppPane, view])
}, [tagsState, view])
const onBlur = useCallback(() => {
tagsState.save(view, title).catch(console.error)

View File

@@ -19,10 +19,8 @@ import {
useRef,
useState,
} from 'react'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { classNames } from '@standardnotes/utils'
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
import { LinkingController } from '@/Controllers/LinkingController'
import { TagListSectionType } from './TagListSection'
import { log, LoggingDomain } from '@/Logging'
@@ -44,8 +42,6 @@ const PADDING_PER_LEVEL_PX = 21
export const TagsListItem: FunctionComponent<Props> = observer(
({ tag, type, features, navigationController: navigationController, level, onContextMenu, linkingController }) => {
const { toggleAppPane } = useResponsiveAppPane()
const [title, setTitle] = useState(tag.title || '')
const [subtagTitle, setSubtagTitle] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
@@ -94,8 +90,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
await navigationController.setSelectedTag(tag, type, {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [navigationController, tag, type, toggleAppPane])
}, [navigationController, tag, type])
const onBlur = useCallback(() => {
navigationController.save(tag, title).catch(console.error)

View File

@@ -4,7 +4,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { CREATE_NEW_TAG_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useMemo } from 'react'
import { useCommandService } from '../ApplicationView/CommandProvider'
import { useCommandService } from '../CommandProvider'
type Props = {
tags: NavigationController

View File

@@ -8,7 +8,7 @@ export const PrefDefaults = {
[PrefKey.EditorLeft]: null,
[PrefKey.EditorMonospaceEnabled]: false,
[PrefKey.EditorSpellcheck]: true,
[PrefKey.EditorResizersEnabled]: true,
[PrefKey.EditorResizersEnabled]: false,
[PrefKey.EditorLineHeight]: EditorLineHeight.Normal,
[PrefKey.EditorFontSize]: EditorFontSize.Normal,
[PrefKey.SortNotesBy]: CollectionSort.CreatedAt,

View File

@@ -52,7 +52,6 @@ export class ItemListController extends AbstractViewController implements Intern
notesToDisplay = 0
pageSize = 0
panelTitle = 'Notes'
panelWidth = 0
renderedItems: ListableContentItem[] = []
searchSubmitted = false
showDisplayOptionsMenu = false
@@ -189,7 +188,6 @@ export class ItemListController extends AbstractViewController implements Intern
notes: observable,
notesToDisplay: observable,
panelTitle: observable,
panelWidth: observable,
items: observable,
renderedItems: observable,
showDisplayOptionsMenu: observable,
@@ -439,6 +437,7 @@ export class ItemListController extends AbstractViewController implements Intern
const activeController = this.getActiveItemController()
if (this.shouldLeaveSelectionUnchanged(activeController)) {
log(LoggingDomain.Selection, 'Leaving selection unchanged')
return
}
@@ -451,7 +450,7 @@ export class ItemListController extends AbstractViewController implements Intern
if (this.shouldSelectFirstItem(itemsReloadSource)) {
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
this.selectionController.selectNextItem()
this.selectionController.selectNextItem({ userTriggered: false })
}
} else if (activeItem && this.shouldSelectActiveItem(activeItem)) {
log(LoggingDomain.Selection, 'Selecting active item')
@@ -460,6 +459,8 @@ export class ItemListController extends AbstractViewController implements Intern
await this.selectFirstItem()
} else if (this.shouldSelectNextItemOrCreateNewNote(activeItem)) {
await this.selectNextItemOrCreateNewNote()
} else {
log(LoggingDomain.Selection, 'No selection change')
}
}
@@ -579,13 +580,6 @@ export class ItemListController extends AbstractViewController implements Intern
this.displayOptions = newDisplayOptions
this.webDisplayOptions = newWebDisplayOptions
const listColumnWidth =
selectedTag?.preferences?.panelWidth || this.application.getPreference(PrefKey.NotesPanelWidth)
if (listColumnWidth && listColumnWidth !== this.panelWidth) {
this.panelWidth = listColumnWidth
}
if (!displayOptionsChanged) {
return { didReloadItems: false }
}

View File

@@ -1,7 +1,7 @@
import { WebApplication } from '@/Application/Application'
import { PopoverFileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'

View File

@@ -25,6 +25,7 @@ import { CrossControllerEvent } from '../CrossControllerEvent'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'
import { TagListSectionType } from '@/Components/Tags/TagListSection'
import { PaneLayout } from '../PaneController/PaneLayout'
export class NavigationController
extends AbstractViewController
@@ -260,6 +261,10 @@ export class NavigationController
return this.selectedUuid === SystemViewId.Files
}
isTagFilesView(tag: AnyTag): boolean {
return tag.uuid === SystemViewId.Files
}
public isInAnySystemView(): boolean {
return (
this.selected instanceof SmartView && Object.values(SystemViewId).includes(this.selected.uuid as SystemViewId)
@@ -458,10 +463,10 @@ export class NavigationController
.catch(console.error)
}
const selectionHasNotChanged = this.selected_?.uuid === tag?.uuid && location === this.selectedLocation
if (selectionHasNotChanged) {
return
if (tag && this.isTagFilesView(tag)) {
this.application.paneController.setPaneLayout(PaneLayout.FilesView)
} else if (userTriggered) {
this.application.paneController.setPaneLayout(PaneLayout.ItemSelection)
}
this.previouslySelected_ = this.selected_
@@ -497,7 +502,7 @@ export class NavigationController
}
get filesNavigationView(): SmartView {
return this.smartViews.find((view) => view.uuid === SystemViewId.Files) as SmartView
return this.smartViews.find(this.isTagFilesView) as SmartView
}
private setSelectedTagInstance(tag: AnyTag | undefined): void {

View File

@@ -1,152 +0,0 @@
import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services'
import { ApplicationEvent, InternalEventBus, PrefKey } from '@standardnotes/snjs'
import { AppPaneId } from './../Components/ResponsivePane/AppPaneMetadata'
import { isMobileScreen } from '@/Utils'
import { makeObservable, observable, action, computed } from 'mobx'
import { Disposer } from '@/Types/Disposer'
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { WebApplication } from '@/Application/Application'
import { AbstractViewController } from './Abstract/AbstractViewController'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants'
const WidthForCollapsedPanel = 5
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
export class PaneController extends AbstractViewController {
currentPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
previousPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
isInMobileView = isMobileScreen()
protected disposers: Disposer[] = []
currentNavPanelWidth = 0
currentItemsPanelWidth = 0
constructor(application: WebApplication, eventBus: InternalEventBus) {
super(application, eventBus)
makeObservable(this, {
currentPane: observable,
previousPane: observable,
isInMobileView: observable,
currentNavPanelWidth: observable,
currentItemsPanelWidth: observable,
isListPaneCollapsed: computed,
isNavigationPaneCollapsed: computed,
setCurrentPane: action,
setPreviousPane: action,
setIsInMobileView: action,
toggleListPane: action,
toggleNavigationPane: action,
setCurrentItemsPanelWidth: action,
setCurrentNavPanelWidth: action,
})
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
const mediaQuery = window.matchMedia(MediaQueryBreakpoints.md)
if (mediaQuery?.addEventListener != undefined) {
mediaQuery.addEventListener('change', this.mediumScreenMQHandler)
} else {
mediaQuery.addListener(this.mediumScreenMQHandler)
}
this.disposers.push(
application.addEventObserver(async () => {
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
}, ApplicationEvent.PreferencesChanged),
application.keyboardService.addCommandHandler({
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.toggleListPane()
},
}),
application.keyboardService.addCommandHandler({
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.toggleNavigationPane()
},
}),
)
}
setCurrentNavPanelWidth(width: number) {
this.currentNavPanelWidth = width
}
setCurrentItemsPanelWidth(width: number) {
this.currentItemsPanelWidth = width
}
deinit() {
super.deinit()
const mq = window.matchMedia(MediaQueryBreakpoints.md)
if (mq?.removeEventListener != undefined) {
mq.removeEventListener('change', this.mediumScreenMQHandler)
} else {
mq.removeListener(this.mediumScreenMQHandler)
}
}
mediumScreenMQHandler = (event: MediaQueryListEvent) => {
if (event.matches) {
this.setIsInMobileView(false)
} else {
this.setIsInMobileView(true)
}
}
setCurrentPane(pane: AppPaneId): void {
this.currentPane = pane
}
setPreviousPane(pane: AppPaneId): void {
this.previousPane = pane
}
setIsInMobileView(isInMobileView: boolean) {
this.isInMobileView = isInMobileView
}
toggleListPane = () => {
const currentItemsPanelWidth = this.application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)
const isCollapsed = currentItemsPanelWidth <= WidthForCollapsedPanel
if (isCollapsed) {
void this.application.setPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)
} else {
void this.application.setPreference(PrefKey.NotesPanelWidth, WidthForCollapsedPanel)
}
this.application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, !isCollapsed)
}
toggleNavigationPane = () => {
const currentNavPanelWidth = this.application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)
const isCollapsed = currentNavPanelWidth <= WidthForCollapsedPanel
if (isCollapsed) {
void this.application.setPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)
} else {
void this.application.setPreference(PrefKey.TagsPanelWidth, WidthForCollapsedPanel)
}
this.application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, !isCollapsed)
}
get isListPaneCollapsed() {
return this.currentItemsPanelWidth > WidthForCollapsedPanel
}
get isNavigationPaneCollapsed() {
return this.currentNavPanelWidth > WidthForCollapsedPanel
}
}

View File

@@ -0,0 +1,256 @@
import {
TOGGLE_FOCUS_MODE_COMMAND,
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
} from '@standardnotes/ui-services'
import { ApplicationEvent, InternalEventBus, PrefKey, removeFromArray } from '@standardnotes/snjs'
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
import { isMobileScreen } from '@/Utils'
import { makeObservable, observable, action, computed } from 'mobx'
import { Disposer } from '@/Types/Disposer'
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { WebApplication } from '@/Application/Application'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { log, LoggingDomain } from '@/Logging'
import { PaneLayout } from './PaneLayout'
import { panesForLayout } from './panesForLayout'
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
const FOCUS_MODE_CLASS_NAME = 'focus-mode'
const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode'
const FOCUS_MODE_ANIMATION_DURATION = 1255
export class PaneController extends AbstractViewController {
isInMobileView = isMobileScreen()
protected disposers: Disposer[] = []
panes: AppPaneId[] = []
currentNavPanelWidth = 0
currentItemsPanelWidth = 0
focusModeEnabled = false
constructor(application: WebApplication, eventBus: InternalEventBus) {
super(application, eventBus)
makeObservable(this, {
panes: observable,
isInMobileView: observable,
currentNavPanelWidth: observable,
currentItemsPanelWidth: observable,
focusModeEnabled: observable,
currentPane: computed,
previousPane: computed,
isListPaneCollapsed: computed,
isNavigationPaneCollapsed: computed,
setIsInMobileView: action,
toggleListPane: action,
toggleNavigationPane: action,
setCurrentItemsPanelWidth: action,
setCurrentNavPanelWidth: action,
presentPane: action,
dismissLastPane: action,
replacePanes: action,
popToPane: action,
removePane: action,
insertPaneAtIndex: action,
setPaneLayout: action,
setFocusModeEnabled: action,
})
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
const screen = getIsTabletOrMobileScreen(application)
this.panes = screen.isTabletOrMobile
? [AppPaneId.Navigation, AppPaneId.Items]
: [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
const mediaQuery = window.matchMedia(MediaQueryBreakpoints.md)
if (mediaQuery?.addEventListener != undefined) {
mediaQuery.addEventListener('change', this.mediumScreenMQHandler)
} else {
mediaQuery.addListener(this.mediumScreenMQHandler)
}
this.disposers.push(
application.addEventObserver(async () => {
this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth))
this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth))
}, ApplicationEvent.PreferencesChanged),
application.keyboardService.addCommandHandler({
command: TOGGLE_FOCUS_MODE_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.setFocusModeEnabled(!this.focusModeEnabled)
return true
},
}),
application.keyboardService.addCommandHandler({
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.toggleListPane()
},
}),
application.keyboardService.addCommandHandler({
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.toggleNavigationPane()
},
}),
)
}
setCurrentNavPanelWidth(width: number) {
this.currentNavPanelWidth = width
}
setCurrentItemsPanelWidth(width: number) {
this.currentItemsPanelWidth = width
}
deinit() {
super.deinit()
const mq = window.matchMedia(MediaQueryBreakpoints.md)
if (mq?.removeEventListener != undefined) {
mq.removeEventListener('change', this.mediumScreenMQHandler)
} else {
mq.removeListener(this.mediumScreenMQHandler)
}
}
get currentPane(): AppPaneId {
return this.panes[this.panes.length - 1] || this.panes[0]
}
get previousPane(): AppPaneId {
return this.panes[this.panes.length - 2] || this.panes[0]
}
mediumScreenMQHandler = (event: MediaQueryListEvent) => {
if (event.matches) {
this.setIsInMobileView(false)
} else {
this.setIsInMobileView(true)
}
}
setIsInMobileView = (isInMobileView: boolean) => {
this.isInMobileView = isInMobileView
}
setPaneLayout = (layout: PaneLayout) => {
log(LoggingDomain.Panes, 'Set pane layout', layout)
this.replacePanes(panesForLayout(layout, this.application))
}
replacePanes = (panes: AppPaneId[]) => {
log(LoggingDomain.Panes, 'Replacing panes', panes)
this.panes = panes
}
presentPane = (pane: AppPaneId) => {
log(LoggingDomain.Panes, 'Presenting pane', pane)
if (pane === this.currentPane) {
return
}
if (pane === AppPaneId.Items && this.currentPane === AppPaneId.Editor) {
this.dismissLastPane()
return
}
if (this.currentPane !== pane) {
this.panes.push(pane)
}
}
insertPaneAtIndex = (pane: AppPaneId, index: number) => {
log(LoggingDomain.Panes, 'Inserting pane', pane, 'at index', index)
this.panes.splice(index, 0, pane)
}
dismissLastPane = (): AppPaneId | undefined => {
log(LoggingDomain.Panes, 'Dismissing last pane')
return this.panes.pop()
}
removePane = (pane: AppPaneId) => {
log(LoggingDomain.Panes, 'Removing pane', pane)
removeFromArray(this.panes, pane)
}
popToPane = (pane: AppPaneId) => {
log(LoggingDomain.Panes, 'Popping to pane', pane)
let index = this.panes.length - 1
while (index >= 0) {
if (this.panes[index] === pane) {
break
}
this.dismissLastPane()
index--
}
}
toggleListPane = () => {
if (this.panes.includes(AppPaneId.Items)) {
this.removePane(AppPaneId.Items)
} else {
if (this.panes.includes(AppPaneId.Navigation)) {
this.insertPaneAtIndex(AppPaneId.Items, 1)
} else {
this.insertPaneAtIndex(AppPaneId.Items, 0)
}
}
}
toggleNavigationPane = () => {
if (this.panes.includes(AppPaneId.Navigation)) {
this.removePane(AppPaneId.Navigation)
} else {
this.insertPaneAtIndex(AppPaneId.Navigation, 0)
}
}
get isListPaneCollapsed() {
return !this.panes.includes(AppPaneId.Items)
}
get isNavigationPaneCollapsed() {
return !this.panes.includes(AppPaneId.Navigation)
}
setFocusModeEnabled = (enabled: boolean): void => {
this.focusModeEnabled = enabled
if (enabled) {
document.body.classList.add(FOCUS_MODE_CLASS_NAME)
return
}
if (document.body.classList.contains(FOCUS_MODE_CLASS_NAME)) {
document.body.classList.add(DISABLING_FOCUS_MODE_CLASS_NAME)
document.body.classList.remove(FOCUS_MODE_CLASS_NAME)
setTimeout(() => {
document.body.classList.remove(DISABLING_FOCUS_MODE_CLASS_NAME)
}, FOCUS_MODE_ANIMATION_DURATION)
}
}
}

View File

@@ -0,0 +1,6 @@
export enum PaneLayout {
TagSelection = 'tag-selection',
ItemSelection = 'item-selection',
FilesView = 'files-view',
Editing = 'editing',
}

View File

@@ -0,0 +1,47 @@
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
import { PaneLayout } from './PaneLayout'
import { WebApplication } from '@/Application/Application'
import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen'
export function panesForLayout(layout: PaneLayout, application: WebApplication): AppPaneId[] {
const screen = getIsTabletOrMobileScreen(application)
if (screen.isTablet) {
if (layout === PaneLayout.TagSelection) {
return [AppPaneId.Navigation, AppPaneId.Items]
} else if (
layout === PaneLayout.ItemSelection ||
layout === PaneLayout.Editing ||
layout === PaneLayout.FilesView
) {
return [AppPaneId.Items, AppPaneId.Editor]
}
} else if (screen.isMobile) {
if (layout === PaneLayout.TagSelection) {
return [AppPaneId.Navigation]
} else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.FilesView) {
return [AppPaneId.Navigation, AppPaneId.Items]
} else if (layout === PaneLayout.Editing) {
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
}
} else {
if (layout === PaneLayout.FilesView) {
return [AppPaneId.Navigation, AppPaneId.Items]
} else {
return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]
}
}
throw Error(`Unhandled pane layout ${layout}`)
}
export function isPanesChangeLeafDismiss(from: AppPaneId[], to: AppPaneId[]): boolean {
const fromWithoutLast = from.slice(0, from.length - 1)
return fromWithoutLast.length === to.length && fromWithoutLast.every((pane, index) => pane === to[index])
}
export function isPanesChangePush(from: AppPaneId[], to: AppPaneId[]): boolean {
const toWithoutLast = to.slice(0, to.length - 1)
return toWithoutLast.length === from.length && toWithoutLast.every((pane, index) => pane === from[index])
}

View File

@@ -2,13 +2,10 @@ import { InternalEventBus } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/Application'
import { action, makeObservable, observable } from 'mobx'
import { AbstractViewController } from './Abstract/AbstractViewController'
import { TOGGLE_FOCUS_MODE_COMMAND } from '@standardnotes/ui-services'
import { toggleFocusMode } from '@/Utils/toggleFocusMode'
export class QuickSettingsController extends AbstractViewController {
open = false
shouldAnimateCloseMenu = false
focusModeEnabled = false
constructor(application: WebApplication, eventBus: InternalEventBus) {
super(application, eventBus)
@@ -16,25 +13,12 @@ export class QuickSettingsController extends AbstractViewController {
makeObservable(this, {
open: observable,
shouldAnimateCloseMenu: observable,
focusModeEnabled: observable,
setOpen: action,
setShouldAnimateCloseMenu: action,
setFocusModeEnabled: action,
toggle: action,
closeQuickSettingsMenu: action,
})
this.disposers.push(
application.keyboardService.addCommandHandler({
command: TOGGLE_FOCUS_MODE_COMMAND,
onKeyDown: (event) => {
event.preventDefault()
this.setFocusModeEnabled(!this.focusModeEnabled)
return true
},
}),
)
}
setOpen = (open: boolean): void => {
@@ -45,12 +29,6 @@ export class QuickSettingsController extends AbstractViewController {
this.shouldAnimateCloseMenu = shouldAnimate
}
setFocusModeEnabled = (enabled: boolean): void => {
this.focusModeEnabled = enabled
toggleFocusMode(enabled)
}
toggle = (): void => {
if (this.open) {
this.closeQuickSettingsMenu()

View File

@@ -1,3 +1,4 @@
import { isMobileScreen } from '@/Utils'
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
import { log, LoggingDomain } from '@/Logging'
import {
@@ -18,6 +19,7 @@ import { AbstractViewController } from './Abstract/AbstractViewController'
import { Persistable } from './Abstract/Persistable'
import { CrossControllerEvent } from './CrossControllerEvent'
import { ItemListController } from './ItemList/ItemListController'
import { PaneLayout } from './PaneController/PaneLayout'
export class SelectedItemsController
extends AbstractViewController
@@ -139,6 +141,7 @@ export class SelectedItemsController
}
setSelectedUuids = (selectedUuids: Set<UuidString>) => {
log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids)
this.selectedUuids = new Set(selectedUuids)
this.setSelectedItems()
}
@@ -150,6 +153,7 @@ export class SelectedItemsController
}
public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => {
log(LoggingDomain.Selection, 'Deselecting item', item.uuid)
this.removeSelectedItem(item.uuid)
if (item.uuid === this.lastSelectedItem?.uuid) {
@@ -228,7 +232,7 @@ export class SelectedItemsController
this.lastSelectedItem = undefined
}
openSingleSelectedItem = async () => {
openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => {
if (this.selectedItemsCount === 1) {
const item = this.firstSelectedItem
@@ -237,6 +241,10 @@ export class SelectedItemsController
} else if (item.content_type === ContentType.File) {
await this.itemListController.openFile(item.uuid)
}
if (!this.application.paneController.isInMobileView || userTriggered) {
void this.application.paneController.setPaneLayout(PaneLayout.Editing)
}
}
}
@@ -254,7 +262,7 @@ export class SelectedItemsController
}
}
log(LoggingDomain.Selection, 'selectItem', item.uuid)
log(LoggingDomain.Selection, 'Select item', item.uuid)
const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta)
const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl)
@@ -278,7 +286,7 @@ export class SelectedItemsController
}
}
await this.openSingleSelectedItem()
await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false })
return {
didSelect: this.selectedUuids.has(uuid),
@@ -293,7 +301,9 @@ export class SelectedItemsController
): Promise<void> => {
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
if (didSelect && scrollIntoView) {
const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen()
if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) {
this.scrollToItem(item, animated)
}
}
@@ -319,11 +329,11 @@ export class SelectedItemsController
this.setSelectedUuids(new Set(Uuids(itemsForUuids)))
if (itemsForUuids.length === 1) {
void this.openSingleSelectedItem()
void this.openSingleSelectedItem({ userTriggered })
}
}
selectNextItem = () => {
selectNextItem = ({ userTriggered } = { userTriggered: true }) => {
const displayableItems = this.itemListController.items
const currentIndex = displayableItems.findIndex((candidate) => {
@@ -341,7 +351,7 @@ export class SelectedItemsController
continue
}
this.selectItemWithScrollHandling(nextItem, { userTriggered: true }).catch(console.error)
this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error)
const nextNoteElement = document.getElementById(nextItem.uuid)

View File

@@ -1,4 +1,4 @@
import { PaneController } from './PaneController'
import { PaneController } from './PaneController/PaneController'
import {
PersistedStateValue,
PersistenceKey,

View File

@@ -0,0 +1,19 @@
import { ForwardedRef, useEffect, useRef } from 'react'
export const useForwardedRef = <T,>(ref: ForwardedRef<T>, initialValue = null) => {
const targetRef = useRef<T>(initialValue)
useEffect(() => {
if (!ref) {
return
}
if (typeof ref === 'function') {
ref(targetRef.current)
} else {
ref.current = targetRef.current
}
}, [ref])
return targetRef
}

View File

@@ -0,0 +1,37 @@
import { WebApplication } from '@/Application/Application'
import { useApplication } from '@/Components/ApplicationProvider'
import { isMobileScreen, isTabletOrMobileScreen, isTabletScreen } from '@/Utils'
import { useEffect, useState } from 'react'
export function getIsTabletOrMobileScreen(application: WebApplication) {
const isNativeMobile = application.isNativeMobileWeb()
const isTabletOrMobile = isTabletOrMobileScreen() || isNativeMobile
const isTablet = isTabletScreen() || (isNativeMobile && !isMobileScreen())
const isMobile = isMobileScreen() || (isNativeMobile && !isTablet)
return {
isTabletOrMobile,
isTablet,
isMobile,
}
}
export default function useIsTabletOrMobileScreen() {
const [_windowSize, setWindowSize] = useState(0)
const application = useApplication()
useEffect(() => {
const handleResize = () => {
setWindowSize(window.innerWidth)
}
window.addEventListener('resize', handleResize)
handleResize()
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return getIsTabletOrMobileScreen(application)
}

View File

@@ -10,6 +10,7 @@ export enum LoggingDomain {
Selection,
BlockEditor,
Purchasing,
Panes,
}
const LoggingStatus: Record<LoggingDomain, boolean> = {
@@ -21,6 +22,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
[LoggingDomain.Selection]: false,
[LoggingDomain.BlockEditor]: false,
[LoggingDomain.Purchasing]: false,
[LoggingDomain.Panes]: false,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,4 +1,5 @@
export type PanelResizedData = {
panel: string
collapsed: boolean
width: number
}

View File

@@ -205,9 +205,12 @@ export const disableIosTextFieldZoom = () => {
}
export const isMobileScreen = () => !window.matchMedia(MediaQueryBreakpoints.md).matches
export const isTabletScreen = () =>
!window.matchMedia(MediaQueryBreakpoints.sm).matches && !window.matchMedia(MediaQueryBreakpoints.lg).matches
export const isTabletOrMobileScreen = () => isMobileScreen() || isTabletScreen()
export const getBase64FromBlob = (blob: Blob) => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()

View File

@@ -1,20 +0,0 @@
import { focusModeAnimationDuration } from '../Components/QuickSettingsMenu/QuickSettingsMenu'
export const FOCUS_MODE_CLASS_NAME = 'focus-mode'
export const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode'
export const toggleFocusMode = (enabled: boolean) => {
if (enabled) {
document.body.classList.add(FOCUS_MODE_CLASS_NAME)
return
}
if (document.body.classList.contains(FOCUS_MODE_CLASS_NAME)) {
document.body.classList.add(DISABLING_FOCUS_MODE_CLASS_NAME)
document.body.classList.remove(FOCUS_MODE_CLASS_NAME)
setTimeout(() => {
document.body.classList.remove(DISABLING_FOCUS_MODE_CLASS_NAME)
}, focusModeAnimationDuration)
}
}

View File

@@ -1,7 +1,3 @@
.animate-slide-in-top {
animation: slide-in-top 0.1s ease-out;
}
@keyframes slide-in-top {
0% {
opacity: 0;

View File

@@ -1,39 +0,0 @@
.app-column-container {
display: flex;
flex-direction: column;
@media screen and (min-width: 768px) {
display: grid;
grid-template-rows: auto;
grid-template-columns: auto auto 2fr;
}
}
.app-column {
overflow: hidden;
.content {
height: 100%;
}
@media screen and (max-width: 768px) {
&[data-selected-pane] {
flex-grow: 1;
.content {
overflow-y: auto;
}
}
#editor-content {
width: 100% !important;
left: 0 !important;
}
&:not([data-selected-pane]) {
min-height: 0;
height: auto;
padding: 0;
}
}
}

View File

@@ -5,6 +5,10 @@
padding-top: 35px;
}
.app {
transition: grid-template-columns 0.25s;
}
.mac-desktop #editor-column:before {
content: '';
display: block;
@@ -28,8 +32,8 @@
background-color: var(--sn-stylekit-contrast-background-color);
.content {
box-shadow: 0 0 4px 1px var(--sn-stylekit-shadow-color);
.editor {
box-shadow: 0 0 5px 0.5px var(--sn-stylekit-shadow-color);
overflow: hidden;
}

View File

@@ -6,7 +6,6 @@
user-select: none;
@media screen and (min-width: 768px) {
border-left: 1px solid var(--items-column-border-left-color);
border-right: 1px solid var(--items-column-border-right-color);
}

View File

@@ -156,7 +156,6 @@ body,
width: 100%;
.section {
position: relative;
overflow: hidden;
.section-title-bar {

View File

@@ -8,6 +8,10 @@ $content-horizontal-padding: 16px;
display: flex;
flex-direction: column;
@media screen and (min-width: 768px) {
border-right: 1px solid var(--items-column-border-right-color);
}
&,
#navigation-content {
background-color: var(--navigation-column-background-color);

View File

@@ -19,7 +19,6 @@ $blocks-editor-icons-path: '../../../blocks-editor/src/Lexical/Icons';
@import 'preferences';
@import 'focused';
@import 'sn';
@import 'columns';
@import 'animation';
@import '../../../blocks-editor/src/Lexical/Theme/lexical.scss';

View File

@@ -1,6 +1,5 @@
const { merge } = require('webpack-merge')
const config = require('./web.webpack.config.js')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const mergeWithEnvDefaults = require('./web.webpack-defaults.js')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

View File

@@ -6330,7 +6330,6 @@ __metadata:
eslint-config-prettier: ^8.5.0
eslint-plugin-react: ^7.31.10
eslint-plugin-react-hooks: ^4.6.0
html-webpack-plugin: ^5.5.0
identity-obj-proxy: ^3.0.0
jest: ^29.3.1
jest-environment-jsdom: ^29.3.1