зеркало из https://github.com/mozilla/email-tabs.git
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:
Родитель
e87417fff3
Коммит
1a17bdcfd7
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
})();
|
Загрузка…
Ссылка в новой задаче