Merge branch '004' into develop
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export const PANEL_NAME_NOTES = 'notes';
|
||||
export const PANEL_NAME_TAGS = 'tags';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export { PureCtrl } from './abstract/pure_ctrl';
|
||||
export { EditorPanel } from './editor';
|
||||
export { Footer } from './footer';
|
||||
export { NotesPanel } from './notes/notes';
|
||||
export { TagsPanel } from './tags';
|
||||
export { Root } from './root';
|
||||
export { LockScreen } from './lockScreen';
|
||||
@@ -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: '&',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user