Merge branch '004' into develop

This commit is contained in:
Baptiste Grob
2020-07-24 12:18:09 +02:00
243 changed files with 43408 additions and 8500 deletions

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

View File

@@ -1,2 +0,0 @@
export const PANEL_NAME_NOTES = 'notes';
export const PANEL_NAME_TAGS = 'tags';

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,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';

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