feat: display warning banner when using the app with no account
This commit is contained in:
3
app/assets/javascripts/@types/modules.ts
Normal file
3
app/assets/javascripts/@types/modules.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module '*.svg' {
|
||||
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element;
|
||||
}
|
||||
@@ -55,7 +55,8 @@ import { BrowserBridge } from './services/browserBridge';
|
||||
import { startErrorReporting } from './services/errorReporting';
|
||||
import { StartApplication } from './startApplication';
|
||||
import { Bridge } from './services/bridge';
|
||||
import { SessionsModalDirective } from './directives/views/sessionsModal';
|
||||
import { SessionsModalDirective } from './components/SessionsModal';
|
||||
import { NoAccountWarningDirective } from './components/NoAccountWarning';
|
||||
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
@@ -141,7 +142,8 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
|
||||
.directive('historyMenu', () => new HistoryMenu())
|
||||
.directive('syncResolutionMenu', () => new SyncResolutionMenu())
|
||||
.directive('sessionsModal', SessionsModalDirective);
|
||||
.directive('sessionsModal', SessionsModalDirective)
|
||||
.directive('noAccountWarning', NoAccountWarningDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
|
||||
46
app/assets/javascripts/components/NoAccountWarning.tsx
Normal file
46
app/assets/javascripts/components/NoAccountWarning.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { toDirective, useAutorunValue } from './utils';
|
||||
import Close from '../../icons/ic_close.svg';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
function NoAccountWarning({
|
||||
application,
|
||||
appState,
|
||||
}: {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}) {
|
||||
const canShow = useAutorunValue(() => appState.noAccountWarning.show);
|
||||
if (!canShow || application.hasAccount()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="mt-5 p-5 rounded-md shadow-sm grid grid-template-cols-1fr">
|
||||
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
|
||||
<p className="m-0 mt-1 col-start-1 col-end-3">
|
||||
Sign in or register to back up your notes
|
||||
</p>
|
||||
<button
|
||||
className="sn-btn mt-3 col-start-1 col-end-3 justify-self-start"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
appState.accountMenu.setShow(true);
|
||||
}}
|
||||
>
|
||||
Open Account menu
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
appState.noAccountWarning.hide();
|
||||
}}
|
||||
title="Ignore"
|
||||
label="Ignore"
|
||||
className="border-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1"
|
||||
>
|
||||
<Close className="fill-neutral hover:fill-info" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const NoAccountWarningDirective = toDirective(NoAccountWarning);
|
||||
@@ -1,14 +1,12 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PureViewCtrl } from '@/views';
|
||||
import {
|
||||
SNApplication,
|
||||
RemoteSession,
|
||||
SessionStrings,
|
||||
UuidString,
|
||||
isNullOrUndefined,
|
||||
RemoteSession,
|
||||
} from '@standardnotes/snjs';
|
||||
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
|
||||
import { render, FunctionComponent } from 'preact';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
||||
import { Dialog } from '@reach/dialog';
|
||||
import { Alert } from '@reach/alert';
|
||||
@@ -17,13 +15,8 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
|
||||
function useAutorun(
|
||||
view: (r: IReactionPublic) => unknown,
|
||||
opts?: IAutorunOptions
|
||||
) {
|
||||
useEffect(() => autorun(view, opts), [view, opts]);
|
||||
}
|
||||
import { toDirective, useAutorun } from './utils';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Session = RemoteSession & {
|
||||
revoking?: true;
|
||||
@@ -250,7 +243,7 @@ const SessionsModal: FunctionComponent<{
|
||||
|
||||
const Sessions: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: SNApplication;
|
||||
application: WebApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
|
||||
@@ -262,27 +255,4 @@ const Sessions: FunctionComponent<{
|
||||
}
|
||||
};
|
||||
|
||||
class SessionsModalCtrl extends PureViewCtrl<unknown, unknown> {
|
||||
/* @ngInject */
|
||||
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
}
|
||||
$onChanges() {
|
||||
render(
|
||||
<Sessions appState={this.appState} application={this.application} />,
|
||||
this.$element[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function SessionsModalDirective() {
|
||||
return {
|
||||
controller: SessionsModalCtrl,
|
||||
bindToController: true,
|
||||
scope: {
|
||||
application: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
export const SessionsModalDirective = toDirective(Sessions);
|
||||
56
app/assets/javascripts/components/utils.ts
Normal file
56
app/assets/javascripts/components/utils.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
|
||||
import { FunctionComponent, h, render } from 'preact';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useAutorunValue<T>(query: () => T): T {
|
||||
const [value, setValue] = useState(query);
|
||||
useAutorun(() => {
|
||||
setValue(query());
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useAutorun(
|
||||
view: (r: IReactionPublic) => unknown,
|
||||
opts?: IAutorunOptions
|
||||
): void {
|
||||
useEffect(() => autorun(view, opts), [view, opts]);
|
||||
}
|
||||
|
||||
export function toDirective(
|
||||
component: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}>
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
return function () {
|
||||
return {
|
||||
controller: [
|
||||
'$element',
|
||||
'$scope',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
($element: JQLite, $scope: any) => {
|
||||
return {
|
||||
$onChanges() {
|
||||
render(
|
||||
h(component, {
|
||||
application: $scope.application,
|
||||
appState: $scope.appState,
|
||||
}),
|
||||
$element[0]
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
scope: {
|
||||
application: '=',
|
||||
appState: '=',
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export class DesktopManager extends ApplicationService {
|
||||
|
||||
getExtServerHost() {
|
||||
console.assert(
|
||||
this.bridge.extensionsServerHost,
|
||||
!!this.bridge.extensionsServerHost,
|
||||
'extServerHost is null'
|
||||
);
|
||||
return this.bridge.extensionsServerHost;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
export enum StorageKey {
|
||||
DisableErrorReporting = 'DisableErrorReporting',
|
||||
AnonymousUserId = 'AnonymousUserId',
|
||||
ShowBetaWarning = 'ShowBetaWarning',
|
||||
ShowNoAccountWarning = 'ShowNoAccountWarning',
|
||||
}
|
||||
|
||||
export type StorageValue = {
|
||||
[StorageKey.DisableErrorReporting]: boolean;
|
||||
[StorageKey.AnonymousUserId]: string;
|
||||
[StorageKey.ShowBetaWarning]: boolean;
|
||||
[StorageKey.ShowNoAccountWarning]: boolean;
|
||||
}
|
||||
|
||||
export const storage = {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"isolatedModules": false,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"newLine": "lf",
|
||||
@@ -14,6 +14,7 @@
|
||||
"baseUrl": ".",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"typeRoots": ["./@types"],
|
||||
"paths": {
|
||||
"%/*": ["../templates/*"],
|
||||
"@/*": ["./*"],
|
||||
|
||||
@@ -15,6 +15,7 @@ import { WebApplication } from '@/ui_models/application';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { storage, StorageKey } from '@/services/localStorage';
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -30,7 +31,7 @@ export enum AppStateEvent {
|
||||
export type PanelResizedData = {
|
||||
panel: string;
|
||||
collapsed: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export enum EventSource {
|
||||
UserInteraction,
|
||||
@@ -39,8 +40,6 @@ export enum EventSource {
|
||||
|
||||
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>;
|
||||
|
||||
const SHOW_BETA_WARNING_KEY = 'show_beta_warning';
|
||||
|
||||
class ActionsMenuState {
|
||||
hiddenExtensions: Record<UuidString, boolean> = {};
|
||||
|
||||
@@ -75,7 +74,7 @@ export class SyncState {
|
||||
});
|
||||
}
|
||||
|
||||
update(status: SyncOpStatus) {
|
||||
update(status: SyncOpStatus): void {
|
||||
this.errorMessage = status.error?.message;
|
||||
this.inProgress = status.syncInProgress;
|
||||
const stats = status.getStats();
|
||||
@@ -95,6 +94,41 @@ export class SyncState {
|
||||
}
|
||||
}
|
||||
|
||||
class AccountMenuState {
|
||||
show = false;
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
show: observable,
|
||||
setShow: action,
|
||||
toggleShow: action,
|
||||
});
|
||||
}
|
||||
setShow(show: boolean) {
|
||||
this.show = show;
|
||||
}
|
||||
toggleShow() {
|
||||
this.show = !this.show;
|
||||
}
|
||||
}
|
||||
|
||||
class NoAccountWarningState {
|
||||
show: boolean;
|
||||
constructor() {
|
||||
this.show = storage.get(StorageKey.ShowNoAccountWarning) ?? true;
|
||||
makeObservable(this, {
|
||||
show: observable,
|
||||
hide: action,
|
||||
});
|
||||
}
|
||||
hide() {
|
||||
this.show = false;
|
||||
storage.set(StorageKey.ShowNoAccountWarning, false);
|
||||
}
|
||||
reset() {
|
||||
storage.remove(StorageKey.ShowNoAccountWarning);
|
||||
}
|
||||
}
|
||||
|
||||
export class AppState {
|
||||
readonly enableUnfinishedFeatures =
|
||||
isDev || location.host.includes('app-dev.standardnotes.org');
|
||||
@@ -109,8 +143,10 @@ export class AppState {
|
||||
rootScopeCleanup2: any;
|
||||
onVisibilityChange: any;
|
||||
selectedTag?: SNTag;
|
||||
showBetaWarning = false;
|
||||
showBetaWarning: boolean;
|
||||
readonly accountMenu = new AccountMenuState();
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly noAccountWarning = new NoAccountWarningState();
|
||||
readonly sync = new SyncState();
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
@@ -124,15 +160,6 @@ export class AppState {
|
||||
this.$timeout = $timeout;
|
||||
this.$rootScope = $rootScope;
|
||||
this.application = application;
|
||||
makeObservable(this, {
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
|
||||
enableBetaWarning: action,
|
||||
disableBetaWarning: action,
|
||||
openSessionsModal: action,
|
||||
closeSessionsModal: action,
|
||||
});
|
||||
this.addAppEventObserver();
|
||||
this.streamNotesAndTags();
|
||||
this.onVisibilityChange = () => {
|
||||
@@ -143,12 +170,28 @@ export class AppState {
|
||||
this.notifyEvent(event);
|
||||
};
|
||||
this.registerVisibilityObservers();
|
||||
this.determineBetaWarningValue();
|
||||
|
||||
if (this.bridge.appVersion.includes('-beta')) {
|
||||
this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true;
|
||||
} else {
|
||||
this.showBetaWarning = false;
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
|
||||
enableBetaWarning: action,
|
||||
disableBetaWarning: action,
|
||||
openSessionsModal: action,
|
||||
closeSessionsModal: action,
|
||||
});
|
||||
}
|
||||
|
||||
deinit(source: DeinitSource) {
|
||||
deinit(source: DeinitSource): void {
|
||||
if (source === DeinitSource.SignOut) {
|
||||
localStorage.removeItem(SHOW_BETA_WARNING_KEY);
|
||||
storage.remove(StorageKey.ShowBetaWarning);
|
||||
this.noAccountWarning.reset();
|
||||
}
|
||||
this.actionsMenu.deinit();
|
||||
this.unsubApp();
|
||||
@@ -174,30 +217,12 @@ export class AppState {
|
||||
|
||||
disableBetaWarning() {
|
||||
this.showBetaWarning = false;
|
||||
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'false');
|
||||
storage.set(StorageKey.ShowBetaWarning, false);
|
||||
}
|
||||
|
||||
enableBetaWarning() {
|
||||
this.showBetaWarning = true;
|
||||
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'true');
|
||||
}
|
||||
|
||||
clearBetaWarning() {
|
||||
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'true');
|
||||
}
|
||||
|
||||
private determineBetaWarningValue() {
|
||||
if (this.bridge.appVersion.includes('-beta')) {
|
||||
switch (localStorage.getItem(SHOW_BETA_WARNING_KEY)) {
|
||||
case 'true':
|
||||
default:
|
||||
this.enableBetaWarning();
|
||||
break;
|
||||
case 'false':
|
||||
this.disableBetaWarning();
|
||||
break;
|
||||
}
|
||||
}
|
||||
storage.set(StorageKey.ShowBetaWarning, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
declare const process : {
|
||||
env: {
|
||||
NODE_ENV: string | null | undefined
|
||||
}
|
||||
};
|
||||
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export function getPlatformString() {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z")
|
||||
sessions-modal(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
challenge-modal(
|
||||
ng-repeat="challenge in self.challenges track by challenge.id"
|
||||
|
||||
@@ -62,7 +62,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
public arbitraryStatusMessage?: string
|
||||
public user?: any
|
||||
private offline = true
|
||||
private showAccountMenu = false
|
||||
public showAccountMenu = false
|
||||
private didCheckForOffline = false
|
||||
private queueExtReload = false
|
||||
private reloadInProgress = false
|
||||
@@ -75,7 +75,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
private observerRemovers: Array<() => void> = [];
|
||||
private completedInitialSync = false;
|
||||
private showingDownloadStatus = false;
|
||||
private removeBetaWarningListener?: IReactionDisposer;
|
||||
private autorunDisposer?: IReactionDisposer;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -103,7 +103,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
this.rootScopeListener2 = undefined;
|
||||
(this.closeAccountMenu as any) = undefined;
|
||||
(this.toggleSyncResolutionMenu as any) = undefined;
|
||||
this.removeBetaWarningListener?.();
|
||||
this.autorunDisposer?.();
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -115,8 +115,9 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
});
|
||||
});
|
||||
this.loadAccountSwitcherState();
|
||||
this.removeBetaWarningListener = autorun(() => {
|
||||
this.autorunDisposer = autorun(() => {
|
||||
const showBetaWarning = this.appState.showBetaWarning;
|
||||
this.showAccountMenu = this.appState.accountMenu.show;
|
||||
this.setState({
|
||||
showBetaWarning: showBetaWarning,
|
||||
showDataUpgrade: !showBetaWarning
|
||||
@@ -255,7 +256,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
if (!this.didCheckForOffline) {
|
||||
this.didCheckForOffline = true;
|
||||
if (this.offline && this.application.getNoteCount() === 0) {
|
||||
this.showAccountMenu = true;
|
||||
this.appState.accountMenu.setShow(true);
|
||||
}
|
||||
}
|
||||
this.syncUpdated();
|
||||
@@ -437,7 +438,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
}
|
||||
|
||||
accountMenuPressed() {
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.appState.accountMenu.toggleShow();
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
@@ -446,7 +447,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
}
|
||||
|
||||
closeAccountMenu() {
|
||||
this.showAccountMenu = false;
|
||||
this.appState.accountMenu.setShow(false);
|
||||
}
|
||||
|
||||
lockApp() {
|
||||
@@ -563,7 +564,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
if (this.application && this.application.authenticationInProgress()) {
|
||||
return;
|
||||
}
|
||||
this.showAccountMenu = false;
|
||||
this.appState.accountMenu.setShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#notes-title-bar.section-title-bar
|
||||
.padded
|
||||
.section-title-bar-header
|
||||
.title {{self.state.panelTitle}}
|
||||
.sk-h2.font-semibold.title {{self.state.panelTitle}}
|
||||
.sk-button.contrast.wide(
|
||||
ng-click='self.createNewNote()',
|
||||
title='Create a new note in the selected tag'
|
||||
@@ -24,6 +24,10 @@
|
||||
ng-click='self.clearFilterText();',
|
||||
ng-show='self.state.noteFilter.text'
|
||||
) ✕
|
||||
no-account-warning(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
#notes-menu-bar.sn-component
|
||||
.sk-app-bar.no-edges
|
||||
.left
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
.tag-info
|
||||
.title(ng-if="!tag.errorDecrypting") {{tag.title}}
|
||||
.count(ng-show='tag.isAllTag') {{self.state.noteCounts[tag.uuid]}}
|
||||
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.tags-title-section.section-title-bar
|
||||
.section-title-bar-header
|
||||
.sk-h3.title
|
||||
@@ -52,9 +52,9 @@
|
||||
spellcheck='false'
|
||||
)
|
||||
.count {{self.state.noteCounts[tag.uuid]}}
|
||||
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.menu(ng-show='self.state.selectedTag == tag')
|
||||
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
|
||||
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
|
||||
|
||||
Reference in New Issue
Block a user