feat: display warning banner when using the app with no account

This commit is contained in:
Baptiste Grob
2021-02-02 15:40:20 +01:00
parent 94473da61b
commit c084268f51
26 changed files with 709 additions and 197 deletions

View File

@@ -0,0 +1,3 @@
declare module '*.svg' {
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element;
}

View File

@@ -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]);

View 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);

View File

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

View 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: '=',
},
};
};
}

View File

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

View File

@@ -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 = {

View File

@@ -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/*"],
"@/*": ["./*"],

View File

@@ -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);
}
/**

View File

@@ -1,3 +1,9 @@
declare const process : {
env: {
NODE_ENV: string | null | undefined
}
};
export const isDev = process.env.NODE_ENV === 'development';
export function getPlatformString() {

View File

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

View File

@@ -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);
}
}

View File

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

View File

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