Bug 1572648 - OpenGraph + Twitter Card support for message compose. r=aleca
Differential Revision: https://phabricator.services.mozilla.com/D137999 --HG-- extra : rebase_source : 68fbdb48a7af20a7a4a2e802690f0e99b618c3f2 extra : amend_source : 2e33bd5cdceab5e2e31c30fe093eb79b4ab17457
This commit is contained in:
Родитель
3b244c226a
Коммит
5324b54d28
|
@ -307,6 +307,10 @@ pref("editor.singleLine.pasteNewlines", 4); // substitute commas for new lines
|
|||
pref("editor.CR_creates_new_p", true);
|
||||
pref("mail.compose.default_to_paragraph", true);
|
||||
|
||||
// If true, when pasting a URL, paste the Open Graph / Twitter Card details
|
||||
// we can extract from the URL instead.
|
||||
pref("mail.compose.add_link_preview", false);
|
||||
|
||||
// hidden pref to ensure a certain number of headers in the message pane
|
||||
// to avoid the height of the header area from changing when headers are present / not present
|
||||
pref("mailnews.headers.minNumHeaders", 0); // 0 means we ignore this pref
|
||||
|
|
|
@ -193,6 +193,8 @@ var gUserTouchedEncryptSubject = false;
|
|||
var gIsRelatedToEncryptedOriginal = false;
|
||||
var gIsRelatedToSignedOriginal = false;
|
||||
|
||||
var gOpened = Date.now();
|
||||
|
||||
var gEncryptedURIService = Cc[
|
||||
"@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
|
||||
].getService(Ci.nsIEncryptedSMIMEURIsService);
|
||||
|
@ -3641,23 +3643,186 @@ var dictionaryRemovalObserver = {
|
|||
},
|
||||
};
|
||||
|
||||
function EditorClick(event) {
|
||||
if (event.target.matches(".remove-card")) {
|
||||
event.target.closest(".moz-card").remove();
|
||||
} else if (event.target.matches(`.add-card[data-opened='${gOpened}']`)) {
|
||||
let url = event.target.getAttribute("data-url");
|
||||
let meRect = document.getElementById("messageEditor").getClientRects()[0];
|
||||
let settings = document.getElementById("linkPreviewSettings");
|
||||
let settingsW = 500;
|
||||
settings.style.position = "fixed";
|
||||
settings.style.left =
|
||||
Math.max(settingsW + 20, event.clientX) - settingsW + "px";
|
||||
settings.style.top = meRect.top + event.clientY + 20 + "px";
|
||||
settings.hidden = false;
|
||||
event.target.remove();
|
||||
settings.querySelector(".close").onclick = event => {
|
||||
settings.hidden = true;
|
||||
};
|
||||
settings.querySelector(".preview-replace").onclick = event => {
|
||||
addLinkPreview(url, true);
|
||||
settings.hidden = true;
|
||||
};
|
||||
settings.querySelector(".preview-autoadd").onclick = event => {
|
||||
Services.prefs.setBoolPref(
|
||||
"mail.compose.add_link_preview",
|
||||
event.target.checked
|
||||
);
|
||||
};
|
||||
settings.querySelector(".preview-replace").focus();
|
||||
settings.onkeydown = event => {
|
||||
if (event.key == "Escape") {
|
||||
settings.hidden = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab Open Graph or Twitter card data from the URL and insert a link preview
|
||||
* into the editor
|
||||
* @param {string} url - The URL to add preview for.
|
||||
* @param {boolean} skipBad - Skip adding anything on bad data. When false,
|
||||
* insert the url.
|
||||
*/
|
||||
async function addLinkPreview(url, skipBad) {
|
||||
return fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
let doc = new DOMParser().parseFromString(text, "text/html");
|
||||
|
||||
// If the url has an Open Graph or Twitter card, create a nicer
|
||||
// representation and use that instead.
|
||||
// @see https://ogp.me/
|
||||
// @see https://developer.twitter.com/en/docs/twitter-for-websites/cards/
|
||||
|
||||
let title = doc
|
||||
.querySelector("meta[property='og:title'],meta[name='twitter:title']")
|
||||
?.getAttribute("content");
|
||||
let description = doc
|
||||
.querySelector(
|
||||
"meta[property='og:description'],meta[name='twitter:description']"
|
||||
)
|
||||
?.getAttribute("content");
|
||||
|
||||
// Handle the case where we didn't get proper data.
|
||||
if (!title && !description) {
|
||||
console.debug(`No link preview data for url=${url}`);
|
||||
if (skipBad) {
|
||||
return;
|
||||
}
|
||||
getBrowser().contentDocument.execCommand("insertHTML", false, url);
|
||||
return;
|
||||
}
|
||||
|
||||
let image = doc
|
||||
.querySelector("meta[property='og:image'],meta[name='twitter:image']")
|
||||
?.getAttribute("content");
|
||||
let alt = doc
|
||||
.querySelector(
|
||||
"meta[property='og:image:alt'],meta[name='twitter:image:alt']"
|
||||
)
|
||||
?.getAttribute("content");
|
||||
let creator = doc.querySelector("meta[name='twitter:creator']");
|
||||
|
||||
// Grab our template and fill in the variables.
|
||||
let card = document
|
||||
.getElementById("dataCardTemplate")
|
||||
.content.cloneNode(true);
|
||||
card.querySelector("img").src = image;
|
||||
card.querySelector("img").alt = alt;
|
||||
card.querySelector(".title").textContent = title;
|
||||
card.querySelector(".creator").textContent = creator;
|
||||
if (!creator) {
|
||||
card.querySelector(".creator").remove();
|
||||
}
|
||||
card.querySelector(".description").textContent = description;
|
||||
card.querySelector(".url").textContent = "🔗 " + url;
|
||||
card.querySelector(".url").href = url;
|
||||
card.querySelector(".url").title = new URL(url).hostname;
|
||||
card.querySelector(".site").textContent = new URL(url).hostname;
|
||||
|
||||
// twitter:card "summary" = Summary Card
|
||||
// twitter:card "summary_large_image" = Summary Card with Large Image
|
||||
if (
|
||||
doc.querySelector(
|
||||
"meta[name='twitter:card'][content='summary_large_image']"
|
||||
)
|
||||
) {
|
||||
card.querySelector("img").style.width = "600px";
|
||||
card.querySelector(".url").style.maxWidth = "550px";
|
||||
card.querySelector(".site").parentNode.remove();
|
||||
}
|
||||
|
||||
// If subject is empty, set that as well.
|
||||
let subject = document.getElementById("msgSubject");
|
||||
if (!subject.value && title) {
|
||||
subject.value = title;
|
||||
}
|
||||
|
||||
// Add a line after the card. Otherwise it's hard to continue writing.
|
||||
let line = GetCurrentEditor().returnInParagraphCreatesNewParagraph
|
||||
? "<p> </p>"
|
||||
: "<br />";
|
||||
getBrowser().contentDocument.execCommand(
|
||||
"insertHTML",
|
||||
false,
|
||||
card.firstElementChild.outerHTML + line
|
||||
);
|
||||
|
||||
for (let card of getBrowser().contentDocument.querySelectorAll(
|
||||
".moz-card"
|
||||
)) {
|
||||
card.classList.add("loaded");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On paste or drop, we may want to modify the content before inserting it into
|
||||
* the editor, replacing file URLs with data URLs when appropriate.
|
||||
*/
|
||||
function onPasteOrDrop(e) {
|
||||
// For paste use e.clipboardData, for drop use e.dataTransfer.
|
||||
let dataTransfer = "clipboardData" in e ? e.clipboardData : e.dataTransfer;
|
||||
|
||||
if (!dataTransfer.types.includes("text/html")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gMsgCompose.composeHTML) {
|
||||
// We're in the plain text editor. Nothing to do here.
|
||||
return;
|
||||
}
|
||||
|
||||
// For paste use e.clipboardData, for drop use e.dataTransfer.
|
||||
let dataTransfer = "clipboardData" in e ? e.clipboardData : e.dataTransfer;
|
||||
if (!Services.io.offline && !dataTransfer.types.includes("text/html")) {
|
||||
let type = dataTransfer.types.find(t =>
|
||||
["text/uri-list", "text/x-moz-url", "text/plain"].includes(t)
|
||||
);
|
||||
if (type) {
|
||||
let url = dataTransfer
|
||||
.getData(type)
|
||||
.split("\n")[0]
|
||||
.trim();
|
||||
if (/^https?:\/\//.test(url)) {
|
||||
e.preventDefault(); // We'll handle the pasting manually.
|
||||
if (Services.prefs.getBoolPref("mail.compose.add_link_preview", true)) {
|
||||
getBrowser().contentDocument.execCommand("insertHTML", false, url);
|
||||
addLinkPreview(url);
|
||||
} else {
|
||||
getBrowser().contentDocument.execCommand("insertHTML", false, url);
|
||||
/*
|
||||
// FIXME - see bug 1572648
|
||||
// Make the below UI nicer, and remove the inserHTML above.
|
||||
getBrowser().contentDocument.execCommand(
|
||||
"insertHTML",
|
||||
false,
|
||||
`${url} <span class='add-card' data-url='${url}' data-opened='${gOpened}'>📰</span>`
|
||||
);
|
||||
*/
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ok, we have html content to paste.
|
||||
let html = dataTransfer.getData("text/html");
|
||||
let doc = new DOMParser().parseFromString(html, "text/html");
|
||||
let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
|
@ -5552,6 +5717,15 @@ function GenericSendMessage(msgType) {
|
|||
msgCompFields.newsgroups = "";
|
||||
}
|
||||
|
||||
if (Services.prefs.getBoolPref("mail.compose.add_link_preview", true)) {
|
||||
// Remove any card "close" button from content before sending.
|
||||
for (let close of getBrowser().contentDocument.querySelectorAll(
|
||||
".moz-card .remove-card"
|
||||
)) {
|
||||
close.remove();
|
||||
}
|
||||
}
|
||||
|
||||
let sendFormat = determineSendFormat();
|
||||
switch (sendFormat) {
|
||||
case Ci.nsIMsgCompSendFormat.PlainText:
|
||||
|
|
|
@ -2435,7 +2435,6 @@
|
|||
<html:hr is="pane-splitter" id="headersSplitter"
|
||||
resize-direction="vertical"
|
||||
resize-id="MsgHeadersToolbar" />
|
||||
|
||||
<html:div id="messageArea">
|
||||
<html:div id="dropAttachmentOverlay" class="drop-attachment-overlay">
|
||||
<html:aside id="addInline" class="drop-attachment-box">
|
||||
|
@ -2465,8 +2464,24 @@
|
|||
aria-label="&aria.message.bodyName;"
|
||||
messagemanagergroup="browsers"
|
||||
oncontextmenu="this._contextX = event.pageX; this._contextY = event.pageY;"
|
||||
onclick="EditorClick(event);"
|
||||
ondblclick="EditorDblClick(event);"
|
||||
context="msgComposeContext"/>
|
||||
|
||||
<html:div id="linkPreviewSettings" xmlns="http://www.w3.org/1999/xhtml" hidden="hidden">
|
||||
<span class="close">+</span>
|
||||
<h2 data-l10n-id="link-preview-title"></h2>
|
||||
<p data-l10n-id="link-preview-description"></p>
|
||||
<p>
|
||||
<input class="preview-autoadd" id="link-preview-autoadd" type="checkbox" />
|
||||
<label data-l10n-id="link-preview-autoadd" for="link-preview-autoadd"></label>
|
||||
</p>
|
||||
<p class="bottom">
|
||||
<span data-l10n-id="link-preview-replace-now"></span>
|
||||
<button class="preview-replace" data-l10n-id="link-preview-yes-replace"></button>
|
||||
</p>
|
||||
</html:div>
|
||||
|
||||
<findbar id="FindToolbar" browserid="messageEditor"/>
|
||||
</html:div>
|
||||
|
||||
|
@ -2536,5 +2551,29 @@
|
|||
|
||||
#include ../../../base/content/tabDialogs.inc.xhtml
|
||||
#include ../../../extensions/openpgp/content/ui/keyAssistant.inc.xhtml
|
||||
|
||||
<html:template id="dataCardTemplate" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<aside class="moz-card" style="width:600px; display:flex; align-items:center; justify-content:center; flex-direction:row; flex-wrap:wrap; border-radius:10px; border:1px solid silver;">
|
||||
<a class="remove-card">+</a>
|
||||
<div class="card-pic" style="display:flex; flex-direction:column; flex-basis:100%; flex:1;">
|
||||
<div style="margin:0 5px;">
|
||||
<img src="IMAGE" style="width:120px;" alt="ALT" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content" style="display:flex; flex-direction:column; flex-basis:100%; flex:3; background-color:#F5F5F5;">
|
||||
<div style="margin:5px; ">
|
||||
<p><small class="site">SITE</small></p>
|
||||
<p>
|
||||
<big class="title">TITLE</big>
|
||||
</p>
|
||||
<p class="creator">CREATOR</p>
|
||||
<p class="description">DESCRIPTION</p>
|
||||
<p>
|
||||
<a href="#" class="url" style="display:inline-block; max-width:440px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; text-decoration:none;">URL</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</html:template>
|
||||
</html:body>
|
||||
</html>
|
||||
|
|
|
@ -49,6 +49,12 @@
|
|||
preference="mail.warn_on_send_accel_key"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
<hbox>
|
||||
<checkbox id="addLinkPreviews"
|
||||
data-l10n-id="add-link-previews"
|
||||
preference="mail.compose.add_link_preview"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
</html:fieldset>
|
||||
</html:div>
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ Preferences.addAll([
|
|||
{ id: "mail.compose.big_attachments.notify", type: "bool" },
|
||||
{ id: "mail.compose.big_attachments.threshold_kb", type: "int" },
|
||||
{ id: "mail.default_send_format", type: "int" },
|
||||
{ id: "mail.compose.add_link_preview", type: "bool" },
|
||||
]);
|
||||
|
||||
var gComposePane = {
|
||||
|
|
|
@ -423,3 +423,11 @@ cloud-file-attachment-error = Failed to update the Filelink attachment { $filena
|
|||
# $filename (string) - name of the file that was renamed and caused the error
|
||||
cloud-file-account-error-title = Filelink Account Error
|
||||
cloud-file-account-error = Failed to update the Filelink attachment { $filename }, because its Filelink account has been deleted.
|
||||
|
||||
## Link Preview
|
||||
|
||||
link-preview-title = Link Preview
|
||||
link-preview-description = { -brand-short-name } can add an embedded preview when pasting links.
|
||||
link-preview-autoadd = Automatically add link previews when possible
|
||||
link-preview-replace-now = Add a Link Preview for this link?
|
||||
link-preview-yes-replace = Yes
|
||||
|
|
|
@ -527,6 +527,10 @@ warn-on-send-accel-key =
|
|||
.label = Confirm when using keyboard shortcut to send message
|
||||
.accesskey = C
|
||||
|
||||
add-link-previews =
|
||||
.label = Add link previews when pasting URLs
|
||||
.accesskey = i
|
||||
|
||||
spellcheck-label =
|
||||
.label = Check spelling before sending
|
||||
.accesskey = C
|
||||
|
|
|
@ -43,7 +43,10 @@ prefs =
|
|||
datareporting.policy.dataSubmissionPolicyBypassNotification=true
|
||||
skip-if = os == 'linux' && bits == 32 && debug # Perma-fail
|
||||
subsuite = thunderbird
|
||||
support-files = data/** ../openpgp/data/**
|
||||
support-files =
|
||||
data/**
|
||||
../openpgp/data/**
|
||||
html/linkpreview.html
|
||||
|
||||
[browser_addressWidgets.js]
|
||||
[browser_attachment.js]
|
||||
|
@ -82,6 +85,7 @@ reason = Cannot open the Format menu
|
|||
[browser_imageInsertionDialog.js]
|
||||
[browser_inlineImage.js]
|
||||
skip-if = headless # clipboard doesn't work with headless
|
||||
[browser_linkPreviews.js]
|
||||
[browser_messageBody.js]
|
||||
[browser_multipartRelated.js]
|
||||
[browser_newmsgComposeIdentity.js]
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Test link previews.
|
||||
*/
|
||||
|
||||
var { close_compose_window, open_compose_new_mail } = ChromeUtils.import(
|
||||
"resource://testing-common/mozmill/ComposeHelpers.jsm"
|
||||
);
|
||||
|
||||
var url =
|
||||
"http://mochi.test:8888/browser/comm/mail/test/browser/composition/html/linkpreview.html";
|
||||
|
||||
add_task(async function previewEnabled() {
|
||||
Services.prefs.setBoolPref("mail.compose.add_link_preview", true);
|
||||
let controller = open_compose_new_mail();
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
let messageEditor = controller.e("messageEditor");
|
||||
messageEditor.focus();
|
||||
|
||||
// Ctrl+V = Paste
|
||||
EventUtils.synthesizeKey(
|
||||
"v",
|
||||
{ shiftKey: false, accelKey: true },
|
||||
controller.window
|
||||
);
|
||||
|
||||
await TestUtils.waitForCondition(
|
||||
() => messageEditor.contentDocument.body.querySelector(".moz-card"),
|
||||
"link preview should have appeared"
|
||||
);
|
||||
|
||||
close_compose_window(controller);
|
||||
Services.prefs.clearUserPref("mail.compose.add_link_preview");
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
<title>OG test</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta property="og:title" content="Article title" />
|
||||
<meta property="og:url" content="https://www.example.com/?test=true" />
|
||||
<meta property="og:image" content="http://mochi.test:8888/browser/comm/mail/test/browser/content-policy/html/pass.png" />
|
||||
<meta property="og:description" content="Description of test article." />
|
||||
</head>
|
||||
<body>
|
||||
<p>See <a href="https://ogp.me/">https://ogp.me/</a>
|
||||
</body>
|
||||
</html>
|
|
@ -59,6 +59,36 @@ img {
|
|||
-moz-force-broken-image-icon: 1;
|
||||
}
|
||||
|
||||
.moz-card {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
}
|
||||
.moz-card.loaded {
|
||||
opacity: 1;
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.moz-card .remove-card {
|
||||
position: absolute;
|
||||
inset-inline-end: 15px;
|
||||
top: 15px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 1.4em;
|
||||
float: inline-end;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
transform: rotate(45deg);
|
||||
margin-block: -0.2em 0.2em;
|
||||
margin-inline: 0.2em -0.2em;
|
||||
}
|
||||
.moz-card .remove-card:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Can be removed when it is in messageQuotes.css enabled again */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
|
|
|
@ -1332,6 +1332,37 @@ button:is(
|
|||
display: none;
|
||||
}
|
||||
|
||||
#linkPreviewSettings {
|
||||
border: 1px solid silver;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
width: 500px;
|
||||
}
|
||||
#linkPreviewSettings h2 {
|
||||
color: blue;
|
||||
font-size: 1em;
|
||||
}
|
||||
#linkPreviewSettings p {
|
||||
margin: 0.5em 0.2em;
|
||||
}
|
||||
#linkPreviewSettings .bottom {
|
||||
padding: 1em 0;
|
||||
}
|
||||
#linkPreviewSettings button {
|
||||
background-color: navy;
|
||||
color: white;
|
||||
padding: 0.2em 2em;
|
||||
}
|
||||
#linkPreviewSettings .close {
|
||||
font-size: 1.4em;
|
||||
float: inline-end;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
transform: rotate(45deg);
|
||||
margin-block: -0.2em 0.2em;
|
||||
margin-inline: 0.2em -0.2em;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
Загрузка…
Ссылка в новой задаче