From af96b0309c1a48129f79e029cc6683d127eabdba Mon Sep 17 00:00:00 2001 From: Erayd Date: Sun, 22 Apr 2018 00:26:05 +1200 Subject: [PATCH] Track recently used logins & display first in popup (#9) * Track recently used logins & display first in popup Results are sorted by: - Most recent store first - Within each store, login with highest use count first - Within same usage count, most recent first There is a 60-sec debounce on logins, to avoid excessive incrementing of the counter when a login is used multiple times in rapid succession (e.g. for copying user / pass etc.). --- src/background.js | 2 +- src/package.json | 2 + src/popup/icon-history.svg | 1 + src/popup/interface.js | 34 +++++++++++++++-- src/popup/popup.css | 9 +++++ src/popup/popup.js | 75 +++++++++++++++++++++++++++++++++++--- src/yarn.lock | 19 ++++++++++ 7 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 src/popup/icon-history.svg diff --git a/src/background.js b/src/background.js index 3d04926..7c28a11 100644 --- a/src/background.js +++ b/src/background.js @@ -301,7 +301,7 @@ function hostAction(settings, action, params = {}) { */ async function parseFields(settings, login) { var response = await hostAction(settings, "fetch", { - store: login.store, + store: login.store.name, file: login.login + ".gpg" }); if (response.status != "ok") { diff --git a/src/package.json b/src/package.json index 7e22ddd..68a0d35 100644 --- a/src/package.json +++ b/src/package.json @@ -18,6 +18,8 @@ "chrome-extension-async": "^3.0.0", "fuzzysort": "^1.1.0", "mithril": "^1.1.0", + "moment": "^2.22.1", + "sha1": "^1.1.1", "tldjs": "^2.3.0" }, "devDependencies": { diff --git a/src/popup/icon-history.svg b/src/popup/icon-history.svg new file mode 100644 index 0000000..5374ae4 --- /dev/null +++ b/src/popup/icon-history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/popup/interface.js b/src/popup/interface.js index 5d313cd..01a211f 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -2,6 +2,7 @@ module.exports = Interface; var m = require("mithril"); var FuzzySort = require("fuzzysort"); +var Moment = require("moment"); var SearchInterface = require("./searchinterface"); /** @@ -102,8 +103,21 @@ function view(ctl, params) { } }, [ - badges ? m("div.store.badge", result.store) : null, - m("div.name", m.trust(result.display)), + badges ? m("div.store.badge", result.store.name) : null, + m("div.name", [ + m.trust(result.display), + result.recent.when > 0 + ? m("div.recent", { + title: + "Used here " + + result.recent.count + + " time" + + (result.recent.count > 1 ? "s" : "") + + ", last " + + Moment(new Date(result.recent.when)).fromNow() + }) + : null + ]), m("div.action.copy-password", { title: "Copy password", onclick: function(e) { @@ -148,7 +162,19 @@ function search(s) { // get candidate list var candidates = this.logins.map(result => Object.assign(result, { display: result.login })); if (this.currentDomainOnly) { - candidates = candidates.filter(login => login.inCurrentDomain); + var recent = candidates.filter(login => login.recent.count > 0); + recent.sort(function(a, b) { + if (a.store.when != b.store.when) { + return b.store.when - a.store.when; + } + if (a.recent.count != b.recent.count) { + return b.recent.count - a.recent.count; + } + return b.recent.when - a.recent.when; + }); + candidates = recent.concat( + candidates.filter(login => login.inCurrentDomain && !login.recent.count) + ); } if (s.length) { @@ -156,7 +182,7 @@ function search(s) { // fuzzy-search first word & add highlighting if (fuzzyFirstWord) { candidates = FuzzySort.go(filter[0], candidates, { - keys: ["login", "store"], + keys: ["login", "store.name"], allowTypo: false }).map(result => Object.assign(result.obj, { diff --git a/src/popup/popup.css b/src/popup/popup.css index cddd267..05e72bb 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -95,9 +95,18 @@ body { } .part.login > .name { + display: flex; width: 100%; } +.part.login > .name > .recent { + background-image: url("icon-history.svg"); + background-repeat: no-repeat; + background-size: contain; + margin-left: 4px; + width: 16px; +} + .part.login > .action { background-position: right 3px top 0; background-repeat: no-repeat; diff --git a/src/popup/popup.js b/src/popup/popup.js index 0fb719d..9f8ba49 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -3,6 +3,7 @@ require("chrome-extension-async"); var TldJS = require("tldjs"); +var sha1 = require("sha1"); var Interface = require("./interface"); // wrap with current tab & settings @@ -15,6 +16,20 @@ chrome.tabs.query({ active: true, currentWindow: true }, async function(tabs) { var settings = response.settings; settings.tab = tabs[0]; settings.host = new URL(settings.tab.url).hostname; + for (var store in settings.stores) { + var when = localStorage.getItem("recent:" + settings.stores[store].path); + if (when) { + settings.stores[store].when = JSON.parse(when); + } else { + settings.stores[store].when = 0; + } + } + settings.recent = localStorage.getItem("recent"); + if (settings.recent) { + settings.recent = JSON.parse(settings.recent); + } else { + settings.recent = {}; + } run(settings); } catch (e) { handleError(e); @@ -79,21 +94,34 @@ async function run(settings) { var response = await chrome.runtime.sendMessage({ action: "listFiles" }); var logins = []; var index = 0; + var recent = localStorage.getItem("recent:" + settings.host); + if (recent) { + recent = JSON.parse(recent); + } for (var store in response) { for (var key in response[store]) { // set login fields var login = { index: index++, - store: store, + store: settings.stores[store], login: response[store][key].replace(/\.gpg$/i, ""), allowFill: true }; - login.domain = pathToDomain(login.store + "/" + login.login); + login.domain = pathToDomain(store + "/" + login.login); login.inCurrentDomain = settings.host == login.domain || settings.host.endsWith("." + login.domain); - + login.recent = + settings.recent[ + sha1(settings.host + sha1(login.store.path + sha1(login.login))) + ]; + if (!login.recent) { + login.recent = { + when: 0, + count: 0 + }; + } // bind handlers - login.doAction = withLogin.bind(login); + login.doAction = withLogin.bind({ settings: settings, login: login }); logins.push(login); } @@ -105,6 +133,34 @@ async function run(settings) { } } +/** + * Save login to recent list for current domain + * + * @since 3.0.0 + * + * @param object settings Settings object + * @param object login Login object + * @param bool remove Remove this item from recent history + * @return void + */ +function saveRecent(settings, login, remove = false) { + var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window + + // save store timestamp + localStorage.setItem("recent:" + login.store.path, JSON.stringify(Date.now())); + + // update login usage count & timestamp + if (Date.now() > login.recent.when + ignoreInterval) { + login.recent.count++; + } + login.recent.when = Date.now(); + settings.recent[sha1(settings.host + sha1(login.store.path + sha1(login.login)))] = + login.recent; + + // save to local storage + localStorage.setItem("recent", JSON.stringify(settings.recent)); +} + /** * Do a login action * @@ -142,10 +198,19 @@ async function withLogin(action) { } // hand off action to background script - var response = await chrome.runtime.sendMessage({ action: action, login: this }); + var response = await chrome.runtime.sendMessage({ + action: action, + login: this.login + }); if (response.status != "ok") { throw new Error(response.message); } else { + switch (action) { + case "fill": + case "copyPassword": + case "copyUsername": + saveRecent(this.settings, this.login); + } window.close(); } } catch (e) { diff --git a/src/yarn.lock b/src/yarn.lock index b4858e8..cb64ede 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -224,6 +224,10 @@ cached-path-relative@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +"charenc@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + chrome-extension-async@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/chrome-extension-async/-/chrome-extension-async-3.2.4.tgz#c5b3e206688ca81c903b5e239ff3f9a73a29c47e" @@ -303,6 +307,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +"crypt@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + crypto-browserify@^3.0.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -606,6 +614,10 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" +moment@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -759,6 +771,13 @@ sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: inherits "^2.0.1" safe-buffer "^5.0.1" +sha1@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" + dependencies: + charenc ">= 0.0.1" + crypt ">= 0.0.1" + shasum@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"