summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs')
-rw-r--r--toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs291
1 files changed, 291 insertions, 0 deletions
diff --git a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
new file mode 100644
index 0000000000..f11a8a3198
--- /dev/null
+++ b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs
@@ -0,0 +1,291 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { DefaultAggregator } from "resource://gre/modules/megalist/aggregator/DefaultAggregator.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+});
+
+/**
+ * View Model for Megalist.
+ *
+ * Responsible for filtering, grouping, moving selection, editing.
+ * Refers to the same MegalistAggregator in the parent process to access data.
+ * Paired to exactly one MegalistView in the child process to present to the user.
+ * Receives user commands from MegalistView.
+ *
+ * There can be multiple snapshots of the same line displayed in different contexts.
+ *
+ * snapshotId - an id for a snapshot of a line used between View Model and View.
+ */
+export class MegalistViewModel {
+ /**
+ *
+ * View Model prepares snapshots in the parent process to be displayed
+ * by the View in the child process. View gets the firstSnapshotId + length of the
+ * list. Making it a very short message for each time we filter or refresh data.
+ *
+ * View requests line data by providing snapshotId = firstSnapshotId + index.
+ *
+ */
+ #firstSnapshotId = 0;
+ #snapshots = [];
+ #selectedIndex = 0;
+ #searchText = "";
+ #messageToView;
+ static #aggregator = new DefaultAggregator();
+
+ constructor(messageToView) {
+ this.#messageToView = messageToView;
+ MegalistViewModel.#aggregator.attachViewModel(this);
+ }
+
+ willDestroy() {
+ MegalistViewModel.#aggregator.detachViewModel(this);
+ }
+
+ refreshAllLinesOnScreen() {
+ this.#rebuildSnapshots();
+ }
+
+ refreshSingleLineOnScreen(line) {
+ if (this.#searchText) {
+ // Data is filtered, which may require rebuilding the whole list
+ //@sg check if current filter would affected by this line
+ //@sg throttle refresh operation
+ this.#rebuildSnapshots();
+ } else {
+ const snapshotIndex = this.#snapshots.indexOf(line);
+ if (snapshotIndex >= 0) {
+ const snapshotId = snapshotIndex + this.#firstSnapshotId;
+ this.#sendSnapshotToView(snapshotId, line);
+ }
+ }
+ }
+
+ /**
+ *
+ * Send snapshot of necessary line data across parent-child boundary.
+ *
+ * @param {number} snapshotId
+ * @param {object} snapshotData
+ */
+ async #sendSnapshotToView(snapshotId, snapshotData) {
+ if (!snapshotData) {
+ return;
+ }
+
+ // Only usable set of fields is sent over to the View.
+ // Line object may contain other data used by the Data Source.
+ const snapshot = {
+ label: snapshotData.label,
+ value: await snapshotData.value,
+ };
+ if ("template" in snapshotData) {
+ snapshot.template = snapshotData.template;
+ }
+ if ("start" in snapshotData) {
+ snapshot.start = snapshotData.start;
+ }
+ if ("end" in snapshotData) {
+ snapshot.end = snapshotData.end;
+ }
+ if ("commands" in snapshotData) {
+ snapshot.commands = snapshotData.commands;
+ }
+ if ("valueIcon" in snapshotData) {
+ snapshot.valueIcon = snapshotData.valueIcon;
+ }
+ if ("href" in snapshotData) {
+ snapshot.href = snapshotData.href;
+ }
+ if (snapshotData.stickers) {
+ for (const sticker of snapshotData.stickers) {
+ snapshot.stickers ??= [];
+ snapshot.stickers.push(sticker);
+ }
+ }
+
+ this.#messageToView("Snapshot", { snapshotId, snapshot });
+ }
+
+ receiveRequestSnapshot({ snapshotId }) {
+ const snapshotIndex = snapshotId - this.#firstSnapshotId;
+ const snapshot = this.#snapshots[snapshotIndex];
+ if (!snapshot) {
+ // Ignore request for unknown line index or outdated list
+ return;
+ }
+
+ if (snapshot.lineIsReady()) {
+ this.#sendSnapshotToView(snapshotId, snapshot);
+ }
+ }
+
+ handleViewMessage({ name, data }) {
+ const handlerName = `receive${name}`;
+ if (!(handlerName in this)) {
+ throw new Error(`Received unknown message "${name}"`);
+ }
+ return this[handlerName](data);
+ }
+
+ receiveRefresh() {
+ this.#rebuildSnapshots();
+ }
+
+ #rebuildSnapshots() {
+ // Remember current selection to attempt to restore it later
+ const prevSelected = this.#snapshots[this.#selectedIndex];
+
+ // Rebuild snapshots
+ this.#firstSnapshotId += this.#snapshots.length;
+ this.#snapshots = Array.from(
+ MegalistViewModel.#aggregator.enumerateLines(this.#searchText)
+ );
+
+ // Update snapshots on screen
+ this.#messageToView("ShowSnapshots", {
+ firstSnapshotId: this.#firstSnapshotId,
+ count: this.#snapshots.length,
+ });
+
+ // Restore selection
+ const usedToBeSelectedNewIndex = this.#snapshots.findIndex(
+ snapshot => snapshot == prevSelected
+ );
+ if (usedToBeSelectedNewIndex >= 0) {
+ this.#selectSnapshotByIndex(usedToBeSelectedNewIndex);
+ } else {
+ // Make sure selection is within visible lines
+ this.#selectSnapshotByIndex(
+ Math.min(this.#selectedIndex, this.#snapshots.length - 1)
+ );
+ }
+ }
+
+ receiveUpdateFilter({ searchText } = { searchText: "" }) {
+ if (this.#searchText != searchText) {
+ this.#searchText = searchText;
+ this.#messageToView("MegalistUpdateFilter", { searchText });
+ this.#rebuildSnapshots();
+ }
+ }
+
+ async receiveCommand({ commandId, snapshotId, value } = {}) {
+ const index = snapshotId
+ ? snapshotId - this.#firstSnapshotId
+ : this.#selectedIndex;
+ const snapshot = this.#snapshots[index];
+ if (snapshot) {
+ commandId = commandId ?? snapshot.commands[0]?.id;
+ const mustVerify = snapshot.commands.find(c => c.id == commandId)?.verify;
+ if (!mustVerify || (await this.#verifyUser())) {
+ // TODO:Enter the prompt message and pref for #verifyUser()
+ await snapshot[`execute${commandId}`]?.(value);
+ }
+ }
+ }
+
+ receiveSelectSnapshot({ snapshotId }) {
+ const index = snapshotId - this.#firstSnapshotId;
+ if (index >= 0) {
+ this.#selectSnapshotByIndex(index);
+ }
+ }
+
+ receiveSelectNextSnapshot() {
+ this.#selectSnapshotByIndex(this.#selectedIndex + 1);
+ }
+
+ receiveSelectPreviousSnapshot() {
+ this.#selectSnapshotByIndex(this.#selectedIndex - 1);
+ }
+
+ receiveSelectNextGroup() {
+ let i = this.#selectedIndex + 1;
+ while (i < this.#snapshots.length - 1 && !this.#snapshots[i].start) {
+ i += 1;
+ }
+ this.#selectSnapshotByIndex(i);
+ }
+
+ receiveSelectPreviousGroup() {
+ let i = this.#selectedIndex - 1;
+ while (i >= 0 && !this.#snapshots[i].start) {
+ i -= 1;
+ }
+ this.#selectSnapshotByIndex(i);
+ }
+
+ #selectSnapshotByIndex(index) {
+ if (index >= 0 && index < this.#snapshots.length) {
+ this.#selectedIndex = index;
+ const selectedIndex = this.#selectedIndex;
+ this.#messageToView("UpdateSelection", { selectedIndex });
+ }
+ }
+
+ async #verifyUser(promptMessage, prefName) {
+ if (!this.getOSAuthEnabled(prefName)) {
+ promptMessage = false;
+ }
+ let result = await lazy.OSKeyStore.ensureLoggedIn(promptMessage);
+ return result.authenticated;
+ }
+
+ /**
+ * Get the decrypted value for a string pref.
+ *
+ * @param {string} prefName -> The pref whose value is needed.
+ * @param {string} safeDefaultValue -> Value to be returned incase the pref is not yet set.
+ * @returns {string}
+ */
+ #getSecurePref(prefName, safeDefaultValue) {
+ try {
+ let encryptedValue = Services.prefs.getStringPref(prefName, "");
+ return this._crypto.decrypt(encryptedValue);
+ } catch {
+ return safeDefaultValue;
+ }
+ }
+
+ /**
+ * Set the pref to the encrypted form of the value.
+ *
+ * @param {string} prefName -> The pref whose value is to be set.
+ * @param {string} value -> The value to be set in its encryoted form.
+ */
+ #setSecurePref(prefName, value) {
+ let encryptedValue = this._crypto.encrypt(value);
+ Services.prefs.setStringPref(prefName, encryptedValue);
+ }
+
+ /**
+ * Get whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The name of the pref (creditcards or addresses)
+ * @returns {boolean}
+ */
+ getOSAuthEnabled(prefName) {
+ return this.#getSecurePref(prefName, "") !== "opt out";
+ }
+
+ /**
+ * Set whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The pref to encrypt.
+ * @param {boolean} enable -> Whether the pref is to be enabled.
+ */
+ setOSAuthEnabled(prefName, enable) {
+ if (enable) {
+ Services.prefs.clearUserPref(prefName);
+ } else {
+ this.#setSecurePref(prefName, "opt out");
+ }
+ }
+}