Skip to content

Commit

Permalink
feat: persist selected tag & note locally (#1851)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara committed Oct 20, 2022
1 parent 6c47f95 commit 4432f1c
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { WebApplication } from '@/Application/Application'
import { KeyboardKey } from '@standardnotes/ui-services'
import { UuidString } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
Expand All @@ -22,7 +21,7 @@ type Props = {
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
selectedItems: Record<UuidString, ListableContentItem>
selectedUuids: SelectedItemsController['selectedUuids']
paginate: () => void
}

Expand All @@ -34,7 +33,7 @@ const ContentList: FunctionComponent<Props> = ({
navigationController,
notesController,
selectionController,
selectedItems,
selectedUuids,
paginate,
}) => {
const { selectPreviousItem, selectNextItem } = selectionController
Expand Down Expand Up @@ -82,7 +81,7 @@ const ContentList: FunctionComponent<Props> = ({
key={item.uuid}
application={application}
item={item}
selected={!!selectedItems[item.uuid]}
selected={selectedUuids.has(item.uuid)}
hideDate={hideDate}
hidePreview={hideNotePreview}
hideTags={hideTags}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const ContentListView: FunctionComponent<Props> = ({
searchBarElement,
} = itemListController

const { selectedItems, selectNextItem, selectPreviousItem } = selectionController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController

const isFilesSmartView = useMemo(
() => navigationController.selected?.uuid === SystemViewId.Files,
Expand Down Expand Up @@ -276,7 +276,7 @@ const ContentListView: FunctionComponent<Props> = ({
{renderedItems.length ? (
<ContentList
items={renderedItems}
selectedItems={selectedItems}
selectedUuids={selectedUuids}
application={application}
paginate={paginate}
filesController={filesController}
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/javascripts/Components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SwitchProps } from '@/Components/Switch/SwitchProps'
import { IconType } from '@standardnotes/snjs'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { MenuItemType } from './MenuItemType'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'

type MenuItemProps = {
children: ReactNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LabsPane from './Labs/Labs'
import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSection'
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
import PlaintextDefaults from './PlaintextDefaults'
import Persistence from './Persistence'

type Props = {
viewControllerManager: ViewControllerManager
Expand All @@ -18,6 +19,7 @@ type Props = {

const General: FunctionComponent<Props> = ({ viewControllerManager, application, extensionsLatestVersions }) => (
<PreferencesPane>
<Persistence application={application} />
<PlaintextDefaults application={application} />
<Defaults application={application} />
<Tools application={application} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { WebApplication } from '@/Application/Application'
import StyledRadioInput from '@/Components/Radio/StyledRadioInput'
import { useState } from 'react'
import { Title } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'

type Props = {
application: WebApplication
}

export const ShouldPersistNoteStateKey = 'ShouldPersistNoteState'

const Persistence = ({ application }: Props) => {
const [shouldPersistNoteState, setShouldPersistNoteState] = useState(application.getValue(ShouldPersistNoteStateKey))

const toggleStatePersistence = (shouldPersist: boolean) => {
application.setValue(ShouldPersistNoteStateKey, shouldPersist)
setShouldPersistNoteState(shouldPersist)
}

return (
<PreferencesGroup>
<PreferencesSegment>
<Title className="mb-2">When opening the app, show...</Title>
<label className="mb-2 flex items-center gap-2 text-sm font-medium">
<StyledRadioInput
name="state-persistence"
checked={!shouldPersistNoteState}
onChange={(event) => {
toggleStatePersistence(!event.target.checked)
}}
/>
The first note in the list
</label>
<label className="flex items-center gap-2 text-sm font-medium">
<StyledRadioInput
name="state-persistence"
checked={!!shouldPersistNoteState}
onChange={(event) => {
toggleStatePersistence(event.target.checked)
}}
/>
The last viewed note
</label>
</PreferencesSegment>
</PreferencesGroup>
)
}

export default Persistence
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import FocusModeSwitch from './FocusModeSwitch'
import ThemesMenuButton from './ThemesMenuButton'
import { ThemeItem } from './ThemeItem'
import { sortThemes } from '@/Utils/SortThemes'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import PanelSettingsSection from './PanelSettingsSection'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Icon from '@/Components/Icon/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import Switch from '@/Components/Switch/Switch'
import { ThemeItem } from './ThemeItem'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { isMobileScreen } from '@/Utils'

Expand Down
16 changes: 16 additions & 0 deletions packages/web/src/javascripts/Components/Radio/StyledRadioInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { classNames } from '@/Utils/ConcatenateClassNames'
import { ComponentPropsWithoutRef } from 'react'
import RadioIndicator from './RadioIndicator'

type Props = ComponentPropsWithoutRef<'input'>

const StyledRadioInput = (props: Props) => {
return (
<div className="flex">
<input type="radio" className={classNames('h-0 w-0 opacity-0', props.className)} {...props} />
<RadioIndicator checked={!!props.checked} />
</div>
)
}

export default StyledRadioInput
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { FunctionComponent, ReactNode } from 'react'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import RadioIndicator from '../Radio/RadioIndicator'

type HistoryListItemProps = {
isSelected: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
}, [setTitle, view])

const selectCurrentTag = useCallback(async () => {
await tagsState.setSelectedTag(view)
await tagsState.setSelectedTag(view, {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [tagsState, toggleAppPane, view])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export const TagsListItem: FunctionComponent<Props> = observer(
)

const selectCurrentTag = useCallback(async () => {
await tagsState.setSelectedTag(tag)
await tagsState.setSelectedTag(tag, {
userTriggered: true,
})
toggleAppPane(AppPaneId.Items)
}, [tagsState, tag, toggleAppPane])

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Persistable<T> {
getPersistableValue(): T
hydrateFromPersistedValue(value: T | undefined): void
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { WebApplication } from '@/Application/Application'
import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence'
import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs'
import { CrossControllerEvent } from '../CrossControllerEvent'

const MasterPersistenceKey = 'master-persistence-key'

export enum PersistenceKey {
SelectedItemsController = 'selected-items-controller',
NavigationController = 'navigation-controller',
ItemListController = 'item-list-controller',
}

export type MasterPersistedValue = Record<PersistenceKey, unknown>

export class PersistenceService {
private unsubAppEventObserver: () => void

constructor(private application: WebApplication, private eventBus: InternalEventBus) {
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
if (!this.application) {
return
}

void this.onAppEvent(eventName)
})
}

async onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.LocalDataLoaded) {
let shouldHydrateState = this.application.getValue(ShouldPersistNoteStateKey)

if (typeof shouldHydrateState === 'undefined') {
this.application.setValue(ShouldPersistNoteStateKey, true)
shouldHydrateState = true
}

this.eventBus.publish({
type: CrossControllerEvent.HydrateFromPersistedValues,
payload: shouldHydrateState ? this.getPersistedValues() : undefined,
})
}
}

persistValues(values: MasterPersistedValue): void {
if (!this.application.isDatabaseLoaded()) {
return
}

this.application.setValue(MasterPersistenceKey, values)
}

getPersistedValues(): MasterPersistedValue {
return this.application.getValue(MasterPersistenceKey) as MasterPersistedValue
}

deinit() {
this.unsubAppEventObserver()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export enum CrossControllerEvent {
TagChanged = 'TagChanged',
ActiveEditorChanged = 'ActiveEditorChanged',
HydrateFromPersistedValues = 'HydrateFromPersistedValues',
RequestValuePersistence = 'RequestValuePersistence',
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/Application'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { WebDisplayOptions } from './WebDisplayOptions'
import { NavigationController } from '../Navigation/NavigationController'
import { CrossControllerEvent } from '../CrossControllerEvent'
Expand All @@ -33,6 +32,8 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import dayjs from 'dayjs'
import { LinkingController } from '../LinkingController'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'

const MinNoteCellHeight = 51.0
const DefaultListNumNotes = 20
Expand All @@ -45,10 +46,18 @@ enum ItemsReloadSource {
DisplayOptionsChange,
Pagination,
TagChange,
UserTriggeredTagChange,
FilterTextChange,
}

export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
export type ItemListControllerPersistableValue = {
displayOptions: DisplayOptions
}

export class ItemListController
extends AbstractViewController
implements Persistable<ItemListControllerPersistableValue>, InternalEventHandlerInterface
{
completedFullSync = false
noteFilterText = ''
notes: SNNote[] = []
Expand Down Expand Up @@ -204,16 +213,33 @@ export class ItemListController extends AbstractViewController implements Intern
handleFilterTextChanged: action,

optionsSubtitle: computed,
activeControllerItem: computed,
})

window.onresize = () => {
this.resetPagination(true)
}
}

getPersistableValue = (): ItemListControllerPersistableValue => {
return {
displayOptions: this.displayOptions,
}
}

hydrateFromPersistedValue = (state: ItemListControllerPersistableValue | undefined) => {
if (!state) {
return
}
if (state.displayOptions) {
this.displayOptions = state.displayOptions
}
}

async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === CrossControllerEvent.TagChanged) {
this.handleTagChange()
const payload = event.payload as { userTriggered: boolean }
this.handleTagChange(payload.userTriggered)
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
this.handleEditorChange().catch(console.error)
}
Expand Down Expand Up @@ -335,8 +361,10 @@ export class ItemListController extends AbstractViewController implements Intern
)
}

private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource, activeItem: SNNote | FileItem | undefined) => {
return itemsReloadSource === ItemsReloadSource.TagChange || !activeItem
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
return (
itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange || !this.selectionController.selectedUuids.size
)
}

private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
Expand All @@ -359,7 +387,7 @@ export class ItemListController extends AbstractViewController implements Intern
}

private shouldSelectActiveItem = (activeItem: SNNote | FileItem | undefined) => {
return activeItem && !this.selectionController.selectedItems[activeItem.uuid]
return activeItem && !this.selectionController.isItemSelected(activeItem)
}

private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
Expand All @@ -371,7 +399,7 @@ export class ItemListController extends AbstractViewController implements Intern

const activeItem = activeController?.item

if (this.shouldSelectFirstItem(itemsReloadSource, activeItem)) {
if (this.shouldSelectFirstItem(itemsReloadSource)) {
await this.selectFirstItem()
} else if (this.shouldCloseActiveItem(activeItem) && activeController) {
this.closeItemController(activeController)
Expand Down Expand Up @@ -500,6 +528,11 @@ export class ItemListController extends AbstractViewController implements Intern
if (newDisplayOptions.sortBy !== currentSortBy) {
await this.selectFirstItem()
}

this.eventBus.publish({
type: CrossControllerEvent.RequestValuePersistence,
payload: undefined,
})
}

async createNewNoteController(title?: string) {
Expand Down Expand Up @@ -664,7 +697,7 @@ export class ItemListController extends AbstractViewController implements Intern
this.application.itemControllerGroup.closeItemController(controller)
}

handleTagChange = () => {
handleTagChange = (userTriggered: boolean) => {
const activeNoteController = this.getActiveItemController()
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
this.closeItemController(activeNoteController)
Expand All @@ -682,7 +715,7 @@ export class ItemListController extends AbstractViewController implements Intern

this.reloadNotesDisplayOptions()

void this.reloadItems(ItemsReloadSource.TagChange)
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
}

onFilterEnter = () => {
Expand Down

0 comments on commit 4432f1c

Please sign in to comment.