diff --git a/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx b/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx index 982231644..12198cefb 100644 --- a/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx +++ b/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx @@ -118,7 +118,7 @@ export const AccountMenu: FunctionComponent = observer( return (
{ return } - this.group = new ApplicationGroup( - props.server, - props.device, - props.enableUnfinished ? Runtime.Dev : Runtime.Prod, - props.websocketUrl, - ) + this.group = new ApplicationGroup(props.server, props.device, props.websocketUrl) window.mainApplicationGroup = this.group diff --git a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx index 2d1d23f3f..80ad7b923 100644 --- a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -6,10 +6,9 @@ import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants' import { alertDialog } from '@/Services/AlertService' import { WebApplication } from '@/UIModels/Application' import { Navigation } from '@/Components/Navigation/Navigation' -import { NotesView } from '@/Components/NotesView/NotesView' import { NoteGroupView } from '@/Components/NoteGroupView/NoteGroupView' import { Footer } from '@/Components/Footer/Footer' -import { SessionsModal } from '@/Components/SessionsModal' +import { SessionsModal } from '@/Components/SessionsModal/SessionsModal' import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesViewWrapper' import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal' import { NotesContextMenu } from '@/Components/NotesContextMenu/NotesContextMenu' @@ -21,9 +20,11 @@ import { PremiumModalProvider } from '@/Hooks/usePremiumModal' import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' import { TagsContextMenu } from '@/Components/Tags/TagContextMenu' import { ToastContainer } from '@standardnotes/stylekit' -import { FilePreviewModal } from '../Files/FilePreviewModal' +import { FilePreviewModal } from '@/Components/Files/FilePreviewModal' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import { isStateDealloced } from '@/UIModels/AppState/AbstractState' +import { ContentListView } from '@/Components/ContentListView/ContentListView' +import { FileContextMenu } from '@/Components/FileContextMenu/FileContextMenu' type Props = { application: WebApplication @@ -211,7 +212,7 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp
- +
@@ -227,6 +228,7 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp <> + = observer( } const premiumModal = usePremiumModal() - const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0] + const note: SNNote | undefined = appState.notes.firstSelectedNote const [open, setOpen] = useState(false) const [position, setPosition] = useState({ @@ -59,10 +59,8 @@ export const AttachedFilesButton: FunctionComponent = observer( const attachedFilesCount = attachedFiles.length useEffect(() => { - application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc') - const unregisterFileStream = application.streamItems(ContentType.File, () => { - setAllFiles(application.items.getDisplayableItems(ContentType.File)) + setAllFiles(application.items.getDisplayableFiles()) if (note) { setAttachedFiles(application.items.getFilesForNote(note)) } @@ -174,7 +172,7 @@ export const AttachedFilesButton: FunctionComponent = observer( } const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { - const authorizedFiles = await application.protections.authorizeProtectedActionForFiles([file], challengeReason) + const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason) const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) return isAuthorized } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx index 1963b9fca..f46485834 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -4,7 +4,7 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure import { FunctionComponent } from 'preact' import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks' import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { PopoverFileItemProps } from './PopoverFileItem' import { PopoverFileItemActionType } from './PopoverFileItemAction' diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index c8f3f019a..169fb734f 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -23,7 +23,7 @@ export const ChangeEditorButton: FunctionComponent = observer( return null } - const note = Object.values(appState.notes.selectedNotes)[0] + const note = appState.notes.firstSelectedNote const [isOpen, setIsOpen] = useState(false) const [isVisible, setIsVisible] = useState(false) const [position, setPosition] = useState({ diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 91c352e81..f6c8bb96b 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -29,7 +29,7 @@ type ChangeEditorMenuProps = { closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeMenu: () => void isVisible: boolean - note: SNNote + note: SNNote | undefined } const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-') @@ -75,97 +75,103 @@ export const ChangeEditorMenu: FunctionComponent = ({ [currentEditor], ) - const selectComponent = async (component: SNComponent | null, note: SNNote) => { - if (component) { - if (component.conflictOf) { - application.mutator - .changeAndSaveItem(component, (mutator) => { - mutator.conflictOf = undefined + const selectComponent = useCallback( + async (component: SNComponent | null, note: SNNote) => { + if (component) { + if (component.conflictOf) { + application.mutator + .changeAndSaveItem(component, (mutator) => { + mutator.conflictOf = undefined + }) + .catch(console.error) + } + } + + const transactions: TransactionalMutation[] = [] + + await application.getAppState().contentListView.insertCurrentIfTemplate() + + if (note.locked) { + application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error) + return + } + + if (!component) { + if (!note.prefersPlainEditor) { + transactions.push({ + itemUuid: note.uuid, + mutate: (m: ItemMutator) => { + const noteMutator = m as NoteMutator + noteMutator.prefersPlainEditor = true + }, }) - .catch(console.error) + } + const currentEditor = application.componentManager.editorForNote(note) + if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) { + transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note)) + } + reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)) + } else if (component.area === ComponentArea.Editor) { + const currentEditor = application.componentManager.editorForNote(note) + if (currentEditor && component.uuid !== currentEditor.uuid) { + transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note)) + } + const prefersPlain = note.prefersPlainEditor + if (prefersPlain) { + transactions.push({ + itemUuid: note.uuid, + mutate: (m: ItemMutator) => { + const noteMutator = m as NoteMutator + noteMutator.prefersPlainEditor = false + }, + }) + } + transactions.push(transactionForAssociateComponentWithCurrentNote(component, note)) } - } - const transactions: TransactionalMutation[] = [] + await application.mutator.runTransactionalMutations(transactions) + /** Dirtying can happen above */ + application.sync.sync().catch(console.error) - await application.getAppState().notesView.insertCurrentIfTemplate() + setCurrentEditor(application.componentManager.editorForNote(note)) + }, + [application], + ) - if (note.locked) { - application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error) - return - } - - if (!component) { - if (!note.prefersPlainEditor) { - transactions.push({ - itemUuid: note.uuid, - mutate: (m: ItemMutator) => { - const noteMutator = m as NoteMutator - noteMutator.prefersPlainEditor = true - }, - }) + const selectEditor = useCallback( + async (itemToBeSelected: EditorMenuItem) => { + if (!itemToBeSelected.isEntitled) { + premiumModal.activate(itemToBeSelected.name) + return } - const currentEditor = application.componentManager.editorForNote(note) - if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) { - transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note)) + + const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component + + if (areBothEditorsPlain) { + return } - reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)) - } else if (component.area === ComponentArea.Editor) { - const currentEditor = application.componentManager.editorForNote(note) - if (currentEditor && component.uuid !== currentEditor.uuid) { - transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note)) + + let shouldSelectEditor = true + + if (itemToBeSelected.component) { + const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert( + currentEditor, + itemToBeSelected.component, + ) + + if (changeRequiresAlert) { + shouldSelectEditor = await application.componentManager.showEditorChangeAlert() + } } - const prefersPlain = note.prefersPlainEditor - if (prefersPlain) { - transactions.push({ - itemUuid: note.uuid, - mutate: (m: ItemMutator) => { - const noteMutator = m as NoteMutator - noteMutator.prefersPlainEditor = false - }, - }) + + if (shouldSelectEditor && note) { + selectComponent(itemToBeSelected.component ?? null, note).catch(console.error) } - transactions.push(transactionForAssociateComponentWithCurrentNote(component, note)) - } - await application.mutator.runTransactionalMutations(transactions) - /** Dirtying can happen above */ - application.sync.sync().catch(console.error) - - setCurrentEditor(application.componentManager.editorForNote(note)) - } - - const selectEditor = async (itemToBeSelected: EditorMenuItem) => { - if (!itemToBeSelected.isEntitled) { - premiumModal.activate(itemToBeSelected.name) - return - } - - const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component - - if (areBothEditorsPlain) { - return - } - - let shouldSelectEditor = true - - if (itemToBeSelected.component) { - const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert( - currentEditor, - itemToBeSelected.component, - ) - - if (changeRequiresAlert) { - shouldSelectEditor = await application.componentManager.showEditorChangeAlert() - } - } - - if (shouldSelectEditor) { - selectComponent(itemToBeSelected.component ?? null, note).catch(console.error) - } - - closeMenu() - } + closeMenu() + }, + [application.componentManager, closeMenu, currentEditor, note, premiumModal, selectComponent], + ) return ( diff --git a/app/assets/javascripts/Components/ContentListView/ContentList.tsx b/app/assets/javascripts/Components/ContentListView/ContentList.tsx new file mode 100644 index 000000000..b249e6429 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ContentList.tsx @@ -0,0 +1,75 @@ +import { WebApplication } from '@/UIModels/Application' +import { KeyboardKey } from '@/Services/IOService' +import { AppState } from '@/UIModels/AppState' +import { UuidString } from '@standardnotes/snjs' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants' +import { ListableContentItem } from './Types/ListableContentItem' +import { ContentListItem } from './ContentListItem' +import { useCallback } from 'preact/hooks' + +type Props = { + application: WebApplication + appState: AppState + items: ListableContentItem[] + selectedItems: Record + paginate: () => void +} + +export const ContentList: FunctionComponent = observer( + ({ application, appState, items, selectedItems, paginate }) => { + const { selectPreviousItem, selectNextItem } = appState.contentListView + const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions + const { sortBy } = appState.contentListView.displayOptions + + const onScroll = useCallback( + (e: Event) => { + const offset = NOTES_LIST_SCROLL_THRESHOLD + const element = e.target as HTMLElement + if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) { + paginate() + } + }, + [paginate], + ) + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === KeyboardKey.Up) { + e.preventDefault() + selectPreviousItem() + } else if (e.key === KeyboardKey.Down) { + e.preventDefault() + selectNextItem() + } + }, + [selectNextItem, selectPreviousItem], + ) + + return ( +
+ {items.map((item) => ( + + ))} +
+ ) + }, +) diff --git a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx new file mode 100644 index 000000000..df29c4870 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx @@ -0,0 +1,36 @@ +import { ContentType, SNTag } from '@standardnotes/snjs' +import { FunctionComponent } from 'preact' +import { FileListItem } from './FileListItem' +import { NoteListItem } from './NoteListItem' +import { AbstractListItemProps } from './Types/AbstractListItemProps' + +export const ContentListItem: FunctionComponent = (props) => { + const getTags = () => { + if (props.hideTags) { + return [] + } + + const selectedTag = props.appState.tags.selected + if (!selectedTag) { + return [] + } + + const tags = props.appState.getItemTags(props.item) + + const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1 + if (isNavigatingOnlyTag) { + return [] + } + + return tags.map((tag) => tag.title).sort() + } + + switch (props.item.content_type) { + case ContentType.Note: + return + case ContentType.File: + return + default: + return null + } +} diff --git a/app/assets/javascripts/Components/NotesView/NotesView.tsx b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx similarity index 78% rename from app/assets/javascripts/Components/NotesView/NotesView.tsx rename to app/assets/javascripts/Components/ContentListView/ContentListView.tsx index 0fed68a54..cf5796d42 100644 --- a/app/assets/javascripts/Components/NotesView/NotesView.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx @@ -6,9 +6,9 @@ import { PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { ContentList } from '@/Components/ContentListView/ContentList' +import { NotesListOptionsMenu } from '@/Components/ContentListView/NotesListOptionsMenu' import { NoAccountWarning } from '@/Components/NoAccountWarning/NoAccountWarning' -import { NotesList } from '@/Components/NotesList/NotesList' -import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu' import { SearchOptions } from '@/Components/SearchOptions/SearchOptions' import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' @@ -20,34 +20,32 @@ type Props = { appState: AppState } -export const NotesView: FunctionComponent = observer(({ application, appState }: Props) => { +export const ContentListView: FunctionComponent = observer(({ application, appState }) => { if (isStateDealloced(appState)) { return null } - const notesViewPanelRef = useRef(null) + const itemsViewPanelRef = useRef(null) const displayOptionsMenuRef = useRef(null) const { completedFullSync, - displayOptions, noteFilterText, optionsSubtitle, panelTitle, - renderedNotes, + renderedItems, + setNoteFilterText, searchBarElement, + selectNextItem, + selectPreviousItem, + onFilterEnter, + clearFilterText, paginate, panelWidth, - } = appState.notesView + createNewNote, + } = appState.contentListView - const { selectedNotes } = appState.notes - - const createNewNote = useCallback(() => appState.notesView.createNewNote(), [appState]) - const onFilterEnter = useCallback(() => appState.notesView.onFilterEnter(), [appState]) - const clearFilterText = useCallback(() => appState.notesView.clearFilterText(), [appState]) - const setNoteFilterText = useCallback((text: string) => appState.notesView.setNoteFilterText(text), [appState]) - const selectNextNote = useCallback(() => appState.notesView.selectNextNote(), [appState]) - const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote(), [appState]) + const { selectedItems } = appState.selectedItems const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [focusedSearch, setFocusedSearch] = useState(false) @@ -76,7 +74,7 @@ export const NotesView: FunctionComponent = observer(({ application, appS if (searchBarElement === document.activeElement) { searchBarElement?.blur() } - selectNextNote() + selectNextItem() }, }) @@ -84,7 +82,7 @@ export const NotesView: FunctionComponent = observer(({ application, appS key: KeyboardKey.Up, element: document.body, onKeyDown: () => { - selectPreviousNote() + selectPreviousItem() }, }) @@ -104,7 +102,7 @@ export const NotesView: FunctionComponent = observer(({ application, appS previousNoteKeyObserver() searchKeyObserver() } - }, [application, createNewNote, selectPreviousNote, searchBarElement, selectNextNote]) + }, [application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem]) const onNoteFilterTextChange = useCallback( (e: Event) => { @@ -144,14 +142,14 @@ export const NotesView: FunctionComponent = observer(({ application, appS return (
-
-
+
+
{panelTitle}
-
+
@@ -227,27 +225,24 @@ export const NotesView: FunctionComponent = observer(({ application, appS
- {completedFullSync && !renderedNotes.length ?

No notes.

: null} - {!completedFullSync && !renderedNotes.length ? ( -

Loading notes...

- ) : null} - {renderedNotes.length ? ( - No items.

: null} + {!completedFullSync && !renderedItems.length ?

Loading...

: null} + {renderedItems.length ? ( + ) : null}
- {notesViewPanelRef.current && ( + {itemsViewPanelRef.current && ( = observer( + ({ application, appState, hideDate, hideIcon, hideTags, item, selected, sortBy, tags }) => { + const openFileContextMenu = useCallback( + (posX: number, posY: number) => { + appState.files.setFileContextMenuLocation({ + x: posX, + y: posY, + }) + appState.files.setShowFileContextMenu(true) + }, + [appState.files], + ) + + const openContextMenu = useCallback( + (posX: number, posY: number) => { + void appState.contentListView.selectItemWithScrollHandling(item, { + userTriggered: true, + scrollIntoView: false, + }) + openFileContextMenu(posX, posY) + }, + [appState.contentListView, item, openFileContextMenu], + ) + + const onClick = useCallback(() => { + void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => { + if (didSelect && appState.selectedItems.selectedItemsCount < 2) { + appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles) + } + }) + }, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item]) + + const IconComponent = () => + getFileIconComponent( + application.iconsController.getIconForFileType((item as FileItem).mimeType), + 'w-5 h-5 flex-shrink-0', + ) + + return ( +
{ + event.preventDefault() + openContextMenu(event.clientX, event.clientY) + }} + > + {!hideIcon ? ( +
+ +
+ ) : ( +
+ )} +
+
+
{item.title}
+
+ + + +
+ +
+ ) + }, +) diff --git a/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx b/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx new file mode 100644 index 000000000..b1146b707 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx @@ -0,0 +1,16 @@ +import { FunctionComponent } from 'preact' +import { ListableContentItem } from './Types/ListableContentItem' + +export const ListItemConflictIndicator: FunctionComponent<{ + item: { + conflictOf?: ListableContentItem['conflictOf'] + } +}> = ({ item }) => { + return item.conflictOf ? ( +
+
+
Conflicted Copy
+
+
+ ) : null +} diff --git a/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx b/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx new file mode 100644 index 000000000..1cfb57dd3 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx @@ -0,0 +1,45 @@ +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon/Icon' +import { ListableContentItem } from './Types/ListableContentItem' + +type Props = { + item: { + locked: ListableContentItem['locked'] + trashed: ListableContentItem['trashed'] + archived: ListableContentItem['archived'] + pinned: ListableContentItem['pinned'] + } + hasFiles?: boolean +} + +export const ListItemFlagIcons: FunctionComponent = ({ item, hasFiles = false }) => { + return ( +
+ {item.locked && ( + + + + )} + {item.trashed && ( + + + + )} + {item.archived && ( + + + + )} + {item.pinned && ( + + + + )} + {hasFiles && ( + + + + )} +
+ ) +} diff --git a/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx b/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx new file mode 100644 index 000000000..94f54a2af --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx @@ -0,0 +1,29 @@ +import { CollectionSort, SortableItem } from '@standardnotes/snjs' +import { FunctionComponent } from 'preact' +import { ListableContentItem } from './Types/ListableContentItem' + +type Props = { + item: { + protected: ListableContentItem['protected'] + updatedAtString?: ListableContentItem['updatedAtString'] + createdAtString?: ListableContentItem['createdAtString'] + } + hideDate: boolean + sortBy: keyof SortableItem | undefined +} + +export const ListItemMetadata: FunctionComponent = ({ item, hideDate, sortBy }) => { + const showModifiedDate = sortBy === CollectionSort.UpdatedAt + + if (hideDate && !item.protected) { + return null + } + + return ( +
+ {item.protected && Protected {hideDate ? '' : ' • '}} + {!hideDate && showModifiedDate && Modified {item.updatedAtString || 'Now'}} + {!hideDate && !showModifiedDate && {item.createdAtString || 'Now'}} +
+ ) +} diff --git a/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx b/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx new file mode 100644 index 000000000..ec29935d2 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx @@ -0,0 +1,22 @@ +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon/Icon' + +export const ListItemTags: FunctionComponent<{ + hideTags: boolean + tags: string[] +}> = ({ hideTags, tags }) => { + if (hideTags || !tags.length) { + return null + } + + return ( +
+ {tags.map((tag) => ( + + + {tag} + + ))} +
+ ) +} diff --git a/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx new file mode 100644 index 000000000..f51b530bf --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx @@ -0,0 +1,84 @@ +import { PLAIN_EDITOR_NAME } from '@/Constants' +import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon/Icon' +import { ListItemConflictIndicator } from './ListItemConflictIndicator' +import { ListItemFlagIcons } from './ListItemFlagIcons' +import { ListItemTags } from './ListItemTags' +import { ListItemMetadata } from './ListItemMetadata' +import { DisplayableListItemProps } from './Types/DisplayableListItemProps' + +export const NoteListItem: FunctionComponent = observer( + ({ application, appState, hideDate, hideIcon, hideTags, hidePreview, item, selected, sortBy, tags }) => { + const editorForNote = application.componentManager.editorForNote(item as SNNote) + const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME + const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) + const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0 + + const openNoteContextMenu = (posX: number, posY: number) => { + appState.notes.setContextMenuClickLocation({ + x: posX, + y: posY, + }) + appState.notes.reloadContextMenuLayout() + appState.notes.setContextMenuOpen(true) + } + + const openContextMenu = (posX: number, posY: number) => { + void appState.selectedItems.selectItem(item.uuid, true) + openNoteContextMenu(posX, posY) + } + + return ( +
{ + void appState.selectedItems.selectItem(item.uuid, true) + }} + onContextMenu={(event) => { + event.preventDefault() + openContextMenu(event.clientX, event.clientY) + }} + > + {!hideIcon ? ( +
+ +
+ ) : ( +
+ )} +
+
+
{item.title}
+
+ {!hidePreview && !item.hidePreview && !item.protected && ( +
+ {item.preview_html && ( +
+ )} + {!item.preview_html && item.preview_plain && ( +
{item.preview_plain}
+ )} + {!item.preview_html && !item.preview_plain && item.text && ( +
{item.text}
+ )} +
+ )} + + + +
+ +
+ ) + }, +) diff --git a/app/assets/javascripts/Components/NotesList/NotesListOptionsMenu.tsx b/app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx similarity index 100% rename from app/assets/javascripts/Components/NotesList/NotesListOptionsMenu.tsx rename to app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx diff --git a/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts b/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts new file mode 100644 index 000000000..64abf5983 --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts @@ -0,0 +1,16 @@ +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' +import { SortableItem } from '@standardnotes/snjs' +import { ListableContentItem } from './ListableContentItem' + +export type AbstractListItemProps = { + application: WebApplication + appState: AppState + hideDate: boolean + hideIcon: boolean + hideTags: boolean + hidePreview: boolean + item: ListableContentItem + selected: boolean + sortBy: keyof SortableItem | undefined +} diff --git a/app/assets/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts b/app/assets/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts new file mode 100644 index 000000000..ab82db9ba --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/Types/DisplayableListItemProps.ts @@ -0,0 +1,5 @@ +import { AbstractListItemProps } from './AbstractListItemProps' + +export type DisplayableListItemProps = AbstractListItemProps & { + tags: string[] +} diff --git a/app/assets/javascripts/Components/ContentListView/Types/ListableContentItem.ts b/app/assets/javascripts/Components/ContentListView/Types/ListableContentItem.ts new file mode 100644 index 000000000..797bc9bcc --- /dev/null +++ b/app/assets/javascripts/Components/ContentListView/Types/ListableContentItem.ts @@ -0,0 +1,14 @@ +import { ContentType, DecryptedItem, ItemContent } from '@standardnotes/snjs' + +export type ListableContentItem = DecryptedItem & { + title: string + protected: boolean + uuid: string + content_type: ContentType + updatedAtString?: string + createdAtString?: string + hidePreview?: boolean + preview_html?: string + preview_plain?: string + text?: string +} diff --git a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx new file mode 100644 index 000000000..7c3d6d9b5 --- /dev/null +++ b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx @@ -0,0 +1,120 @@ +import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants' +import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' +import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' +import { AppState } from '@/UIModels/AppState' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import React from 'react' +import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction' +import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs' +import { FileMenuOptions } from './FileMenuOptions' + +type Props = { + appState: AppState +} + +export const FileContextMenu: FunctionComponent = observer(({ appState }) => { + const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = appState.files + + const [contextMenuStyle, setContextMenuStyle] = useState({ + top: 0, + left: 0, + visibility: 'hidden', + }) + const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState('auto') + const contextMenuRef = useRef(null) + const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open)) + useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false)) + + const selectedFile = Object.values(selectedFiles)[0] + if (!showFileContextMenu || !selectedFile) { + return null + } + + const reloadContextMenuLayout = useCallback(() => { + const { clientHeight } = document.documentElement + const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize + const maxContextMenuHeight = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER + const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() + const footerHeightInPx = footerElementRect?.height + + let openUpBottom = true + + if (footerHeightInPx) { + const bottomSpace = clientHeight - footerHeightInPx - fileContextMenuLocation.y + const upSpace = fileContextMenuLocation.y + + if (maxContextMenuHeight > bottomSpace) { + if (upSpace > maxContextMenuHeight) { + openUpBottom = false + setContextMenuMaxHeight('auto') + } else { + if (upSpace > bottomSpace) { + setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER) + openUpBottom = false + } else { + setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER) + } + } + } else { + setContextMenuMaxHeight('auto') + } + } + + if (openUpBottom) { + setContextMenuStyle({ + top: fileContextMenuLocation.y, + left: fileContextMenuLocation.x, + visibility: 'visible', + }) + } else { + setContextMenuStyle({ + bottom: clientHeight - fileContextMenuLocation.y, + left: fileContextMenuLocation.x, + visibility: 'visible', + }) + } + }, [fileContextMenuLocation.x, fileContextMenuLocation.y]) + + useEffect(() => { + if (showFileContextMenu) { + reloadContextMenuLayout() + } + }, [reloadContextMenuLayout, showFileContextMenu]) + + useEffect(() => { + window.addEventListener('resize', reloadContextMenuLayout) + return () => { + window.removeEventListener('resize', reloadContextMenuLayout) + } + }, [reloadContextMenuLayout]) + + const handleFileAction = useCallback( + async (action: PopoverFileItemAction) => { + const { didHandleAction } = await appState.files.handleFileAction(action, PopoverTabs.AllFiles) + return didHandleAction + }, + [appState.files], + ) + + return ( +
+ setShowFileContextMenu(false)} + shouldShowRenameOption={false} + shouldShowAttachOption={false} + /> +
+ ) +}) diff --git a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx new file mode 100644 index 000000000..17bcbcb2b --- /dev/null +++ b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -0,0 +1,141 @@ +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' +import { FileItem } from '@standardnotes/snjs' +import { FunctionComponent } from 'preact' +import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' +import { Icon } from '@/Components/Icon/Icon' +import { Switch } from '@/Components/Switch/Switch' + +type Props = { + closeMenu: () => void + closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void + file: FileItem + fileProtectionToggleCallback?: (isProtected: boolean) => void + handleFileAction: (action: PopoverFileItemAction) => Promise + isFileAttachedToNote?: boolean + renameToggleCallback?: (isRenamingFile: boolean) => void + shouldShowRenameOption: boolean + shouldShowAttachOption: boolean +} + +export const FileMenuOptions: FunctionComponent = ({ + closeMenu, + closeOnBlur, + file, + fileProtectionToggleCallback, + handleFileAction, + isFileAttachedToNote, + renameToggleCallback, + shouldShowRenameOption, + shouldShowAttachOption, +}) => { + return ( + <> + + {isFileAttachedToNote ? ( + + ) : shouldShowAttachOption ? ( + + ) : null} +
+ +
+ + {shouldShowRenameOption && ( + + )} + + + ) +} diff --git a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx index fd8451bcf..aaf468222 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx @@ -130,7 +130,7 @@ export const FilePreviewModal: FunctionComponent = observer(({ applicatio width: '90%', maxWidth: '90%', minHeight: '90%', - background: 'var(--sn-stylekit-background-color)', + background: 'var(--modal-background-color)', }} >
{ this.reloadUpgradeStatus() this.updateOfflineStatus() this.findErrors() - this.streamItems() } reloadUser() { @@ -217,10 +216,6 @@ export class Footer extends PureComponent { } } - streamItems() { - this.application.items.setDisplayOptions(ContentType.Theme, CollectionSort.Title, 'asc') - } - updateSyncStatus() { const statusManager = this.application.status const syncStatus = this.application.sync.getSyncStatus() diff --git a/app/assets/javascripts/Components/Menu/MenuItem.tsx b/app/assets/javascripts/Components/Menu/MenuItem.tsx index 0afc42f57..aa2be8a30 100644 --- a/app/assets/javascripts/Components/Menu/MenuItem.tsx +++ b/app/assets/javascripts/Components/Menu/MenuItem.tsx @@ -2,7 +2,7 @@ import { ComponentChildren, FunctionComponent, VNode } from 'preact' import { forwardRef, Ref } from 'preact/compat' import { JSXInternal } from 'preact/src/jsx' import { Icon } from '@/Components/Icon/Icon' -import { Switch, SwitchProps } from '@/Components/Switch' +import { Switch, SwitchProps } from '@/Components/Switch/Switch' import { IconType } from '@standardnotes/snjs' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' diff --git a/app/assets/javascripts/Components/NotesList/NotesList.tsx b/app/assets/javascripts/Components/NotesList/NotesList.tsx deleted file mode 100644 index 690cc911a..000000000 --- a/app/assets/javascripts/Components/NotesList/NotesList.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { WebApplication } from '@/UIModels/Application' -import { KeyboardKey } from '@/Services/IOService' -import { AppState } from '@/UIModels/AppState' -import { DisplayOptions } from '@/UIModels/AppState/NotesViewState' -import { SNNote, SNTag } from '@standardnotes/snjs' -import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { NotesListItem } from './NotesListItem' -import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants' -import { useCallback } from 'preact/hooks' - -type Props = { - application: WebApplication - appState: AppState - notes: SNNote[] - selectedNotes: Record - displayOptions: DisplayOptions - paginate: () => void -} - -export const NotesList: FunctionComponent = observer( - ({ application, appState, notes, selectedNotes, displayOptions, paginate }) => { - const selectNextNote = useCallback(() => appState.notesView.selectNextNote(), [appState]) - const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote(), [appState]) - - const { hideTags, hideDate, hideNotePreview, hideEditorIcon, sortBy } = displayOptions - - const tagsForNote = useCallback( - (note: SNNote): string[] => { - if (hideTags) { - return [] - } - const selectedTag = appState.tags.selected - if (!selectedTag) { - return [] - } - const tags = appState.getNoteTags(note) - if (selectedTag instanceof SNTag && tags.length === 1) { - return [] - } - return tags.map((tag) => tag.title).sort() - }, - [appState, hideTags], - ) - - const openNoteContextMenu = useCallback( - (posX: number, posY: number) => { - appState.notes.setContextMenuClickLocation({ - x: posX, - y: posY, - }) - appState.notes.reloadContextMenuLayout() - appState.notes.setContextMenuOpen(true) - }, - [appState], - ) - - const onContextMenu = useCallback( - (note: SNNote, posX: number, posY: number) => { - appState.notes.selectNote(note.uuid, true).catch(console.error) - openNoteContextMenu(posX, posY) - }, - [appState, openNoteContextMenu], - ) - - const onScroll = useCallback( - (e: Event) => { - const offset = NOTES_LIST_SCROLL_THRESHOLD - const element = e.target as HTMLElement - if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) { - paginate() - } - }, - [paginate], - ) - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === KeyboardKey.Up) { - e.preventDefault() - selectPreviousNote() - } else if (e.key === KeyboardKey.Down) { - e.preventDefault() - selectNextNote() - } - }, - [selectNextNote, selectPreviousNote], - ) - - return ( -
- {notes.map((note) => ( - { - appState.notes.selectNote(note.uuid, true).catch(console.error) - }} - onContextMenu={(e: MouseEvent) => { - e.preventDefault() - onContextMenu(note, e.clientX, e.clientY) - }} - /> - ))} -
- ) - }, -) diff --git a/app/assets/javascripts/Components/NotesList/NotesListItem.tsx b/app/assets/javascripts/Components/NotesList/NotesListItem.tsx deleted file mode 100644 index 4b2506536..000000000 --- a/app/assets/javascripts/Components/NotesList/NotesListItem.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { WebApplication } from '@/UIModels/Application' -import { CollectionSort, CollectionSortProperty, sanitizeHtmlString, SNNote } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' -import { PLAIN_EDITOR_NAME } from '@/Constants' - -type Props = { - application: WebApplication - note: SNNote - tags: string[] - hideDate: boolean - hidePreview: boolean - hideTags: boolean - hideEditorIcon: boolean - onClick: () => void - onContextMenu: (e: MouseEvent) => void - selected: boolean - sortedBy?: CollectionSortProperty -} - -type NoteFlag = { - text: string - class: 'info' | 'neutral' | 'warning' | 'success' | 'danger' -} - -const flagsForNote = (note: SNNote) => { - const flags = [] as NoteFlag[] - if (note.conflictOf) { - flags.push({ - text: 'Conflicted Copy', - class: 'danger', - }) - } - - return flags -} - -export const NotesListItem: FunctionComponent = ({ - application, - hideDate, - hidePreview, - hideTags, - hideEditorIcon, - note, - onClick, - onContextMenu, - selected, - sortedBy, - tags, -}) => { - const flags = flagsForNote(note) - const hasFiles = application.items.getFilesForNote(note).length > 0 - const showModifiedDate = sortedBy === CollectionSort.UpdatedAt - const editorForNote = application.componentManager.editorForNote(note) - const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME - const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) - - return ( -
- {!hideEditorIcon && ( -
- -
- )} -
-
{note.title.length ?
{note.title}
: null}
- {!hidePreview && !note.hidePreview && !note.protected && ( -
- {note.preview_html && ( -
- )} - {!note.preview_html && note.preview_plain &&
{note.preview_plain}
} - {!note.preview_html && !note.preview_plain && note.text && ( -
{note.text}
- )} -
- )} - {!hideDate || note.protected ? ( -
- {note.protected && Protected {hideDate ? '' : ' • '}} - {!hideDate && showModifiedDate && Modified {note.updatedAtString || 'Now'}} - {!hideDate && !showModifiedDate && {note.createdAtString || 'Now'}} -
- ) : null} - {!hideTags && tags.length ? ( -
- {tags.map((tag) => ( - - - {tag} - - ))} -
- ) : null} - {flags.length ? ( -
- {flags.map((flag) => ( -
-
{flag.text}
-
- ))} -
- ) : null} -
-
- {note.locked && ( - - - - )} - {note.trashed && ( - - - - )} - {note.archived && ( - - - - )} - {note.pinned && ( - - - - )} - {hasFiles && ( - - - - )} -
-
- ) -} diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx index d8a823b8c..3c8f2b006 100644 --- a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -1,6 +1,6 @@ import { AppState } from '@/UIModels/AppState' import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { observer } from 'mobx-react-lite' import { useState, useEffect, useMemo, useCallback } from 'preact/hooks' import { SNApplication, SNNote } from '@standardnotes/snjs' diff --git a/app/assets/javascripts/Components/Preferences/Panes/Appearance.tsx b/app/assets/javascripts/Components/Preferences/Panes/Appearance.tsx index cba7f95ce..dfa4cced4 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Appearance.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Appearance.tsx @@ -1,7 +1,7 @@ import { Dropdown, DropdownItem } from '@/Components/Dropdown/Dropdown' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { WebApplication } from '@/UIModels/Application' import { ContentType, FeatureIdentifier, FeatureStatus, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' @@ -43,8 +43,9 @@ export const Appearance: FunctionComponent = observer(({ application }) = useEffect(() => { const themesAsItems: DropdownItem[] = application.items - .getDisplayableItems(ContentType.Theme) - .filter((theme) => !theme.isLayerable()) + .getDisplayableComponents() + .filter((component) => component.isTheme()) + .filter((component) => !(component as SNTheme).isLayerable()) .sort(sortThemes) .map((theme) => { return { diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx index 6b98d9c2a..baa8a0b2a 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx @@ -18,7 +18,7 @@ import { } from '@standardnotes/snjs' import { FunctionComponent } from 'preact' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { convertStringifiedBooleanToBoolean } from '@/Utils' import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings' diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx index 8eab28e24..f5e55926c 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx @@ -11,7 +11,7 @@ import { Title, } from '@/Components/Preferences/PreferencesComponents' import { Dropdown, DropdownItem } from '@/Components/Dropdown/Dropdown' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' import { FeatureStatus, diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/Files/FileBackups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/Files/FileBackups.tsx index 44e657a27..e611acace 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/Files/FileBackups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/Files/FileBackups.tsx @@ -9,7 +9,7 @@ import { } from '@/Components/Preferences/PreferencesComponents' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import { Button } from '@/Components/Button/Button' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' import { EncryptionStatusItem } from '../../Security/Encryption' import { Icon } from '@/Components/Icon/Icon' diff --git a/app/assets/javascripts/Components/Preferences/Panes/Extensions/ExtensionItem.tsx b/app/assets/javascripts/Components/Preferences/Panes/Extensions/ExtensionItem.tsx index 6baa6030a..35cbb38dd 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Extensions/ExtensionItem.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Extensions/ExtensionItem.tsx @@ -1,7 +1,7 @@ import { FunctionComponent } from 'preact' import { SNComponent } from '@standardnotes/snjs' import { PreferencesSegment, SubtitleLight } from '@/Components/Preferences/PreferencesComponents' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { WebApplication } from '@/UIModels/Application' import { useState } from 'preact/hooks' import { Button } from '@/Components/Button/Button' diff --git a/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx b/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx index afa3ea097..c68c0290d 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx @@ -11,7 +11,7 @@ import { WebApplication } from '@/UIModels/Application' import { FunctionComponent } from 'preact' import { useEffect, useState } from 'preact/hooks' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { PLAIN_EDITOR_NAME } from '@/Constants' type Props = { diff --git a/app/assets/javascripts/Components/Preferences/Panes/General/Labs.tsx b/app/assets/javascripts/Components/Preferences/Panes/General/Labs.tsx index ffb4bb11b..6650642d4 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/General/Labs.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/General/Labs.tsx @@ -1,4 +1,4 @@ -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { PreferencesGroup, PreferencesSegment, diff --git a/app/assets/javascripts/Components/Preferences/Panes/General/Tools.tsx b/app/assets/javascripts/Components/Preferences/Panes/General/Tools.tsx index b08685335..098799a3d 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/General/Tools.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/General/Tools.tsx @@ -1,5 +1,5 @@ import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { PreferencesGroup, PreferencesSegment, diff --git a/app/assets/javascripts/Components/Preferences/Panes/Security/Privacy.tsx b/app/assets/javascripts/Components/Preferences/Panes/Security/Privacy.tsx index 237a80421..fdb44b16b 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Security/Privacy.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Security/Privacy.tsx @@ -1,5 +1,5 @@ import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { PreferencesGroup, PreferencesSegment, diff --git a/app/assets/javascripts/Components/Preferences/Panes/TwoFactorAuth/TwoFactorAuthView.tsx b/app/assets/javascripts/Components/Preferences/Panes/TwoFactorAuth/TwoFactorAuthView.tsx index c9ab5fbfc..a657f478a 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/TwoFactorAuth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/TwoFactorAuth/TwoFactorAuthView.tsx @@ -1,6 +1,6 @@ import { FunctionComponent } from 'preact' import { Title, Text, PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { observer } from 'mobx-react-lite' import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth' import { TwoFactorActivationView } from './TwoFactorActivationView' diff --git a/app/assets/javascripts/Components/QuickSettingsMenu/FocusModeSwitch.tsx b/app/assets/javascripts/Components/QuickSettingsMenu/FocusModeSwitch.tsx index 01f1ba8ef..c800c52a1 100644 --- a/app/assets/javascripts/Components/QuickSettingsMenu/FocusModeSwitch.tsx +++ b/app/assets/javascripts/Components/QuickSettingsMenu/FocusModeSwitch.tsx @@ -5,7 +5,7 @@ import { useCallback } from 'preact/hooks' import { JSXInternal } from 'preact/src/jsx' import { Icon } from '@/Components/Icon/Icon' import { usePremiumModal } from '@/Hooks/usePremiumModal' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' type Props = { application: WebApplication diff --git a/app/assets/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/app/assets/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index 3d7d27fe7..3bbbc29c1 100644 --- a/app/assets/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/app/assets/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -1,13 +1,13 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' -import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent, SNTheme } from '@standardnotes/snjs' +import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { JSXInternal } from 'preact/src/jsx' import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers' import { FocusModeSwitch } from './FocusModeSwitch' @@ -18,7 +18,7 @@ import { sortThemes } from '@/Utils/SortThemes' const focusModeAnimationDuration = 1255 -const MENU_CLASSNAME = 'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto' +const MENU_CLASSNAME = 'sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto' type MenuProps = { appState: AppState @@ -65,13 +65,16 @@ export const QuickSettingsMenu: FunctionComponent = observer(({ appli }, [focusModeEnabled]) const reloadThemes = useCallback(() => { - const themes = application.items.getDisplayableItems(ContentType.Theme).map((item) => { - return { - name: item.displayName, - identifier: item.identifier, - component: item, - } - }) as ThemeItem[] + const themes = application.items + .getDisplayableComponents() + .filter((component) => component.isTheme()) + .map((item) => { + return { + name: item.displayName, + identifier: item.identifier, + component: item, + } + }) as ThemeItem[] GetFeatures() .filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable) @@ -91,9 +94,10 @@ export const QuickSettingsMenu: FunctionComponent = observer(({ appli const reloadToggleableComponents = useCallback(() => { const toggleableComponents = application.items - .getDisplayableItems(ContentType.Component) + .getDisplayableComponents() .filter( (component) => + !component.isTheme() && [ComponentArea.EditorStack].includes(component.area) && component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent, ) diff --git a/app/assets/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx b/app/assets/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx index 7770d2f4d..ce3bef119 100644 --- a/app/assets/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx +++ b/app/assets/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'preact/hooks' import { JSXInternal } from 'preact/src/jsx' import { Icon } from '@/Components/Icon/Icon' import { usePremiumModal } from '@/Hooks/usePremiumModal' -import { Switch } from '@/Components/Switch' +import { Switch } from '@/Components/Switch/Switch' import { ThemeItem } from './ThemeItem' type Props = { diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx index 6e7dad12b..e5878369d 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx @@ -57,13 +57,17 @@ export const RevisionHistoryModal: FunctionComponent ({ application, appState }) => { const closeButtonRef = useRef(null) - const dismissModal = () => { + const dismissModal = useCallback(() => { appState.notes.setShowRevisionHistoryModal(false) - } + }, [appState.notes]) - const note = Object.values(appState.notes.selectedNotes)[0] + const note = appState.notes.firstSelectedNote const editorForCurrentNote = useMemo(() => { - return application.componentManager.editorForNote(note) + if (note) { + return application.componentManager.editorForNote(note) + } else { + return undefined + } }, [application, note]) const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false) @@ -100,7 +104,7 @@ export const RevisionHistoryModal: FunctionComponent } }, [fetchRemoteHistory, remoteHistory?.length]) - const restore = () => { + const restore = useCallback(() => { if (selectedRevision) { const originalNote = application.items.findItem(selectedRevision.payload.uuid) as SNNote @@ -130,9 +134,9 @@ export const RevisionHistoryModal: FunctionComponent }) .catch(console.error) } - } + }, [application.alertService, application.items, application.mutator, dismissModal, selectedRevision]) - const restoreAsCopy = async () => { + const restoreAsCopy = useCallback(async () => { if (selectedRevision) { const originalNote = application.items.findSureItem(selectedRevision.payload.uuid) @@ -143,11 +147,11 @@ export const RevisionHistoryModal: FunctionComponent : undefined, }) - appState.notes.selectNote(duplicatedItem.uuid).catch(console.error) + appState.selectedItems.selectItem(duplicatedItem.uuid).catch(console.error) dismissModal() } - } + }, [appState.selectedItems, application.items, application.mutator, dismissModal, selectedRevision]) useEffect(() => { const fetchTemplateNote = async () => { @@ -164,7 +168,7 @@ export const RevisionHistoryModal: FunctionComponent fetchTemplateNote().catch(console.error) }, [application, selectedRevision]) - const deleteSelectedRevision = () => { + const deleteSelectedRevision = useCallback(() => { if (!selectedRemoteEntry) { return } @@ -178,7 +182,7 @@ export const RevisionHistoryModal: FunctionComponent 'Cancel', ) .then((shouldDelete) => { - if (shouldDelete) { + if (shouldDelete && note) { setIsDeletingRevision(true) application.historyManager @@ -195,7 +199,7 @@ export const RevisionHistoryModal: FunctionComponent } }) .catch(console.error) - } + }, [application.alertService, application.historyManager, fetchRemoteHistory, note, selectedRemoteEntry]) return ( width: '90%', maxWidth: '90%', minHeight: '90%', - background: 'var(--sn-stylekit-background-color)', + background: 'var(--modal-background-color)', }} >
}`} >
- + {note && ( + + )}
{ if (view.uuid === SystemViewId.AllNotes) { return 'notes' } + if (view.uuid === SystemViewId.Files) { + return 'file' + } if (view.uuid === SystemViewId.ArchivedNotes) { return 'archive' } diff --git a/app/assets/javascripts/Services/ThemeManager.ts b/app/assets/javascripts/Services/ThemeManager.ts index 50a3d672f..d0b69c606 100644 --- a/app/assets/javascripts/Services/ThemeManager.ts +++ b/app/assets/javascripts/Services/ThemeManager.ts @@ -167,7 +167,9 @@ export class ThemeManager extends ApplicationService { private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) { const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier - const themes = this.application.items.getDisplayableItems(ContentType.Theme) as SNTheme[] + const themes = this.application.items + .getDisplayableComponents() + .filter((component) => component.isTheme()) as SNTheme[] const activeTheme = themes.find((theme) => theme.active && !theme.isLayerable()) const activeThemeIdentifier = activeTheme ? activeTheme.identifier : DefaultThemeIdentifier diff --git a/app/assets/javascripts/UIModels/AppState/AppState.ts b/app/assets/javascripts/UIModels/AppState/AppState.ts index 4e85b1656..fcee9724c 100644 --- a/app/assets/javascripts/UIModels/AppState/AppState.ts +++ b/app/assets/javascripts/UIModels/AppState/AppState.ts @@ -7,7 +7,6 @@ import { ContentType, DeinitSource, PrefKey, - SNNote, SNTag, removeFromArray, WebOrDesktopDeviceInterface, @@ -17,7 +16,7 @@ import { ActionsMenuState } from './ActionsMenuState' import { FeaturesState } from './FeaturesState' import { FilesState } from './FilesState' import { NotesState } from './NotesState' -import { NotesViewState } from './NotesViewState' +import { ContentListViewState } from './ContentListViewState' import { NoteTagsState } from './NoteTagsState' import { NoAccountWarningState } from './NoAccountWarningState' import { PreferencesState } from './PreferencesState' @@ -29,6 +28,8 @@ import { SyncState } from './SyncState' import { TagsState } from './TagsState' import { FilePreviewModalState } from './FilePreviewModalState' import { AbstractState } from './AbstractState' +import { SelectedItemsState } from './SelectedItemsState' +import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' export enum AppStateEvent { TagChanged, @@ -70,7 +71,7 @@ export class AppState extends AbstractState { readonly files: FilesState readonly noAccountWarning: NoAccountWarningState readonly notes: NotesState - readonly notesView: NotesViewState + readonly contentListView: ContentListViewState readonly noteTags: NoteTagsState readonly preferences = new PreferencesState() readonly purchaseFlow: PurchaseFlowState @@ -79,6 +80,7 @@ export class AppState extends AbstractState { readonly subscription: SubscriptionState readonly sync = new SyncState() readonly tags: TagsState + readonly selectedItems: SelectedItemsState isSessionsModalVisible = false @@ -89,6 +91,7 @@ export class AppState extends AbstractState { constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) { super(application) + this.selectedItems = new SelectedItemsState(application, this, this.appEventObserverRemovers) this.notes = new NotesState( application, this, @@ -106,8 +109,8 @@ export class AppState extends AbstractState { this.searchOptions = new SearchOptionsState(application, this.appEventObserverRemovers) this.subscription = new SubscriptionState(application, this.appEventObserverRemovers) this.purchaseFlow = new PurchaseFlowState(application) - this.notesView = new NotesViewState(application, this, this.appEventObserverRemovers) - this.files = new FilesState(application) + this.contentListView = new ContentListViewState(application, this, this.appEventObserverRemovers) + this.files = new FilesState(application, this, this.appEventObserverRemovers) this.addAppEventObserver() this.onVisibilityChange = () => { const visible = document.visibilityState === 'visible' @@ -177,8 +180,8 @@ export class AppState extends AbstractState { this.notes.deinit(source) ;(this.notes as unknown) = undefined - this.notesView.deinit(source) - ;(this.notesView as unknown) = undefined + this.contentListView.deinit(source) + ;(this.contentListView as unknown) = undefined this.noteTags.deinit(source) ;(this.noteTags as unknown) = undefined @@ -321,8 +324,8 @@ export class AppState extends AbstractState { } /** Returns the tags that are referncing this note */ - public getNoteTags(note: SNNote) { - return this.application.items.itemsReferencingItem(note).filter((ref) => { + public getItemTags(item: ListableContentItem) { + return this.application.items.itemsReferencingItem(item).filter((ref) => { return ref.content_type === ContentType.Tag }) as SNTag[] } diff --git a/app/assets/javascripts/UIModels/AppState/NotesViewState.ts b/app/assets/javascripts/UIModels/AppState/ContentListViewState.ts similarity index 62% rename from app/assets/javascripts/UIModels/AppState/NotesViewState.ts rename to app/assets/javascripts/UIModels/AppState/ContentListViewState.ts index df6bcb876..f413c5e8d 100644 --- a/app/assets/javascripts/UIModels/AppState/NotesViewState.ts +++ b/app/assets/javascripts/UIModels/AppState/ContentListViewState.ts @@ -1,72 +1,64 @@ +import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' import { destroyAllObjectProperties } from '@/Utils' import { ApplicationEvent, CollectionSort, - CollectionSortProperty, ContentType, DeinitSource, findInArray, - NotesDisplayCriteria, NoteViewController, PrefKey, SmartView, SNNote, SNTag, SystemViewId, + DisplayOptions, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' import { AppState, AppStateEvent } from '.' import { WebApplication } from '../Application' import { AbstractState } from './AbstractState' +import { WebDisplayOptions } from './WebDisplayOptions' -const MIN_NOTE_CELL_HEIGHT = 51.0 -const DEFAULT_LIST_NUM_NOTES = 20 -const ELEMENT_ID_SEARCH_BAR = 'search-bar' -const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable' +const MinNoteCellHeight = 51.0 +const DefaultListNumNotes = 20 +const ElementIdSearchBar = 'search-bar' +const ElementIdScrollContainer = 'notes-scrollable' +const SupportsFileSelectionState = false -export type DisplayOptions = { - sortBy: CollectionSortProperty - sortReverse: boolean - hidePinned: boolean - showArchived: boolean - showTrashed: boolean - hideProtected: boolean - hideTags: boolean - hideNotePreview: boolean - hideDate: boolean - hideEditorIcon: boolean -} - -export class NotesViewState extends AbstractState { +export class ContentListViewState extends AbstractState { completedFullSync = false noteFilterText = '' notes: SNNote[] = [] + items: ListableContentItem[] = [] notesToDisplay = 0 pageSize = 0 panelTitle = 'All Notes' panelWidth = 0 - renderedNotes: SNNote[] = [] + renderedItems: ListableContentItem[] = [] searchSubmitted = false showDisplayOptionsMenu = false - displayOptions = { + displayOptions: DisplayOptions = { sortBy: CollectionSort.CreatedAt, - sortReverse: false, - hidePinned: false, - showArchived: false, - showTrashed: false, - hideProtected: false, + sortDirection: 'dsc', + includePinned: true, + includeArchived: false, + includeTrashed: false, + includeProtected: true, + } + webDisplayOptions: WebDisplayOptions = { hideTags: true, hideDate: false, hideNotePreview: false, hideEditorIcon: false, } - private reloadNotesPromise?: Promise + private reloadItemsPromise?: Promise override deinit(source: DeinitSource) { super.deinit(source) ;(this.noteFilterText as unknown) = undefined ;(this.notes as unknown) = undefined - ;(this.renderedNotes as unknown) = undefined + ;(this.renderedItems as unknown) = undefined ;(window.onresize as unknown) = undefined destroyAllObjectProperties(this) @@ -79,7 +71,7 @@ export class NotesViewState extends AbstractState { appObservers.push( application.streamItems(ContentType.Note, () => { - void this.reloadNotes() + void this.reloadItems() }), application.streamItems([ContentType.Tag], async ({ changed, inserted }) => { @@ -88,7 +80,7 @@ export class NotesViewState extends AbstractState { /** A tag could have changed its relationships, so we need to reload the filter */ this.reloadNotesDisplayOptions() - void this.reloadNotes() + void this.reloadItems() if (appState.tags.selected && findInArray(tags, 'uuid', appState.tags.selected.uuid)) { /** Tag title could have changed */ @@ -100,11 +92,11 @@ export class NotesViewState extends AbstractState { }, ApplicationEvent.PreferencesChanged), application.addEventObserver(async () => { this.application.noteControllerGroup.closeAllNoteControllers() - void this.selectFirstNote() + void this.selectFirstItem() this.setCompletedFullSync(false) }, ApplicationEvent.SignedIn), application.addEventObserver(async () => { - void this.reloadNotes().then(() => { + void this.reloadItems().then(() => { if ( this.notes.length === 0 && appState.tags.selected instanceof SmartView && @@ -126,7 +118,7 @@ export class NotesViewState extends AbstractState { ], () => { this.reloadNotesDisplayOptions() - void this.reloadNotes() + void this.reloadItems() }, ), @@ -144,14 +136,15 @@ export class NotesViewState extends AbstractState { makeObservable(this, { completedFullSync: observable, displayOptions: observable.struct, + webDisplayOptions: observable.struct, noteFilterText: observable, notes: observable, notesToDisplay: observable, panelTitle: observable, - renderedNotes: observable, + renderedItems: observable, showDisplayOptionsMenu: observable, - reloadNotes: action, + reloadItems: action, reloadPanelTitle: action, reloadPreferences: action, resetPagination: action, @@ -186,7 +179,7 @@ export class NotesViewState extends AbstractState { } get searchBarElement() { - return document.getElementById(ELEMENT_ID_SEARCH_BAR) + return document.getElementById(ElementIdSearchBar) } get isFiltering(): boolean { @@ -206,17 +199,17 @@ export class NotesViewState extends AbstractState { this.panelTitle = title } - reloadNotes = async (): Promise => { - if (this.reloadNotesPromise) { - await this.reloadNotesPromise + reloadItems = async (): Promise => { + if (this.reloadItemsPromise) { + await this.reloadItemsPromise } - this.reloadNotesPromise = this.performReloadNotes() + this.reloadItemsPromise = this.performReloadItems() - await this.reloadNotesPromise + await this.reloadItemsPromise } - private async performReloadNotes() { + private async performReloadItems() { const tag = this.appState.tags.selected if (!tag) { return @@ -224,32 +217,43 @@ export class NotesViewState extends AbstractState { const notes = this.application.items.getDisplayableNotes() - const renderedNotes = notes.slice(0, this.notesToDisplay) + const items = this.application.items.getDisplayableNotesAndFiles() - this.notes = notes + const renderedItems = items.slice(0, this.notesToDisplay) runInAction(() => { - this.renderedNotes = renderedNotes + this.notes = notes + this.items = items + this.renderedItems = renderedItems }) - await this.recomputeSelectionAfterNotesReload() + await this.recomputeSelectionAfterItemsReload() this.reloadPanelTitle() } - private async recomputeSelectionAfterNotesReload() { + private async recomputeSelectionAfterItemsReload() { const appState = this.appState const activeController = this.getActiveNoteController() const activeNote = activeController?.note const isSearching = this.noteFilterText.length > 0 - const hasMultipleNotesSelected = appState.notes.selectedNotesCount >= 2 + const hasMultipleItemsSelected = appState.selectedItems.selectedItemsCount >= 2 - if (hasMultipleNotesSelected) { + if (hasMultipleItemsSelected) { + return + } + + const selectedItem = Object.values(appState.selectedItems.selectedItems)[0] + + const isSelectedItemFile = + this.items.includes(selectedItem) && selectedItem && selectedItem.content_type === ContentType.File + + if (isSelectedItemFile && !SupportsFileSelectionState) { return } if (!activeNote) { - await this.selectFirstNote() + await this.selectFirstItem() return } @@ -262,7 +266,7 @@ export class NotesViewState extends AbstractState { if (!noteExistsInUpdatedResults && !isSearching) { this.closeNoteController(activeController) - this.selectNextNote() + this.selectNextItem() return } @@ -277,9 +281,9 @@ export class NotesViewState extends AbstractState { this.application.getPreference(PrefKey.NotesShowArchived, false) if ((activeNote.trashed && !showTrashedNotes) || (activeNote.archived && !showArchivedNotes)) { - await this.selectNextOrCreateNew() - } else if (!this.appState.notes.selectedNotes[activeNote.uuid]) { - await this.selectNoteWithScrollHandling(activeNote).catch(console.error) + await this.selectNextItemOrCreateNewNote() + } else if (!this.appState.selectedItems.selectedItems[activeNote.uuid]) { + await this.appState.selectedItems.selectItem(activeNote.uuid).catch(console.error) } } @@ -295,30 +299,32 @@ export class NotesViewState extends AbstractState { includeArchived = this.appState.searchOptions.includeArchived includeTrashed = this.appState.searchOptions.includeTrashed } else { - includeArchived = this.displayOptions.showArchived ?? false - includeTrashed = this.displayOptions.showTrashed ?? false + includeArchived = this.displayOptions.includeArchived ?? false + includeTrashed = this.displayOptions.includeTrashed ?? false } - const criteria = NotesDisplayCriteria.Create({ - sortProperty: this.displayOptions.sortBy, - sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc', + const criteria: DisplayOptions = { + sortBy: this.displayOptions.sortBy, + sortDirection: this.displayOptions.sortDirection, tags: tag instanceof SNTag ? [tag] : [], views: tag instanceof SmartView ? [tag] : [], includeArchived, includeTrashed, - includePinned: !this.displayOptions.hidePinned, - includeProtected: !this.displayOptions.hideProtected, + includePinned: this.displayOptions.includePinned, + includeProtected: this.displayOptions.includeProtected, searchQuery: { query: searchText, includeProtectedNoteText: this.appState.searchOptions.includeProtectedContents, }, - }) + } - this.application.items.setNotesDisplayCriteria(criteria) + this.application.items.setPrimaryItemDisplayOptions(criteria) } reloadPreferences = async () => { - const freshDisplayOptions = {} as DisplayOptions + const newDisplayOptions = {} as DisplayOptions + const newWebDisplayOptions = {} as WebDisplayOptions + const currentSortBy = this.displayOptions.sortBy let sortBy = this.application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt) @@ -326,42 +332,47 @@ export class NotesViewState extends AbstractState { sortBy = CollectionSort.UpdatedAt } - freshDisplayOptions.sortBy = sortBy - freshDisplayOptions.sortReverse = this.application.getPreference(PrefKey.SortNotesReverse, false) - freshDisplayOptions.showArchived = this.application.getPreference(PrefKey.NotesShowArchived, false) - freshDisplayOptions.showTrashed = this.application.getPreference(PrefKey.NotesShowTrashed, false) as boolean - freshDisplayOptions.hidePinned = this.application.getPreference(PrefKey.NotesHidePinned, false) - freshDisplayOptions.hideProtected = this.application.getPreference(PrefKey.NotesHideProtected, false) - freshDisplayOptions.hideNotePreview = this.application.getPreference(PrefKey.NotesHideNotePreview, false) - freshDisplayOptions.hideDate = this.application.getPreference(PrefKey.NotesHideDate, false) - freshDisplayOptions.hideTags = this.application.getPreference(PrefKey.NotesHideTags, true) - freshDisplayOptions.hideEditorIcon = this.application.getPreference(PrefKey.NotesHideEditorIcon, false) + newDisplayOptions.sortBy = sortBy + newDisplayOptions.sortDirection = + this.application.getPreference(PrefKey.SortNotesReverse, false) === false ? 'dsc' : 'asc' + newDisplayOptions.includeArchived = this.application.getPreference(PrefKey.NotesShowArchived, false) + newDisplayOptions.includeTrashed = this.application.getPreference(PrefKey.NotesShowTrashed, false) as boolean + newDisplayOptions.includePinned = !this.application.getPreference(PrefKey.NotesHidePinned, false) + newDisplayOptions.includeProtected = !this.application.getPreference(PrefKey.NotesHideProtected, false) + + newWebDisplayOptions.hideNotePreview = this.application.getPreference(PrefKey.NotesHideNotePreview, false) + newWebDisplayOptions.hideDate = this.application.getPreference(PrefKey.NotesHideDate, false) + newWebDisplayOptions.hideTags = this.application.getPreference(PrefKey.NotesHideTags, true) + newWebDisplayOptions.hideEditorIcon = this.application.getPreference(PrefKey.NotesHideEditorIcon, false) const displayOptionsChanged = - freshDisplayOptions.sortBy !== this.displayOptions.sortBy || - freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse || - freshDisplayOptions.hidePinned !== this.displayOptions.hidePinned || - freshDisplayOptions.showArchived !== this.displayOptions.showArchived || - freshDisplayOptions.showTrashed !== this.displayOptions.showTrashed || - freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected || - freshDisplayOptions.hideEditorIcon !== this.displayOptions.hideEditorIcon || - freshDisplayOptions.hideTags !== this.displayOptions.hideTags + newDisplayOptions.sortBy !== this.displayOptions.sortBy || + newDisplayOptions.sortDirection !== this.displayOptions.sortDirection || + newDisplayOptions.includePinned !== this.displayOptions.includePinned || + newDisplayOptions.includeArchived !== this.displayOptions.includeArchived || + newDisplayOptions.includeTrashed !== this.displayOptions.includeTrashed || + newDisplayOptions.includeProtected !== this.displayOptions.includeProtected || + newWebDisplayOptions.hideNotePreview !== this.webDisplayOptions.hideNotePreview || + newWebDisplayOptions.hideDate !== this.webDisplayOptions.hideDate || + newWebDisplayOptions.hideEditorIcon !== this.webDisplayOptions.hideEditorIcon || + newWebDisplayOptions.hideTags !== this.webDisplayOptions.hideTags - this.displayOptions = freshDisplayOptions + this.displayOptions = newDisplayOptions + this.webDisplayOptions = newWebDisplayOptions if (displayOptionsChanged) { this.reloadNotesDisplayOptions() } - await this.reloadNotes() + await this.reloadItems() const width = this.application.getPreference(PrefKey.NotesPanelWidth) if (width) { this.panelWidth = width } - if (freshDisplayOptions.sortBy !== currentSortBy) { - await this.selectFirstNote() + if (newDisplayOptions.sortBy !== currentSortBy) { + await this.selectFirstItem() } } @@ -389,6 +400,7 @@ export class NotesViewState extends AbstractState { get optionsSubtitle(): string { let base = '' + if (this.displayOptions.sortBy === CollectionSort.CreatedAt) { base += ' Date Added' } else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) { @@ -396,28 +408,34 @@ export class NotesViewState extends AbstractState { } else if (this.displayOptions.sortBy === CollectionSort.Title) { base += ' Title' } - if (this.displayOptions.showArchived) { + + if (this.displayOptions.includeArchived) { base += ' | + Archived' } - if (this.displayOptions.showTrashed) { + + if (this.displayOptions.includeTrashed) { base += ' | + Trashed' } - if (this.displayOptions.hidePinned) { + + if (!this.displayOptions.includePinned) { base += ' | – Pinned' } - if (this.displayOptions.hideProtected) { + + if (!this.displayOptions.includeProtected) { base += ' | – Protected' } - if (this.displayOptions.sortReverse) { + + if (this.displayOptions.sortDirection === 'asc') { base += ' | Reversed' } + return base } paginate = () => { this.notesToDisplay += this.pageSize - void this.reloadNotes() + void this.reloadItems() if (this.searchSubmitted) { this.application.getDesktopService()?.searchText(this.noteFilterText) @@ -426,9 +444,9 @@ export class NotesViewState extends AbstractState { resetPagination = (keepCurrentIfLarger = false) => { const clientHeight = document.documentElement.clientHeight - this.pageSize = Math.ceil(clientHeight / MIN_NOTE_CELL_HEIGHT) + this.pageSize = Math.ceil(clientHeight / MinNoteCellHeight) if (this.pageSize === 0) { - this.pageSize = DEFAULT_LIST_NUM_NOTES + this.pageSize = DefaultListNumNotes } if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) { return @@ -436,60 +454,64 @@ export class NotesViewState extends AbstractState { this.notesToDisplay = this.pageSize } - getFirstNonProtectedNote = () => { - return this.notes.find((note) => !note.protected) + getFirstNonProtectedItem = () => { + return this.items.find((item) => !item.protected) } get notesListScrollContainer() { - return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER) + return document.getElementById(ElementIdScrollContainer) } - selectNoteWithScrollHandling = async ( - note: SNNote, - userTriggered?: boolean, - scrollIntoView = true, + selectItemWithScrollHandling = async ( + item: { + uuid: ListableContentItem['uuid'] + }, + { userTriggered = false, scrollIntoView = true }, ): Promise => { - await this.appState.notes.selectNote(note.uuid, userTriggered) + await this.appState.selectedItems.selectItem(item.uuid, userTriggered) if (scrollIntoView) { - const noteElement = document.getElementById(`note-${note.uuid}`) - noteElement?.scrollIntoView({ + const itemElement = document.getElementById(item.uuid) + itemElement?.scrollIntoView({ behavior: 'smooth', }) } } - selectFirstNote = async () => { - const note = this.getFirstNonProtectedNote() + selectFirstItem = async () => { + const item = this.getFirstNonProtectedItem() - if (note) { - await this.selectNoteWithScrollHandling(note, false, false) + if (item) { + await this.selectItemWithScrollHandling(item, { + userTriggered: false, + scrollIntoView: false, + }) this.resetScrollPosition() } } - selectNextNote = () => { - const displayableNotes = this.notes + selectNextItem = () => { + const displayableItems = this.items - const currentIndex = displayableNotes.findIndex((candidate) => { - return candidate.uuid === this.activeControllerNote?.uuid + const currentIndex = displayableItems.findIndex((candidate) => { + return candidate.uuid === this.appState.selectedItems.lastSelectedItem?.uuid }) let nextIndex = currentIndex + 1 - while (nextIndex < displayableNotes.length) { - const nextNote = displayableNotes[nextIndex] + while (nextIndex < displayableItems.length) { + const nextItem = displayableItems[nextIndex] nextIndex++ - if (nextNote.protected) { + if (nextItem.protected) { continue } - this.selectNoteWithScrollHandling(nextNote).catch(console.error) + this.selectItemWithScrollHandling(nextItem, { userTriggered: true }).catch(console.error) - const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`) + const nextNoteElement = document.getElementById(nextItem.uuid) nextNoteElement?.focus() @@ -497,39 +519,42 @@ export class NotesViewState extends AbstractState { } } - selectNextOrCreateNew = async () => { - const note = this.getFirstNonProtectedNote() + selectNextItemOrCreateNewNote = async () => { + const item = this.getFirstNonProtectedItem() - if (note) { - await this.selectNoteWithScrollHandling(note, false, false).catch(console.error) + if (item) { + await this.selectItemWithScrollHandling(item, { + userTriggered: false, + scrollIntoView: false, + }).catch(console.error) } else { await this.createNewNote() } } - selectPreviousNote = () => { - const displayableNotes = this.notes + selectPreviousItem = () => { + const displayableItems = this.items - if (!this.activeControllerNote) { + if (!this.appState.selectedItems.lastSelectedItem) { return } - const currentIndex = displayableNotes.indexOf(this.activeControllerNote) + const currentIndex = displayableItems.indexOf(this.appState.selectedItems.lastSelectedItem) let previousIndex = currentIndex - 1 while (previousIndex >= 0) { - const previousNote = displayableNotes[previousIndex] + const previousItem = displayableItems[previousIndex] previousIndex-- - if (previousNote.protected) { + if (previousItem.protected) { continue } - this.selectNoteWithScrollHandling(previousNote).catch(console.error) + this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error) - const previousNoteElement = document.getElementById(`note-${previousNote.uuid}`) + const previousNoteElement = document.getElementById(previousItem.uuid) previousNoteElement?.focus() @@ -591,7 +616,7 @@ export class NotesViewState extends AbstractState { this.reloadNotesDisplayOptions() - void this.reloadNotes() + void this.reloadItems() } onFilterEnter = () => { @@ -624,7 +649,7 @@ export class NotesViewState extends AbstractState { this.reloadNotesDisplayOptions() - void this.reloadNotes() + void this.reloadItems() } clearFilterText = () => { diff --git a/app/assets/javascripts/UIModels/AppState/FilesState.ts b/app/assets/javascripts/UIModels/AppState/FilesState.ts index 9c859cd96..7981e444f 100644 --- a/app/assets/javascripts/UIModels/AppState/FilesState.ts +++ b/app/assets/javascripts/UIModels/AppState/FilesState.ts @@ -1,4 +1,10 @@ +import { + PopoverFileItemAction, + PopoverFileItemActionType, +} from '@/Components/AttachedFilesPopover/PopoverFileItemAction' +import { PopoverTabs } from '@/Components/AttachedFilesPopover/PopoverTabs' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants' +import { confirmDialog } from '@/Services/AlertService' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { ClassicFileReader, @@ -7,11 +13,202 @@ import { ClassicFileSaver, parseFileName, } from '@standardnotes/filepicker' -import { ClientDisplayableError, FileItem } from '@standardnotes/snjs' +import { ChallengeReason, ClientDisplayableError, ContentType, FileItem } from '@standardnotes/snjs' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit' +import { action, computed, makeObservable, observable, reaction } from 'mobx' +import { WebApplication } from '../Application' import { AbstractState } from './AbstractState' +import { AppState } from './AppState' + +const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection] +const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile] + +type FileContextMenuLocation = { x: number; y: number } export class FilesState extends AbstractState { + allFiles: FileItem[] = [] + attachedFiles: FileItem[] = [] + showFileContextMenu = false + fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 } + + constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) { + super(application, appState) + + makeObservable(this, { + allFiles: observable, + attachedFiles: observable, + showFileContextMenu: observable, + fileContextMenuLocation: observable, + + selectedFiles: computed, + + reloadAllFiles: action, + reloadAttachedFiles: action, + setShowFileContextMenu: action, + setFileContextMenuLocation: action, + }) + + appObservers.push( + application.streamItems(ContentType.File, () => { + this.reloadAllFiles() + this.reloadAttachedFiles() + }), + reaction( + () => appState.notes.selectedNotes, + () => { + this.reloadAttachedFiles() + }, + ), + ) + } + + get selectedFiles() { + return this.appState.selectedItems.getSelectedItems(ContentType.File) + } + + setShowFileContextMenu = (enabled: boolean) => { + this.showFileContextMenu = enabled + } + + setFileContextMenuLocation = (location: FileContextMenuLocation) => { + this.fileContextMenuLocation = location + } + + reloadAllFiles = () => { + this.allFiles = this.application.items.getDisplayableFiles() + } + + reloadAttachedFiles = () => { + const note = this.appState.notes.firstSelectedNote + if (note) { + this.attachedFiles = this.application.items.getFilesForNote(note) + } + } + + deleteFile = async (file: FileItem) => { + const shouldDelete = await confirmDialog({ + text: `Are you sure you want to permanently delete "${file.name}"?`, + confirmButtonStyle: 'danger', + }) + if (shouldDelete) { + const deletingToastId = addToast({ + type: ToastType.Loading, + message: `Deleting file "${file.name}"...`, + }) + await this.application.files.deleteFile(file) + addToast({ + type: ToastType.Success, + message: `Deleted file "${file.name}"`, + }) + dismissToast(deletingToastId) + } + } + + attachFileToNote = async (file: FileItem) => { + const note = this.appState.notes.firstSelectedNote + if (!note) { + addToast({ + type: ToastType.Error, + message: 'Could not attach file because selected note was deleted', + }) + return + } + + await this.application.items.associateFileWithNote(file, note) + } + + detachFileFromNote = async (file: FileItem) => { + const note = this.appState.notes.firstSelectedNote + if (!note) { + addToast({ + type: ToastType.Error, + message: 'Could not attach file because selected note was deleted', + }) + return + } + await this.application.items.disassociateFileWithNote(file, note) + } + + toggleFileProtection = async (file: FileItem) => { + let result: FileItem | undefined + if (file.protected) { + result = await this.application.mutator.unprotectFile(file) + } else { + result = await this.application.mutator.protectFile(file) + } + const isProtected = result ? result.protected : file.protected + return isProtected + } + + authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { + const authorizedFiles = await this.application.protections.authorizeProtectedActionForItems([file], challengeReason) + const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) + return isAuthorized + } + + renameFile = async (file: FileItem, fileName: string) => { + await this.application.items.renameFile(file, fileName) + } + + handleFileAction = async ( + action: PopoverFileItemAction, + currentTab: PopoverTabs, + ): Promise<{ + didHandleAction: boolean + }> => { + const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file + let isAuthorizedForAction = true + + const requiresAuthorization = file.protected && !UnprotectedFileActions.includes(action.type) + + if (requiresAuthorization) { + isAuthorizedForAction = await this.authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile) + } + + if (!isAuthorizedForAction) { + return { + didHandleAction: false, + } + } + + switch (action.type) { + case PopoverFileItemActionType.AttachFileToNote: + await this.attachFileToNote(file) + break + case PopoverFileItemActionType.DetachFileToNote: + await this.detachFileFromNote(file) + break + case PopoverFileItemActionType.DeleteFile: + await this.deleteFile(file) + break + case PopoverFileItemActionType.DownloadFile: + await this.downloadFile(file) + break + case PopoverFileItemActionType.ToggleFileProtection: { + const isProtected = await this.toggleFileProtection(file) + action.callback(isProtected) + break + } + case PopoverFileItemActionType.RenameFile: + await this.renameFile(file, action.payload.name) + break + case PopoverFileItemActionType.PreviewFile: + this.appState.filePreviewModal.activate( + file, + currentTab === PopoverTabs.AllFiles ? this.allFiles : this.attachedFiles, + ) + break + } + + if (!NonMutatingFileActions.includes(action.type)) { + this.application.sync.sync().catch(console.error) + } + + return { + didHandleAction: true, + } + } + public async downloadFile(file: FileItem): Promise { let downloadingToastId = '' diff --git a/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts b/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts index 671622d8a..6219de9bd 100644 --- a/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts +++ b/app/assets/javascripts/UIModels/AppState/NoteTagsState.ts @@ -147,7 +147,7 @@ export class NoteTagsState extends AbstractState { searchActiveNoteAutocompleteTags(): void { const newResults = this.application.items.searchTags( this.autocompleteSearchQuery, - this.appState.notesView.activeControllerNote, + this.appState.contentListView.activeControllerNote, ) this.setAutocompleteTagResults(newResults) } @@ -157,7 +157,7 @@ export class NoteTagsState extends AbstractState { } reloadTags(): void { - const activeNote = this.appState.notesView.activeControllerNote + const activeNote = this.appState.contentListView.activeControllerNote if (activeNote) { const tags = this.application.items.getSortedTagsForNote(activeNote) @@ -173,7 +173,7 @@ export class NoteTagsState extends AbstractState { } async addTagToActiveNote(tag: SNTag): Promise { - const activeNote = this.appState.notesView.activeControllerNote + const activeNote = this.appState.contentListView.activeControllerNote if (activeNote) { await this.application.items.addTagToNote(activeNote, tag, this.addNoteToParentFolders) @@ -183,7 +183,7 @@ export class NoteTagsState extends AbstractState { } async removeTagFromActiveNote(tag: SNTag): Promise { - const activeNote = this.appState.notesView.activeControllerNote + const activeNote = this.appState.contentListView.activeControllerNote if (activeNote) { await this.application.mutator.changeItem(tag, (mutator) => { diff --git a/app/assets/javascripts/UIModels/AppState/NotesState.ts b/app/assets/javascripts/UIModels/AppState/NotesState.ts index a8c638c19..cff236367 100644 --- a/app/assets/javascripts/UIModels/AppState/NotesState.ts +++ b/app/assets/javascripts/UIModels/AppState/NotesState.ts @@ -1,9 +1,8 @@ import { destroyAllObjectProperties } from '@/Utils' import { confirmDialog } from '@/Services/AlertService' -import { KeyboardModifier } from '@/Services/IOService' import { StringEmptyTrash, Strings, StringUtils } from '@/Strings' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants' -import { UuidString, SNNote, NoteMutator, ContentType, SNTag, ChallengeReason, DeinitSource } from '@standardnotes/snjs' +import { SNNote, NoteMutator, ContentType, SNTag, DeinitSource, TagMutator } from '@standardnotes/snjs' import { makeObservable, observable, action, computed, runInAction } from 'mobx' import { WebApplication } from '../Application' import { AppState } from './AppState' @@ -11,7 +10,6 @@ import { AbstractState } from './AbstractState' export class NotesState extends AbstractState { lastSelectedNote: SNNote | undefined - selectedNotes: Record = {} contextMenuOpen = false contextMenuPosition: { top?: number; left: number; bottom?: number } = { top: 0, @@ -25,7 +23,6 @@ export class NotesState extends AbstractState { override deinit(source: DeinitSource) { super.deinit(source) ;(this.lastSelectedNote as unknown) = undefined - ;(this.selectedNotes as unknown) = undefined ;(this.onActiveEditorChanged as unknown) = undefined destroyAllObjectProperties(this) @@ -40,12 +37,13 @@ export class NotesState extends AbstractState { super(application, appState) makeObservable(this, { - selectedNotes: observable, contextMenuOpen: observable, contextMenuPosition: observable, showProtectedWarning: observable, showRevisionHistoryModal: observable, + selectedNotes: computed, + firstSelectedNote: computed, selectedNotesCount: computed, trashedNotesCount: computed, @@ -89,6 +87,14 @@ export class NotesState extends AbstractState { ) } + get selectedNotes() { + return this.appState.selectedItems.getSelectedItems(ContentType.Note) + } + + get firstSelectedNote(): SNNote | undefined { + return Object.values(this.selectedNotes)[0] + } + get selectedNotesCount(): number { if (this.dealloced) { return 0 @@ -101,79 +107,8 @@ export class NotesState extends AbstractState { return this.application.items.trashedItems.length } - private async selectNotesRange(selectedNote: SNNote): Promise { - const notes = this.application.items.getDisplayableNotes() - - const lastSelectedNoteIndex = notes.findIndex((note) => note.uuid == this.lastSelectedNote?.uuid) - const selectedNoteIndex = notes.findIndex((note) => note.uuid == selectedNote.uuid) - - let notesToSelect = [] - if (selectedNoteIndex > lastSelectedNoteIndex) { - notesToSelect = notes.slice(lastSelectedNoteIndex, selectedNoteIndex + 1) - } else { - notesToSelect = notes.slice(selectedNoteIndex, lastSelectedNoteIndex + 1) - } - - const authorizedNotes = await this.application.authorizeProtectedActionForNotes( - notesToSelect, - ChallengeReason.SelectProtectedNote, - ) - - for (const note of authorizedNotes) { - runInAction(() => { - this.selectedNotes[note.uuid] = note - this.lastSelectedNote = note - }) - } - } - - async selectNote(uuid: UuidString, userTriggered?: boolean): Promise { - const note = this.application.items.findItem(uuid) as SNNote - if (!note) { - return - } - - if (this.selectedNotes[uuid]) { - return - } - - const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta) - const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl) - const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift) - - const isMultipleSelectSingle = userTriggered && (hasMeta || hasCtrl) - const isMultipleSelectRange = userTriggered && hasShift - - if (isMultipleSelectSingle) { - if (this.selectedNotes[uuid]) { - delete this.selectedNotes[uuid] - } else if (await this.application.authorizeNoteAccess(note)) { - runInAction(() => { - this.selectedNotes[uuid] = note - this.lastSelectedNote = note - }) - } - } else if (isMultipleSelectRange) { - await this.selectNotesRange(note) - } else { - const shouldSelectNote = this.selectedNotesCount > 1 || !this.selectedNotes[uuid] - if (shouldSelectNote && (await this.application.authorizeNoteAccess(note))) { - runInAction(() => { - this.selectedNotes = { - [note.uuid]: note, - } - this.lastSelectedNote = note - }) - } - } - - if (this.selectedNotesCount === 1) { - await this.openNote(Object.keys(this.selectedNotes)[0]) - } - } - - private async openNote(noteUuid: string): Promise { - if (this.appState.notesView.activeControllerNote?.uuid === noteUuid) { + async openNote(noteUuid: string): Promise { + if (this.appState.contentListView.activeControllerNote?.uuid === noteUuid) { return } @@ -359,7 +294,7 @@ export class NotesState extends AbstractState { }) runInAction(() => { - this.selectedNotes = {} + this.appState.selectedItems.setSelectedItems({}) this.contextMenuOpen = false }) } @@ -376,7 +311,7 @@ export class NotesState extends AbstractState { } unselectNotes(): void { - this.selectedNotes = {} + this.appState.selectedItems.setSelectedItems({}) } getSpellcheckStateForNote(note: SNNote) { @@ -400,9 +335,9 @@ export class NotesState extends AbstractState { const tagsToAdd = [...parentChainTags, tag] await Promise.all( tagsToAdd.map(async (tag) => { - await this.application.mutator.changeItem(tag, (mutator) => { + await this.application.mutator.changeItem(tag, (mutator) => { for (const note of selectedNotes) { - mutator.addItemAsRelationship(note) + mutator.addNote(note) } }) }), @@ -422,7 +357,7 @@ export class NotesState extends AbstractState { isTagInSelectedNotes(tag: SNTag): boolean { const selectedNotes = this.getSelectedNotesList() - return selectedNotes.every((note) => this.appState.getNoteTags(note).find((noteTag) => noteTag.uuid === tag.uuid)) + return selectedNotes.every((note) => this.appState.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid)) } setShowProtectedWarning(show: boolean): void { @@ -445,10 +380,6 @@ export class NotesState extends AbstractState { return Object.values(this.selectedNotes) } - private get io() { - return this.application.io - } - setShowRevisionHistoryModal(show: boolean): void { this.showRevisionHistoryModal = show } diff --git a/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts b/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts new file mode 100644 index 000000000..f27cd92fc --- /dev/null +++ b/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts @@ -0,0 +1,140 @@ +import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' +import { ChallengeReason, ContentType, KeyboardModifier, FileItem, SNNote, UuidString } from '@standardnotes/snjs' +import { action, computed, makeObservable, observable, runInAction } from 'mobx' +import { WebApplication } from '../Application' +import { AbstractState } from './AbstractState' +import { AppState } from './AppState' + +type SelectedItems = Record + +export class SelectedItemsState extends AbstractState { + lastSelectedItem: ListableContentItem | undefined + selectedItems: SelectedItems = {} + + constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) { + super(application) + + makeObservable(this, { + selectedItems: observable, + + selectedItemsCount: computed, + + selectItem: action, + setSelectedItems: action, + }) + + appObservers.push( + application.streamItems( + [ContentType.Note, ContentType.File], + ({ changed, inserted, removed }) => { + runInAction(() => { + for (const removedNote of removed) { + delete this.selectedItems[removedNote.uuid] + } + + for (const item of [...changed, ...inserted]) { + if (this.selectedItems[item.uuid]) { + this.selectedItems[item.uuid] = item + } + } + }) + }, + ), + ) + } + + private get io() { + return this.application.io + } + + get selectedItemsCount(): number { + return Object.keys(this.selectedItems).length + } + + getSelectedItems = (contentType: ContentType) => { + const filteredEntries = Object.entries(this.appState.selectedItems.selectedItems).filter( + ([_, item]) => item.content_type === contentType, + ) as [UuidString, T][] + return Object.fromEntries(filteredEntries) + } + + setSelectedItems = (selectedItems: SelectedItems) => { + this.selectedItems = selectedItems + } + + private selectItemsRange = async (selectedItem: ListableContentItem): Promise => { + const items = this.appState.contentListView.renderedItems + + const lastSelectedItemIndex = items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid) + const selectedItemIndex = items.findIndex((item) => item.uuid == selectedItem.uuid) + + let itemsToSelect = [] + if (selectedItemIndex > lastSelectedItemIndex) { + itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1) + } else { + itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1) + } + + const authorizedItems = await this.application.protections.authorizeProtectedActionForItems( + itemsToSelect, + ChallengeReason.SelectProtectedNote, + ) + + for (const item of authorizedItems) { + runInAction(() => { + this.selectedItems[item.uuid] = item + this.lastSelectedItem = item + }) + } + } + + selectItem = async ( + uuid: UuidString, + userTriggered?: boolean, + ): Promise<{ + didSelect: boolean + }> => { + const item = this.application.items.findItem(uuid) + if (!item) { + return { + didSelect: false, + } + } + + const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta) + const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl) + const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift) + const hasMoreThanOneSelected = this.selectedItemsCount > 1 + const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item) + + if (userTriggered && (hasMeta || hasCtrl)) { + if (this.selectedItems[uuid] && hasMoreThanOneSelected) { + delete this.selectedItems[uuid] + } else if (isAuthorizedForAccess) { + this.selectedItems[uuid] = item + this.lastSelectedItem = item + } + } else if (userTriggered && hasShift) { + await this.selectItemsRange(item) + } else { + const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid] + if (shouldSelectNote && isAuthorizedForAccess) { + this.setSelectedItems({ + [item.uuid]: item, + }) + this.lastSelectedItem = item + } + } + + if (this.selectedItemsCount === 1) { + const item = Object.values(this.selectedItems)[0] + if (item.content_type === ContentType.Note) { + await this.appState.notes.openNote(item.uuid) + } + } + + return { + didSelect: this.selectedItems[uuid] != undefined, + } + } +} diff --git a/app/assets/javascripts/UIModels/AppState/TagsState.ts b/app/assets/javascripts/UIModels/AppState/TagsState.ts index 36c87bb0b..32963d9ea 100644 --- a/app/assets/javascripts/UIModels/AppState/TagsState.ts +++ b/app/assets/javascripts/UIModels/AppState/TagsState.ts @@ -25,7 +25,7 @@ type AnyTag = SNTag | SmartView const rootTags = (application: SNApplication): SNTag[] => { const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag) - const allTags = application.items.getDisplayableItems(ContentType.Tag) + const allTags = application.items.getDisplayableTags() const rootTags = allTags.filter(hasNoParent) return rootTags @@ -132,7 +132,7 @@ export class TagsState extends AbstractState { appEventListeners.push( this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => { runInAction(() => { - this.tags = this.application.items.getDisplayableItems(ContentType.Tag) + this.tags = this.application.items.getDisplayableTags() this.smartViews = this.application.items.getSmartViews() diff --git a/app/assets/javascripts/UIModels/AppState/WebDisplayOptions.ts b/app/assets/javascripts/UIModels/AppState/WebDisplayOptions.ts new file mode 100644 index 000000000..1b8eb0104 --- /dev/null +++ b/app/assets/javascripts/UIModels/AppState/WebDisplayOptions.ts @@ -0,0 +1,6 @@ +export type WebDisplayOptions = { + hideTags: boolean + hideDate: boolean + hideNotePreview: boolean + hideEditorIcon: boolean +} diff --git a/app/assets/javascripts/UIModels/Application.ts b/app/assets/javascripts/UIModels/Application.ts index defd3d991..c3308e24e 100644 --- a/app/assets/javascripts/UIModels/Application.ts +++ b/app/assets/javascripts/UIModels/Application.ts @@ -14,7 +14,6 @@ import { NoteGroupController, removeFromArray, IconsController, - Runtime, DesktopDeviceInterface, isDesktopDevice, DeinitMode, @@ -49,7 +48,6 @@ export class WebApplication extends SNApplication { identifier: string, defaultSyncServerHost: string, webSocketUrl: string, - runtime: Runtime, ) { super({ environment: deviceInterface.environment, @@ -61,7 +59,7 @@ export class WebApplication extends SNApplication { defaultHost: defaultSyncServerHost, appVersion: deviceInterface.appVersion, webSocketUrl: webSocketUrl, - runtime, + supportsFileNavigation: window.enabledUnfinishedFeatures, }) deviceInterface.setApplication(this) diff --git a/app/assets/javascripts/UIModels/ApplicationGroup.ts b/app/assets/javascripts/UIModels/ApplicationGroup.ts index 88f7a6737..ba77e21b6 100644 --- a/app/assets/javascripts/UIModels/ApplicationGroup.ts +++ b/app/assets/javascripts/UIModels/ApplicationGroup.ts @@ -3,7 +3,6 @@ import { ApplicationDescriptor, SNApplicationGroup, Platform, - Runtime, InternalEventBus, isDesktopDevice, } from '@standardnotes/snjs' @@ -21,7 +20,6 @@ const createApplication = ( deviceInterface: WebOrDesktopDevice, defaultSyncServerHost: string, device: WebOrDesktopDevice, - runtime: Runtime, webSocketUrl: string, ) => { const platform = getPlatform() @@ -32,7 +30,6 @@ const createApplication = ( descriptor.identifier, defaultSyncServerHost, webSocketUrl, - runtime, ) const appState = new AppState(application, device) @@ -54,23 +51,17 @@ const createApplication = ( } export class ApplicationGroup extends SNApplicationGroup { - constructor( - private defaultSyncServerHost: string, - device: WebOrDesktopDevice, - private runtime: Runtime, - private webSocketUrl: string, - ) { + constructor(private defaultSyncServerHost: string, device: WebOrDesktopDevice, private webSocketUrl: string) { super(device) } override async initialize(): Promise { const defaultSyncServerHost = this.defaultSyncServerHost - const runtime = this.runtime const webSocketUrl = this.webSocketUrl await super.initialize({ applicationCreator: async (descriptor, device) => { - return createApplication(descriptor, device, defaultSyncServerHost, device, runtime, webSocketUrl) + return createApplication(descriptor, device, defaultSyncServerHost, device, webSocketUrl) }, }) diff --git a/app/assets/stylesheets/_editor.scss b/app/assets/stylesheets/_editor.scss index 5548a570e..3933e7223 100644 --- a/app/assets/stylesheets/_editor.scss +++ b/app/assets/stylesheets/_editor.scss @@ -10,8 +10,8 @@ $heading-height: 75px; display: flex; flex-direction: column; overflow-y: hidden; - background-color: var(--sn-stylekit-editor-background-color); - color: var(--sn-stylekit-editor-foreground-color); + background-color: var(--editor-background-color); + color: var(--editor-foreground-color); } #error-decrypting-container { @@ -34,7 +34,7 @@ $heading-height: 75px; padding-bottom: 10px; padding-right: 14px; - border-bottom: 1px solid var(--sn-stylekit-border-color); + border-bottom: 1px solid var(--editor-title-bar-border-bottom-color); z-index: $z-index-editor-title-bar; height: auto; @@ -53,10 +53,10 @@ $heading-height: 75px; border: none; outline: none; background-color: transparent; - color: var(--sn-stylekit-editor-foreground-color); + color: var(--editor-title-input-color); &:disabled { - color: var(--sn-stylekit-editor-foreground-color); + color: var(--editor-title-input-color); } &:focus { box-shadow: none; @@ -100,7 +100,7 @@ $heading-height: 75px; height: 100%; display: flex; tab-size: 2; - background-color: var(--sn-stylekit-background-color); + background-color: var(--editor-pane-background-color); position: relative; @@ -112,8 +112,8 @@ $heading-height: 75px; .editable { overflow-y: scroll; width: 100%; - background-color: var(--sn-stylekit-editor-background-color); - color: var(--sn-stylekit-editor-foreground-color); + background-color: var(--editor-pane-editor-background-color); + color: var(--editor-pane-editor-foreground-color); border: none; outline: none; @@ -143,7 +143,7 @@ $heading-height: 75px; iframe { width: 100%; - background-color: var(--sn-stylekit-background-color); + background-color: var(--editor-pane-component-stack-item-background-color); // we moved the border top from the .component-stack-item to the .iframe, as on parent, // it increases its height and caused unneccessary scrollbars on windows. border-top: 1px solid var(--sn-stylekit-border-color); diff --git a/app/assets/stylesheets/_focused.scss b/app/assets/stylesheets/_focused.scss index 2f85dc8c3..a426fb99d 100644 --- a/app/assets/stylesheets/_focused.scss +++ b/app/assets/stylesheets/_focused.scss @@ -36,7 +36,7 @@ } #navigation, - #notes-column { + #items-column { will-change: opacity; animation: fade-out 1.25s forwards; transition: width 1.25s; @@ -50,7 +50,7 @@ width: 0px !important; } - #notes-column:hover { + #items-column:hover { flex: initial; width: 0px !important; } @@ -58,7 +58,7 @@ .disable-focus-mode { #navigation, - #notes-column { + #items-column { transition: width 1.25s; will-change: opacity; animation: fade-in 1.25s forwards; diff --git a/app/assets/stylesheets/_footer.scss b/app/assets/stylesheets/_footer.scss index 49676775f..9c734f16c 100644 --- a/app/assets/stylesheets/_footer.scss +++ b/app/assets/stylesheets/_footer.scss @@ -26,22 +26,6 @@ left: inherit; } } - - &.dock-shortcut { - width: 12px; - - &:hover .sk-app-bar-item-column { - border-bottom: 2px solid var(--sn-stylekit-info-color); - } - - .sk-app-bar-item-column { - width: 100%; - - * { - width: 100%; - } - } - } } #account-panel, diff --git a/app/assets/stylesheets/_items-column.scss b/app/assets/stylesheets/_items-column.scss new file mode 100644 index 000000000..d225dcd68 --- /dev/null +++ b/app/assets/stylesheets/_items-column.scss @@ -0,0 +1,167 @@ +@import './scrollbar'; + +#items-column { + background-color: var(--items-column-background-color); + border-left: 1px solid var(--items-column-border-left-color); + border-right: 1px solid var(--items-column-border-right-color); + font-size: var(--sn-stylekit-font-size-h2); + user-select: none; + + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + + .empty-items-list { + flex-grow: 1; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + font-size: var(--sn-stylekit-font-size-h3); + } + + .content { + display: flex; + flex-direction: column; + } + + #items-title-bar-container { + padding: 0.8125rem; + } + + #items-title-bar { + font-weight: normal; + overflow: visible; + + .section-title-bar-header .title { + width: calc(90% - 45px); + } + + p { + font-size: var(--sn-stylekit-font-size-p); + } + } + + #items-menu-bar { + position: relative; + } + + #notes-options-menu { + margin-left: 10px; + } + + .filter-section { + clear: left; + max-height: 80px; + margin-top: 10px; + position: relative; + display: flex; + flex-direction: column; + + .filter-bar { + background-color: var(--items-column-search-background-color); + border-radius: var(--sn-stylekit-general-border-radius); + height: 100%; + color: #909090; + text-align: center; + font-weight: normal; + font-size: var(--sn-stylekit-font-size-h3); + + border-style: solid; + border-color: transparent; + width: 100%; + position: relative; + height: 28px; + } + + .search-options { + margin-top: 10px; + + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + + font-size: var(--sn-stylekit-font-size-p); + white-space: nowrap; + + overflow-x: auto; + } + + #search-clear-button { + padding: 0; + border: none; + border-radius: 50%; + width: 17px; + height: 17px; + cursor: default; + background-color: var(--sn-stylekit-neutral-color); + color: var(--sn-stylekit-neutral-contrast-color); + font-size: 10px; + line-height: 17px; + text-align: center; + position: absolute; + top: 20%; + transform: translateY(-50%); + right: 10px; + cursor: pointer; + + transition: background-color 0.15s linear; + + &:hover { + background-color: var(--sn-stylekit-info-color); + } + } + } + + .infinite-scroll { + @include minimal_scrollbar(); + height: inherit; + background-color: var(--items-column-items-background-color); + } + + .content-list-item { + &.selected, &:hover { + background-color: var(--item-cell-selected-background-color); + } + + progress { + background-color: var(--note-preview-progress-background-color); + color: var(--note-preview-progress-color); + border: none; + + &::-webkit-progress-bar { + background-color: var(--note-preview-progress-background-color); + } + + &::-webkit-progress-value { + background-color: var(--note-preview-progress-color); + } + + &::-moz-progress-bar { + background-color: var(--note-preview-progress-color); + } + } + + &.selected { + background-color: var(--item-cell-selected-background-color); + border-left: 2px solid var(--item-cell-selected-border-left-color); + + progress { + background-color: var(--note-preview-selected-progress-background-color); + color: var(--note-preview-selected-progress-color); + + &::-webkit-progress-bar { + background-color: var(--note-preview-selected-progress-background-color); + } + + &::-webkit-progress-value { + background-color: var(--note-preview-progress-color); + } + + &::-moz-progress-bar { + background-color: var(--note-preview-progress-color); + } + } + } + } +} diff --git a/app/assets/stylesheets/_lock-screen.scss b/app/assets/stylesheets/_lock-screen.scss deleted file mode 100644 index 89ce4dfb4..000000000 --- a/app/assets/stylesheets/_lock-screen.scss +++ /dev/null @@ -1,35 +0,0 @@ -#lock-screen { - position: relative; - height: 100%; - width: 100vw; - - margin-left: auto; - margin-right: auto; - - left: 0; - right: 0; - top: 0; - bottom: 0; - - z-index: $z-index-lock-screen; - background-color: var(--sn-stylekit-background-color); - color: var(--sn-stylekit-foreground-color); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .sk-panel { - width: 315px; - flex-grow: 0; - // border-radius: 0; - - .sk-panel-header { - justify-content: center; - } - } - - #passcode-reset { - text-align: center; - } -} diff --git a/app/assets/stylesheets/_main.scss b/app/assets/stylesheets/_main.scss index df5dcccde..760a713bf 100644 --- a/app/assets/stylesheets/_main.scss +++ b/app/assets/stylesheets/_main.scss @@ -38,8 +38,6 @@ body { height: 100%; line-height: normal; margin: 0; - color: var(--sn-stylekit-foreground-color); - background-color: var(--sn-stylekit-background-color); } * { @@ -50,14 +48,6 @@ body { text-transform: uppercase; } -.tinted { - color: var(--sn-stylekit-info-color); -} - -.tinted-selected { - color: var(--sn-stylekit-info-contrast-color); -} - h1 { font-size: var(--sn-stylekit-font-size-h1); } @@ -96,13 +86,13 @@ a { } ::selection { - background: var(--sn-stylekit-info-color) !important; /* WebKit/Blink Browsers */ - color: var(--sn-stylekit-info-contrast-color); + background: var(--text-selection-background-color) !important; + color: var(--text-selection-color); } ::-moz-selection { - background: var(--sn-stylekit-info-color) !important; - color: var(--sn-stylekit-info-contrast-color); + background: var(--text-selection-background-color) !important; + color: var(--text-selection-color); } p { @@ -115,7 +105,7 @@ p { height: 100vh; position: relative; overflow: auto; - background-color: var(--sn-stylekit-background-color); + background-color: var(--editor-header-bar-background-color); } $footer-height: 2rem; @@ -145,8 +135,7 @@ $footer-height: 2rem; height: 100%; position: absolute; cursor: col-resize; - // needs to be a color that works on main bg and contrast bg - background-color: var(--sn-stylekit-secondary-contrast-background-color); + background-color: var(--panel-resizer-background-color); opacity: 0; border-top: none; border-bottom: none; @@ -176,8 +165,6 @@ $footer-height: 2rem; &.collapsed { opacity: 1; - // so it blends in with editor a bit more - // background-color: var(--sn-stylekit-editor-background-color); } &.dragging { @@ -207,7 +194,6 @@ $footer-height: 2rem; > .content { height: 100%; max-height: 100%; - background-color: var(--sn-stylekit-background-color); position: relative; } @@ -245,21 +231,3 @@ $footer-height: 2rem; .z-index-purchase-flow { z-index: $z-index-purchase-flow; } - -textarea { - &.non-interactive { - user-select: text !important; - resize: none; - background-color: transparent; - border-color: var(--sn-stylekit-border-color); - font-family: monospace; - outline: 0; - - -webkit-user-select: none; - -webkit-touch-callout: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - } -} diff --git a/app/assets/stylesheets/_menus.scss b/app/assets/stylesheets/_menus.scss index 7b160d90a..5934765e1 100644 --- a/app/assets/stylesheets/_menus.scss +++ b/app/assets/stylesheets/_menus.scss @@ -25,10 +25,6 @@ @extend .cursor-auto; } -.sn-menu-border { - border: var(--sn-stylekit-menu-border); -} - .sn-account-menu-headline { @extend .sk-h2; @extend .sk-bold; diff --git a/app/assets/stylesheets/_modals.scss b/app/assets/stylesheets/_modals.scss index 35e25ff9f..d67ce5d37 100644 --- a/app/assets/stylesheets/_modals.scss +++ b/app/assets/stylesheets/_modals.scss @@ -64,7 +64,6 @@ height: 100%; background-color: transparent; - color: var(--sn-stylekit-contrast-foreground-color); display: flex; align-items: center; justify-content: center; diff --git a/app/assets/stylesheets/_navigation.scss b/app/assets/stylesheets/_navigation.scss index 0b8628527..88ab24fa0 100644 --- a/app/assets/stylesheets/_navigation.scss +++ b/app/assets/stylesheets/_navigation.scss @@ -17,11 +17,11 @@ $content-horizontal-padding: 16px; #navigation-content { display: flex; flex-direction: column; - background-color: var(--sn-stylekit-secondary-background-color); + background-color: var(--navigation-column-background-color); } .section-title-bar { - color: var(--sn-stylekit-secondary-foreground-color); + color: var(--navigation-section-title-color); padding-top: 0.8125rem; padding-bottom: 8px; padding-left: $content-horizontal-padding; @@ -64,10 +64,6 @@ $content-horizontal-padding: 16px; .tag { border: 0; background-color: transparent; - - &:focus { - background-color: var(--sn-stylekit-secondary-contrast-background-color); - } } .tag, @@ -123,8 +119,8 @@ $content-horizontal-padding: 16px; width: 80%; background-color: transparent; font-weight: 600; - color: var(--sn-stylekit-secondary-foreground-color); - -webkit-text-fill-color: var(--sn-stylekit-secondary-foreground-color); + color: var(--navigation-item-text-color); + -webkit-text-fill-color: var(--navigation-item-text-color); border: none; cursor: pointer; text-overflow: ellipsis; @@ -158,7 +154,7 @@ $content-horizontal-padding: 16px; padding-right: 4px; padding-top: 1px; font-weight: bold; - color: var(--sn-stylekit-neutral-color); + color: var(--navigation-item-count-color); min-width: 15px; text-align: right; } @@ -201,12 +197,7 @@ $content-horizontal-padding: 16px; &:hover:not(.selected), &.selected, &.is-drag-over { - background-color: var(--sn-stylekit-secondary-contrast-background-color); - color: var(--sn-stylekit-secondary-contrast-foreground-color); - - > .title { - color: var(--sn-stylekit-secondary-contrast-foreground-color); - } + background-color: var(--navigation-item-selected-background-color); } } } diff --git a/app/assets/stylesheets/_notes.scss b/app/assets/stylesheets/_notes.scss deleted file mode 100644 index bc6b28b93..000000000 --- a/app/assets/stylesheets/_notes.scss +++ /dev/null @@ -1,324 +0,0 @@ -@import './scrollbar'; - -#notes-column, -.notes { - border-left: 1px solid var(--sn-stylekit-border-color); - border-right: 1px solid var(--sn-stylekit-border-color); - font-size: var(--sn-stylekit-font-size-h2); - user-select: none; - - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - - .empty-notes-list { - flex-grow: 1; - margin: 0; - display: flex; - justify-content: center; - align-items: center; - font-size: var(--sn-stylekit-font-size-h3); - } - - .content { - display: flex; - flex-direction: column; - } - - #notes-title-bar-container { - padding: 0.8125rem; - } - - #notes-title-bar { - font-weight: normal; - overflow: visible; - - .section-title-bar-header .title { - width: calc(90% - 45px); - } - - p { - font-size: var(--sn-stylekit-font-size-p); - } - } - - #notes-menu-bar { - position: relative; - } - - #notes-options-menu { - margin-left: 10px; - } - - .filter-section { - clear: left; - max-height: 80px; - margin-top: 10px; - position: relative; - display: flex; - flex-direction: column; - - .filter-bar { - background-color: var(--sn-stylekit-contrast-background-color); - border-radius: var(--sn-stylekit-general-border-radius); - height: 100%; - color: #909090; - text-align: center; - font-weight: normal; - font-size: var(--sn-stylekit-font-size-h3); - - border-style: solid; - border-color: transparent; - width: 100%; - position: relative; - height: 28px; - } - - .search-options { - margin-top: 10px; - - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.5rem; - - font-size: var(--sn-stylekit-font-size-p); - white-space: nowrap; - - overflow-x: auto; - } - - #search-clear-button { - padding: 0; - border: none; - border-radius: 50%; - width: 17px; - height: 17px; - cursor: default; - background-color: var(--sn-stylekit-neutral-color); - color: var(--sn-stylekit-neutral-contrast-color); - font-size: 10px; - line-height: 17px; - text-align: center; - position: absolute; - top: 20%; - transform: translateY(-50%); - right: 10px; - cursor: pointer; - - transition: background-color 0.15s linear; - - &:hover { - background-color: var(--sn-stylekit-info-color); - } - } - } - - .infinite-scroll { - @include minimal_scrollbar(); - height: inherit; - background-color: var(--sn-stylekit-background-color); - } - - .note { - display: flex; - align-items: stretch; - - width: 100%; - cursor: pointer; - - &:hover { - background-color: var(--sn-stylekit-contrast-background-color); - } - - .icon { - display: flex; - flex-flow: column; - align-items: center; - justify-content: space-between; - padding: 1rem; - padding-right: 0.75rem; - margin-right: 0; - } - - .meta { - flex-grow: 1; - min-width: 0; - padding: 1rem 0; - border-bottom: 1px solid var(--sn-stylekit-border-color); - - &.icon-hidden { - padding-left: 1rem; - } - - .name-container { - display: flex; - align-items: flex-start; - justify-content: space-between; - font-weight: 600; - font-size: 1rem; - line-height: 1.3; - overflow: hidden; - } - - .name { - word-break: break-word; - margin-right: 0.5rem; - } - - .bottom-info { - font-size: 12px; - line-height: 1.4; - margin-top: 0.25rem; - } - } - - .flag-icons { - display: flex; - align-items: flex-start; - padding: 1rem; - padding-left: 0; - border-bottom: 1px solid var(--sn-stylekit-border-color); - - & > * { - display: flex; - align-items: center; - } - - & > * + * { - margin-left: 0.375rem; - } - } - - .tags-string { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-top: 0.345rem; - font-size: 0.725rem; - - .tag { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.375rem 0.25rem 0.325rem; - background-color: var(--sn-stylekit-grey-4-opacity-variant); - border-radius: 0.125rem; - } - } - - .note-preview { - font-size: var(--sn-stylekit-font-size-h3); - overflow: hidden; - text-overflow: ellipsis; - - & > * { - margin-top: 0.15rem; - } - - .default-preview, - .plain-preview { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; /* number of lines to show */ - line-height: 1.3; - overflow: hidden; - } - - .html-preview { - margin-top: 4px; - margin-bottom: 4px; - } - } - - .note-flag { - color: var(--sn-stylekit-info-color); - } - - .note-flags { - display: flex; - flex-direction: row; - align-items: center; - margin-top: 0.125rem; - - .flag { - padding: 4px; - padding-left: 6px; - padding-right: 6px; - border-radius: var(--sn-stylekit-general-border-radius); - margin-right: 4px; - margin-top: 4px; - - &.info { - background-color: var(--sn-stylekit-info-color); - color: var(--sn-stylekit-info-contrast-color); - } - - &.success { - background-color: var(--sn-stylekit-success-color); - color: var(--sn-stylekit-success-contrast-color); - } - - &.warning { - background-color: var(--sn-stylekit-warning-color); - color: var(--sn-stylekit-warning-contrast-color); - } - - &.neutral { - background-color: var(--sn-stylekit-neutral-color); - color: var(--sn-stylekit-neutral-contrast-color); - } - - &.danger { - background-color: var(--sn-stylekit-danger-color); - color: var(--sn-stylekit-danger-contrast-color); - } - - .label { - font-size: 10px; - font-weight: bold; - text-align: center; - } - } - } - - progress { - background-color: var(--sn-stylekit-contrast-background-color); - color: var(--sn-stylekit-info-color); - border: none; - - &::-webkit-progress-bar { - background-color: var(--sn-stylekit-contrast-background-color); - } - - &::-webkit-progress-value { - background-color: var(--sn-stylekit-info-color); - } - - &::-moz-progress-bar { - background-color: var(--sn-stylekit-info-color); - } - } - - &.selected { - background-color: var(--sn-stylekit-contrast-background-color); - border-left: 2px solid var(--sn-stylekit-info-color); - - progress { - background-color: var(--sn-stylekit-secondary-foreground-color); - color: var(--sn-stylekit-secondary-background-color); - - &::-webkit-progress-bar { - background-color: var(--sn-stylekit-secondary-foreground-color); - } - - &::-webkit-progress-value { - background-color: var(--sn-stylekit-info-color); - } - - &::-moz-progress-bar { - background-color: var(--sn-stylekit-info-color); - } - } - } - } -} - diff --git a/app/assets/stylesheets/_preferences.scss b/app/assets/stylesheets/_preferences.scss index 5337a5d19..b4d9b9cfb 100644 --- a/app/assets/stylesheets/_preferences.scss +++ b/app/assets/stylesheets/_preferences.scss @@ -21,7 +21,7 @@ @extend .border-1; .icon { - color: var(--sn-stylekit-neutral-color); + color: var(--preferences-navigation-icon-color); @extend .text-base; } @@ -38,7 +38,7 @@ @extend .color-info; @extend .font-bold; - background-color: var(--sn-stylekit-info-backdrop-color); + background-color: var(--preferences-navigation-selected-background-color); .icon { @extend .color-info; diff --git a/app/assets/stylesheets/_reach-sub.scss b/app/assets/stylesheets/_reach-sub.scss index ec50ce6f1..511443cba 100644 --- a/app/assets/stylesheets/_reach-sub.scss +++ b/app/assets/stylesheets/_reach-sub.scss @@ -7,6 +7,7 @@ align-items: center; overflow: unset; } + [data-reach-dialog-overlay]::before { background-color: var(--sn-stylekit-contrast-background-color); content: ''; @@ -17,6 +18,7 @@ left: 0px; opacity: 0.75; } + .challenge-modal-overlay::before { background-color: var(--sn-stylekit-grey-5); } diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 286a75b4d..6884a76e7 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -242,6 +242,10 @@ margin-left: 0.125rem; } +.sn-component .ml-1\.5 { + margin-left: 0.375rem; +} + .ml-3 { margin-left: 0.75rem; } @@ -299,6 +303,10 @@ margin-top: 0.125rem; } +.mt-1\.5 { + margin-top: 0.375rem; +} + .mt-2\.5 { margin-top: 0.625rem; } @@ -538,6 +546,10 @@ padding: 0.375rem; } +.p-4 { + padding: 1rem; +} + .p-6 { padding: 1.5rem; } @@ -590,6 +602,14 @@ padding-bottom: 0.625rem; } +.p-4 { + padding: 1rem; +} + +.sn-component .pr-4 { + padding-left: 1rem; +} + .sn-component .px-0 { padding-left: 0; padding-right: 0; @@ -656,6 +676,10 @@ padding-bottom: 3rem; } +.pl-0 { + padding-left: 0; +} + .pl-2 { padding-left: 0.5rem; } @@ -858,7 +882,7 @@ @extend .border-solid; @extend .rounded-full; @extend .relative; - border-color: var(--sn-stylekit-grey-1); + border-color: var(--dropdown-menu-radio-button-inactive-color); &--checked { @extend .border-info; @@ -939,10 +963,6 @@ } } -.border-info-contrast { - border-color: var(--sn-stylekit-info-contrast-color); -} - .sn-icon-button { &:focus { border-color: transparent; @@ -995,6 +1015,14 @@ line-height: 140%; } +.leading-1\.3 { + line-height: 1.3; +} + +.leading-1\.4 { + line-height: 1.4; +} + .dimmed { opacity: 0.5; cursor: default; @@ -1034,6 +1062,10 @@ border-top-width: 0; } +.sn-component .border-l-2px { + border-left-width: 2px; +} + .text-editor { font-size: var(--sn-stylekit-font-size-editor); } @@ -1135,6 +1167,10 @@ background-color: var(--sn-stylekit-background-color); } +.sn-component .hover\:bg-grey-5:hover { + background-color: var(--sn-stylekit-grey-5); +} + .sn-component .progress-bar { border-radius: 0.5rem; background-color: var(--sn-stylekit-contrast-background-color); @@ -1156,6 +1192,12 @@ } } +.line-clamp-1 { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + .sn-component .flex-row-reverse { flex-flow: row-reverse; } diff --git a/app/assets/stylesheets/_stylekit-sub.scss b/app/assets/stylesheets/_stylekit-sub.scss index 64569719e..69e122a4e 100644 --- a/app/assets/stylesheets/_stylekit-sub.scss +++ b/app/assets/stylesheets/_stylekit-sub.scss @@ -3,23 +3,6 @@ } .sn-component { - .sk-notification { - &.unpadded { - padding: 0; - padding-bottom: 0 !important; - padding-top: 0; - } - - .padded-row { - padding: 10px 12px; - } - - .bordered-row { - border-bottom: 1px solid var(--sn-stylekit-border-color); - border-top: 1px solid var(--sn-stylekit-border-color); - } - } - .sk-app-bar { &.dynamic-height { min-height: 1.625rem !important; @@ -86,7 +69,7 @@ a, .sk-a { background: none; border: none; - color: var(--sn-stylekit-info-color); + color: var(--link-element-color); } button.sk-a { diff --git a/app/assets/stylesheets/_theme.scss b/app/assets/stylesheets/_theme.scss new file mode 100644 index 000000000..24ad1999f --- /dev/null +++ b/app/assets/stylesheets/_theme.scss @@ -0,0 +1,44 @@ +:root { + --modal-background-color: var(--sn-stylekit-background-color); + + --editor-header-bar-background-color: var(--sn-stylekit-background-color); + --editor-background-color: var(--sn-stylekit-editor-background-color); + --editor-foreground-color: var(--sn-stylekit-editor-foreground-color); + --editor-title-bar-border-bottom-color: var(--sn-stylekit-border-color); + --editor-title-input-color: var(--sn-stylekit-editor-foreground-color); + --editor-pane-background-color: var(--sn-stylekit-background-color); + --editor-pane-editor-background-color: var(--sn-stylekit-editor-background-color); + --editor-pane-editor-foreground-color: var(--sn-stylekit-editor-foreground-color); + --editor-pane-component-stack-item-background-color: var(--sn-stylekit-background-color); + + --text-selection-color: var(--sn-stylekit-info-contrast-color); + --text-selection-background-color: var(--sn-stylekit-info-color); + + --note-preview-progress-color: var(--sn-stylekit-info-color); + --note-preview-progress-background-color: var(--sn-stylekit-contrast-background-color); + + --note-preview-selected-progress-color: var(--sn-stylekit-secondary-background-color); + --note-preview-selected-progress-background-color: var(--sn-stylekit-secondary-foreground-color); + + --items-column-background-color: var(--sn-stylekit-background-color); + --items-column-items-background-color: var(--sn-stylekit-background-color); + --items-column-border-left-color: var(--sn-stylekit-border-color); + --items-column-border-right-color: var(--sn-stylekit-border-color); + --items-column-search-background-color: var(--sn-stylekit-contrast-background-color); + --item-cell-selected-background-color: var(--sn-stylekit-grey-5); + --item-cell-selected-border-left-color: var(--sn-stylekit-info-color); + + --navigation-column-background-color: var(--sn-stylekit-secondary-background-color); + --navigation-section-title-color: var(--sn-stylekit-secondary-foreground-color); + --navigation-item-text-color: var(--sn-stylekit-secondary-foreground-color); + --navigation-item-count-color: var(--sn-stylekit-neutral-color); + --navigation-item-selected-background-color: var(--sn-stylekit-secondary-contrast-background-color); + + --preferences-navigation-icon-color: var(--sn-stylekit-neutral-color); + --preferences-navigation-selected-background-color: var(--sn-stylekit-info-backdrop-color); + + --dropdown-menu-radio-button-inactive-color: var(--sn-stylekit-grey-1); + + --panel-resizer-background-color: var(--sn-stylekit-secondary-contrast-background-color); + --link-element-color: var(--sn-stylekit-info-color); +} \ No newline at end of file diff --git a/app/assets/stylesheets/_ui.scss b/app/assets/stylesheets/_ui.scss index d16d323b2..e323e7916 100644 --- a/app/assets/stylesheets/_ui.scss +++ b/app/assets/stylesheets/_ui.scss @@ -164,9 +164,6 @@ $screen-md-max: ($screen-lg-min - 1) !default; border-radius: 0.3046875rem; } -.bg-main { - background-color: var(--sn-stylekit-info-color); -} .bg-transparent { background-color: transparent; } @@ -243,9 +240,6 @@ $screen-md-max: ($screen-lg-min - 1) !default; .text-sm { font-size: var(--sn-stylekit-font-size-h5); } -.text-info-contrast { - color: var(--sn-stylekit-info-contrast-color); -} .wrap { word-wrap: break-word; diff --git a/app/assets/stylesheets/index.css.scss b/app/assets/stylesheets/index.css.scss index ae927f70a..b91b1a2ee 100644 --- a/app/assets/stylesheets/index.css.scss +++ b/app/assets/stylesheets/index.css.scss @@ -1,13 +1,13 @@ @import '@standardnotes/stylekit/dist/stylekit'; +@import 'theme'; @import 'main'; @import 'ui'; @import 'footer'; @import 'navigation'; -@import 'notes'; +@import 'items-column'; @import 'editor'; @import 'menus'; @import 'modals'; -@import 'lock-screen'; @import 'stylekit-sub'; @import 'ionicons'; @import 'reach-sub'; diff --git a/package.json b/package.json index e424295c9..c74dd2e2f 100644 --- a/package.json +++ b/package.json @@ -70,11 +70,11 @@ "@reach/tooltip": "^0.16.2", "@reach/visually-hidden": "^0.16.0", "@standardnotes/components": "1.8.1", - "@standardnotes/filepicker": "1.14.12", + "@standardnotes/filepicker": "1.15.0", "@standardnotes/icons": "^1.1.7", - "@standardnotes/services": "^1.12.2", + "@standardnotes/services": "^1.13.1", "@standardnotes/sncrypto-web": "1.10.1", - "@standardnotes/snjs": "2.110.3", + "@standardnotes/snjs": "2.113.0", "@standardnotes/stylekit": "5.27.1", "@zip.js/zip.js": "^2.4.10", "mobx": "^6.5.0", diff --git a/yarn.lock b/yarn.lock index 5b1903a7d..f5bff1409 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,18 +2274,18 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@standardnotes/auth@^3.18.16": - version "3.18.16" - resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.18.16.tgz#56af6d347f7d66a9c07bfee31396d3e3486f2aa0" - integrity sha512-wrkBRrHs4oMMyZ1yB1fFCiulVPf+vrB/x5+fZnlhOFxFS6oRaq2xop0sa+XIDZD6qat54JxT2ati5LyJglZ1nw== +"@standardnotes/auth@^3.18.17": + version "3.18.17" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.18.17.tgz#1adff2f4e9e42a9843cbe235cf8dd35c158e894a" + integrity sha512-RWA1z6xu9GD3/zdBXCmgMicSDGMG4vn/Ta0+LnEveQJTaDlNus+080lepiEwP20MyncUMqfvie/HSfqUMoM0iQ== dependencies: - "@standardnotes/common" "^1.21.0" + "@standardnotes/common" "^1.22.0" jsonwebtoken "^8.5.1" -"@standardnotes/common@^1.21.0": - version "1.21.0" - resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.21.0.tgz#57cd8ddd65ca827966d65801c5270f17aa16189d" - integrity sha512-TZKH/l2colOc68mn8FRTCoILpRHw5ZaJjpt/LPtPoRDy0ZPYbyKzRRzYVdoqZeEvJwDmGG24o3QB4xQpf3K0dA== +"@standardnotes/common@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.22.0.tgz#397604fb4b92901bac276940a2647509b70a7ad2" + integrity sha512-xg2SU/Pq36O2ksSotuJm94ZEaVdFzpR6rVNwBAaZzjNtW41K4e107LOVfpu1Nv6Qykv79AKBbGzZ600f5QJ3jg== "@standardnotes/components@1.8.1": version "1.8.1" @@ -2303,84 +2303,84 @@ eslint-plugin-prettier "^4.0.0" prettier "^2.6.2" -"@standardnotes/domain-events@^2.28.8": - version "2.28.8" - resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.28.8.tgz#7df9ea426b6723c0517c23e94853358b437a8cf9" - integrity sha512-OQuEWoVWyVBMdXAJtokGzQbhaRKVAs4/zShcSQLVhHHujoUIGJE9hJVu7uH4LqgPnLZ/tlaZGRylXWmK+7wSXQ== +"@standardnotes/domain-events@^2.28.9": + version "2.28.9" + resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.28.9.tgz#f2ad87ba29e45eb0d1991a3a91d917c49f5e3d99" + integrity sha512-LsmvWW5v9sMFViNcuKL/CMgz0Dww6Qi5a6jJ8CzmmbgeNLuPj8FjiQGUV5u4iF70UR633EKMiHXdmh0Gub8gSA== dependencies: - "@standardnotes/auth" "^3.18.16" - "@standardnotes/features" "^1.44.1" + "@standardnotes/auth" "^3.18.17" + "@standardnotes/features" "^1.44.2" -"@standardnotes/encryption@^1.7.12": - version "1.7.12" - resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.7.12.tgz#01edca2e84949ce64a0d3c05e438f89bc40b8e38" - integrity sha512-QOtYzdPOZhldBQ2t4C6bwBUvxsycyEfBzd/ii9UGknl1TovRR01a+B2A1DqeN8oQxBecvUC5B9Uxhxfsiq2efw== +"@standardnotes/encryption@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.8.1.tgz#b3b0ba644d6cae0395687cd1eecea452d9109fb5" + integrity sha512-FEHH88xd4zvQ5vPm8GusjM50j/G/C/zDg1997z+IvPsWubqA7ZHBXJ+9Rul87QbMZDRpP/TzBaTViWpAc8DkaQ== dependencies: - "@standardnotes/models" "^1.8.8" - "@standardnotes/responses" "^1.6.24" - "@standardnotes/services" "^1.12.2" + "@standardnotes/models" "^1.10.0" + "@standardnotes/responses" "^1.6.25" + "@standardnotes/services" "^1.13.1" -"@standardnotes/features@^1.44.1": - version "1.44.1" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.44.1.tgz#49ae92babbf29b2bc5608ccac5a1e2aa026d5b0b" - integrity sha512-++2moW5tY6bv6ahin9rP9ay3UEezhktJR5LOILx6ZsuM4KUvx2trBapxpzzTysPVvW2zuiWWeJq82M4d4ChOEw== +"@standardnotes/features@^1.44.2": + version "1.44.2" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.44.2.tgz#26a8240bf3fd88034d3f9202ffd55586098590b5" + integrity sha512-GEIFu4Fj42XTbnjUcipZHEVc3BtG2UtyGNB5IS1qXazluL5VLvY6Z42aOkWKnUX2bnJPPCpkJ7xLLijkjQtAew== dependencies: - "@standardnotes/auth" "^3.18.16" - "@standardnotes/common" "^1.21.0" + "@standardnotes/auth" "^3.18.17" + "@standardnotes/common" "^1.22.0" -"@standardnotes/filepicker@1.14.12", "@standardnotes/filepicker@^1.14.12": - version "1.14.12" - resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.14.12.tgz#ded6526f11ad7cb3309a1dc23955235350d35bef" - integrity sha512-GYIc8f0eVDJfvWV2bagRq4oJ2Uj0XJ5lemf4EReZzPK0MTpqmqvCJrlYd8WzVc8OYBaWNNUwP6qs8Esop2wuRg== +"@standardnotes/filepicker@1.15.0", "@standardnotes/filepicker@^1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.15.0.tgz#d9a5e93d31c74c7ec0f8aedc16903e678fffaeb6" + integrity sha512-44cWnUNhSubsu3fWxlVEVnw25GwCn4N9c8J59a0m9VzAtYe0aeGc0/DGRlh7vB8SVOwaOtnIL3IvDMpn6rg71A== dependencies: - "@standardnotes/common" "^1.21.0" - "@standardnotes/services" "^1.12.2" - "@standardnotes/utils" "^1.6.9" + "@standardnotes/common" "^1.22.0" + "@standardnotes/services" "^1.13.1" + "@standardnotes/utils" "^1.6.10" -"@standardnotes/files@^1.1.13": - version "1.1.13" - resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.1.13.tgz#0bc4a67c6a43538cf5fbbd2e8c34a72c5cdef61e" - integrity sha512-jEsz5+MbM3dKY3xPfjQ+FIz+0bzA2/e80hnmsqX0dzaMuYXtpEFuTG4gpkaSoGCLRm9vqJPUmgXf736s7+bs7g== +"@standardnotes/files@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.2.1.tgz#006b8fe9cd38b498984f6c6068c5c65c2160abb8" + integrity sha512-Wf+p+dwxjvs1rkqwM8N9WTIjUhCsDivE/u+8hHy9yza/UHntfzcr4mqsE7wilNSZannMgp1SBlvW4+LT+FOX/w== dependencies: - "@standardnotes/encryption" "^1.7.12" - "@standardnotes/models" "^1.8.8" - "@standardnotes/responses" "^1.6.24" - "@standardnotes/services" "^1.12.2" - "@standardnotes/utils" "^1.6.9" + "@standardnotes/encryption" "^1.8.1" + "@standardnotes/models" "^1.10.0" + "@standardnotes/responses" "^1.6.25" + "@standardnotes/services" "^1.13.1" + "@standardnotes/utils" "^1.6.10" "@standardnotes/icons@^1.1.7": version "1.1.7" resolved "https://registry.yarnpkg.com/@standardnotes/icons/-/icons-1.1.7.tgz#4523d8e2e3aa34d0e921df802d54c0babe054279" integrity sha512-yHB/KInj7qSmtnUqtTo6CPttu+2+oAwdANfjE3a69sgS7nVBxUuwSHHxb9xUOKpJBmipFjJi5F0YVF9kLmHiwg== -"@standardnotes/models@^1.8.8": - version "1.8.8" - resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.8.8.tgz#a086b597a4da9169add163fa4770f1afb40d8598" - integrity sha512-H/7BGU8wdkFNcQLhemdKUDmqoC5CacfVOU0uctTjXdOwW7PKMhtV2g7Fvj8lWWtnOYW86bKB7VV9uK7rgQcieQ== +"@standardnotes/models@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.10.0.tgz#7b8c27b8d5d7f8d60552ea0fafbae2b3061f4847" + integrity sha512-Pvi0zKDhVSvjs54ewXyrIjh9XdjTvG1dK/Z7MfPDjEPWUz8/XnaVnFuDo/0riCMTR77op3H5q402qfx3jS2dLA== dependencies: - "@standardnotes/features" "^1.44.1" - "@standardnotes/responses" "^1.6.24" - "@standardnotes/utils" "^1.6.9" + "@standardnotes/features" "^1.44.2" + "@standardnotes/responses" "^1.6.25" + "@standardnotes/utils" "^1.6.10" -"@standardnotes/responses@^1.6.24": - version "1.6.24" - resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.24.tgz#4ba71a17b70b0ba70445cdec5d1fee66ee455888" - integrity sha512-XYZ0ndkLSoPAmsTig1ER6DdN+aFWge6DS4NRCw+bYXcKu73Km/r8F5Q44nOKIqj6Hvn0jDceD276UcUpesahqw== +"@standardnotes/responses@^1.6.25": + version "1.6.25" + resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.25.tgz#96867fd1154bbf6004b87d4fb87d4c37c13f0534" + integrity sha512-sEWx7izTaeaO5gWPMeEkX0oNOVWkBEelK1na6cy5oIf81nAsVQ3sQbr8qRXXs7nnGnJR+PeUTEwJA0aHyr156g== dependencies: - "@standardnotes/auth" "^3.18.16" - "@standardnotes/common" "^1.21.0" - "@standardnotes/features" "^1.44.1" + "@standardnotes/auth" "^3.18.17" + "@standardnotes/common" "^1.22.0" + "@standardnotes/features" "^1.44.2" -"@standardnotes/services@^1.12.2": - version "1.12.2" - resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.12.2.tgz#efc52192ac63668b2de5bcc7c3a5d082b40d942c" - integrity sha512-n+taTbHxnA63bFgioJGCoAqASbk6CFlLzYQdaoV/vAyyVntN4vyuy9hb06ku6kl2vBQQbTJse7CPHe9W1istWQ== +"@standardnotes/services@^1.13.1": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.13.1.tgz#e727c35f2eecbc5d6a4e17470f13d90a84e7203f" + integrity sha512-0cu3Glla4Ir7k5pgfD1ztNxQ37/xOE/0+MvLnU98bzAdI2oqeS9POOpPeu2EKxYU1hKjacPZTZEaHl2iR7PP0w== dependencies: - "@standardnotes/auth" "^3.18.16" - "@standardnotes/common" "^1.21.0" - "@standardnotes/models" "^1.8.8" - "@standardnotes/responses" "^1.6.24" - "@standardnotes/utils" "^1.6.9" + "@standardnotes/auth" "^3.18.17" + "@standardnotes/common" "^1.22.0" + "@standardnotes/models" "^1.10.0" + "@standardnotes/responses" "^1.6.25" + "@standardnotes/utils" "^1.6.10" "@standardnotes/settings@^1.14.3": version "1.14.3" @@ -2401,24 +2401,24 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.110.3": - version "2.110.3" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.110.3.tgz#ea9fc61598a8ddebac342e74f2cfb7fce45ac4f4" - integrity sha512-oXj0cSs+NxCHahqo3hYr/nGlOP0u+jPmsDDCUfG1XAOi/mftMTXTPeIWzNceNYVZNLoF/HmDME3zlcWn7WTXXw== +"@standardnotes/snjs@2.113.0": + version "2.113.0" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.113.0.tgz#4dd65cafc633da1c0a7e04a23ee7f0fed9ba80cf" + integrity sha512-fZHZbFDfZWHkpQsz8qOBy4Q0kbdmHNtW28cbbzwGKglVbmWeCEb07DG3/vkCMSJj+4UiHUw6lWv8D7qm08k7rw== dependencies: - "@standardnotes/auth" "^3.18.16" - "@standardnotes/common" "^1.21.0" - "@standardnotes/domain-events" "^2.28.8" - "@standardnotes/encryption" "^1.7.12" - "@standardnotes/features" "^1.44.1" - "@standardnotes/filepicker" "^1.14.12" - "@standardnotes/files" "^1.1.13" - "@standardnotes/models" "^1.8.8" - "@standardnotes/responses" "^1.6.24" - "@standardnotes/services" "^1.12.2" + "@standardnotes/auth" "^3.18.17" + "@standardnotes/common" "^1.22.0" + "@standardnotes/domain-events" "^2.28.9" + "@standardnotes/encryption" "^1.8.1" + "@standardnotes/features" "^1.44.2" + "@standardnotes/filepicker" "^1.15.0" + "@standardnotes/files" "^1.2.1" + "@standardnotes/models" "^1.10.0" + "@standardnotes/responses" "^1.6.25" + "@standardnotes/services" "^1.13.1" "@standardnotes/settings" "^1.14.3" "@standardnotes/sncrypto-common" "^1.9.0" - "@standardnotes/utils" "^1.6.9" + "@standardnotes/utils" "^1.6.10" "@standardnotes/stylekit@5.27.1": version "5.27.1" @@ -2433,12 +2433,12 @@ nanostores "^0.5.10" prop-types "^15.8.1" -"@standardnotes/utils@^1.6.9": - version "1.6.9" - resolved "https://registry.yarnpkg.com/@standardnotes/utils/-/utils-1.6.9.tgz#dcfbcd1a16455b327dd714fa299f4092542ce061" - integrity sha512-Umog99gJgvVx/EDDsaF63+KXg7jkWL8qAgLT/oxLI8/kRGQfe+16nwlIPR/QkaTHJa415mvChz2aIhf4now0/w== +"@standardnotes/utils@^1.6.10": + version "1.6.10" + resolved "https://registry.yarnpkg.com/@standardnotes/utils/-/utils-1.6.10.tgz#25816fd072ebe4b0e83585237bfd7ba528b4faef" + integrity sha512-XXWAx67CMMRZBFcLYbOxeAIBhlPAziU+hBcwHfHMiZ1qZ+2/tNEVZHsXIcBPwZAQ0bvGWE7TIgCSaTk45qdKUg== dependencies: - "@standardnotes/common" "^1.21.0" + "@standardnotes/common" "^1.22.0" dompurify "^2.3.6" lodash "^4.17.21"