Merge branch 'release/3.5.12'

This commit is contained in:
Baptiste Grob
2020-12-29 12:58:15 +01:00
37 changed files with 8862 additions and 13528 deletions

View File

@@ -1,10 +1,13 @@
{ {
"presets": [ "presets": [
"@babel/typescript", "@babel/preset-typescript",
"@babel/preset-env" "@babel/preset-env"
], ],
"plugins": [ "plugins": [
"@babel/plugin-proposal-class-properties", "angularjs-annotate",
"angularjs-annotate" ["@babel/plugin-transform-react-jsx", {
"pragma": "h",
"pragmaFrag": "Fragment"
}]
] ]
} }

View File

@@ -17,10 +17,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: npm ci run: yarn install --pure-lockfile
- name: Typescript - name: Typescript
run: npm run tsc run: yarn tsc
deploy: deploy:

View File

@@ -17,10 +17,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: npm ci run: yarn install --pure-lockfile
- name: Typescript - name: Typescript
run: npm run tsc run: yarn tsc
deploy: deploy:

View File

@@ -13,6 +13,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: npm ci run: yarn install --pure-lockfile
- name: Typescript - name: Typescript
run: npm run tsc run: yarn tsc

View File

@@ -18,10 +18,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: npm ci run: yarn install --pure-lockfile
- name: Typescript - name: Typescript
run: npm run tsc run: yarn tsc
deploy: deploy:

4
.gitignore vendored
View File

@@ -42,3 +42,7 @@ dump.rdb
/dist/stylesheets /dist/stylesheets
/dist/fonts /dist/fonts
/dist/@types /dist/@types
# Yarn
yarn-error.log
package-lock.json

View File

@@ -1,4 +1,4 @@
FROM ruby:2.7.1-alpine FROM ruby:2.7.1-alpine3.12
ARG UID=1000 ARG UID=1000
ARG GID=1000 ARG GID=1000
@@ -7,10 +7,11 @@ RUN addgroup -S webapp -g $GID && adduser -D -S webapp -G webapp -u $UID
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
alpine-sdk \ alpine-sdk \
nodejs \ nodejs-current \
python2 \ python2 \
git \ git \
nodejs-npm \ nodejs-npm \
yarn \
tzdata tzdata
WORKDIR /app/ WORKDIR /app/
@@ -19,17 +20,17 @@ RUN chown -R $UID:$GID .
USER webapp USER webapp
COPY --chown=$UID:$GID package.json package-lock.json Gemfile Gemfile.lock /app/ COPY --chown=$UID:$GID package.json yarn.lock Gemfile Gemfile.lock /app/
COPY --chown=$UID:$GID vendor /app/vendor COPY --chown=$UID:$GID vendor /app/vendor
RUN npm ci RUN yarn install --pure-lockfile
RUN gem install bundler && bundle install RUN gem install bundler && bundle install
COPY --chown=$UID:$GID . /app/ COPY --chown=$UID:$GID . /app/
RUN npm run bundle RUN yarn bundle
RUN bundle exec rails assets:precompile RUN bundle exec rails assets:precompile

View File

@@ -88,9 +88,10 @@ This repo contains the core code used in the web app, as well as the Electron-ba
**Instructions:** **Instructions:**
1. Ensure you have [Yarn](https://classic.yarnpkg.com) installed
1. Clone the repo 1. Clone the repo
1. `npm run setup` 1. `yarn setup`
1. `npm start` 1. `yarn start`
Then open your browser to `http://localhost:3001`. Then open your browser to `http://localhost:3001`.

View File

@@ -55,9 +55,9 @@ import { trusted } from './filters';
import { isDev } from './utils'; import { isDev } from './utils';
import { BrowserBridge } from './services/browserBridge'; import { BrowserBridge } from './services/browserBridge';
import { startErrorReporting } from './services/errorReporting'; import { startErrorReporting } from './services/errorReporting';
import { alertDialog } from './services/alertService';
import { StartApplication } from './startApplication'; import { StartApplication } from './startApplication';
import { Bridge } from './services/bridge'; import { Bridge } from './services/bridge';
import { SessionsModalDirective } from './directives/views/sessionsModal';
const startApplication: StartApplication = async function startApplication( const startApplication: StartApplication = async function startApplication(
defaultSyncServerHost: string, defaultSyncServerHost: string,
@@ -122,7 +122,8 @@ const startApplication: StartApplication = async function startApplication(
) )
.directive('revisionPreviewModal', () => new RevisionPreviewModal()) .directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('historyMenu', () => new HistoryMenu()) .directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu()); .directive('syncResolutionMenu', () => new SyncResolutionMenu())
.directive('sessionsModal', SessionsModalDirective);
// Filters // Filters
angular.module('app').filter('trusted', ['$sce', trusted]); angular.module('app').filter('trusted', ['$sce', trusted]);

View File

@@ -69,6 +69,7 @@ type AccountMenuState = {
syncInProgress: boolean; syncInProgress: boolean;
syncError: string; syncError: string;
syncPercentage: string; syncPercentage: string;
showSessions: boolean;
} }
class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
@@ -101,6 +102,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
mutable: {}, mutable: {},
showBetaWarning: false, showBetaWarning: false,
errorReportingEnabled: !storage.get(StorageKey.DisableErrorReporting), errorReportingEnabled: !storage.get(StorageKey.DisableErrorReporting),
showSessions: false,
} as AccountMenuState; } as AccountMenuState;
} }
@@ -130,8 +132,12 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
}; };
} }
$onInit() { async $onInit() {
super.$onInit(); super.$onInit();
this.setState({
showSessions: this.appState.enableUnfinishedFeatures && await this.application.userCanManageSessions()
});
const sync = this.appState.sync; const sync = this.appState.sync;
this.removeSyncObserver = autorun(() => { this.removeSyncObserver = autorun(() => {
this.setState({ this.setState({
@@ -320,6 +326,11 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword); this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
} }
openSessionsModal() {
this.close();
this.appState.openSessionsModal();
}
async openPrivilegesModal() { async openPrivilegesModal() {
const run = () => { const run = () => {
this.application!.presentPrivilegesManagementModal(); this.application!.presentPrivilegesManagementModal();

View File

@@ -0,0 +1,252 @@
import { AppState } from '@/ui_models/app_state';
import { PureViewCtrl } from '@/views';
import { SNApplication, RemoteSession, UuidString } from '@standardnotes/snjs';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { render, FunctionComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import { Dialog } from '@reach/dialog';
import { Alert } from '@reach/alert';
import {
AlertDialog,
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';
function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
useEffect(() => autorun(view, opts), []);
}
function useSessions(
application: SNApplication
): [
RemoteSession[],
() => void,
boolean,
(uuid: UuidString) => Promise<void>,
string
] {
const [sessions, setSessions] = useState<RemoteSession[]>([]);
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
const [refreshing, setRefreshing] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
(async () => {
setRefreshing(true);
const response = await application.getSessions();
if ('error' in response) {
if (response.error?.message) {
setErrorMessage(response.error.message);
} else {
setErrorMessage('An unknown error occured while loading sessions.');
}
} else {
const sessions = response as RemoteSession[];
setSessions(sessions);
setErrorMessage('');
}
setRefreshing(false);
})();
}, [lastRefreshDate]);
function refresh() {
setLastRefreshDate(Date.now());
}
async function revokeSession(uuid: UuidString) {
let sessionsBeforeRevoke = sessions;
setSessions(sessions.filter((session) => session.uuid !== uuid));
const response = await application.revokeSession(uuid);
if ('error' in response) {
if (response.error?.message) {
setErrorMessage(response.error?.message);
} else {
setErrorMessage('An unknown error occured while revoking the session.');
}
setSessions(sessionsBeforeRevoke);
}
}
return [sessions, refresh, refreshing, revokeSession, errorMessage];
}
const SessionsModal: FunctionComponent<{
appState: AppState;
application: SNApplication;
}> = ({ appState, application }) => {
const close = () => appState.closeSessionsModal();
const [
sessions,
refresh,
refreshing,
revokeSession,
errorMessage,
] = useSessions(application);
const [revokingSessionUuid, setRevokingSessionUuid] = useState('');
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
const cancelRevokeRef = useRef<HTMLButtonElement>();
const formatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
});
return (
<>
<Dialog onDismiss={close}>
<div className="sk-modal-content sessions-modal">
<div class="sn-component">
<div class="sk-panel">
<div class="sk-panel-header">
<div class="sk-panel-header-title">Active Sessions</div>
<div className="buttons">
<button
class="sk-a close-button info"
disabled={refreshing}
onClick={refresh}
>
Refresh
</button>
<button class="sk-a close-button info" onClick={close}>
Close
</button>
</div>
</div>
<div class="sk-panel-content">
{refreshing ? (
<>
<div class="sk-spinner small info"></div>
<h2 className="sk-p sessions-modal-refreshing">
Loading sessions
</h2>
</>
) : (
<>
{errorMessage && (
<Alert className="sk-p bold">{errorMessage}</Alert>
)}
{sessions.length > 0 && (
<ul>
{sessions.map((session) => (
<li>
<h2>{session.device_info}</h2>
{session.current ? (
<span className="info bold">Current session</span>
) : (
<>
<p>
Signed in on{' '}
{formatter.format(session.updated_at)}
</p>
<button
className="sk-button danger sk-label"
onClick={() =>
setRevokingSessionUuid(session.uuid)
}
>
<span>Revoke</span>
</button>
</>
)}
</li>
))}
</ul>
)}
</>
)}
</div>
</div>
</div>
</div>
</Dialog>
{revokingSessionUuid && (
<AlertDialog leastDestructiveRef={cancelRevokeRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-content">
<div className="sk-panel-section">
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
Revoke this session?
</AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row">
<p>
The associated app will be signed out and all data
removed from the device when it is next launched. You
can sign back in on that device at any time.
</p>
</AlertDialogDescription>
<div className="sk-panel-row">
<div className="sk-button-group">
<button
className="sk-button neutral sk-label"
ref={cancelRevokeRef}
onClick={closeRevokeSessionAlert}
>
<span>Cancel</span>
</button>
<button
className="sk-button danger sk-label"
onClick={() => {
closeRevokeSessionAlert();
revokeSession(revokingSessionUuid);
}}
>
<span>Revoke</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AlertDialog>
)}
</>
);
};
const Sessions: FunctionComponent<{
appState: AppState;
application: SNApplication;
}> = ({ appState, application }) => {
const [showModal, setShowModal] = useState(false);
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
if (showModal) {
return <SessionsModal application={application} appState={appState} />;
} else {
return null;
}
};
class SessionsModalCtrl extends PureViewCtrl<{}, {}> {
/* @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]
);
}
}
export function SessionsModalDirective() {
return {
controller: SessionsModalCtrl,
bindToController: true,
scope: {
application: '=',
},
};
}

View File

@@ -1,8 +1,10 @@
//= require_tree ./app //= require_tree ./app
// css // css
import '@reach/dialog/styles.css';
import 'sn-stylekit/dist/stylekit.css'; import 'sn-stylekit/dist/stylekit.css';
import '../stylesheets/index.css.scss'; import '../stylesheets/index.css.scss';
// import '../stylesheets/_reach-sub.scss';
// Vendor // Vendor
import 'angular'; import 'angular';

View File

@@ -87,7 +87,7 @@ export class ArchiveManager {
scriptTag.async = false; scriptTag.async = false;
const headTag = document.getElementsByTagName('head')[0]; const headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag); headTag.appendChild(scriptTag);
return new Promise((resolve) => { return new Promise<void>((resolve) => {
scriptTag.onload = () => { scriptTag.onload = () => {
this.zip.workerScriptsPath = 'assets/zip/'; this.zip.workerScriptsPath = 'assets/zip/';
resolve(); resolve();

View File

@@ -4,6 +4,5 @@ export { DesktopManager } from './desktopManager';
export { KeyboardManager } from './keyboardManager'; export { KeyboardManager } from './keyboardManager';
export { AutolockService } from './autolock_service'; export { AutolockService } from './autolock_service';
export { NativeExtManager } from './nativeExtManager'; export { NativeExtManager } from './nativeExtManager';
export { PreferencesManager } from './preferencesManager';
export { StatusManager } from './statusManager'; export { StatusManager } from './statusManager';
export { ThemeManager } from './themeManager'; export { ThemeManager } from './themeManager';

View File

@@ -2,12 +2,16 @@ export enum StorageKey {
DisableErrorReporting = 'DisableErrorReporting', DisableErrorReporting = 'DisableErrorReporting',
} }
export type StorageValue = {
[StorageKey.DisableErrorReporting]: boolean;
}
export const storage = { export const storage = {
get(key: StorageKey) { get<K extends StorageKey>(key: K): StorageValue[K] | null {
const value = localStorage.getItem(key); const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null; return value ? JSON.parse(value) : null;
}, },
set(key: StorageKey, value: unknown) { set<K extends StorageKey>(key: K, value: StorageValue[K]) {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
}, },
remove(key: StorageKey) { remove(key: StorageKey) {

View File

@@ -1,99 +0,0 @@
import { WebApplication } from '@/ui_models/application';
import {
SNPredicate,
ContentType,
ApplicationService,
SNUserPrefs,
WebPrefKey,
UserPrefsMutator,
FillItemContent,
ApplicationEvent,
} from '@standardnotes/snjs';
export class PreferencesManager extends ApplicationService {
private userPreferences!: SNUserPrefs;
private loadingPrefs = false;
private unubscribeStreamItems?: () => void;
private needsSingletonReload = true;
/** @override */
async onAppLaunch() {
super.onAppLaunch();
this.reloadSingleton();
this.streamPreferences();
}
async onAppEvent(event: ApplicationEvent) {
super.onAppEvent(event);
if (event === ApplicationEvent.CompletedFullSync) {
this.reloadSingleton();
}
}
deinit() {
this.unubscribeStreamItems?.();
}
get webApplication() {
return this.application as WebApplication;
}
streamPreferences() {
this.unubscribeStreamItems = this.application!.streamItems(
ContentType.UserPrefs,
() => {
this.needsSingletonReload = true;
}
);
}
private async reloadSingleton() {
if (this.loadingPrefs || !this.needsSingletonReload) {
return;
}
this.loadingPrefs = true;
const contentType = ContentType.UserPrefs;
const predicate = new SNPredicate('content_type', '=', contentType);
const previousRef = this.userPreferences;
this.userPreferences = (await this.application!.singletonManager!.findOrCreateSingleton(
predicate,
contentType,
FillItemContent({})
)) as SNUserPrefs;
this.loadingPrefs = false;
this.needsSingletonReload = false;
if (
previousRef?.uuid !== this.userPreferences.uuid ||
this.userPreferences.lastSyncBegan?.getTime() !==
previousRef?.lastSyncBegan?.getTime()
) {
this.webApplication
.getAppState()
.setUserPreferences(this.userPreferences);
}
}
syncUserPreferences() {
if (this.userPreferences) {
this.application!.saveItem(this.userPreferences.uuid);
}
}
getValue(key: WebPrefKey, defaultValue?: any) {
if (!this.userPreferences) {
return defaultValue;
}
const value = this.userPreferences.getPref(key);
return value !== undefined && value !== null ? value : defaultValue;
}
async setUserPrefValue(key: WebPrefKey, value: any, sync = false) {
await this.application!.changeItem(this.userPreferences.uuid, (m) => {
const mutator = m as UserPrefsMutator;
mutator.setWebPref(key, value);
});
if (sync) {
this.syncUserPreferences();
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "ES2019",
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"allowJs": true, "allowJs": true,
@@ -12,6 +12,8 @@
"newLine": "lf", "newLine": "lf",
"declarationDir": "../../../dist/@types", "declarationDir": "../../../dist/@types",
"baseUrl": ".", "baseUrl": ".",
"jsx": "react-jsx",
"jsxImportSource": "preact",
"paths": { "paths": {
"%/*": ["../templates/*"], "%/*": ["../templates/*"],
"@/*": ["./*"], "@/*": ["./*"],

View File

@@ -1,4 +1,4 @@
import { isDesktopApplication } from '@/utils'; import { isDesktopApplication, isDev } from '@/utils';
import pull from 'lodash/pull'; import pull from 'lodash/pull';
import { import {
ProtectedAction, ProtectedAction,
@@ -11,7 +11,7 @@ import {
PayloadSource, PayloadSource,
DeinitSource, DeinitSource,
UuidString, UuidString,
SyncOpStatus SyncOpStatus,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { Editor } from '@/ui_models/editor'; import { Editor } from '@/ui_models/editor';
@@ -19,23 +19,22 @@ import { action, makeObservable, observable } from 'mobx';
import { Bridge } from '@/services/bridge'; import { Bridge } from '@/services/bridge';
export enum AppStateEvent { export enum AppStateEvent {
TagChanged = 1, TagChanged,
ActiveEditorChanged = 2, ActiveEditorChanged,
PreferencesChanged = 3, PanelResized,
PanelResized = 4, EditorFocused,
EditorFocused = 5, BeganBackupDownload,
BeganBackupDownload = 6, EndedBackupDownload,
EndedBackupDownload = 7, WindowDidFocus,
WindowDidFocus = 9, WindowDidBlur,
WindowDidBlur = 10, }
};
export enum EventSource { export enum EventSource {
UserInteraction = 1, UserInteraction,
Script = 2 Script,
}; }
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void> type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>;
const SHOW_BETA_WARNING_KEY = 'show_beta_warning'; const SHOW_BETA_WARNING_KEY = 'show_beta_warning';
@@ -61,8 +60,8 @@ class ActionsMenuState {
export class SyncState { export class SyncState {
inProgress = false; inProgress = false;
errorMessage?: string; errorMessage?: string = undefined;
humanReadablePercentage?: string; humanReadablePercentage?: string = undefined;
constructor() { constructor() {
makeObservable(this, { makeObservable(this, {
@@ -77,7 +76,8 @@ export class SyncState {
this.errorMessage = status.error?.message; this.errorMessage = status.error?.message;
this.inProgress = status.syncInProgress; this.inProgress = status.syncInProgress;
const stats = status.getStats(); const stats = status.getStats();
const completionPercentage = stats.uploadCompletionCount === 0 const completionPercentage =
stats.uploadCompletionCount === 0
? 0 ? 0
: stats.uploadCompletionCount / stats.uploadTotalCount; : stats.uploadCompletionCount / stats.uploadTotalCount;
@@ -93,6 +93,9 @@ export class SyncState {
} }
export class AppState { export class AppState {
readonly enableUnfinishedFeatures =
isDev || location.host.includes('app-dev.standardnotes.org');
$rootScope: ng.IRootScopeService; $rootScope: ng.IRootScopeService;
$timeout: ng.ITimeoutService; $timeout: ng.ITimeoutService;
application: WebApplication; application: WebApplication;
@@ -103,36 +106,40 @@ export class AppState {
rootScopeCleanup2: any; rootScopeCleanup2: any;
onVisibilityChange: any; onVisibilityChange: any;
selectedTag?: SNTag; selectedTag?: SNTag;
userPreferences?: SNUserPrefs;
multiEditorEnabled = false; multiEditorEnabled = false;
showBetaWarning = false; showBetaWarning = false;
readonly actionsMenu = new ActionsMenuState(); readonly actionsMenu = new ActionsMenuState();
readonly sync = new SyncState(); readonly sync = new SyncState();
isSessionsModalVisible = false;
/* @ngInject */ /* @ngInject */
constructor( constructor(
$rootScope: ng.IRootScopeService, $rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService, $timeout: ng.ITimeoutService,
application: WebApplication, application: WebApplication,
private bridge: Bridge, private bridge: Bridge
) { ) {
this.$timeout = $timeout; this.$timeout = $timeout;
this.$rootScope = $rootScope; this.$rootScope = $rootScope;
this.application = application; this.application = application;
makeObservable(this, { makeObservable(this, {
showBetaWarning: observable, showBetaWarning: observable,
isSessionsModalVisible: observable,
enableBetaWarning: action, enableBetaWarning: action,
disableBetaWarning: action, disableBetaWarning: action,
openSessionsModal: action,
closeSessionsModal: action,
}); });
this.addAppEventObserver(); this.addAppEventObserver();
this.streamNotesAndTags(); this.streamNotesAndTags();
this.onVisibilityChange = () => { this.onVisibilityChange = () => {
const visible = document.visibilityState === "visible"; const visible = document.visibilityState === 'visible';
const event = visible const event = visible
? AppStateEvent.WindowDidFocus ? AppStateEvent.WindowDidFocus
: AppStateEvent.WindowDidBlur; : AppStateEvent.WindowDidBlur;
this.notifyEvent(event); this.notifyEvent(event);
} };
this.registerVisibilityObservers(); this.registerVisibilityObservers();
this.determineBetaWarningValue(); this.determineBetaWarningValue();
} }
@@ -155,6 +162,14 @@ export class AppState {
this.onVisibilityChange = undefined; this.onVisibilityChange = undefined;
} }
openSessionsModal() {
this.isSessionsModalVisible = true;
}
closeSessionsModal() {
this.isSessionsModalVisible = false;
}
disableBetaWarning() { disableBetaWarning() {
this.showBetaWarning = false; this.showBetaWarning = false;
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'false'); localStorage.setItem(SHOW_BETA_WARNING_KEY, 'false');
@@ -190,10 +205,10 @@ export class AppState {
async createEditor(title?: string) { async createEditor(title?: string) {
const activeEditor = this.getActiveEditor(); const activeEditor = this.getActiveEditor();
const activeTagUuid = this.selectedTag const activeTagUuid = this.selectedTag
? this.selectedTag.isSmartTag() ? this.selectedTag.isSmartTag()
? undefined ? undefined
: this.selectedTag.uuid : this.selectedTag.uuid
: undefined; : undefined;
if (!activeEditor || this.multiEditorEnabled) { if (!activeEditor || this.multiEditorEnabled) {
this.application.editorGroup.createEditor( this.application.editorGroup.createEditor(
@@ -218,10 +233,13 @@ export class AppState {
} }
await this.notifyEvent(AppStateEvent.ActiveEditorChanged); await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
}; };
if (note && note.safeContent.protected && if (
await this.application.privilegesService!.actionRequiresPrivilege( note &&
note.safeContent.protected &&
(await this.application.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ViewProtectedNotes ProtectedAction.ViewProtectedNotes
)) { ))
) {
return new Promise((resolve) => { return new Promise((resolve) => {
this.application.presentPrivilegesModal( this.application.presentPrivilegesModal(
ProtectedAction.ViewProtectedNotes, ProtectedAction.ViewProtectedNotes,
@@ -269,8 +287,8 @@ export class AppState {
async (items, source) => { async (items, source) => {
/** Close any editors for deleted/trashed/archived notes */ /** Close any editors for deleted/trashed/archived notes */
if (source === PayloadSource.PreSyncSave) { if (source === PayloadSource.PreSyncSave) {
const notes = items.filter((candidate) => const notes = items.filter(
candidate.content_type === ContentType.Note (candidate) => candidate.content_type === ContentType.Note
) as SNNote[]; ) as SNNote[];
for (const note of notes) { for (const note of notes) {
const editor = this.editorForNote(note); const editor = this.editorForNote(note);
@@ -287,7 +305,9 @@ export class AppState {
} }
} }
if (this.selectedTag) { if (this.selectedTag) {
const matchingTag = items.find((candidate) => candidate.uuid === this.selectedTag!.uuid); const matchingTag = items.find(
(candidate) => candidate.uuid === this.selectedTag!.uuid
);
if (matchingTag) { if (matchingTag) {
this.selectedTag = matchingTag as SNTag; this.selectedTag = matchingTag as SNTag;
} }
@@ -321,9 +341,12 @@ export class AppState {
this.rootScopeCleanup1 = this.$rootScope.$on('window-lost-focus', () => { this.rootScopeCleanup1 = this.$rootScope.$on('window-lost-focus', () => {
this.notifyEvent(AppStateEvent.WindowDidBlur); this.notifyEvent(AppStateEvent.WindowDidBlur);
}); });
this.rootScopeCleanup2 = this.$rootScope.$on('window-gained-focus', () => { this.rootScopeCleanup2 = this.$rootScope.$on(
this.notifyEvent(AppStateEvent.WindowDidFocus); 'window-gained-focus',
}); () => {
this.notifyEvent(AppStateEvent.WindowDidFocus);
}
);
} else { } else {
/* Tab visibility listener, web only */ /* Tab visibility listener, web only */
document.addEventListener('visibilitychange', this.onVisibilityChange); document.addEventListener('visibilitychange', this.onVisibilityChange);
@@ -343,7 +366,7 @@ export class AppState {
* Timeout is particullary important so we can give all initial * Timeout is particullary important so we can give all initial
* controllers a chance to construct before propogting any events * * controllers a chance to construct before propogting any events *
*/ */
return new Promise((resolve) => { return new Promise<void>((resolve) => {
this.$timeout(async () => { this.$timeout(async () => {
for (const callback of this.observers) { for (const callback of this.observers) {
await callback(eventName, data); await callback(eventName, data);
@@ -359,71 +382,39 @@ export class AppState {
} }
const previousTag = this.selectedTag; const previousTag = this.selectedTag;
this.selectedTag = tag; this.selectedTag = tag;
this.notifyEvent( this.notifyEvent(AppStateEvent.TagChanged, {
AppStateEvent.TagChanged, tag: tag,
{ previousTag: previousTag,
tag: tag, });
previousTag: previousTag
}
);
} }
/** Returns the tags that are referncing this note */ /** Returns the tags that are referncing this note */
public getNoteTags(note: SNNote) { public getNoteTags(note: SNNote) {
return this.application.referencingForItem(note).filter((ref) => { return this.application.referencingForItem(note).filter((ref) => {
return ref.content_type === ContentType.Tag; return ref.content_type === ContentType.Tag;
}) as SNTag[] }) as SNTag[];
}
/** Returns the notes this tag references */
public getTagNotes(tag: SNTag) {
if (tag.isSmartTag()) {
return this.application.notesMatchingSmartTag(tag as SNSmartTag);
} else {
return this.application.referencesForItem(tag).filter((ref) => {
return ref.content_type === ContentType.Note;
}) as SNNote[]
}
} }
public getSelectedTag() { public getSelectedTag() {
return this.selectedTag; return this.selectedTag;
} }
setUserPreferences(preferences: SNUserPrefs) {
this.userPreferences = preferences;
this.notifyEvent(
AppStateEvent.PreferencesChanged
);
}
panelDidResize(name: string, collapsed: boolean) { panelDidResize(name: string, collapsed: boolean) {
this.notifyEvent( this.notifyEvent(AppStateEvent.PanelResized, {
AppStateEvent.PanelResized, panel: name,
{ collapsed: collapsed,
panel: name, });
collapsed: collapsed
}
);
} }
editorDidFocus(eventSource: EventSource) { editorDidFocus(eventSource: EventSource) {
this.notifyEvent( this.notifyEvent(AppStateEvent.EditorFocused, { eventSource: eventSource });
AppStateEvent.EditorFocused,
{ eventSource: eventSource }
);
} }
beganBackupDownload() { beganBackupDownload() {
this.notifyEvent( this.notifyEvent(AppStateEvent.BeganBackupDownload);
AppStateEvent.BeganBackupDownload
);
} }
endedBackupDownload(success: boolean) { endedBackupDownload(success: boolean) {
this.notifyEvent( this.notifyEvent(AppStateEvent.EndedBackupDownload, { success: success });
AppStateEvent.EndedBackupDownload,
{ success: success }
);
} }
} }

View File

@@ -22,7 +22,6 @@ import {
NativeExtManager, NativeExtManager,
StatusManager, StatusManager,
ThemeManager, ThemeManager,
PreferencesManager,
KeyboardManager KeyboardManager
} from '@/services'; } from '@/services';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
@@ -38,7 +37,6 @@ type WebServices = {
nativeExtService: NativeExtManager nativeExtService: NativeExtManager
statusManager: StatusManager statusManager: StatusManager
themeService: ThemeManager themeService: ThemeManager
prefsService: PreferencesManager
keyboardService: KeyboardManager keyboardService: KeyboardManager
} }
@@ -141,10 +139,6 @@ export class WebApplication extends SNApplication {
return this.webServices.themeService; return this.webServices.themeService;
} }
public getPrefsService() {
return this.webServices.prefsService;
}
public getKeyboardService() { public getKeyboardService() {
return this.webServices.keyboardService; return this.webServices.keyboardService;
} }

View File

@@ -7,7 +7,6 @@ import {
KeyboardManager, KeyboardManager,
AutolockService, AutolockService,
NativeExtManager, NativeExtManager,
PreferencesManager,
StatusManager, StatusManager,
ThemeManager ThemeManager
} from '@/services'; } from '@/services';
@@ -82,9 +81,6 @@ export class ApplicationGroup extends SNApplicationGroup {
const nativeExtService = new NativeExtManager( const nativeExtService = new NativeExtManager(
application application
); );
const prefsService = new PreferencesManager(
application
);
const statusService = new StatusManager(); const statusService = new StatusManager();
const themeService = new ThemeManager( const themeService = new ThemeManager(
application, application,
@@ -96,7 +92,6 @@ export class ApplicationGroup extends SNApplicationGroup {
keyboardService, keyboardService,
autolockService, autolockService,
nativeExtService, nativeExtService,
prefsService,
statusManager: statusService, statusManager: statusService,
themeService themeService
}); });

View File

@@ -69,7 +69,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
if (!this.$timeout) { if (!this.$timeout) {
return; return;
} }
return new Promise((resolve) => { return new Promise<void>((resolve) => {
this.stateTimeout = this.$timeout(() => { this.stateTimeout = this.$timeout(() => {
/** /**
* State changes must be *inside* the timeout block for them to be affected in the UI * State changes must be *inside* the timeout block for them to be affected in the UI

View File

@@ -24,3 +24,6 @@
symbol#layers-sharp.ionicon(viewbox="0 0 512 512") symbol#layers-sharp.ionicon(viewbox="0 0 512 512")
path(d="M480 150L256 48 32 150l224 104 224-104zM255.71 392.95l-144.81-66.2L32 362l224 102 224-102-78.69-35.3-145.6 66.25z") path(d="M480 150L256 48 32 150l224 104 224-104zM255.71 392.95l-144.81-66.2L32 362l224 102 224-102-78.69-35.3-145.6 66.25z")
path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z") path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z")
sessions-modal(
application='self.application'
)

View File

@@ -16,7 +16,7 @@ import {
Uuids, Uuids,
ComponentArea, ComponentArea,
ComponentAction, ComponentAction,
WebPrefKey, PrefKey,
ComponentMutator, ComponentMutator,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import find from 'lodash/find'; import find from 'lodash/find';
@@ -135,9 +135,9 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
onReady: () => this.reloadPreferences() onReady: () => this.reloadPreferences()
}; };
/** Used by .pug template */ /** Used by .pug template */
this.prefKeyMonospace = WebPrefKey.EditorMonospaceEnabled; this.prefKeyMonospace = PrefKey.EditorMonospaceEnabled;
this.prefKeySpellcheck = WebPrefKey.EditorSpellcheck; this.prefKeySpellcheck = PrefKey.EditorSpellcheck;
this.prefKeyMarginResizers = WebPrefKey.EditorResizersEnabled; this.prefKeyMarginResizers = PrefKey.EditorResizersEnabled;
this.editorMenuOnSelect = this.editorMenuOnSelect.bind(this); this.editorMenuOnSelect = this.editorMenuOnSelect.bind(this);
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this); this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
@@ -239,38 +239,39 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.registerComponentHandler(); this.registerComponentHandler();
} }
/** @override */
onAppStateEvent(eventName: AppStateEvent, data: any) {
if (eventName === AppStateEvent.PreferencesChanged) {
this.reloadPreferences();
}
}
/** @override */ /** @override */
onAppEvent(eventName: ApplicationEvent) { onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.HighLatencySync) { switch (eventName) {
this.setState({ syncTakingTooLong: true }); case ApplicationEvent.PreferencesChanged:
} else if (eventName === ApplicationEvent.CompletedFullSync) { this.reloadPreferences();
this.setState({ syncTakingTooLong: false }); break;
const isInErrorState = this.state.saveError; case ApplicationEvent.HighLatencySync:
/** if we're still dirty, don't change status, a sync is likely upcoming. */ this.setState({ syncTakingTooLong: true });
if (!this.note.dirty && isInErrorState) { break;
this.showAllChangesSavedStatus(); case ApplicationEvent.CompletedFullSync:
} this.setState({ syncTakingTooLong: false });
} else if (eventName === ApplicationEvent.FailedSync) { const isInErrorState = this.state.saveError;
/** /** if we're still dirty, don't change status, a sync is likely upcoming. */
* Only show error status in editor if the note is dirty. if (!this.note.dirty && isInErrorState) {
* Otherwise, it means the originating sync came from somewhere else this.showAllChangesSavedStatus();
* and we don't want to display an error here. }
*/ break;
if (this.note.dirty) { case ApplicationEvent.FailedSync:
this.showErrorStatus(); /**
} * Only show error status in editor if the note is dirty.
} else if (eventName === ApplicationEvent.LocalDatabaseWriteError) { * Otherwise, it means the originating sync came from somewhere else
this.showErrorStatus({ * and we don't want to display an error here.
message: "Offline Saving Issue", */
desc: "Changes not saved" if (this.note.dirty) {
}); this.showErrorStatus();
}
break;
case ApplicationEvent.LocalDatabaseWriteError:
this.showErrorStatus({
message: "Offline Saving Issue",
desc: "Changes not saved"
});
break;
} }
} }
@@ -891,40 +892,40 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
async onPanelResizeFinish(width: number, left: number, isMaxWidth: boolean) { async onPanelResizeFinish(width: number, left: number, isMaxWidth: boolean) {
if (isMaxWidth) { if (isMaxWidth) {
await this.application.getPrefsService().setUserPrefValue( await this.application.setPreference(
WebPrefKey.EditorWidth, PrefKey.EditorWidth,
null null
); );
} else { } else {
if (width !== undefined && width !== null) { if (width !== undefined && width !== null) {
await this.application.getPrefsService().setUserPrefValue( await this.application.setPreference(
WebPrefKey.EditorWidth, PrefKey.EditorWidth,
width width
); );
this.leftPanelPuppet!.setWidth!(width); this.leftPanelPuppet!.setWidth!(width);
} }
} }
if (left !== undefined && left !== null) { if (left !== undefined && left !== null) {
await this.application.getPrefsService().setUserPrefValue( await this.application.setPreference(
WebPrefKey.EditorLeft, PrefKey.EditorLeft,
left left
); );
this.rightPanelPuppet!.setLeft!(left); this.rightPanelPuppet!.setLeft!(left);
} }
this.application.getPrefsService().syncUserPreferences(); this.application.sync();
} }
async reloadPreferences() { async reloadPreferences() {
const monospaceFont = this.application.getPrefsService().getValue( const monospaceFont = this.application.getPreference(
WebPrefKey.EditorMonospaceEnabled, PrefKey.EditorMonospaceEnabled,
true true
); );
const spellcheck = this.application.getPrefsService().getValue( const spellcheck = this.application.getPreference(
WebPrefKey.EditorSpellcheck, PrefKey.EditorSpellcheck,
true true
); );
const marginResizersEnabled = this.application.getPrefsService().getValue( const marginResizersEnabled = this.application.getPreference(
WebPrefKey.EditorResizersEnabled, PrefKey.EditorResizersEnabled,
true true
); );
await this.setState({ await this.setState({
@@ -945,16 +946,16 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.leftPanelPuppet?.ready && this.leftPanelPuppet?.ready &&
this.rightPanelPuppet?.ready this.rightPanelPuppet?.ready
) { ) {
const width = this.application.getPrefsService().getValue( const width = this.application.getPreference(
WebPrefKey.EditorWidth, PrefKey.EditorWidth,
null null
); );
if (width != null) { if (width != null) {
this.leftPanelPuppet!.setWidth!(width); this.leftPanelPuppet!.setWidth!(width);
this.rightPanelPuppet!.setWidth!(width); this.rightPanelPuppet!.setWidth!(width);
} }
const left = this.application.getPrefsService().getValue( const left = this.application.getPreference(
WebPrefKey.EditorLeft, PrefKey.EditorLeft,
null null
); );
if (left != null) { if (left != null) {
@@ -982,24 +983,23 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
} }
} }
async toggleWebPrefKey(key: WebPrefKey) { async toggleWebPrefKey(key: PrefKey) {
const currentValue = (this.state as any)[key]; const currentValue = (this.state as any)[key];
await this.application.getPrefsService().setUserPrefValue( await this.application.setPreference(
key, key,
!currentValue, !currentValue,
true
); );
await this.setState({ await this.setState({
[key]: !currentValue [key]: !currentValue
}) })
this.reloadFont(); this.reloadFont();
if (key === WebPrefKey.EditorSpellcheck) { if (key === PrefKey.EditorSpellcheck) {
/** Allows textarea to reload */ /** Allows textarea to reload */
await this.setState({ textareaUnloading: true }); await this.setState({ textareaUnloading: true });
await this.setState({ textareaUnloading: false }); await this.setState({ textareaUnloading: false });
this.reloadFont(); this.reloadFont();
} else if (key === WebPrefKey.EditorResizersEnabled && this.state[key] === true) { } else if (key === PrefKey.EditorResizersEnabled && this.state[key] === true) {
this.$timeout(() => { this.$timeout(() => {
this.leftPanelPuppet!.flash!(); this.leftPanelPuppet!.flash!();
this.rightPanelPuppet!.flash!(); this.rightPanelPuppet!.flash!();

View File

@@ -1,16 +1,5 @@
import { SNNote } from '@standardnotes/snjs'; import { SNNote } from '@standardnotes/snjs';
export enum NoteSortKey {
CreatedAt = 'created_at',
UserUpdatedAt = 'userModifiedDate',
Title = 'title',
/** @legacy Use UserUpdatedAt instead */
UpdatedAt = 'updated_at',
/** @legacy Use UserUpdatedAt instead */
ClientUpdatedAt = 'client_updated_at',
}
export function notePassesFilter( export function notePassesFilter(
note: SNNote, note: SNNote,
showArchived: boolean, showArchived: boolean,

View File

@@ -6,7 +6,7 @@ import {
removeFromArray, removeFromArray,
SNNote, SNNote,
SNTag, SNTag,
WebPrefKey, PrefKey,
findInArray, findInArray,
CollectionSort, CollectionSort,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
@@ -17,7 +17,6 @@ import {
PANEL_NAME_NOTES PANEL_NAME_NOTES
} from '@/views/constants'; } from '@/views/constants';
import { import {
NoteSortKey,
notePassesFilter notePassesFilter
} from './note_utils'; } from './note_utils';
import { UuidString } from '@standardnotes/snjs'; import { UuidString } from '@standardnotes/snjs';
@@ -37,13 +36,13 @@ type NotesState = {
noteFilter: { text: string } noteFilter: { text: string }
mutable: { showMenu: boolean } mutable: { showMenu: boolean }
completedFullSync: boolean completedFullSync: boolean
[WebPrefKey.TagsPanelWidth]?: number [PrefKey.TagsPanelWidth]?: number
[WebPrefKey.NotesPanelWidth]?: number [PrefKey.NotesPanelWidth]?: number
[WebPrefKey.EditorWidth]?: number [PrefKey.EditorWidth]?: number
[WebPrefKey.EditorLeft]?: number [PrefKey.EditorLeft]?: number
[WebPrefKey.EditorMonospaceEnabled]?: boolean [PrefKey.EditorMonospaceEnabled]?: boolean
[WebPrefKey.EditorSpellcheck]?: boolean [PrefKey.EditorSpellcheck]?: boolean
[WebPrefKey.EditorResizersEnabled]?: boolean [PrefKey.EditorResizersEnabled]?: boolean
} }
type NoteFlag = { type NoteFlag = {
@@ -147,8 +146,6 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
this.handleTagChange(this.selectedTag!); this.handleTagChange(this.selectedTag!);
} else if (eventName === AppStateEvent.ActiveEditorChanged) { } else if (eventName === AppStateEvent.ActiveEditorChanged) {
this.handleEditorChange(); this.handleEditorChange();
} else if (eventName === AppStateEvent.PreferencesChanged) {
this.reloadPreferences();
} else if (eventName === AppStateEvent.EditorFocused) { } else if (eventName === AppStateEvent.EditorFocused) {
this.setShowMenuFalse(); this.setShowMenuFalse();
} }
@@ -166,6 +163,9 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
/** @override */ /** @override */
async onAppEvent(eventName: ApplicationEvent) { async onAppEvent(eventName: ApplicationEvent) {
switch (eventName) { switch (eventName) {
case ApplicationEvent.PreferencesChanged:
this.reloadPreferences();
break;
case ApplicationEvent.SignedIn: case ApplicationEvent.SignedIn:
this.appState.closeAllEditors(); this.appState.closeAllEditors();
this.selectFirstNote(); this.selectFirstNote();
@@ -420,37 +420,40 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
async reloadPreferences() { async reloadPreferences() {
const viewOptions = {} as NotesState; const viewOptions = {} as NotesState;
const prevSortValue = this.getState().sortBy; const prevSortValue = this.getState().sortBy;
let sortBy = this.application!.getPrefsService().getValue( let sortBy = this.application!.getPreference(
WebPrefKey.SortNotesBy, PrefKey.SortNotesBy,
NoteSortKey.CreatedAt CollectionSort.CreatedAt
); );
if (sortBy === NoteSortKey.UpdatedAt || sortBy === NoteSortKey.ClientUpdatedAt) { if (
sortBy === CollectionSort.UpdatedAt ||
(sortBy as string) === "client_updated_at"
) {
/** Use UserUpdatedAt instead */ /** Use UserUpdatedAt instead */
sortBy = NoteSortKey.UserUpdatedAt; sortBy = CollectionSort.UpdatedAt;
} }
viewOptions.sortBy = sortBy; viewOptions.sortBy = sortBy;
viewOptions.sortReverse = this.application!.getPrefsService().getValue( viewOptions.sortReverse = this.application!.getPreference(
WebPrefKey.SortNotesReverse, PrefKey.SortNotesReverse,
false false
); );
viewOptions.showArchived = this.application!.getPrefsService().getValue( viewOptions.showArchived = this.application!.getPreference(
WebPrefKey.NotesShowArchived, PrefKey.NotesShowArchived,
false false
); );
viewOptions.hidePinned = this.application!.getPrefsService().getValue( viewOptions.hidePinned = this.application!.getPreference(
WebPrefKey.NotesHidePinned, PrefKey.NotesHidePinned,
false false
); );
viewOptions.hideNotePreview = this.application!.getPrefsService().getValue( viewOptions.hideNotePreview = this.application!.getPreference(
WebPrefKey.NotesHideNotePreview, PrefKey.NotesHideNotePreview,
false false
); );
viewOptions.hideDate = this.application!.getPrefsService().getValue( viewOptions.hideDate = this.application!.getPreference(
WebPrefKey.NotesHideDate, PrefKey.NotesHideDate,
false false
); );
viewOptions.hideTags = this.application.getPrefsService().getValue( viewOptions.hideTags = this.application.getPreference(
WebPrefKey.NotesHideTags, PrefKey.NotesHideTags,
true, true,
); );
const state = this.getState(); const state = this.getState();
@@ -475,8 +478,8 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
} }
reloadPanelWidth() { reloadPanelWidth() {
const width = this.application!.getPrefsService().getValue( const width = this.application!.getPreference(
WebPrefKey.NotesPanelWidth PrefKey.NotesPanelWidth
); );
if (width && this.panelPuppet!.ready) { if (width && this.panelPuppet!.ready) {
this.panelPuppet!.setWidth!(width); this.panelPuppet!.setWidth!(width);
@@ -495,10 +498,9 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
__: boolean, __: boolean,
isCollapsed: boolean isCollapsed: boolean
) { ) {
this.application!.getPrefsService().setUserPrefValue( this.application!.setPreference(
WebPrefKey.NotesPanelWidth, PrefKey.NotesPanelWidth,
newWidth, newWidth
true
); );
this.application!.getAppState().panelDidResize( this.application!.getAppState().panelDidResize(
PANEL_NAME_NOTES, PANEL_NAME_NOTES,
@@ -541,11 +543,11 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
optionsSubtitle() { optionsSubtitle() {
let base = ""; let base = "";
if (this.getState().sortBy === NoteSortKey.CreatedAt) { if (this.getState().sortBy === CollectionSort.CreatedAt) {
base += " Date Added"; base += " Date Added";
} else if (this.getState().sortBy === NoteSortKey.UserUpdatedAt) { } else if (this.getState().sortBy === CollectionSort.UpdatedAt) {
base += " Date Modified"; base += " Date Modified";
} else if (this.getState().sortBy === NoteSortKey.Title) { } else if (this.getState().sortBy === CollectionSort.Title) {
base += " Title"; base += " Title";
} }
if (this.getState().showArchived) { if (this.getState().showArchived) {
@@ -709,40 +711,37 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
this.setShowMenuFalse(); this.setShowMenuFalse();
} }
toggleWebPrefKey(key: WebPrefKey) { togglePrefKey(key: PrefKey) {
this.application!.getPrefsService().setUserPrefValue( this.application!.setPreference(
key, key,
!this.state[key], !this.state[key]
true
); );
} }
selectedSortByCreated() { selectedSortByCreated() {
this.setSortBy(NoteSortKey.CreatedAt); this.setSortBy(CollectionSort.CreatedAt);
} }
selectedSortByUpdated() { selectedSortByUpdated() {
this.setSortBy(NoteSortKey.ClientUpdatedAt); this.setSortBy(CollectionSort.UpdatedAt);
} }
selectedSortByTitle() { selectedSortByTitle() {
this.setSortBy(NoteSortKey.Title); this.setSortBy(CollectionSort.Title);
} }
toggleReverseSort() { toggleReverseSort() {
this.selectedMenuItem(); this.selectedMenuItem();
this.application!.getPrefsService().setUserPrefValue( this.application!.setPreference(
WebPrefKey.SortNotesReverse, PrefKey.SortNotesReverse,
!this.getState().sortReverse, !this.getState().sortReverse
true
); );
} }
setSortBy(type: NoteSortKey) { setSortBy(type: CollectionSort) {
this.application!.getPrefsService().setUserPrefValue( this.application!.setPreference(
WebPrefKey.SortNotesBy, PrefKey.SortNotesBy,
type, type
true
); );
} }

View File

@@ -9,7 +9,7 @@ import {
SNSmartTag, SNSmartTag,
ComponentArea, ComponentArea,
SNComponent, SNComponent,
WebPrefKey, PrefKey,
UuidString, UuidString,
TagMutator TagMutator
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
@@ -145,10 +145,8 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
} }
/** @override */ /** @override */
onAppStateEvent(eventName: AppStateEvent, data?: any) { onAppStateEvent(eventName: AppStateEvent) {
if (eventName === AppStateEvent.PreferencesChanged) { if (eventName === AppStateEvent.TagChanged) {
this.loadPreferences();
} else if (eventName === AppStateEvent.TagChanged) {
this.setState({ this.setState({
selectedTag: this.application.getAppState().getSelectedTag() selectedTag: this.application.getAppState().getSelectedTag()
}); });
@@ -159,8 +157,13 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
/** @override */ /** @override */
async onAppEvent(eventName: ApplicationEvent) { async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName); super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) { switch (eventName) {
this.reloadNoteCounts(); case ApplicationEvent.LocalDataIncrementalLoad:
this.reloadNoteCounts();
break;
case ApplicationEvent.PreferencesChanged:
this.loadPreferences();
break;
} }
} }
@@ -203,7 +206,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
if (!this.panelPuppet.ready) { if (!this.panelPuppet.ready) {
return; return;
} }
const width = this.application.getPrefsService().getValue(WebPrefKey.TagsPanelWidth); const width = this.application.getPreference(PrefKey.TagsPanelWidth);
if (width) { if (width) {
this.panelPuppet.setWidth!(width); this.panelPuppet.setWidth!(width);
if (this.panelPuppet.isCollapsed!()) { if (this.panelPuppet.isCollapsed!()) {
@@ -221,11 +224,10 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
_isAtMaxWidth: boolean, _isAtMaxWidth: boolean,
isCollapsed: boolean isCollapsed: boolean
) => { ) => {
this.application.getPrefsService().setUserPrefValue( this.application.setPreference(
WebPrefKey.TagsPanelWidth, PrefKey.TagsPanelWidth,
newWidth, newWidth
true ).then(() => this.application.sync());
);
this.application.getAppState().panelDidResize( this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS, PANEL_NAME_TAGS,
isCollapsed isCollapsed

View File

@@ -48,10 +48,16 @@ body {
color: var(--sn-stylekit-info-contrast-color); color: var(--sn-stylekit-info-contrast-color);
} }
*:focus {outline:0;} h1 {
font-size: var(--sn-stylekit-font-size-h1);
}
button:focus { h2 {
outline:0; font-size: var(--sn-stylekit-font-size-h2);
}
h3 {
font-size: var(--sn-stylekit-font-size-h3);
} }
input, button, select, textarea { input, button, select, textarea {

View File

@@ -63,9 +63,16 @@
font-weight: normal; font-weight: normal;
font-size: var(--sn-stylekit-font-size-h3); font-size: var(--sn-stylekit-font-size-h3);
border: none; border-style: solid;
border-color: transparent;
width: 100%; width: 100%;
position: relative; position: relative;
&:focus {
outline: 0;
border-color: var(--sn-stylekit-info-color);
border-width: 1px;
}
} }
#search-clear-button { #search-clear-button {

View File

@@ -0,0 +1,38 @@
[data-reach-dialog-overlay] {
z-index: $z-index-modal;
background: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: unset;
}
[data-reach-dialog-overlay]::before {
background-color: var(--sn-stylekit-contrast-background-color);
content: "";
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
opacity: 0.75;
}
[data-reach-dialog-content] {
padding: 0;
margin: 0;
position: relative;
overflow: unset;
flex-basis: 0;
}
[data-reach-dialog-content] .sk-modal-content,
[data-reach-dialog-content] .sn-component,
[data-reach-dialog-content] .sk-panel {
height: 100%;
}
[data-reach-alert-dialog-content] {
width: auto;
}

View File

@@ -0,0 +1,48 @@
.sessions-modal {
h2, ul, p {
margin: 0;
padding: 0;
}
h2 {
font-size: var(--sn-stylekit-font-size-h2);
}
ul {
grid-column: 1 / 3;
display: grid;
gap: 16px;
grid-gap: 16px;
}
li {
display: grid;
grid-template-columns: 1fr max-content;
border-bottom: 1px solid var(--sn-stylekit-border-color);
padding-bottom: 16px;
grid-column-gap: 12px;
column-gap: 12px;
}
li:last-of-type {
border: none;
padding-bottom: 0;
}
li > * {
grid-column: 1;
}
li button {
grid-column: 2;
grid-row: 1 / span 3;
align-self: center;
}
.sn-component .sk-panel-content {
padding-bottom: 1.6rem;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
grid-gap: 8px;
gap: 8px;
}
}
.sessions-modal-refreshing {
grid-column: 2;
font-weight: normal;
}

View File

@@ -77,6 +77,12 @@ button.sk-button {
border: none; border: none;
} }
a { a, .sk-a {
background: none;
border: none;
color: var(--sn-stylekit-info-color); color: var(--sn-stylekit-info-color);
} }
button.sk-a {
min-height: 24px;
}

View File

@@ -9,3 +9,5 @@
@import "lock-screen"; @import "lock-screen";
@import "stylekit-sub"; @import "stylekit-sub";
@import "ionicons"; @import "ionicons";
@import "reach-sub";
@import "sessions-modal";

View File

@@ -163,8 +163,11 @@
.sk-panel-row .sk-panel-row
a.sk-a.info.sk-panel-row.condensed( a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPasswordWizard()" ng-click="self.openPasswordWizard()"
ng-if="!self.state.showBetaWarning"
) Change Password ) Change Password
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openSessionsModal()"
ng-if="self.state.showSessions"
) Manage Sessions
a.sk-a.info.sk-panel-row.condensed( a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPrivilegesModal('')", ng-click="self.openPrivilegesModal('')",
ng-show='self.state.user' ng-show='self.state.user'

13118
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,35 +7,34 @@
"url": "https://github.com/standardnotes/web" "url": "https://github.com/standardnotes/web"
}, },
"scripts": { "scripts": {
"setup": "npm run submodules && npm install", "setup": "yarn submodules && yarn install",
"start": "webpack-dev-server --progress --config webpack.dev.js", "start": "webpack-dev-server --progress --config webpack.dev.js",
"watch": "webpack -w --config webpack.dev.js", "watch": "webpack -w --config webpack.dev.js",
"watch:desktop": "webpack -w --config webpack.dev.js --env.platform='desktop'", "watch:desktop": "webpack -w --config webpack.dev.js --env.platform='desktop'",
"bundle": "webpack --config webpack.prod.js && npm run tsc", "bundle": "webpack --config webpack.prod.js && yarn tsc",
"bundle:desktop": "webpack --config webpack.prod.js --env.platform='desktop'", "bundle:desktop": "webpack --config webpack.prod.js --env.platform='desktop'",
"bundle:desktop:beta": "webpack --config webpack.prod.js --env.platform='desktop' --env.public_beta='true'", "bundle:desktop:beta": "webpack --config webpack.prod.js --env.platform='desktop' --env.public_beta='true'",
"build": "bundle install && npm ci && bundle exec rails assets:precompile && npm run bundle", "build": "bundle install && yarn install --pure-lockfile && bundle exec rails assets:precompile && yarn bundle",
"submodules": "git submodule update --init --force", "submodules": "git submodule update --init --force",
"lint": "eslint --fix app/assets/javascripts/**/*.js", "lint": "eslint --fix app/assets/javascripts/**/*.js",
"tsc": "tsc --project app/assets/javascripts/tsconfig.json" "tsc": "tsc --project app/assets/javascripts/tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.10.5", "@babel/core": "^7.12.10",
"@babel/core": "^7.11.1", "@babel/plugin-transform-react-jsx": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/preset-env": "^7.12.10",
"@babel/preset-env": "^7.11.0", "@babel/preset-typescript": "^7.12.7",
"@babel/preset-typescript": "^7.10.4", "@types/angular": "^1.8.0",
"@types/angular": "^1.7.0",
"@types/chai": "^4.2.11", "@types/chai": "^4.2.11",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/pug": "^2.0.4", "@types/pug": "^2.0.4",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0", "@typescript-eslint/parser": "^2.23.0",
"angular": "1.8.0", "angular": "^1.8.2",
"apply-loader": "^2.0.0", "apply-loader": "^2.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-angularjs-annotate": "^0.10.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"connect": "^3.7.0", "connect": "^3.7.0",
@@ -43,8 +42,6 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^6.10.0",
"eslint-config-semistandard": "^15.0.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.1",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-standard": "^4.0.1",
@@ -60,8 +57,8 @@
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"serve-static": "^1.14.1", "serve-static": "^1.14.1",
"sn-stylekit": "2.1.0", "sn-stylekit": "2.1.0",
"ts-loader": "^8.0.2", "ts-loader": "^8.0.12",
"typescript": "^3.9.7", "typescript": "^4.1.3",
"typescript-eslint": "0.0.1-alpha.0", "typescript-eslint": "0.0.1-alpha.0",
"webpack": "^4.44.1", "webpack": "^4.44.1",
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
@@ -70,8 +67,13 @@
}, },
"dependencies": { "dependencies": {
"@bugsnag/js": "^7.5.1", "@bugsnag/js": "^7.5.1",
"@reach/alert": "^0.12.1",
"@reach/alert-dialog": "^0.12.1",
"@reach/dialog": "^0.12.1",
"@standardnotes/sncrypto-web": "^1.2.9", "@standardnotes/sncrypto-web": "^1.2.9",
"@standardnotes/snjs": "^2.0.16", "@standardnotes/snjs": "^2.0.30",
"mobx": "^6.0.1" "babel-loader": "^8.2.2",
"mobx": "^6.0.4",
"preact": "^10.5.7"
} }
} }

View File

@@ -3,12 +3,14 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
require('dotenv').config(); require('dotenv').config();
module.exports = (env = { module.exports = (
platform: 'web' env = {
}) => ({ platform: 'web',
}
) => ({
entry: './app/assets/javascripts/index.ts', entry: './app/assets/javascripts/index.ts',
output: { output: {
filename: './javascripts/app.js' filename: './javascripts/app.js',
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
@@ -19,28 +21,40 @@ module.exports = (env = {
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output // Options similar to the same options in webpackOptions.output
filename: './stylesheets/app.css', filename: './stylesheets/app.css',
ignoreOrder: true // Enable to remove warnings about conflicting order ignoreOrder: true, // Enable to remove warnings about conflicting order
}) }),
], ],
resolve: { resolve: {
extensions: ['.ts', '.js'], extensions: ['.ts', '.tsx', '.js'],
alias: { alias: {
'%': path.resolve(__dirname, 'app/assets/templates'), '%': path.resolve(__dirname, 'app/assets/templates'),
'@': path.resolve(__dirname, 'app/assets/javascripts'), '@': path.resolve(__dirname, 'app/assets/javascripts'),
'@Controllers': path.resolve(__dirname, 'app/assets/javascripts/controllers'), '@Controllers': path.resolve(
__dirname,
'app/assets/javascripts/controllers'
),
'@Views': path.resolve(__dirname, 'app/assets/javascripts/views'), '@Views': path.resolve(__dirname, 'app/assets/javascripts/views'),
'@Services': path.resolve(__dirname, 'app/assets/javascripts/services'), '@Services': path.resolve(__dirname, 'app/assets/javascripts/services'),
'@node_modules': path.resolve(__dirname, 'node_modules'), '@node_modules': path.resolve(__dirname, 'node_modules'),
} react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat',
},
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.(js|ts)$/, test: /\.(js|tsx?)$/,
exclude: /(node_modules|snjs)/, exclude: /(node_modules)/,
use: { use: [
loader: 'babel-loader' 'babel-loader',
} {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
}, },
{ {
test: /\.s?css$/, test: /\.s?css$/,
@@ -49,12 +63,12 @@ module.exports = (env = {
loader: MiniCssExtractPlugin.loader, loader: MiniCssExtractPlugin.loader,
options: { options: {
publicPath: '../', // The base assets directory in relation to the stylesheets publicPath: '../', // The base assets directory in relation to the stylesheets
hmr: process.env.NODE_ENV === 'development' hmr: process.env.NODE_ENV === 'development',
} },
}, },
'css-loader', 'css-loader',
'sass-loader' 'sass-loader',
] ],
}, },
{ {
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
@@ -63,36 +77,34 @@ module.exports = (env = {
loader: 'file-loader', loader: 'file-loader',
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'fonts/' outputPath: 'fonts/',
} },
} },
] ],
}, },
{ {
test: /\.html$/, test: /\.html$/,
exclude: [ exclude: [path.resolve(__dirname, 'index.html')],
path.resolve(__dirname, 'index.html'),
],
use: [ use: [
{ {
loader: 'ng-cache-loader', loader: 'ng-cache-loader',
options: { options: {
prefix: 'templates:**' prefix: 'templates:**',
} },
} },
] ],
}, },
{ {
test: /\.pug$/, test: /\.pug$/,
use: [ use: [
{ {
loader: 'apply-loader' loader: 'apply-loader',
}, },
{ {
loader: 'pug-loader' loader: 'pug-loader',
} },
] ],
} },
] ],
} },
}); });

8174
yarn.lock Normal file

File diff suppressed because it is too large Load Diff