From be68391c37b46ac1776287c3af950fcc1bc722da Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 10 May 2019 21:43:40 +0200 Subject: [PATCH] store "openTabs" session --- .../0134__create_openTabs_option.sql | 4 ++ src/public/javascripts/services/link.js | 6 +- .../javascripts/services/note_detail.js | 60 +++++++++++++++--- .../{note_context.js => tab_context.js} | 2 +- src/public/javascripts/services/tree.js | 61 +++++++++++-------- src/routes/api/options.js | 18 +++++- src/routes/api/recent_notes.js | 2 - src/routes/api/tree.js | 1 - src/services/app_info.js | 2 +- src/services/options_init.js | 7 ++- 10 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 db/migrations/0134__create_openTabs_option.sql rename src/public/javascripts/services/{note_context.js => tab_context.js} (98%) diff --git a/db/migrations/0134__create_openTabs_option.sql b/db/migrations/0134__create_openTabs_option.sql new file mode 100644 index 000000000..289e27a54 --- /dev/null +++ b/db/migrations/0134__create_openTabs_option.sql @@ -0,0 +1,4 @@ +INSERT INTO options (name, value, utcDateCreated, utcDateModified, isSynced) + SELECT 'openTabs', '[{"notePath":"' || value || '","active": true}]', '2019-05-01T18:31:00.874Z', '2019-05-01T18:31:00.874Z', 0 FROM options WHERE name = 'startNotePath'; + +DELETE FROM options WHERE name = 'startNotePath'; \ No newline at end of file diff --git a/src/public/javascripts/services/link.js b/src/public/javascripts/services/link.js index 98d353a48..43af73d18 100644 --- a/src/public/javascripts/services/link.js +++ b/src/public/javascripts/services/link.js @@ -51,7 +51,7 @@ function goToLink(e) { if (notePath) { if (e.ctrlKey) { - noteDetailService.loadNoteDetail(notePath.split("/").pop(), true); + noteDetailService.loadNoteDetail(notePath.split("/").pop(), { newTab: true }); } else { treeService.activateNote(notePath); @@ -117,7 +117,7 @@ function tabContextMenu(e) { }, selectContextMenuItem: (e, cmd) => { if (cmd === 'openNoteInNewTab') { - noteDetailService.loadNoteDetail(notePath.split("/").pop(), true); + noteDetailService.loadNoteDetail(notePath.split("/").pop(), { newTab: true }); } } }); @@ -138,7 +138,7 @@ $(document).on('click', '.note-detail-text a', function (e) { // if it's a ctrl-click, then we open on new tab, otherwise normal flow (CKEditor opens link-editing dialog) e.preventDefault(); - noteDetailService.loadNoteDetail(notePath.split("/").pop(), true); + noteDetailService.loadNoteDetail(notePath.split("/").pop(), { newTab: true }); } }); diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 13273d94d..ad59b6d84 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -1,5 +1,5 @@ import treeService from './tree.js'; -import TabContext from './note_context.js'; +import TabContext from './tab_context.js'; import server from './server.js'; import messagingService from "./messaging.js"; import infoService from "./info.js"; @@ -54,7 +54,7 @@ async function reloadAllTabs() { } async function openInTab(noteId) { - await loadNoteDetail(noteId, true); + await loadNoteDetail(noteId, { newTab: true }); } async function switchToNote(notePath) { @@ -178,7 +178,10 @@ async function loadNoteDetailToContext(ctx, note, notePath) { } } -async function loadNoteDetail(notePath, newTab = false) { +async function loadNoteDetail(notePath, options) { + const newTab = !!options.newTab; + const activate = !!options.activate; + const noteId = treeUtils.getNoteIdFromNotePath(notePath); const loadedNote = await loadNote(noteId); let ctx; @@ -203,13 +206,10 @@ async function loadNoteDetail(notePath, newTab = false) { await loadNoteDetailToContext(ctx, loadedNote, notePath); - if (!chromeTabs.activeTabEl) { + if (activate) { // will also trigger showTab via event chromeTabs.setCurrentTab(ctx.tab); } - else if (!newTab) { - await showTab(ctx.tabId); - } } async function loadNote(noteId) { @@ -332,6 +332,49 @@ if (utils.isElectron()) { }); } +chromeTabsEl.addEventListener('activeTabChange', openTabsChanged); +chromeTabsEl.addEventListener('tabAdd', openTabsChanged); +chromeTabsEl.addEventListener('tabRemove', openTabsChanged); +chromeTabsEl.addEventListener('tabReorder', openTabsChanged); + +let tabsChangedTaskId = null; + +function clearOpenTabsTask() { + if (tabsChangedTaskId) { + clearTimeout(tabsChangedTaskId); + } +} + +function openTabsChanged() { + // we don't want to send too many requests with tab changes so we always schedule task to do this in 3 seconds, + // but if there's any change in between, we cancel the old one and schedule new one + // so effectively we kind of wait until user stopped e.g. quickly switching tabs + clearOpenTabsTask(); + + tabsChangedTaskId = setTimeout(saveOpenTabs, 3000); +} + +async function saveOpenTabs() { + const activeTabEl = chromeTabs.activeTabEl; + const openTabs = []; + + for (const tabEl of chromeTabs.tabEls) { + const tabId = parseInt(tabEl.getAttribute('data-tab-id')); + const tabContext = tabContexts.find(tc => tc.tabId === tabId); + + if (tabContext) { + openTabs.push({ + notePath: tabContext.notePath, + active: activeTabEl === tabEl + }); + } + } + + await server.put('options', { + openTabs: JSON.stringify(openTabs) + }); +} + // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved // this sends the request asynchronously and doesn't wait for result $(window).on('beforeunload', () => { saveNotesIfChanged(); }); // don't convert to short form, handler doesn't like returned promise @@ -355,5 +398,6 @@ export default { onNoteChange, addDetailLoadedListener, getActiveContext, - getActiveComponent + getActiveComponent, + clearOpenTabsTask }; \ No newline at end of file diff --git a/src/public/javascripts/services/note_context.js b/src/public/javascripts/services/tab_context.js similarity index 98% rename from src/public/javascripts/services/note_context.js rename to src/public/javascripts/services/tab_context.js index 69f8dbd42..9042fc1fa 100644 --- a/src/public/javascripts/services/note_context.js +++ b/src/public/javascripts/services/tab_context.js @@ -74,7 +74,7 @@ class TabContext { this.$unprotectButton = this.$tabContent.find(".unprotect-button"); this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer); - console.log(`Created note tab ${this.tabId} for ${this.noteId}`); + console.log(`Created note tab ${this.tabId}`); } setNote(note, notePath) { diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 1957a6c7b..376d1a8c3 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -1,5 +1,4 @@ import contextMenuWidget from './context_menu.js'; -import treeContextMenuService from './tree_context_menu.js'; import dragAndDropSetup from './drag_and_drop.js'; import linkService from './link.js'; import messagingService from './messaging.js'; @@ -16,6 +15,7 @@ import Branch from '../entities/branch.js'; import NoteShort from '../entities/note_short.js'; import hoistedNoteService from '../services/hoisted_note.js'; import confirmDialog from "../dialogs/confirm.js"; +import optionsInit from "../services/options_init.js"; import TreeContextMenu from "./tree_context_menu.js"; const $tree = $("#tree"); @@ -25,8 +25,6 @@ const $scrollToActiveNoteButton = $("#scroll-to-active-note-button"); const $notePathList = $("#note-path-list"); const $notePathCount = $("#note-path-count"); -let startNotePath = null; - // focused & not active node can happen during multiselection where the node is selected but not activated // (its content is not displayed in the detail) function getFocusedNode() { @@ -360,29 +358,45 @@ function clearSelectedNodes() { } async function treeInitialized() { - // - is used in mobile to indicate that we don't want to activate any note after load - if (startNotePath === '-') { - return; + let openTabs = []; + + try { + const options = await optionsInit.optionsReady; + + openTabs = JSON.parse(options.openTabs); + } + catch (e) { + messagingService.logError("Cannot retrieve open tabs: " + e.stack); } - const noteId = treeUtils.getNoteIdFromNotePath(startNotePath); + const filteredTabs = []; - if (!await treeCache.noteExists(noteId)) { - // note doesn't exist so don't try to activate it - startNotePath = null; + for (const openTab of openTabs) { + const noteId = treeUtils.getNoteIdFromNotePath(openTab.notePath); + + if (await treeCache.noteExists(noteId)) { + // note doesn't exist so don't try to open tab for it + filteredTabs.push(openTab); + } } - if (startNotePath) { - // this is weird but it looks like even though init event has been called, but we the tree still - // can't find nodes for given path which causes double loading of data. Little timeout fixes this. - setTimeout(async () => { - const node = await activateNote(startNotePath); - - // looks like this this doesn't work when triggered immediatelly after activating node - // so waiting a second helps - setTimeout(() => node.makeVisible({scrollIntoView: true}), 1000); - }, 100); + if (filteredTabs.length === 0) { + filteredTabs.push({ + notePath: 'root', + active: true + }); } + + for (const tab of filteredTabs) { + await noteDetailService.loadNoteDetail(tab.notePath, { + newTab: true, + activate: tab.active + }); + } + + // previous opening triggered task to save tab changes but these are bogus changes (this is init) + // so we'll cancel it + noteDetailService.clearOpenTabsTask(); } let ignoreNextActivationNoteId = null; @@ -406,7 +420,7 @@ function initFancyTree(tree) { node.setSelected(!node.isSelected()); } else if (event.ctrlKey) { - noteDetailService.loadNoteDetail(node.data.noteId, true); + noteDetailService.loadNoteDetail(node.data.noteId, { newTab: true }); } else { node.setActive(); @@ -532,11 +546,6 @@ function getHashValueFromAddress() { async function loadTreeCache() { const resp = await server.get('tree'); - startNotePath = resp.startNotePath; - - if (isNotePathInAddress()) { - startNotePath = getHashValueFromAddress(); - } treeCache.load(resp.notes, resp.branches, resp.relations); } diff --git a/src/routes/api/options.js b/src/routes/api/options.js index 3cb730b81..ca95fc4e7 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -5,8 +5,22 @@ const log = require('../../services/log'); const attributes = require('../../services/attributes'); // options allowed to be updated directly in options dialog -const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', - 'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy', 'leftPaneMinWidth', 'leftPaneWidthPercent', 'hoistedNoteId', 'mainFontSize', 'treeFontSize', 'detailFontSize']; +const ALLOWED_OPTIONS = [ + 'protectedSessionTimeout', + 'noteRevisionSnapshotTimeInterval', + 'zoomFactor', + 'theme', + 'syncServerHost', + 'syncServerTimeout', + 'syncProxy', + 'leftPaneMinWidth', + 'leftPaneWidthPercent', + 'hoistedNoteId', + 'mainFontSize', + 'treeFontSize', + 'detailFontSize', + 'openTabs' +]; async function getOptions() { return await optionService.getOptionsMap(ALLOWED_OPTIONS); diff --git a/src/routes/api/recent_notes.js b/src/routes/api/recent_notes.js index 0e3037f08..375eb4f2f 100644 --- a/src/routes/api/recent_notes.js +++ b/src/routes/api/recent_notes.js @@ -11,8 +11,6 @@ async function addRecentNote(req) { branchId: branchId, notePath: notePath }).save(); - - await optionService.setOption('startNotePath', notePath); } module.exports = { diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js index 488731424..16f1a7e5b 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.js @@ -76,7 +76,6 @@ async function getTree() { const relations = await getRelations(noteIds); return { - startNotePath: (await optionService.getOption('startNotePath')) || 'root', branches, notes, relations diff --git a/src/services/app_info.js b/src/services/app_info.js index 3949bd2a9..ff794645d 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -4,7 +4,7 @@ const build = require('./build'); const packageJson = require('../../package'); const {TRILIUM_DATA_DIR} = require('./data_dir'); -const APP_DB_VERSION = 133; +const APP_DB_VERSION = 134; const SYNC_VERSION = 8; module.exports = { diff --git a/src/services/options_init.js b/src/services/options_init.js index 9e2541af9..43fa0f792 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -29,7 +29,12 @@ async function initSyncedOptions(username, password) { } async function initNotSyncedOptions(initialized, startNotePath = 'root', syncServerHost = '', syncProxy = '') { - await optionService.createOption('startNotePath', startNotePath, false); + await optionService.createOption('openTabs', JSON.stringify([ + { + notePath: startNotePath, + active: 1 + } + ]), false); await optionService.createOption('hoistedNoteId', 'root', false); await optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false); await optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false);