Start #41, implement template chooser

Adds one new template (Just Links), and a chooser. Also adds persistent storage of preferred template. Done except styling of the selector screen
This commit is contained in:
Ian Bicking 2018-07-16 17:55:13 -05:00
Родитель e87417fff3
Коммит 1a17bdcfd7
7 изменённых файлов: 200 добавлений и 28 удалений

Просмотреть файл

@ -1,8 +1,10 @@
/* globals TestPilotGA, emailTemplates */
/* globals TestPilotGA, emailTemplates, templateMetadata */
let selectedTemplate = templateMetadata.defaultTemplateName;
browser.runtime.onMessage.addListener((message, source) => {
if (message.type === "sendEmail") {
sendEmail(message.tabIds).catch((e) => {
// FIXME: maybe we should abort the email in this case?
console.error("Error sending email:", e, String(e), e.stack);
});
// Note we don't need the popup to wait for us to send the email, so we return immediately:
@ -23,6 +25,10 @@ browser.runtime.onMessage.addListener((message, source) => {
delete message.type;
sendEvent(message);
return Promise.resolve(null);
} else if (message.type === "setSelectedTemplate") {
return setSelectedTemplate(message.name);
} else if (message.type === "getSelectedTemplate") {
return Promise.resolve(selectedTemplate);
}
console.error("Unexpected message type:", message.type);
return null;
@ -53,7 +59,7 @@ sendEvent({
ni: true
});
async function getTabInfo(tabIds) {
async function getTabInfo(tabIds, wantsScreenshots) {
let allTabs = await browser.tabs.query({});
let tabInfo = {};
for (let tab of allTabs) {
@ -68,10 +74,11 @@ async function getTabInfo(tabIds) {
}
for (let tabId of tabIds) {
try {
let data = await browser.tabs.executeScript(tabId, {
await browser.tabs.executeScript(tabId, {
file: "capture-data.js",
});
Object.assign(tabInfo[tabId], data[0]);
let data = await browser.tabs.sendMessage(tabId, {type: "getData", wantsScreenshots});
Object.assign(tabInfo[tabId], data);
} catch (e) {
console.warn("Error getting info for tab", tabId, tabInfo[tabId].url, ":", String(e));
}
@ -99,8 +106,13 @@ async function sendEmail(tabIds) {
loginInterrupt();
}
}, 1000);
let tabInfo = await getTabInfo(tabIds);
let html = emailTemplates.renderEmail(tabIds.map(id => tabInfo[id]), emailTemplates.Email);
let { wantsScreenshots } = templateMetadata.getTemplate(selectedTemplate);
let tabInfo = await getTabInfo(tabIds, wantsScreenshots);
let TemplateComponent = emailTemplates[templateMetadata.getTemplate(selectedTemplate).componentName];
if (!TemplateComponent) {
throw new Error(`No component found for template: ${selectedTemplate}`);
}
let html = emailTemplates.renderEmail(tabIds.map(id => tabInfo[id]), TemplateComponent);
await browser.tabs.executeScript(newTab.id, {
file: "set-html-email.js",
});
@ -113,8 +125,10 @@ async function sendEmail(tabIds) {
}
async function copyTabHtml(tabIds) {
let tabInfo = await getTabInfo(tabIds);
let html = emailTemplates.renderEmail(tabIds.map(id => tabInfo[id]), emailTemplates.Email);
let { wantsScreenshots } = templateMetadata.getTemplate(selectedTemplate);
let tabInfo = await getTabInfo(tabIds, wantsScreenshots);
let TemplateComponent = emailTemplates[templateMetadata.getTemplate(selectedTemplate).componentName];
let html = emailTemplates.renderEmail(tabIds.map(id => tabInfo[id]), TemplateComponent);
copyHtmlToClipboard(html);
}
@ -159,3 +173,23 @@ async function closeManyTabs(composeTabId, otherTabInfo) {
}
await browser.tabs.remove(toClose);
}
async function setSelectedTemplate(newName) {
selectedTemplate = newName;
await browser.storage.local.set({selectedTemplate: newName});
}
async function init() {
let result = await browser.storage.local.get(["selectedTemplate"]);
if (result && result.selectedTemplate) {
try {
// Checks that the template really exists:
templateMetadata.getTemplate(result.selectedTemplate);
selectedTemplate = result.selectedTemplate;
} catch (e) {
console.error("Could not set template", result.selectedTemplate, "to:", String(e));
}
}
}
init();

Просмотреть файл

@ -34,10 +34,23 @@
};
}
return {
title,
url,
selection,
screenshot: screenshotBox({left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight}, SCREENSHOT_WIDTH / window.innerWidth)
};
async function onMessage(message) {
if (message.type !== "getData") {
console.warn("Unexpected message type:", message.type);
return;
}
browser.runtime.onMessage.removeListener(onMessage);
let data = {
title,
url,
selection
};
if (message.wantsScreenshots) {
data.screenshot = screenshotBox({left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight}, SCREENSHOT_WIDTH / window.innerWidth);
}
return data;
}
browser.runtime.onMessage.addListener(onMessage);
})();

Просмотреть файл

@ -6,30 +6,35 @@ this.emailTemplates = (function () {
let exports = {};
const SELECTION_TEXT_LIMIT = 1000; // 1000 characters max
class Email extends React.Component {
/** Returns '"selection..."', with quotes added and ellipsis if needed */
function selectionDisplay(text) {
text = text.replace(/^\s*/, "");
text = text.replace(/\s*$/, "");
if (text.length > SELECTION_TEXT_LIMIT) {
text = text.substr(0, SELECTION_TEXT_LIMIT) + "…";
}
return `${text}`;
}
class TitleScreenshot extends React.Component {
render() {
let tabList = this.props.tabs.map(
tab => <EmailTab key={tab.id} tab={tab} />
tab => <TitleScreenshotTab key={tab.id} tab={tab} />
);
// Note that <React.Fragment> elements do not show up in the final HTML
return <Fragment>{tabList}</Fragment>;
}
}
exports.Email = Email;
exports.TitleScreenshot = TitleScreenshot;
class EmailTab extends React.Component {
class TitleScreenshotTab extends React.Component {
render() {
let tab = this.props.tab;
let img = null;
let selection = null;
if (tab.selection) {
let text = tab.selection;
if (text.length > SELECTION_TEXT_LIMIT) {
text = text.substr(0, SELECTION_TEXT_LIMIT) + "...";
}
text = `"${text}"`;
selection = <Fragment>{text} <br /></Fragment>;
selection = <Fragment>{selectionDisplay(tab.selection)} <br /></Fragment>;
}
if (tab.screenshot) {
// Note: the alt attribute is searched by gmail, but the title attribute is NOT searched
@ -55,6 +60,24 @@ this.emailTemplates = (function () {
}
}
class JustLinks extends React.Component {
render() {
let tabList = this.props.tabs.map(tab => {
let selection = null;
if (tab.selection) {
selection = <Fragment>{selectionDisplay(tab.selection)} <br /><br /></Fragment>;
}
return <Fragment>
<a href={tab.url}>{tab.title}</a> <br />
{ selection }
</Fragment>;
});
return <Fragment>{tabList}</Fragment>;
}
}
exports.JustLinks = JustLinks;
exports.renderEmail = function(tabs, BaseComponent) {
let emailHtml = ReactDOMServer.renderToStaticMarkup(<BaseComponent tabs={tabs} />);
let lastValue;

Просмотреть файл

@ -20,6 +20,7 @@
"background": {
"scripts": [
"build/testpilot-ga.js",
"templateMetadata.js",
"background.js",
"build/react.production.min.js",
"build/react-dom-server.browser.production.min.js",
@ -40,6 +41,7 @@
"tabs",
"notifications",
"clipboardWrite",
"storage",
"<all_urls>"
]
}

Просмотреть файл

@ -10,6 +10,7 @@
</div>
<script src="build/react.production.min.js"></script>
<script src="build/react-dom.production.min.js"></script>
<script src="templateMetadata.js"></script>
<script src="build/popup.js"></script>
</body>
</html>

Просмотреть файл

@ -1,7 +1,9 @@
/* globals React, ReactDOM, ReactDOMServer */
/* globals React, ReactDOM, ReactDOMServer, templateMetadata */
let activeTabLi;
let selected = new Map();
let isChoosingTemplate = false;
let selectedTemplate = templateMetadata.defaultTemplateName;
const LOGIN_ERROR_TIME = 90 * 1000; // 90 seconds
class Tab extends React.Component {
@ -76,6 +78,7 @@ class Popup extends React.Component {
<input checked={allChecked} ref={allCheckbox => this.allCheckbox = allCheckbox} type="checkbox" id="allCheckbox" onChange={this.onClickCheckAll.bind(this)} />
Select All
</label>
<button onClick={this.chooseTemplate.bind(this)}>Change template ({selectedTemplate})</button>
</div>
</div>
<div className="separator"></div>
@ -137,6 +140,11 @@ class Popup extends React.Component {
window.close();
}, 300);
}
chooseTemplate() {
isChoosingTemplate = true;
render();
}
}
class LoginError extends React.Component {
@ -148,6 +156,47 @@ class LoginError extends React.Component {
}
}
class TemplateChooser extends React.Component {
render() {
let templates = templateMetadata.metadata.map(template => {
return <TemplateItem key={template.name} selected={template.name === selectedTemplate} {...template} />;
});
return <div>
<ul>
{ templates }
</ul>
<footer className="panel-footer toggle-enabled">
<button onClick={this.onCancel.bind(this)}>
Cancel
</button>
</footer>
</div>;
}
onCancel() {
isChoosingTemplate = false;
render();
}
}
class TemplateItem extends React.Component {
render() {
let className = "";
if (this.props.selected) {
className += " selected";
}
return <li className={className} onClick={this.selectTemplate.bind(this)} role="button">
{this.props.title}
</li>;
}
selectTemplate() {
setSelectedTemplate(this.props.name);
isChoosingTemplate = false;
render();
}
}
async function render(firstRun) {
let tabs = await browser.tabs.query({currentWindow: true});
if (firstRun) {
@ -163,8 +212,13 @@ async function render(firstRun) {
if (Date.now() - showLoginError > LOGIN_ERROR_TIME) {
showLoginError = 0;
}
let popup = <Popup selected={selected} tabs={tabs} showLoginError={showLoginError} />;
ReactDOM.render(popup, document.getElementById("panel"));
let page;
if (isChoosingTemplate) {
page = <TemplateChooser />;
} else {
page = <Popup selected={selected} tabs={tabs} showLoginError={showLoginError} />;
}
ReactDOM.render(page, document.getElementById("panel"));
if (firstRun) {
activeTabLi.scrollIntoView({
behavior: "instant",
@ -240,4 +294,14 @@ for (let eventName of ["onAttached", "onCreated", "onDetached", "onMoved", "onUp
browser.tabs.onRemoved.addListener(renderWithDelay);
render(true);
async function setSelectedTemplate(name) {
selectedTemplate = name;
await browser.runtime.sendMessage({type: "setSelectedTemplate", name});
}
async function init() {
selectedTemplate = await browser.runtime.sendMessage({type: "getSelectedTemplate"});
render(true);
}
init();

35
addon/templateMetadata.js Normal file
Просмотреть файл

@ -0,0 +1,35 @@
this.templateMetadata = (function() {
let exports = {};
exports.metadata = [
{
name: "title_screenshot",
title: "With screenshots",
wantsScreenshots: true,
componentName: "TitleScreenshot",
},
{
name: "just_links",
title: "Just the links",
wantsScreenshots: false,
componentName: "JustLinks",
}
];
for (let name in this.templateMetadata) {
this.templateMetadata[name].name = name;
}
exports.defaultTemplateName = "title_screenshot";
exports.getTemplate = function(name) {
for (let template of exports.metadata) {
if (template.name === name) {
return template;
}
}
throw new Error("No template found");
};
return exports;
})();