Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change options.html to React/Redux #1387

Merged
merged 11 commits into from
Dec 23, 2019
Merged
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Build directory
dist/**
dist-dev-html/**

# Ignore sub-module directories
addon/content/modules/stdlib/**
Expand Down
7 changes: 6 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ module.exports = {
webextensions: true,
},
excludedFiles: ["addon/bootstrap.js"],
files: ["addon/*.js"],
files: [
"addon/*.js",
"addon/*.jsx",
"addon/content/es-modules/**/*.js",
"addon/content/es-modules/**/*.jsx",
],
parserOptions: {
sourceType: "module",
},
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ conversations.xpi
.vimsession
.DS_Store
dist
dist-dev-html
siefkenj marked this conversation as resolved.
Show resolved Hide resolved
node_modules
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ Building

This will package a `converstions.xpi` file of the latest codebase which can be installed via add-on manager in Thunderbird (hint: you can drag & drop it onto the add-on manager view).

Development
Standard8 marked this conversation as resolved.
Show resolved Hide resolved
===========

Some `thunderbird-conversations` components can be developed fully in the browser. To build these components do

```
npm run build:dev-html
```

and then browse to http://localhost:8126 and select a browser-compatible component file. For example, http://localhost:8126/options.html Missing Thunderbird APIs are mocked in `addon/content/es-modules/thunderbird-compat.js`.


Testing
=======

Expand Down
39 changes: 39 additions & 0 deletions addon/content/es-modules/modules-compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* 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 https://mozilla.org/MPL/2.0/. */

// Emulate AMD modules' `define` function. All modules imported after this
// module is loaded may be assumed to be AMD modules. It is your responsibility
// to handle the initialization of the modules.

export const amdModules = [];

window.define = function define(deps, moduleFactory = deps) {
// Sometimes the function is called `define(f)` and sometimes
// it's called `define([],f)` when there are no deps. Either way,
// normalize the result.
if (deps === moduleFactory) {
deps = [];
}
amdModules.push({ deps, moduleFactory });
};
window.define.amd = true;

/**
* Call a function passing in arguments from a dependency list.
*
* @export
* @param {{deps: string[], moduleFactory: function}} amdItem - an AMD object with a list of deps and a moduleFactory (as created by `define`)
* @param {object} deps - dependencies indexed by keys in `amdItem.deps`
* @returns
*/
export function callWithDeps(amdItem, deps) {
// If `"exports"` is in `amdItem.deps`, it means the module
// wants to save itself on the exports object. We want the module
// to be returned instead, so create an exports object that we can return.
if (!("exports" in deps)) {
deps = { ...deps, exports: {} };
}
const ret = amdItem.moduleFactory(...amdItem.deps.map(dep => deps[dep]));
return ret != null ? ret : deps.exports;
}
88 changes: 88 additions & 0 deletions addon/content/es-modules/thunderbird-compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* 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 https://mozilla.org/MPL/2.0/. */

// A compatibility layer that can be imported whether in the browser or
// in Thunderbird

import { kPrefDefaults } from "../../prefs.js";

// Make sure the browser object exists
const browser = window.browser || {};

// `i18n` is a replacement for `browser.i18n`. `getMessage` defaults
// `browser.i18n.getMessage` if the function exists. Otherwise, locale
// information is `fetch`ed and `getMessage` is polyfilled. The `isLoaded`
// promise resolves to `true` when the library has fully loaded.
export const i18n = {
getMessage: (messageName, substitutions) => `<not loaded>${messageName}`,
isLoaded: Promise.resolve(true),
isPolyfilled: true,
};

if (browser.i18n) {
i18n.getMessage = browser.i18n.getMessage;
i18n.isPolyfilled = false;
} else {
async function initializeI18n(resolve) {
const resp = await fetch("_locales/en/messages.json");
const json = await resp.json();
// Replace the `getMessage` function with one that retrieves
// values from the loaded JSON.
i18n.getMessage = (messageName, substitutions) =>
(json[messageName] || {}).message ||
`<translation not found>${messageName}`;
resolve(true);
}

// Fake what we need from the i18n library
i18n.isLoaded = new Promise((resolve, reject) => {
// initializeI18n modifies the global i18n object and calls
// `resolve(true)` when finished.
initializeI18n(resolve).catch(reject);
});
}

if (!browser.storage) {
const DEFAULT_PREFS = {
...kPrefDefaults,
// DEFAULT_PREFS is only used when browser.storage does not exist. I.e.,
// when running in the browser in dev mode. Turn on logging in this case.
logging_enabled: true,
};

// Fake what we need from the browser storage library
const _stored = { preferences: DEFAULT_PREFS };
browser.storage = {
local: {
async get(key) {
if (typeof key === "undefined") {
return _stored;
}
if (typeof key === "string") {
return { [key]: _stored[key] };
}
if (Array.isArray(key)) {
const ret = {};
for (const k of key) {
if (k in _stored) {
ret[k] = _stored[k];
}
}
return ret;
}
// the last case is that we are an object with default values
const ret = {};
for (const [k, v] of Object.entries(key)) {
ret[k] = k in _stored ? _stored[k] : v;
}
return ret;
},
async set(key) {
return Object.assign(_stored, key);
},
},
};
}

export { browser };
49 changes: 49 additions & 0 deletions addon/content/es-modules/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* 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 https://mozilla.org/MPL/2.0/. */

// `amdModules` will be an array of AMD module initialization information.
// It is up to us to initialize all the modules in the proper order and re-export them.
import { amdModules, callWithDeps } from "./modules-compat.js";

// Load vendor modules as AMD modules
// IMPORTANT: the order here must be kept in sync
// with `amdModuleNames` and the order must resolve deps.
// e.g., `react-dom` must come after `react`, since `react` is a
// dependency of `react-dom`
import "../vendor/react.js";
import "../vendor/react-dom.js";
import "../vendor/redux.js";
import "../vendor/react-redux.js";
import "../vendor/redux-toolkit.umd.js";
import "../vendor/prop-types.js";

const initializedDeps = {};

// These names must be in the same order as the AMD modules were imported
const amdModuleNames = [
"react",
"react-dom",
"redux",
"react-redux",
"redux-toolkit",
"prop-types",
];
amdModuleNames.forEach((name, i) => {
const amdItem = amdModules[i];
if (!amdItem) {
throw new Error(
`An ${i}th AMD module was assumed to be loaded, but none was found`
);
}
initializedDeps[name] = callWithDeps(amdItem, initializedDeps);
});

const React = initializedDeps.react;
const ReactDOM = initializedDeps["react-dom"];
const Redux = initializedDeps.redux;
const ReactRedux = initializedDeps["react-redux"];
const RTK = initializedDeps["redux-toolkit"];
const PropTypes = initializedDeps["prop-types"];

export { React, ReactDOM, Redux, ReactRedux, RTK, PropTypes };
100 changes: 2 additions & 98 deletions addon/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,103 +10,7 @@
</head>

<body>
<h1 data-l10n-id="extensionName"></h1>
<form id="conversationOptions">
<div id="preferencesGrid">
<div>
<label data-l10n-id="options.expand_who"></label>
</div>
<div>
<input type="radio" class="pref" name="expand_who" value="1" onchange="saveOptions();" />
<label data-l10n-id="options.expand_none"></label>
<input type="radio" class="pref" name="expand_who" value="3" onchange="saveOptions();" />
<label data-l10n-id="options.expand_all"></label>
<input type="radio" class="pref" name="expand_who" value="4" onchange="saveOptions();" />
<label data-l10n-id="options.expand_auto"
data-l10n-tooltip="option.expand_auto.tooltip"></label>
</div>
<div>
<label class="title" data-l10n-id="options.quoting_title"></label>
<br>
<label data-l10n-id="options.quoting_desc"></label>
</div>
<div class="prefSectionContent">
<input type="number" min="0" class="pref" name="hide_quote_length"/>
</div>
<div>
<label class="title" data-l10n-id="options.monospaced_senders_title"></label>
<br>
<label data-l10n-id="options.monospaced_senders_desc"></label>
</div>
<div class="prefSectionContent">
<input type="text" class="pref" name="monospaced_senders"/>
</div>
<div>
<label class="title" data-l10n-id="options.hide_sigs_title"></label>
<br>
<label data-l10n-id="options.hide_sigs_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="hide_sigs"/>
</div>
<div>
<label class="title" data-l10n-id="options.friendly_date_title"></label>
<br>
<label data-l10n-id="options.friendly_date_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="no_friendly_date"/>
</div>
<div>
<label class="title" data-l10n-id="options.tweak_chrome_title"></label>
<br>
<label data-l10n-id="options.tweak_chrome_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="tweak_chrome"/>
</div>
<div>
<label class="title" data-l10n-id="options.tweak_bodies_title"></label>
<br>
<label data-l10n-id="options.tweak_bodies_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="tweak_bodies"/>
</div>
<div>
<label class="title" data-l10n-id="options.operate_on_conversations_title"></label>
<br>
<label data-l10n-id="options.operate_on_conversations_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="operate_on_conversations"/>
</div>
<div>
<label class="title" data-l10n-id="options.extra_attachments_title"></label>
<br>
<label data-l10n-id="options.extra_attachments_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="extra_attachments"/>
</div>
<div>
<label class="title" data-l10n-id="options.compose_in_tab_title"></label>
<br>
<label data-l10n-id="options.compose_in_tab_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="compose_in_tab"/>
</div>
<div>
<label class="title" data-l10n-id="options.debugging_title"></label>
<br>
<label data-l10n-id="options.debugging_desc"></label>
</div>
<div class="prefSectionContent">
<input type="checkbox" class="pref" name="logging_enabled"/>
</div>
</div>
</form>
<script src="options.js"></script>
<div id="root"></div>
<script type="module" src="options.js"></script>
</body>
</html>