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("editor.CR_creates_new_p", true);
|
||||||
pref("mail.compose.default_to_paragraph", 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
|
// 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
|
// 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
|
pref("mailnews.headers.minNumHeaders", 0); // 0 means we ignore this pref
|
||||||
|
|
|
@ -193,6 +193,8 @@ var gUserTouchedEncryptSubject = false;
|
||||||
var gIsRelatedToEncryptedOriginal = false;
|
var gIsRelatedToEncryptedOriginal = false;
|
||||||
var gIsRelatedToSignedOriginal = false;
|
var gIsRelatedToSignedOriginal = false;
|
||||||
|
|
||||||
|
var gOpened = Date.now();
|
||||||
|
|
||||||
var gEncryptedURIService = Cc[
|
var gEncryptedURIService = Cc[
|
||||||
"@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
|
"@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"
|
||||||
].getService(Ci.nsIEncryptedSMIMEURIsService);
|
].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
|
* 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.
|
* the editor, replacing file URLs with data URLs when appropriate.
|
||||||
*/
|
*/
|
||||||
function onPasteOrDrop(e) {
|
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) {
|
if (!gMsgCompose.composeHTML) {
|
||||||
// We're in the plain text editor. Nothing to do here.
|
// We're in the plain text editor. Nothing to do here.
|
||||||
return;
|
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 html = dataTransfer.getData("text/html");
|
||||||
let doc = new DOMParser().parseFromString(html, "text/html");
|
let doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||||
|
@ -5552,6 +5717,15 @@ function GenericSendMessage(msgType) {
|
||||||
msgCompFields.newsgroups = "";
|
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();
|
let sendFormat = determineSendFormat();
|
||||||
switch (sendFormat) {
|
switch (sendFormat) {
|
||||||
case Ci.nsIMsgCompSendFormat.PlainText:
|
case Ci.nsIMsgCompSendFormat.PlainText:
|
||||||
|
|
|
@ -2435,7 +2435,6 @@
|
||||||
<html:hr is="pane-splitter" id="headersSplitter"
|
<html:hr is="pane-splitter" id="headersSplitter"
|
||||||
resize-direction="vertical"
|
resize-direction="vertical"
|
||||||
resize-id="MsgHeadersToolbar" />
|
resize-id="MsgHeadersToolbar" />
|
||||||
|
|
||||||
<html:div id="messageArea">
|
<html:div id="messageArea">
|
||||||
<html:div id="dropAttachmentOverlay" class="drop-attachment-overlay">
|
<html:div id="dropAttachmentOverlay" class="drop-attachment-overlay">
|
||||||
<html:aside id="addInline" class="drop-attachment-box">
|
<html:aside id="addInline" class="drop-attachment-box">
|
||||||
|
@ -2465,8 +2464,24 @@
|
||||||
aria-label="&aria.message.bodyName;"
|
aria-label="&aria.message.bodyName;"
|
||||||
messagemanagergroup="browsers"
|
messagemanagergroup="browsers"
|
||||||
oncontextmenu="this._contextX = event.pageX; this._contextY = event.pageY;"
|
oncontextmenu="this._contextX = event.pageX; this._contextY = event.pageY;"
|
||||||
|
onclick="EditorClick(event);"
|
||||||
ondblclick="EditorDblClick(event);"
|
ondblclick="EditorDblClick(event);"
|
||||||
context="msgComposeContext"/>
|
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"/>
|
<findbar id="FindToolbar" browserid="messageEditor"/>
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|
||||||
|
@ -2536,5 +2551,29 @@
|
||||||
|
|
||||||
#include ../../../base/content/tabDialogs.inc.xhtml
|
#include ../../../base/content/tabDialogs.inc.xhtml
|
||||||
#include ../../../extensions/openpgp/content/ui/keyAssistant.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:body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -49,6 +49,12 @@
|
||||||
preference="mail.warn_on_send_accel_key"/>
|
preference="mail.warn_on_send_accel_key"/>
|
||||||
<spacer flex="1"/>
|
<spacer flex="1"/>
|
||||||
</hbox>
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox id="addLinkPreviews"
|
||||||
|
data-l10n-id="add-link-previews"
|
||||||
|
preference="mail.compose.add_link_preview"/>
|
||||||
|
<spacer flex="1"/>
|
||||||
|
</hbox>
|
||||||
</html:fieldset>
|
</html:fieldset>
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ Preferences.addAll([
|
||||||
{ id: "mail.compose.big_attachments.notify", type: "bool" },
|
{ id: "mail.compose.big_attachments.notify", type: "bool" },
|
||||||
{ id: "mail.compose.big_attachments.threshold_kb", type: "int" },
|
{ id: "mail.compose.big_attachments.threshold_kb", type: "int" },
|
||||||
{ id: "mail.default_send_format", type: "int" },
|
{ id: "mail.default_send_format", type: "int" },
|
||||||
|
{ id: "mail.compose.add_link_preview", type: "bool" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var gComposePane = {
|
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
|
# $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-title = Filelink Account Error
|
||||||
cloud-file-account-error = Failed to update the Filelink attachment { $filename }, because its Filelink account has been deleted.
|
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
|
.label = Confirm when using keyboard shortcut to send message
|
||||||
.accesskey = C
|
.accesskey = C
|
||||||
|
|
||||||
|
add-link-previews =
|
||||||
|
.label = Add link previews when pasting URLs
|
||||||
|
.accesskey = i
|
||||||
|
|
||||||
spellcheck-label =
|
spellcheck-label =
|
||||||
.label = Check spelling before sending
|
.label = Check spelling before sending
|
||||||
.accesskey = C
|
.accesskey = C
|
||||||
|
|
|
@ -43,7 +43,10 @@ prefs =
|
||||||
datareporting.policy.dataSubmissionPolicyBypassNotification=true
|
datareporting.policy.dataSubmissionPolicyBypassNotification=true
|
||||||
skip-if = os == 'linux' && bits == 32 && debug # Perma-fail
|
skip-if = os == 'linux' && bits == 32 && debug # Perma-fail
|
||||||
subsuite = thunderbird
|
subsuite = thunderbird
|
||||||
support-files = data/** ../openpgp/data/**
|
support-files =
|
||||||
|
data/**
|
||||||
|
../openpgp/data/**
|
||||||
|
html/linkpreview.html
|
||||||
|
|
||||||
[browser_addressWidgets.js]
|
[browser_addressWidgets.js]
|
||||||
[browser_attachment.js]
|
[browser_attachment.js]
|
||||||
|
@ -82,6 +85,7 @@ reason = Cannot open the Format menu
|
||||||
[browser_imageInsertionDialog.js]
|
[browser_imageInsertionDialog.js]
|
||||||
[browser_inlineImage.js]
|
[browser_inlineImage.js]
|
||||||
skip-if = headless # clipboard doesn't work with headless
|
skip-if = headless # clipboard doesn't work with headless
|
||||||
|
[browser_linkPreviews.js]
|
||||||
[browser_messageBody.js]
|
[browser_messageBody.js]
|
||||||
[browser_multipartRelated.js]
|
[browser_multipartRelated.js]
|
||||||
[browser_newmsgComposeIdentity.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-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 */
|
/* Can be removed when it is in messageQuotes.css enabled again */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -1332,6 +1332,37 @@ button:is(
|
||||||
display: none;
|
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 {
|
.statusbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
Загрузка…
Ссылка в новой задаче