Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 38 additions & 33 deletions frontend/discourse/app/lib/lightbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { isRailsTesting, isTesting } from "discourse/lib/environment";
import { helperContext } from "discourse/lib/helpers";
import { renderIcon } from "discourse/lib/icon-library";
import { SELECTORS } from "discourse/lib/lightbox/constants";
import quoteImage, { canQuoteImage } from "discourse/lib/lightbox/quote-image";
import quoteImage, {
canBuildImageQuote,
} from "discourse/lib/lightbox/quote-image";
import { isDocumentRTL } from "discourse/lib/text-direction";
import {
escapeExpression,
postRNWebviewMessage,
} from "discourse/lib/utilities";
import User from "discourse/models/user";
import { i18n } from "discourse-i18n";

export async function loadMagnificPopup() {
Expand All @@ -27,10 +28,12 @@ export default async function lightbox(
return;
}

const currentUser = helperContext()?.currentUser;
const caps = helperContext().capabilities;
const imageClickNavigation = caps.touch;
const canDownload =
!siteSettings.prevent_anons_from_downloading_files || User.current();
!siteSettings.prevent_anons_from_downloading_files || !!currentUser;
const canQuoteImage = currentUser;

if (siteSettings.experimental_lightbox) {
const { default: PhotoSwipeLightbox } = await import("photoswipe/lightbox");
Expand Down Expand Up @@ -204,37 +207,39 @@ export default async function lightbox(
},
});

lightboxEl.pswp.ui.registerElement({
name: "quote-image",
order: 10,
isButton: true,
title: i18n("lightbox.quote"),
html: {
isCustomSVG: true,
inner:
'<path id="pswp__icn-quote" d="M0 216C0 149.7 53.7 96 120 96l8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-8 0c-30.9 0-56 25.1-56 56l0 8 64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32 0-32 0-72zm256 0c0-66.3 53.7-120 120-120l8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-8 0c-30.9 0-56 25.1-56 56l0 8 64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32 0-32 0-72z"/>',
outlineID: "pswp__icn-quote",
size: 512,
},
onInit: (el, pswp) => {
pswp.on("change", () => {
const slideData = pswp.currSlide?.data;
if (canQuoteImage) {
lightboxEl.pswp.ui.registerElement({
name: "quote-image",
order: 10,
isButton: true,
title: i18n("lightbox.quote"),
html: {
isCustomSVG: true,
inner:
'<path id="pswp__icn-quote" d="M0 216C0 149.7 53.7 96 120 96l8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-8 0c-30.9 0-56 25.1-56 56l0 8 64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32 0-32 0-72zm256 0c0-66.3 53.7-120 120-120l8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-8 0c-30.9 0-56 25.1-56 56l0 8 64 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64l-64 0c-35.3 0-64-28.7-64-64l0-32 0-32 0-72z"/>',
outlineID: "pswp__icn-quote",
size: 512,
},
onInit: (el, pswp) => {
pswp.on("change", () => {
const slideData = pswp.currSlide?.data;
const slideElement = slideData?.element;
el.style.display = canBuildImageQuote(slideElement, slideData)
? ""
: "none";
});
},
onClick: () => {
const slideData = lightboxEl.pswp.currSlide?.data;
const slideElement = slideData?.element;
el.style.display = canQuoteImage(slideElement, slideData)
? ""
: "none";
});
},
onClick: () => {
const slideData = lightboxEl.pswp.currSlide?.data;
const slideElement = slideData?.element;
quoteImage(slideElement, slideData).then((didQuote) => {
if (didQuote) {
lightboxEl.pswp.close();
}
});
},
});
quoteImage(slideElement, slideData).then((didQuote) => {
if (didQuote) {
lightboxEl.pswp.close();
}
});
},
});
}

lightboxEl.pswp.ui.registerElement({
name: "custom-counter",
Expand Down
18 changes: 16 additions & 2 deletions frontend/discourse/app/lib/lightbox/quote-image.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { getOwner } from "@ember/owner";
import { helperContext } from "discourse/lib/helpers";
import { buildImageMarkdown as buildImageMarkdownShared } from "discourse/lib/markdown-image-builder";
import {
buildImageMarkdown as buildImageMarkdownShared,
extensionFromUrl,
} from "discourse/lib/markdown-image-builder";
import { buildQuote } from "discourse/lib/quote";
import Composer from "discourse/models/composer";
import Draft from "discourse/models/draft";
Expand All @@ -16,7 +19,11 @@ function buildImageMarkdown(slideElement, slideData) {

// Check for base62 SHA1 to use short upload:// URL format (same as to-markdown.js)
if (slideData.base62SHA1) {
const extension = extensionFromUrl(slideData.origSrc);
src = `upload://${slideData.base62SHA1}`;
if (extension) {
src += `.${extension}`;
}
} else {
// Prefer data-orig-src (same as to-markdown.js)
src = slideData.origSrc || slideData.src;
Expand All @@ -35,7 +42,14 @@ function buildImageMarkdown(slideElement, slideData) {
});
}

export function canQuoteImage(slideElement, slideData) {
/**
* Checks if the slide element and data can be used to build a quote.
*
* @param {Element} slideElement
* @param {object} slideData
* @returns {boolean}
*/
export function canBuildImageQuote(slideElement, slideData) {
return buildImageMarkdown(slideElement, slideData) !== null;
}

Expand Down
16 changes: 16 additions & 0 deletions frontend/discourse/app/lib/markdown-image-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ export function sanitizeAlt(text, options = {}) {
return trimmed.replace(/\|/g, "&#124;").replace(/([\\\[\]])/g, "\\$1");
}

/**
* Extracts the extension (without dot) from a URL or path.
* Returns null when no extension is present.
*
* @param {string} url
* @returns {string|null}
*/
export function extensionFromUrl(url) {
if (!url) {
return null;
}

const match = url.match(/\.([a-zA-Z0-9]+)$/);
return match ? match[1] : null;
}

export function buildImageMarkdown(imageData) {
const {
src,
Expand Down
20 changes: 19 additions & 1 deletion frontend/discourse/app/lib/to-markdown.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { buildImageMarkdown } from "discourse/lib/markdown-image-builder";
import {
buildImageMarkdown,
extensionFromUrl,
} from "discourse/lib/markdown-image-builder";

const MSO_LIST_CLASSES = [
"MsoListParagraphCxSpFirst",
Expand Down Expand Up @@ -365,6 +368,14 @@ export class Tag {

if (base62SHA1) {
href = `upload://${base62SHA1}`;
const extension =
extensionFromUrl(attr.href) ||
extensionFromUrl(img.attributes.src) ||
extensionFromUrl(attr["data-download-href"]);

if (extension) {
href += `.${extension}`;
}
}

const width = img.attributes.width;
Expand Down Expand Up @@ -413,6 +424,13 @@ export class Tag {
const base62SHA1 = attr["data-base62-sha1"];
if (base62SHA1) {
src = `upload://${base62SHA1}`;
const extension =
extensionFromUrl(attr.src || pAttr.src) ||
extensionFromUrl(attr["data-orig-src"]);

if (extension) {
src += `.${extension}`;
}
}

if (cssClass?.includes("emoji")) {
Expand Down
28 changes: 21 additions & 7 deletions frontend/discourse/tests/unit/lib/lightbox/quote-image-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import Service from "@ember/service";
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";
import sinon from "sinon";
import quoteImage, { canQuoteImage } from "discourse/lib/lightbox/quote-image";
import { createHelperContext, helperContext } from "discourse/lib/helpers";
import quoteImage, {
canBuildImageQuote,
} from "discourse/lib/lightbox/quote-image";
import Draft from "discourse/models/draft";
import { logIn } from "discourse/tests/helpers/qunit-helpers";

class ComposerStub extends Service {
model = { viewOpen: false };
Expand All @@ -27,6 +31,15 @@ module("Unit | Lib | lightbox | quote image", function (hooks) {
setupTest(hooks);

hooks.beforeEach(function () {
this.originalHelperContext = helperContext();

logIn(this.owner);

createHelperContext({
...(this.originalHelperContext || {}),
currentUser: this.owner.lookup("service:current-user"),
});

this.owner.unregister("service:composer");
this.owner.unregister("service:app-events");

Expand Down Expand Up @@ -55,6 +68,7 @@ module("Unit | Lib | lightbox | quote image", function (hooks) {
hooks.afterEach(function () {
document.querySelectorAll(".topic-post").forEach((el) => el.remove());
this.draftGetStub.restore();
createHelperContext(this.originalHelperContext);
});

function buildLightbox(context, overrides = {}) {
Expand Down Expand Up @@ -132,12 +146,12 @@ module("Unit | Lib | lightbox | quote image", function (hooks) {
);
});

test("canQuoteImage only returns true when context and metadata exist", function (assert) {
test("canBuildImageQuote only returns true when context and metadata exist", function (assert) {
const invalid = document.createElement("a");
assert.false(canQuoteImage(invalid, {}));
assert.false(canBuildImageQuote(invalid, {}));

const { element, slideData } = buildLightbox(this);
assert.true(canQuoteImage(element, slideData));
assert.true(canBuildImageQuote(element, slideData));
});

test("builds markdown using data-orig-src and dimensions when composer is closed", async function (assert) {
Expand Down Expand Up @@ -187,7 +201,7 @@ module("Unit | Lib | lightbox | quote image", function (hooks) {
);
});

test("uses short upload:// URL when data-base62-sha1 is present", async function (assert) {
test("uses short upload:// URL with extension when data-base62-sha1 is present", async function (assert) {
const { element, slideData } = buildLightbox(this, {
base62SHA1: "a4bcwvmLAy8cGHKPUrK4G3AUbt9",
href: "//localhost:4200/uploads/default/original/1X/468eb8aa1f0126f1ce7e7ea7a2f64f25da0b58db.png",
Expand All @@ -200,9 +214,9 @@ module("Unit | Lib | lightbox | quote image", function (hooks) {
const quote = this.composer.openCalls[0].quote;
assert.true(
quote.includes(
"![diagram|640x480](upload://a4bcwvmLAy8cGHKPUrK4G3AUbt9)"
"![diagram|640x480](upload://a4bcwvmLAy8cGHKPUrK4G3AUbt9.png)"
),
"uses short upload:// URL format with base62-sha1"
"uses short upload:// URL format with base62-sha1 and extension"
);
});

Expand Down
4 changes: 2 additions & 2 deletions frontend/discourse/tests/unit/lib/to-markdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ module("Unit | Utility | to-markdown", function (hooks) {
html = `<img src="${url}" width="100" height="50" title="some title" data-base62-sha1="${base62SHA1}">`;
assert.strictEqual(
toMarkdown(html),
`![|100x50](upload://${base62SHA1} "some title")`
`![|100x50](upload://${base62SHA1}.png "some title")`
);

html = `<div><span><img src="${url}" alt="description" width="50" height="100" /></span></div>`;
Expand Down Expand Up @@ -437,7 +437,7 @@ helloWorld();</code>consectetur.`;
<span class="filename">sherlock3_sig.jpg</span><span class="informations">5496×3664 2 MB</span><span class="expand"></span>
</div></a>
`;
markdown = `![sherlock3_sig.jpg|689x459](upload://1frsimI7TOtFJyD2LLyKSHM8JWe)`;
markdown = `![sherlock3_sig.jpg|689x459](upload://1frsimI7TOtFJyD2LLyKSHM8JWe.jpeg)`;

assert.strictEqual(toMarkdown(html), markdown);
});
Expand Down