Merge branch 'release/3.5.1'

This commit is contained in:
Baptiste Grob 2020-11-02 10:28:35 +01:00
commit 1bf08decea
No known key found for this signature in database
GPG key ID: 4CC6BD922C8AF137
289 changed files with 46182 additions and 12016 deletions

View file

@ -1,14 +1,9 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": "> 0.25%, not dead"
}
]
"@babel/typescript",
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"angularjs-annotate"
]

6
.browserslistrc Normal file
View file

@ -0,0 +1,6 @@
chrome 80 # electron 8.2
last 2 Firefox versions
last 2 Edge versions
last 2 Safari versions
last 2 Opera versions
Firefox ESR

View file

@ -1,3 +1,5 @@
# git
.git/
.gitignore
node_modules
public/assets

View file

@ -1,14 +1,19 @@
RAILS_ENV=development
PORT=3000
PORT=3001
WEB_CONCURRENCY=0
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
SECRET_KEY_BASE=test
APP_HOST=http://localhost:3000
APP_HOST=http://localhost:3001
EXTENSIONS_MANAGER_LOCATION=extensions/extensions-manager/dist/index.html
BATCH_MANAGER_LOCATION=extensions/batch-manager/dist/index.min.html
SF_DEFAULT_SERVER=http://localhost:3001
SF_DEFAULT_SERVER=http://localhost:3000
# Datadog
DATADOG_ENABLED=false
# Development options
DEV_DEFAULT_SYNC_SERVER=https://sync.standardnotes.org
DEV_EXTENSIONS_MANAGER_LOCATION=public/extensions/extensions-manager/dist/index.html
DEV_BATCH_MANAGER_LOCATION=public/extensions/batch-manager/dist/index.min.html

View file

@ -4,7 +4,7 @@
"rules": {
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals
"no-throw-literal": 0,
"no-console": "error",
// "no-console": "error",
"semi": 1
},
"env": {

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [standardnotes]

83
.github/workflows/beta.yml vendored Normal file
View file

@ -0,0 +1,83 @@
name: Beta
on:
push:
branches: [ release/* ]
jobs:
tsc:
name: Check types
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Typescript
run: npm run tsc
deploy:
runs-on: ubuntu-latest
needs: tsc
steps:
- uses: actions/checkout@v2
- name: Initiate submodules
run: git submodule update --init
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: standardnotes/web
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: "beta,${{ github.sha }}"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition app-beta-prod --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: app-beta-prod
image: "standardnotes/web:${{ github.sha }}"
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: app-beta-prod
cluster: prod
wait-for-service-stability: true
notify_slack:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Notify slack
uses: pullreminders/slack-action@master
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
args: '{ \"channel\": \"${{ secrets.SLACK_NOTIFICATION_CHANNEL }}\", \"blocks\": [{\"type\": \"section\", \"text\": {\"type\": \"mrkdwn\", \"text\": \"Successfully deployed <https://app-beta.standardnotes.org|[BETA] Web App>\"}}, {\"type\": \"section\", \"text\": {\"type\": \"mrkdwn\", \"text\": \"Changes: <https://github.com/standardnotes/web/commit/${{ github.sha }}|${{ github.sha }}>\"}, \"accessory\": {\"type\": \"image\", \"image_url\": \"https://website-dev.standardnotes.org/assets/icon.png\", \"alt_text\": \"Standard Notes\"}}, { \"type\": \"section\", \"fields\": [{\"type\": \"mrkdwn\", \"text\": \"<https://github.com/standardnotes/web/actions/runs/${{ github.run_id }}|Build details>\"}]}]}'

View file

@ -5,15 +5,34 @@ on:
branches: [ develop ]
jobs:
tsc:
name: Check types
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Typescript
run: npm run tsc
deploy:
runs-on: ubuntu-latest
needs: tsc
steps:
- uses: actions/checkout@v2
- name: Initiate submodules
run: git submodule update --init --force --remote
run: git submodule update --init
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master

18
.github/workflows/pr.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Pull request
on:
pull_request:
branches:
- develop
- master
jobs:
tsc:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Typescript
run: npm run tsc

View file

@ -5,15 +5,35 @@ on:
branches: [ master ]
jobs:
tsc:
name: Check types
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Typescript
run: npm run tsc
deploy:
runs-on: ubuntu-latest
needs: tsc
steps:
- uses: actions/checkout@v2
- name: Initiate submodules
run: git submodule update --init --force --remote
run: git submodule update --init
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master

7
.gitignore vendored
View file

@ -28,8 +28,6 @@
dump.rdb
index.html
# Ignore compiled assets
/public/assets
@ -38,3 +36,8 @@ index.html
!/public/uploads/.keep
.vscode
# Generated Files
/dist/javascripts
/dist/stylesheets
/dist/fonts

View file

@ -1,5 +1,10 @@
FROM ruby:2.7.1-alpine
ARG UID=1000
ARG GID=1000
RUN addgroup -S webapp -g $GID && adduser -D -S webapp -G webapp -u $UID
RUN apk add --update --no-cache \
alpine-sdk \
nodejs \
@ -10,20 +15,24 @@ RUN apk add --update --no-cache \
WORKDIR /app/
COPY package.json package-lock.json Gemfile Gemfile.lock /app/
RUN chown -R $UID:$GID .
COPY vendor /app/vendor
USER webapp
COPY --chown=$UID:$GID package.json package-lock.json Gemfile Gemfile.lock /app/
COPY --chown=$UID:$GID vendor /app/vendor
RUN npm ci
RUN gem install bundler && bundle install
COPY . /app/
RUN bundle exec rails assets:precompile
COPY --chown=$UID:$GID . /app/
RUN npm run bundle
RUN bundle exec rails assets:precompile
EXPOSE 3000
ENTRYPOINT [ "./docker/entrypoint.sh" ]

View file

@ -62,6 +62,26 @@ Questions? Find answers on our [Help page](https://standardnotes.org/help).
---
### Docker setup
Docker is the quick and easy way to try out Standard Notes. We highly recommend using our official [Docker hub image](https://hub.docker.com/repository/docker/standardnotes/web).
### Standalone instance
Before you start make sure you have a `.env` file copied from the sample `.env.sample` and configured with your parameters.
If your intention is not contributing but just running the app we recommend using our official image from Docker hub like this:
```
docker run -d -p 3001:3001 --env-file=your-env-file standardnotes/web:stable
```
Or if you want to use the `develop` branch that is in a work-in-progress state please use:
```
docker run -d -p 3001:3001 --env-file=your-env-file standardnotes/web:latest
```
You can then access the app at `http://localhost:3001` (please check Docker container logs if the server has started already and is listening on connections).
### Running Locally
This repo contains the core code used in the web app, as well as the Electron-based [desktop application](https://github.com/standardnotes/desktop).
@ -69,11 +89,10 @@ This repo contains the core code used in the web app, as well as the Electron-ba
**Instructions:**
1. Clone the repo
2. `npm install`
3. `cp index.html.sample index.html`
4. `npm start`
1. `npm run setup`
1. `npm start`
Then open your browser to `http://localhost:3000`.
Then open your browser to `http://localhost:3001`.
---
@ -81,7 +100,7 @@ Then open your browser to `http://localhost:3000`.
The web app makes use of two optional native extensions, which, when running the app with Rails, can be configured to work as follows:
1. `git submodule update --init --force --remote` (will load the submodules in the `public/extensions` folder)
1. `git submodule update --init` (will load the submodules in the `public/extensions` folder)
1. Set the following environment variables in the .env file:
```
EXTENSIONS_MANAGER_LOCATION=extensions/extensions-manager/dist/index.html
@ -101,3 +120,17 @@ SF_DEFAULT_SERVER=https://sync.myserver
- Desktop app: https://github.com/standardnotes/desktop
- Mobile (iOS & Android): https://github.com/standardnotes/mobile
- Extensions: https://github.com/sn-extensions
## Contributing
For contributing we highly recommend you use our docker-compose setup that is provided in this repository.
### Docker compose setup
Use the included [docker-compose.yml](docker-compose.yml) file to build Standard Notes with `docker-compose`. Once your `.env` file has been copied and configured, simply run:
```
docker-compose up -d
```
This should load the app container and run the necessary scripts. You should then be able to reach the app at `http://localhost:3001`

View file

@ -1,165 +0,0 @@
'use strict';
import angular from 'angular';
import { configRoutes } from './routes';
import {
AppState
} from './state';
import {
Root,
TagsPanel,
NotesPanel,
EditorPanel,
Footer,
LockScreen
} from './controllers';
import {
autofocus,
clickOutside,
delayHide,
elemReady,
fileChange,
infiniteScroll,
lowercase,
selectOnClick,
snEnter
} from './directives/functional';
import {
AccountMenu,
ActionsMenu,
ComponentModal,
ComponentView,
ConflictResolutionModal,
EditorMenu,
InputModal,
MenuRow,
PanelResizer,
PasswordWizard,
PermissionsModal,
PrivilegesAuthModal,
PrivilegesManagementModal,
RevisionPreviewModal,
SessionHistoryMenu,
SyncResolutionMenu
} from './directives/views';
import { trusted } from './filters';
import {
ActionsManager,
ArchiveManager,
AuthManager,
ComponentManager,
DBManager,
DesktopManager,
HttpManager,
KeyboardManager,
MigrationManager,
ModelManager,
NativeExtManager,
PasscodeManager,
PrivilegesManager,
SessionHistory,
SingletonManager,
StatusManager,
StorageManager,
SyncManager,
ThemeManager,
AlertManager,
PreferencesManager
} from './services';
angular.module('app', ['ngSanitize']);
// Config
angular
.module('app')
.config(configRoutes)
.constant('appVersion', __VERSION__);
// Controllers
angular
.module('app')
.directive('root', () => new Root())
.directive('tagsPanel', () => new TagsPanel())
.directive('notesPanel', () => new NotesPanel())
.directive('editorPanel', () => new EditorPanel())
.directive('footer', () => new Footer())
.directive('lockScreen', () => new LockScreen());
// Directives - Functional
angular
.module('app')
.directive('snAutofocus', ['$timeout', autofocus])
.directive('clickOutside', ['$document', clickOutside])
.directive('delayHide', delayHide)
.directive('elemReady', elemReady)
.directive('fileChange', fileChange)
.directive('infiniteScroll', [
'$rootScope',
'$window',
'$timeout',
infiniteScroll
])
.directive('lowercase', lowercase)
.directive('selectOnClick', ['$window', selectOnClick])
.directive('snEnter', snEnter);
// Directives - Views
angular
.module('app')
.directive('accountMenu', () => new AccountMenu())
.directive('actionsMenu', () => new ActionsMenu())
.directive('componentModal', () => new ComponentModal())
.directive(
'componentView',
($rootScope, componentManager, desktopManager, $timeout) =>
new ComponentView($rootScope, componentManager, desktopManager, $timeout)
)
.directive('conflictResolutionModal', () => new ConflictResolutionModal())
.directive('editorMenu', () => new EditorMenu())
.directive('inputModal', () => new InputModal())
.directive('menuRow', () => new MenuRow())
.directive('panelResizer', () => new PanelResizer())
.directive('passwordWizard', () => new PasswordWizard())
.directive('permissionsModal', () => new PermissionsModal())
.directive('privilegesAuthModal', () => new PrivilegesAuthModal())
.directive('privilegesManagementModal', () => new PrivilegesManagementModal())
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('sessionHistoryMenu', () => new SessionHistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu());
// Filters
angular
.module('app')
.filter('trusted', ['$sce', trusted]);
// Services
angular
.module('app')
.service('appState', AppState)
.service('preferencesManager', PreferencesManager)
.service('actionsManager', ActionsManager)
.service('archiveManager', ArchiveManager)
.service('authManager', AuthManager)
.service('componentManager', ComponentManager)
.service('dbManager', DBManager)
.service('desktopManager', DesktopManager)
.service('httpManager', HttpManager)
.service('keyboardManager', KeyboardManager)
.service('migrationManager', MigrationManager)
.service('modelManager', ModelManager)
.service('nativeExtManager', NativeExtManager)
.service('passcodeManager', PasscodeManager)
.service('privilegesManager', PrivilegesManager)
.service('sessionHistory', SessionHistory)
.service('singletonManager', SingletonManager)
.service('statusManager', StatusManager)
.service('storageManager', StorageManager)
.service('syncManager', SyncManager)
.service('alertManager', AlertManager)
.service('themeManager', ThemeManager);

View file

@ -0,0 +1,143 @@
'use strict';
declare const __VERSION__: string;
declare const __WEB__: boolean;
import angular from 'angular';
import { configRoutes } from './routes';
import { ApplicationGroup } from './ui_models/application_group';
import { AccountSwitcher } from './views/account_switcher/account_switcher';
import {
ApplicationGroupView,
ApplicationView,
EditorGroupView,
EditorView,
TagsView,
NotesView,
FooterView,
ChallengeModal
} from '@/views';
import {
autofocus,
clickOutside,
delayHide,
elemReady,
fileChange,
infiniteScroll,
lowercase,
selectOnFocus,
snEnter
} from './directives/functional';
import {
AccountMenu,
ActionsMenu,
ComponentModal,
ComponentView,
EditorMenu,
InputModal,
MenuRow,
PanelResizer,
PasswordWizard,
PermissionsModal,
PrivilegesAuthModal,
PrivilegesManagementModal,
RevisionPreviewModal,
HistoryMenu,
SyncResolutionMenu
} from './directives/views';
import { trusted } from './filters';
import { isDev } from './utils';
import { Bridge, BrowserBridge } from './services/bridge';
if (__WEB__) {
startApplication(
(window as any)._default_sync_server,
new BrowserBridge()
);
} else {
(window as any).startApplication = startApplication;
}
function startApplication(
defaultSyncServerHost: string,
bridge: Bridge
) {
angular.module('app', ['ngSanitize']);
// Config
angular
.module('app')
.config(configRoutes)
.constant('bridge', bridge)
.constant('defaultSyncServerHost', defaultSyncServerHost)
.constant('appVersion', __VERSION__);
// Controllers
angular
.module('app')
.directive('applicationGroupView', () => new ApplicationGroupView())
.directive('applicationView', () => new ApplicationView())
.directive('editorGroupView', () => new EditorGroupView())
.directive('editorView', () => new EditorView())
.directive('tagsView', () => new TagsView())
.directive('notesView', () => new NotesView())
.directive('footerView', () => new FooterView())
// Directives - Functional
angular
.module('app')
.directive('snAutofocus', ['$timeout', autofocus])
.directive('clickOutside', ['$document', clickOutside])
.directive('delayHide', delayHide)
.directive('elemReady', elemReady)
.directive('fileChange', fileChange)
.directive('infiniteScroll', [infiniteScroll])
.directive('lowercase', lowercase)
.directive('selectOnFocus', ['$window', selectOnFocus])
.directive('snEnter', snEnter);
// Directives - Views
angular
.module('app')
.directive('accountMenu', () => new AccountMenu())
.directive('accountSwitcher', () => new AccountSwitcher())
.directive('actionsMenu', () => new ActionsMenu())
.directive('challengeModal', () => new ChallengeModal())
.directive('componentModal', () => new ComponentModal())
.directive('componentView', () => new ComponentView())
.directive('editorMenu', () => new EditorMenu())
.directive('inputModal', () => new InputModal())
.directive('menuRow', () => new MenuRow())
.directive('panelResizer', () => new PanelResizer())
.directive('passwordWizard', () => new PasswordWizard())
.directive('permissionsModal', () => new PermissionsModal())
.directive('privilegesAuthModal', () => new PrivilegesAuthModal())
.directive('privilegesManagementModal', () => new PrivilegesManagementModal())
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu());
// Filters
angular
.module('app')
.filter('trusted', ['$sce', trusted]);
// Services
angular.module('app').service('mainApplicationGroup', ApplicationGroup);
// Debug
if (isDev) {
Object.defineProperties(window, {
application: {
get: () =>
(angular.element(document).injector().get('mainApplicationGroup') as any)
.primaryApplication
},
});
}
}

View file

@ -1,28 +0,0 @@
export class PureCtrl {
constructor(
$timeout
) {
if(!$timeout) {
throw 'Invalid PureCtrl construction.';
}
this.$timeout = $timeout;
this.state = {};
this.props = {};
}
async setState(state) {
return new Promise((resolve) => {
this.$timeout(() => {
this.state = Object.freeze(Object.assign({}, this.state, state));
resolve();
});
});
}
initProps(props) {
if (Object.keys(this.props).length > 0) {
throw 'Already init-ed props.';
}
this.props = Object.freeze(Object.assign({}, this.props, props));
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,382 +0,0 @@
import { PrivilegesManager } from '@/services/privilegesManager';
import { dateToLocalizedString } from '@/utils';
import template from '%/footer.pug';
import {
APP_STATE_EVENT_EDITOR_FOCUSED,
APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD,
APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD,
EVENT_SOURCE_USER_INTERACTION
} from '@/state';
import {
STRING_GENERIC_SYNC_ERROR,
STRING_NEW_UPDATE_READY
} from '@/strings';
class FooterCtrl {
/* @ngInject */
constructor(
$rootScope,
$timeout,
alertManager,
appState,
authManager,
componentManager,
modelManager,
nativeExtManager,
passcodeManager,
privilegesManager,
statusManager,
syncManager,
) {
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.appState = appState;
this.authManager = authManager;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.nativeExtManager = nativeExtManager;
this.passcodeManager = passcodeManager;
this.privilegesManager = privilegesManager;
this.statusManager = statusManager;
this.syncManager = syncManager;
this.rooms = [];
this.themesWithIcons = [];
this.showSyncResolution = false;
this.addAppStateObserver();
this.updateOfflineStatus();
this.addSyncEventHandler();
this.findErrors();
this.registerMappingObservers();
this.registerComponentHandler();
this.addRootScopeListeners();
this.authManager.checkForSecurityUpdate().then((available) => {
this.securityUpdateAvailable = available;
});
this.statusManager.addStatusObserver((string) => {
this.$timeout(() => {
this.arbitraryStatusMessage = string;
});
});
}
addRootScopeListeners() {
this.$rootScope.$on("security-update-status-changed", () => {
this.securityUpdateAvailable = this.authManager.securityUpdateAvailable;
});
this.$rootScope.$on("reload-ext-data", () => {
this.reloadExtendedData();
});
this.$rootScope.$on("new-update-available", () => {
this.$timeout(() => {
this.onNewUpdateAvailable();
});
});
}
addAppStateObserver() {
this.appState.addObserver((eventName, data) => {
if(eventName === APP_STATE_EVENT_EDITOR_FOCUSED) {
if (data.eventSource === EVENT_SOURCE_USER_INTERACTION) {
this.closeAllRooms();
this.closeAccountMenu();
}
} else if(eventName === APP_STATE_EVENT_BEGAN_BACKUP_DOWNLOAD) {
this.backupStatus = this.statusManager.addStatusFromString(
"Saving local backup..."
);
} else if(eventName === APP_STATE_EVENT_ENDED_BACKUP_DOWNLOAD) {
if(data.success) {
this.backupStatus = this.statusManager.replaceStatusWithString(
this.backupStatus,
"Successfully saved backup."
);
} else {
this.backupStatus = this.statusManager.replaceStatusWithString(
this.backupStatus,
"Unable to save local backup."
);
}
this.$timeout(() => {
this.backupStatus = this.statusManager.removeStatus(this.backupStatus);
}, 2000);
}
});
}
addSyncEventHandler() {
this.syncManager.addEventHandler((syncEvent, data) => {
this.$timeout(() => {
if(syncEvent === "local-data-loaded") {
if(this.offline && this.modelManager.noteCount() === 0) {
this.showAccountMenu = true;
}
} else if(syncEvent === "enter-out-of-sync") {
this.outOfSync = true;
} else if(syncEvent === "exit-out-of-sync") {
this.outOfSync = false;
} else if(syncEvent === 'sync:completed') {
this.syncUpdated();
this.findErrors();
this.updateOfflineStatus();
} else if(syncEvent === 'sync:error') {
this.findErrors();
this.updateOfflineStatus();
}
});
});
}
registerMappingObservers() {
this.modelManager.addItemSyncObserver(
'room-bar',
'SN|Component',
(allItems, validItems, deletedItems, source) => {
this.rooms = this.modelManager.components.filter((candidate) => {
return candidate.area === 'rooms' && !candidate.deleted;
});
if(this.queueExtReload) {
this.queueExtReload = false;
this.reloadExtendedData();
}
}
);
this.modelManager.addItemSyncObserver(
'footer-bar-themes',
'SN|Theme',
(allItems, validItems, deletedItems, source) => {
const themes = this.modelManager.validItemsForContentType('SN|Theme')
.filter((candidate) => {
return (
!candidate.deleted &&
candidate.content.package_info &&
candidate.content.package_info.dock_icon
);
}).sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const differ = themes.length !== this.themesWithIcons.length;
this.themesWithIcons = themes;
if(differ) {
this.reloadDockShortcuts();
}
}
);
}
registerComponentHandler() {
this.componentManager.registerHandler({
identifier: "roomBar",
areas: ["rooms", "modal"],
activationHandler: (component) => {},
actionHandler: (component, action, data) => {
if(action === "set-size") {
component.setLastSize(data);
}
},
focusHandler: (component, focused) => {
if(component.isEditor() && focused) {
this.closeAllRooms();
this.closeAccountMenu();
}
}
});
}
reloadExtendedData() {
if(this.reloadInProgress) {
return;
}
this.reloadInProgress = true;
/**
* A reload consists of opening the extensions manager,
* then closing it after a short delay.
*/
const extWindow = this.rooms.find((room) => {
return room.package_info.identifier === this.nativeExtManager.extManagerId;
});
if(!extWindow) {
this.queueExtReload = true;
this.reloadInProgress = false;
return;
}
this.selectRoom(extWindow);
this.$timeout(() => {
this.selectRoom(extWindow);
this.reloadInProgress = false;
this.$rootScope.$broadcast('ext-reload-complete');
}, 2000);
}
getUser() {
return this.authManager.user;
}
updateOfflineStatus() {
this.offline = this.authManager.offline();
}
openSecurityUpdate() {
this.authManager.presentPasswordWizard('upgrade-security');
}
findErrors() {
this.error = this.syncManager.syncStatus.error;
}
accountMenuPressed() {
this.showAccountMenu = !this.showAccountMenu;
this.closeAllRooms();
}
toggleSyncResolutionMenu = () => {
this.showSyncResolution = !this.showSyncResolution;
}
closeAccountMenu = () => {
this.showAccountMenu = false;
}
hasPasscode() {
return this.passcodeManager.hasPasscode();
}
lockApp() {
this.$rootScope.lockApplication();
}
refreshData() {
this.isRefreshing = true;
this.syncManager.sync({
force: true,
performIntegrityCheck: true
}).then((response) => {
this.$timeout(() => {
this.isRefreshing = false;
}, 200);
if(response && response.error) {
this.alertManager.alert({
text: STRING_GENERIC_SYNC_ERROR
});
} else {
this.syncUpdated();
}
});
}
syncUpdated() {
this.lastSyncDate = dateToLocalizedString(new Date());
}
onNewUpdateAvailable() {
this.newUpdateAvailable = true;
}
clickedNewUpdateAnnouncement() {
this.newUpdateAvailable = false;
this.alertManager.alert({
text: STRING_NEW_UPDATE_READY
});
}
reloadDockShortcuts() {
const shortcuts = [];
for(const theme of this.themesWithIcons) {
const name = theme.content.package_info.name;
const icon = theme.content.package_info.dock_icon;
if(!icon) {
continue;
}
shortcuts.push({
name: name,
component: theme,
icon: icon
});
}
this.dockShortcuts = shortcuts.sort((a, b) => {
/** Circles first, then images */
const aType = a.icon.type;
const bType = b.icon.type;
if(aType === bType) {
return 0;
} else if(aType === 'circle' && bType === 'svg') {
return -1;
} else if(bType === 'circle' && aType === 'svg') {
return 1;
}
});
}
initSvgForShortcut(shortcut) {
const id = 'dock-svg-' + shortcut.component.uuid;
const element = document.getElementById(id);
const parser = new DOMParser();
const svg = shortcut.component.content.package_info.dock_icon.source;
const doc = parser.parseFromString(svg, 'image/svg+xml');
element.appendChild(doc.documentElement);
}
selectShortcut(shortcut) {
this.componentManager.toggleComponent(shortcut.component);
}
onRoomDismiss(room) {
room.showRoom = false;
}
closeAllRooms() {
for(const room of this.rooms) {
room.showRoom = false;
}
}
async selectRoom(room) {
const run = () => {
this.$timeout(() => {
room.showRoom = !room.showRoom;
});
};
if(!room.showRoom) {
const requiresPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManageExtensions
);
if(requiresPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManageExtensions,
run
);
} else {
run();
}
} else {
run();
}
}
clickOutsideAccountMenu() {
if(this.privilegesManager.authenticationInProgress()) {
return;
}
this.showAccountMenu = false;
}
}
export class Footer {
constructor() {
this.restrict = 'E';
this.scope = {};
this.template = template;
this.controller = FooterCtrl;
this.replace = true;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
}

View file

@ -1,104 +0,0 @@
import template from '%/lock-screen.pug';
const ELEMENT_ID_PASSCODE_INPUT = 'passcode-input';
class LockScreenCtrl {
/* @ngInject */
constructor(
$scope,
alertManager,
authManager,
passcodeManager,
) {
this.$scope = $scope;
this.alertManager = alertManager;
this.authManager = authManager;
this.passcodeManager = passcodeManager;
this.formData = {};
this.addVisibilityObserver();
this.addDestroyHandler();
}
get passcodeInput() {
return document.getElementById(
ELEMENT_ID_PASSCODE_INPUT
);
}
addDestroyHandler() {
this.$scope.$on('$destroy', () => {
this.passcodeManager.removeVisibilityObserver(
this.visibilityObserver
);
});
}
addVisibilityObserver() {
this.visibilityObserver = this.passcodeManager
.addVisibilityObserver((visible) => {
if(visible) {
const input = this.passcodeInput;
if(input) {
input.focus();
}
}
});
}
submitPasscodeForm($event) {
if(
!this.formData.passcode ||
this.formData.passcode.length === 0
) {
return;
}
this.passcodeInput.blur();
this.passcodeManager.unlock(
this.formData.passcode,
(success) => {
if(!success) {
this.formData.passcode = null;
this.alertManager.alert({
text: "Invalid passcode. Please try again.",
onClose: () => {
this.passcodeInput.focus();
}
});
} else {
this.onSuccess()();
}
}
);
}
forgotPasscode() {
this.formData.showRecovery = true;
}
beginDeleteData() {
this.alertManager.confirm({
text: "Are you sure you want to clear all local data?",
destructive: true,
onConfirm: () => {
this.authManager.signout(true).then(() => {
window.location.reload();
});
}
});
}
}
export class LockScreen {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = LockScreenCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
onSuccess: '&',
};
}
}

View file

@ -1,153 +0,0 @@
export const SORT_KEY_CREATED_AT = 'created_at';
export const SORT_KEY_UPDATED_AT = 'updated_at';
export const SORT_KEY_CLIENT_UPDATED_AT = 'client_updated_at';
export const SORT_KEY_TITLE = 'title';
export function filterAndSortNotes({
notes,
selectedTag,
showArchived,
hidePinned,
filterText,
sortBy,
reverse
}) {
const filtered = filterNotes({
notes,
selectedTag,
showArchived,
hidePinned,
filterText,
});
const sorted = sortNotes({
notes: filtered,
sortBy,
reverse
});
return sorted;
}
export function filterNotes({
notes,
selectedTag,
showArchived,
hidePinned,
filterText
}) {
return notes.filter((note) => {
let canShowArchived = showArchived;
const canShowPinned = !hidePinned;
const isTrash = selectedTag.content.isTrashTag;
if (!isTrash && note.content.trashed) {
return false;
}
const isSmartTag = selectedTag.isSmartTag();
if (isSmartTag) {
canShowArchived = (
canShowArchived ||
selectedTag.content.isArchiveTag ||
isTrash
);
}
if (
(note.archived && !canShowArchived) ||
(note.pinned && !canShowPinned)
) {
return false;
}
return noteMatchesQuery({
note,
query: filterText
});
});
}
function noteMatchesQuery({
note,
query
}) {
if(query.length === 0) {
return true;
}
const title = note.safeTitle().toLowerCase();
const text = note.safeText().toLowerCase();
const lowercaseText = query.toLowerCase();
const quotedText = stringBetweenQuotes(lowercaseText);
if(quotedText) {
return title.includes(quotedText) || text.includes(quotedText);
}
if (stringIsUuid(lowercaseText)) {
return note.uuid === lowercaseText;
}
const words = lowercaseText.split(" ");
const matchesTitle = words.every((word) => {
return title.indexOf(word) >= 0;
});
const matchesBody = words.every((word) => {
return text.indexOf(word) >= 0;
});
return matchesTitle || matchesBody;
}
function stringBetweenQuotes(text) {
const matches = text.match(/"(.*?)"/);
return matches ? matches[1] : null;
}
function stringIsUuid(text) {
const matches = text.match(
/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/
);
// eslint-disable-next-line no-unneeded-ternary
return matches ? true : false;
}
export function sortNotes({
notes = [],
sortBy,
reverse
}) {
const sortValueFn = (a, b, pinCheck = false) => {
if (a.dummy) { return -1; }
if (b.dummy) { return 1; }
if (!pinCheck) {
if (a.pinned && b.pinned) {
return sortValueFn(a, b, true);
}
if (a.pinned) { return -1; }
if (b.pinned) { return 1; }
}
let aValue = a[sortBy] || '';
let bValue = b[sortBy] || '';
let vector = 1;
if (reverse) {
vector *= -1;
}
if (sortBy === SORT_KEY_TITLE) {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
if (aValue.length === 0 && bValue.length === 0) {
return 0;
} else if (aValue.length === 0 && bValue.length !== 0) {
return 1 * vector;
} else if (aValue.length !== 0 && bValue.length === 0) {
return -1 * vector;
} else {
vector *= -1;
}
}
if (aValue > bValue) { return -1 * vector; }
else if (aValue < bValue) { return 1 * vector; }
return 0;
};
const result = notes.sort(function (a, b) {
return sortValueFn(a, b);
});
return result;
}

View file

@ -1,724 +0,0 @@
import _ from 'lodash';
import angular from 'angular';
import template from '%/notes.pug';
import { SFAuthManager } from 'snjs';
import { KeyboardManager } from '@/services/keyboardManager';
import { PureCtrl } from '@Controllers';
import {
APP_STATE_EVENT_NOTE_CHANGED,
APP_STATE_EVENT_TAG_CHANGED,
APP_STATE_EVENT_PREFERENCES_CHANGED,
APP_STATE_EVENT_EDITOR_FOCUSED
} from '@/state';
import {
PREF_NOTES_PANEL_WIDTH,
PREF_SORT_NOTES_BY,
PREF_SORT_NOTES_REVERSE,
PREF_NOTES_SHOW_ARCHIVED,
PREF_NOTES_HIDE_PINNED,
PREF_NOTES_HIDE_NOTE_PREVIEW,
PREF_NOTES_HIDE_DATE,
PREF_NOTES_HIDE_TAGS
} from '@/services/preferencesManager';
import {
PANEL_NAME_NOTES
} from '@/controllers/constants';
import {
SORT_KEY_CREATED_AT,
SORT_KEY_UPDATED_AT,
SORT_KEY_CLIENT_UPDATED_AT,
SORT_KEY_TITLE,
filterAndSortNotes
} from './note_utils';
/**
* This is the height of a note cell with nothing but the title,
* which *is* a display option
*/
const MIN_NOTE_CELL_HEIGHT = 51.0;
const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
class NotesCtrl extends PureCtrl {
/* @ngInject */
constructor(
$timeout,
$rootScope,
appState,
authManager,
desktopManager,
keyboardManager,
modelManager,
preferencesManager,
privilegesManager,
syncManager,
) {
super($timeout);
this.$rootScope = $rootScope;
this.appState = appState;
this.authManager = authManager;
this.desktopManager = desktopManager;
this.keyboardManager = keyboardManager;
this.modelManager = modelManager;
this.preferencesManager = preferencesManager;
this.privilegesManager = privilegesManager;
this.syncManager = syncManager;
this.state = {
notes: [],
renderedNotes: [],
selectedNote: null,
tag: null,
sortBy: null,
showArchived: null,
hidePinned: null,
sortReverse: null,
panelTitle: null,
mutable: { showMenu: false },
noteFilter: { text: '' },
};
this.panelController = {};
window.onresize = (event) => {
this.resetPagination({
keepCurrentIfLarger: true
});
};
this.addAppStateObserver();
this.addSignInObserver();
this.addSyncEventHandler();
this.addMappingObserver();
this.reloadPreferences();
this.resetPagination();
this.registerKeyboardShortcuts();
angular.element(document).ready(() => {
this.reloadPreferences();
});
}
addAppStateObserver() {
this.appState.addObserver((eventName, data) => {
if (eventName === APP_STATE_EVENT_TAG_CHANGED) {
this.handleTagChange(this.appState.getSelectedTag(), data.previousTag);
} else if (eventName === APP_STATE_EVENT_NOTE_CHANGED) {
this.handleNoteSelection(this.appState.getSelectedNote());
} else if (eventName === APP_STATE_EVENT_PREFERENCES_CHANGED) {
this.reloadPreferences();
this.reloadNotes();
} else if (eventName === APP_STATE_EVENT_EDITOR_FOCUSED) {
this.setShowMenuFalse();
}
});
}
addSignInObserver() {
this.authManager.addEventHandler((event) => {
if (event === SFAuthManager.DidSignInEvent) {
/** Delete dummy note if applicable */
if (this.state.selectedNote && this.state.selectedNote.dummy) {
this.modelManager.removeItemLocally(this.state.selectedNote);
this.selectNote(null).then(() => {
this.reloadNotes();
});
/**
* We want to see if the user will download any items from the server.
* If the next sync completes and our notes are still 0,
* we need to create a dummy.
*/
this.createDummyOnSynCompletionIfNoNotes = true;
}
}
});
}
addSyncEventHandler() {
this.syncManager.addEventHandler((syncEvent, data) => {
if (syncEvent === 'local-data-loaded') {
if (this.state.notes.length === 0) {
this.createNewNote();
}
} else if (syncEvent === 'sync:completed') {
if (this.createDummyOnSynCompletionIfNoNotes && this.state.notes.length === 0) {
this.createDummyOnSynCompletionIfNoNotes = false;
this.createNewNote();
}
}
});
}
addMappingObserver() {
this.modelManager.addItemSyncObserver(
'note-list',
'*',
async (allItems, validItems, deletedItems, source, sourceKey) => {
await this.reloadNotes();
const selectedNote = this.state.selectedNote;
if (selectedNote) {
const discarded = selectedNote.deleted || selectedNote.content.trashed;
if (discarded) {
this.selectNextOrCreateNew();
}
} else {
this.selectFirstNote();
}
/** Note has changed values, reset its flags */
const notes = allItems.filter((item) => item.content_type === 'Note');
for (const note of notes) {
this.loadFlagsForNote(note);
note.cachedCreatedAtString = note.createdAtString();
note.cachedUpdatedAtString = note.updatedAtString();
}
});
}
async handleTagChange(tag, previousTag) {
if (this.state.selectedNote && this.state.selectedNote.dummy) {
this.modelManager.removeItemLocally(this.state.selectedNote);
if (previousTag) {
_.remove(previousTag.notes, this.state.selectedNote);
}
await this.selectNote(null);
}
await this.setState({
tag: tag
});
this.resetScrollPosition();
this.setShowMenuFalse();
await this.setNoteFilterText('');
this.desktopManager.searchText();
this.resetPagination();
await this.reloadNotes();
if (this.state.notes.length > 0) {
this.selectFirstNote();
} else if (this.syncManager.initialDataLoaded()) {
if (!tag.isSmartTag() || tag.content.isAllTag) {
this.createNewNote();
} else if (
this.state.selectedNote &&
!this.state.notes.includes(this.state.selectedNote)
) {
this.selectNote(null);
}
}
}
resetScrollPosition() {
const scrollable = document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
if (scrollable) {
scrollable.scrollTop = 0;
scrollable.scrollLeft = 0;
}
}
/**
* @template
* @internal
*/
async selectNote(note) {
this.appState.setSelectedNote(note);
}
async removeNoteFromList(note) {
const notes = this.state.notes;
_.pull(notes, note);
await this.setState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
}
async reloadNotes() {
if (!this.state.tag) {
return;
}
const notes = filterAndSortNotes({
notes: this.state.tag.notes,
selectedTag: this.state.tag,
showArchived: this.state.showArchived,
hidePinned: this.state.hidePinned,
filterText: this.state.noteFilter.text.toLowerCase(),
sortBy: this.state.sortBy,
reverse: this.state.sortReverse
});
for (const note of notes) {
if (note.errorDecrypting) {
this.loadFlagsForNote(note);
}
note.shouldShowTags = this.shouldShowTagsForNote(note);
}
await this.setState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
this.reloadPanelTitle();
}
setShowMenuFalse() {
this.setState({
mutable: {
...this.state.mutable,
showMenu: false
}
});
}
async handleNoteSelection(note) {
if (this.state.selectedNote === note) {
return;
}
const previousNote = this.state.selectedNote;
if (previousNote && previousNote.dummy) {
this.modelManager.removeItemLocally(previousNote);
this.removeNoteFromList(previousNote);
}
await this.setState({
selectedNote: note
});
if (!note) {
return;
}
this.selectedIndex = Math.max(0, this.displayableNotes().indexOf(note));
if (note.content.conflict_of) {
note.content.conflict_of = null;
this.modelManager.setItemDirty(note);
this.syncManager.sync();
}
if (this.isFiltering()) {
this.desktopManager.searchText(this.state.noteFilter.text);
}
}
reloadPreferences() {
const viewOptions = {};
const prevSortValue = this.state.sortBy;
let sortBy = this.preferencesManager.getValue(
PREF_SORT_NOTES_BY,
SORT_KEY_CREATED_AT
);
if (sortBy === SORT_KEY_UPDATED_AT) {
/** Use client_updated_at instead */
sortBy = SORT_KEY_CLIENT_UPDATED_AT;
}
viewOptions.sortBy = sortBy;
viewOptions.sortReverse = this.preferencesManager.getValue(
PREF_SORT_NOTES_REVERSE,
false
);
viewOptions.showArchived = this.preferencesManager.getValue(
PREF_NOTES_SHOW_ARCHIVED,
false
);
viewOptions.hidePinned = this.preferencesManager.getValue(
PREF_NOTES_HIDE_PINNED,
false
);
viewOptions.hideNotePreview = this.preferencesManager.getValue(
PREF_NOTES_HIDE_NOTE_PREVIEW,
false
);
viewOptions.hideDate = this.preferencesManager.getValue(
PREF_NOTES_HIDE_DATE,
false
);
viewOptions.hideTags = this.preferencesManager.getValue(
PREF_NOTES_HIDE_TAGS,
false
);
this.setState({
...viewOptions
});
if (prevSortValue && prevSortValue !== sortBy) {
this.selectFirstNote();
}
const width = this.preferencesManager.getValue(
PREF_NOTES_PANEL_WIDTH
);
if (width) {
this.panelController.setWidth(width);
if (this.panelController.isCollapsed()) {
this.appState.panelDidResize({
name: PANEL_NAME_NOTES,
collapsed: this.panelController.isCollapsed()
});
}
}
}
onPanelResize = (newWidth, lastLeft, isAtMaxWidth, isCollapsed) => {
this.preferencesManager.setUserPrefValue(
PREF_NOTES_PANEL_WIDTH,
newWidth
);
this.preferencesManager.syncUserPreferences();
this.appState.panelDidResize({
name: PANEL_NAME_NOTES,
collapsed: isCollapsed
});
}
paginate() {
this.notesToDisplay += this.pageSize;
this.reloadNotes();
if (this.searchSubmitted) {
this.desktopManager.searchText(this.state.noteFilter.text);
}
}
resetPagination({ keepCurrentIfLarger } = {}) {
const clientHeight = document.documentElement.clientHeight;
this.pageSize = clientHeight / MIN_NOTE_CELL_HEIGHT;
if (this.pageSize === 0) {
this.pageSize = DEFAULT_LIST_NUM_NOTES;
}
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
return;
}
this.notesToDisplay = this.pageSize;
}
reloadPanelTitle() {
let title;
if (this.isFiltering()) {
const resultCount = this.state.notes.length;
title = `${resultCount} search results`;
} else if (this.state.tag) {
title = `${this.state.tag.title}`;
}
this.setState({
panelTitle: title
});
}
optionsSubtitle() {
let base = "";
if (this.state.sortBy === 'created_at') {
base += " Date Added";
} else if (this.state.sortBy === 'client_updated_at') {
base += " Date Modified";
} else if (this.state.sortBy === 'title') {
base += " Title";
}
if (this.state.showArchived) {
base += " | + Archived";
}
if (this.state.hidePinned) {
base += " | Pinned";
}
if (this.state.sortReverse) {
base += " | Reversed";
}
return base;
}
loadFlagsForNote(note) {
const flags = [];
if (note.pinned) {
flags.push({
text: "Pinned",
class: 'info'
});
}
if (note.archived) {
flags.push({
text: "Archived",
class: 'warning'
});
}
if (note.content.protected) {
flags.push({
text: "Protected",
class: 'success'
});
}
if (note.locked) {
flags.push({
text: "Locked",
class: 'neutral'
});
}
if (note.content.trashed) {
flags.push({
text: "Deleted",
class: 'danger'
});
}
if (note.content.conflict_of) {
flags.push({
text: "Conflicted Copy",
class: 'danger'
});
}
if (note.errorDecrypting) {
flags.push({
text: "Missing Keys",
class: 'danger'
});
}
if (note.deleted) {
flags.push({
text: "Deletion Pending Sync",
class: 'danger'
});
}
note.flags = flags;
return flags;
}
displayableNotes() {
return this.state.notes;
}
getFirstNonProtectedNote() {
const displayableNotes = this.displayableNotes();
let index = 0;
let note = displayableNotes[index];
while (note && note.content.protected) {
index++;
if (index >= displayableNotes.length) {
break;
}
note = displayableNotes[index];
}
return note;
}
selectFirstNote() {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note);
}
}
selectNextNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.indexOf(this.state.selectedNote);
if (currentIndex + 1 < displayableNotes.length) {
this.selectNote(displayableNotes[currentIndex + 1]);
}
}
selectNextOrCreateNew() {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note);
} else if (!this.state.tag || !this.state.tag.isSmartTag()) {
this.createNewNote();
} else {
this.selectNote(null);
}
}
selectPreviousNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.indexOf(this.state.selectedNote);
if (currentIndex - 1 >= 0) {
this.selectNote(displayableNotes[currentIndex - 1]);
return true;
} else {
return false;
}
}
createNewNote() {
let title;
let isDummyNote = true;
if (this.isFiltering()) {
title = this.state.noteFilter.text;
isDummyNote = false;
} else if (this.state.selectedNote && this.state.selectedNote.dummy) {
return;
} else {
title = `Note ${this.state.notes.length + 1}`;
}
const newNote = this.modelManager.createItem({
content_type: 'Note',
content: {
text: '',
title: title
}
});
newNote.client_updated_at = new Date();
newNote.dummy = isDummyNote;
this.modelManager.addItem(newNote);
this.modelManager.setItemDirty(newNote);
const selectedTag = this.appState.getSelectedTag();
if (!selectedTag.isSmartTag()) {
selectedTag.addItemAsRelationship(newNote);
this.modelManager.setItemDirty(selectedTag);
}
this.selectNote(newNote);
}
isFiltering() {
return this.state.noteFilter.text &&
this.state.noteFilter.text.length > 0;
}
async setNoteFilterText(text) {
await this.setState({
noteFilter: {
...this.state.noteFilter,
text: text
}
});
}
async clearFilterText() {
await this.setNoteFilterText('');
this.onFilterEnter();
this.filterTextChanged();
this.resetPagination();
}
async filterTextChanged() {
if (this.searchSubmitted) {
this.searchSubmitted = false;
}
await this.reloadNotes();
}
onFilterEnter() {
/**
* For Desktop, performing a search right away causes
* input to lose focus. We wait until user explicity hits
* enter before highlighting desktop search results.
*/
this.searchSubmitted = true;
this.desktopManager.searchText(this.state.noteFilter.text);
}
selectedMenuItem() {
this.setShowMenuFalse();
}
togglePrefKey(key) {
this.preferencesManager.setUserPrefValue(key, !this.state[key]);
this.preferencesManager.syncUserPreferences();
}
selectedSortByCreated() {
this.setSortBy(SORT_KEY_CREATED_AT);
}
selectedSortByUpdated() {
this.setSortBy(SORT_KEY_CLIENT_UPDATED_AT);
}
selectedSortByTitle() {
this.setSortBy(SORT_KEY_TITLE);
}
toggleReverseSort() {
this.selectedMenuItem();
this.preferencesManager.setUserPrefValue(
PREF_SORT_NOTES_REVERSE,
!this.state.sortReverse
);
this.preferencesManager.syncUserPreferences();
}
setSortBy(type) {
this.preferencesManager.setUserPrefValue(
PREF_SORT_NOTES_BY,
type
);
this.preferencesManager.syncUserPreferences();
}
shouldShowTagsForNote(note) {
if (this.state.hideTags || note.content.protected) {
return false;
}
if (this.state.tag.content.isAllTag) {
return note.tags && note.tags.length > 0;
}
if (this.state.tag.isSmartTag()) {
return true;
}
/**
* Inside a tag, only show tags string if
* note contains tags other than this.state.tag
*/
return note.tags && note.tags.length > 1;
}
getSearchBar() {
return document.getElementById(ELEMENT_ID_SEARCH_BAR);
}
registerKeyboardShortcuts() {
/**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
* use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent.
*/
this.newNoteKeyObserver = this.keyboardManager.addKeyObserver({
key: 'n',
modifiers: [
KeyboardManager.KeyModifierMeta,
KeyboardManager.KeyModifierCtrl
],
onKeyDown: (event) => {
event.preventDefault();
this.createNewNote();
}
});
this.nextNoteKeyObserver = this.keyboardManager.addKeyObserver({
key: KeyboardManager.KeyDown,
elements: [
document.body,
this.getSearchBar()
],
onKeyDown: (event) => {
const searchBar = this.getSearchBar();
if (searchBar === document.activeElement) {
searchBar.blur();
}
this.selectNextNote();
}
});
this.nextNoteKeyObserver = this.keyboardManager.addKeyObserver({
key: KeyboardManager.KeyUp,
element: document.body,
onKeyDown: (event) => {
this.selectPreviousNote();
}
});
this.searchKeyObserver = this.keyboardManager.addKeyObserver({
key: "f",
modifiers: [
KeyboardManager.KeyModifierMeta,
KeyboardManager.KeyModifierShift
],
onKeyDown: (event) => {
const searchBar = this.getSearchBar();
if (searchBar) { searchBar.focus(); };
}
});
}
}
export class NotesPanel {
constructor() {
this.scope = {};
this.template = template;
this.replace = true;
this.controller = NotesCtrl;
this.controllerAs = 'self';
this.bindToController = true;
}
}

View file

@ -1,329 +0,0 @@
import { SFAuthManager } from 'snjs';
import { getPlatformString } from '@/utils';
import template from '%/root.pug';
import {
APP_STATE_EVENT_PANEL_RESIZED
} from '@/state';
import {
PANEL_NAME_NOTES,
PANEL_NAME_TAGS
} from '@/controllers/constants';
import {
STRING_SESSION_EXPIRED,
STRING_DEFAULT_FILE_ERROR,
StringSyncException
} from '@/strings';
/** How often to automatically sync, in milliseconds */
const AUTO_SYNC_INTERVAL = 30000;
class RootCtrl {
/* @ngInject */
constructor(
$location,
$rootScope,
$scope,
$timeout,
alertManager,
appState,
authManager,
dbManager,
modelManager,
passcodeManager,
preferencesManager,
themeManager /** Unused below, required to load globally */,
statusManager,
storageManager,
syncManager,
) {
this.$rootScope = $rootScope;
this.$scope = $scope;
this.$location = $location;
this.$timeout = $timeout;
this.dbManager = dbManager;
this.syncManager = syncManager;
this.statusManager = statusManager;
this.storageManager = storageManager;
this.appState = appState;
this.authManager = authManager;
this.modelManager = modelManager;
this.alertManager = alertManager;
this.preferencesManager = preferencesManager;
this.passcodeManager = passcodeManager;
this.defineRootScopeFunctions();
this.initializeStorageManager();
this.addAppStateObserver();
this.defaultLoad();
this.handleAutoSignInFromParams();
this.addDragDropHandlers();
}
defineRootScopeFunctions() {
this.$rootScope.lockApplication = () => {
/** Reloading wipes current objects from memory */
window.location.reload();
};
this.$rootScope.safeApply = (fn) => {
const phase = this.$scope.$root.$$phase;
if(phase === '$apply' || phase === '$digest') {
this.$scope.$eval(fn);
} else {
this.$scope.$apply(fn);
}
};
}
defaultLoad() {
this.$scope.platform = getPlatformString();
if(this.passcodeManager.isLocked()) {
this.$scope.needsUnlock = true;
} else {
this.loadAfterUnlock();
}
this.$scope.onSuccessfulUnlock = () => {
this.$timeout(() => {
this.$scope.needsUnlock = false;
this.loadAfterUnlock();
});
};
this.$scope.onUpdateAvailable = () => {
this.$rootScope.$broadcast('new-update-available');
};
}
initializeStorageManager() {
this.storageManager.initialize(
this.passcodeManager.hasPasscode(),
this.authManager.isEphemeralSession()
);
}
addAppStateObserver() {
this.appState.addObserver((eventName, data) => {
if(eventName === APP_STATE_EVENT_PANEL_RESIZED) {
if(data.panel === PANEL_NAME_NOTES) {
this.notesCollapsed = data.collapsed;
}
if(data.panel === PANEL_NAME_TAGS) {
this.tagsCollapsed = data.collapsed;
}
let appClass = "";
if(this.notesCollapsed) { appClass += "collapsed-notes"; }
if(this.tagsCollapsed) { appClass += " collapsed-tags"; }
this.$scope.appClass = appClass;
}
});
}
loadAfterUnlock() {
this.openDatabase();
this.authManager.loadInitialData();
this.preferencesManager.load();
this.addSyncStatusObserver();
this.configureKeyRequestHandler();
this.addSyncEventHandler();
this.addSignOutObserver();
this.loadLocalData();
}
openDatabase() {
this.dbManager.setLocked(false);
this.dbManager.openDatabase({
onUpgradeNeeded: () => {
/**
* New database, delete syncToken so that items
* can be refetched entirely from server
*/
this.syncManager.clearSyncToken();
this.syncManager.sync();
}
});
}
addSyncStatusObserver() {
this.syncStatusObserver = this.syncManager.registerSyncStatusObserver((status) => {
if(status.retrievedCount > 20) {
const text = `Downloading ${status.retrievedCount} items. Keep app open.`;
this.syncStatus = this.statusManager.replaceStatusWithString(
this.syncStatus,
text
);
this.showingDownloadStatus = true;
} else if(this.showingDownloadStatus) {
this.showingDownloadStatus = false;
const text = "Download Complete.";
this.syncStatus = this.statusManager.replaceStatusWithString(
this.syncStatus,
text
);
setTimeout(() => {
this.syncStatus = this.statusManager.removeStatus(this.syncStatus);
}, 2000);
} else if(status.total > 20) {
this.uploadSyncStatus = this.statusManager.replaceStatusWithString(
this.uploadSyncStatus,
`Syncing ${status.current}/${status.total} items...`
);
} else if(this.uploadSyncStatus) {
this.uploadSyncStatus = this.statusManager.removeStatus(
this.uploadSyncStatus
);
}
});
}
configureKeyRequestHandler() {
this.syncManager.setKeyRequestHandler(async () => {
const offline = this.authManager.offline();
const authParams = (
offline
? this.passcodeManager.passcodeAuthParams()
: await this.authManager.getAuthParams()
);
const keys = offline
? this.passcodeManager.keys()
: await this.authManager.keys();
return {
keys: keys,
offline: offline,
auth_params: authParams
};
});
}
addSyncEventHandler() {
let lastShownDate;
this.syncManager.addEventHandler((syncEvent, data) => {
this.$rootScope.$broadcast(
syncEvent,
data || {}
);
if(syncEvent === 'sync-session-invalid') {
/** Don't show repeatedly; at most 30 seconds in between */
const SHOW_INTERVAL = 30;
const lastShownSeconds = (new Date() - lastShownDate) / 1000;
if(!lastShownDate || lastShownSeconds > SHOW_INTERVAL) {
lastShownDate = new Date();
setTimeout(() => {
this.alertManager.alert({
text: STRING_SESSION_EXPIRED
});
}, 500);
}
} else if(syncEvent === 'sync-exception') {
this.alertManager.alert({
text: StringSyncException(data)
});
}
});
}
loadLocalData() {
const encryptionEnabled = this.authManager.user || this.passcodeManager.hasPasscode();
this.syncStatus = this.statusManager.addStatusFromString(
encryptionEnabled ? "Decrypting items..." : "Loading items..."
);
const incrementalCallback = (current, total) => {
const notesString = `${current}/${total} items...`;
const status = encryptionEnabled
? `Decrypting ${notesString}`
: `Loading ${notesString}`;
this.syncStatus = this.statusManager.replaceStatusWithString(
this.syncStatus,
status
);
};
this.syncManager.loadLocalItems({incrementalCallback}).then(() => {
this.$timeout(() => {
this.$rootScope.$broadcast("initial-data-loaded");
this.syncStatus = this.statusManager.replaceStatusWithString(
this.syncStatus,
"Syncing..."
);
this.syncManager.sync({
performIntegrityCheck: true
}).then(() => {
this.syncStatus = this.statusManager.removeStatus(this.syncStatus);
});
setInterval(() => {
this.syncManager.sync();
}, AUTO_SYNC_INTERVAL);
});
});
}
addSignOutObserver() {
this.authManager.addEventHandler((event) => {
if(event === SFAuthManager.DidSignOutEvent) {
this.modelManager.handleSignout();
this.syncManager.handleSignout();
}
});
}
addDragDropHandlers() {
/**
* Disable dragging and dropping of files (but allow text) into main SN interface.
* both 'dragover' and 'drop' are required to prevent dropping of files.
* This will not prevent extensions from receiving drop events.
*/
window.addEventListener('dragover', (event) => {
if (event.dataTransfer.files.length > 0) {
event.preventDefault();
}
}, false);
window.addEventListener('drop', (event) => {
if(event.dataTransfer.files.length > 0) {
event.preventDefault();
this.alertManager.alert({
text: STRING_DEFAULT_FILE_ERROR
});
}
}, false);
}
async handleAutoSignInFromParams() {
const params = this.$location.search();
const server = params.server;
const email = params.email;
const password = params.pw;
if (!server || !email || !password) return;
if (this.authManager.offline()) {
const { error } = await this.authManager.login(
server,
email,
password,
false,
false,
{}
);
if (!error) {
window.location.reload();
}
} else if (
this.authManager.user.email === email &&
(await this.syncManager.getServerURL()) === server
) {
/** Already signed in, return */
// eslint-disable-next-line no-useless-return
return;
} else {
this.authManager.signout(true);
window.location.reload();
}
}
}
export class Root {
constructor() {
this.template = template;
this.controller = RootCtrl;
}
}

View file

@ -1,284 +0,0 @@
import { SNNote, SNSmartTag } from 'snjs';
import template from '%/tags.pug';
import {
APP_STATE_EVENT_PREFERENCES_CHANGED,
APP_STATE_EVENT_TAG_CHANGED
} from '@/state';
import { PANEL_NAME_TAGS } from '@/controllers/constants';
import { PREF_TAGS_PANEL_WIDTH } from '@/services/preferencesManager';
import { STRING_DELETE_TAG } from '@/strings';
import { PureCtrl } from '@Controllers';
class TagsPanelCtrl extends PureCtrl {
/* @ngInject */
constructor(
$rootScope,
$timeout,
alertManager,
appState,
componentManager,
modelManager,
preferencesManager,
syncManager,
) {
super($timeout);
this.$rootScope = $rootScope;
this.alertManager = alertManager;
this.appState = appState;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.preferencesManager = preferencesManager;
this.syncManager = syncManager;
this.panelController = {};
this.addSyncEventHandler();
this.addAppStateObserver();
this.addMappingObserver();
this.loadPreferences();
this.registerComponentHandler();
this.state = {
smartTags: this.modelManager.getSmartTags(),
noteCounts: {}
};
}
$onInit() {
this.selectTag(this.state.smartTags[0]);
}
addSyncEventHandler() {
this.syncManager.addEventHandler(async (syncEvent, data) => {
if (
syncEvent === 'local-data-loaded' ||
syncEvent === 'sync:completed' ||
syncEvent === 'local-data-incremental-load'
) {
await this.setState({
tags: this.modelManager.tags,
smartTags: this.modelManager.getSmartTags()
});
this.reloadNoteCounts();
}
});
}
addAppStateObserver() {
this.appState.addObserver((eventName, data) => {
if (eventName === APP_STATE_EVENT_PREFERENCES_CHANGED) {
this.loadPreferences();
} else if (eventName === APP_STATE_EVENT_TAG_CHANGED) {
this.setState({
selectedTag: this.appState.getSelectedTag()
});
}
});
}
addMappingObserver() {
this.modelManager.addItemSyncObserver(
'tags-list-tags',
'Tag',
(allItems, validItems, deletedItems, source, sourceKey) => {
this.reloadNoteCounts();
if (!this.state.selectedTag) {
return;
}
/** If the selected tag has been deleted, revert to All view. */
const selectedTag = allItems.find((tag) => {
return tag.uuid === this.state.selectedTag.uuid;
});
if (selectedTag && selectedTag.deleted) {
this.selectTag(this.state.smartTags[0]);
}
}
);
}
reloadNoteCounts() {
let allTags = [];
if (this.state.tags) {
allTags = allTags.concat(this.state.tags);
}
if (this.state.smartTags) {
allTags = allTags.concat(this.state.smartTags);
}
const noteCounts = {};
for (const tag of allTags) {
const validNotes = SNNote.filterDummyNotes(tag.notes).filter((note) => {
return !note.archived && !note.content.trashed;
});
noteCounts[tag.uuid] = validNotes.length;
}
this.setState({
noteCounts: noteCounts
});
}
loadPreferences() {
const width = this.preferencesManager.getValue(PREF_TAGS_PANEL_WIDTH);
if (width) {
this.panelController.setWidth(width);
if (this.panelController.isCollapsed()) {
this.appState.panelDidResize({
name: PANEL_NAME_TAGS,
collapsed: this.panelController.isCollapsed()
});
}
}
}
onPanelResize = (newWidth, lastLeft, isAtMaxWidth, isCollapsed) => {
this.preferencesManager.setUserPrefValue(
PREF_TAGS_PANEL_WIDTH,
newWidth,
true
);
this.appState.panelDidResize({
name: PANEL_NAME_TAGS,
collapsed: isCollapsed
});
}
registerComponentHandler() {
this.componentManager.registerHandler({
identifier: 'tags',
areas: ['tags-list'],
activationHandler: (component) => {
this.component = component;
},
contextRequestHandler: (component) => {
return null;
},
actionHandler: (component, action, data) => {
if (action === 'select-item') {
if (data.item.content_type === 'Tag') {
const tag = this.modelManager.findItem(data.item.uuid);
if (tag) {
this.selectTag(tag);
}
} else if (data.item.content_type === 'SN|SmartTag') {
const smartTag = new SNSmartTag(data.item);
this.selectTag(smartTag);
}
} else if (action === 'clear-selection') {
this.selectTag(this.state.smartTags[0]);
}
}
});
}
async selectTag(tag) {
if (tag.isSmartTag()) {
Object.defineProperty(tag, 'notes', {
get: () => {
return this.modelManager.notesMatchingSmartTag(tag);
}
});
}
if (tag.content.conflict_of) {
tag.content.conflict_of = null;
this.modelManager.setItemDirty(tag);
this.syncManager.sync();
}
this.appState.setSelectedTag(tag);
}
clickedAddNewTag() {
if (this.state.editingTag) {
return;
}
const newTag = this.modelManager.createItem({
content_type: 'Tag'
});
this.setState({
previousTag: this.state.selectedTag,
selectedTag: newTag,
editingTag: newTag,
newTag: newTag
});
this.modelManager.addItem(newTag);
}
tagTitleDidChange(tag) {
this.setState({
editingTag: tag
});
}
async saveTag($event, tag) {
$event.target.blur();
await this.setState({
editingTag: null
});
if (!tag.title || tag.title.length === 0) {
if (this.state.editingTag) {
tag.title = this.editingOriginalName;
this.editingOriginalName = null;
} else if(this.state.newTag) {
this.modelManager.removeItemLocally(tag);
this.setState({
selectedTag: this.state.previousTag
});
}
this.setState({ newTag: null });
return;
}
this.editingOriginalName = null;
const matchingTag = this.modelManager.findTag(tag.title);
const alreadyExists = matchingTag && matchingTag !== tag;
if (this.state.newTag === tag && alreadyExists) {
this.alertManager.alert({
text: "A tag with this name already exists."
});
this.modelManager.removeItemLocally(tag);
this.setState({ newTag: null });
return;
}
this.modelManager.setItemDirty(tag);
this.syncManager.sync();
this.modelManager.resortTag(tag);
this.selectTag(tag);
this.setState({
newTag: null
});
}
async selectedRenameTag($event, tag) {
this.editingOriginalName = tag.title;
await this.setState({
editingTag: tag
});
document.getElementById('tag-' + tag.uuid).focus();
}
selectedDeleteTag(tag) {
this.removeTag(tag);
this.selectTag(this.state.smartTags[0]);
}
removeTag(tag) {
this.alertManager.confirm({
text: STRING_DELETE_TAG,
destructive: true,
onConfirm: () => {
this.modelManager.setItemToBeDeleted(tag);
this.syncManager.sync();
}
});
}
}
export class TagsPanel {
constructor() {
this.restrict = 'E';
this.scope = {};
this.template = template;
this.replace = true;
this.controller = TagsPanelCtrl;
this.controllerAs = 'self';
this.bindToController = true;
}
}

View file

@ -0,0 +1,219 @@
import { SNAlertService } from "snjs/dist/@types";
const STORE_NAME = 'items';
const READ_WRITE = 'readwrite';
const OUT_OF_SPACE =
'Unable to save changes locally because your device is out of space. ' +
'Please free up some disk space and try again, otherwise, your data may end ' +
'up in an inconsistent state.';
const DB_DELETION_BLOCKED =
'Your browser is blocking Standard Notes from deleting the local database. ' +
'Make sure there are no other open windows of this app and try again. ' +
'If the issue persists, please manually delete app data to sign out.';
const QUOTE_EXCEEDED_ERROR = 'QuotaExceededError';
export class Database {
private locked = true
private db?: IDBDatabase
constructor(
public databaseName: string,
private alertService: SNAlertService) {
}
public deinit() {
(this.alertService as any) = undefined;
this.db = undefined;
}
/**
* Relinquishes the lock and allows db operations to proceed
*/
public unlock() {
this.locked = false;
}
/**
* Opens the database natively, or returns the existing database object if already opened.
* @param onNewDatabase - Callback to invoke when a database has been created
* as part of the open process. This can happen on new application sessions, or if the
* browser deleted the database without the user being aware.
*/
public async openDatabase(onNewDatabase?: () => void): Promise<IDBDatabase | undefined> {
if (this.locked) {
throw Error('Attempting to open locked database');
}
if (this.db) {
return this.db;
}
const request = window.indexedDB.open(this.databaseName, 1);
return new Promise((resolve, reject) => {
request.onerror = (event) => {
const target = event!.target! as any;
if (target.errorCode) {
this.showAlert('Offline database issue: ' + target.errorCode);
} else {
this.displayOfflineAlert();
}
reject(new Error('Unable to open db'));
};
request.onblocked = (event) => {
reject(Error('IndexedDB open request blocked'));
};
request.onsuccess = (event) => {
const target = event!.target! as IDBOpenDBRequest;
const db = target.result;
db.onversionchange = () => {
db.close();
};
db.onerror = (errorEvent) => {
const target = errorEvent?.target as any;
throw Error('Database error: ' + target.errorCode);
};
this.db = db;
resolve(db);
};
request.onupgradeneeded = (event) => {
const target = event!.target! as IDBOpenDBRequest;
const db = target.result;
db.onversionchange = () => {
db.close();
};
/* Create an objectStore for this database */
const objectStore = db.createObjectStore(
STORE_NAME,
{ keyPath: 'uuid' }
);
objectStore.createIndex(
'uuid',
'uuid',
{ unique: true }
);
objectStore.transaction.oncomplete = () => {
/* Ready to store values in the newly created objectStore. */
if (db.version === 1 && onNewDatabase) {
onNewDatabase && onNewDatabase();
}
};
};
});
}
public async getAllPayloads(): Promise<any[]> {
const db = (await this.openDatabase())!;
return new Promise((resolve) => {
const objectStore =
db.transaction(STORE_NAME).
objectStore(STORE_NAME);
const payloads: any = [];
const cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = (event) => {
const target = event!.target! as any;
const cursor = target.result;
if (cursor) {
payloads.push(cursor.value);
cursor.continue();
} else {
resolve(payloads);
}
};
});
}
public async savePayload(payload: any): Promise<void> {
return this.savePayloads([payload]);
}
public async savePayloads(payloads: any[]): Promise<void> {
if (payloads.length === 0) {
return;
}
const db = (await this.openDatabase())!;
const transaction = db.transaction(STORE_NAME, READ_WRITE);
return new Promise((resolve, reject) => {
transaction.oncomplete = () => { };
transaction.onerror = (event) => {
const target = event!.target! as any;
this.showGenericError(target.error);
};
transaction.onabort = (event) => {
const target = event!.target! as any;
const error = target.error;
if (error.name === QUOTE_EXCEEDED_ERROR) {
this.showAlert(OUT_OF_SPACE);
} else {
this.showGenericError(error);
}
reject(error);
};
const objectStore = transaction.objectStore(STORE_NAME);
this.putItems(objectStore, payloads).then(resolve);
});
}
private async putItems(objectStore: IDBObjectStore, items: any[]): Promise<void> {
await Promise.all(items.map((item) => {
return new Promise((resolve) => {
const request = objectStore.put(item);
request.onerror = resolve;
request.onsuccess = resolve;
});
}));
}
public async deletePayload(uuid: string): Promise<void> {
const db = (await this.openDatabase())!;
return new Promise((resolve, reject) => {
const request =
db.transaction(STORE_NAME, READ_WRITE)
.objectStore(STORE_NAME)
.delete(uuid);
request.onsuccess = () => {
resolve();
};
request.onerror = reject;
});
}
public async clearAllPayloads(): Promise<void> {
const deleteRequest = window.indexedDB.deleteDatabase(this.databaseName);
return new Promise((resolve, reject) => {
deleteRequest.onerror = () => {
reject(Error('Error deleting database.'));
};
deleteRequest.onsuccess = () => {
this.db = undefined;
resolve();
};
deleteRequest.onblocked = (event) => {
this.showAlert(DB_DELETION_BLOCKED);
reject(Error('Delete request blocked'));
};
});
}
private showAlert(message: string) {
this.alertService!.alert(message);
}
private showGenericError(error: { code: number, name: string }) {
const message =
`Unable to save changes locally due to an unknown system issue. ` +
`Issue Code: ${error.code} Issue Name: ${error.name}.`;
this.showAlert(message);
}
private displayOfflineAlert() {
const message =
"There was an issue loading your offline database. This could happen for two reasons:" +
"\n\n1. You're in a private window in your browser. We can't save your data without " +
"access to the local database. Please use a non-private window." +
"\n\n2. You have two windows of the app open at the same time. " +
"Please close any other app instances and reload the page.";
this.alertService!.alert(message);
}
}

View file

@ -1,16 +0,0 @@
/* @ngInject */
export function autofocus($timeout) {
return {
restrict: 'A',
scope: {
shouldFocus: '='
},
link: function($scope, $element) {
$timeout(function() {
if ($scope.shouldFocus) {
$element[0].focus();
}
});
}
};
}

View file

@ -0,0 +1,19 @@
/* @ngInject */
export function autofocus($timeout: ng.ITimeoutService) {
return {
restrict: 'A',
scope: {
shouldFocus: '='
},
link: function (
$scope: ng.IScope,
$element: JQLite
) {
$timeout(() => {
if (($scope as any).shouldFocus) {
$element[0].focus();
}
});
}
};
}

View file

@ -1,29 +0,0 @@
/* @ngInject */
export function clickOutside($document) {
return {
restrict: 'A',
replace: false,
link: function($scope, $element, attrs) {
var didApplyClickOutside = false;
$element.bind('click', function(e) {
didApplyClickOutside = false;
if (attrs.isOpen) {
e.stopPropagation();
}
});
$document.bind('click', function() {
// Ignore click if on SKAlert
if (event.target.closest(".sk-modal")) {
return;
}
if (!didApplyClickOutside) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
});
}
};
}

View file

@ -0,0 +1,37 @@
/* @ngInject */
export function clickOutside($document: ng.IDocumentService) {
return {
restrict: 'A',
replace: false,
link($scope: ng.IScope, $element: JQLite, attrs: any) {
let didApplyClickOutside = false;
function onElementClick(event: JQueryEventObject) {
didApplyClickOutside = false;
if (attrs.isOpen) {
event.stopPropagation();
}
}
function onDocumentClick(event: JQueryEventObject) {
/** Ignore click if on SKAlert */
if (event.target.closest('.sk-modal')) {
return;
}
if (!didApplyClickOutside) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
};
$scope.$on('$destroy', () => {
attrs.clickOutside = undefined;
$element.unbind('click', onElementClick);
$document.unbind('click', onDocumentClick);
});
$element.bind('click', onElementClick);
$document.bind('click', onDocumentClick);
}
};
}

View file

@ -1,44 +0,0 @@
import angular from 'angular';
/* @ngInject */
export function delayHide($timeout) {
return {
restrict: 'A',
scope: {
show: '=',
delay: '@'
},
link: function(scope, elem, attrs) {
showElement(false);
// This is where all the magic happens!
// Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function(newVal) {
newVal ? showSpinner() : hideSpinner();
});
function showSpinner() {
if (scope.hidePromise) {
$timeout.cancel(scope.hidePromise);
scope.hidePromise = null;
}
showElement(true);
}
function hideSpinner() {
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
}
function showElement(show) {
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
}
function getDelay() {
var delay = parseInt(scope.delay);
return angular.isNumber(delay) ? delay : 200;
}
}
};
}

View file

@ -0,0 +1,45 @@
import angular from 'angular';
/* @ngInject */
export function delayHide($timeout: ng.ITimeoutService) {
return {
restrict: 'A',
scope: {
show: '=',
delay: '@'
},
link: function (scope: ng.IScope, elem: JQLite) {
const scopeAny = scope as any;
const showSpinner = () => {
if (scopeAny.hidePromise) {
$timeout.cancel(scopeAny.hidePromise);
scopeAny.hidePromise = null;
}
showElement(true);
}
const hideSpinner = () => {
scopeAny.hidePromise = $timeout(
showElement.bind(this as any, false),
getDelay()
);
}
const showElement = (show: boolean) => {
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
}
const getDelay = () => {
const delay = parseInt(scopeAny.delay);
return angular.isNumber(delay) ? delay : 200;
}
showElement(false);
// Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function (newVal) {
newVal ? showSpinner() : hideSpinner();
});
}
};
}

View file

@ -1,8 +1,8 @@
/* @ngInject */
export function elemReady($parse) {
export function elemReady($parse: ng.IParseService) {
return {
restrict: 'A',
link: function($scope, elem, attrs) {
link: function($scope: ng.IScope, elem: JQLite, attrs: any) {
elem.ready(function() {
$scope.$apply(function() {
var func = $parse(attrs.elemReady);

View file

@ -1,16 +0,0 @@
/* @ngInject */
export function fileChange() {
return {
restrict: 'A',
scope: {
handler: '&'
},
link: function(scope, element) {
element.on('change', function(event) {
scope.$apply(function() {
scope.handler({ files: event.target.files });
});
});
}
};
}

View file

@ -0,0 +1,19 @@
/* @ngInject */
export function fileChange() {
return {
restrict: 'A',
scope: {
handler: '&'
},
link: function (scope: ng.IScope, element: JQLite) {
element.on('change', (event) => {
scope.$apply(() => {
const files = (event.target as HTMLInputElement).files;
(scope as any).handler({
files: files
});
});
});
}
};
}

View file

@ -5,5 +5,5 @@ export { elemReady } from './elemReady';
export { fileChange } from './file-change';
export { infiniteScroll } from './infiniteScroll';
export { lowercase } from './lowercase';
export { selectOnClick } from './selectOnClick';
export { selectOnFocus } from './selectOnFocus';
export { snEnter } from './snEnter';

View file

@ -1,17 +0,0 @@
/* @ngInject */
export function infiniteScroll($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
const offset = parseInt(attrs.threshold) || 0;
const e = elem[0];
elem.on('scroll', function() {
if (
scope.$eval(attrs.canLoad) &&
e.scrollTop + e.offsetHeight >= e.scrollHeight - offset
) {
scope.$apply(attrs.infiniteScroll);
}
});
}
};
}

View file

@ -0,0 +1,26 @@
import { debounce } from '@/utils';
/* @ngInject */
export function infiniteScroll() {
return {
link: function (scope: ng.IScope, elem: JQLite, attrs: any) {
const scopeAny = scope as any;
const offset = parseInt(attrs.threshold) || 0;
const element = elem[0];
scopeAny.paginate = debounce(() => {
scope.$apply(attrs.infiniteScroll);
}, 10);
scopeAny.onScroll = () => {
if (
scope.$eval(attrs.canLoad) &&
element.scrollTop + element.offsetHeight >= element.scrollHeight - offset
) {
scopeAny.paginate();
}
};
elem.on('scroll', scopeAny.onScroll);
scope.$on('$destroy', () => {
elem.off('scroll', scopeAny.onScroll);;
});
}
};
}

View file

@ -1,19 +0,0 @@
/* @ngInject */
export function lowercase() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var lowercase = function(inputValue) {
if (inputValue === undefined) inputValue = '';
var lowercased = inputValue.toLowerCase();
if (lowercased !== inputValue) {
modelCtrl.$setViewValue(lowercased);
modelCtrl.$render();
}
return lowercased;
};
modelCtrl.$parsers.push(lowercase);
lowercase(scope[attrs.ngModel]);
}
};
}

View file

@ -0,0 +1,24 @@
/* @ngInject */
export function lowercase() {
return {
require: 'ngModel',
link: function (
scope: ng.IScope,
_: JQLite,
attrs: any,
ctrl: any
) {
const lowercase = (inputValue: string) => {
if (inputValue === undefined) inputValue = '';
const lowercased = inputValue.toLowerCase();
if (lowercased !== inputValue) {
ctrl.$setViewValue(lowercased);
ctrl.$render();
}
return lowercased;
};
ctrl.$parsers.push(lowercase);
lowercase((scope as any)[attrs.ngModel]);
}
};
}

View file

@ -1,14 +0,0 @@
/* @ngInject */
export function selectOnClick($window) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('focus', function() {
if (!$window.getSelection().toString()) {
/** Required for mobile Safari */
this.setSelectionRange(0, this.value.length);
}
});
}
};
}

View file

@ -0,0 +1,17 @@
/* @ngInject */
export function selectOnFocus($window: ng.IWindowService) {
return {
restrict: 'A',
link: function (scope: ng.IScope, element: JQLite) {
element.on('focus', () => {
if (!$window.getSelection()!.toString()) {
const input = element[0] as HTMLInputElement;
/** Allow text to populate */
setTimeout(() => {
input.setSelectionRange(0, input.value.length);
}, 0);
}
});
}
};
}

View file

@ -1,9 +1,13 @@
/* @ngInject */
export function snEnter() {
return function(scope, element, attrs) {
element.bind('keydown keypress', function(event) {
return function (
scope: ng.IScope,
element: JQLite,
attrs: any
) {
element.bind('keydown keypress', function (event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$apply(function () {
scope.$eval(attrs.snEnter, { event: event });
});

View file

@ -1,629 +0,0 @@
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
import { PrivilegesManager } from '@/services/privilegesManager';
import template from '%/directives/account-menu.pug';
import { protocolManager } from 'snjs';
import { PureCtrl } from '@Controllers';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_SIGN_OUT_CONFIRMATION,
STRING_ERROR_DECRYPTING_IMPORT,
STRING_E2E_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_IMPORT_SUCCESS,
STRING_REMOVE_PASSCODE_CONFIRMATION,
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
STRING_INVALID_IMPORT_FILE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
StringImportError
} from '@/strings';
import { STRING_IMPORT_FAILED_NEWER_BACKUP } from '../../strings';
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
const ELEMENT_NAME_AUTH_EMAIL = 'email';
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
class AccountMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$scope,
$rootScope,
$timeout,
alertManager,
archiveManager,
appVersion,
authManager,
modelManager,
passcodeManager,
privilegesManager,
storageManager,
syncManager,
) {
super($timeout);
this.$scope = $scope;
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.passcodeManager = passcodeManager;
this.privilegesManager = privilegesManager;
this.storageManager = storageManager;
this.syncManager = syncManager;
this.state = {
appVersion: 'v' + (window.electronAppVersion || appVersion),
user: this.authManager.user,
canAddPasscode: !this.authManager.isEphemeralSession(),
passcodeAutoLockOptions: this.passcodeManager.getAutoLockIntervalOptions(),
formData: {
mergeLocal: true,
ephemeral: false
},
mutable: {
backupEncrypted: this.encryptedBackupsAvailable()
}
};
this.syncStatus = this.syncManager.syncStatus;
this.syncManager.getServerURL().then((url) => {
this.setState({
server: url,
formData: { ...this.state.formData, url: url }
});
});
this.authManager.checkForSecurityUpdate().then((available) => {
this.setState({
securityUpdateAvailable: available
});
});
this.reloadAutoLockInterval();
}
$onInit() {
this.initProps({
closeFunction: this.closeFunction
});
}
close() {
this.$timeout(() => {
this.props.closeFunction()();
});
}
encryptedBackupsAvailable() {
return !isNullOrUndefined(this.authManager.user) || this.passcodeManager.hasPasscode();
}
submitMfaForm() {
const params = {
[this.state.formData.mfa.payload.mfa_key]: this.state.formData.userMfaCode
};
this.login(params);
}
blurAuthFields() {
const names = [
ELEMENT_NAME_AUTH_EMAIL,
ELEMENT_NAME_AUTH_PASSWORD,
ELEMENT_NAME_AUTH_PASSWORD_CONF
];
for(const name of names) {
const element = document.getElementsByName(name)[0];
if(element) {
element.blur();
}
}
}
submitAuthForm() {
if (!this.state.formData.email || !this.state.formData.user_password) {
return;
}
this.blurAuthFields();
if (this.state.formData.showLogin) {
this.login();
} else {
this.register();
}
}
async setFormDataState(formData) {
return this.setState({
formData: {
...this.state.formData,
...formData
}
});
}
async login(extraParams) {
/** Prevent a timed sync from occuring while signing in. */
this.syncManager.lockSyncing();
await this.setFormDataState({
status: STRING_GENERATING_LOGIN_KEYS,
authenticating: true
});
const response = await this.authManager.login(
this.state.formData.url,
this.state.formData.email,
this.state.formData.user_password,
this.state.formData.ephemeral,
this.state.formData.strictSignin,
extraParams
);
const hasError = !response || response.error;
if (!hasError) {
this.setFormDataState({
user_password: null
});
await this.onAuthSuccess();
this.syncManager.unlockSyncing();
this.syncManager.sync({ performIntegrityCheck: true });
return;
}
this.syncManager.unlockSyncing();
await this.setFormDataState({
status: null
});
const error = response
? response.error
: { message: "An unknown error occured." };
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
await this.setFormDataState({
showLogin: false,
mfa: error
});
} else {
await this.setFormDataState({
showLogin: true,
mfa: null
});
if (error.message) {
this.alertManager.alert({
text: error.message
});
}
}
await this.setFormDataState({
authenticating: false,
});
}
async register() {
const confirmation = this.state.formData.password_conf;
if (confirmation !== this.state.formData.user_password) {
this.alertManager.alert({
text: STRING_NON_MATCHING_PASSWORDS
});
return;
}
await this.setFormDataState({
confirmPassword: false,
status: STRING_GENERATING_REGISTER_KEYS,
authenticating: true
});
const response = await this.authManager.register(
this.state.formData.url,
this.state.formData.email,
this.state.formData.user_password,
this.state.formData.ephemeral
);
if (!response || response.error) {
await this.setFormDataState({
status: null
});
const error = response
? response.error
: { message: "An unknown error occured." };
await this.setFormDataState({
authenticating: false
});
this.alertManager.alert({
text: error.message
});
} else {
await this.onAuthSuccess();
this.syncManager.sync();
}
}
mergeLocalChanged() {
if (!this.state.formData.mergeLocal) {
this.alertManager.confirm({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
destructive: true,
onCancel: () => {
this.setFormDataState({
mergeLocal: true
});
}
});
}
}
async onAuthSuccess() {
if (this.state.formData.mergeLocal) {
this.$rootScope.$broadcast('major-data-change');
await this.clearDatabaseAndRewriteAllItems({ alternateUuids: true });
} else {
this.modelManager.removeAllItemsFromMemory();
await this.storageManager.clearAllModels();
}
await this.setFormDataState({
authenticating: false
});
this.syncManager.refreshErroredItems();
this.close();
}
openPasswordWizard(type) {
this.close();
this.authManager.presentPasswordWizard(type);
}
async openPrivilegesModal() {
this.close();
const run = () => {
this.privilegesManager.presentPrivilegesManagementModal();
};
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePrivileges
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePrivileges,
() => {
run();
}
);
} else {
run();
}
}
/**
* Allows IndexedDB unencrypted logs to be deleted
* `clearAllModels` will remove data from backing store,
* but not from working memory See:
* https://github.com/standardnotes/desktop/issues/131
*/
async clearDatabaseAndRewriteAllItems({ alternateUuids } = {}) {
await this.storageManager.clearAllModels();
await this.syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids);
}
destroyLocalData() {
this.alertManager.confirm({
text: STRING_SIGN_OUT_CONFIRMATION,
destructive: true,
onConfirm: async () => {
await this.authManager.signout(true);
window.location.reload();
}
});
}
async submitImportPassword() {
await this.performImport(
this.state.importData.data,
this.state.importData.password
);
}
async readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
try {
const data = JSON.parse(e.target.result);
resolve(data);
} catch (e) {
this.alertManager.alert({
text: STRING_INVALID_IMPORT_FILE
});
}
};
reader.readAsText(file);
});
}
/**
* @template
*/
async importFileSelected(files) {
const run = async () => {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
const version = data.version || data.auth_params?.version || data?.keyParams?.version;
if (version && !protocolManager.supportedVersions().includes(version)) {
this.setState({ importData: null });
this.alertManager.alert({ text: STRING_IMPORT_FAILED_NEWER_BACKUP });
return;
}
if (data.auth_params) {
await this.setState({
importData: {
...this.state.importData,
requestPassword: true,
data: data
}
});
const element = document.getElementById(
ELEMENT_ID_IMPORT_PASSWORD_INPUT
);
if (element) {
element.scrollIntoView(false);
}
} else {
await this.performImport(data, null);
}
};
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManageBackups
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManageBackups,
run
);
} else {
run();
}
}
async performImport(data, password) {
await this.setState({
importData: {
...this.state.importData,
loading: true
}
});
const errorCount = await this.importJSONData(data, password);
this.setState({
importData: null
});
if (errorCount > 0) {
const message = StringImportError({ errorCount: errorCount });
this.alertManager.alert({
text: message
});
} else {
this.alertManager.alert({
text: STRING_IMPORT_SUCCESS
});
}
}
async importJSONData(data, password) {
let errorCount = 0;
if (data.auth_params) {
const keys = await protocolManager.computeEncryptionKeysForUser(
password,
data.auth_params
);
try {
const throws = false;
await protocolManager.decryptMultipleItems(data.items, keys, throws);
const items = [];
for (const item of data.items) {
item.enc_item_key = null;
item.auth_hash = null;
if (item.errorDecrypting) {
errorCount++;
} else {
items.push(item);
}
}
data.items = items;
} catch (e) {
this.alertManager.alert({
text: STRING_ERROR_DECRYPTING_IMPORT
});
return;
}
}
const items = await this.modelManager.importItems(data.items);
for (const item of items) {
/**
* Don't want to activate any components during import process in
* case of exceptions breaking up the import proccess
*/
if (item.content_type === 'SN|Component') {
item.active = false;
}
}
this.syncManager.sync();
return errorCount;
}
async downloadDataArchive() {
this.archiveManager.downloadBackup(this.state.mutable.backupEncrypted);
this.close();
}
notesAndTagsCount() {
return this.modelManager.allItemsMatchingTypes([
'Note',
'Tag'
]).length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + "/" + length + " notes and tags encrypted";
}
encryptionEnabled() {
return this.passcodeManager.hasPasscode() || !this.authManager.offline();
}
encryptionSource() {
if (!this.authManager.offline()) {
return "Account keys";
} else if (this.passcodeManager.hasPasscode()) {
return "Local Passcode";
} else {
return null;
}
}
encryptionStatusString() {
if (!this.authManager.offline()) {
return STRING_E2E_ENABLED;
} else if (this.passcodeManager.hasPasscode()) {
return STRING_LOCAL_ENC_ENABLED;
} else {
return STRING_ENC_NOT_ENABLED;
}
}
async reloadAutoLockInterval() {
const interval = await this.passcodeManager.getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval
});
}
async selectAutoLockInterval(interval) {
const run = async () => {
await this.passcodeManager.setAutoLockInterval(interval);
this.reloadAutoLockInterval();
};
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
() => {
run();
}
);
} else {
run();
}
}
hidePasswordForm() {
this.setFormDataState({
showLogin: false,
showRegister: false,
user_password: null,
password_conf: null
});
}
hasPasscode() {
return this.passcodeManager.hasPasscode();
}
addPasscodeClicked() {
this.setFormDataState({
showPasscodeForm: true
});
}
submitPasscodeForm() {
const passcode = this.state.formData.passcode;
if (passcode !== this.state.formData.confirmPasscode) {
this.alertManager.alert({
text: STRING_NON_MATCHING_PASSCODES
});
return;
}
const func = this.state.formData.changingPasscode
? this.passcodeManager.changePasscode.bind(this.passcodeManager)
: this.passcodeManager.setPasscode.bind(this.passcodeManager);
func(passcode, async () => {
await this.setFormDataState({
passcode: null,
confirmPasscode: null,
showPasscodeForm: false
});
if (await this.authManager.offline()) {
this.$rootScope.$broadcast('major-data-change');
this.clearDatabaseAndRewriteAllItems();
}
});
}
async changePasscodePressed() {
const run = () => {
this.state.formData.changingPasscode = true;
this.addPasscodeClicked();
};
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
run
);
} else {
run();
}
}
async removePasscodePressed() {
const run = () => {
const signedIn = !this.authManager.offline();
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
if (!signedIn) {
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
}
this.alertManager.confirm({
text: message,
destructive: true,
onConfirm: () => {
this.passcodeManager.clearPasscode();
if (this.authManager.offline()) {
this.syncManager.markAllItemsDirtyAndSaveOffline();
}
}
});
};
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
run
);
} else {
run();
}
}
isDesktopApplication() {
return isDesktopApplication();
}
}
export class AccountMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = AccountMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
closeFunction: '&'
};
}
}

View file

@ -0,0 +1,608 @@
import { WebDirective } from './../../types';
import { isDesktopApplication, preventRefreshing } from '@/utils';
import template from '%/directives/account-menu.pug';
import { ProtectedAction, ContentType } from 'snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_SIGN_OUT_CONFIRMATION,
STRING_E2E_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_IMPORT_SUCCESS,
STRING_REMOVE_PASSCODE_CONFIRMATION,
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
STRING_INVALID_IMPORT_FILE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
StringImportError,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_UNSUPPORTED_BACKUP_FILE_VERSION
} from '@/strings';
import { SyncOpStatus } from 'snjs/dist/@types/services/sync/sync_op_status';
import { PasswordWizardType } from '@/types';
import { BackupFile } from 'snjs/dist/@types/services/protocol_service';
import { confirmDialog, alertDialog } from '@/services/alertService';
import { autorun, IReactionDisposer } from 'mobx';
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
const ELEMENT_NAME_AUTH_EMAIL = 'email';
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
type FormData = {
email: string
user_password: string
password_conf: string
confirmPassword: boolean
showLogin: boolean
showRegister: boolean
showPasscodeForm: boolean
strictSignin?: boolean
ephemeral: boolean
mergeLocal?: boolean
url: string
authenticating: boolean
status: string
passcode: string
confirmPasscode: string
changingPasscode: boolean
}
type AccountMenuState = {
formData: Partial<FormData>;
appVersion: string;
passcodeAutoLockOptions: any;
user: any;
mutable: any;
importData: any;
encryptionStatusString: string;
server: string;
encryptionEnabled: boolean;
selectedAutoLockInterval: any;
showBetaWarning: boolean;
}
class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
public appVersion: string
/** @template */
syncStatus?: SyncOpStatus
private closeFunction?: () => void
private removeBetaWarningListener?: IReactionDisposer
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
appVersion: string,
) {
super($timeout);
this.appVersion = appVersion;
}
/** @override */
getInitialState() {
return {
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(),
user: this.application!.getUser(),
formData: {
mergeLocal: true,
ephemeral: false,
},
mutable: {},
showBetaWarning: false,
} as AccountMenuState;
}
getState() {
return this.state as AccountMenuState;
}
async onAppKeyChange() {
super.onAppKeyChange();
this.setState(this.refreshedCredentialState());
}
async onAppLaunch() {
super.onAppLaunch();
this.setState(this.refreshedCredentialState());
this.loadHost();
this.reloadAutoLockInterval();
this.refreshEncryptionStatus();
}
refreshedCredentialState() {
return {
user: this.application!.getUser(),
canAddPasscode: !this.application!.isEphemeralSession(),
hasPasscode: this.application!.hasPasscode(),
showPasscodeForm: false
};
}
$onInit() {
super.$onInit();
this.syncStatus = this.application!.getSyncStatus();
this.removeBetaWarningListener = autorun(() => {
this.setState({
showBetaWarning: this.appState.showBetaWarning
});
});
}
deinit() {
this.removeBetaWarningListener?.();
super.deinit();
}
close() {
this.$timeout(() => {
this.closeFunction?.();
});
}
async loadHost() {
const host = await this.application!.getHost();
this.setState({
server: host,
formData: {
...this.getState().formData,
url: host
}
});
}
onHostInputChange() {
const url = this.getState().formData.url!;
this.application!.setHost(url);
}
refreshEncryptionStatus() {
const hasUser = this.application!.hasAccount();
const hasPasscode = this.application!.hasPasscode();
const encryptionEnabled = hasUser || hasPasscode;
this.setState({
encryptionStatusString: hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED,
encryptionEnabled,
mutable: {
...this.getState().mutable,
backupEncrypted: encryptionEnabled
}
});
}
submitMfaForm() {
this.login();
}
blurAuthFields() {
const names = [
ELEMENT_NAME_AUTH_EMAIL,
ELEMENT_NAME_AUTH_PASSWORD,
ELEMENT_NAME_AUTH_PASSWORD_CONF
];
for (const name of names) {
const element = document.getElementsByName(name)[0];
if (element) {
element.blur();
}
}
}
submitAuthForm() {
if (!this.getState().formData.email || !this.getState().formData.user_password) {
return;
}
this.blurAuthFields();
if (this.getState().formData.showLogin) {
this.login();
} else {
this.register();
}
}
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.getState().formData,
...formData
}
});
}
async login() {
await this.setFormDataState({
status: STRING_GENERATING_LOGIN_KEYS,
authenticating: true
});
const formData = this.getState().formData;
const response = await this.application!.signIn(
formData.email!,
formData.user_password!,
formData.strictSignin,
formData.ephemeral,
formData.mergeLocal
);
const error = response.error;
if (!error) {
await this.setFormDataState({
authenticating: false,
user_password: undefined
});
this.close();
return;
}
await this.setFormDataState({
showLogin: true,
status: undefined,
user_password: undefined
});
if (error.message) {
this.application!.alertService!.alert(error.message);
}
await this.setFormDataState({
authenticating: false
});
}
async register() {
const confirmation = this.getState().formData.password_conf;
if (confirmation !== this.getState().formData.user_password) {
this.application!.alertService!.alert(
STRING_NON_MATCHING_PASSWORDS
);
return;
}
await this.setFormDataState({
confirmPassword: false,
status: STRING_GENERATING_REGISTER_KEYS,
authenticating: true
});
const response = await this.application!.register(
this.getState().formData.email!,
this.getState().formData.user_password!,
this.getState().formData.ephemeral,
this.getState().formData.mergeLocal
);
const error = response.error;
if (error) {
await this.setFormDataState({
status: undefined
});
await this.setFormDataState({
authenticating: false
});
this.application!.alertService!.alert(
error.message
);
} else {
await this.setFormDataState({ authenticating: false });
this.close();
}
}
async mergeLocalChanged() {
if (!this.getState().formData.mergeLocal) {
this.setFormDataState({
mergeLocal: !(await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger'
}))
});
}
}
openPasswordWizard() {
this.close();
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
}
async openPrivilegesModal() {
const run = () => {
this.application!.presentPrivilegesManagementModal();
this.close();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePrivileges
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePrivileges,
() => {
run();
}
);
} else {
run();
}
}
async destroyLocalData() {
if (await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: "danger"
})) {
this.application.signOut();
}
}
async submitImportPassword() {
await this.performImport(
this.getState().importData.data,
this.getState().importData.password
);
}
showRegister() {
this.setFormDataState({
showRegister: true
});
}
async readFile(file: File): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target!.result as string);
resolve(data);
} catch (e) {
this.application!.alertService!.alert(
STRING_INVALID_IMPORT_FILE
);
}
};
reader.readAsText(file);
});
}
/**
* @template
*/
async importFileSelected(files: File[]) {
const run = async () => {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.version || data.auth_params || data.keyParams) {
const version = data.version || data.keyParams?.version || data.auth_params?.version;
if (
!this.application!.protocolService!.supportedVersions().includes(version)
) {
await this.setState({ importData: null });
alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
return;
}
if (data.keyParams || data.auth_params) {
await this.setState({
importData: {
...this.getState().importData,
requestPassword: true,
data,
}
});
const element = document.getElementById(
ELEMENT_ID_IMPORT_PASSWORD_INPUT
);
if (element) {
element.scrollIntoView(false);
}
} else {
await this.performImport(data, undefined);
}
} else {
await this.performImport(data, undefined);
}
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManageBackups
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManageBackups,
run
);
} else {
run();
}
}
async performImport(data: BackupFile, password?: string) {
await this.setState({
importData: {
...this.getState().importData,
loading: true
}
});
const result = await this.application!.importData(
data,
password
);
this.setState({
importData: null
});
if ('error' in result) {
this.application!.alertService!.alert(
result.error
);
} else if (result.errorCount) {
const message = StringImportError(result.errorCount);
this.application!.alertService!.alert(
message
);
} else {
this.application!.alertService!.alert(
STRING_IMPORT_SUCCESS
);
}
}
async downloadDataArchive() {
this.application!.getArchiveService().downloadBackup(this.getState().mutable.backupEncrypted);
}
notesAndTagsCount() {
return this.application!.getItems(
[
ContentType.Note,
ContentType.Tag
]
).length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + "/" + length + " notes and tags encrypted";
}
async reloadAutoLockInterval() {
const interval = await this.application!.getAutolockService().getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval
});
}
async selectAutoLockInterval(interval: number) {
const run = async () => {
await this.application!.getAutolockService().setAutoLockInterval(interval);
this.reloadAutoLockInterval();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
() => {
run();
}
);
} else {
run();
}
}
hidePasswordForm() {
this.setFormDataState({
showLogin: false,
showRegister: false,
user_password: undefined,
password_conf: undefined
});
}
hasPasscode() {
return this.application!.hasPasscode();
}
addPasscodeClicked() {
this.setFormDataState({
showPasscodeForm: true
});
}
async submitPasscodeForm() {
const passcode = this.getState().formData.passcode!;
if (passcode !== this.getState().formData.confirmPasscode!) {
this.application!.alertService!.alert(
STRING_NON_MATCHING_PASSCODES
);
return;
}
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => {
if (this.application!.hasPasscode()) {
await this.application!.changePasscode(passcode);
} else {
await this.application!.setPasscode(passcode);
}
});
this.setFormDataState({
passcode: undefined,
confirmPasscode: undefined,
showPasscodeForm: false
});
this.refreshEncryptionStatus();
}
async changePasscodePressed() {
const run = () => {
this.getState().formData.changingPasscode = true;
this.addPasscodeClicked();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
run
);
} else {
run();
}
}
async removePasscodePressed() {
const run = async () => {
const signedIn = this.application!.hasAccount();
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
if (!signedIn) {
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
}
if (await confirmDialog({
text: message,
confirmButtonStyle: 'danger'
})) {
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
await this.application.getAutolockService().deleteAutolockPreference();
await this.application!.removePasscode();
await this.reloadAutoLockInterval();
});
this.refreshEncryptionStatus();
}
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
run
);
} else {
run();
}
}
isDesktopApplication() {
return isDesktopApplication();
}
}
export class AccountMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = AccountMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
closeFunction: '&',
application: '='
};
}
}

View file

@ -1,104 +0,0 @@
import template from '%/directives/actions-menu.pug';
import { PureCtrl } from '@Controllers';
class ActionsMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$scope,
$timeout,
actionsManager,
) {
super($timeout);
this.$timeout = $timeout;
this.actionsManager = actionsManager;
}
$onInit() {
this.initProps({
item: this.item
});
this.loadExtensions();
};
async loadExtensions() {
const extensions = this.actionsManager.extensions.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
for (const extension of extensions) {
extension.loading = true;
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
extension.loading = false;
}
this.setState({
extensions: extensions
});
}
async executeAction(action, extension) {
if (action.verb === 'nested') {
if (!action.subrows) {
action.subrows = this.subRowsForAction(action, extension);
} else {
action.subrows = null;
}
return;
}
action.running = true;
const result = await this.actionsManager.executeAction(
action,
extension,
this.props.item
);
if (action.error) {
return;
}
action.running = false;
this.handleActionResult(action, result);
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
this.setState({
extensions: this.state.extensions
});
}
handleActionResult(action, result) {
switch (action.verb) {
case 'render': {
const item = result.item;
this.actionsManager.presentRevisionPreviewModal(
item.uuid,
item.content
);
}
}
}
subRowsForAction(parentAction, extension) {
if (!parentAction.subactions) {
return null;
}
return parentAction.subactions.map((subaction) => {
return {
onClick: () => {
this.executeAction(subaction, extension, parentAction);
},
label: subaction.label,
subtitle: subaction.desc,
spinnerClass: subaction.running ? 'info' : null
};
});
}
}
export class ActionsMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.replace = true;
this.controller = ActionsMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
item: '='
};
}
}

View file

@ -0,0 +1,285 @@
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from '%/directives/actions-menu.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { SNItem, Action, SNActionsExtension, UuidString } from 'snjs/dist/@types';
import { ActionResponse } from 'snjs';
import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension';
import { autorun, IReactionDisposer } from 'mobx';
type ActionsMenuScope = {
application: WebApplication
item: SNItem
}
type ActionSubRow = {
onClick: () => void
label: string
subtitle: string
spinnerClass?: string
}
type ExtensionState = {
loading: boolean
error: boolean
}
type ActionsMenuState = {
extensions: SNActionsExtension[]
extensionsState: Record<UuidString, ExtensionState>
selectedActionId?: number
menu: {
uuid: UuidString,
name: string,
loading: boolean,
error: boolean,
hidden: boolean,
actions: (Action & {
subrows?: ActionSubRow[]
})[]
}[]
}
class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements ActionsMenuScope {
application!: WebApplication
item!: SNItem
private removeHiddenExtensionsListener?: IReactionDisposer;
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService
) {
super($timeout);
}
$onInit() {
super.$onInit();
this.initProps({
item: this.item
});
this.loadExtensions();
this.removeHiddenExtensionsListener = autorun(() => {
this.rebuildMenu({
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
});
});
};
deinit() {
this.removeHiddenExtensionsListener?.();
}
/** @override */
getInitialState() {
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
let extensionsState: Record<UuidString, ExtensionState> = {};
extensions.map((extension) => {
extensionsState[extension.uuid] = {
loading: false,
error: false,
};
});
return {
extensions,
extensionsState,
hiddenExtensions: {},
menu: [],
};
}
rebuildMenu({
extensions = this.state.extensions,
extensionsState = this.state.extensionsState,
selectedActionId = this.state.selectedActionId,
hiddenExtensions = this.appState.actionsMenu.hiddenExtensions,
} = {}) {
return this.setState({
extensions,
extensionsState,
selectedActionId,
menu: extensions.map(extension => {
const state = extensionsState[extension.uuid];
const hidden = hiddenExtensions[extension.uuid];
return {
uuid: extension.uuid,
name: extension.name,
loading: state?.loading ?? false,
error: state?.error ?? false,
hidden: hidden ?? false,
deprecation: extension.deprecation!,
actions: extension.actionsWithContextForItem(this.item).map(action => {
if (action.id === selectedActionId) {
return {
...action,
subrows: this.subRowsForAction(action, extension)
}
} else {
return action;
}
})
};
})
});
}
async loadExtensions() {
await Promise.all(this.state.extensions.map(async (extension: SNActionsExtension) => {
this.setLoadingExtension(extension.uuid, true);
const updatedExtension = await this.application.actionsManager!.loadExtensionInContextOfItem(
extension,
this.item
);
if (updatedExtension) {
await this.updateExtension(updatedExtension!);
} else {
this.setErrorExtension(extension.uuid, true);
}
this.setLoadingExtension(extension.uuid, false);
}));
}
async executeAction(action: Action, extensionUuid: UuidString) {
if (action.verb === 'nested') {
this.rebuildMenu({
selectedActionId: action.id
});
return;
}
const extension = this.application.findItem(extensionUuid) as SNActionsExtension;
await this.updateAction(action, extension, { running: true });
const response = await this.application.actionsManager!.runAction(
action,
this.item,
async () => {
/** @todo */
return '';
}
);
if (response.error) {
await this.updateAction(action, extension, { error: true });
return;
}
await this.updateAction(action, extension, { running: false });
this.handleActionResponse(action, response);
await this.reloadExtension(extension);
}
handleActionResponse(action: Action, result: ActionResponse) {
switch (action.verb) {
case 'render': {
const item = result.item;
this.application.presentRevisionPreviewModal(
item.uuid,
item.content
);
}
}
}
private subRowsForAction(parentAction: Action, extension: Pick<SNActionsExtension, 'uuid'>): ActionSubRow[] | undefined {
if (!parentAction.subactions) {
return undefined;
}
return parentAction.subactions.map((subaction) => {
return {
id: subaction.id,
onClick: () => {
this.executeAction(subaction, extension.uuid);
},
label: subaction.label,
subtitle: subaction.desc,
spinnerClass: subaction.running ? 'info' : undefined
};
});
}
private async updateAction(
action: Action,
extension: SNActionsExtension,
params: {
running?: boolean
error?: boolean
}
) {
const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => {
const extensionMutator = mutator as ActionsExtensionMutator;
extensionMutator.actions = extension!.actions.map((act) => {
if (act && params && act.verb === action.verb && act.url === action.url) {
return {
...action,
running: params?.running,
error: params?.error,
} as Action;
}
return act;
});
}) as SNActionsExtension;
await this.updateExtension(updatedExtension);
}
private async updateExtension(extension: SNActionsExtension) {
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
if (extension.uuid === ext.uuid) {
return extension;
}
return ext;
});
await this.rebuildMenu({
extensions
});
}
private async reloadExtension(extension: SNActionsExtension) {
const extensionInContext = await this.application.actionsManager!.loadExtensionInContextOfItem(
extension,
this.item
);
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
if (extension.uuid === ext.uuid) {
return extensionInContext!;
}
return ext;
});
this.rebuildMenu({
extensions
});
}
public toggleExtensionVisibility(extensionUuid: UuidString) {
this.appState.actionsMenu.toggleExtensionVisibility(extensionUuid);
}
private setLoadingExtension(extensionUuid: UuidString, value = false) {
const { extensionsState } = this.state;
extensionsState[extensionUuid].loading = value;
this.rebuildMenu({
extensionsState
});
}
private setErrorExtension(extensionUuid: UuidString, value = false) {
const { extensionsState } = this.state;
extensionsState[extensionUuid].error = value;
this.rebuildMenu({
extensionsState
});
}
}
export class ActionsMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.replace = true;
this.controller = ActionsMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
item: '=',
application: '='
};
}
}

View file

@ -1,34 +0,0 @@
import template from '%/directives/component-modal.pug';
export class ComponentModalCtrl {
/* @ngInject */
constructor($scope, $element) {
this.$element = $element;
this.$scope = $scope;
}
dismiss(callback) {
this.$element.remove();
this.$scope.$destroy();
if(this.onDismiss && this.onDismiss()) {
this.onDismiss()(this.component);
}
callback && callback();
}
}
export class ComponentModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = ComponentModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
show: '=',
component: '=',
callback: '=',
onDismiss: '&'
};
}
}

View file

@ -0,0 +1,64 @@
import { WebApplication } from '@/ui_models/application';
import { SNComponent, LiveItem } from 'snjs';
import { WebDirective } from './../../types';
import template from '%/directives/component-modal.pug';
export type ComponentModalScope = {
componentUuid: string
onDismiss: () => void
application: WebApplication
}
export class ComponentModalCtrl implements ComponentModalScope {
$element: JQLite
componentUuid!: string
onDismiss!: () => void
application!: WebApplication
liveComponent!: LiveItem<SNComponent>
component!: SNComponent
/* @ngInject */
constructor($element: JQLite) {
this.$element = $element;
}
$onInit() {
this.liveComponent = new LiveItem(
this.componentUuid,
this.application,
(component) => {
this.component = component;
}
);
this.application.componentGroup.activateComponent(this.component);
}
$onDestroy() {
this.application.componentGroup.deactivateComponent(this.component);
this.liveComponent.deinit();
}
dismiss() {
this.onDismiss && this.onDismiss();
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class ComponentModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = ComponentModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
componentUuid: '=',
onDismiss: '&',
application: '='
};
}
}

View file

@ -0,0 +1,269 @@
import { RootScopeMessages } from './../../messages';
import { WebApplication } from '@/ui_models/application';
import { SNComponent, ComponentAction, LiveItem } from 'snjs';
import { WebDirective } from './../../types';
import template from '%/directives/component-view.pug';
import { isDesktopApplication } from '../../utils';
/**
* The maximum amount of time we'll wait for a component
* to load before displaying error
*/
const MaxLoadThreshold = 4000;
const VisibilityChangeKey = 'visibilitychange';
interface ComponentViewScope {
componentUuid: string
onLoad?: (component: SNComponent) => void
application: WebApplication
}
class ComponentViewCtrl implements ComponentViewScope {
/** @scope */
onLoad?: (component: SNComponent) => void
componentUuid!: string
templateComponent!: SNComponent
application!: WebApplication
liveComponent!: LiveItem<SNComponent>
private $rootScope: ng.IRootScopeService
private $timeout: ng.ITimeoutService
private componentValid = true
private cleanUpOn: () => void
private unregisterComponentHandler!: () => void
private unregisterDesktopObserver!: () => void
private issueLoading = false
public reloading = false
private expired = false
private loading = false
private didAttemptReload = false
public error: 'offline-restricted' | 'url-missing' | undefined
private loadTimeout: any
/* @ngInject */
constructor(
$scope: ng.IScope,
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService,
) {
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.cleanUpOn = $scope.$on('ext-reload-complete', () => {
this.reloadStatus(false);
});
/** To allow for registering events */
this.onVisibilityChange = this.onVisibilityChange.bind(this);
}
$onDestroy() {
if(this.application.componentManager) {
/** Component manager Can be destroyed already via locking */
this.application.componentManager.onComponentIframeDestroyed(this.component.uuid);
if(this.templateComponent) {
this.application.componentManager.removeTemporaryTemplateComponent(this.templateComponent);
}
}
if(this.liveComponent) {
this.liveComponent.deinit();
}
this.cleanUpOn();
(this.cleanUpOn as any) = undefined;
this.unregisterComponentHandler();
(this.unregisterComponentHandler as any) = undefined;
this.unregisterDesktopObserver();
(this.unregisterDesktopObserver as any) = undefined;
(this.templateComponent as any) = undefined;
(this.liveComponent as any) = undefined;
(this.application as any) = undefined;
(this.onVisibilityChange as any) = undefined;
this.onLoad = undefined;
document.removeEventListener(
VisibilityChangeKey,
this.onVisibilityChange
);
}
$onInit() {
if(this.componentUuid) {
this.liveComponent = new LiveItem(this.componentUuid, this.application);
} else {
this.application.componentManager.addTemporaryTemplateComponent(this.templateComponent);
}
this.registerComponentHandlers();
this.registerPackageUpdateObserver();
}
get component() {
return this.templateComponent || this.liveComponent?.item;
}
/** @template */
public onIframeInit() {
/** Perform in timeout required so that dynamic iframe id is set (based on ctrl values) */
this.$timeout(() => {
this.loadComponent();
});
}
private loadComponent() {
if (!this.component) {
throw Error('Component view is missing component');
}
if (!this.component.active && !this.component.isEditor()) {
/** Editors don't need to be active to be displayed */
throw Error('Component view component must be active');
}
const iframe = this.application.componentManager!.iframeForComponent(
this.component.uuid
);
if (!iframe) {
return;
}
this.loading = true;
if (this.loadTimeout) {
this.$timeout.cancel(this.loadTimeout);
}
this.loadTimeout = this.$timeout(() => {
this.handleIframeLoadTimeout();
}, MaxLoadThreshold);
iframe.onload = () => {
this.reloadStatus();
this.handleIframeLoad(iframe);
};
}
private registerPackageUpdateObserver() {
this.unregisterDesktopObserver = this.application.getDesktopService()
.registerUpdateObserver((component: SNComponent) => {
if (component.uuid === this.component.uuid && component.active) {
this.reloadIframe();
}
});
}
private registerComponentHandlers() {
this.unregisterComponentHandler = this.application.componentManager!.registerHandler({
identifier: 'component-view-' + Math.random(),
areas: [this.component.area],
actionHandler: (component, action, data) => {
if (action === ComponentAction.SetSize) {
this.application.componentManager!.handleSetSizeEvent(component, data);
}
}
});
}
private reloadIframe() {
this.$timeout(() => {
this.reloading = true;
this.$timeout(() => {
this.reloading = false;
});
})
}
private onVisibilityChange() {
if (document.visibilityState === 'hidden') {
return;
}
if (this.issueLoading) {
this.reloadIframe();
}
}
public reloadStatus(doManualReload = true) {
const component = this.component;
const offlineRestricted = component.offlineOnly && !isDesktopApplication();
const hasUrlError = function () {
if (isDesktopApplication()) {
return !component.local_url && !component.hasValidHostedUrl();
} else {
return !component.hasValidHostedUrl();
}
}();
this.expired = component.valid_until && component.valid_until <= new Date();
const readonlyState = this.application.componentManager!
.getReadonlyStateForComponent(component);
if (!readonlyState.lockReadonly) {
this.application.componentManager!
.setReadonlyStateForComponent(component, this.expired);
}
this.componentValid = !offlineRestricted && !hasUrlError;
if (!this.componentValid) {
this.loading = false;
}
if (offlineRestricted) {
this.error = 'offline-restricted';
} else if (hasUrlError) {
this.error = 'url-missing';
} else {
this.error = undefined;
}
if (this.expired && doManualReload) {
this.$rootScope.$broadcast(RootScopeMessages.ReloadExtendedData);
}
}
private async handleIframeLoadTimeout() {
if (this.loading) {
this.loading = false;
this.issueLoading = true;
if (!this.didAttemptReload) {
this.didAttemptReload = true;
this.reloadIframe();
} else {
document.addEventListener(
VisibilityChangeKey,
this.onVisibilityChange
);
}
}
}
private async handleIframeLoad(iframe: HTMLIFrameElement) {
let desktopError = false;
if (isDesktopApplication()) {
try {
/** Accessing iframe.contentWindow.origin only allowed in desktop app. */
if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
desktopError = true;
}
} catch (e) { }
}
this.$timeout.cancel(this.loadTimeout);
await this.application.componentManager!.registerComponentWindow(
this.component,
iframe.contentWindow!
);
const avoidFlickerTimeout = 7;
this.$timeout(() => {
this.loading = false;
// eslint-disable-next-line no-unneeded-ternary
this.issueLoading = desktopError ? true : false;
this.onLoad && this.onLoad(this.component!);
}, avoidFlickerTimeout);
}
/** @template */
public getUrl() {
const url = this.application.componentManager!.urlForComponent(this.component);
return url;
}
}
export class ComponentView extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.scope = {
componentUuid: '=',
templateComponent: '=?',
onLoad: '=?',
application: '='
};
this.controller = ComponentViewCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
}

View file

@ -1,95 +0,0 @@
import template from '%/directives/conflict-resolution-modal.pug';
class ConflictResolutionCtrl {
/* @ngInject */
constructor(
$element,
alertManager,
archiveManager,
modelManager,
syncManager
) {
this.$element = $element;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
}
$onInit() {
this.contentType = this.item1.content_type;
this.item1Content = this.createContentString(this.item1);
this.item2Content = this.createContentString(this.item2);
};
createContentString(item) {
const data = Object.assign({
created_at: item.created_at,
updated_at: item.updated_at
}, item.content);
return JSON.stringify(data, null, 2);
}
keepItem1() {
this.alertManager.confirm({
text: `Are you sure you want to delete the item on the right?`,
destructive: true,
onConfirm: () => {
this.modelManager.setItemToBeDeleted(this.item2);
this.syncManager.sync().then(() => {
this.applyCallback();
});
this.dismiss();
}
});
}
keepItem2() {
this.alertManager.confirm({
text: `Are you sure you want to delete the item on the left?`,
destructive: true,
onConfirm: () => {
this.modelManager.setItemToBeDeleted(this.item1);
this.syncManager.sync().then(() => {
this.applyCallback();
});
this.dismiss();
}
});
}
keepBoth() {
this.applyCallback();
this.dismiss();
}
export() {
this.archiveManager.downloadBackupOfItems(
[this.item1, this.item2],
true
);
}
applyCallback() {
this.callback && this.callback();
}
dismiss() {
this.$element.remove();
}
}
export class ConflictResolutionModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = ConflictResolutionCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
item1: '=',
item2: '=',
callback: '='
};
}
}

View file

@ -1,110 +0,0 @@
import { isDesktopApplication } from '@/utils';
import template from '%/directives/editor-menu.pug';
import { PureCtrl } from '@Controllers';
class EditorMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$timeout,
componentManager,
modelManager,
syncManager,
) {
super($timeout);
this.$timeout = $timeout;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.state = {
isDesktop: isDesktopApplication()
};
}
$onInit() {
const editors = this.componentManager.componentsForArea('editor-editor')
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
this.setState({
editors: editors,
defaultEditor: defaultEditor
});
};
selectComponent(component) {
if(component) {
if(component.content.conflict_of) {
component.content.conflict_of = null;
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
}
}
this.$timeout(() => {
this.callback()(component);
});
}
toggleDefaultForEditor(editor) {
if(this.state.defaultEditor === editor) {
this.removeEditorDefault(editor);
} else {
this.makeEditorDefault(editor);
}
}
offlineAvailableForComponent(component) {
return component.local_url && this.state.isDesktop;
}
makeEditorDefault(component) {
const currentDefault = this.componentManager
.componentsForArea('editor-editor')
.filter((e) => e.isDefaultEditor())[0];
if(currentDefault) {
currentDefault.setAppDataItem('defaultEditor', false);
this.modelManager.setItemDirty(currentDefault);
}
component.setAppDataItem('defaultEditor', true);
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.setState({
defaultEditor: component
});
}
removeEditorDefault(component) {
component.setAppDataItem('defaultEditor', false);
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.setState({
defaultEditor: null
});
}
shouldDisplayRunningLocallyLabel(component) {
if(!component.runningLocally) {
return false;
}
if(component === this.selectedEditor) {
return true;
} else {
return false;
}
}
}
export class EditorMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = EditorMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
callback: '&',
selectedEditor: '=',
currentItem: '='
};
}
}

View file

@ -0,0 +1,127 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import { SNComponent, SNItem, ComponentArea } from 'snjs';
import { isDesktopApplication } from '@/utils';
import template from '%/directives/editor-menu.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { ComponentMutator } from 'snjs/dist/@types/models';
interface EditorMenuScope {
callback: (component: SNComponent) => void
selectedEditorUuid: string
currentItem: SNItem
application: WebApplication
}
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
callback!: () => (component: SNComponent) => void
selectedEditorUuid!: string
currentItem!: SNItem
application!: WebApplication
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
) {
super($timeout);
this.state = {
isDesktop: isDesktopApplication()
};
}
public isEditorSelected(editor: SNComponent) {
if(!this.selectedEditorUuid) {
return false;
}
return this.selectedEditorUuid === editor.uuid;
}
public isEditorDefault(editor: SNComponent) {
return this.state.defaultEditor?.uuid === editor.uuid;
}
$onInit() {
super.$onInit();
const editors = this.application.componentManager!.componentsForArea(ComponentArea.Editor)
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
this.setState({
editors: editors,
defaultEditor: defaultEditor
});
};
selectComponent(component: SNComponent) {
if (component) {
if (component.conflictOf) {
this.application.changeAndSaveItem(component.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
}
}
this.$timeout(() => {
this.callback()(component);
});
}
toggleDefaultForEditor(editor: SNComponent) {
if (this.state.defaultEditor === editor) {
this.removeEditorDefault(editor);
} else {
this.makeEditorDefault(editor);
}
}
offlineAvailableForComponent(component: SNComponent) {
return component.local_url && this.state.isDesktop;
}
makeEditorDefault(component: SNComponent) {
const currentDefault = this.application.componentManager!
.componentsForArea(ComponentArea.Editor)
.filter((e) => e.isDefaultEditor())[0];
if (currentDefault) {
this.application.changeItem(currentDefault.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.defaultEditor = false;
})
}
this.application.changeAndSaveItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.defaultEditor = true;
});
this.setState({
defaultEditor: component
});
}
removeEditorDefault(component: SNComponent) {
this.application.changeAndSaveItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.defaultEditor = false;
});
this.setState({
defaultEditor: null
});
}
}
export class EditorMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = EditorMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
callback: '&',
selectedEditorUuid: '=',
currentItem: '=',
application: '='
};
}
}

View file

@ -0,0 +1,191 @@
import { WebDirective } from '../../types';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/history-menu.pug';
import { SNItem, ItemHistoryEntry } from 'snjs/dist/@types';
import { PureViewCtrl } from '@/views';
import { ItemSessionHistory } from 'snjs/dist/@types/services/history/session/item_session_history';
import { RevisionListEntry, SingleRevision } from 'snjs/dist/@types/services/api/responses';
import { confirmDialog } from '@/services/alertService';
type HistoryState = {
fetchingRemoteHistory: boolean
}
interface HistoryScope {
application: WebApplication
item: SNItem
}
class HistoryMenuCtrl extends PureViewCtrl<{}, HistoryState> implements HistoryScope {
diskEnabled = false
autoOptimize = false
application!: WebApplication
item!: SNItem
sessionHistory?: ItemSessionHistory
remoteHistory?: RevisionListEntry[]
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService
) {
super($timeout);
this.state = {
fetchingRemoteHistory: false
};
}
$onInit() {
super.$onInit();
this.reloadSessionHistory();
this.fetchRemoteHistory();
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
this.autoOptimize = this.application.historyManager!.isAutoOptimizeEnabled();
}
reloadSessionHistory() {
this.sessionHistory = this.application.historyManager!.sessionHistoryForItem(this.item);
}
get isfetchingRemoteHistory() {
return this.state.fetchingRemoteHistory;
}
set fetchingRemoteHistory(value: boolean) {
this.setState({
fetchingRemoteHistory: value
});
}
async fetchRemoteHistory() {
this.fetchingRemoteHistory = true;
this.remoteHistory = await this.application.historyManager!.remoteHistoryForItem(this.item)
.finally(() => {
this.fetchingRemoteHistory = false;
});
}
async openSessionRevision(revision: ItemHistoryEntry) {
this.application.presentRevisionPreviewModal(
revision.payload.uuid,
revision.payload.content
);
}
async openRemoteRevision(revision: RevisionListEntry) {
this.fetchingRemoteHistory = true;
const remoteRevision = await this.application.historyManager!.fetchRemoteRevision(this.item.uuid, revision);
this.fetchingRemoteHistory = false;
if (!remoteRevision) {
this.application.alertService!.alert("The remote revision could not be loaded. Please try again later.");
return;
}
this.application.presentRevisionPreviewModal(
remoteRevision.payload.uuid,
remoteRevision.payload.content
);
}
classForSessionRevision(revision: ItemHistoryEntry) {
const vector = revision.operationVector();
if (vector === 0) {
return 'default';
} else if (vector === 1) {
return 'success';
} else if (vector === -1) {
return 'danger';
}
}
clearItemSessionHistory() {
confirmDialog({
text: "Are you sure you want to delete the local session history for this note?",
confirmButtonStyle: "danger"
}).then((confirmed) => {
if (!confirmed) {
return;
}
this.application.historyManager!.clearHistoryForItem(this.item).then(() => {
this.$timeout(() => {
this.reloadSessionHistory();
});
});
});
}
clearAllSessionHistory() {
confirmDialog({
text: "Are you sure you want to delete the local session history for all notes?",
confirmButtonStyle: "danger"
}).then((confirmed) => {
if (!confirmed) {
return;
}
this.application.historyManager!.clearAllHistory().then(() => {
this.$timeout(() => {
this.reloadSessionHistory();
});
});
});
}
get sessionHistoryEntries() {
return this.sessionHistory?.entries;
}
get remoteHistoryEntries() {
return this.remoteHistory;
}
toggleSessionHistoryDiskSaving() {
const run = () => {
this.application.historyManager!.toggleDiskSaving().then(() => {
this.$timeout(() => {
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
});
});
};
if (!this.application.historyManager!.isDiskEnabled()) {
confirmDialog({
text: "Are you sure you want to save history to disk? This will decrease general " +
"performance, especially as you type. You are advised to disable this feature " +
"if you experience any lagging.",
confirmButtonStyle: "danger"
}).then((confirmed) => {
if (confirmed) {
run();
}
});
} else {
run();
}
}
toggleSessionHistoryAutoOptimize() {
this.application.historyManager!.toggleAutoOptimize().then(() => {
this.$timeout(() => {
this.autoOptimize = this.application.historyManager!.autoOptimize;
});
});
}
previewRemoteHistoryTitle(revision: SingleRevision) {
const createdAt = revision.created_at!;
return new Date(createdAt).toLocaleString();
}
}
export class HistoryMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = HistoryMenuCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
item: '=',
application: '='
};
}
}

View file

@ -2,7 +2,6 @@ export { AccountMenu } from './accountMenu';
export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
export { ComponentView } from './componentView';
export { ConflictResolutionModal } from './conflictResolutionModal';
export { EditorMenu } from './editorMenu';
export { InputModal } from './inputModal';
export { MenuRow } from './menuRow';
@ -12,5 +11,5 @@ export { PermissionsModal } from './permissionsModal';
export { PrivilegesAuthModal } from './privilegesAuthModal';
export { PrivilegesManagementModal } from './privilegesManagementModal';
export { RevisionPreviewModal } from './revisionPreviewModal';
export { SessionHistoryMenu } from './sessionHistoryMenu';
export { HistoryMenu } from './historyMenu';
export { SyncResolutionMenu } from './syncResolutionMenu';

View file

@ -1,37 +0,0 @@
import template from '%/directives/input-modal.pug';
class InputModalCtrl {
/* @ngInject */
constructor($scope, $element) {
this.$element = $element;
this.formData = {};
}
dismiss() {
this.$element.remove();
this.$scope.$destroy();
}
submit() {
this.callback()(this.formData.input);
this.dismiss();
}
}
export class InputModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = InputModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '=',
title: '=',
message: '=',
placeholder: '=',
callback: '&'
};
}
}

View file

@ -0,0 +1,53 @@
import { WebDirective } from './../../types';
import template from '%/directives/input-modal.pug';
export interface InputModalScope extends Partial<ng.IScope> {
type: string
title: string
message: string
callback: (value: string) => void
}
class InputModalCtrl implements InputModalScope {
$element: JQLite
type!: string
title!: string
message!: string
callback!: (value: string) => void
formData = { input: '' }
/* @ngInject */
constructor($element: JQLite) {
this.$element = $element;
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
submit() {
this.callback(this.formData.input);
this.dismiss();
}
}
export class InputModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = InputModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '=',
title: '=',
message: '=',
callback: '&'
};
}
}

View file

@ -1,8 +1,13 @@
import { WebDirective } from './../../types';
import template from '%/directives/menu-row.pug';
class MenuRowCtrl {
onClick($event) {
disabled!: boolean
action!: () => void
buttonAction!: () => void
onClick($event: Event) {
if(this.disabled) {
return;
}
@ -10,7 +15,7 @@ class MenuRowCtrl {
this.action();
}
clickAccessoryButton($event) {
clickAccessoryButton($event: Event) {
if(this.disabled) {
return;
}
@ -19,8 +24,9 @@ class MenuRowCtrl {
}
}
export class MenuRow {
export class MenuRow extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.transclude = true;
this.template = template;

View file

@ -1,71 +1,141 @@
import { PanelPuppet, WebDirective } from './../../types';
import angular from 'angular';
import template from '%/directives/panel-resizer.pug';
import { debounce } from '@/utils';
const PANEL_SIDE_RIGHT = 'right';
const PANEL_SIDE_LEFT = 'left';
const MOUSE_EVENT_MOVE = 'mousemove';
const MOUSE_EVENT_DOWN = 'mousedown';
const MOUSE_EVENT_UP = 'mouseup';
enum PanelSide {
Right = 'right',
Left = 'left'
};
enum MouseEventType {
Move = 'mousemove',
Down = 'mousedown',
Up = 'mouseup'
};
enum CssClass {
Hoverable = 'hoverable',
AlwaysVisible = 'always-visible',
Dragging = 'dragging',
NoSelection = 'no-selection',
Collapsed = 'collapsed',
AnimateOpacity = 'animate-opacity',
};
const WINDOW_EVENT_RESIZE = 'resize';
const PANEL_CSS_CLASS_HOVERABLE = 'hoverable';
const PANEL_CSS_CLASS_ALWAYS_VISIBLE = 'always-visible';
const PANEL_CSS_CLASS_DRAGGING = 'dragging';
const PANEL_CSS_CLASS_NO_SELECTION = 'no-selection';
const PANEL_CSS_CLASS_COLLAPSED = 'collapsed';
const PANEL_CSS_CLASS_ANIMATE_OPACITY = 'animate-opacity';
type ResizeFinishCallback = (
lastWidth: number,
lastLeft: number,
isMaxWidth: boolean,
isCollapsed: boolean
) => void
interface PanelResizerScope {
alwaysVisible: boolean
collapsable: boolean
control: PanelPuppet
defaultWidth: number
hoverable: boolean
index: number
minWidth: number
onResizeFinish: () => ResizeFinishCallback
panelId: string
property: PanelSide
}
class PanelResizerCtrl implements PanelResizerScope {
/** @scope */
alwaysVisible!: boolean
collapsable!: boolean
control!: PanelPuppet
defaultWidth!: number
hoverable!: boolean
index!: number
minWidth!: number
onResizeFinish!: () => ResizeFinishCallback
panelId!: string
property!: PanelSide
$compile: ng.ICompileService
$element: JQLite
$timeout: ng.ITimeoutService
panel!: HTMLElement
resizerColumn!: HTMLElement
currentMinWidth = 0
pressed = false
startWidth = 0
lastDownX = 0
collapsed = false
lastWidth = 0
startLeft = 0
lastLeft = 0
appFrame?: DOMRect
widthBeforeLastDblClick = 0
overlay?: JQLite
class PanelResizerCtrl {
/* @ngInject */
constructor(
$compile,
$element,
$scope,
$timeout,
$compile: ng.ICompileService,
$element: JQLite,
$timeout: ng.ITimeoutService,
) {
this.$compile = $compile;
this.$element = $element;
this.$scope = $scope;
this.$timeout = $timeout;
/** To allow for registering events */
this.handleResize = debounce(this.handleResize.bind(this), 250);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
}
$onInit() {
this.configureControl();
this.configureDefaults();
this.addDoubleClickHandler();
this.reloadDefaultValues();
this.addMouseDownListener();
this.addMouseMoveListener();
this.addMouseUpListener();
this.configureControl();
this.addDoubleClickHandler();
this.resizerColumn.addEventListener(MouseEventType.Down, this.onMouseDown);
document.addEventListener(MouseEventType.Move, this.onMouseMove);
document.addEventListener(MouseEventType.Up, this.onMouseUp);
}
$onDestroy() {
(this.onResizeFinish as any) = undefined;
(this.control as any) = undefined;
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
document.removeEventListener(MouseEventType.Up, this.onMouseUp);
this.resizerColumn.removeEventListener(MouseEventType.Down, this.onMouseDown);
(this.handleResize as any) = undefined;
(this.onMouseMove as any) = undefined;
(this.onMouseUp as any) = undefined;
(this.onMouseDown as any) = undefined;
}
configureControl() {
this.control.setWidth = (value) => {
this.setWidth(value, true);
};
this.control.setLeft = (value) => {
this.setLeft(value);
};
this.control.flash = () => {
this.flash();
};
this.control.isCollapsed = () => {
return this.isCollapsed();
};
this.control.ready = true;
this.control.onReady!();
}
configureDefaults() {
this.panel = document.getElementById(this.panelId);
this.panel = document.getElementById(this.panelId)!;
if (!this.panel) {
console.error('Panel not found for', this.panelId);
return;
}
this.resizerColumn = this.$element[0];
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth;
this.pressed = false;
@ -75,36 +145,34 @@ class PanelResizerCtrl {
this.lastWidth = this.startWidth;
this.startLeft = this.panel.offsetLeft;
this.lastLeft = this.startLeft;
this.appFrame = null;
this.appFrame = undefined;
this.widthBeforeLastDblClick = 0;
if (this.property === PANEL_SIDE_RIGHT) {
if (this.property === PanelSide.Right) {
this.configureRightPanel();
}
if (this.alwaysVisible) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ALWAYS_VISIBLE);
this.resizerColumn.classList.add(CssClass.AlwaysVisible);
}
if (this.hoverable) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_HOVERABLE);
this.resizerColumn.classList.add(CssClass.Hoverable);
}
}
configureRightPanel() {
const handleResize = debounce(event => {
this.reloadDefaultValues();
this.handleWidthEvent();
this.$timeout(() => {
this.finishSettingWidth();
});
}, 250);
window.addEventListener(WINDOW_EVENT_RESIZE, handleResize);
this.$scope.$on('$destroy', () => {
window.removeEventListener(WINDOW_EVENT_RESIZE, handleResize);
window.addEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
}
handleResize() {
this.reloadDefaultValues();
this.handleWidthEvent();
this.$timeout(() => {
this.finishSettingWidth();
});
}
getParentRect() {
return this.panel.parentNode.getBoundingClientRect();
const node = this.panel!.parentNode! as HTMLElement;
return node.getBoundingClientRect();
}
reloadDefaultValues() {
@ -112,7 +180,7 @@ class PanelResizerCtrl {
? this.getParentRect().width
: this.panel.scrollWidth;
this.lastWidth = this.startWidth;
this.appFrame = document.getElementById('app').getBoundingClientRect();
this.appFrame = document.getElementById('app')!.getBoundingClientRect();
}
addDoubleClickHandler() {
@ -125,9 +193,7 @@ class PanelResizerCtrl {
this.widthBeforeLastDblClick = this.lastWidth;
this.setWidth(this.currentMinWidth);
}
this.finishSettingWidth();
const newCollapseState = !preClickCollapseState;
this.onResizeFinish()(
this.lastWidth,
@ -139,53 +205,65 @@ class PanelResizerCtrl {
};
}
addMouseDownListener() {
this.resizerColumn.addEventListener(MOUSE_EVENT_DOWN, (event) => {
this.addInvisibleOverlay();
this.pressed = true;
this.lastDownX = event.clientX;
this.startWidth = this.panel.scrollWidth;
this.startLeft = this.panel.offsetLeft;
this.panel.classList.add(PANEL_CSS_CLASS_NO_SELECTION);
if (this.hoverable) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_DRAGGING);
}
});
onMouseDown(event: MouseEvent) {
this.addInvisibleOverlay();
this.pressed = true;
this.lastDownX = event.clientX;
this.startWidth = this.panel.scrollWidth;
this.startLeft = this.panel.offsetLeft;
this.panel.classList.add(CssClass.NoSelection);
if (this.hoverable) {
this.resizerColumn.classList.add(CssClass.Dragging);
}
}
addMouseMoveListener() {
document.addEventListener(MOUSE_EVENT_MOVE, (event) => {
if (!this.pressed) {
return;
}
event.preventDefault();
if (this.property && this.property === PANEL_SIDE_LEFT) {
this.handleLeftEvent(event);
} else {
this.handleWidthEvent(event);
}
});
onMouseUp() {
this.removeInvisibleOverlay();
if (!this.pressed) {
return;
}
this.pressed = false;
this.resizerColumn.classList.remove(CssClass.Dragging);
this.panel.classList.remove(CssClass.NoSelection);
const isMaxWidth = this.isAtMaxWidth();
if (this.onResizeFinish) {
this.onResizeFinish()(
this.lastWidth,
this.lastLeft,
isMaxWidth,
this.isCollapsed()
);
}
this.finishSettingWidth();
}
handleWidthEvent(event) {
onMouseMove(event: MouseEvent) {
if (!this.pressed) {
return;
}
event.preventDefault();
if (this.property && this.property === PanelSide.Left) {
this.handleLeftEvent(event);
} else {
this.handleWidthEvent(event);
}
}
handleWidthEvent(event?: MouseEvent) {
let x;
if (event) {
x = event.clientX;
x = event!.clientX;
} else {
/** Coming from resize event */
x = 0;
this.lastDownX = 0;
}
const deltaX = x - this.lastDownX;
const newWidth = this.startWidth + deltaX;
this.setWidth(newWidth, false);
if (this.onResize()) {
this.onResize()(this.lastWidth, this.panel);
}
}
handleLeftEvent(event) {
handleLeftEvent(event: MouseEvent) {
const panelRect = this.panel.getBoundingClientRect();
const x = event.clientX || panelRect.x;
let deltaX = x - this.lastDownX;
@ -205,34 +283,13 @@ class PanelResizerCtrl {
if (newLeft + newWidth > parentRect.width) {
newLeft = parentRect.width - newWidth;
}
this.setLeft(newLeft, false);
this.setLeft(newLeft);
this.setWidth(newWidth, false);
}
addMouseUpListener() {
document.addEventListener(MOUSE_EVENT_UP, event => {
this.removeInvisibleOverlay();
if (this.pressed) {
this.pressed = false;
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_DRAGGING);
this.panel.classList.remove(PANEL_CSS_CLASS_NO_SELECTION);
const isMaxWidth = this.isAtMaxWidth();
if (this.onResizeFinish) {
this.onResizeFinish()(
this.lastWidth,
this.lastLeft,
isMaxWidth,
this.isCollapsed()
);
}
this.finishSettingWidth();
}
});
}
isAtMaxWidth() {
return (
Math.round(this.lastWidth + this.lastLeft) ===
Math.round(this.lastWidth + this.lastLeft) ===
Math.round(this.getParentRect().width)
);
}
@ -241,7 +298,7 @@ class PanelResizerCtrl {
return this.lastWidth <= this.currentMinWidth;
}
setWidth(width, finish) {
setWidth(width: number, finish = false) {
if (width < this.currentMinWidth) {
width = this.currentMinWidth;
}
@ -250,7 +307,7 @@ class PanelResizerCtrl {
width = parentRect.width;
}
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
const maxWidth = this.appFrame!.width - this.panel.getBoundingClientRect().x;
if (width > maxWidth) {
width = maxWidth;
}
@ -267,7 +324,7 @@ class PanelResizerCtrl {
}
}
setLeft(left) {
setLeft(left: number) {
this.panel.style.left = left + 'px';
this.lastLeft = left;
}
@ -279,9 +336,9 @@ class PanelResizerCtrl {
this.collapsed = this.isCollapsed();
if (this.collapsed) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_COLLAPSED);
this.resizerColumn.classList.add(CssClass.Collapsed);
} else {
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_COLLAPSED);
this.resizerColumn.classList.remove(CssClass.Collapsed);
}
}
@ -295,28 +352,29 @@ class PanelResizerCtrl {
if (this.overlay) {
return;
}
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this.$scope);
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this as any);
angular.element(document.body).prepend(this.overlay);
}
removeInvisibleOverlay() {
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
this.overlay = undefined;
}
}
flash() {
const FLASH_DURATION = 3000;
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ANIMATE_OPACITY);
this.resizerColumn.classList.add(CssClass.AnimateOpacity);
this.$timeout(() => {
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_ANIMATE_OPACITY);
this.resizerColumn.classList.remove(CssClass.AnimateOpacity);
}, FLASH_DURATION);
}
}
export class PanelResizer {
export class PanelResizer extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PanelResizerCtrl;
@ -330,7 +388,6 @@ export class PanelResizer {
hoverable: '=',
index: '=',
minWidth: '=',
onResize: '&',
onResizeFinish: '&',
panelId: '=',
property: '='

View file

@ -1,302 +0,0 @@
import { protocolManager } from 'snjs';
import template from '%/directives/password-wizard.pug';
import { STRING_FAILED_PASSWORD_CHANGE } from '@/strings';
const DEFAULT_CONTINUE_TITLE = "Continue";
const Steps = {
IntroStep: 0,
BackupStep: 1,
SignoutStep: 2,
PasswordStep: 3,
SyncStep: 4,
FinishStep: 5
};
class PasswordWizardCtrl {
/* @ngInject */
constructor(
$element,
$scope,
$timeout,
alertManager,
archiveManager,
authManager,
modelManager,
syncManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.$scope = $scope;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.registerWindowUnloadStopper();
}
$onInit() {
this.syncStatus = this.syncManager.syncStatus;
this.formData = {};
this.configureDefaults();
}
configureDefaults() {
if (this.type === 'change-pw') {
this.title = "Change Password";
this.changePassword = true;
} else if (this.type === 'upgrade-security') {
this.title = "Security Update";
this.securityUpdate = true;
}
this.continueTitle = DEFAULT_CONTINUE_TITLE;
this.step = Steps.IntroStep;
}
/** Confirms with user before closing tab */
registerWindowUnloadStopper() {
window.onbeforeunload = (e) => {
return true;
};
this.$scope.$on("$destroy", () => {
window.onbeforeunload = null;
});
}
titleForStep(step) {
switch (step) {
case Steps.BackupStep:
return "Download a backup of your data";
case Steps.SignoutStep:
return "Sign out of all your devices";
case Steps.PasswordStep:
return this.changePassword
? "Password information"
: "Enter your current password";
case Steps.SyncStep:
return "Encrypt and sync data with new keys";
case Steps.FinishStep:
return "Sign back in to your devices";
default:
return null;
}
}
async nextStep() {
if (this.lockContinue || this.isContinuing) {
return;
}
this.isContinuing = true;
if (this.step === Steps.FinishStep) {
this.dismiss();
return;
}
const next = () => {
this.step++;
this.initializeStep(this.step);
this.isContinuing = false;
};
const preprocessor = this.preprocessorForStep(this.step);
if (preprocessor) {
await preprocessor().then((success) => {
if(success) {
next();
} else {
this.$timeout(() => {
this.isContinuing = false;
});
}
}).catch(() => {
this.isContinuing = false;
});
} else {
next();
}
}
preprocessorForStep(step) {
if (step === Steps.PasswordStep) {
return async () => {
this.showSpinner = true;
this.continueTitle = "Generating Keys...";
const success = await this.validateCurrentPassword();
this.showSpinner = false;
this.continueTitle = DEFAULT_CONTINUE_TITLE;
return success;
};
}
}
async initializeStep(step) {
if (step === Steps.SyncStep) {
await this.initializeSyncingStep();
} else if (step === Steps.FinishStep) {
this.continueTitle = "Finish";
}
}
async initializeSyncingStep() {
this.lockContinue = true;
this.formData.status = "Processing encryption keys...";
this.formData.processing = true;
const passwordSuccess = await this.processPasswordChange();
this.formData.statusError = !passwordSuccess;
this.formData.processing = passwordSuccess;
if(!passwordSuccess) {
this.formData.status = "Unable to process your password. Please try again.";
return;
}
this.formData.status = "Encrypting and syncing data with new keys...";
const syncSuccess = await this.resyncData();
this.formData.statusError = !syncSuccess;
this.formData.processing = !syncSuccess;
if (syncSuccess) {
this.lockContinue = false;
if (this.changePassword) {
this.formData.status = "Successfully changed password and synced all items.";
} else if (this.securityUpdate) {
this.formData.status = "Successfully performed security update and synced all items.";
}
} else {
this.formData.status = STRING_FAILED_PASSWORD_CHANGE;
}
}
async validateCurrentPassword() {
const currentPassword = this.formData.currentPassword;
const newPass = this.securityUpdate ? currentPassword : this.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.alertManager.alert({
text: "Please enter your current password."
});
return false;
}
if (this.changePassword) {
if (!newPass || newPass.length === 0) {
this.alertManager.alert({
text: "Please enter a new password."
});
return false;
}
if (newPass !== this.formData.newPasswordConfirmation) {
this.alertManager.alert({
text: "Your new password does not match its confirmation."
});
this.formData.status = null;
return false;
}
}
if (!this.authManager.user.email) {
this.alertManager.alert({
text: "We don't have your email stored. Please log out then log back in to fix this issue."
});
this.formData.status = null;
return false;
}
const minLength = this.authManager.getMinPasswordLength();
if (!this.securityUpdate && newPass.length < minLength) {
const message = `Your password must be at least ${minLength} characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.`;
this.alertManager.alert({
text: message
});
return false;
}
/** Validate current password */
const authParams = await this.authManager.getAuthParams();
const password = this.formData.currentPassword;
const keys = await protocolManager.computeEncryptionKeysForUser(
password,
authParams
);
const success = keys.mk === (await this.authManager.keys()).mk;
if (success) {
this.currentServerPw = keys.pw;
} else {
this.alertManager.alert({
text: "The current password you entered is not correct. Please try again."
});
}
return success;
}
async resyncData() {
await this.modelManager.setAllItemsDirty();
const response = await this.syncManager.sync();
if (!response || response.error) {
this.alertManager.alert({
text: STRING_FAILED_PASSWORD_CHANGE
});
return false;
} else {
return true;
}
}
async processPasswordChange() {
const newUserPassword = this.securityUpdate
? this.formData.currentPassword
: this.formData.newPassword;
const currentServerPw = this.currentServerPw;
const results = await protocolManager.generateInitialKeysAndAuthParamsForUser(
this.authManager.user.email,
newUserPassword
);
const newKeys = results.keys;
const newAuthParams = results.authParams;
/**
* Perform a sync beforehand to pull in any last minutes changes before we change
* the encryption key (and thus cant decrypt new changes).
*/
await this.syncManager.sync();
const response = await this.authManager.changePassword(
await this.syncManager.getServerURL(),
this.authManager.user.email,
currentServerPw,
newKeys,
newAuthParams
);
if (response.error) {
this.alertManager.alert({
text: response.error.message
? response.error.message
: "There was an error changing your password. Please try again."
});
return false;
} else {
return true;
}
}
downloadBackup(encrypted) {
this.archiveManager.downloadBackup(encrypted);
}
dismiss() {
if (this.lockContinue) {
this.alertManager.alert({
text: "Cannot close window until pending tasks are complete."
});
} else {
this.$element.remove();
this.$scope.$destroy();
}
}
}
export class PasswordWizard {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PasswordWizardCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '='
};
}
}

View file

@ -0,0 +1,239 @@
import { WebApplication } from '@/ui_models/application';
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
import template from '%/directives/password-wizard.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
const DEFAULT_CONTINUE_TITLE = "Continue";
enum Steps {
PasswordStep = 1,
FinishStep = 2
};
type FormData = {
currentPassword?: string,
newPassword?: string,
newPasswordConfirmation?: string,
status?: string
}
type State = {
lockContinue: boolean
formData: FormData,
continueTitle: string,
step: Steps,
title: string,
showSpinner: boolean
processing: boolean
}
type Props = {
type: PasswordWizardType,
changePassword: boolean,
securityUpdate: boolean
}
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
$element: JQLite
application!: WebApplication
type!: PasswordWizardType
isContinuing = false
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService,
) {
super($timeout);
this.$element = $element;
this.registerWindowUnloadStopper();
}
$onInit() {
super.$onInit();
this.initProps({
type: this.type,
changePassword: this.type === PasswordWizardType.ChangePassword,
securityUpdate: this.type === PasswordWizardType.AccountUpgrade
});
this.setState({
formData: {},
continueTitle: DEFAULT_CONTINUE_TITLE,
step: Steps.PasswordStep,
title: this.props.changePassword ? 'Change Password' : 'Account Update'
});
}
$onDestroy() {
super.$onDestroy();
window.onbeforeunload = null;
}
/** Confirms with user before closing tab */
registerWindowUnloadStopper() {
window.onbeforeunload = () => {
return true;
};
}
resetContinueState() {
this.setState({
showSpinner: false,
continueTitle: DEFAULT_CONTINUE_TITLE
});
this.isContinuing = false;
}
async nextStep() {
if (this.state.lockContinue || this.isContinuing) {
return;
}
if (this.state.step === Steps.FinishStep) {
this.dismiss();
return;
}
this.isContinuing = true;
await this.setState({
showSpinner: true,
continueTitle: "Generating Keys..."
});
const valid = await this.validateCurrentPassword();
if (!valid) {
this.resetContinueState();
return;
}
const success = await this.processPasswordChange();
if (!success) {
this.resetContinueState();
return;
}
this.isContinuing = false;
this.setState({
showSpinner: false,
continueTitle: "Finish",
step: Steps.FinishStep
});
}
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.state.formData,
...formData
}
});
}
async validateCurrentPassword() {
const currentPassword = this.state.formData.currentPassword;
const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.application.alertService!.alert(
"Please enter your current password."
);
return false;
}
if (this.props.changePassword) {
if (!newPass || newPass.length === 0) {
this.application.alertService!.alert(
"Please enter a new password."
);
return false;
}
if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alertService!.alert(
"Your new password does not match its confirmation."
);
this.setFormDataState({
status: undefined
});
return false;
}
}
if (!this.application.getUser()?.email) {
this.application.alertService!.alert(
"We don't have your email stored. Please log out then log back in to fix this issue."
);
this.setFormDataState({
status: undefined
});
return false;
}
/** Validate current password */
const success = await this.application.validateAccountPassword(
this.state.formData.currentPassword!
);
if (!success) {
this.application.alertService!.alert(
"The current password you entered is not correct. Please try again."
);
}
return success;
}
async processPasswordChange() {
await this.setState({
lockContinue: true,
processing: true
});
await this.setFormDataState({
status: "Processing encryption keys..."
});
const newPassword = this.props.securityUpdate
? this.state.formData.currentPassword
: this.state.formData.newPassword;
const response = await this.application.changePassword(
this.state.formData.currentPassword!,
newPassword!
);
const success = !response.error;
await this.setState({
processing: false,
lockContinue: false,
});
if (!success) {
this.setFormDataState({
status: "Unable to process your password. Please try again."
});
} else {
this.setState({
formData: {
...this.state.formData,
status: this.props.changePassword
? "Successfully changed password."
: "Successfully performed account update."
}
});
}
return success;
}
dismiss() {
if (this.state.lockContinue) {
this.application.alertService!.alert(
"Cannot close window until pending tasks are complete."
);
} else {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
}
export class PasswordWizard extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PasswordWizardCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '=',
application: '='
};
}
}

View file

@ -1,13 +1,21 @@
import { WebDirective } from './../../types';
import template from '%/directives/permissions-modal.pug';
class PermissionsModalCtrl {
$element: JQLite
callback!: (success: boolean) => void
/* @ngInject */
constructor($element) {
constructor($element: JQLite) {
this.$element = $element;
}
dismiss() {
this.$element.remove();
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
accept() {
@ -21,8 +29,9 @@ class PermissionsModalCtrl {
}
}
export class PermissionsModal {
export class PermissionsModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PermissionsModalCtrl;

View file

@ -1,101 +0,0 @@
import template from '%/directives/privileges-auth-modal.pug';
class PrivilegesAuthModalCtrl {
/* @ngInject */
constructor(
$element,
$timeout,
privilegesManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.privilegesManager = privilegesManager;
}
$onInit() {
this.authParameters = {};
this.sessionLengthOptions = this.privilegesManager.getSessionLengthOptions();
this.privilegesManager.getSelectedSessionLength().then((length) => {
this.$timeout(() => {
this.selectedSessionLength = length;
});
});
this.privilegesManager.netCredentialsForAction(this.action).then((credentials) => {
this.$timeout(() => {
this.requiredCredentials = credentials.sort();
});
});
}
selectSessionLength(length) {
this.selectedSessionLength = length;
}
promptForCredential(credential) {
return this.privilegesManager.displayInfoForCredential(credential).prompt;
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
isCredentialInFailureState(credential) {
if (!this.failedCredentials) {
return false;
}
return this.failedCredentials.find((candidate) => {
return candidate === credential;
}) != null;
}
validate() {
const failed = [];
for (const cred of this.requiredCredentials) {
const value = this.authParameters[cred];
if (!value || value.length === 0) {
failed.push(cred);
}
}
this.failedCredentials = failed;
return failed.length === 0;
}
async submit() {
if (!this.validate()) {
return;
}
const result = await this.privilegesManager.authenticateAction(
this.action,
this.authParameters
);
this.$timeout(() => {
if (result.success) {
this.privilegesManager.setSessionLength(this.selectedSessionLength);
this.onSuccess();
this.dismiss();
} else {
this.failedCredentials = result.failedCredentials;
}
});
}
dismiss() {
this.$element.remove();
}
}
export class PrivilegesAuthModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesAuthModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '=',
onSuccess: '=',
onCancel: '='
};
}
}

View file

@ -0,0 +1,128 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from 'snjs';
import template from '%/directives/privileges-auth-modal.pug';
type PrivilegesAuthModalScope = {
application: WebApplication
action: ProtectedAction
onSuccess: () => void
onCancel: () => void
}
class PrivilegesAuthModalCtrl implements PrivilegesAuthModalScope {
$element: JQLite
$timeout: ng.ITimeoutService
application!: WebApplication
action!: ProtectedAction
onSuccess!: () => void
onCancel!: () => void
authParameters: Partial<Record<PrivilegeCredential, string>> = {}
sessionLengthOptions!: { value: PrivilegeSessionLength, label: string }[]
selectedSessionLength!: PrivilegeSessionLength
requiredCredentials!: PrivilegeCredential[]
failedCredentials!: PrivilegeCredential[]
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService
) {
this.$element = $element;
this.$timeout = $timeout;
}
$onInit() {
this.sessionLengthOptions = this.application!.privilegesService!
.getSessionLengthOptions();
this.application.privilegesService!.getSelectedSessionLength()
.then((length) => {
this.$timeout(() => {
this.selectedSessionLength = length;
});
});
this.application.privilegesService!.netCredentialsForAction(this.action)
.then((credentials) => {
this.$timeout(() => {
this.requiredCredentials = credentials.sort();
});
});
}
selectSessionLength(length: PrivilegeSessionLength) {
this.selectedSessionLength = length;
}
promptForCredential(credential: PrivilegeCredential) {
return this.application.privilegesService!.displayInfoForCredential(credential).prompt;
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
isCredentialInFailureState(credential: PrivilegeCredential) {
if (!this.failedCredentials) {
return false;
}
return this.failedCredentials.find((candidate) => {
return candidate === credential;
}) != null;
}
validate() {
const failed = [];
for (const cred of this.requiredCredentials) {
const value = this.authParameters[cred];
if (!value || value.length === 0) {
failed.push(cred);
}
}
this.failedCredentials = failed;
return failed.length === 0;
}
async submit() {
if (!this.validate()) {
return;
}
const result = await this.application.privilegesService!.authenticateAction(
this.action,
this.authParameters
);
this.$timeout(() => {
if (result.success) {
this.application.privilegesService!.setSessionLength(this.selectedSessionLength);
this.onSuccess();
this.dismiss();
} else {
this.failedCredentials = result.failedCredentials;
}
});
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesAuthModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesAuthModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '=',
onSuccess: '=',
onCancel: '=',
application: '='
};
}
}

View file

@ -1,89 +0,0 @@
import { PrivilegesManager } from '@/services/privilegesManager';
import template from '%/directives/privileges-management-modal.pug';
class PrivilegesManagementModalCtrl {
/* @ngInject */
constructor(
$timeout,
$element,
privilegesManager,
authManager,
passcodeManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.privilegesManager = privilegesManager;
this.hasPasscode = passcodeManager.hasPasscode();
this.hasAccount = !authManager.offline();
this.reloadPrivileges();
}
displayInfoForCredential(credential) {
const info = this.privilegesManager.displayInfoForCredential(credential);
if (credential === PrivilegesManager.CredentialLocalPasscode) {
info.availability = this.hasPasscode;
} else if (credential === PrivilegesManager.CredentialAccountPassword) {
info.availability = this.hasAccount;
} else {
info.availability = true;
}
return info;
}
displayInfoForAction(action) {
return this.privilegesManager.displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action, credential) {
if (!this.privileges) {
return false;
}
return this.privileges.isCredentialRequiredForAction(action, credential);
}
async clearSession() {
await this.privilegesManager.clearSession();
this.reloadPrivileges();
}
async reloadPrivileges() {
this.availableActions = this.privilegesManager.getAvailableActions();
this.availableCredentials = this.privilegesManager.getAvailableCredentials();
const sessionEndDate = await this.privilegesManager.getSessionExpirey();
this.sessionExpirey = sessionEndDate.toLocaleString();
this.sessionExpired = new Date() >= sessionEndDate;
this.credentialDisplayInfo = {};
for (const cred of this.availableCredentials) {
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
}
const privs = await this.privilegesManager.getPrivileges();
this.$timeout(() => {
this.privileges = privs;
});
}
checkboxValueChanged(action, credential) {
this.privileges.toggleCredentialForAction(action, credential);
this.privilegesManager.savePrivileges();
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
dismiss() {
this.$element.remove();
}
}
export class PrivilegesManagementModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesManagementModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {};
}
}

View file

@ -0,0 +1,118 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/privileges-management-modal.pug';
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from 'snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PrivilegeMutator } from 'snjs/dist/@types/models';
type DisplayInfo = {
label: string
prompt: string
}
class PrivilegesManagementModalCtrl extends PureViewCtrl {
hasPasscode = false
hasAccount = false
$element: JQLite
application!: WebApplication
privileges!: SNPrivileges
availableActions!: ProtectedAction[]
availableCredentials!: PrivilegeCredential[]
sessionExpirey!: string
sessionExpired = true
credentialDisplayInfo: Partial<Record<PrivilegeCredential, DisplayInfo>> = {}
onCancel!: () => void
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
$element: JQLite
) {
super($timeout);
this.$element = $element;
}
async onAppLaunch() {
super.onAppLaunch();
this.hasPasscode = this.application.hasPasscode();
this.hasAccount = !this.application.noAccount();
this.reloadPrivileges();
}
displayInfoForCredential(credential: PrivilegeCredential) {
const info: any = this.application.privilegesService!.displayInfoForCredential(credential);
if (credential === PrivilegeCredential.LocalPasscode) {
info.availability = this.hasPasscode;
} else if (credential === PrivilegeCredential.AccountPassword) {
info.availability = this.hasAccount;
} else {
info.availability = true;
}
return info;
}
displayInfoForAction(action: ProtectedAction) {
return this.application.privilegesService!.displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action: ProtectedAction, credential: PrivilegeCredential) {
if (!this.privileges) {
return false;
}
return this.privileges.isCredentialRequiredForAction(action, credential);
}
async clearSession() {
await this.application.privilegesService!.clearSession();
this.reloadPrivileges();
}
async reloadPrivileges() {
this.availableActions = this.application.privilegesService!.getAvailableActions();
this.availableCredentials = this.application.privilegesService!.getAvailableCredentials();
const sessionEndDate = await this.application.privilegesService!.getSessionExpirey();
this.sessionExpirey = sessionEndDate.toLocaleString();
this.sessionExpired = new Date() >= sessionEndDate;
for (const cred of this.availableCredentials) {
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
}
const privs = await this.application.privilegesService!.getPrivileges();
this.$timeout(() => {
this.privileges = privs;
});
}
checkboxValueChanged(action: ProtectedAction, credential: PrivilegeCredential) {
this.application.changeAndSaveItem(this.privileges.uuid, (m) => {
const mutator = m as PrivilegeMutator;
mutator.toggleCredentialForAction(action, credential);
})
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesManagementModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesManagementModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
application: '='
};
}
}

View file

@ -1,133 +0,0 @@
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
import template from '%/directives/revision-preview-modal.pug';
class RevisionPreviewModalCtrl {
/* @ngInject */
constructor(
$element,
$scope,
$timeout,
alertManager,
componentManager,
modelManager,
syncManager,
) {
this.$element = $element;
this.$scope = $scope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.createNote();
this.configureEditor();
$scope.$on('$destroy', () => {
if (this.identifier) {
this.componentManager.deregisterHandler(this.identifier);
}
});
}
createNote() {
this.note = new SFItem({
content: this.content,
content_type: "Note"
});
}
configureEditor() {
/**
* Set UUID so editoForNote can find proper editor, but then generate new uuid
* for note as not to save changes to original, if editor makes changes.
*/
this.note.uuid = this.uuid;
const editorForNote = this.componentManager.editorForNote(this.note);
this.note.uuid = protocolManager.crypto.generateUUIDSync();
if (editorForNote) {
/**
* Create temporary copy, as a lot of componentManager is uuid based, so might
* interfere with active editor. Be sure to copy only the content, as the top level
* editor object has non-copyable properties like .window, which cannot be transfered
*/
const editorCopy = new SNComponent({
content: editorForNote.content
});
editorCopy.readonly = true;
editorCopy.lockReadonly = true;
this.identifier = editorCopy.uuid;
this.componentManager.registerHandler({
identifier: this.identifier,
areas: ['editor-editor'],
contextRequestHandler: (component) => {
if (component === this.editor) {
return this.note;
}
},
componentForSessionKeyHandler: (key) => {
if (key === this.editor.sessionKey) {
return this.editor;
}
}
});
this.editor = editorCopy;
}
}
restore(asCopy) {
const run = () => {
let item;
if (asCopy) {
const contentCopy = Object.assign({}, this.content);
if (contentCopy.title) {
contentCopy.title += " (copy)";
}
item = this.modelManager.createItem({
content_type: 'Note',
content: contentCopy
});
this.modelManager.addItem(item);
} else {
const uuid = this.uuid;
item = this.modelManager.findItem(uuid);
item.content = Object.assign({}, this.content);
this.modelManager.mapResponseItemsToLocalModels(
[item],
SFModelManager.MappingSourceRemoteActionRetrieved
);
}
this.modelManager.setItemDirty(item);
this.syncManager.sync();
this.dismiss();
};
if (!asCopy) {
this.alertManager.confirm({
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
destructive: true,
onConfirm: run
});
} else {
run();
}
}
dismiss() {
this.$element.remove();
this.$scope.$destroy();
}
}
export class RevisionPreviewModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = RevisionPreviewModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
uuid: '=',
content: '='
};
}
}

View file

@ -0,0 +1,145 @@
import { PureViewCtrl } from './../../views/abstract/pure_view_ctrl';
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import {
ContentType,
PayloadSource,
SNComponent,
SNNote,
ComponentArea
} from 'snjs';
import template from '%/directives/revision-preview-modal.pug';
import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator';
import { confirmDialog } from '@/services/alertService';
interface RevisionPreviewScope {
uuid: string
content: PayloadContent
application: WebApplication
}
class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewScope {
$element: JQLite
$timeout: ng.ITimeoutService
uuid!: string
content!: PayloadContent
application!: WebApplication
unregisterComponent?: any
note!: SNNote
private originalNote!: SNNote;
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService
) {
super($timeout);
this.$element = $element;
this.$timeout = $timeout;
}
$onInit() {
this.configure();
}
$onDestroy() {
if (this.unregisterComponent) {
this.unregisterComponent();
this.unregisterComponent = undefined;
}
}
get componentManager() {
return this.application.componentManager!;
}
async configure() {
this.note = await this.application.createTemplateItem(
ContentType.Note,
this.content
) as SNNote;
this.originalNote = this.application.findItem(this.uuid) as SNNote;
const editorForNote = this.componentManager.editorForNote(this.originalNote);
if (editorForNote) {
/**
* Create temporary copy, as a lot of componentManager is uuid based, so might
* interfere with active editor. Be sure to copy only the content, as the top level
* editor object has non-copyable properties like .window, which cannot be transfered
*/
const editorCopy = await this.application.createTemplateItem(
ContentType.Component,
editorForNote.safeContent
) as SNComponent;
this.componentManager.setReadonlyStateForComponent(editorCopy, true, true);
this.unregisterComponent = this.componentManager.registerHandler({
identifier: editorCopy.uuid,
areas: [ComponentArea.Editor],
contextRequestHandler: (componentUuid) => {
if (componentUuid === this.state.editor?.uuid) {
return this.note;
}
},
componentForSessionKeyHandler: (key) => {
if (key === this.componentManager.sessionKeyForComponent(this.state.editor!)) {
return this.state.editor;
}
}
});
this.setState({editor: editorCopy});
}
}
restore(asCopy: boolean) {
const run = async () => {
if (asCopy) {
await this.application.duplicateItem(this.originalNote, {
...this.content,
title: this.content.title ? this.content.title + ' (copy)' : undefined
});
} else {
this.application.changeAndSaveItem(this.uuid, (mutator) => {
mutator.setContent(this.content);
}, true, PayloadSource.RemoteActionRetrieved);
}
this.dismiss();
};
if (!asCopy) {
confirmDialog({
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
confirmButtonStyle: "danger"
}).then((confirmed) => {
if (confirmed) {
run();
}
});
} else {
run();
}
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class RevisionPreviewModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = RevisionPreviewModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
uuid: '=',
content: '=',
application: '='
};
}
}

View file

@ -1,118 +0,0 @@
import template from '%/directives/session-history-menu.pug';
class SessionHistoryMenuCtrl {
/* @ngInject */
constructor(
$timeout,
actionsManager,
alertManager,
sessionHistory,
) {
this.$timeout = $timeout;
this.alertManager = alertManager;
this.actionsManager = actionsManager;
this.sessionHistory = sessionHistory;
this.diskEnabled = this.sessionHistory.diskEnabled;
this.autoOptimize = this.sessionHistory.autoOptimize;
}
$onInit() {
this.reloadHistory();
}
reloadHistory() {
const history = this.sessionHistory.historyForItem(this.item);
this.entries = history.entries.slice(0).sort((a, b) => {
return a.item.updated_at < b.item.updated_at ? 1 : -1;
});
this.history = history;
}
openRevision(revision) {
this.actionsManager.presentRevisionPreviewModal(
revision.item.uuid,
revision.item.content
);
}
classForRevision(revision) {
const vector = revision.operationVector();
if (vector === 0) {
return 'default';
} else if (vector === 1) {
return 'success';
} else if (vector === -1) {
return 'danger';
}
}
clearItemHistory() {
this.alertManager.confirm({
text: "Are you sure you want to delete the local session history for this note?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
this.$timeout(() => {
this.reloadHistory();
});
});
}
});
}
clearAllHistory() {
this.alertManager.confirm({
text: "Are you sure you want to delete the local session history for all notes?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearAllHistory().then(() => {
this.$timeout(() => {
this.reloadHistory();
});
});
}
});
}
toggleDiskSaving() {
const run = () => {
this.sessionHistory.toggleDiskSaving().then(() => {
this.$timeout(() => {
this.diskEnabled = this.sessionHistory.diskEnabled;
});
});
};
if (!this.sessionHistory.diskEnabled) {
this.alertManager.confirm({
text: `Are you sure you want to save history to disk? This will decrease general
performance, especially as you type. You are advised to disable this feature
if you experience any lagging.`,
destructive: true,
onConfirm: run
});
} else {
run();
}
}
toggleAutoOptimize() {
this.sessionHistory.toggleAutoOptimize().then(() => {
this.$timeout(() => {
this.autoOptimize = this.sessionHistory.autoOptimize;
});
});
}
}
export class SessionHistoryMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = SessionHistoryMenuCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
item: '='
};
}
}

View file

@ -1,20 +1,30 @@
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from '%/directives/sync-resolution-menu.pug';
class SyncResolutionMenuCtrl {
closeFunction!: () => void
application!: WebApplication
$timeout: ng.ITimeoutService
status: Partial<{
backupFinished: boolean,
resolving: boolean,
attemptedResolution: boolean,
success: boolean
fail: boolean
}> = {}
/* @ngInject */
constructor(
$timeout,
archiveManager,
syncManager,
$timeout: ng.ITimeoutService
) {
this.$timeout = $timeout;
this.archiveManager = archiveManager;
this.syncManager = syncManager;
this.status = {};
}
downloadBackup(encrypted) {
this.archiveManager.downloadBackup(encrypted);
downloadBackup(encrypted: boolean) {
this.application.getArchiveService().downloadBackup(encrypted);
this.status.backupFinished = true;
}
@ -24,11 +34,11 @@ class SyncResolutionMenuCtrl {
async performSyncResolution() {
this.status.resolving = true;
await this.syncManager.resolveOutOfSync();
await this.application.resolveOutOfSync();
this.$timeout(() => {
this.status.resolving = false;
this.status.attemptedResolution = true;
if (this.syncManager.isOutOfSync()) {
if (this.application.isOutOfSync()) {
this.status.fail = true;
} else {
this.status.success = true;
@ -38,20 +48,22 @@ class SyncResolutionMenuCtrl {
close() {
this.$timeout(() => {
this.closeFunction()();
this.closeFunction();
});
}
}
export class SyncResolutionMenu {
export class SyncResolutionMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = SyncResolutionMenuCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
closeFunction: '&'
closeFunction: '&',
application: '='
};
}
}

View file

@ -1,6 +0,0 @@
/* @ngInject */
export function trusted($sce) {
return function(url) {
return $sce.trustAsResourceUrl(url);
};
}

View file

@ -0,0 +1,6 @@
/* @ngInject */
export function trusted($sce: ng.ISCEService) {
return function(url: string) {
return $sce.trustAsResourceUrl(url);
};
}

View file

@ -12,11 +12,6 @@ import '../../../vendor/assets/javascripts/zip/inflate';
import '../../../vendor/assets/javascripts/zip/zip';
import '../../../vendor/assets/javascripts/zip/z-worker';
import { SFItem } from 'snjs';
// Set the app domain before starting the app
SFItem.AppDomain = 'org.standardnotes.sn';
// entry point
// eslint-disable-next-line import/first
import './app';

View file

@ -0,0 +1,4 @@
export enum RootScopeMessages {
ReloadExtendedData = 'reload-ext-data',
NewUpdateAvailable = 'new-update-available'
}

View file

@ -1,20 +0,0 @@
import { SFItemHistoryEntry } from 'snjs';
export class NoteHistoryEntry extends SFItemHistoryEntry {
previewTitle() {
return this.item.updated_at.toLocaleString();
}
previewSubTitle() {
if(!this.hasPreviousEntry) {
return `${this.textCharDiffLength} characters loaded`;
} else if(this.textCharDiffLength < 0) {
return `${this.textCharDiffLength * -1} characters removed`;
} else if(this.textCharDiffLength > 0) {
return `${this.textCharDiffLength} characters added`;
} else {
return "Title or metadata changed";
}
}
}

View file

@ -1,7 +1,7 @@
import { isDesktopApplication } from './utils';
/* @ngInject */
export function configRoutes($locationProvider) {
export function configRoutes($locationProvider: ng.ILocationProvider) {
if (!isDesktopApplication()) {
if (window.history && window.history.pushState) {
$locationProvider.html5Mode({

View file

@ -1,297 +0,0 @@
import _ from 'lodash';
import angular from 'angular';
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
export class ActionsManager {
/* @ngInject */
constructor(
$compile,
$rootScope,
$timeout,
alertManager,
authManager,
httpManager,
modelManager,
syncManager,
) {
this.$compile = $compile;
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.authManager = authManager;
this.httpManager = httpManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
/* Used when decrypting old items with new keys. This array is only kept in memory. */
this.previousPasswords = [];
}
get extensions() {
return this.modelManager.validItemsForContentType('Extension');
}
extensionsInContextOfItem(item) {
return this.extensions.filter((ext) => {
return _.includes(ext.supported_types, item.content_type) ||
ext.actionsWithContextForItem(item).length > 0;
});
}
/**
* Loads an extension in the context of a certain item.
* The server then has the chance to respond with actions that are
* relevant just to this item. The response extension is not saved,
* just displayed as a one-time thing.
*/
async loadExtensionInContextOfItem(extension, item) {
const params = {
content_type: item.content_type,
item_uuid: item.uuid
};
const emptyFunc = () => { };
return this.httpManager.getAbsolute(
extension.url,
params,
emptyFunc,
emptyFunc
).then((response) => {
this.updateExtensionFromRemoteResponse(extension, response);
return extension;
}).catch((response) => {
console.error("Error loading extension", response);
return null;
});
}
updateExtensionFromRemoteResponse(extension, response) {
if (response.description) {
extension.description = response.description;
}
if (response.supported_types) {
extension.supported_types = response.supported_types;
}
if (response.actions) {
extension.actions = response.actions.map((action) => {
return new Action(action);
});
} else {
extension.actions = [];
}
}
async executeAction(action, extension, item) {
action.running = true;
let result;
switch (action.verb) {
case 'get':
result = await this.handleGetAction(action);
break;
case 'render':
result = await this.handleRenderAction(action);
break;
case 'show':
result = await this.handleShowAction(action);
break;
case 'post':
result = await this.handlePostAction(action, item, extension);
break;
default:
break;
}
action.lastExecuted = new Date();
action.running = false;
return result;
}
async decryptResponse(response, keys) {
const responseItem = response.item;
await protocolManager.decryptItem(responseItem, keys);
if (!responseItem.errorDecrypting) {
return {
response: response,
item: responseItem
};
}
if (!response.auth_params) {
/**
* In some cases revisions were missing auth params.
* Instruct the user to email us to get this remedied.
*/
this.alertManager.alert({
text: `We were unable to decrypt this revision using your current keys,
and this revision is missing metadata that would allow us to try different
keys to decrypt it. This can likely be fixed with some manual intervention.
Please email hello@standardnotes.org for assistance.`
});
return {};
}
/* Try previous passwords */
const triedPasswords = [];
for (const passwordCandidate of this.previousPasswords) {
if (triedPasswords.includes(passwordCandidate)) {
continue;
}
triedPasswords.push(passwordCandidate);
const keyResults = await protocolManager.computeEncryptionKeysForUser(
passwordCandidate,
response.auth_params
);
if (!keyResults) {
continue;
}
const nestedResponse = await this.decryptResponse(
response,
keyResults
);
if (nestedResponse.item) {
return nestedResponse;
}
}
return new Promise((resolve, reject) => {
this.presentPasswordModal((password) => {
this.previousPasswords.push(password);
const result = this.decryptResponse(response, keys);
resolve(result);
});
});
}
async handlePostAction(action, item, extension) {
const decrypted = action.access_type === 'decrypted';
const itemParams = await this.outgoingParamsForItem(item, extension, decrypted);
const params = {
items: [itemParams]
};
/* Needed until SNJS detects null function */
const emptyFunc = () => { };
return this.httpManager.postAbsolute(
action.url,
params,
emptyFunc,
emptyFunc
).then((response) => {
action.error = false;
return {response: response};
}).catch((response) => {
action.error = true;
console.error("Action error response:", response);
this.alertManager.alert({
text: "An issue occurred while processing this action. Please try again."
});
return { response: response };
});
}
async handleShowAction(action) {
const win = window.open(action.url, '_blank');
if (win) {
win.focus();
}
return { response: null };
}
async handleGetAction(action) {
/* Needed until SNJS detects null function */
const emptyFunc = () => {};
const onConfirm = async () => {
return this.httpManager.getAbsolute(action.url, {}, emptyFunc, emptyFunc)
.then(async (response) => {
action.error = false;
await this.decryptResponse(response, await this.authManager.keys());
const items = await this.modelManager.mapResponseItemsToLocalModels(
[response.item],
SFModelManager.MappingSourceRemoteActionRetrieved
);
for (const mappedItem of items) {
this.modelManager.setItemDirty(mappedItem, true);
}
this.syncManager.sync();
return {
response: response,
item: response.item
};
}).catch((response) => {
const error = (response && response.error)
|| { message: "An issue occurred while processing this action. Please try again." };
this.alertManager.alert({ text: error.message });
action.error = true;
return { error: error };
});
};
return new Promise((resolve, reject) => {
this.alertManager.confirm({
text: "Are you sure you want to replace the current note contents with this action's results?",
onConfirm: () => {
onConfirm().then(resolve);
}
});
});
}
async handleRenderAction(action) {
/* Needed until SNJS detects null function */
const emptyFunc = () => {};
return this.httpManager.getAbsolute(
action.url,
{},
emptyFunc,
emptyFunc
).then(async (response) => {
action.error = false;
const result = await this.decryptResponse(response, await this.authManager.keys());
const item = this.modelManager.createItem(result.item);
return {
response: result.response,
item: item
};
}).catch((response) => {
const error = (response && response.error)
|| { message: "An issue occurred while processing this action. Please try again." };
this.alertManager.alert({ text: error.message });
action.error = true;
return { error: error };
});
}
async outgoingParamsForItem(item, extension, decrypted = false) {
let keys = await this.authManager.keys();
if (decrypted) {
keys = null;
}
const itemParams = new SFItemParams(
item,
keys,
await this.authManager.getAuthParams()
);
return itemParams.paramsForExtension();
}
presentRevisionPreviewModal(uuid, content) {
const scope = this.$rootScope.$new(true);
scope.uuid = uuid;
scope.content = content;
const el = this.$compile(
`<revision-preview-modal uuid='uuid' content='content'
class='sk-modal'></revision-preview-modal>`
)(scope);
angular.element(document.body).append(el);
}
presentPasswordModal(callback) {
const scope = this.$rootScope.$new(true);
scope.type = "password";
scope.title = "Decryption Assistance";
scope.message = `Unable to decrypt this item with your current keys.
Please enter your account password at the time of this revision.`;
scope.callback = callback;
const el = this.$compile(
`<input-modal type='type' message='message'
title='title' callback='callback'></input-modal>`
)(scope);
angular.element(document.body).append(el);
}
}

View file

@ -1,71 +0,0 @@
import { SFAlertManager } from 'snjs';
import { SKAlert } from 'sn-stylekit';
export class AlertManager extends SFAlertManager {
/* @ngInject */
constructor($timeout) {
super();
this.$timeout = $timeout;
}
async alert({
title,
text,
closeButtonText = "OK",
onClose} = {}
) {
return new Promise((resolve, reject) => {
const buttons = [
{
text: closeButtonText,
style: "neutral",
action: async () => {
if(onClose) {
this.$timeout(onClose);
}
resolve(true);
}
}
];
const alert = new SKAlert({title, text, buttons});
alert.present();
});
}
async confirm({
title,
text,
confirmButtonText = "Confirm",
cancelButtonText = "Cancel",
onConfirm,
onCancel,
destructive = false
} = {}) {
return new Promise((resolve, reject) => {
const buttons = [
{
text: cancelButtonText,
style: "neutral",
action: async () => {
if(onCancel) {
this.$timeout(onCancel);
}
reject(false);
}
},
{
text: confirmButtonText,
style: destructive ? "danger" : "info",
action: async () => {
if(onConfirm) {
this.$timeout(onConfirm);
}
resolve(true);
}
},
];
const alert = new SKAlert({title, text, buttons});
alert.present();
});
}
}

View file

@ -0,0 +1,101 @@
/* eslint-disable prefer-promise-reject-errors */
import { SNAlertService, ButtonType } from 'snjs';
import { SKAlert } from 'sn-stylekit';
/** @returns a promise resolving to true if the user confirmed, false if they canceled */
export function confirmDialog({
text,
title,
confirmButtonText = 'Confirm',
cancelButtonText = 'Cancel',
confirmButtonStyle = 'info',
}: {
text: string;
title?: string;
confirmButtonText?: string;
cancelButtonText?: string;
confirmButtonStyle?: 'danger' | 'info';
}) {
return new Promise<boolean>((resolve) => {
const alert = new SKAlert({
title,
text,
buttons: [
{
text: cancelButtonText,
style: 'neutral',
action() {
resolve(false);
},
},
{
text: confirmButtonText,
style: confirmButtonStyle,
action() {
resolve(true);
},
},
],
});
alert.present();
});
}
export function alertDialog({
title,
text,
closeButtonText = 'OK',
}: {
title?: string;
text: string;
closeButtonText?: string;
}) {
return new Promise<void>((resolve) => {
const alert = new SKAlert({
title,
text,
buttons: [
{
text: closeButtonText,
style: 'neutral',
action: resolve,
},
],
});
alert.present();
});
}
export class AlertService implements SNAlertService {
/**
* @deprecated use the standalone `alertDialog` function instead
*/
alert(text: string, title?: string, closeButtonText?: string) {
return alertDialog({ text, title, closeButtonText });
}
confirm(
text: string,
title?: string,
confirmButtonText?: string,
confirmButtonType?: ButtonType,
cancelButtonText?: string
): Promise<boolean> {
return confirmDialog({
text,
title,
confirmButtonText,
cancelButtonText,
confirmButtonStyle:
confirmButtonType === ButtonType.Danger ? 'danger' : 'info',
});
}
blockingDialog(text: string, title?: string) {
const alert = new SKAlert({ text, title });
alert.present();
return () => {
alert.dismiss();
};
}
}

View file

@ -1,163 +0,0 @@
import { PrivilegesManager } from '@/services/privilegesManager';
export class ArchiveManager {
/* @ngInject */
constructor(passcodeManager, authManager, modelManager, privilegesManager) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.privilegesManager = privilegesManager;
}
/*
Public
*/
async downloadBackup(encrypted) {
return this.downloadBackupOfItems(this.modelManager.allItems, encrypted);
}
async downloadBackupOfItems(items, encrypted) {
const run = async () => {
// download in Standard Notes format
let keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
}
const data = await this.__itemsData(items, keys, authParams);
this.__downloadData(data,
`Standard Notes Encrypted Backup and Import File - ${this.__formattedDate()}.txt`);
} else {
this.__downloadZippedItems(items);
}
};
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {
run();
}
}
/*
Private
*/
__formattedDate() {
var string = `${new Date()}`;
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
var matches = string.match(/^(.*?) \(/);
if(matches.length >= 2) {
return matches[1];
}
return string;
}
async __itemsData(items, keys, authParams) {
const data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
const blobData = new Blob([data], {type: 'text/json'});
return blobData;
}
__loadZip(callback) {
if(window.zip) {
callback();
return;
}
var scriptTag = document.createElement('script');
scriptTag.src = "/assets/zip/zip.js";
scriptTag.async = false;
var headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
scriptTag.onload = function() {
zip.workerScriptsPath = "assets/zip/";
callback();
};
}
__downloadZippedItems(items) {
this.__loadZip(() => {
zip.createWriter(new zip.BlobWriter("application/zip"), async (zipWriter) => {
var index = 0;
const data = await this.modelManager.getJSONDataForItems(items);
await new Promise((resolve) => {
const blob = new Blob([data], {type: 'text/plain'});
zipWriter.add(
'Standard Notes Backup and Import File.txt',
new zip.BlobReader(blob),
resolve
);
});
const nextFile = () => {
var item = items[index];
var name, contents;
if(item.content_type === "Note") {
name = item.content.title;
contents = item.content.text;
} else {
name = item.content_type;
contents = JSON.stringify(item.content, null, 2);
}
if(!name) {
name = "";
}
const blob = new Blob([contents], {type: 'text/plain'});
let filePrefix = name.replace(/\//g, "").replace(/\\+/g, "");
const fileSuffix = `-${item.uuid.split("-")[0]}.txt`;
// Standard max filename length is 255. Slice the note name down to allow filenameEnd
filePrefix = filePrefix.slice(0, (255 - fileSuffix.length));
const fileName = `Items/${item.content_type}/${filePrefix}${fileSuffix}`;
zipWriter.add(fileName, new zip.BlobReader(blob), () => {
index++;
if(index < items.length) {
nextFile();
} else {
zipWriter.close((blob) => {
this.__downloadData(blob, `Standard Notes Backup - ${this.__formattedDate()}.zip`);
zipWriter = null;
});
}
});
};
nextFile();
}, onerror);
});
}
__hrefForData(data) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile !== null) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return this.textFile;
}
__downloadData(data, fileName) {
var link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.__hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
}

View file

@ -0,0 +1,171 @@
import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs';
function zippableTxtName(name: string, suffix = ""): string {
const sanitizedName = name
.replace(/\//g, '')
.replace(/\\+/g, '')
.replace(/:/g, ' ')
.replace(/\./g, ' ');
const nameEnd = suffix + ".txt";
const maxFileNameLength = 255;
return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd;
}
export class ArchiveManager {
private readonly application: WebApplication
private textFile?: string
constructor(application: WebApplication) {
this.application = application;
}
public async downloadBackup(encrypted: boolean) {
const items = this.application.allItems();
const run = async () => {
// download in Standard Notes format
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
if (encrypted) {
const data = await this.itemsData(items, intent);
this.downloadData(
data!,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
);
} else {
/** download as zipped plain text files */
this.downloadZippedItems(items);
}
};
if (
await this.application.privilegesService!
.actionRequiresPrivilege(ProtectedAction.ManageBackups)
) {
this.application.presentPrivilegesModal(
ProtectedAction.ManageBackups,
() => {
run();
});
} else {
run();
}
}
private formattedDate() {
const string = `${new Date()}`;
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
const matches = string.match(/^(.*?) \(/);
if (matches && matches.length >= 2) {
return matches[1];
}
return string;
}
private async itemsData(items: SNItem[], intent: EncryptionIntent) {
const data = await this.application.createBackupFile(items, intent);
if (!data) {
return undefined;
}
const blobData = new Blob([data], { type: 'text/json' });
return blobData;
}
private get zip() {
return (window as any).zip;
}
private async loadZip() {
if (this.zip) {
return;
}
const scriptTag = document.createElement('script');
scriptTag.src = '/assets/zip/zip.js';
scriptTag.async = false;
const headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
return new Promise((resolve) => {
scriptTag.onload = () => {
this.zip.workerScriptsPath = 'assets/zip/';
resolve();
};
});
}
private async downloadZippedItems(
items: SNItem[]
) {
await this.loadZip();
this.zip.createWriter(
new this.zip.BlobWriter('application/zip'),
async (zipWriter: any) => {
const data = await this.application.createBackupFile(items, EncryptionIntent.FileDecrypted);
await new Promise((resolve) => {
const blob = new Blob([data!], { type: 'text/plain' });
const fileName = zippableTxtName(
'Standard Notes Backup and Import File.txt'
);
zipWriter.add(fileName, new this.zip.BlobReader(blob), resolve);
});
let index = 0;
const nextFile = () => {
const item = items[index];
let name, contents;
if (item.content_type === ContentType.Note) {
const note = item as SNNote;
name = note.title;
contents = note.text;
} else {
name = item.content_type;
contents = JSON.stringify(item.content, null, 2);
}
if (!name) {
name = '';
}
const blob = new Blob([contents], { type: 'text/plain' });
const fileName = `Items/${item.content_type}/` +
zippableTxtName(name, `-${item.uuid.split('-')[0]}`);
zipWriter.add(fileName, new this.zip.BlobReader(blob), () => {
index++;
if (index < items.length) {
nextFile();
} else {
zipWriter.close((blob: any) => {
this.downloadData(
blob,
`Standard Notes Backup - ${this.formattedDate()}.zip`
);
zipWriter = null;
});
}
});
};
nextFile();
}, onerror);
}
private hrefForData(data: Blob) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return this.textFile;
}
private downloadData(data: Blob, fileName: string) {
const link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
}

View file

@ -1,140 +0,0 @@
import angular from 'angular';
import { StorageManager } from './storageManager';
import { protocolManager, SFAuthManager } from 'snjs';
export class AuthManager extends SFAuthManager {
/* @ngInject */
constructor(
modelManager,
singletonManager,
storageManager,
dbManager,
httpManager,
$rootScope,
$timeout,
$compile
) {
super(storageManager, httpManager, null, $timeout);
this.$rootScope = $rootScope;
this.$compile = $compile;
this.modelManager = modelManager;
this.singletonManager = singletonManager;
this.storageManager = storageManager;
this.dbManager = dbManager;
}
loadInitialData() {
const userData = this.storageManager.getItemSync("user");
if(userData) {
this.user = JSON.parse(userData);
} else {
// legacy, check for uuid
const idData = this.storageManager.getItemSync("uuid");
if(idData) {
this.user = {uuid: idData};
}
}
this.checkForSecurityUpdate();
}
offline() {
return !this.user;
}
isEphemeralSession() {
if(this.ephemeral == null || this.ephemeral == undefined) {
this.ephemeral = JSON.parse(this.storageManager.getItemSync("ephemeral", StorageManager.Fixed));
}
return this.ephemeral;
}
setEphemeral(ephemeral) {
this.ephemeral = ephemeral;
if(ephemeral) {
this.storageManager.setModelStorageMode(StorageManager.Ephemeral);
this.storageManager.setItemsMode(StorageManager.Ephemeral);
} else {
this.storageManager.setModelStorageMode(StorageManager.Fixed);
this.storageManager.setItemsMode(this.storageManager.bestStorageMode());
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
}
}
async getAuthParamsForEmail(url, email, extraParams) {
return super.getAuthParamsForEmail(url, email, extraParams);
}
async login(url, email, password, ephemeral, strictSignin, extraParams) {
return super.login(url, email, password, strictSignin, extraParams).then((response) => {
if(!response.error) {
this.setEphemeral(ephemeral);
this.checkForSecurityUpdate();
}
return response;
});
}
async register(url, email, password, ephemeral) {
return super.register(url, email, password).then((response) => {
if(!response.error) {
this.setEphemeral(ephemeral);
}
return response;
});
}
async changePassword(url, email, current_server_pw, newKeys, newAuthParams) {
return super.changePassword(url, email, current_server_pw, newKeys, newAuthParams).then((response) => {
if(!response.error) {
this.checkForSecurityUpdate();
}
return response;
});
}
async handleAuthResponse(response, email, url, authParams, keys) {
try {
await super.handleAuthResponse(response, email, url, authParams, keys);
this.user = response.user;
this.storageManager.setItem("user", JSON.stringify(response.user));
} catch (e) {
this.dbManager.displayOfflineAlert();
}
}
async verifyAccountPassword(password) {
const authParams = await this.getAuthParams();
const keys = await protocolManager.computeEncryptionKeysForUser(password, authParams);
const success = keys.mk === (await this.keys()).mk;
return success;
}
async checkForSecurityUpdate() {
if(this.offline()) {
return false;
}
const latest = protocolManager.version();
const updateAvailable = await this.protocolVersion() !== latest;
if(updateAvailable !== this.securityUpdateAvailable) {
this.securityUpdateAvailable = updateAvailable;
this.$rootScope.$broadcast("security-update-status-changed");
}
return this.securityUpdateAvailable;
}
presentPasswordWizard(type) {
var scope = this.$rootScope.$new(true);
scope.type = type;
var el = this.$compile( "<password-wizard type='type'></password-wizard>" )(scope);
angular.element(document.body).append(el);
}
signOut() {
super.signout();
this.user = null;
this._authParams = null;
}
}

View file

@ -0,0 +1,176 @@
import { ApplicationService } from 'snjs';
import { WebApplication } from '@/ui_models/application';
import { isDesktopApplication } from '@/utils';
import { AppStateEvent } from '@/ui_models/app_state';
const MILLISECONDS_PER_SECOND = 1000;
const FOCUS_POLL_INTERVAL = 1 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_NONE = 0;
const LOCK_INTERVAL_IMMEDIATE = 1;
const LOCK_INTERVAL_ONE_MINUTE = 60 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_FIVE_MINUTES = 300 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND;
const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey";
export class AutolockService extends ApplicationService {
private unsubState?: () => void;
private pollFocusInterval: any
private lastFocusState?: 'hidden' | 'visible'
private lockAfterDate?: Date
private lockTimeout?: any
onAppLaunch() {
this.observeVisibility();
return super.onAppLaunch();
}
observeVisibility() {
this.unsubState = (this.application as WebApplication).getAppState().addObserver(
async (eventName) => {
if (eventName === AppStateEvent.WindowDidBlur) {
this.documentVisibilityChanged(false);
} else if (eventName === AppStateEvent.WindowDidFocus) {
this.documentVisibilityChanged(true);
}
}
);
if (!isDesktopApplication()) {
this.beginWebFocusPolling();
}
}
deinit() {
this.unsubState?.();
this.cancelAutoLockTimer();
if (this.pollFocusInterval) {
clearInterval(this.pollFocusInterval);
}
}
private lockApplication() {
if (!this.application.hasPasscode()) {
throw Error('Attempting to lock application with no passcode');
}
this.application.lock();
}
async setAutoLockInterval(interval: number) {
return this.application!.setValue(
STORAGE_KEY_AUTOLOCK_INTERVAL,
interval
);
}
async getAutoLockInterval() {
const interval = await this.application!.getValue(
STORAGE_KEY_AUTOLOCK_INTERVAL
);
if (interval) {
return interval;
} else {
return LOCK_INTERVAL_NONE;
}
}
async deleteAutolockPreference() {
await this.application!.removeValue(
STORAGE_KEY_AUTOLOCK_INTERVAL
);
this.cancelAutoLockTimer();
}
/**
* Verify document is in focus every so often as visibilitychange event is
* not triggered on a typical window blur event but rather on tab changes.
*/
beginWebFocusPolling() {
this.pollFocusInterval = setInterval(() => {
if (document.hidden) {
/** Native event listeners will have fired */
return;
}
const hasFocus = document.hasFocus();
if (hasFocus && this.lastFocusState === 'hidden') {
this.documentVisibilityChanged(true);
} else if (!hasFocus && this.lastFocusState === 'visible') {
this.documentVisibilityChanged(false);
}
/* Save this to compare against next time around */
this.lastFocusState = hasFocus ? 'visible' : 'hidden';
}, FOCUS_POLL_INTERVAL);
}
getAutoLockIntervalOptions() {
return [
{
value: LOCK_INTERVAL_NONE,
label: "Off"
},
{
value: LOCK_INTERVAL_IMMEDIATE,
label: "Immediately"
},
{
value: LOCK_INTERVAL_ONE_MINUTE,
label: "1m"
},
{
value: LOCK_INTERVAL_FIVE_MINUTES,
label: "5m"
},
{
value: LOCK_INTERVAL_ONE_HOUR,
label: "1h"
}
];
}
async documentVisibilityChanged(visible: boolean) {
if (visible) {
const locked = await this.application.isLocked();
if (
!locked &&
this.lockAfterDate &&
new Date() > this.lockAfterDate
) {
this.lockApplication();
}
this.cancelAutoLockTimer();
} else {
this.beginAutoLockTimer();
}
}
async beginAutoLockTimer() {
const interval = await this.getAutoLockInterval();
if (interval === LOCK_INTERVAL_NONE) {
return;
}
/**
* Use a timeout if possible, but if the computer is put to sleep, timeouts won't
* work. Need to set a date as backup. this.lockAfterDate does not need to be
* persisted, as living in memory is sufficient. If memory is cleared, then the
* application will lock anyway.
*/
const addToNow = (seconds: number) => {
const date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
};
this.lockAfterDate = addToNow(interval / MILLISECONDS_PER_SECOND);
clearTimeout(this.lockTimeout);
this.lockTimeout = setTimeout(() => {
this.cancelAutoLockTimer();
this.lockApplication();
this.lockAfterDate = undefined;
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
this.lockAfterDate = undefined;
this.lockTimeout = undefined;
}
}

View file

@ -0,0 +1,51 @@
import { PurePayload, Environment } from "snjs";
/** Platform-specific (i-e Electron/browser) behavior is handled by a Bridge object. */
export interface Bridge {
environment: Environment,
getKeychainValue(): Promise<unknown>;
setKeychainValue(value: any): Promise<void>;
clearKeychainValue(): Promise<void>;
extensionsServerHost?: string;
syncComponents(payloads: PurePayload[]): void;
onMajorDataChange(): void;
onInitialDataLoad(): void;
onSearch(text?: string): void;
downloadBackup(): void;
}
const KEYCHAIN_STORAGE_KEY = 'keychain';
export class BrowserBridge implements Bridge {
environment = Environment.Web;
async getKeychainValue(): Promise<unknown> {
const value = localStorage.getItem(KEYCHAIN_STORAGE_KEY);
if (value) {
return JSON.parse(value);
}
}
async setKeychainValue(value: any): Promise<void> {
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value));
}
async clearKeychainValue(): Promise<void> {
localStorage.removeItem(KEYCHAIN_STORAGE_KEY);
}
/** No-ops */
syncComponents() {
}
onMajorDataChange() {
}
onInitialDataLoad() {
}
onSearch() {
}
downloadBackup() {
}
}

View file

@ -1,50 +0,0 @@
import angular from 'angular';
import { SNComponentManager, SFAlertManager } from 'snjs';
import { isDesktopApplication, getPlatformString } from '@/utils';
export class ComponentManager extends SNComponentManager {
/* @ngInject */
constructor(
modelManager,
syncManager,
desktopManager,
nativeExtManager,
$rootScope,
$timeout,
$compile
) {
super({
modelManager,
syncManager,
desktopManager,
nativeExtManager,
alertManager: new SFAlertManager(),
$uiRunner: $rootScope.safeApply,
$timeout: $timeout,
environment: isDesktopApplication() ? "desktop" : "web",
platform: getPlatformString()
});
// this.loggingEnabled = true;
this.$compile = $compile;
this.$rootScope = $rootScope;
}
openModalComponent(component) {
var scope = this.$rootScope.$new(true);
scope.component = component;
var el = this.$compile( "<component-modal component='component' class='sk-modal'></component-modal>" )(scope);
angular.element(document.body).append(el);
}
presentPermissionsDialog(dialog) {
const scope = this.$rootScope.$new(true);
scope.permissionsString = dialog.permissionsString;
scope.component = dialog.component;
scope.callback = dialog.callback;
var el = this.$compile( "<permissions-modal component='component' permissions-string='permissionsString' callback='callback' class='sk-modal'></permissions-modal>" )(scope);
angular.element(document.body).append(el);
}
}

View file

@ -1,179 +0,0 @@
export class DBManager {
/* @ngInject */
constructor(alertManager) {
this.locked = true;
this.alertManager = alertManager;
}
displayOfflineAlert() {
var message = "There was an issue loading your offline database. This could happen for two reasons:";
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
message += "\n\n2. You have two windows of the app open at the same time. Please close any other app instances and reload the page.";
this.alertManager.alert({text: message});
}
setLocked(locked) {
this.locked = locked;
}
async openDatabase({onUpgradeNeeded} = {}) {
if(this.locked) {
return;
}
const request = window.indexedDB.open("standardnotes", 1);
return new Promise((resolve, reject) => {
request.onerror = (event) => {
if(event.target.errorCode) {
this.alertManager.alert({text: "Offline database issue: " + event.target.errorCode});
} else {
this.displayOfflineAlert();
}
console.error("Offline database issue:", event);
resolve(null);
};
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = function(event) {
db.close();
};
db.onerror = function(errorEvent) {
console.error("Database error: " + errorEvent.target.errorCode);
};
resolve(db);
};
request.onblocked = (event) => {
console.error("Request blocked error:", event.target.errorCode);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.onversionchange = function(event) {
db.close();
};
// Create an objectStore for this database
const objectStore = db.createObjectStore("items", { keyPath: "uuid" });
objectStore.createIndex("uuid", "uuid", { unique: true });
objectStore.transaction.oncomplete = function(event) {
// Ready to store values in the newly created objectStore.
if(db.version === 1 && onUpgradeNeeded) {
onUpgradeNeeded();
}
};
};
});
}
async getAllModels() {
const db = await this.openDatabase();
const objectStore = db.transaction("items").objectStore("items");
const items = [];
return new Promise(async (resolve, reject) => {
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
items.push(cursor.value);
cursor.continue();
} else {
resolve(items);
}
};
});
}
async saveModel(item) {
this.saveModels([item]);
}
async saveModels(items) {
const showGenericError = (error) => {
this.alertManager.alert({text: `Unable to save changes locally due to an unknown system issue. Issue Code: ${error.code} Issue Name: ${error.name}.`});
};
return new Promise(async (resolve, reject) => {
if(items.length === 0) {
resolve();
return;
}
const db = await this.openDatabase();
const transaction = db.transaction("items", "readwrite");
transaction.oncomplete = (event) => {};
transaction.onerror = function(event) {
console.error("Transaction error:", event.target.errorCode);
showGenericError(event.target.error);
};
transaction.onblocked = function(event) {
console.error("Transaction blocked error:", event.target.errorCode);
showGenericError(event.target.error);
};
transaction.onabort = function(event) {
console.error("Offline saving aborted:", event);
const error = event.target.error;
if(error.name == "QuotaExceededError") {
this.alertManager.alert({text: "Unable to save changes locally because your device is out of space. Please free up some disk space and try again, otherwise, your data may end up in an inconsistent state."});
} else {
showGenericError(error);
}
reject(error);
};
const itemObjectStore = transaction.objectStore("items");
const putItem = async (item) => {
return new Promise((resolve, reject) => {
const request = itemObjectStore.put(item);
request.onerror = (event) => {
console.error("DB put error:", event.target.error);
resolve();
};
request.onsuccess = resolve;
});
};
for(const item of items) {
await putItem(item);
}
resolve();
});
}
async deleteModel(item) {
return new Promise(async (resolve, reject) => {
const db = await this.openDatabase();
const request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
request.onsuccess = (event) => {
resolve();
};
request.onerror = (event) => {
reject();
};
});
}
async clearAllModels() {
const deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
return new Promise((resolve, reject) => {
deleteRequest.onerror = function(event) {
console.error("Error deleting database.");
resolve();
};
deleteRequest.onsuccess = function(event) {
resolve();
};
deleteRequest.onblocked = function(event) {
console.error("Delete request blocked");
this.alertManager.alert({text: "Your browser is blocking Standard Notes from deleting the local database. Make sure there are no other open windows of this app and try again. If the issue persists, please manually delete app data to sign out."});
resolve();
};
});
}
}

View file

@ -1,235 +0,0 @@
// An interface used by the Desktop app to interact with SN
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import { SFItemParams, SFModelManager } from 'snjs';
const COMPONENT_DATA_KEY_INSTALL_ERROR = 'installError';
const COMPONENT_CONTENT_KEY_PACKAGE_INFO = 'package_info';
const COMPONENT_CONTENT_KEY_LOCAL_URL = 'local_url';
export class DesktopManager {
/* @ngInject */
constructor(
$rootScope,
$timeout,
modelManager,
syncManager,
authManager,
passcodeManager,
appState
) {
this.passcodeManager = passcodeManager;
this.modelManager = modelManager;
this.authManager = authManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this.appState = appState;
this.timeout = $timeout;
this.updateObservers = [];
this.componentActivationObservers = [];
this.isDesktop = isDesktopApplication();
$rootScope.$on("initial-data-loaded", () => {
this.dataLoaded = true;
if(this.dataLoadHandler) {
this.dataLoadHandler();
}
});
$rootScope.$on("major-data-change", () => {
if(this.majorDataChangeHandler) {
this.majorDataChangeHandler();
}
});
}
saveBackup() {
this.majorDataChangeHandler && this.majorDataChangeHandler();
}
getExtServerHost() {
console.assert(
this.extServerHost,
'extServerHost is null'
);
return this.extServerHost;
}
/*
Sending a component in its raw state is really slow for the desktop app
Keys are not passed into ItemParams, so the result is not encrypted
*/
async convertComponentForTransmission(component) {
return new SFItemParams(component).paramsForExportFile(true);
}
// All `components` should be installed
syncComponentsInstallation(components) {
if(!this.isDesktop) {
return;
}
Promise.all(components.map((component) => {
return this.convertComponentForTransmission(component);
})).then((data) => {
this.installationSyncHandler(data);
});
}
async installComponent(component) {
this.installComponentHandler(
await this.convertComponentForTransmission(component)
);
}
registerUpdateObserver(callback) {
const observer = {
callback: callback
};
this.updateObservers.push(observer);
return observer;
}
searchText(text) {
if(!this.isDesktop) {
return;
}
this.lastSearchedText = text;
this.searchHandler && this.searchHandler(text);
}
redoSearch() {
if(this.lastSearchedText) {
this.searchText(this.lastSearchedText);
}
}
deregisterUpdateObserver(observer) {
_.pull(this.updateObservers, observer);
}
// Pass null to cancel search
desktop_setSearchHandler(handler) {
this.searchHandler = handler;
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast("window-gained-focus");
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast("window-lost-focus");
}
desktop_onComponentInstallationComplete(componentData, error) {
const component = this.modelManager.findItem(componentData.uuid);
if(!component) {
return;
}
if(error) {
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
error
);
} else {
const permissableKeys = [
COMPONENT_CONTENT_KEY_PACKAGE_INFO,
COMPONENT_CONTENT_KEY_LOCAL_URL
];
for(const key of permissableKeys) {
component[key] = componentData.content[key];
}
this.modelManager.notifySyncObserversOfModels(
[component],
SFModelManager.MappingSourceDesktopInstalled
);
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
null
);
}
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.timeout(() => {
for(const observer of this.updateObservers) {
observer.callback(component);
}
});
}
desktop_registerComponentActivationObserver(callback) {
const observer = {id: Math.random, callback: callback};
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(observer) {
_.pull(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component) {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.timeout(() => {
for(const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
/* Used to resolve "sn://" */
desktop_setExtServerHost(host) {
this.extServerHost = host;
this.appState.desktopExtensionsReady();
}
desktop_setComponentInstallationSyncHandler(handler) {
this.installationSyncHandler = handler;
}
desktop_setInstallComponentHandler(handler) {
this.installComponentHandler = handler;
}
desktop_setInitialDataLoadHandler(handler) {
this.dataLoadHandler = handler;
if(this.dataLoaded) {
this.dataLoadHandler();
}
}
async desktop_requestBackupFile(callback) {
let keys, authParams;
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
}
const nullOnEmpty = true;
this.modelManager.getAllItemsJSONData(
keys,
authParams,
nullOnEmpty
).then((data) => {
callback(data);
});
}
desktop_setMajorDataChangeHandler(handler) {
this.majorDataChangeHandler = handler;
}
desktop_didBeginBackup() {
this.appState.beganBackupDownload();
}
desktop_didFinishBackup(success) {
this.appState.endedBackupDownload({
success: success
});
}
}

View file

@ -0,0 +1,205 @@
import { SNComponent, PurePayload, ComponentMutator, AppDataField, ContentType } from 'snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/ui_models/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from 'snjs';
import { Bridge } from './bridge';
type UpdateObserverCallback = (component: SNComponent) => void
type ComponentActivationCallback = (payload: PurePayload) => void
type ComponentActivationObserver = {
id: string,
callback: ComponentActivationCallback
}
export class DesktopManager extends ApplicationService {
$rootScope: ng.IRootScopeService
$timeout: ng.ITimeoutService
componentActivationObservers: ComponentActivationObserver[] = []
updateObservers: {
callback: UpdateObserverCallback
}[] = []
isDesktop = isDesktopApplication();
dataLoaded = false
lastSearchedText?: string
private removeComponentObserver?: () => void;
constructor(
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService,
application: WebApplication,
private bridge: Bridge,
) {
super(application);
this.$rootScope = $rootScope;
this.$timeout = $timeout;
}
get webApplication() {
return this.application as WebApplication;
}
deinit() {
this.componentActivationObservers.length = 0;
this.updateObservers.length = 0;
this.removeComponentObserver?.();
this.removeComponentObserver = undefined;
super.deinit();
}
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataLoaded) {
this.dataLoaded = true;
this.bridge.onInitialDataLoad();
} else if (eventName === ApplicationEvent.MajorDataChange) {
this.bridge.onMajorDataChange();
}
}
saveBackup() {
this.bridge.onMajorDataChange();
}
getExtServerHost() {
console.assert(
this.bridge.extensionsServerHost,
'extServerHost is null'
);
return this.bridge.extensionsServerHost;
}
/**
* Sending a component in its raw state is really slow for the desktop app
* Keys are not passed into ItemParams, so the result is not encrypted
*/
convertComponentForTransmission(component: SNComponent) {
return this.application!.protocolService!.payloadByEncryptingPayload(
component.payloadRepresentation(),
EncryptionIntent.FileDecrypted
);
}
// All `components` should be installed
syncComponentsInstallation(components: SNComponent[]) {
if (!this.isDesktop) {
return;
}
Promise.all(components.map((component) => {
return this.convertComponentForTransmission(component);
})).then((payloads) => {
this.bridge.syncComponents(
payloads.filter(payload =>
!payload.errorDecrypting && !payload.waitingForKey
)
);
});
}
registerUpdateObserver(callback: UpdateObserverCallback) {
const observer = {
callback: callback
};
this.updateObservers.push(observer);
return () => {
removeFromArray(this.updateObservers, observer);
};
}
searchText(text?: string) {
if (!this.isDesktop) {
return;
}
this.lastSearchedText = text;
this.bridge.onSearch(text);
}
redoSearch() {
if (this.lastSearchedText) {
this.searchText(this.lastSearchedText);
}
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast('window-gained-focus');
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast('window-lost-focus');
}
async desktop_onComponentInstallationComplete(
componentData: any,
error: any
) {
const component = this.application!.findItem(componentData.uuid);
if (!component) {
return;
}
const updatedComponent = await this.application!.changeAndSaveItem(
component.uuid,
(m) => {
const mutator = m as ComponentMutator;
if (error) {
mutator.setAppDataItem(
AppDataField.ComponentInstallError,
error
);
} else {
mutator.local_url = componentData.content.local_url;
mutator.package_info = componentData.content.package_info;
mutator.setAppDataItem(
AppDataField.ComponentInstallError,
undefined
);
}
})
this.$timeout(() => {
for (const observer of this.updateObservers) {
observer.callback(updatedComponent as SNComponent);
}
});
}
desktop_registerComponentActivationObserver(callback: ComponentActivationCallback) {
const observer = { id: `${Math.random}`, callback: callback };
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(observer: ComponentActivationObserver) {
removeFromArray(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component: SNComponent) {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.$timeout(() => {
for (const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
desktop_requestBackupFile() {
return this.application!.createBackupFile(
undefined,
undefined,
true
);
}
desktop_didBeginBackup() {
this.webApplication.getAppState().beganBackupDownload();
}
desktop_didFinishBackup(success: boolean) {
this.webApplication.getAppState().endedBackupDownload(success);
}
}

View file

@ -1,13 +0,0 @@
import { SFHttpManager } from 'snjs';
export class HttpManager extends SFHttpManager {
/* @ngInject */
constructor(storageManager, $timeout) {
// calling callbacks in a $timeout allows UI to update
super($timeout);
this.setJWTRequestHandler(async () => {
return storageManager.getItem('jwt');
});
}
}

View file

@ -1,21 +0,0 @@
export { ActionsManager } from './actionsManager';
export { ArchiveManager } from './archiveManager';
export { AuthManager } from './authManager';
export { ComponentManager } from './componentManager';
export { DBManager } from './dbManager';
export { DesktopManager } from './desktopManager';
export { HttpManager } from './httpManager';
export { KeyboardManager } from './keyboardManager';
export { MigrationManager } from './migrationManager';
export { ModelManager } from './modelManager';
export { NativeExtManager } from './nativeExtManager';
export { PasscodeManager } from './passcodeManager';
export { PrivilegesManager } from './privilegesManager';
export { SessionHistory } from './sessionHistory';
export { SingletonManager } from './singletonManager';
export { StatusManager } from './statusManager';
export { StorageManager } from './storageManager';
export { SyncManager } from './syncManager';
export { ThemeManager } from './themeManager';
export { AlertManager } from './alertManager';
export { PreferencesManager } from './preferencesManager';

View file

@ -0,0 +1,9 @@
export { AlertService } from './alertService';
export { ArchiveManager } from './archiveManager';
export { DesktopManager } from './desktopManager';
export { KeyboardManager } from './keyboardManager';
export { AutolockService } from './autolock_service';
export { NativeExtManager } from './nativeExtManager';
export { PreferencesManager } from './preferencesManager';
export { StatusManager } from './statusManager';
export { ThemeManager } from './themeManager';

View file

@ -1,115 +0,0 @@
export class KeyboardManager {
constructor() {
this.observers = [];
KeyboardManager.KeyTab = "Tab";
KeyboardManager.KeyBackspace = "Backspace";
KeyboardManager.KeyUp = "ArrowUp";
KeyboardManager.KeyDown = "ArrowDown";
KeyboardManager.KeyModifierShift = "Shift";
KeyboardManager.KeyModifierCtrl = "Control";
// ⌘ key on Mac, ⊞ key on Windows
KeyboardManager.KeyModifierMeta = "Meta";
KeyboardManager.KeyModifierAlt = "Alt";
KeyboardManager.KeyEventDown = "KeyEventDown";
KeyboardManager.KeyEventUp = "KeyEventUp";
KeyboardManager.AllModifiers = [
KeyboardManager.KeyModifierShift,
KeyboardManager.KeyModifierCtrl,
KeyboardManager.KeyModifierMeta,
KeyboardManager.KeyModifierAlt
];
window.addEventListener('keydown', this.handleKeyDown.bind(this));
window.addEventListener('keyup', this.handleKeyUp.bind(this));
}
modifiersForEvent(event) {
const eventModifiers = KeyboardManager.AllModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches = (
((event.ctrlKey || event.key == KeyboardManager.KeyModifierCtrl) && modifier === KeyboardManager.KeyModifierCtrl) ||
((event.metaKey || event.key == KeyboardManager.KeyModifierMeta) && modifier === KeyboardManager.KeyModifierMeta) ||
((event.altKey || event.key == KeyboardManager.KeyModifierAlt) && modifier === KeyboardManager.KeyModifierAlt) ||
((event.shiftKey || event.key == KeyboardManager.KeyModifierShift) && modifier === KeyboardManager.KeyModifierShift)
);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
const eventModifiers = this.modifiersForEvent(event);
if(eventModifiers.length != modifiers.length) {
return false;
}
for(const modifier of modifiers) {
if(!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if(!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() == event.key.toLowerCase();
}
notifyObserver(event, keyEventType) {
for(const observer of this.observers) {
if(observer.element && event.target != observer.element) {
continue;
}
if(observer.elements && !observer.elements.includes(event.target)) {
continue;
}
if(observer.notElement && observer.notElement == event.target) {
continue;
}
if(observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
continue;
}
if(this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
const callback = keyEventType == KeyboardManager.KeyEventDown ? observer.onKeyDown : observer.onKeyUp;
if(callback) {
callback(event);
}
}
}
}
handleKeyDown(event) {
this.notifyObserver(event, KeyboardManager.KeyEventDown);
}
handleKeyUp(event) {
this.notifyObserver(event, KeyboardManager.KeyEventUp);
}
addKeyObserver({key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds}) {
const observer = {key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds};
this.observers.push(observer);
return observer;
}
removeKeyObserver(observer) {
this.observers.splice(this.observers.indexOf(observer), 1);
}
}

View file

@ -0,0 +1,147 @@
import { removeFromArray } from 'snjs';
export enum KeyboardKey {
Tab = "Tab",
Backspace = "Backspace",
Up = "ArrowUp",
Down = "ArrowDown",
};
export enum KeyboardModifier {
Shift = "Shift",
Ctrl = "Control",
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = "Meta",
Alt = "Alt",
};
enum KeyboardKeyEvent {
Down = "KeyEventDown",
Up = "KeyEventUp"
};
type KeyboardObserver = {
key?: KeyboardKey | string
modifiers?: KeyboardModifier[]
onKeyDown?: (event: KeyboardEvent) => void
onKeyUp?: (event: KeyboardEvent) => void
element?: HTMLElement
elements?: HTMLElement[]
notElement?: HTMLElement
notElementIds?: string[]
}
export class KeyboardManager {
private observers: KeyboardObserver[] = []
private handleKeyDown: any
private handleKeyUp: any
constructor() {
this.handleKeyDown = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Down);
}
this.handleKeyUp = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Up);
}
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
public deinit() {
this.observers.length = 0;
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
this.handleKeyDown = undefined;
this.handleKeyUp = undefined;
}
modifiersForEvent(event: KeyboardEvent) {
const allModifiers = Object.values(KeyboardModifier);
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches = (
(
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
&& modifier === KeyboardModifier.Ctrl
) ||
(
(event.metaKey || event.key === KeyboardModifier.Meta)
&& modifier === KeyboardModifier.Meta
) ||
(
(event.altKey || event.key === KeyboardModifier.Alt)
&& modifier === KeyboardModifier.Alt
) ||
(
(event.shiftKey || event.key === KeyboardModifier.Shift)
&& modifier === KeyboardModifier.Shift
)
);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(
event: KeyboardEvent,
key: KeyboardKey | string,
modifiers: KeyboardModifier[] = []
) {
const eventModifiers = this.modifiersForEvent(event);
if (eventModifiers.length !== modifiers.length) {
return false;
}
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if (!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() === event.key.toLowerCase();
}
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
const target = event.target as HTMLElement;
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue;
}
if (observer.elements && !observer.elements.includes(target)) {
continue;
}
if (observer.notElement && observer.notElement === event.target) {
continue;
}
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
continue;
}
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
const callback = keyEvent === KeyboardKeyEvent.Down
? observer.onKeyDown
: observer.onKeyUp;
if (callback) {
callback(event);
}
}
}
}
addKeyObserver(observer: KeyboardObserver) {
this.observers.push(observer);
return () => {
removeFromArray(this.observers, observer);
};
}
}

View file

@ -1,172 +0,0 @@
import { isDesktopApplication } from '@/utils';
import { SFMigrationManager } from 'snjs';
import { ComponentManager } from '@/services/componentManager';
export class MigrationManager extends SFMigrationManager {
/* @ngInject */
constructor(
modelManager,
syncManager,
componentManager,
storageManager,
statusManager,
authManager,
desktopManager
) {
super(modelManager, syncManager, storageManager, authManager);
this.componentManager = componentManager;
this.statusManager = statusManager;
this.desktopManager = desktopManager;
}
registeredMigrations() {
return [
this.editorToComponentMigration(),
this.componentUrlToHostedUrl(),
this.removeTagReferencesFromNotes()
];
}
/*
Migrate SN|Editor to SN|Component. Editors are deprecated as of November 2017. Editors using old APIs must
convert to using the new component API.
*/
editorToComponentMigration() {
return {
name: "editor-to-component",
content_type: "SN|Editor",
handler: async (editors) => {
// Convert editors to components
for(var editor of editors) {
// If there's already a component for this url, then skip this editor
if(editor.url && !this.componentManager.componentForUrl(editor.url)) {
var component = this.modelManager.createItem({
content_type: "SN|Component",
content: {
url: editor.url,
name: editor.name,
area: "editor-editor"
}
});
component.setAppDataItem("data", editor.data);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
}
}
for(const editor of editors) {
this.modelManager.setItemToBeDeleted(editor);
}
this.syncManager.sync();
}
};
}
/*
Migrate component.url fields to component.hosted_url. This involves rewriting any note data that relied on the
component.url value to store clientData, such as the CodeEditor, which stores the programming language for the note
in the note's clientData[component.url]. We want to rewrite any matching items to transfer that clientData into
clientData[component.uuid].
April 3, 2019 note: it seems this migration is mis-named. The first part of the description doesn't match what the code is actually doing.
It has nothing to do with url/hosted_url relationship and more to do with just mapping client data from the note's hosted_url to its uuid
Created: July 6, 2018
*/
componentUrlToHostedUrl() {
return {
name: "component-url-to-hosted-url",
content_type: "SN|Component",
handler: async (components) => {
let hasChanges = false;
const notes = this.modelManager.validItemsForContentType("Note");
for(const note of notes) {
for(const component of components) {
const clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
if(clientData) {
note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain);
note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain);
this.modelManager.setItemDirty(note, true);
hasChanges = true;
}
}
}
if(hasChanges) {
this.syncManager.sync();
}
}
};
}
/*
Migrate notes which have relationships on tags to migrate those relationships to the tags themselves.
That is, notes.content.references should not include any mention of tags.
This will apply to notes created before the schema change. Now, only tags reference notes.
Created: April 3, 2019
*/
removeTagReferencesFromNotes() {
return {
name: "remove-tag-references-from-notes",
content_type: "Note",
handler: async (notes) => {
const needsSync = false;
let status = this.statusManager.addStatusFromString("Optimizing data...");
let dirtyCount = 0;
for(const note of notes) {
if(!note.content) {
continue;
}
const references = note.content.references;
// Remove any tag references, and transfer them to the tag if neccessary.
const newReferences = [];
for(const reference of references) {
if(reference.content_type != "Tag") {
newReferences.push(reference);
continue;
}
// is Tag content_type, we will not be adding this to newReferences
const tag = this.modelManager.findItem(reference.uuid);
if(tag && !tag.hasRelationshipWithItem(note)) {
tag.addItemAsRelationship(note);
this.modelManager.setItemDirty(tag, true);
dirtyCount++;
}
}
if(newReferences.length != references.length) {
note.content.references = newReferences;
this.modelManager.setItemDirty(note, true);
dirtyCount++;
}
}
if(dirtyCount > 0) {
if(isDesktopApplication()) {
this.desktopManager.saveBackup();
}
status = this.statusManager.replaceStatusWithString(status, `${dirtyCount} items optimized.`);
await this.syncManager.sync();
status = this.statusManager.replaceStatusWithString(status, `Optimization complete.`);
setTimeout(() => {
this.statusManager.removeStatus(status);
}, 2000);
} else {
this.statusManager.removeStatus(status);
}
}
};
}
}

View file

@ -1,170 +0,0 @@
import _ from 'lodash';
import { SFModelManager, SNSmartTag, SFPredicate } from 'snjs';
export class ModelManager extends SFModelManager {
/* @ngInject */
constructor(storageManager, $timeout) {
super($timeout);
this.notes = [];
this.tags = [];
this.components = [];
this.storageManager = storageManager;
this.buildSystemSmartTags();
}
handleSignout() {
super.handleSignout();
this.notes.length = 0;
this.tags.length = 0;
this.components.length = 0;
}
noteCount() {
return this.notes.filter((n) => !n.dummy).length;
}
removeAllItemsFromMemory() {
for(var item of this.items) {
item.deleted = true;
}
this.notifySyncObserversOfModels(this.items);
this.handleSignout();
}
findTag(title) {
return _.find(this.tags, { title: title });
}
findOrCreateTagByTitle(title) {
let tag = this.findTag(title);
if(!tag) {
tag = this.createItem({content_type: "Tag", content: {title: title}});
this.addItem(tag);
this.setItemDirty(tag, true);
}
return tag;
}
addItems(items, globalOnly = false) {
super.addItems(items, globalOnly);
items.forEach((item) => {
// In some cases, you just want to add the item to this.items, and not to the individual arrays
// This applies when you want to keep an item syncable, but not display it via the individual arrays
if(!globalOnly) {
if(item.content_type == "Tag") {
if(!_.find(this.tags, {uuid: item.uuid})) {
this.tags.splice(_.sortedIndexBy(this.tags, item, function(item){
if (item.title) return item.title.toLowerCase();
else return '';
}), 0, item);
}
} else if(item.content_type == "Note") {
if(!_.find(this.notes, {uuid: item.uuid})) {
this.notes.unshift(item);
}
} else if(item.content_type == "SN|Component") {
if(!_.find(this.components, {uuid: item.uuid})) {
this.components.unshift(item);
}
}
}
});
}
resortTag(tag) {
_.pull(this.tags, tag);
this.tags.splice(_.sortedIndexBy(this.tags, tag, function(tag){
if (tag.title) return tag.title.toLowerCase();
else return '';
}), 0, tag);
}
setItemToBeDeleted(item) {
super.setItemToBeDeleted(item);
// remove from relevant array, but don't remove from all items.
// This way, it's removed from the display, but still synced via get dirty items
this.removeItemFromRespectiveArray(item);
}
removeItemLocally(item, callback) {
super.removeItemLocally(item, callback);
this.removeItemFromRespectiveArray(item);
this.storageManager.deleteModel(item).then(callback);
}
removeItemFromRespectiveArray(item) {
if(item.content_type == "Tag") {
_.remove(this.tags, {uuid: item.uuid});
} else if(item.content_type == "Note") {
_.remove(this.notes, {uuid: item.uuid});
} else if(item.content_type == "SN|Component") {
_.remove(this.components, {uuid: item.uuid});
}
}
notesMatchingSmartTag(tag) {
const contentTypePredicate = new SFPredicate("content_type", "=", "Note");
const predicates = [contentTypePredicate, tag.content.predicate];
if(!tag.content.isTrashTag) {
const notTrashedPredicate = new SFPredicate("content.trashed", "=", false);
predicates.push(notTrashedPredicate);
}
const results = this.itemsMatchingPredicates(predicates);
return results;
}
trashSmartTag() {
return this.systemSmartTags.find((tag) => tag.content.isTrashTag);
}
trashedItems() {
return this.notesMatchingSmartTag(this.trashSmartTag());
}
emptyTrash() {
const notes = this.trashedItems();
for(const note of notes) {
this.setItemToBeDeleted(note);
}
}
buildSystemSmartTags() {
this.systemSmartTags = SNSmartTag.systemSmartTags();
}
getSmartTagWithId(id) {
return this.getSmartTags().find((candidate) => candidate.uuid == id);
}
getSmartTags() {
const userTags = this.validItemsForContentType("SN|SmartTag").sort((a, b) => {
return a.content.title < b.content.title ? -1 : 1;
});
return this.systemSmartTags.concat(userTags);
}
/*
Misc
*/
humanReadableDisplayForContentType(contentType) {
return {
"Note" : "note",
"Tag" : "tag",
"SN|SmartTag": "smart tag",
"Extension" : "action-based extension",
"SN|Component" : "component",
"SN|Editor" : "editor",
"SN|Theme" : "theme",
"SF|Extension" : "server extension",
"SF|MFA" : "two-factor authentication setting",
"SN|FileSafe|Credentials": "FileSafe credential",
"SN|FileSafe|FileMetadata": "FileSafe file",
"SN|FileSafe|Integration": "FileSafe integration"
}[contentType];
}
}

View file

@ -1,183 +0,0 @@
/* A class for handling installation of system extensions */
import { isDesktopApplication } from '@/utils';
import { SFPredicate } from 'snjs';
export class NativeExtManager {
/* @ngInject */
constructor(modelManager, syncManager, singletonManager) {
this.modelManager = modelManager;
this.syncManager = syncManager;
this.singletonManager = singletonManager;
this.extManagerId = "org.standardnotes.extensions-manager";
this.batchManagerId = "org.standardnotes.batch-manager";
this.systemExtensions = [];
this.resolveExtensionsManager();
this.resolveBatchManager();
}
isSystemExtension(extension) {
return this.systemExtensions.includes(extension.uuid);
}
resolveExtensionsManager() {
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
const packagePredicate = new SFPredicate("package_info.identifier", "=", this.extManagerId);
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
// Resolved Singleton
this.systemExtensions.push(resolvedSingleton.uuid);
var needsSync = false;
if(isDesktopApplication()) {
if(!resolvedSingleton.local_url) {
resolvedSingleton.local_url = window._extensions_manager_location;
needsSync = true;
}
} else {
if(!resolvedSingleton.hosted_url) {
resolvedSingleton.hosted_url = window._extensions_manager_location;
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permission = resolvedSingleton.content.permissions.find((p) => p.name == "stream-items");
if(!permission.content_types.includes("SN|ExtensionRepo")) {
permission.content_types.push("SN|ExtensionRepo");
needsSync = true;
}
if(needsSync) {
this.modelManager.setItemDirty(resolvedSingleton, true);
this.syncManager.sync();
}
}, (valueCallback) => {
// Safe to create. Create and return object.
const url = window._extensions_manager_location;
if(!url) {
console.error("window._extensions_manager_location must be set.");
return;
}
const packageInfo = {
name: "Extensions",
identifier: this.extManagerId
};
var item = {
content_type: "SN|Component",
content: {
name: packageInfo.name,
area: "rooms",
package_info: packageInfo,
permissions: [
{
name: "stream-items",
content_types: [
"SN|Component", "SN|Theme", "SF|Extension",
"Extension", "SF|MFA", "SN|Editor", "SN|ExtensionRepo"
]
}
]
}
};
if(isDesktopApplication()) {
item.content.local_url = window._extensions_manager_location;
} else {
item.content.hosted_url = window._extensions_manager_location;
}
var component = this.modelManager.createItem(item);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
this.systemExtensions.push(component.uuid);
valueCallback(component);
});
}
resolveBatchManager() {
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
const packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerId);
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
// Resolved Singleton
this.systemExtensions.push(resolvedSingleton.uuid);
var needsSync = false;
if(isDesktopApplication()) {
if(!resolvedSingleton.local_url) {
resolvedSingleton.local_url = window._batch_manager_location;
needsSync = true;
}
} else {
if(!resolvedSingleton.hosted_url) {
resolvedSingleton.hosted_url = window._batch_manager_location;
needsSync = true;
}
}
if(needsSync) {
this.modelManager.setItemDirty(resolvedSingleton, true);
this.syncManager.sync();
}
}, (valueCallback) => {
// Safe to create. Create and return object.
const url = window._batch_manager_location;
if(!url) {
console.error("window._batch_manager_location must be set.");
return;
}
const packageInfo = {
name: "Batch Manager",
identifier: this.batchManagerId
};
var item = {
content_type: "SN|Component",
content: {
name: packageInfo.name,
area: "modal",
package_info: packageInfo,
permissions: [
{
name: "stream-items",
content_types: [
"Note", "Tag", "SN|SmartTag",
"SN|Component", "SN|Theme", "SN|UserPreferences",
"SF|Extension", "Extension", "SF|MFA", "SN|Editor",
"SN|FileSafe|Credentials", "SN|FileSafe|FileMetadata", "SN|FileSafe|Integration"
]
}
]
}
};
if(isDesktopApplication()) {
item.content.local_url = window._batch_manager_location;
} else {
item.content.hosted_url = window._batch_manager_location;
}
var component = this.modelManager.createItem(item);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
this.systemExtensions.push(component.uuid);
valueCallback(component);
});
}
}

View file

@ -0,0 +1,206 @@
import { isDesktopApplication } from '@/utils';
import {
SNPredicate,
ContentType,
SNComponent,
ApplicationService,
ComponentAction,
FillItemContent,
ComponentMutator,
Copy,
dictToArray
} from 'snjs';
import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator';
import { ComponentPermission } from 'snjs/dist/@types/models/app/component';
/** A class for handling installation of system extensions */
export class NativeExtManager extends ApplicationService {
extManagerId = 'org.standardnotes.extensions-manager';
batchManagerId = 'org.standardnotes.batch-manager';
/** @override */
async onAppLaunch() {
super.onAppLaunch();
this.reload();
}
get extManagerPred() {
const extManagerId = 'org.standardnotes.extensions-manager';
return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', extManagerId)
]);
}
get batchManagerPred() {
const batchMgrId = 'org.standardnotes.batch-manager';
return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', batchMgrId)
]);
}
get extMgrUrl() {
return (window as any)._extensions_manager_location;
}
get batchMgrUrl() {
return (window as any)._batch_manager_location;
}
reload() {
this.application!.singletonManager!.registerPredicate(this.extManagerPred);
this.application!.singletonManager!.registerPredicate(this.batchManagerPred);
this.resolveExtensionsManager();
this.resolveBatchManager();
}
async resolveExtensionsManager() {
const extensionsManager = (await this.application!.singletonManager!.findOrCreateSingleton(
this.extManagerPred,
ContentType.Component,
this.extensionsManagerTemplateContent()
)) as SNComponent;
let needsSync = false;
if (isDesktopApplication()) {
if (!extensionsManager.local_url) {
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.local_url = this.extMgrUrl;
});
needsSync = true;
}
} else {
if (!extensionsManager.hosted_url) {
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.hosted_url = this.extMgrUrl;
});
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.permissions = permissions;
});
needsSync = true;
}
if (needsSync) {
this.application!.saveItem(extensionsManager.uuid);
}
}
extensionsManagerTemplateContent() {
const url = this.extMgrUrl;
if (!url) {
throw Error('this.extMgrUrl must be set.');
}
const packageInfo = {
name: 'Extensions',
identifier: this.extManagerId
};
const content = FillItemContent({
name: packageInfo.name,
area: 'rooms',
package_info: packageInfo,
permissions: [
{
name: ComponentAction.StreamItems,
content_types: [
ContentType.Component,
ContentType.Theme,
ContentType.ServerExtension,
ContentType.ActionsExtension,
ContentType.Mfa,
ContentType.Editor,
ContentType.ExtensionRepo
]
}
]
}) as PayloadContent;
if (isDesktopApplication()) {
content.local_url = this.extMgrUrl;
} else {
content.hosted_url = this.extMgrUrl;
}
return content;
}
async resolveBatchManager() {
const batchManager = (await this.application!.singletonManager!.findOrCreateSingleton(
this.batchManagerPred,
ContentType.Component,
this.batchManagerTemplateContent()
)) as SNComponent;
let needsSync = false;
if (isDesktopApplication()) {
if (!batchManager.local_url) {
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.local_url = this.batchMgrUrl;
});
needsSync = true;
}
} else {
if (!batchManager.hosted_url) {
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.hosted_url = this.batchMgrUrl;
});
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.permissions = permissions;
});
needsSync = true;
}
if (needsSync) {
this.application!.saveItem(batchManager.uuid);
}
}
batchManagerTemplateContent() {
const url = this.batchMgrUrl;
if (!url) {
throw Error('window._batch_manager_location must be set.');
}
const packageInfo = {
name: 'Batch Manager',
identifier: this.batchManagerId
};
const allContentType = dictToArray(ContentType);
const content = FillItemContent({
name: packageInfo.name,
area: 'modal',
package_info: packageInfo,
permissions: [
{
name: ComponentAction.StreamItems,
content_types: allContentType
}
]
});
if (isDesktopApplication()) {
content.local_url = this.batchMgrUrl;
} else {
content.hosted_url = this.batchMgrUrl;
}
return content;
}
}

View file

@ -1,286 +0,0 @@
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import { StorageManager } from './storageManager';
import { protocolManager } from 'snjs';
const MillisecondsPerSecond = 1000;
export class PasscodeManager {
/* @ngInject */
constructor($rootScope, authManager, storageManager, syncManager) {
this.authManager = authManager;
this.storageManager = storageManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._locked = this._hasPasscode;
this.visibilityObservers = [];
this.passcodeChangeObservers = [];
this.configureAutoLock();
}
addPasscodeChangeObserver(callback) {
this.passcodeChangeObservers.push(callback);
}
lockApplication() {
window.location.reload();
this.cancelAutoLockTimer();
}
isLocked() {
return this._locked;
}
hasPasscode() {
return this._hasPasscode;
}
keys() {
return this._keys;
}
addVisibilityObserver(callback) {
this.visibilityObservers.push(callback);
return callback;
}
removeVisibilityObserver(callback) {
_.pull(this.visibilityObservers, callback);
}
notifiyVisibilityObservers(visible) {
for(const callback of this.visibilityObservers) {
callback(visible);
}
}
async setAutoLockInterval(interval) {
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
}
async getAutoLockInterval() {
const interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
if(interval) {
return JSON.parse(interval);
} else {
return PasscodeManager.AutoLockIntervalNone;
}
}
passcodeAuthParams() {
var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed));
if(authParams && !authParams.version) {
var keys = this.keys();
if(keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams.
authParams.version = "002";
} else {
authParams.version = "001";
}
}
return authParams;
}
async verifyPasscode(passcode) {
return new Promise(async (resolve, reject) => {
var params = this.passcodeAuthParams();
const keys = await protocolManager.computeEncryptionKeysForUser(passcode, params);
if(keys.pw !== params.hash) {
resolve(false);
} else {
resolve(true);
}
});
}
unlock(passcode, callback) {
var params = this.passcodeAuthParams();
protocolManager.computeEncryptionKeysForUser(passcode, params).then((keys) => {
if(keys.pw !== params.hash) {
callback(false);
return;
}
this._keys = keys;
this._authParams = params;
this.decryptLocalStorage(keys, params).then(() => {
this._locked = false;
callback(true);
});
});
}
setPasscode(passcode, callback) {
var uuid = protocolManager.crypto.generateUUIDSync();
protocolManager.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
const keys = results.keys;
const authParams = results.authParams;
authParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;
this._authParams = authParams;
// Encrypting will initially clear localStorage
this.encryptLocalStorage(keys, authParams);
// After it's cleared, it's safe to write to it
this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
callback(true);
this.notifyObserversOfPasscodeChange();
});
}
changePasscode(newPasscode, callback) {
this.setPasscode(newPasscode, callback);
}
clearPasscode() {
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
this.storageManager.removeItem("offlineParams", StorageManager.Fixed);
this._keys = null;
this._hasPasscode = false;
this.notifyObserversOfPasscodeChange();
}
notifyObserversOfPasscodeChange() {
for(var observer of this.passcodeChangeObservers) {
observer();
}
}
encryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
// Switch to Ephemeral storage, wiping Fixed storage
// Last argument is `force`, which we set to true because in the case of changing passcode
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
}
async decryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
return this.storageManager.decryptStorage();
}
configureAutoLock() {
PasscodeManager.AutoLockPollFocusInterval = 1 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalNone = 0;
PasscodeManager.AutoLockIntervalImmediate = 1;
PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
if(isDesktopApplication()) {
// desktop only
this.$rootScope.$on("window-lost-focus", () => {
this.documentVisibilityChanged(false);
});
this.$rootScope.$on("window-gained-focus", () => {
this.documentVisibilityChanged(true);
});
} else {
// tab visibility listener, web only
document.addEventListener('visibilitychange', (e) => {
const visible = document.visibilityState === "visible";
this.documentVisibilityChanged(visible);
});
// verify document is in focus every so often as visibilitychange event is not triggered
// on a typical window blur event but rather on tab changes
this.pollFocusTimeout = setInterval(() => {
const hasFocus = document.hasFocus();
if(hasFocus && this.lastFocusState === "hidden") {
this.documentVisibilityChanged(true);
} else if(!hasFocus && this.lastFocusState === "visible") {
this.documentVisibilityChanged(false);
}
// save this to compare against next time around
this.lastFocusState = hasFocus ? "visible" : "hidden";
}, PasscodeManager.AutoLockPollFocusInterval);
}
}
getAutoLockIntervalOptions() {
return [
{
value: PasscodeManager.AutoLockIntervalNone,
label: "Off"
},
{
value: PasscodeManager.AutoLockIntervalImmediate,
label: "Immediately"
},
{
value: PasscodeManager.AutoLockIntervalOneMinute,
label: "1m"
},
{
value: PasscodeManager.AutoLockIntervalFiveMinutes,
label: "5m"
},
{
value: PasscodeManager.AutoLockIntervalOneHour,
label: "1h"
}
];
}
documentVisibilityChanged(visible) {
if(visible) {
// check to see if lockAfterDate is not null, and if the application isn't locked.
// if that's the case, it needs to be locked immediately.
if(this.lockAfterDate && new Date() > this.lockAfterDate && !this.isLocked()) {
this.lockApplication();
} else {
if(!this.isLocked()) {
this.syncManager.sync();
}
}
this.cancelAutoLockTimer();
} else {
this.beginAutoLockTimer();
}
this.notifiyVisibilityObservers(visible);
}
async beginAutoLockTimer() {
var interval = await this.getAutoLockInterval();
if(interval == PasscodeManager.AutoLockIntervalNone) {
return;
}
// Use a timeout if possible, but if the computer is put to sleep, timeouts won't work.
// Need to set a date as backup. this.lockAfterDate does not need to be persisted, as
// living in memory seems sufficient. If memory is cleared, then the application will lock anyway.
const addToNow = (seconds) => {
const date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
};
this.lockAfterDate = addToNow(interval / MillisecondsPerSecond);
clearTimeout(this.lockTimeout);
this.lockTimeout = setTimeout(() => {
this.lockApplication();
// We don't need to look at this anymore since we've succeeded with timeout lock
this.lockAfterDate = null;
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
this.lockAfterDate = null;
}
}

View file

@ -1,86 +0,0 @@
import { SFPredicate, SFItem } from 'snjs';
export const PREF_TAGS_PANEL_WIDTH = 'tagsPanelWidth';
export const PREF_NOTES_PANEL_WIDTH = 'notesPanelWidth';
export const PREF_EDITOR_WIDTH = 'editorWidth';
export const PREF_EDITOR_LEFT = 'editorLeft';
export const PREF_EDITOR_MONOSPACE_ENABLED = 'monospaceFont';
export const PREF_EDITOR_SPELLCHECK = 'spellcheck';
export const PREF_EDITOR_RESIZERS_ENABLED = 'marginResizersEnabled';
export const PREF_SORT_NOTES_BY = 'sortBy';
export const PREF_SORT_NOTES_REVERSE = 'sortReverse';
export const PREF_NOTES_SHOW_ARCHIVED = 'showArchived';
export const PREF_NOTES_HIDE_PINNED = 'hidePinned';
export const PREF_NOTES_HIDE_NOTE_PREVIEW = 'hideNotePreview';
export const PREF_NOTES_HIDE_DATE = 'hideDate';
export const PREF_NOTES_HIDE_TAGS = 'hideTags';
export class PreferencesManager {
/* @ngInject */
constructor(
modelManager,
singletonManager,
appState,
syncManager
) {
this.singletonManager = singletonManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.appState = appState;
this.modelManager.addItemSyncObserver(
'user-prefs',
'SN|UserPreferences',
(allItems, validItems, deletedItems, source, sourceKey) => {
this.preferencesDidChange();
}
);
}
load() {
const prefsContentType = 'SN|UserPreferences';
const contentTypePredicate = new SFPredicate(
'content_type',
'=',
prefsContentType
);
this.singletonManager.registerSingleton(
[contentTypePredicate],
(resolvedSingleton) => {
this.userPreferences = resolvedSingleton;
},
(valueCallback) => {
// Safe to create. Create and return object.
const prefs = new SFItem({content_type: prefsContentType});
this.modelManager.addItem(prefs);
this.modelManager.setItemDirty(prefs);
this.syncManager.sync();
valueCallback(prefs);
}
);
}
preferencesDidChange() {
this.appState.setUserPreferences(this.userPreferences);
}
syncUserPreferences() {
if(this.userPreferences) {
this.modelManager.setItemDirty(this.userPreferences);
this.syncManager.sync();
}
}
getValue(key, defaultValue) {
if(!this.userPreferences) { return defaultValue; }
const value = this.userPreferences.getAppDataItem(key);
return (value !== undefined && value != null) ? value : defaultValue;
}
setUserPrefValue(key, value, sync) {
this.userPreferences.setAppDataItem(key, value);
if(sync) {
this.syncUserPreferences();
}
}
}

View file

@ -0,0 +1,99 @@
import { WebApplication } from '@/ui_models/application';
import {
SNPredicate,
ContentType,
ApplicationService,
SNUserPrefs,
WebPrefKey,
UserPrefsMutator,
FillItemContent,
ApplicationEvent,
} from '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,81 +0,0 @@
import angular from 'angular';
import { SFPrivilegesManager } from 'snjs';
export class PrivilegesManager extends SFPrivilegesManager {
/* @ngInject */
constructor(
passcodeManager,
authManager,
syncManager,
singletonManager,
modelManager,
storageManager,
$rootScope,
$compile
) {
super(modelManager, syncManager, singletonManager);
this.$rootScope = $rootScope;
this.$compile = $compile;
this.setDelegate({
isOffline: async () => {
return authManager.offline();
},
hasLocalPasscode: async () => {
return passcodeManager.hasPasscode();
},
saveToStorage: async (key, value) => {
return storageManager.setItem(key, value, storageManager.bestStorageMode());
},
getFromStorage: async (key) => {
return storageManager.getItem(key, storageManager.bestStorageMode());
},
verifyAccountPassword: async (password) => {
return authManager.verifyAccountPassword(password);
},
verifyLocalPasscode: async (passcode) => {
return passcodeManager.verifyPasscode(passcode);
},
});
}
async presentPrivilegesModal(action, onSuccess, onCancel) {
if (this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
const customSuccess = async () => {
onSuccess && await onSuccess();
this.currentAuthenticationElement = null;
};
const customCancel = async () => {
onCancel && await onCancel();
this.currentAuthenticationElement = null;
};
const scope = this.$rootScope.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
const el = this.$compile(`
<privileges-auth-modal action='action' on-success='onSuccess'
on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>
`)(scope);
angular.element(document.body).append(el);
this.currentAuthenticationElement = el;
}
presentPrivilegesManagementModal() {
var scope = this.$rootScope.$new(true);
var el = this.$compile("<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
angular.element(document.body).append(el);
}
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
}

Some files were not shown because too many files have changed in this diff Show more