Merge branch 'develop' into feature/account-menu-react

This commit is contained in:
Antonella Sgarlatta
2021-07-12 12:26:51 -03:00
39 changed files with 892 additions and 215 deletions

View File

@@ -65,6 +65,7 @@ import { NotesContextMenuDirective } from './components/NotesContextMenu';
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
import { IconDirective } from './components/Icon';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
import { PreferencesDirective } from './components/preferences';
function reloadHiddenFirefoxTab(): boolean {
/**
@@ -90,7 +91,7 @@ function reloadHiddenFirefoxTab(): boolean {
const startApplication: StartApplication = async function startApplication(
defaultSyncServerHost: string,
bridge: Bridge,
nextVersionSyncServerHost: string,
nextVersionSyncServerHost: string
) {
if (reloadHiddenFirefoxTab()) {
return;
@@ -161,7 +162,8 @@ const startApplication: StartApplication = async function startApplication(
.directive('notesContextMenu', NotesContextMenuDirective)
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
.directive('icon', IconDirective)
.directive('noteTagsContainer', NoteTagsContainerDirective);
.directive('noteTagsContainer', NoteTagsContainerDirective)
.directive('preferences', PreferencesDirective);
// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);
@@ -174,10 +176,12 @@ const startApplication: StartApplication = async function startApplication(
Object.defineProperties(window, {
application: {
get: () =>
(angular
.element(document)
.injector()
.get('mainApplicationGroup') as any).primaryApplication,
(
angular
.element(document)
.injector()
.get('mainApplicationGroup') as any
).primaryApplication,
},
});
}

View File

@@ -13,41 +13,60 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg';
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
import MoreIcon from '../../icons/ic-more.svg';
import TuneIcon from '../../icons/ic-tune.svg';
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
import HelpIcon from '../../icons/ic-help.svg';
import KeyboardIcon from '../../icons/ic-keyboard.svg';
import ListedIcon from '../../icons/ic-listed.svg';
import SecurityIcon from '../../icons/ic-security.svg';
import SettingsIcon from '../../icons/ic-settings.svg';
import StarIcon from '../../icons/ic-star.svg';
import ThemesIcon from '../../icons/ic-themes.svg';
import UserIcon from '../../icons/ic-user.svg';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
const ICONS = {
'pencil-off': PencilOffIcon,
'rich-text': RichTextIcon,
'trash': TrashIcon,
'pin': PinIcon,
'unpin': UnpinIcon,
'archive': ArchiveIcon,
'unarchive': UnarchiveIcon,
'hashtag': HashtagIcon,
trash: TrashIcon,
pin: PinIcon,
unpin: UnpinIcon,
archive: ArchiveIcon,
unarchive: UnarchiveIcon,
hashtag: HashtagIcon,
'chevron-right': ChevronRightIcon,
'restore': RestoreIcon,
'close': CloseIcon,
'password': PasswordIcon,
restore: RestoreIcon,
close: CloseIcon,
password: PasswordIcon,
'trash-sweep': TrashSweepIcon,
'more': MoreIcon,
'tune': TuneIcon,
more: MoreIcon,
tune: TuneIcon,
accessibility: AccessibilityIcon,
help: HelpIcon,
keyboard: KeyboardIcon,
listed: ListedIcon,
security: SecurityIcon,
settings: SettingsIcon,
star: StarIcon,
themes: ThemesIcon,
user: UserIcon,
};
export type IconType = keyof typeof ICONS;
type Props = {
type: keyof (typeof ICONS);
className: string;
}
type: IconType;
className?: string;
};
export const Icon: FunctionalComponent<Props> = ({ type, className }) => {
const IconComponent = ICONS[type];
return <IconComponent className={`sn-icon ${className}`} />;
};
export const IconDirective = toDirective<Props>(
Icon,
{
type: '@',
className: '@',
}
);
export const IconDirective = toDirective<Props>(Icon, {
type: '@',
className: '@',
});

View File

@@ -0,0 +1,53 @@
import { FunctionComponent } from 'preact';
import { Icon, IconType } from './Icon';
const ICON_BUTTON_TYPES: {
[type: string]: { className: string };
} = {
normal: {
className: '',
},
primary: {
className: 'info',
},
};
export type IconButtonType = keyof typeof ICON_BUTTON_TYPES;
interface IconButtonProps {
/**
* onClick - preventDefault is handled within the component
*/
onClick: () => void;
type: IconButtonType;
className?: string;
iconType: IconType;
}
/**
* CircleButton component with an icon for SPA
* preventDefault is already handled within the component
*/
export const IconButton: FunctionComponent<IconButtonProps> = ({
onClick,
type,
className,
iconType,
}) => {
const click = (e: MouseEvent) => {
e.preventDefault();
onClick();
};
const typeProps = ICON_BUTTON_TYPES[type];
return (
<button
className={`sn-icon-button ${typeProps.className} ${className ?? ''}`}
onClick={click}
>
<Icon type={iconType} />
</button>
);
};

View File

@@ -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<PreferencesMenuItemProps> =
({ iconType, label, selected, onClick }) => (
<div
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<Icon className="icon" type={iconType} />
{label}
</div>
);

View File

@@ -0,0 +1,13 @@
import { FunctionComponent } from 'preact';
export const TitleBar: FunctionComponent<{ className?: string }> = ({
children,
className,
}) => <div className={`sn-titlebar ${className ?? ''}`}>{children}</div>;
export const Title: FunctionComponent<{ className?: string }> = ({
children,
className,
}) => {
return <div className={`sn-title ${className ?? ''}`}>{children}</div>;
};

View File

@@ -0,0 +1,28 @@
import { FunctionalComponent } from 'preact';
export const Title: FunctionalComponent = ({ children }) => (
<h2 className="text-base m-0 mb-3">{children}</h2>
);
export const Subtitle: FunctionalComponent = ({ children }) => (
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
);
export const Text: FunctionalComponent = ({ children }) => (
<p className="text-xs">{children}</p>
);
export const Button: FunctionalComponent<{ label: string; link: string }> = ({
label,
link,
}) => (
<a
target="_blank"
className="block bg-default color-text rounded border-solid border-1
border-gray-300 px-4 py-2 font-bold text-sm fit-content mt-3
focus:bg-contrast hover:bg-contrast "
href={link}
>
{label}
</a>
);

View File

@@ -0,0 +1,92 @@
import { FunctionalComponent } from 'preact';
import { PreferencesGroup, PreferencesPane, PreferencesSegment } from './pane';
import { Title, Subtitle, Text, Button } from './content';
export const HelpAndFeedback: FunctionalComponent = () => (
<PreferencesPane>
<PreferencesGroup>
<PreferencesSegment>
<Title>Frequently asked questions</Title>
<Subtitle>Who can read my private notes?</Subtitle>
<Text>
Quite simply: no one but you. Not us, not your ISP, not a hacker, and
not a government agency. As long as you keep your password safe, and
your password is reasonably strong, then you are the only person in
the world with the ability to decrypt your notes. For more on how we
handle your privacy and security, check out our easy to read{' '}
<a target="_blank" href="https://standardnotes.com/privacy">
Privacy Manifesto.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I collaborate with others on a note?</Subtitle>
<Text>
Because of our encrypted architecture, Standard Notes does not
currently provide a real-time collaboration solution. Multiple users
can share the same account however, but editing at the same time may
result in sync conflicts, which may result in the duplication of
notes.
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I use Standard Notes totally offline?</Subtitle>
<Text>
Standard Notes can be used totally offline without an account, and
without an internet connection. You can find{' '}
<a
target="_blank"
href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline"
>
more details here.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Cant find your question here?</Subtitle>
<Button label="Open FAQ" link="https://standardnotes.com/help" />
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Community forum</Title>
<Text>
If you have an issue, found a bug or want to suggest a feature, you
can browse or post to the forum. Its recommended for non-account
related issues. Please read our{' '}
<a target="_blank" href="https://standardnotes.com/longevity/">
Longevity statement
</a>{' '}
before advocating for a feature request.
</Text>
<Button
label="Go to the forum"
link="https://forum.standardnotes.org/"
/>
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Slack group</Title>
<Text>
Want to meet other passionate note-takers and privacy enthusiasts?
Want to share your feedback with us? Join the Standard Notes Slack
group for discussions on security, themes, editors and more.
</Text>
<Button
link="https://standardnotes.com/slack"
label="Join our Slack group"
/>
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Account related issue?</Title>
<Text>
Send an email to help@standardnotes.org and well sort it out.
</Text>
<Button link="mailto: help@standardnotes.org" label="Email us" />
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
);

View File

@@ -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<WrapperProps> = observer(
({ appState }) => {
if (!appState.preferences.isOpen) return null;
return (
<PreferencesView close={() => appState.preferences.closePreferences()} />
);
}
);
export const PreferencesDirective = toDirective<WrapperProps>(
PreferencesViewWrapper
);

View File

@@ -0,0 +1,23 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { PreferencesMenuItem } from '../PreferencesMenuItem';
import { Preferences } from './preferences';
interface PreferencesMenuProps {
preferences: Preferences;
}
export const PreferencesMenu: FunctionComponent<PreferencesMenuProps> =
observer(({ preferences }) => (
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
{preferences.items.map((pref) => (
<PreferencesMenuItem
key={pref.id}
iconType={pref.icon}
label={pref.label}
selected={pref.selected}
onClick={() => preferences.selectItem(pref.id)}
/>
))}
</div>
));

View File

@@ -0,0 +1,33 @@
import { FunctionalComponent } from 'preact';
const HorizontalLine: FunctionalComponent<{ index: number; length: number }> =
({ index, length }) =>
index < length - 1 ? (
<hr className="h-1px w-full bg-border no-border" />
) : null;
export const PreferencesSegment: FunctionalComponent = ({ children }) => (
<div>{children}</div>
);
export const PreferencesGroup: FunctionalComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
{!Array.isArray(children)
? children
: children.map((c, i, arr) => (
<>
{c}
<HorizontalLine index={i} length={arr.length} />
</>
))}
</div>
);
export const PreferencesPane: FunctionalComponent = ({ children }) => (
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
<div className="flex-grow flex flex-col py-6 items-center">
<div className="max-w-124 flex flex-col gap-3">{children}</div>
</div>
<div className="flex-basis-55 flex-shrink-max" />
</div>
);

View File

@@ -0,0 +1,55 @@
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' },
{ 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 Preferences {
private readonly _items: PreferenceListItem[];
private _selectedId = 0;
constructor(items: PreferenceItem[] = predefinedItems) {
makeObservable<Preferences, '_selectedId'>(this, {
_selectedId: observable,
selectedItem: computed,
items: computed,
selectItem: action,
});
this._items = items.map((p, idx) => ({ ...p, id: idx }));
this._selectedId = this._items[0].id;
}
selectItem(id: number) {
this._selectedId = id;
}
get items(): (PreferenceListItem & { selected: boolean })[] {
return this._items.map((p) => ({
...p,
selected: p.id === this._selectedId,
}));
}
get selectedItem(): PreferenceListItem {
return this._items.find((item) => item.id === this._selectedId)!;
}
}

View File

@@ -0,0 +1,45 @@
import { IconButton } from '@/components/IconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
import { Preferences } from './preferences';
import { PreferencesMenu } from './menu';
import { HelpAndFeedback } from './help-feedback';
import { observer } from 'mobx-react-lite';
interface PreferencesViewProps {
close: () => void;
}
export const PreferencesCanvas: FunctionComponent<{
preferences: Preferences;
}> = observer(({ preferences: prefs }) => (
<div className="flex flex-row flex-grow min-h-0 justify-between">
<PreferencesMenu preferences={prefs}></PreferencesMenu>
{/* Temporary selector until a full solution is implemented */}
{prefs.selectedItem.label === 'Help & feedback' ? (
<HelpAndFeedback />
) : null}
</div>
));
export const PreferencesView: FunctionComponent<PreferencesViewProps> =
observer(({ close }) => {
const prefs = new Preferences();
return (
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
<TitleBar className="items-center justify-between">
{/* div is added so flex justify-between can center the title */}
<div className="h-8 w-8" />
<Title className="text-lg">Your preferences for Standard Notes</Title>
<IconButton
onClick={() => {
close();
}}
type="normal"
iconType="close"
/>
</TitleBar>
<PreferencesCanvas preferences={prefs} />
</div>
);
});

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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 { SearchOptionsState } from './search_options_state';
import { NotesState } from './notes_state';
import { TagsState } from './tags_state';
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
import { PreferencesState } from './preferences_state';
export enum AppStateEvent {
TagChanged,
@@ -47,8 +48,8 @@ export enum EventSource {
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>;
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: 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
@@ -132,6 +131,7 @@ export class AppState {
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
preferences: observable,
enableBetaWarning: action,
disableBetaWarning: action,

View File

@@ -0,0 +1,26 @@
import { action, computed, makeObservable, observable } from 'mobx';
export class PreferencesState {
private _open = false;
constructor() {
makeObservable<PreferencesState, '_open'>(this, {
_open: observable,
openPreferences: action,
closePreferences: action,
isOpen: computed,
});
}
openPreferences = (): void => {
this._open = true;
};
closePreferences = (): void => {
this._open = false;
};
get isOpen() {
return this._open;
}
}

View File

@@ -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'
)

View File

@@ -863,7 +863,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
.io
.addKeyObserver({
key: KeyboardKey.Backspace,
notElementIds: [ElementIds.NoteTextEditor, ElementIds.NoteTitleEditor],
notTags: ['INPUT', 'TEXTAREA'],
modifiers: [KeyboardModifier.Meta],
onKeyDown: () => {
this.deleteNote(false);

View File

@@ -18,6 +18,11 @@
application='ctrl.application'
ng-if='ctrl.showAccountMenu',
)
.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',

View File

@@ -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<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<Record<string, boolean>> = {}
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<Record<string, boolean>> = {};
private observerRemovers: Array<() => void> = [];
private completedInitialSync = false;
private showingDownloadStatus = false;
@@ -117,7 +120,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
this.showAccountMenu = this.appState.accountMenu.show;
this.setState({
showBetaWarning: showBetaWarning,
showDataUpgrade: !showBetaWarning
showDataUpgrade: !showBetaWarning,
});
});
}
@@ -128,7 +131,9 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
/** Enable permanently for this user so they don't lose the feature after its disabled */
localStorage.setItem(ACCOUNT_SWITCHER_FEATURE_KEY, JSON.stringify(true));
}
const hasAccountSwitcher = stringValue ? JSON.parse(stringValue) : ACCOUNT_SWITCHER_ENABLED;
const hasAccountSwitcher = stringValue
? JSON.parse(stringValue)
: ACCOUNT_SWITCHER_ENABLED;
this.setState({ hasAccountSwitcher });
}
@@ -148,7 +153,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
reloadUpgradeStatus() {
this.application.checkForSecurityUpdate().then((available) => {
this.setState({
dataUpgradeAvailable: available
dataUpgradeAvailable: available,
});
});
}
@@ -176,19 +181,25 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
async reloadPasscodeStatus() {
const hasPasscode = this.application.hasPasscode();
this.setState({
hasPasscode: hasPasscode
hasPasscode: hasPasscode,
});
}
addRootScopeListeners() {
this.rootScopeListener1 = this.$rootScope.$on(RootScopeMessages.ReloadExtendedData, () => {
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<unknown, {
}
break;
case AppStateEvent.BeganBackupDownload:
statusService.setMessage("Saving local backup…");
statusService.setMessage('Saving local backup…');
break;
case AppStateEvent.EndedBackupDownload: {
const successMessage = "Successfully saved backup.";
const errorMessage = "Unable to save local backup.";
const successMessage = 'Successfully saved backup.';
const errorMessage = 'Unable to save local backup.';
statusService.setMessage(data.success ? successMessage : errorMessage);
const twoSeconds = 2000;
@@ -237,12 +248,12 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
break;
case ApplicationEvent.EnteredOutOfSync:
this.setState({
outOfSync: true
outOfSync: true,
});
break;
case ApplicationEvent.ExitedOutOfSync:
this.setState({
outOfSync: false
outOfSync: false,
});
break;
case ApplicationEvent.CompletedFullSync:
@@ -290,17 +301,15 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
CollectionSort.Title,
'asc',
(theme: SNTheme) => {
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<unknown, {
this.queueExtReload = false;
this.reloadExtendedData();
}
}
));
})
);
this.observerRemovers.push(this.application.streamItems(
ContentType.Theme,
async () => {
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<unknown, {
statusManager.setMessage('');
}, 2000);
} else if (stats.uploadTotalCount > 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<unknown, {
* then closing it after a short delay.
*/
const extWindow = this.rooms.find((room) => {
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<unknown, {
}
async openSecurityUpdate() {
if (await confirmDialog({
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE,
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
})) {
if (
await confirmDialog({
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE,
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
})
) {
preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => {
await this.application.upgradeProtocolVersion();
});
@@ -453,25 +471,27 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
refreshData() {
this.isRefreshing = true;
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();
}
});
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 PureViewCtrl<unknown, {
clickedNewUpdateAnnouncement() {
this.newUpdateAvailable = false;
this.application.alertService!.alert(
STRING_NEW_UPDATE_READY
);
this.application.alertService!.alert(STRING_NEW_UPDATE_READY);
}
reloadDockShortcuts() {
@@ -499,7 +517,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
shortcuts.push({
name: name,
component: theme,
icon: icon
icon: icon,
} as DockShortcut);
}
this.setState({
@@ -514,7 +532,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
} else {
return a.name.localeCompare(b.name);
}
})
}),
});
}
@@ -553,7 +571,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
text:
'If you wish to go back to a stable version, make sure to sign out ' +
'of this beta app first.<br>You can silence this warning from the ' +
'<em>Account</em> menu.'
'<em>Account</em> menu.',
});
}
@@ -563,6 +581,10 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
}
this.appState.accountMenu.setShow(false);
}
clickPreferences() {
this.appState.preferences.openPreferences();
}
}
export class FooterView extends WebDirective {
@@ -575,7 +597,7 @@ export class FooterView extends WebDirective {
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
application: '='
application: '=',
};
}
}