diff --git a/app/assets/javascripts/components/PreferencesMenuItem.tsx b/app/assets/javascripts/components/PreferencesMenuItem.tsx
new file mode 100644
index 000000000..eb872053a
--- /dev/null
+++ b/app/assets/javascripts/components/PreferencesMenuItem.tsx
@@ -0,0 +1,23 @@
+import { Icon, IconType } from '@/components/Icon';
+import { FunctionComponent } from 'preact';
+
+interface PreferencesMenuItemProps {
+ iconType: IconType;
+ label: string;
+ selected: boolean;
+ onClick: () => void;
+}
+
+export const PreferencesMenuItem: FunctionComponent
=
+ ({ iconType, label, selected, onClick }) => (
+ {
+ e.preventDefault();
+ onClick();
+ }}
+ >
+
+ {label}
+
+ );
diff --git a/app/assets/javascripts/components/SearchOptions.tsx b/app/assets/javascripts/components/SearchOptions.tsx
index 8315c3a91..ddd320d93 100644
--- a/app/assets/javascripts/components/SearchOptions.tsx
+++ b/app/assets/javascripts/components/SearchOptions.tsx
@@ -11,6 +11,7 @@ import {
} from '@reach/disclosure';
import { Switch } from './Switch';
import { observer } from 'mobx-react-lite';
+import { useEffect } from 'react';
type Props = {
appState: AppState;
@@ -45,16 +46,27 @@ const SearchOptions = observer(({ appState }: Props) => {
}
}
+ const updateWidthAndPosition = () => {
+ const rect = buttonRef.current.getBoundingClientRect();
+ setMaxWidth(rect.right - 16);
+ setPosition({
+ top: rect.bottom,
+ right: document.body.clientWidth - rect.right,
+ });
+ };
+
+ useEffect(() => {
+ window.addEventListener('resize', updateWidthAndPosition);
+ return () => {
+ window.removeEventListener('resize', updateWidthAndPosition);
+ };
+ }, []);
+
return (
{
- const rect = buttonRef.current.getBoundingClientRect();
- setMaxWidth(rect.right - 16);
- setPosition({
- top: rect.bottom,
- right: document.body.clientWidth - rect.right,
- });
+ updateWidthAndPosition();
setOpen(!open);
}}
>
diff --git a/app/assets/javascripts/components/SessionsModal.tsx b/app/assets/javascripts/components/SessionsModal.tsx
index 6677ff943..875f25f9c 100644
--- a/app/assets/javascripts/components/SessionsModal.tsx
+++ b/app/assets/javascripts/components/SessionsModal.tsx
@@ -76,7 +76,6 @@ function useSessions(
setSessions(sessionsDuringRevoke);
const response = await responsePromise;
- console.log(response);
if (isNullOrUndefined(response)) {
setSessions(sessionsBeforeRevoke);
} else if ('error' in response) {
diff --git a/app/assets/javascripts/components/TitleBar.tsx b/app/assets/javascripts/components/TitleBar.tsx
new file mode 100644
index 000000000..8cf9e9208
--- /dev/null
+++ b/app/assets/javascripts/components/TitleBar.tsx
@@ -0,0 +1,13 @@
+import { FunctionComponent } from 'preact';
+
+export const TitleBar: FunctionComponent<{ className?: string }> = ({
+ children,
+ className,
+}) => {children}
;
+
+export const Title: FunctionComponent<{ className?: string }> = ({
+ children,
+ className,
+}) => {
+ return {children}
;
+};
diff --git a/app/assets/javascripts/components/preferences/index.tsx b/app/assets/javascripts/components/preferences/index.tsx
new file mode 100644
index 000000000..7ad6bb318
--- /dev/null
+++ b/app/assets/javascripts/components/preferences/index.tsx
@@ -0,0 +1,22 @@
+import { observer } from 'mobx-react-lite';
+import { FunctionComponent } from 'preact';
+
+import { toDirective } from '../utils';
+import { PreferencesView } from './view';
+
+interface WrapperProps {
+ appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
+}
+
+const PreferencesViewWrapper: FunctionComponent = observer(
+ ({ appState }) => {
+ if (!appState.preferences.isOpen) return null;
+ return (
+ appState.preferences.closePreferences()} />
+ );
+ }
+);
+
+export const PreferencesDirective = toDirective(
+ PreferencesViewWrapper
+);
diff --git a/app/assets/javascripts/components/preferences/mock-state.ts b/app/assets/javascripts/components/preferences/mock-state.ts
new file mode 100644
index 000000000..dd018f243
--- /dev/null
+++ b/app/assets/javascripts/components/preferences/mock-state.ts
@@ -0,0 +1,50 @@
+import { IconType } from '@/components/Icon';
+import { action, computed, makeObservable, observable } from 'mobx';
+
+interface PreferenceItem {
+ icon: IconType;
+ label: string;
+}
+
+interface PreferenceListItem extends PreferenceItem {
+ id: number;
+}
+
+const predefinedItems: PreferenceItem[] = [
+ { label: 'General', icon: 'settings-filled' },
+ { label: 'Account', icon: 'user' },
+ { label: 'Appearance', icon: 'themes' },
+ { label: 'Security', icon: 'security' },
+ { label: 'Listed', icon: 'listed' },
+ { label: 'Shortcuts', icon: 'keyboard' },
+ { label: 'Accessibility', icon: 'accessibility' },
+ { label: 'Get a free month', icon: 'star' },
+ { label: 'Help & feedback', icon: 'help' },
+];
+
+export class MockState {
+ private readonly _items: PreferenceListItem[];
+ private _selectedId = 0;
+
+ constructor(items: PreferenceItem[] = predefinedItems) {
+ makeObservable(this, {
+ _selectedId: observable,
+ items: computed,
+ select: action,
+ });
+
+ this._items = items.map((p, idx) => ({ ...p, id: idx }));
+ this._selectedId = this._items[0].id;
+ }
+
+ select(id: number) {
+ this._selectedId = id;
+ }
+
+ get items(): (PreferenceListItem & { selected: boolean })[] {
+ return this._items.map((p) => ({
+ ...p,
+ selected: p.id === this._selectedId,
+ }));
+ }
+}
diff --git a/app/assets/javascripts/components/preferences/pane.tsx b/app/assets/javascripts/components/preferences/pane.tsx
new file mode 100644
index 000000000..2738239ee
--- /dev/null
+++ b/app/assets/javascripts/components/preferences/pane.tsx
@@ -0,0 +1,33 @@
+import { observer } from 'mobx-react-lite';
+import { FunctionComponent } from 'preact';
+import { PreferencesMenuItem } from '../PreferencesMenuItem';
+import { MockState } from './mock-state';
+
+interface PreferencesMenuProps {
+ store: MockState;
+}
+
+const PreferencesMenu: FunctionComponent = observer(
+ ({ store }) => (
+
+ {store.items.map((pref) => (
+
store.select(pref.id)}
+ />
+ ))}
+
+ )
+);
+
+export const PreferencesPane: FunctionComponent = () => {
+ const store = new MockState();
+ return (
+
+ );
+};
diff --git a/app/assets/javascripts/components/preferences/view.tsx b/app/assets/javascripts/components/preferences/view.tsx
new file mode 100644
index 000000000..01ceb8773
--- /dev/null
+++ b/app/assets/javascripts/components/preferences/view.tsx
@@ -0,0 +1,28 @@
+import { IconButton } from '@/components/IconButton';
+import { TitleBar, Title } from '@/components/TitleBar';
+import { FunctionComponent } from 'preact';
+import { PreferencesPane } from './pane';
+
+interface PreferencesViewProps {
+ close: () => void;
+}
+
+export const PreferencesView: FunctionComponent = ({
+ close,
+}) => (
+
+
+ {/* div is added so flex justify-between can center the title */}
+
+ Your preferences for Standard Notes
+ {
+ close();
+ }}
+ type="normal"
+ iconType="close"
+ />
+
+
+
+);
diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts
index cf6851e4f..cd4ae8984 100644
--- a/app/assets/javascripts/directives/views/accountMenu.ts
+++ b/app/assets/javascripts/directives/views/accountMenu.ts
@@ -18,6 +18,7 @@ import {
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
StringUtils,
+ Strings,
} from '@/strings';
import { PasswordWizardType } from '@/types';
import {
@@ -509,8 +510,17 @@ class AccountMenuCtrl extends PureViewCtrl {
}
async submitPasscodeForm() {
- const passcode = this.getState().formData.passcode!;
- if (passcode !== this.getState().formData.confirmPasscode!) {
+ const passcode = this.getState().formData.passcode;
+
+ if (!passcode || passcode.length === 0) {
+ await alertDialog({
+ text: Strings.enterPasscode,
+ });
+ this.passcodeInput[0].focus();
+ return;
+ }
+
+ if (passcode !== this.getState().formData.confirmPasscode) {
await alertDialog({
text: STRING_NON_MATCHING_PASSCODES,
});
diff --git a/app/assets/javascripts/directives/views/componentView.ts b/app/assets/javascripts/directives/views/componentView.ts
index 85b18c29d..5df917399 100644
--- a/app/assets/javascripts/directives/views/componentView.ts
+++ b/app/assets/javascripts/directives/views/componentView.ts
@@ -33,6 +33,9 @@ class ComponentViewCtrl implements ComponentViewScope {
private unregisterComponentHandler!: () => void
private unregisterDesktopObserver!: () => void
private issueLoading = false
+ private isDeprecated = false
+ private deprecationMessage = ''
+ private deprecationMessageDismissed = false
public reloading = false
private expired = false
private loading = false
@@ -175,6 +178,12 @@ class ComponentViewCtrl implements ComponentViewScope {
});
}
+ private dismissDeprecationMessage() {
+ this.$timeout(() => {
+ this.deprecationMessageDismissed = true;
+ });
+ }
+
private onVisibilityChange() {
if (document.visibilityState === 'hidden') {
return;
@@ -215,6 +224,8 @@ class ComponentViewCtrl implements ComponentViewScope {
if (this.expired && doManualReload) {
this.$rootScope.$broadcast(RootScopeMessages.ReloadExtendedData);
}
+ this.isDeprecated = component.isDeprecated;
+ this.deprecationMessage = component.package_info.deprecation_message;
}
private async handleIframeLoadTimeout() {
diff --git a/app/assets/javascripts/services/ioService.ts b/app/assets/javascripts/services/ioService.ts
index bfc503dee..e1c775047 100644
--- a/app/assets/javascripts/services/ioService.ts
+++ b/app/assets/javascripts/services/ioService.ts
@@ -28,6 +28,7 @@ type KeyboardObserver = {
elements?: HTMLElement[];
notElement?: HTMLElement;
notElementIds?: string[];
+ notTags?: string[];
};
export class IOService {
@@ -175,6 +176,10 @@ export class IOService {
continue;
}
+ if (observer.notTags && observer.notTags.includes(target.tagName)) {
+ continue;
+ }
+
if (
this.eventMatchesKeyAndModifiers(
event,
diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts
index 1c112f341..3041fa42b 100644
--- a/app/assets/javascripts/strings.ts
+++ b/app/assets/javascripts/strings.ts
@@ -5,7 +5,7 @@ import { getPlatform, isDesktopApplication } from './utils';
export const STRING_SESSION_EXPIRED =
'Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.';
export const STRING_DEFAULT_FILE_ERROR =
- 'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.';
+ 'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.com/filesafe.';
export const STRING_GENERIC_SYNC_ERROR =
'There was an error syncing. Please try again. If all else fails, try signing out and signing back in.';
export function StringSyncException(data: any) {
@@ -105,7 +105,7 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
'Encryption version 004 is available. ' +
'This version strengthens the encryption algorithms your account and ' +
'local storage use. To learn more about this upgrade, visit our ' +
- 'Security Upgrade page.';
+ 'Security Upgrade page.';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
export const Strings = {
@@ -113,6 +113,7 @@ export const Strings = {
openAccountMenu: 'Open Account Menu',
trashNotesTitle: 'Move to Trash',
trashNotesText: 'Are you sure you want to move these notes to the trash?',
+ enterPasscode: 'Please enter a passcode.',
};
export const StringUtils = {
diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts
index 1c8191bd7..396968cf4 100644
--- a/app/assets/javascripts/ui_models/app_state/app_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/app_state.ts
@@ -1,4 +1,4 @@
-import { isDesktopApplication, isDev } from '@/utils';
+import { isDesktopApplication } from '@/utils';
import pull from 'lodash/pull';
import {
ApplicationEvent,
@@ -22,6 +22,7 @@ import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state';
import { NotesState } from './notes_state';
import { TagsState } from './tags_state';
+import { PreferencesState } from './preferences_state';
export enum AppStateEvent {
TagChanged,
@@ -47,8 +48,8 @@ export enum EventSource {
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise;
export class AppState {
- readonly enableUnfinishedFeatures =
- isDev || location.host.includes('app-dev.standardnotes.org');
+ readonly enableUnfinishedFeatures: boolean = (window as any)
+ ?._enable_unfinished_features;
$rootScope: ng.IRootScopeService;
$timeout: ng.ITimeoutService;
@@ -63,6 +64,7 @@ export class AppState {
showBetaWarning: boolean;
readonly accountMenu = new AccountMenuState();
readonly actionsMenu = new ActionsMenuState();
+ readonly preferences = new PreferencesState();
readonly noAccountWarning: NoAccountWarningState;
readonly noteTags: NoteTagsState;
readonly sync = new SyncState();
@@ -89,17 +91,14 @@ export class AppState {
async () => {
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
},
- this.appEventObserverRemovers,
+ this.appEventObserverRemovers
);
this.noteTags = new NoteTagsState(
application,
this,
this.appEventObserverRemovers
);
- this.tags = new TagsState(
- application,
- this.appEventObserverRemovers,
- ),
+ this.tags = new TagsState(application, this.appEventObserverRemovers);
this.noAccountWarning = new NoAccountWarningState(
application,
this.appEventObserverRemovers
@@ -128,6 +127,7 @@ export class AppState {
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
+ preferences: observable,
enableBetaWarning: action,
disableBetaWarning: action,
diff --git a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts
index 3f46a0781..3ad0900e8 100644
--- a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts
@@ -30,9 +30,6 @@ export class NoteTagsState {
autocompleteTagHintVisible: computed,
- clearAutocompleteSearch: action,
- focusNextTag: action,
- focusPreviousTag: action,
setAutocompleteInputFocused: action,
setAutocompleteSearchQuery: action,
setAutocompleteTagHintFocused: action,
@@ -41,7 +38,6 @@ export class NoteTagsState {
setFocusedTagUuid: action,
setTags: action,
setTagsContainerMaxWidth: action,
- reloadTags: action,
});
appEventListeners.push(
diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts
index 66d1ad024..9b59fc6c9 100644
--- a/app/assets/javascripts/ui_models/app_state/notes_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts
@@ -28,6 +28,7 @@ export class NotesState {
top: 0,
left: 0,
};
+ contextMenuClickLocation: { x: number, y: number } = { x: 0, y: 0 };
contextMenuMaxHeight: number | 'auto' = 'auto';
showProtectedWarning = false;
@@ -47,6 +48,7 @@ export class NotesState {
trashedNotesCount: computed,
setContextMenuOpen: action,
+ setContextMenuClickLocation: action,
setContextMenuPosition: action,
setContextMenuMaxHeight: action,
setShowProtectedWarning: action,
@@ -183,6 +185,10 @@ export class NotesState {
this.contextMenuOpen = open;
}
+ setContextMenuClickLocation(location: { x: number, y: number }): void {
+ this.contextMenuClickLocation = location;
+ }
+
setContextMenuPosition(position: {
top?: number;
left: number;
@@ -195,6 +201,60 @@ export class NotesState {
this.contextMenuMaxHeight = maxHeight;
}
+ reloadContextMenuLayout(): void {
+ const { clientHeight } = document.documentElement;
+ const defaultFontSize = window.getComputedStyle(
+ document.documentElement
+ ).fontSize;
+ const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
+ const footerHeight = 32;
+
+ // Open up-bottom is default behavior
+ let openUpBottom = true;
+
+ const bottomSpace = clientHeight - footerHeight - this.contextMenuClickLocation.y;
+ const upSpace = this.contextMenuClickLocation.y;
+
+ // If not enough space to open up-bottom
+ if (maxContextMenuHeight > bottomSpace) {
+ // If there's enough space, open bottom-up
+ if (upSpace > maxContextMenuHeight) {
+ openUpBottom = false;
+ this.setContextMenuMaxHeight(
+ 'auto'
+ );
+ // Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
+ } else {
+ if (upSpace > bottomSpace) {
+ this.setContextMenuMaxHeight(
+ upSpace - 2
+ );
+ openUpBottom = false;
+ } else {
+ this.setContextMenuMaxHeight(
+ bottomSpace - 2
+ );
+ }
+ }
+ } else {
+ this.setContextMenuMaxHeight(
+ 'auto'
+ );
+ }
+
+ if (openUpBottom) {
+ this.setContextMenuPosition({
+ top: this.contextMenuClickLocation.y,
+ left: this.contextMenuClickLocation.x,
+ });
+ } else {
+ this.setContextMenuPosition({
+ bottom: clientHeight - this.contextMenuClickLocation.y,
+ left: this.contextMenuClickLocation.x,
+ });
+ }
+ }
+
async changeSelectedNotes(
mutate: (mutator: NoteMutator) => void
): Promise {
diff --git a/app/assets/javascripts/ui_models/app_state/preferences_state.ts b/app/assets/javascripts/ui_models/app_state/preferences_state.ts
new file mode 100644
index 000000000..607cd23e3
--- /dev/null
+++ b/app/assets/javascripts/ui_models/app_state/preferences_state.ts
@@ -0,0 +1,26 @@
+import { action, computed, makeObservable, observable } from 'mobx';
+
+export class PreferencesState {
+ private _open = false;
+
+ constructor() {
+ makeObservable(this, {
+ _open: observable,
+ openPreferences: action,
+ closePreferences: action,
+ isOpen: computed,
+ });
+ }
+
+ openPreferences = (): void => {
+ this._open = true;
+ };
+
+ closePreferences = (): void => {
+ this._open = false;
+ };
+
+ get isOpen() {
+ return this._open;
+ }
+}
diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug
index a1c08a28d..0f473bbea 100644
--- a/app/assets/javascripts/views/application/application-view.pug
+++ b/app/assets/javascripts/views/application/application-view.pug
@@ -26,6 +26,9 @@
application='self.application'
app-state='self.appState'
)
+ preferences(
+ app-state='self.appState'
+ )
challenge-modal(
ng-repeat="challenge in self.challenges track by challenge.id"
class="sk-modal"
@@ -36,3 +39,4 @@
notes-context-menu(
app-state='self.appState'
)
+
diff --git a/app/assets/javascripts/views/editor/editor_view.ts b/app/assets/javascripts/views/editor/editor_view.ts
index 822c4d50e..2e2936fac 100644
--- a/app/assets/javascripts/views/editor/editor_view.ts
+++ b/app/assets/javascripts/views/editor/editor_view.ts
@@ -863,7 +863,7 @@ class EditorViewCtrl extends PureViewCtrl {
.io
.addKeyObserver({
key: KeyboardKey.Backspace,
- notElementIds: [ElementIds.NoteTextEditor, ElementIds.NoteTitleEditor],
+ notTags: ['INPUT', 'TEXTAREA'],
modifiers: [KeyboardModifier.Meta],
onKeyDown: () => {
this.deleteNote(false);
diff --git a/app/assets/javascripts/views/footer/footer-view.pug b/app/assets/javascripts/views/footer/footer-view.pug
index a4aa5c293..8abd9c47a 100644
--- a/app/assets/javascripts/views/footer/footer-view.pug
+++ b/app/assets/javascripts/views/footer/footer-view.pug
@@ -18,6 +18,11 @@
ng-if='ctrl.showAccountMenu',
application='ctrl.application'
)
+ .sk-app-bar-item(
+ ng-click='ctrl.clickPreferences()'
+ ng-if='ctrl.appState.enableUnfinishedFeatures'
+ )
+ .sk-label.title Preferences
.sk-app-bar-item
a.no-decoration.sk-label.title(
href='https://standardnotes.com/help',
diff --git a/app/assets/javascripts/views/footer/footer_view.ts b/app/assets/javascripts/views/footer/footer_view.ts
index 64a1b2703..9764d3c2a 100644
--- a/app/assets/javascripts/views/footer/footer_view.ts
+++ b/app/assets/javascripts/views/footer/footer_view.ts
@@ -33,44 +33,47 @@ const ACCOUNT_SWITCHER_ENABLED = false;
const ACCOUNT_SWITCHER_FEATURE_KEY = 'account_switcher';
type DockShortcut = {
- name: string,
- component: SNComponent,
+ name: string;
+ component: SNComponent;
icon: {
- type: string
- background_color: string
- border_color: string
- }
-}
+ type: string;
+ background_color: string;
+ border_color: string;
+ };
+};
-class FooterViewCtrl extends PureViewCtrl {
- private $rootScope: ng.IRootScopeService
- private rooms: SNComponent[] = []
- private themesWithIcons: SNTheme[] = []
- private showSyncResolution = false
- private unregisterComponent: any
- private rootScopeListener1: any
- private rootScopeListener2: any
- public arbitraryStatusMessage?: string
- public user?: any
- private offline = true
- public showAccountMenu = false
- private didCheckForOffline = false
- private queueExtReload = false
- private reloadInProgress = false
- public hasError = false
- public isRefreshing = false
- public lastSyncDate?: string
- public newUpdateAvailable = false
- public dockShortcuts: DockShortcut[] = []
- public roomShowState: Partial> = {}
+class FooterViewCtrl extends PureViewCtrl<
+ unknown,
+ {
+ outOfSync: boolean;
+ hasPasscode: boolean;
+ dataUpgradeAvailable: boolean;
+ dockShortcuts: DockShortcut[];
+ hasAccountSwitcher: boolean;
+ showBetaWarning: boolean;
+ showDataUpgrade: boolean;
+ }
+> {
+ private $rootScope: ng.IRootScopeService;
+ private rooms: SNComponent[] = [];
+ private themesWithIcons: SNTheme[] = [];
+ private showSyncResolution = false;
+ private unregisterComponent: any;
+ private rootScopeListener1: any;
+ private rootScopeListener2: any;
+ public arbitraryStatusMessage?: string;
+ public user?: any;
+ private offline = true;
+ public showAccountMenu = false;
+ private didCheckForOffline = false;
+ private queueExtReload = false;
+ private reloadInProgress = false;
+ public hasError = false;
+ public isRefreshing = false;
+ public lastSyncDate?: string;
+ public newUpdateAvailable = false;
+ public dockShortcuts: DockShortcut[] = [];
+ public roomShowState: Partial> = {};
private observerRemovers: Array<() => void> = [];
private completedInitialSync = false;
private showingDownloadStatus = false;
@@ -117,7 +120,7 @@ class FooterViewCtrl extends PureViewCtrl {
this.setState({
- dataUpgradeAvailable: available
+ dataUpgradeAvailable: available,
});
});
}
@@ -176,19 +181,25 @@ class FooterViewCtrl extends PureViewCtrl {
- this.reloadExtendedData();
- });
- this.rootScopeListener2 = this.$rootScope.$on(RootScopeMessages.NewUpdateAvailable, () => {
- this.$timeout(() => {
- this.onNewUpdateAvailable();
- });
- });
+ this.rootScopeListener1 = this.$rootScope.$on(
+ RootScopeMessages.ReloadExtendedData,
+ () => {
+ this.reloadExtendedData();
+ }
+ );
+ this.rootScopeListener2 = this.$rootScope.$on(
+ RootScopeMessages.NewUpdateAvailable,
+ () => {
+ this.$timeout(() => {
+ this.onNewUpdateAvailable();
+ });
+ }
+ );
}
/** @override */
@@ -202,11 +213,11 @@ class FooterViewCtrl extends PureViewCtrl {
- return (
- theme.package_info &&
- theme.package_info.dock_icon
- );
+ return theme.package_info && theme.package_info.dock_icon;
}
);
- this.observerRemovers.push(this.application.streamItems(
- ContentType.Component,
- async () => {
- const components = this.application.getItems(ContentType.Component) as SNComponent[];
+ this.observerRemovers.push(
+ this.application.streamItems(ContentType.Component, async () => {
+ const components = this.application.getItems(
+ ContentType.Component
+ ) as SNComponent[];
this.rooms = components.filter((candidate) => {
return candidate.area === ComponentArea.Rooms && !candidate.deleted;
});
@@ -308,33 +317,38 @@ class FooterViewCtrl extends PureViewCtrl {
- const themes = this.application.getDisplayableItems(ContentType.Theme) as SNTheme[];
+ this.observerRemovers.push(
+ this.application.streamItems(ContentType.Theme, async () => {
+ const themes = this.application.getDisplayableItems(
+ ContentType.Theme
+ ) as SNTheme[];
this.themesWithIcons = themes;
this.reloadDockShortcuts();
- }
- ));
+ })
+ );
}
registerComponentHandler() {
- this.unregisterComponent = this.application.componentManager!.registerHandler({
- identifier: 'room-bar',
- areas: [ComponentArea.Rooms, ComponentArea.Modal],
- focusHandler: (component, focused) => {
- if (component.isEditor() && focused) {
- if (component.package_info?.identifier === 'org.standardnotes.standard-sheets') {
- return;
+ this.unregisterComponent =
+ this.application.componentManager!.registerHandler({
+ identifier: 'room-bar',
+ areas: [ComponentArea.Rooms, ComponentArea.Modal],
+ focusHandler: (component, focused) => {
+ if (component.isEditor() && focused) {
+ if (
+ component.package_info?.identifier ===
+ 'org.standardnotes.standard-sheets'
+ ) {
+ return;
+ }
+ this.closeAllRooms();
+ this.closeAccountMenu();
}
- this.closeAllRooms();
- this.closeAccountMenu();
- }
- }
- });
+ },
+ });
}
updateSyncStatus() {
@@ -354,17 +368,17 @@ class FooterViewCtrl extends PureViewCtrl 20) {
- const completionPercentage = stats.uploadCompletionCount === 0
- ? 0
- : stats.uploadCompletionCount / stats.uploadTotalCount;
+ const completionPercentage =
+ stats.uploadCompletionCount === 0
+ ? 0
+ : stats.uploadCompletionCount / stats.uploadTotalCount;
- const stringPercentage = completionPercentage.toLocaleString(
- undefined,
- { style: 'percent' }
- );
+ const stringPercentage = completionPercentage.toLocaleString(undefined, {
+ style: 'percent',
+ });
statusManager.setMessage(
- `Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`,
+ `Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`
);
} else {
statusManager.setMessage('');
@@ -398,8 +412,10 @@ class FooterViewCtrl extends PureViewCtrl {
- return room.package_info.identifier === this.application
- .getNativeExtService().extManagerId;
+ return (
+ room.package_info.identifier ===
+ this.application.getNativeExtService().extManagerId
+ );
});
if (!extWindow) {
this.queueExtReload = true;
@@ -419,11 +435,13 @@ class FooterViewCtrl extends PureViewCtrl {
await this.application.upgradeProtocolVersion();
});
@@ -453,25 +471,27 @@ class FooterViewCtrl extends PureViewCtrl {
- this.$timeout(() => {
- this.isRefreshing = false;
- }, 200);
- if (response && response.error) {
- this.application.alertService!.alert(
- STRING_GENERIC_SYNC_ERROR
- );
- } else {
- this.syncUpdated();
- }
- });
+ this.application
+ .sync({
+ queueStrategy: SyncQueueStrategy.ForceSpawnNew,
+ checkIntegrity: true,
+ })
+ .then((response) => {
+ this.$timeout(() => {
+ this.isRefreshing = false;
+ }, 200);
+ if (response && response.error) {
+ this.application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
+ } else {
+ this.syncUpdated();
+ }
+ });
}
syncUpdated() {
- this.lastSyncDate = dateToLocalizedString(this.application.getLastSyncDate()!);
+ this.lastSyncDate = dateToLocalizedString(
+ this.application.getLastSyncDate()!
+ );
}
onNewUpdateAvailable() {
@@ -480,9 +500,7 @@ class FooterViewCtrl extends PureViewCtrlYou can silence this warning from the ' +
- 'Account menu.'
+ 'Account menu.',
});
}
@@ -563,6 +581,10 @@ class FooterViewCtrl extends PureViewCtrl {
await this.selectNote(note, true);
}
if (this.state.selectedNotes[note.uuid]) {
- const { clientHeight } = document.documentElement;
- const defaultFontSize = window.getComputedStyle(
- document.documentElement
- ).fontSize;
- const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
- const footerHeight = 32;
-
- // Open up-bottom is default behavior
- let openUpBottom = true;
-
- const bottomSpace = clientHeight - footerHeight - e.clientY;
- const upSpace = e.clientY;
-
- // If not enough space to open up-bottom
- if (maxContextMenuHeight > bottomSpace) {
- // If there's enough space, open bottom-up
- if (upSpace > maxContextMenuHeight) {
- openUpBottom = false;
- this.appState.notes.setContextMenuMaxHeight(
- 'auto'
- );
- // Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
- } else {
- if (upSpace > bottomSpace) {
- this.appState.notes.setContextMenuMaxHeight(
- upSpace - 2
- );
- openUpBottom = false;
- } else {
- this.appState.notes.setContextMenuMaxHeight(
- bottomSpace - 2
- );
- }
- }
- } else {
- this.appState.notes.setContextMenuMaxHeight(
- 'auto'
- );
- }
-
- if (openUpBottom) {
- this.appState.notes.setContextMenuPosition({
- top: e.clientY,
- left: e.clientX,
- });
- } else {
- this.appState.notes.setContextMenuPosition({
- bottom: clientHeight - e.clientY,
- left: e.clientX,
- });
- }
-
+ this.appState.notes.setContextMenuClickLocation({
+ x: e.clientX,
+ y: e.clientY,
+ });
+ this.appState.notes.reloadContextMenuLayout();
this.appState.notes.setContextMenuOpen(true);
}
}
diff --git a/app/assets/stylesheets/_main.scss b/app/assets/stylesheets/_main.scss
index a173d667c..f57cc05db 100644
--- a/app/assets/stylesheets/_main.scss
+++ b/app/assets/stylesheets/_main.scss
@@ -12,6 +12,8 @@ $z-index-footer-bar: 2000;
$z-index-footer-bar-item: 2000;
$z-index-footer-bar-item-panel: 2000;
+$z-index-preferences: 3000;
+
$z-index-lock-screen: 10000;
$z-index-modal: 10000;
@@ -238,3 +240,7 @@ $footer-height: 32px;
.icon {
margin-right: 4px;
}
+
+.z-index-preferences {
+ z-index: $z-index-preferences;
+}
diff --git a/app/assets/stylesheets/_preferences.scss b/app/assets/stylesheets/_preferences.scss
new file mode 100644
index 000000000..d57b438e4
--- /dev/null
+++ b/app/assets/stylesheets/_preferences.scss
@@ -0,0 +1,46 @@
+.preferences-menu-item {
+ @extend .border-box;
+ @extend .w-auto;
+ @extend .h-auto;
+ @extend .rounded;
+ @extend .min-w-42;
+
+ @extend .py-2;
+ @extend .px-4;
+
+ @extend .flex;
+ @extend .flex-row;
+ @extend .items-center;
+ @extend .justify-start;
+
+ @extend .gap-1;
+ @extend .text-sm;
+ @extend .cursor-pointer;
+
+ @extend .border-transparent;
+ @extend .border-solid;
+ @extend .border-1;
+
+ .icon {
+ color: var(--sn-stylekit-grey-1);
+ @extend .text-base;
+ }
+
+ &:hover {
+ @extend .border-gray-300;
+ @extend .border-solid;
+ @extend .border-1;
+ }
+}
+
+.preferences-menu-item.selected {
+ @extend .border-info;
+ @extend .color-info;
+ @extend .font-bold;
+
+ background-color: var(--sn-stylekit-info-backdrop-color);
+
+ .icon {
+ @extend .color-info;
+ }
+}
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss
index 1d6acad2c..167200535 100644
--- a/app/assets/stylesheets/_sn.scss
+++ b/app/assets/stylesheets/_sn.scss
@@ -4,42 +4,14 @@
height: 90vh;
}
-/**
- * A button that is just an icon. Separated from .sn-button because there
- * is almost no style overlap.
- */
-.sn-icon-button {
- @extend .w-8;
- @extend .h-8;
- @extend .flex;
- @extend .justify-center;
- @extend .items-center;
- @extend .border-solid;
- @extend .border-1;
- @extend .border-neutral;
- @extend .bg-clip-padding;
- @extend .m-0;
- @extend .p-0;
- @extend .bg-transparent;
- @extend .cursor-pointer;
- @extend .rounded-full;
- @extend .color-neutral;
- @extend .hover\:color-text;
- @extend .focus\:color-text;
- @extend .hover\:bg-contrast;
- @extend .focus\:bg-contrast;
- @extend .focus\:outline-none;
- @extend .focus\:ring-info;
-}
-
.sn-icon {
@extend .h-5;
@extend .w-5;
@extend .fill-current;
&.sn-icon--small {
- @extend .h-3\.5 ;
- @extend .w-3\.5 ;
+ @extend .h-3\.5;
+ @extend .w-3\.5;
}
}
@@ -57,7 +29,7 @@
&[data-state='collapsed'] {
display: none;
}
-
+
&.sn-dropdown--animated {
@extend .transition-transform;
@extend .duration-150;
@@ -105,10 +77,7 @@
transform: translate(0px, -50%);
&.sn-switch-handle--right {
- transform: translate(
- calc(2rem - 1.125rem),
- -50%
- );
+ transform: translate(calc(2rem - 1.125rem), -50%);
}
}
@@ -152,3 +121,23 @@
@extend .hover\:bg-secondary-contrast;
@extend .focus\:bg-secondary-contrast;
}
+
+.sn-titlebar {
+ @extend .w-full;
+ @extend .bg-default;
+ @extend .h-14;
+
+ @extend .border-bottom-solid;
+ @extend .border-b-1;
+ @extend .border-gray-300;
+
+ @extend .py-3;
+ @extend .px-3;
+
+ @extend .flex;
+ @extend .flex-row;
+}
+
+.sn-title {
+ @extend .font-bold;
+}
diff --git a/app/assets/stylesheets/index.css.scss b/app/assets/stylesheets/index.css.scss
index fa0deecc9..ff9cbf500 100644
--- a/app/assets/stylesheets/index.css.scss
+++ b/app/assets/stylesheets/index.css.scss
@@ -1,15 +1,16 @@
-@import "sn-stylekit/dist/stylekit";
-@import "main";
-@import "ui";
-@import "footer";
-@import "tags";
-@import "notes";
-@import "editor";
-@import "menus";
-@import "modals";
-@import "lock-screen";
-@import "stylekit-sub";
-@import "ionicons";
-@import "reach-sub";
-@import "sessions-modal";
-@import "sn";
+@import 'sn-stylekit/dist/stylekit';
+@import 'main';
+@import 'ui';
+@import 'footer';
+@import 'tags';
+@import 'notes';
+@import 'editor';
+@import 'menus';
+@import 'modals';
+@import 'lock-screen';
+@import 'stylekit-sub';
+@import 'ionicons';
+@import 'reach-sub';
+@import 'sessions-modal';
+@import 'preferences';
+@import 'sn';
diff --git a/app/assets/templates/directives/component-view.pug b/app/assets/templates/directives/component-view.pug
index e8bd06759..917f44a66 100644
--- a/app/assets/templates/directives/component-view.pug
+++ b/app/assets/templates/directives/component-view.pug
@@ -33,6 +33,14 @@
rel='noopener',
target='_blank'
) Help
+.sn-component(ng-if='ctrl.isDeprecated && !ctrl.deprecationMessageDismissed')
+ .sk-app-bar.no-edges.no-top-edge.dynamic-height
+ .left
+ .sk-app-bar-item
+ .sk-label.warning {{ctrl.deprecationMessage || 'This extension is deprecated.'}}
+ .right
+ .sk-app-bar-item(ng-click='ctrl.dismissDeprecationMessage()')
+ button.sn-button.small.info Dismiss
.sn-component(ng-if="ctrl.error == 'offline-restricted'")
.sk-panel.static
diff --git a/app/views/application/app.html.erb b/app/views/application/app.html.erb
index db302df40..d0018ed90 100644
--- a/app/views/application/app.html.erb
+++ b/app/views/application/app.html.erb
@@ -34,6 +34,7 @@
window._extensions_manager_location = "<%= ENV['EXTENSIONS_MANAGER_LOCATION'] %>";
window._batch_manager_location = "<%= ENV['BATCH_MANAGER_LOCATION'] %>";
window._bugsnag_api_key = "<%= ENV['BUGSNAG_API_KEY'] %>";
+ window._enable_unfinished_features = "<%= ENV['ENABLE_UNFINISHED_FEATURES'] %>"
<% if Rails.env.development? %>
diff --git a/index.html b/index.html
index a7cfd5561..c5ff27c9d 100644
--- a/index.html
+++ b/index.html
@@ -33,12 +33,14 @@
data-next-version-sync-server="<%= env.DEV_NEXT_VERSION_SYNC_SERVER || env.DEV_DEFAULT_SYNC_SERVER %>"
data-extensions-manager-location="<%= env.DEV_EXTENSIONS_MANAGER_LOCATION %>"
data-bugsnag-api-key="<%= env.DEV_BUGSNAG_API_KEY %>"
+ data-enable-unfinished-features="<%= env.ENABLE_UNFINISHED_FEATURES %>"
>