Remove dummy concept in favor of editor group and editors

This commit is contained in:
Mo Bitar
2020-04-14 15:01:32 -05:00
parent ef66170ba4
commit 9cf99896a5
81 changed files with 8136 additions and 7179 deletions

View File

@@ -1,139 +0,0 @@
import { AppStateEvent } from '@/services/state';
import { WebApplication } from './../../application';
import { ApplicationEvent } from 'snjs';
export type CtrlState = Partial<Record<string, any>>
export type CtrlProps = Partial<Record<string, any>>
export class PureCtrl {
$timeout: ng.ITimeoutService
/** Passed through templates */
application?: WebApplication
props: CtrlProps = {}
state: CtrlState = {}
private unsubApp: any
private unsubState: any
private stateTimeout: any
/* @ngInject */
constructor($timeout: ng.ITimeoutService) {
this.$timeout = $timeout;
/* Allow caller constructor to finish setting instance variables */
setImmediate(() => {
this.state = this.getInitialState();
});
}
$onInit() {
this.addAppEventObserver();
this.addAppStateObserver();
}
deinit() {
this.unsubApp();
this.unsubState();
this.unsubApp = undefined;
this.unsubState = undefined;
this.application = undefined;
if (this.stateTimeout) {
this.$timeout.cancel(this.stateTimeout);
}
}
$onDestroy() {
this.deinit();
}
public get appState() {
return this.application!.getAppState();
}
/** @private */
async resetState() {
this.state = this.getInitialState();
await this.setState(this.state);
}
/** @override */
getInitialState() {
return {};
}
async setState(state: CtrlState) {
if (!this.$timeout) {
return;
}
return new Promise((resolve) => {
this.stateTimeout = this.$timeout(() => {
this.state = Object.freeze(Object.assign({}, this.state, state));
resolve();
});
});
}
async updateUI(func: () => void) {
this.$timeout(func);
}
initProps(props: CtrlProps) {
if (Object.keys(this.props).length > 0) {
throw 'Already init-ed props.';
}
this.props = Object.freeze(Object.assign({}, this.props, props));
}
addAppStateObserver() {
this.unsubState = this.application!.getAppState().addObserver(
async (eventName, data) => {
this.onAppStateEvent(eventName, data);
}
);
}
onAppStateEvent(eventName: any, data: any) {
/** Optional override */
}
addAppEventObserver() {
if (this.application!.isStarted()) {
this.onAppStart();
}
if (this.application!.isLaunched()) {
this.onAppLaunch();
}
this.unsubApp = this.application!.addEventObserver(async (eventName) => {
this.onAppEvent(eventName);
if (eventName === ApplicationEvent.Started) {
await this.onAppStart();
} else if (eventName === ApplicationEvent.Launched) {
await this.onAppLaunch();
} else if (eventName === ApplicationEvent.CompletedSync) {
this.onAppSync();
} else if (eventName === ApplicationEvent.KeyStatusChanged) {
this.onAppKeyChange();
}
});
}
onAppEvent(eventName: ApplicationEvent) {
/** Optional override */
}
/** @override */
async onAppStart() {
await this.resetState();
}
async onAppLaunch() {
/** Optional override */
}
async onAppKeyChange() {
/** Optional override */
}
onAppSync() {
/** Optional override */
}
}

View File

@@ -1,317 +0,0 @@
import { PanelPuppet, WebDirective, PermissionsModalScope, ModalComponentScope } from './../types';
import { getPlatformString } from '@/utils';
import template from '%/application-view.pug';
import { AppStateEvent } from '@/services/state';
import { ApplicationEvent, SNComponent } from 'snjs';
import angular from 'angular';
import {
PANEL_NAME_NOTES,
PANEL_NAME_TAGS
} from '@/controllers/constants';
import {
STRING_SESSION_EXPIRED,
STRING_DEFAULT_FILE_ERROR
} from '@/strings';
import { PureCtrl } from './abstract/pure_ctrl';
import { PermissionDialog } from '@/../../../../snjs/dist/@types/services/component_manager';
class ApplicationViewCtrl extends PureCtrl {
private $compile?: ng.ICompileService
private $location?: ng.ILocationService
private $rootScope?: ng.IRootScopeService
public platformString: string
private completedInitialSync = false
private syncStatus: any
private notesCollapsed = false
private tagsCollapsed = false
private showingDownloadStatus = false
private uploadSyncStatus: any
private lastAlertShownDate?: Date
/* @ngInject */
constructor(
$compile: ng.ICompileService,
$location: ng.ILocationService,
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService
) {
super($timeout);
this.$location = $location;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.platformString = getPlatformString();
this.state = { appClass: '' };
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.openModalComponent = this.openModalComponent.bind(this);
this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this);
this.addDragDropHandlers();
}
deinit() {
this.$location = undefined;
this.$rootScope = undefined;
this.$compile = undefined;
this.application = undefined;
window.removeEventListener('dragover', this.onDragOver, true);
window.removeEventListener('drop', this.onDragDrop, true);
(this.onDragDrop as any) = undefined;
(this.onDragOver as any) = undefined;
(this.openModalComponent as any) = undefined;
(this.presentPermissionsDialog as any) = undefined;
super.deinit();
}
$onInit() {
super.$onInit();
this.loadApplication();
}
async loadApplication() {
await this.application!.prepareForLaunch({
receiveChallenge: async (challenge, orchestrator) => {
this.application!.promptForChallenge(challenge, orchestrator);
}
});
await this.application!.launch();
}
async onAppStart() {
super.onAppStart();
this.overrideComponentManagerFunctions();
this.application!.componentManager!.setDesktopManager(
this.application!.getDesktopService()
);
this.setState({
ready: true,
needsUnlock: this.application!.hasPasscode()
});
}
async onAppLaunch() {
super.onAppLaunch();
this.setState({ needsUnlock: false });
this.handleAutoSignInFromParams();
}
onUpdateAvailable() {
this.$rootScope!.$broadcast('new-update-available');
};
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
this.updateLocalDataStatus();
} else if (
eventName === ApplicationEvent.SyncStatusChanged ||
eventName === ApplicationEvent.FailedSync
) {
this.updateSyncStatus();
} else if (eventName === ApplicationEvent.LocalDataLoaded) {
this.updateLocalDataStatus();
} else if (eventName === ApplicationEvent.WillSync) {
if (!this.completedInitialSync) {
this.syncStatus = this.application!.getStatusService().replaceStatusWithString(
this.syncStatus,
"Syncing..."
);
}
} else if (eventName === ApplicationEvent.CompletedSync) {
if (!this.completedInitialSync) {
this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus);
this.completedInitialSync = true;
}
} else if (eventName === ApplicationEvent.InvalidSyncSession) {
this.showInvalidSessionAlert();
} else if (eventName === ApplicationEvent.LocalDatabaseReadError) {
this.application!.alertService!.alert(
'Unable to load local database. Please restart the app and try again.'
);
} else if (eventName === ApplicationEvent.LocalDatabaseWriteError) {
this.application!.alertService!.alert(
'Unable to write to local database. Please restart the app and try again.'
);
}
}
/** @override */
async onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.PanelResized) {
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.setState({ appClass });
} else if (eventName === AppStateEvent.WindowDidFocus) {
if (!(await this.application!.isLocked())) {
this.application!.sync();
}
}
}
updateLocalDataStatus() {
const syncStatus = this.application!.getSyncStatus();
const stats = syncStatus.getStats();
const encryption = this.application!.isEncryptionAvailable();
if (stats.localDataDone) {
this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus);
return;
}
const notesString = `${stats.localDataCurrent}/${stats.localDataTotal} items...`;
const loadingStatus = encryption
? `Decrypting ${notesString}`
: `Loading ${notesString}`;
this.syncStatus = this.application!.getStatusService().replaceStatusWithString(
this.syncStatus,
loadingStatus
);
}
updateSyncStatus() {
const syncStatus = this.application!.getSyncStatus();
const stats = syncStatus.getStats();
if (syncStatus.hasError()) {
this.syncStatus = this.application!.getStatusService().replaceStatusWithString(
this.syncStatus,
'Unable to Sync'
);
} else if (stats.downloadCount > 20) {
const text = `Downloading ${stats.downloadCount} items. Keep app open.`;
this.syncStatus = this.application!.getStatusService().replaceStatusWithString(
this.syncStatus,
text
);
this.showingDownloadStatus = true;
} else if (this.showingDownloadStatus) {
this.showingDownloadStatus = false;
const text = "Download Complete.";
this.syncStatus = this.application!.getStatusService().replaceStatusWithString(
this.syncStatus,
text
);
setTimeout(() => {
this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus);
}, 2000);
} else if (stats.uploadTotalCount > 20) {
this.uploadSyncStatus = this.application!.getStatusService().replaceStatusWithString(
this.uploadSyncStatus,
`Syncing ${stats.uploadCompletionCount}/${stats.uploadTotalCount} items...`
);
} else if (this.uploadSyncStatus) {
this.uploadSyncStatus = this.application!.getStatusService().removeStatus(
this.uploadSyncStatus
);
}
}
openModalComponent(component: SNComponent) {
const scope = this.$rootScope!.$new(true) as ModalComponentScope;
scope.component = component;
const el = this.$compile!(
"<component-modal component='component' class='sk-modal'></component-modal>"
)(scope as any);
angular.element(document.body).append(el);
}
presentPermissionsDialog(dialog: PermissionDialog) {
const scope = this.$rootScope!.$new(true) as PermissionsModalScope;
scope.permissionsString = dialog.permissionsString;
scope.component = dialog.component;
scope.callback = dialog.callback;
const el = this.$compile!(
"<permissions-modal component='component' permissions-string='permissionsString'"
+ " callback='callback' class='sk-modal'></permissions-modal>"
)(scope as any);
angular.element(document.body).append(el);
}
overrideComponentManagerFunctions() {
this.application!.componentManager!.openModalComponent = this.openModalComponent;
this.application!.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
}
showInvalidSessionAlert() {
/** Don't show repeatedly; at most 30 seconds in between */
const SHOW_INTERVAL = 30;
if (
!this.lastAlertShownDate ||
(new Date().getTime() - this.lastAlertShownDate!.getTime()) / 1000 > SHOW_INTERVAL
) {
this.lastAlertShownDate = new Date();
setTimeout(() => {
this.application!.alertService!.alert(
STRING_SESSION_EXPIRED
);
}, 500);
}
}
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', this.onDragOver, true);
window.addEventListener('drop', this.onDragDrop, true);
}
onDragOver(event: DragEvent) {
if (event.dataTransfer!.files.length > 0) {
event.preventDefault();
}
}
onDragDrop(event: DragEvent) {
if (event.dataTransfer!.files.length > 0) {
event.preventDefault();
this.application!.alertService!.alert(
STRING_DEFAULT_FILE_ERROR
);
}
}
async handleAutoSignInFromParams() {
const params = this.$location!.search();
const server = params.server;
const email = params.email;
const password = params.pw;
if (!server || !email || !password) return;
const user = this.application!.getUser();
if (user) {
if (user.email === email && await this.application!.getHost() === server) {
/** Already signed in, return */
return;
} else {
/** Sign out */
await this.application!.signOut();
}
}
await this.application!.setHost(server);
this.application!.signIn(
email,
password,
);
}
}
export class ApplicationView extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = ApplicationViewCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = {
application: '='
};
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,442 +0,0 @@
import { FooterStatus, WebDirective } from './../types';
import { dateToLocalizedString } from '@/utils';
import {
ApplicationEvent,
SyncQueueStrategy,
ProtectedAction,
ContentType,
SNComponent,
SNTheme,
ComponentArea,
ComponentAction
} from 'snjs';
import template from '%/footer.pug';
import { AppStateEvent, EventSource } from '@/services/state';
import {
STRING_GENERIC_SYNC_ERROR,
STRING_NEW_UPDATE_READY
} from '@/strings';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { ComponentMutator } from '@/../../../../snjs/dist/@types/models';
type DockShortcut = {
name: string,
component: SNComponent,
icon: {
type: string
background_color: string
border_color: string
}
}
class FooterCtrl extends PureCtrl {
private $rootScope: ng.IRootScopeService
private rooms: SNComponent[] = []
private themesWithIcons: SNTheme[] = []
private showSyncResolution = false
private unregisterComponent: any
private rootScopeListener1: any
private rootScopeListener2: any
public arbitraryStatusMessage?: string
public user?: any
private backupStatus?: FooterStatus
private offline = true
private showAccountMenu = false
private queueExtReload = false
private reloadInProgress = false
public hasError = false
public isRefreshing = false
public lastSyncDate?: string
public newUpdateAvailable = false
public dockShortcuts: DockShortcut[] = []
public roomShowState: Partial<Record<string, boolean>> = {}
/* @ngInject */
constructor(
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService,
) {
super($timeout);
this.$rootScope = $rootScope;
this.addRootScopeListeners();
this.toggleSyncResolutionMenu = this.toggleSyncResolutionMenu.bind(this);
this.closeAccountMenu = this.closeAccountMenu.bind(this);
}
deinit() {
this.rooms.length = 0;
this.themesWithIcons.length = 0;
this.unregisterComponent();
this.unregisterComponent = undefined;
this.rootScopeListener1();
this.rootScopeListener2();
this.rootScopeListener1 = undefined;
this.rootScopeListener2 = undefined;
(this.closeAccountMenu as any) = undefined;
(this.toggleSyncResolutionMenu as any) = undefined;
super.deinit();
}
$onInit() {
super.$onInit();
this.application!.getStatusService().addStatusObserver((string: string) => {
this.$timeout(() => {
this.arbitraryStatusMessage = string;
});
});
}
getInitialState() {
return {
hasPasscode: false
};
}
reloadUpgradeStatus() {
this.application!.checkForSecurityUpdate().then((available) => {
this.setState({
dataUpgradeAvailable: available
});
});
}
async onAppLaunch() {
super.onAppLaunch();
this.reloadPasscodeStatus();
this.reloadUpgradeStatus();
this.user = this.application!.getUser();
this.updateOfflineStatus();
this.findErrors();
this.streamItems();
this.registerComponentHandler();
}
async reloadPasscodeStatus() {
const hasPasscode = this.application!.hasPasscode();
this.setState({
hasPasscode: hasPasscode
});
}
addRootScopeListeners() {
this.rootScopeListener1 = this.$rootScope.$on("reload-ext-data", () => {
this.reloadExtendedData();
});
this.rootScopeListener2 = this.$rootScope.$on("new-update-available", () => {
this.$timeout(() => {
this.onNewUpdateAvailable();
});
});
}
/** @override */
onAppStateEvent(eventName: AppStateEvent, data: any) {
if (eventName === AppStateEvent.EditorFocused) {
if (data.eventSource === EventSource.UserInteraction) {
this.closeAllRooms();
this.closeAccountMenu();
}
} else if (eventName === AppStateEvent.BeganBackupDownload) {
this.backupStatus = this.application!.getStatusService().addStatusFromString(
"Saving local backup..."
);
} else if (eventName === AppStateEvent.EndedBackupDownload) {
if (data.success) {
this.backupStatus = this.application!.getStatusService().replaceStatusWithString(
this.backupStatus!,
"Successfully saved backup."
);
} else {
this.backupStatus = this.application!.getStatusService().replaceStatusWithString(
this.backupStatus!,
"Unable to save local backup."
);
}
this.$timeout(() => {
this.backupStatus = this.application!.getStatusService().removeStatus(this.backupStatus!);
}, 2000);
}
}
/** @override */
async onAppKeyChange() {
super.onAppKeyChange();
this.reloadPasscodeStatus();
}
/** @override */
onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.KeyStatusChanged) {
this.reloadUpgradeStatus();
} else if (eventName === ApplicationEvent.EnteredOutOfSync) {
this.setState({
outOfSync: true
});
} else if (eventName === ApplicationEvent.ExitedOutOfSync) {
this.setState({
outOfSync: false
});
} else if (eventName === ApplicationEvent.CompletedSync) {
if (this.offline && this.application!.getNoteCount() === 0) {
this.showAccountMenu = true;
}
this.syncUpdated();
this.findErrors();
this.updateOfflineStatus();
} else if (eventName === ApplicationEvent.FailedSync) {
this.findErrors();
this.updateOfflineStatus();
}
}
streamItems() {
this.application!.streamItems(
ContentType.Component,
async () => {
const components = this.application!.getItems(ContentType.Component) as SNComponent[];
this.rooms = components.filter((candidate) => {
return candidate.area === ComponentArea.Rooms && !candidate.deleted;
});
if (this.queueExtReload) {
this.queueExtReload = false;
this.reloadExtendedData();
}
}
);
this.application!.streamItems(
ContentType.Theme,
async () => {
const themes = this.application!.getDisplayableItems(ContentType.Theme) as SNTheme[];
const filteredThemes = themes.filter((candidate) => {
return (
!candidate.deleted &&
candidate.package_info &&
candidate.package_info.dock_icon
);
}).sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const differ = filteredThemes.length !== this.themesWithIcons.length;
this.themesWithIcons = filteredThemes;
if (differ) {
this.reloadDockShortcuts();
}
}
);
}
registerComponentHandler() {
this.unregisterComponent = this.application!.componentManager!.registerHandler({
identifier: 'room-bar',
areas: [ComponentArea.Rooms, ComponentArea.Modal],
activationHandler: () => { },
actionHandler: (component, action, data) => {
if (action === ComponentAction.SetSize) {
this.application!.changeItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.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.application!
.getNativeExtService().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);
}
updateOfflineStatus() {
this.offline = this.application!.noAccount();
}
openSecurityUpdate() {
this.application!.performProtocolUpgrade();
}
findErrors() {
this.hasError = this.application!.getSyncStatus().hasError();
}
accountMenuPressed() {
this.showAccountMenu = !this.showAccountMenu;
this.closeAllRooms();
}
toggleSyncResolutionMenu() {
this.showSyncResolution = !this.showSyncResolution;
}
closeAccountMenu() {
this.showAccountMenu = false;
}
lockApp() {
this.application!.lock();
}
refreshData() {
this.isRefreshing = true;
this.application!.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true
}).then((response) => {
this.$timeout(() => {
this.isRefreshing = false;
}, 200);
if (response && response.error) {
this.application!.alertService!.alert(
STRING_GENERIC_SYNC_ERROR
);
} else {
this.syncUpdated();
}
});
}
syncUpdated() {
this.lastSyncDate = dateToLocalizedString(this.application!.getLastSyncDate()!);
}
onNewUpdateAvailable() {
this.newUpdateAvailable = true;
}
clickedNewUpdateAnnouncement() {
this.newUpdateAvailable = false;
this.application!.alertService!.alert(
STRING_NEW_UPDATE_READY
);
}
reloadDockShortcuts() {
const shortcuts = [];
for (const theme of this.themesWithIcons) {
const name = theme.package_info.name;
const icon = theme.package_info.dock_icon;
if (!icon) {
continue;
}
shortcuts.push({
name: name,
component: theme,
icon: icon
} as DockShortcut);
}
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;
} else {
return 0;
}
});
}
initSvgForShortcut(shortcut: DockShortcut) {
const id = 'dock-svg-' + shortcut.component.uuid;
const element = document.getElementById(id)!;
const parser = new DOMParser();
const svg = shortcut.component.package_info.dock_icon.source;
const doc = parser.parseFromString(svg, 'image/svg+xml');
element.appendChild(doc.documentElement);
}
selectShortcut(shortcut: DockShortcut) {
this.application!.componentManager!.toggleComponent(shortcut.component);
}
onRoomDismiss(room: SNComponent) {
this.roomShowState[room.uuid] = false;
}
closeAllRooms() {
for (const room of this.rooms) {
this.roomShowState[room.uuid] = false;
}
}
async selectRoom(room: SNComponent) {
const run = () => {
this.$timeout(() => {
this.roomShowState[room.uuid] = !this.roomShowState[room.uuid];
});
};
if (!this.roomShowState[room.uuid]) {
const requiresPrivilege = await this.application!.privilegesService!
.actionRequiresPrivilege(
ProtectedAction.ManageExtensions
);
if (requiresPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManageExtensions,
run
);
} else {
run();
}
} else {
run();
}
}
clickOutsideAccountMenu() {
if (this.application && this.application!.authenticationInProgress()) {
return;
}
this.showAccountMenu = false;
}
}
export class Footer extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = FooterCtrl;
this.replace = true;
this.controllerAs = 'ctrl';
this.bindToController = {
application: '='
};
}
}

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 { ApplicationView } from './applicationView';

View File

@@ -1,153 +0,0 @@
import { SNNote, SNTag } from 'snjs';
export enum NoteSortKey {
CreatedAt = 'created_at',
UpdatedAt = 'updated_at',
ClientUpdatedAt = 'client_updated_at',
Title = 'title',
}
export function filterAndSortNotes(
notes: SNNote[],
selectedTag: SNTag,
showArchived: boolean,
hidePinned: boolean,
filterText: string,
sortBy: string,
reverse: boolean,
) {
const filtered = filterNotes(
notes,
selectedTag,
showArchived,
hidePinned,
filterText,
);
const sorted = sortNotes(
filtered,
sortBy,
reverse
);
return sorted;
}
export function filterNotes(
notes: SNNote[],
selectedTag: SNTag,
showArchived: boolean,
hidePinned: boolean,
filterText: string
) {
return notes.filter((note) => {
let canShowArchived = showArchived;
const canShowPinned = !hidePinned;
const isTrash = selectedTag.isTrashTag;
if (!isTrash && note.trashed) {
return false;
}
const isSmartTag = selectedTag.isSmartTag();
if (isSmartTag) {
canShowArchived = (
canShowArchived ||
selectedTag.isArchiveTag ||
isTrash
);
}
if (
(note.archived && !canShowArchived) ||
(note.pinned && !canShowPinned)
) {
return false;
}
return noteMatchesQuery(note, filterText);
});
}
function noteMatchesQuery(
note: SNNote,
query: string
) {
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: string) {
const matches = text.match(/"(.*?)"/);
return matches ? matches[1] : null;
}
function stringIsUuid(text: string) {
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: SNNote[] = [],
sortBy: string,
reverse: boolean
) {
const sortValueFn = (a: SNNote, b: SNNote, pinCheck = false): number => {
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 as any)[sortBy] || '';
let bValue = (a as any)[sortBy] || '';
let vector = 1;
if (reverse) {
vector *= -1;
}
if (sortBy === NoteSortKey.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,763 +0,0 @@
import { PanelPuppet, WebDirective } from './../../types';
import angular from 'angular';
import template from '%/notes.pug';
import { ApplicationEvent, ContentType, removeFromArray, SNNote, SNTag, WebPrefKey } from 'snjs';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { AppStateEvent } from '@/services/state';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
import {
PANEL_NAME_NOTES
} from '@/controllers/constants';
import {
NoteSortKey,
filterAndSortNotes
} from './note_utils';
import { UuidString } from '@/../../../../snjs/dist/@types/types';
type NotesState = {
tag?: SNTag
notes?: SNNote[]
renderedNotes?: SNNote[]
sortBy?: string
sortReverse?: boolean
showArchived?: boolean
hidePinned?: boolean
hideNotePreview?: boolean
hideDate?: boolean
hideTags?: boolean
noteFilter: { text: string }
mutable: { showMenu: boolean }
}
type NoteFlag = {
text: string
class: 'info' | 'neutral' | 'warning' | 'success' | 'danger'
}
/**
* 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 {
private panelPuppet?: PanelPuppet
private reloadNotesPromise?: any
private notesToDisplay = 0
private pageSize = 0
private searchSubmitted = false
private newNoteKeyObserver: any
private nextNoteKeyObserver: any
private previousNoteKeyObserver: any
private searchKeyObserver: any
private noteFlags: Partial<Record<UuidString, NoteFlag[]>> = {}
/* @ngInject */
constructor($timeout: ng.ITimeoutService, ) {
super($timeout);
this.resetPagination();
}
$onInit() {
super.$onInit();
angular.element(document).ready(() => {
this.reloadPreferences();
});
this.panelPuppet = {
onReady: () => this.reloadPreferences()
};
this.onWindowResize = this.onWindowResize.bind(this);
this.onPanelResize = this.onPanelResize.bind(this);
window.addEventListener('resize', this.onWindowResize, true);
this.registerKeyboardShortcuts();
}
onWindowResize() {
this.resetPagination(true);
}
deinit() {
this.panelPuppet!.onReady = undefined;
this.panelPuppet = undefined;
window.removeEventListener('resize', this.onWindowResize, true);
(this.onWindowResize as any) = undefined;
(this.onPanelResize as any) = undefined;
this.newNoteKeyObserver();
this.nextNoteKeyObserver();
this.previousNoteKeyObserver();
this.searchKeyObserver();
this.newNoteKeyObserver = undefined;
this.nextNoteKeyObserver = undefined;
this.previousNoteKeyObserver = undefined;
this.searchKeyObserver = undefined;
super.deinit();
}
getState() {
return this.state as NotesState;
}
getInitialState() {
return {
notes: [],
renderedNotes: [],
mutable: { showMenu: false },
noteFilter: { text: '' },
} as NotesState;
}
async onAppLaunch() {
super.onAppLaunch();
this.streamNotesAndTags();
this.reloadPreferences();
}
/** @override */
onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange(
this.application!.getAppState().getSelectedTag()!,
data.previousTag
);
} else if (eventName === AppStateEvent.NoteChanged) {
this.handleNoteSelection(
this.application!.getAppState().getSelectedNote()!
);
} else if (eventName === AppStateEvent.PreferencesChanged) {
this.reloadPreferences();
this.reloadNotes();
} else if (eventName === AppStateEvent.EditorFocused) {
this.setShowMenuFalse();
}
}
get selectedNote() {
return this.appState.getSelectedNote();
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.SignedIn) {
/** Delete dummy note if applicable */
if (this.selectedNote && this.selectedNote!.dummy) {
this.application!.deleteItemLocally(this.selectedNote!);
await this.selectNote(undefined);
await this.reloadNotes();
}
} else if (eventName === ApplicationEvent.CompletedSync) {
this.getMostValidNotes().then((notes) => {
if (notes.length === 0) {
this.createPlaceholderNote();
}
});
}
}
/**
* @access private
* Access the current state notes without awaiting any potential reloads
* that may be in progress. This is the sync alternative to `async getMostValidNotes`
*/
getPossiblyStaleNotes() {
return this.getState().notes!;
}
/**
* @access private
* Access the current state notes after waiting for any pending reloads.
* This returns the most up to date notes, but is the asyncronous counterpart
* to `getPossiblyStaleNotes`
*/
async getMostValidNotes() {
await this.reloadNotesPromise;
return this.getPossiblyStaleNotes();
}
/**
* Triggered programatically to create a new placeholder note
* when conditions allow for it. This is as opposed to creating a new note
* as part of user interaction (pressing the + button).
* @access private
*/
async createPlaceholderNote() {
const selectedTag = this.application!.getAppState().getSelectedTag()!;
if (selectedTag.isSmartTag() && !selectedTag.isAllTag) {
return;
}
return this.createNewNote();
}
streamNotesAndTags() {
this.application!.streamItems(
[ContentType.Note, ContentType.Tag],
async (items) => {
await this.reloadNotes();
const selectedNote = this.selectedNote;
if (selectedNote) {
const discarded = selectedNote.deleted || selectedNote.trashed;
if (discarded) {
this.selectNextOrCreateNew();
}
} else {
this.selectFirstNote();
}
/** Note has changed values, reset its flags */
const notes = items.filter((item) => item.content_type === ContentType.Note) as SNNote[];
for (const note of notes) {
if (note.deleted) {
continue;
}
this.loadFlagsForNote(note);
}
}
);
}
async selectNote(note?: SNNote) {
return this.application!.getAppState().setSelectedNote(note);
}
async createNewNote() {
const selectedTag = this.application!.getAppState().getSelectedTag();
if (!selectedTag) {
throw 'Attempting to create note with no selected tag';
}
let title;
let isDummyNote = true;
if (this.isFiltering()) {
title = this.getState().noteFilter.text;
isDummyNote = false;
} else if (this.selectedNote && this.selectedNote!.dummy) {
return;
} else {
title = `Note ${this.getState().notes!.length + 1}`;
}
const newNote = await this.application!.createManagedItem(
ContentType.Note,
{
text: '',
title: title,
references: []
},
true,
{
dummy: isDummyNote
}
) as SNNote;
if (!selectedTag.isSmartTag()) {
this.application!.changeItem(selectedTag.uuid, (mutator) => {
mutator.addItemAsRelationship(newNote);
});
}
this.selectNote(newNote);
}
async handleTagChange(tag: SNTag, previousTag?: SNTag) {
if (this.selectedNote && this.selectedNote!.dummy) {
await this.application!.deleteItemLocally(this.selectedNote!);
await this.selectNote(undefined);
}
await this.setState({ tag: tag });
this.resetScrollPosition();
this.setShowMenuFalse();
await this.setNoteFilterText('');
this.application!.getDesktopService().searchText();
this.resetPagination();
/* Capture db load state before beginning reloadNotes, since this status may change during reload */
const dbLoaded = this.application!.isDatabaseLoaded();
await this.reloadNotes();
if (this.getState().notes!.length > 0) {
this.selectFirstNote();
} else if (dbLoaded) {
if (!tag.isSmartTag() || tag.isAllTag) {
this.createPlaceholderNote();
} else if (
this.selectedNote &&
!this.getState().notes!.includes(this.selectedNote!)
) {
this.selectNote(undefined);
}
}
}
resetScrollPosition() {
const scrollable = document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
if (scrollable) {
scrollable.scrollTop = 0;
scrollable.scrollLeft = 0;
}
}
async removeNoteFromList(note: SNNote) {
const notes = this.getState().notes!;
removeFromArray(notes, note);
await this.setState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
}
async reloadNotes() {
this.reloadNotesPromise = this.performPeloadNotes();
return this.reloadNotesPromise;
}
async performPeloadNotes() {
const tag = this.getState().tag!;
if (!tag) {
return;
}
const tagNotes = this.appState.getTagNotes(tag);
const notes = filterAndSortNotes(
tagNotes,
tag,
this.getState().showArchived!,
this.getState().hidePinned!,
this.getState().noteFilter.text.toLowerCase(),
this.getState().sortBy!,
this.getState().sortReverse!
);
for (const note of notes) {
if (note.errorDecrypting) {
this.loadFlagsForNote(note);
}
}
await this.setState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
this.reloadPanelTitle();
}
setShowMenuFalse() {
this.setState({
mutable: {
...this.getState().mutable,
showMenu: false
}
});
}
async handleNoteSelection(note: SNNote) {
const previousNote = this.selectedNote;
if (previousNote === note) {
return;
}
if (previousNote && previousNote.dummy) {
await this.application!.deleteItemLocally(previousNote);
this.removeNoteFromList(previousNote);
}
if (!note) {
return;
}
if (note.conflictOf) {
this.application!.changeAndSaveItem(note.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
}
if (this.isFiltering()) {
this.application!.getDesktopService().searchText(this.getState().noteFilter.text);
}
}
reloadPreferences() {
const viewOptions = {} as NotesState;
const prevSortValue = this.getState().sortBy;
let sortBy = this.application!.getPrefsService().getValue(
WebPrefKey.SortNotesBy,
NoteSortKey.CreatedAt
);
if (sortBy === NoteSortKey.UpdatedAt) {
/** Use client_updated_at instead */
sortBy = NoteSortKey.ClientUpdatedAt;
}
viewOptions.sortBy = sortBy;
viewOptions.sortReverse = this.application!.getPrefsService().getValue(
WebPrefKey.SortNotesReverse,
false
);
viewOptions.showArchived = this.application!.getPrefsService().getValue(
WebPrefKey.NotesShowArchived,
false
);
viewOptions.hidePinned = this.application!.getPrefsService().getValue(
WebPrefKey.NotesHidePinned,
false
);
viewOptions.hideNotePreview = this.application!.getPrefsService().getValue(
WebPrefKey.NotesHideNotePreview,
false
);
viewOptions.hideDate = this.application!.getPrefsService().getValue(
WebPrefKey.NotesHideDate,
false
);
viewOptions.hideTags = this.application!.getPrefsService().getValue(
WebPrefKey.NotesHideTags,
false
);
this.setState({
...viewOptions
});
if (prevSortValue && prevSortValue !== sortBy) {
this.selectFirstNote();
}
const width = this.application!.getPrefsService().getValue(
WebPrefKey.NotesPanelWidth
);
if (width && this.panelPuppet!.ready) {
this.panelPuppet!.setWidth!(width);
if (this.panelPuppet!.isCollapsed!()) {
this.application!.getAppState().panelDidResize(
PANEL_NAME_NOTES,
this.panelPuppet!.isCollapsed!()
);
}
}
}
onPanelResize(
newWidth: number,
lastLeft: number,
isAtMaxWidth: boolean,
isCollapsed: boolean
) {
this.application!.getPrefsService().setUserPrefValue(
WebPrefKey.NotesPanelWidth,
newWidth
);
this.application!.getPrefsService().syncUserPreferences();
this.application!.getAppState().panelDidResize(
PANEL_NAME_NOTES,
isCollapsed
);
}
paginate() {
this.notesToDisplay += this.pageSize;
this.reloadNotes();
if (this.searchSubmitted) {
this.application!.getDesktopService().searchText(this.getState().noteFilter.text);
}
}
resetPagination(keepCurrentIfLarger = false) {
const clientHeight = document.documentElement.clientHeight;
this.pageSize = Math.ceil(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.getState().notes!.length;
title = `${resultCount} search results`;
} else if (this.getState().tag) {
title = `${this.getState().tag!.title}`;
}
this.setState({
panelTitle: title
});
}
optionsSubtitle() {
let base = "";
if (this.getState().sortBy === 'created_at') {
base += " Date Added";
} else if (this.getState().sortBy === 'client_updated_at') {
base += " Date Modified";
} else if (this.getState().sortBy === 'title') {
base += " Title";
}
if (this.getState().showArchived) {
base += " | + Archived";
}
if (this.getState().hidePinned) {
base += " | Pinned";
}
if (this.getState().sortReverse) {
base += " | Reversed";
}
return base;
}
loadFlagsForNote(note: SNNote) {
const flags = [] as NoteFlag[];
if (note.pinned) {
flags.push({
text: "Pinned",
class: 'info'
});
}
if (note.archived) {
flags.push({
text: "Archived",
class: 'warning'
});
}
if (note.protected) {
flags.push({
text: "Protected",
class: 'success'
});
}
if (note.locked) {
flags.push({
text: "Locked",
class: 'neutral'
});
}
if (note.trashed) {
flags.push({
text: "Deleted",
class: 'danger'
});
}
if (note.conflictOf) {
flags.push({
text: "Conflicted Copy",
class: 'danger'
});
}
if (note.errorDecrypting) {
if (note.waitingForKey) {
flags.push({
text: "Waiting For Keys",
class: 'info'
});
} else {
flags.push({
text: "Missing Keys",
class: 'danger'
});
}
}
if (note.deleted) {
flags.push({
text: "Deletion Pending Sync",
class: 'danger'
});
}
this.noteFlags[note.uuid] = flags;
return flags;
}
displayableNotes() {
return this.getState().notes!;
}
getFirstNonProtectedNote() {
const displayableNotes = this.displayableNotes();
let index = 0;
let note = displayableNotes[index];
while (note && note.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.findIndex((candidate) => {
return candidate.uuid === this.selectedNote!.uuid
});
if (currentIndex + 1 < displayableNotes.length) {
this.selectNote(displayableNotes[currentIndex + 1]);
}
}
selectNextOrCreateNew() {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note);
} else if (!this.getState().tag || !this.getState().tag!.isSmartTag()) {
this.createPlaceholderNote();
} else {
this.selectNote(undefined);
}
}
selectPreviousNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.indexOf(this.selectedNote!);
if (currentIndex - 1 >= 0) {
this.selectNote(displayableNotes[currentIndex - 1]);
return true;
} else {
return false;
}
}
isFiltering() {
return this.getState().noteFilter.text &&
this.getState().noteFilter.text.length > 0;
}
async setNoteFilterText(text: string) {
await this.setState({
noteFilter: {
...this.getState().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.application!.getDesktopService().searchText(this.getState().noteFilter.text);
}
selectedMenuItem() {
this.setShowMenuFalse();
}
toggleWebPrefKey(key: WebPrefKey) {
this.application!.getPrefsService().setUserPrefValue(key, !this.state[key]);
this.application!.getPrefsService().syncUserPreferences();
}
selectedSortByCreated() {
this.setSortBy(NoteSortKey.CreatedAt);
}
selectedSortByUpdated() {
this.setSortBy(NoteSortKey.ClientUpdatedAt);
}
selectedSortByTitle() {
this.setSortBy(NoteSortKey.Title);
}
toggleReverseSort() {
this.selectedMenuItem();
this.application!.getPrefsService().setUserPrefValue(
WebPrefKey.SortNotesReverse,
!this.getState().sortReverse
);
this.application!.getPrefsService().syncUserPreferences();
}
setSortBy(type: NoteSortKey) {
this.application!.getPrefsService().setUserPrefValue(
WebPrefKey.SortNotesBy,
type
);
this.application!.getPrefsService().syncUserPreferences();
}
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.application!.getKeyboardService().addKeyObserver({
key: 'n',
modifiers: [
KeyboardModifier.Meta,
KeyboardModifier.Ctrl
],
onKeyDown: (event) => {
event.preventDefault();
this.createNewNote();
}
});
this.nextNoteKeyObserver = this.application!.getKeyboardService().addKeyObserver({
key: KeyboardKey.Down,
elements: [
document.body,
this.getSearchBar()
],
onKeyDown: (event) => {
const searchBar = this.getSearchBar();
if (searchBar === document.activeElement) {
searchBar.blur();
}
this.selectNextNote();
}
});
this.previousNoteKeyObserver = this.application!.getKeyboardService().addKeyObserver({
key: KeyboardKey.Up,
element: document.body,
onKeyDown: (event) => {
this.selectPreviousNote();
}
});
this.searchKeyObserver = this.application!.getKeyboardService().addKeyObserver({
key: "f",
modifiers: [
KeyboardModifier.Meta,
KeyboardModifier.Shift
],
onKeyDown: (event) => {
const searchBar = this.getSearchBar();
if (searchBar) { searchBar.focus(); };
}
});
}
}
export class NotesPanel extends WebDirective {
constructor() {
super();
this.template = template;
this.replace = true;
this.controller = NotesCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
application: '='
};
}
}

View File

@@ -1,37 +0,0 @@
import { ApplicationManager } from './../applicationManager';
import { WebDirective } from './../types';
import template from '%/root.pug';
import { WebApplication } from '@/application';
class RootCtrl {
private $timeout: ng.ITimeoutService
private applicationManager: ApplicationManager
public applications: WebApplication[] = []
/* @ngInject */
constructor($timeout: ng.ITimeoutService, applicationManager: ApplicationManager) {
this.$timeout = $timeout;
this.applicationManager = applicationManager;
this.applicationManager.addApplicationChangeObserver(() => {
this.reload();
});
}
reload() {
this.$timeout(() => {
this.applications = this.applicationManager.getApplications();
});
}
}
export class Root extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = RootCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = true;
}
}

View File

@@ -1,358 +0,0 @@
import { WebDirective, PanelPuppet } from './../types';
import { WebApplication } from './../application';
import {
SNNote,
SNTag,
ContentType,
ApplicationEvent,
ComponentAction,
SNSmartTag,
ComponentArea,
SNComponent,
WebPrefKey
} from 'snjs';
import template from '%/tags.pug';
import { AppStateEvent } from '@/services/state';
import { PANEL_NAME_TAGS } from '@/controllers/constants';
import { STRING_DELETE_TAG } from '@/strings';
import { PureCtrl } from '@Controllers/abstract/pure_ctrl';
import { UuidString } from '@/../../../../snjs/dist/@types/types';
import { TagMutator } from '@/../../../../snjs/dist/@types/models/app/tag';
type NoteCounts = Partial<Record<string, number>>
class TagsPanelCtrl extends PureCtrl {
/** Passed through template */
readonly application!: WebApplication
private readonly panelPuppet: PanelPuppet
private unregisterComponent?: any
component?: SNComponent
private editingOriginalName?: string
formData: { tagTitle?: string } = {}
titles: Partial<Record<UuidString, string>> = {}
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
) {
super($timeout);
this.panelPuppet = {
onReady: () => this.loadPreferences()
};
}
deinit() {
this.unregisterComponent();
this.unregisterComponent = undefined;
super.deinit();
}
getInitialState() {
return {
tags: [],
smartTags: [],
noteCounts: {},
};
}
async onAppStart() {
super.onAppStart();
this.registerComponentHandler();
}
async onAppLaunch() {
super.onAppLaunch();
this.loadPreferences();
this.beginStreamingItems();
const smartTags = this.application.getSmartTags();
this.setState({
smartTags: smartTags,
});
this.selectTag(smartTags[0]);
}
/** @override */
onAppSync() {
super.onAppSync();
this.reloadNoteCounts();
}
/**
* Returns all officially saved tags as reported by the model manager.
* @access private
*/
getMappedTags() {
const tags = this.application.getItems(ContentType.Tag) as SNTag[];
return tags.sort((a, b) => {
return a.title < b.title ? -1 : 1;
});
}
beginStreamingItems() {
this.application.streamItems(
ContentType.Tag,
async (items) => {
await this.setState({
tags: this.getMappedTags(),
smartTags: this.application.getSmartTags(),
});
this.reloadTitles(items as SNTag[]);
this.reloadNoteCounts();
if (this.state.selectedTag) {
/** If the selected tag has been deleted, revert to All view. */
const matchingTag = items.find((tag) => {
return tag.uuid === this.state.selectedTag.uuid;
});
if (!matchingTag || matchingTag.deleted) {
this.selectTag(this.state.smartTags[0]);
}
}
}
);
}
reloadTitles(tags: Array<SNTag | SNSmartTag>) {
for(const tag of tags) {
this.titles[tag.uuid] = tag.title;
}
}
/** @override */
onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.PreferencesChanged) {
this.loadPreferences();
} else if (eventName === AppStateEvent.TagChanged) {
this.setState({
selectedTag: this.application.getAppState().getSelectedTag()
});
}
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
this.reloadNoteCounts();
} else if (eventName === ApplicationEvent.SyncStatusChanged) {
const syncStatus = this.application.getSyncStatus();
const stats = syncStatus.getStats();
if (stats.downloadCount > 0) {
this.reloadNoteCounts();
}
}
}
reloadNoteCounts() {
let allTags: Array<SNTag | SNSmartTag> = [];
if (this.state.tags) {
allTags = allTags.concat(this.state.tags);
}
if (this.state.smartTags) {
allTags = allTags.concat(this.state.smartTags);
}
const noteCounts: NoteCounts = {};
for (const tag of allTags) {
if (tag.isSmartTag()) {
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag);
noteCounts[tag.uuid] = notes.length;
} else {
const notes = this.application.referencesForItem(tag, ContentType.Note)
.filter((note) => {
return !note.archived && !note.trashed;
})
noteCounts[tag.uuid] = notes.length;
}
}
this.setState({
noteCounts: noteCounts
});
}
loadPreferences() {
if (!this.panelPuppet.ready) {
return;
}
const width = this.application.getPrefsService().getValue(WebPrefKey.TagsPanelWidth);
if (width) {
this.panelPuppet.setWidth!(width);
if (this.panelPuppet.isCollapsed!()) {
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
this.panelPuppet.isCollapsed!()
);
}
}
}
onPanelResize = (
newWidth: number,
lastLeft: number,
isAtMaxWidth: boolean,
isCollapsed: boolean
) => {
this.application.getPrefsService().setUserPrefValue(
WebPrefKey.TagsPanelWidth,
newWidth,
true
);
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
isCollapsed
);
}
registerComponentHandler() {
this.unregisterComponent = this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
activationHandler: (component) => {
this.component = component;
},
contextRequestHandler: () => {
return undefined;
},
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
if (data.item.content_type === ContentType.Tag) {
const tag = this.application.findItem(data.item.uuid);
if (tag) {
this.selectTag(tag as SNTag);
}
} else if (data.item.content_type === ContentType.SmartTag) {
this.application.createTemplateItem(
ContentType.SmartTag,
data.item.content
).then(smartTag => {
this.selectTag(smartTag as SNSmartTag);
});
}
} else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.state.smartTags[0]);
}
}
});
}
async selectTag(tag: SNTag) {
if (tag.conflictOf) {
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
}
this.application.getAppState().setSelectedTag(tag);
}
async clickedAddNewTag() {
if (this.state.editingTag) {
return;
}
const newTag = await this.application.createTemplateItem(
ContentType.Tag
);
this.setState({
tags: [newTag].concat(this.state.tags),
previousTag: this.state.selectedTag,
selectedTag: newTag,
editingTag: newTag,
newTag: newTag
});
}
onTagTitleChange(tag: SNTag | SNSmartTag) {
this.setState({
editingTag: tag
});
}
async saveTag($event: Event, tag: SNTag) {
($event.target! as HTMLInputElement).blur();
await this.setState({
editingTag: null,
});
if (!tag.title || tag.title.length === 0) {
let newSelectedTag = this.state.selectedTag;
if (this.state.editingTag) {
this.titles[tag.uuid] = this.editingOriginalName;
this.editingOriginalName = undefined;
} else if (this.state.newTag) {
newSelectedTag = this.state.previousTag;
}
this.setState({
newTag: null,
selectedTag: newSelectedTag,
tags: this.getMappedTags()
});
return;
}
this.editingOriginalName = undefined;
const matchingTag = this.application.findTagByTitle(tag.title);
const alreadyExists = matchingTag && matchingTag !== tag;
if (this.state.newTag === tag && alreadyExists) {
this.application.alertService!.alert(
"A tag with this name already exists."
);
this.setState({
newTag: null,
tags: this.getMappedTags(),
selectedTag: this.state.previousTag
});
return;
}
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
const tagMutator = mutator as TagMutator;
tagMutator.title = this.titles[tag.uuid]!;
});
this.selectTag(tag);
this.setState({
newTag: null
});
}
async selectedRenameTag(tag: SNTag) {
this.editingOriginalName = tag.title;
await this.setState({
editingTag: tag
});
document.getElementById('tag-' + tag.uuid)!.focus();
}
selectedDeleteTag(tag: SNTag) {
this.removeTag(tag);
}
removeTag(tag: SNTag) {
this.application.alertService!.confirm(
STRING_DELETE_TAG,
undefined,
undefined,
undefined,
() => {
/* On confirm */
this.application.deleteItem(tag);
this.selectTag(this.state.smartTags[0]);
},
undefined,
true,
);
}
}
export class TagsPanel extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.scope = {
application: '='
};
this.template = template;
this.replace = true;
this.controller = TagsPanelCtrl;
this.controllerAs = 'self';
this.bindToController = true;
}
}