Skip to content

Commit

Permalink
feat: native smart tags (#782)
Browse files Browse the repository at this point in the history
* feat: introduce native smart tags

* feat: introduce react navigation

* feat: render smart tag special cases

* feat: add create tag & all count

* feat: move components to react + mobx

* fix: workaround issue with snjs

* feat: nice smart tag icons in experimental

* feat: add back components

* fix: typo on all tags

* feat: add panel resizer + simplif code

* fix: panel resize size & refresh

* fix: auto select all notes

* style: remove legacy tag view

* style: remove legacy directives

* fix: select tag from note view

* feat: WIP smart tag rename

* fix: template checks

* fix: user can create new notes

* panel: init width

* fix: panel resizer ref

* fix: update with new component viewer

* fix: use fixed isTemplateItem & fixed findItems

* refactor: rename tags panel into navigation

* style: remove TODOs that are ok

* feat: smart tag premium check with premium service

* refactor: multi-select variables for debuggability

* fix: clean deinit code

* fix: prevent trigger tag changes event for the same uuid

* fix: typings

* fix: use minimal state

* style: remove dead code

* style: long variable names

* refactor: move magic string to module

* fix: use smart filter feature

* refactor: add task id in todo
  • Loading branch information
laurentsenta authored Jan 4, 2022
1 parent 7dd4a60 commit c3772e0
Show file tree
Hide file tree
Showing 33 changed files with 1,033 additions and 871 deletions.
3 changes: 3 additions & 0 deletions app/assets/icons/ic-notes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 34 additions & 45 deletions app/assets/javascripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,32 @@ declare global {
}
}

import { SNLog } from '@standardnotes/snjs';
import angular from 'angular';
import { configRoutes } from './routes';

import { ApplicationGroup } from './ui_models/application_group';
import { AccountSwitcher } from './views/account_switcher/account_switcher';

import { ComponentViewDirective } from '@/components/ComponentView';
import { NavigationDirective } from '@/components/Navigation';
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
import { IsWebPlatform, WebAppVersion } from '@/version';
import {
ApplicationGroupView,
ApplicationView,
NoteGroupViewDirective,
NoteViewDirective,
TagsView,
FooterView,
ChallengeModal,
ApplicationView, ChallengeModal,
FooterView, NoteGroupViewDirective,
NoteViewDirective
} from '@/views';

import { SNLog } from '@standardnotes/snjs';
import angular from 'angular';
import { AccountMenuDirective } from './components/AccountMenu';
import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal';
import { IconDirective } from './components/Icon';
import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes';
import { NoAccountWarningDirective } from './components/NoAccountWarning';
import { NotesContextMenuDirective } from './components/NotesContextMenu';
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
import { NotesViewDirective } from './components/NotesView';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
import { ProtectedNoteOverlayDirective } from './components/ProtectedNoteOverlay';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
import { SearchOptionsDirective } from './components/SearchOptions';
import { SessionsModalDirective } from './components/SessionsModal';
import {
autofocus,
clickOutside,
Expand All @@ -46,49 +55,31 @@ import {
infiniteScroll,
lowercase,
selectOnFocus,
snEnter,
snEnter
} from './directives/functional';

import {
ActionsMenu,
EditorMenu,
HistoryMenu,
InputModal,
MenuRow,
PanelResizer,
PasswordWizard,
PermissionsModal,
RevisionPreviewModal,
HistoryMenu,
SyncResolutionMenu,
SyncResolutionMenu
} from './directives/views';

import { trusted } from './filters';
import { isDev } from './utils';
import { PreferencesDirective } from './preferences';
import { PurchaseFlowDirective } from './purchaseFlow';
import { configRoutes } from './routes';
import { Bridge } from './services/bridge';
import { BrowserBridge } from './services/browserBridge';
import { startErrorReporting } from './services/errorReporting';
import { StartApplication } from './startApplication';
import { Bridge } from './services/bridge';
import { SessionsModalDirective } from './components/SessionsModal';
import { NoAccountWarningDirective } from './components/NoAccountWarning';
import { ProtectedNoteOverlayDirective } from './components/ProtectedNoteOverlay';
import { SearchOptionsDirective } from './components/SearchOptions';
import { AccountMenuDirective } from './components/AccountMenu';
import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal';
import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes';
import { NotesContextMenuDirective } from './components/NotesContextMenu';
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
import { IconDirective } from './components/Icon';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
import { PreferencesDirective } from './preferences';
import { WebAppVersion, IsWebPlatform } from '@/version';
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
import { PurchaseFlowDirective } from './purchaseFlow';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
import { ComponentViewDirective } from '@/components/ComponentView';
import { TagsListDirective } from '@/components/TagsList';
import { NotesViewDirective } from './components/NotesView';
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
import { TagsSectionDirective } from './components/Tags/TagsSection';
import { ApplicationGroup } from './ui_models/application_group';
import { isDev } from './utils';
import { AccountSwitcher } from './views/account_switcher/account_switcher';

function reloadHiddenFirefoxTab(): boolean {
/**
Expand Down Expand Up @@ -143,7 +134,6 @@ const startApplication: StartApplication = async function startApplication(
.directive('applicationView', () => new ApplicationView())
.directive('noteGroupView', () => new NoteGroupViewDirective())
.directive('noteView', () => new NoteViewDirective())
.directive('tagsView', () => new TagsView())
.directive('footerView', () => new FooterView());

// Directives - Functional
Expand Down Expand Up @@ -188,8 +178,7 @@ const startApplication: StartApplication = async function startApplication(
.directive('notesListOptionsMenu', NotesListOptionsDirective)
.directive('icon', IconDirective)
.directive('noteTagsContainer', NoteTagsContainerDirective)
.directive('tagsList', TagsListDirective)
.directive('tagsSection', TagsSectionDirective)
.directive('navigation', NavigationDirective)
.directive('preferences', PreferencesDirective)
.directive('purchaseFlow', PurchaseFlowDirective)
.directive('notesView', NotesViewDirective)
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import AuthenticatorIcon from '../../icons/ic-authenticator.svg';
import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg';
import TasksIcon from '../../icons/ic-tasks.svg';
import MarkdownIcon from '../../icons/ic-markdown.svg';
import NotesIcon from '../../icons/ic-notes.svg';
import CodeIcon from '../../icons/ic-code.svg';

import AccessibilityIcon from '../../icons/ic-accessibility.svg';
Expand Down Expand Up @@ -69,6 +70,7 @@ import { FunctionalComponent } from 'preact';
const ICONS = {
'menu-arrow-down-alt': MenuArrowDownAlt,
'menu-arrow-right': MenuArrowRight,
notes: NotesIcon,
'arrows-sort-up': ArrowsSortUpIcon,
'arrows-sort-down': ArrowsSortDownIcon,
lock: LockIcon,
Expand Down
117 changes: 117 additions & 0 deletions app/assets/javascripts/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ComponentView } from '@/components/ComponentView';
import { PanelResizer } from '@/components/PanelResizer';
import { SmartTagsSection } from '@/components/Tags/SmartTagsSection';
import { TagsSection } from '@/components/Tags/TagsSection';
import { toDirective } from '@/components/utils';
import {
PanelSide,
ResizeFinishCallback,
} from '@/directives/views/panelResizer';
import { WebApplication } from '@/ui_models/application';
import { PANEL_NAME_NAVIGATION } from '@/views/constants';
import { PrefKey } from '@standardnotes/snjs';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
import { PremiumModalProvider } from './Premium';

type Props = {
application: WebApplication;
};

export const Navigation: FunctionComponent<Props> = observer(
({ application }) => {
const appState = useMemo(() => application.getAppState(), [application]);
const componentViewer = appState.foldersComponentViewer;
const enableNativeSmartTagsFeature =
appState.features.enableNativeSmartTagsFeature;
const [panelRef, setPanelRef] = useState<HTMLDivElement | null>(null);

useEffect(() => {
const elem = document.querySelector(
'navigation'
) as HTMLDivElement | null;
setPanelRef(elem);
}, [setPanelRef]);

const onCreateNewTag = useCallback(() => {
appState.tags.createNewTemplate();
}, [appState]);

const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(_lastWidth, _lastLeft, _isMaxWidth, isCollapsed) => {
appState.noteTags.reloadTagsContainerMaxWidth();
appState.panelDidResize(PANEL_NAME_NAVIGATION, isCollapsed);
},
[appState]
);

const panelWidthEventCallback = useCallback(() => {
appState.noteTags.reloadTagsContainerMaxWidth();
}, [appState]);

return (
<PremiumModalProvider state={appState.features}>
<div
id="tags-column"
ref={setPanelRef}
className="sn-component section tags"
data-aria-label="Tags"
>
{componentViewer ? (
<div className="component-view-container">
<div className="component-view">
<ComponentView
componentViewer={componentViewer}
application={application}
appState={appState}
/>
</div>
</div>
) : (
<div id="tags-content" className="content">
<div className="tags-title-section section-title-bar">
<div className="section-title-bar-header">
<div className="sk-h3 title">
<span className="sk-bold">Views</span>
</div>
{!enableNativeSmartTagsFeature && (
<div
className="sk-button sk-secondary-contrast wide"
onClick={onCreateNewTag}
title="Create a new tag"
>
<div className="sk-label">
<i className="icon ion-plus add-button" />
</div>
</div>
)}
</div>
</div>
<div className="scrollable">
<div className="infinite-scroll">
<SmartTagsSection appState={appState} />
<TagsSection appState={appState} />
</div>
</div>
</div>
)}
{panelRef && (
<PanelResizer
application={application}
collapsable={true}
defaultWidth={150}
panel={panelRef}
prefKey={PrefKey.TagsPanelWidth}
side={PanelSide.Right}
resizeFinishCallback={panelResizeFinishCallback}
widthEventCallback={panelWidthEventCallback}
/>
)}
</div>
</PremiumModalProvider>
);
}
);

export const NavigationDirective = toDirective<Props>(Navigation);
2 changes: 1 addition & 1 deletion app/assets/javascripts/components/NoteTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
const onTagClick = (event: MouseEvent) => {
if (tagClicked && event.target !== deleteTagRef.current) {
setTagClicked(false);
appState.setSelectedTag(tag);
appState.selectedTag = tag;
} else {
setTagClicked(true);
}
Expand Down
6 changes: 3 additions & 3 deletions app/assets/javascripts/components/NotesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ const NotesView: FunctionComponent<Props> = observer(
};

const panelResizeFinishCallback: ResizeFinishCallback = (
_w,
_l,
_mw,
_lastWidth,
_lastLeft,
_isMaxWidth,
isCollapsed
) => {
appState.noteTags.reloadTagsContainerMaxWidth();
Expand Down
56 changes: 30 additions & 26 deletions app/assets/javascripts/components/Premium/usePremiumModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FeaturesState } from '@/ui_models/app_state/features_state';
import { observer } from 'mobx-react-lite';
import { FunctionalComponent } from 'preact';
import { useCallback, useContext, useState } from 'preact/hooks';
import { createContext } from 'react';
Expand All @@ -21,29 +23,31 @@ export const usePremiumModal = (): PremiumModalContextData => {
return value;
};

export const PremiumModalProvider: FunctionalComponent = ({ children }) => {
const [featureName, setFeatureName] = useState<null | string>(null);

const activate = setFeatureName;

const closeModal = useCallback(() => {
setFeatureName(null);
}, [setFeatureName]);

const showModal = !!featureName;

return (
<>
{showModal && (
<PremiumFeaturesModal
showModal={!!featureName}
featureName={featureName}
onClose={closeModal}
/>
)}
<PremiumModalProvider_ value={{ activate }}>
{children}
</PremiumModalProvider_>
</>
);
};
interface Props {
state: FeaturesState;
}

export const PremiumModalProvider: FunctionalComponent<Props> = observer(
({ state, children }) => {
const featureName = state._premiumAlertFeatureName;
const activate = state.showPremiumAlert;
const close = state.closePremiumAlert;

const showModal = !!featureName;

return (
<>
{showModal && (
<PremiumFeaturesModal
showModal={!!featureName}
featureName={featureName}
onClose={close}
/>
)}
<PremiumModalProvider_ value={{ activate }}>
{children}
</PremiumModalProvider_>
</>
);
}
);
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Icon } from '@/components/Icon';
import { usePremiumModal } from '@/components/Premium';
import {
FeaturesState,
TAG_FOLDERS_FEATURE_NAME,
} from '@/ui_models/app_state/features_state';
import { TagsState } from '@/ui_models/app_state/tags_state';
import { observer } from 'mobx-react-lite';
import { useDrop } from 'react-dnd';
import { Icon } from './Icon';
import { usePremiumModal } from './Premium';
import { DropItem, DropProps, ItemTypes } from './TagsListItem';
import { DropItem, DropProps, ItemTypes } from './dragndrop';

type Props = {
tagsState: TagsState;
Expand All @@ -18,7 +18,7 @@ export const RootTagDropZone: React.FC<Props> = observer(
({ tagsState, featuresState }) => {
const premiumModal = usePremiumModal();
const isNativeFoldersEnabled = featuresState.enableNativeFoldersFeature;
const hasFolders = tagsState.hasFolders;
const hasFolders = featuresState.hasFolders;

const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
() => ({
Expand Down
Loading

0 comments on commit c3772e0

Please sign in to comment.