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
4 changes: 4 additions & 0 deletions app/assets/stylesheets/common/base/photoswipe.scss
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,7 @@ div.pswp__img--placeholder,
.pswp--one-slide .pswp__counter {
display: none;
}

.pswp__button--quote-image .pswp__icn {
transform: scale(0.67);
}
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4913,6 +4913,7 @@ en:
download: "Download"
open: "Original image"
image_info: "Image information"
quote: "Quote image"
previous: "Previous (Left arrow key)"
next: "Next (Right arrow key)"
counter: "%curr% of %total%"
Expand Down
32 changes: 21 additions & 11 deletions frontend/discourse/app/controllers/topic.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ export default class TopicController extends Controller {
? Promise.resolve(loadedPost)
: this.get("model.postStream").loadPost(postId);

return promise.then((post) => {
return promise.then(async (post) => {
const composer = this.composer;
const viewOpen = composer.get("model.viewOpen");

Expand Down Expand Up @@ -550,7 +550,6 @@ export default class TopicController extends Controller {
}

const quotedText = buildQuote(post, buffer, opts);
composerOpts.quote = quotedText;

if (composer.get("model.viewOpen")) {
this.appEvents.trigger("composer:insert-block", quotedText);
Expand All @@ -559,6 +558,16 @@ export default class TopicController extends Controller {
model.set("reply", model.get("reply") + "\n" + quotedText);
composer.openIfDraft();
} else {
const draftData = await Draft.get(composerOpts.draftKey);

if (draftData.draft) {
const data = JSON.parse(draftData.draft);
composerOpts.draftSequence = draftData.draft_sequence;
composerOpts.reply = data.reply + "\n" + quotedText;
} else {
composerOpts.quote = quotedText;
}

composer.open(composerOpts);
}
});
Expand Down Expand Up @@ -794,24 +803,25 @@ export default class TopicController extends Controller {
draftSequence: topic.get("draft_sequence"),
};

if (quotedText) {
opts.quote = quotedText;
}

if (post && post.get("post_number") !== 1) {
opts.post = post;
} else {
opts.topic = topic;
}

if (!opts.quote) {
const draftData = await Draft.get(opts.draftKey);
const draftData = await Draft.get(opts.draftKey);

if (draftData.draft) {
const data = JSON.parse(draftData.draft);
if (draftData.draft) {
const data = JSON.parse(draftData.draft);
opts.draftSequence = draftData.draft_sequence;

if (quotedText) {
opts.reply = (data.reply || "") + "\n" + quotedText;
} else {
opts.reply = data.reply;
opts.draftSequence = draftData.draft_sequence;
}
} else if (quotedText) {
opts.quote = quotedText;
}

composerController.open(opts);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export default {
return highlightSyntax(elem, siteSettings, session);
});

api.decorateCookedElement((elem) => {
return lightbox(elem, siteSettings);
api.decorateCookedElement((elem, helper) => {
return lightbox(elem, siteSettings, helper.model);
});

api.decorateCookedElement((elem) => {
Expand Down
52 changes: 50 additions & 2 deletions frontend/discourse/app/lib/lightbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { isDocumentRTL } from "discourse/lib/text-direction";
import {
escapeExpression,
Expand All @@ -17,7 +18,11 @@ export async function loadMagnificPopup() {
await waitForPromise(import("magnific-popup"));
}

export default async function lightbox(elem, siteSettings) {
export default async function lightbox(
elem,
siteSettings,
relatedModel = null
) {
if (!elem) {
return;
}
Expand Down Expand Up @@ -199,6 +204,38 @@ export default async function lightbox(elem, siteSettings) {
},
});

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 = 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();
}
});
},
});

lightboxEl.pswp.ui.registerElement({
name: "custom-counter",
order: 6,
Expand Down Expand Up @@ -239,6 +276,7 @@ export default async function lightbox(elem, siteSettings) {
}

const imgInfo = el.querySelector(".informations")?.textContent || "";
const imgEl = el.tagName === "IMG" ? el : el.querySelector("img");

if (!width || !height) {
const dimensions = imgInfo.trim().split(" ")[0];
Expand All @@ -249,10 +287,20 @@ export default async function lightbox(elem, siteSettings) {
data.thumbCropped = true;

data.src = data.src || el.getAttribute("data-large-src");
data.title = el.title || el.alt;
data.origSrc = imgEl?.getAttribute("data-orig-src");
data.title = el.title || imgEl?.alt || imgEl?.title;
data.base62SHA1 = imgEl?.getAttribute("data-base62-sha1");
data.details = imgInfo;
data.w = data.width = width;
data.h = data.height = height;
data.targetWidth =
el.getAttribute("data-target-width") || imgEl.getAttribute("width");
data.targetHeight =
el.getAttribute("data-target-height") || imgEl.getAttribute("height");

if (relatedModel?.constructor?.name === "Post") {
data.post = relatedModel;
}

return data;
});
Expand Down
113 changes: 113 additions & 0 deletions frontend/discourse/app/lib/lightbox/quote-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { getOwner } from "@ember/owner";
import { helperContext } from "discourse/lib/helpers";
import { buildImageMarkdown as buildImageMarkdownShared } from "discourse/lib/markdown-image-builder";
import { buildQuote } from "discourse/lib/quote";
import Composer from "discourse/models/composer";
import Draft from "discourse/models/draft";

function buildImageMarkdown(slideElement, slideData) {
const img = slideElement?.querySelector("img");

if (!img) {
return null;
}

let src;

// Check for base62 SHA1 to use short upload:// URL format (same as to-markdown.js)
if (slideData.base62SHA1) {
src = `upload://${slideData.base62SHA1}`;
} else {
// Prefer data-orig-src (same as to-markdown.js)
src = slideData.origSrc || slideData.src;
}

if (!src) {
return null;
}

return buildImageMarkdownShared({
src,
alt: slideData.title,
width: slideData.targetWidth,
height: slideData.targetHeight,
fallbackAlt: "image",
});
}

export function canQuoteImage(slideElement, slideData) {
return buildImageMarkdown(slideElement, slideData) !== null;
}

export default async function quoteImage(slideElement, slideData) {
try {
const ownerContext = helperContext();

if (!slideElement || !ownerContext) {
return false;
}

const owner = getOwner(ownerContext);

if (!owner) {
return false;
}

const markdown = buildImageMarkdown(slideElement, slideData);
if (!markdown) {
return false;
}

const composer = owner.lookup("service:composer");
if (!composer) {
return false;
}

const post = slideData.post;
const quote = buildQuote(post, markdown);

if (!quote) {
return false;
}

if (composer.model?.viewOpen) {
const appEvents = owner.lookup("service:app-events");
appEvents?.trigger("composer:insert-block", quote);
return true;
}

if (composer.model?.viewDraft) {
const model = composer.model;
model.set("reply", model.get("reply") + "\n" + quote);
composer.openIfDraft();
return true;
}

const composerOpts = {
action: Composer.REPLY,
draftKey: post.topic.draft_key,
draftSequence: post.topic.draft_sequence,
};

if (post.post_number === 1) {
composerOpts.topic = post.topic;
} else {
composerOpts.post = post;
}

const draftData = await Draft.get(composerOpts.draftKey);

if (draftData.draft) {
const data = JSON.parse(draftData.draft);
composerOpts.draftSequence = draftData.draft_sequence;
composerOpts.reply = data.reply + "\n" + quote;
} else {
composerOpts.quote = quote;
}

await composer.open(composerOpts);
return true;
} catch {
return false;
}
}
37 changes: 37 additions & 0 deletions frontend/discourse/app/lib/markdown-image-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function sanitizeAlt(text, options = {}) {
const fallback = options.fallback ?? "";

if (!text) {
return fallback;
}

const trimmed = text.trim();
if (!trimmed) {
return fallback;
}

return trimmed.replace(/([\\\[\]])/g, "\\$1").replace(/\|/g, "&#124;");
}

export function buildImageMarkdown(imageData) {
const {
src,
alt,
width,
height,
title,
escapeTablePipe = false,
fallbackAlt,
} = imageData;

if (!src) {
return "";
}

const altText = sanitizeAlt(alt, { fallback: fallbackAlt });
const pipe = escapeTablePipe ? "\\|" : "|";
const suffix = width && height ? `${pipe}${width}x${height}` : "";
const titleSuffix = title ? ` "${title}"` : "";

return `![${altText}${suffix}](${src}${titleSuffix})`;
}
23 changes: 13 additions & 10 deletions frontend/discourse/app/lib/to-markdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { buildImageMarkdown } from "discourse/lib/markdown-image-builder";

const MSO_LIST_CLASSES = [
"MsoListParagraphCxSpFirst",
"MsoListParagraphCxSpMiddle",
Expand Down Expand Up @@ -426,19 +428,20 @@ export class Tag {
return "[image]";
}

let alt = attr.alt || pAttr.alt || "";
const alt = attr.alt || pAttr.alt;
const width = attr.width || pAttr.width;
const height = attr.height || pAttr.height;
const title = attr.title;

if (width && height) {
const pipe = this.element.parentNames.includes("table")
? "\\|"
: "|";
alt = `${alt}${pipe}${width}x${height}`;
}

return `![${alt}](${src}${title ? ` "${title}"` : ""})`;
const escapeTablePipe = this.element.parentNames.includes("table");

return buildImageMarkdown({
src,
alt,
width,
height,
title,
escapeTablePipe,
});
}

return "";
Expand Down
Loading
Loading