diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 08fdb75715c..ac22a9a30ac 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -194,7 +194,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = {
const requestedWidth = Number(attribs.width);
let requestedHeight = Number(attribs.height);
if ("data-mx-emoticon" in attribs) {
- requestedHeight = Math.floor(18*window.devicePixelRatio); // 18 is the display height of a normal small emoji
+ requestedHeight = Math.floor(18 * window.devicePixelRatio); // 18 is the display height of a normal small emoji
}
const width = Math.min(requestedWidth || 800, 800);
const height = Math.min(requestedHeight || 600, 600);
@@ -311,7 +311,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
div: ["data-mx-maths"],
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
// img tags also accept width/height, we just map those to max-width & max-height during transformation
- img: ["src", "alt", "title", "style", "data-mx-emoticon"],
+ img: ["src", "alt", "title", "style", "data-mx-emoticon", "data-mx-pack-url"],
ol: ["start"],
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
},
@@ -596,14 +596,24 @@ export function bodyToHtml(content: IContent, highlights: Optional, op
});
safeBodyNeedsSerialisation = true;
}
- if (isAllHtmlEmoji && !opts.disableBigEmoji) { // Big emoji? Big image URLs.
+ if (isAllHtmlEmoji && !opts.disableBigEmoji) {
+ // Big emoji? Big image URLs.
(phtml.root()[0] as cheerio.TagElement).children.forEach((elm) => {
- if (elm.name === "img" && "data-mx-emoticon" in elm.attribs && typeof elm.attribs.src === "string") {
- elm.attribs.src = elm.attribs.src.replace(/height=[0-9]*/, `height=${Math.floor(48*window.devicePixelRatio)}`) // 48 is the display height of a big emoji
+ const tagElm = elm as cheerio.TagElement;
+ if (
+ tagElm.name === "img" &&
+ "data-mx-emoticon" in tagElm.attribs &&
+ typeof tagElm.attribs.src === "string"
+ ) {
+ tagElm.attribs.src = tagElm.attribs.src.replace(
+ /height=[0-9]*/,
+ `height=${Math.floor(48 * window.devicePixelRatio)}`,
+ ); // 48 is the display height of a big emoji
}
- })
+ });
}
- if (safeBodyNeedsSerialisation) { // SchildiChat: all done editing emojis, can finally serialise the body
+ if (safeBodyNeedsSerialisation) {
+ // SchildiChat: all done editing emojis, can finally serialise the body
safeBody = phtml.html();
}
if (bodyHasEmoji) {
@@ -633,11 +643,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
const matched = match?.[0]?.length === contentBodyTrimmed.length;
- emojiBody =
- (matched || isAllHtmlEmoji) &&
- (strippedBody === safeBody || // replies have the html fallbacks, account for that here
- content.formatted_body === undefined ||
- (!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:")));
+ emojiBody = matched || isAllHtmlEmoji;
}
const className = classNames({
diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts
index edd8782a5ff..9fa27fae9cc 100644
--- a/src/autocomplete/Autocompleter.ts
+++ b/src/autocomplete/Autocompleter.ts
@@ -28,6 +28,7 @@ import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SpaceProvider from "./SpaceProvider";
import { TimelineRenderingType } from "../contexts/RoomContext";
import { filterBoolean } from "../utils/arrays";
+import { ICustomEmoji } from "../emojipicker/customemoji";
export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not
@@ -46,6 +47,7 @@ export interface ICompletion {
// If provided, apply a LINK entity to the completion with the
// data = { url: href }.
href?: string;
+ customEmoji?: ICustomEmoji;
}
const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, CommandProvider, SpaceProvider];
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 0a98567ebe4..371d031f77a 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
// Load this room's image sets.
const imageSetEvents = room?.currentState?.getStateEvents("im.ponies.room_emotes");
let loadedImages: ICustomEmoji[] =
- imageSetEvents?.flatMap((imageSetEvent) => loadImageSet(imageSetEvent)) || [];
+ imageSetEvents?.flatMap((imageSetEvent) => loadImageSet(imageSetEvent, room)) || [];
// Global emotes from rooms
const cli = MatrixClientPeg.get();
@@ -115,7 +115,7 @@ export default class EmojiProvider extends AutocompleteProvider {
"im.ponies.room_emotes",
packRoomStateKey,
);
- const moreLoadedImages: ICustomEmoji[] = loadImageSet(packRoomImageSetEvents);
+ const moreLoadedImages: ICustomEmoji[] = loadImageSet(packRoomImageSetEvents, packRoom!);
loadedImages = [...loadedImages, ...(moreLoadedImages || [])];
}
}
@@ -244,6 +244,7 @@ export default class EmojiProvider extends AutocompleteProvider {
),
+ customEmoji: c.emoji,
range: range!,
} as const;
}
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index e166b7048a9..4d2cecbff29 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -76,7 +76,7 @@ class EmojiPicker extends React.Component {
let loadedImages: ICustomEmoji[];
if (props.room) {
const imageSetEvents = props.room.currentState.getStateEvents("im.ponies.room_emotes");
- loadedImages = imageSetEvents.flatMap((imageSetEvent) => loadImageSet(imageSetEvent));
+ loadedImages = imageSetEvents.flatMap((imageSetEvent) => loadImageSet(imageSetEvent, props.room));
} else {
loadedImages = [];
}
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index bcd50d5554b..7609abad0be 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -439,6 +439,13 @@ export default class TextualBody extends React.Component {
let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
// links processed by linkifyjs have their own handler so don't handle those here
if (target.classList.contains(linkifyOpts.className as string)) return;
+ // handle clicking packs
+ const packUrl = target.getAttribute("data-mx-pack-url");
+ if (packUrl) {
+ // it could be converted to a localHref -> therefore handle locally
+ e.preventDefault();
+ window.location.hash = tryTransformPermalinkToLocalHref(packUrl);
+ }
if (target.nodeName !== "A") {
// Jump to parent as the `` may contain children, e.g. an anchor wrapping an inline code section
target = target.closest("a");
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 31c1c61165c..1bc72c18823 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -900,7 +900,7 @@ export default class BasicMessageEditor extends React.Component
if ("unicode" in emoji) {
emojiPart = partCreator.emoji(emoji.unicode);
} else {
- emojiPart = partCreator.customEmoji(emoji.shortcodes[0], emoji.url);
+ emojiPart = partCreator.customEmoji(emoji.shortcodes[0], emoji.url, emoji.roomId, emoji.eventId);
}
model.transform(() => {
const addedLen = model.insert([emojiPart], position);
diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts
index dac8eaaebc2..567db69d28f 100644
--- a/src/editor/autocomplete.ts
+++ b/src/editor/autocomplete.ts
@@ -112,7 +112,14 @@ export default class AutocompleteWrapperModel {
// command needs special handling for auto complete, but also renders as plain texts
return [(this.partCreator as CommandPartCreator).command(text)];
case "customEmoji":
- return [this.partCreator.customEmoji(text, completionId)];
+ return [
+ this.partCreator.customEmoji(
+ text,
+ completionId!,
+ completion.customEmoji?.roomId,
+ completion.customEmoji?.eventId,
+ ),
+ ];
default:
// used for emoji and other plain text completion replacement
return this.partCreator.plainWithEmoji(text);
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index 77b225ff5d7..8eb2756e5c8 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -19,11 +19,12 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { checkBlockNode } from "../HtmlUtils";
-import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
+import { getPrimaryPermalinkEntity, parsePermalink } from "../utils/permalinks/Permalinks";
import { Part, PartCreator, Type } from "./parts";
import SdkConfig from "../SdkConfig";
import { textToHtmlRainbow } from "../utils/colour";
import { stripPlainReply } from "../utils/Reply";
+import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
const LIST_TYPES = ["UL", "OL", "LI"];
@@ -97,7 +98,13 @@ function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
const isCustomEmoji = elm.hasAttribute("data-mx-emoticon");
if (isCustomEmoji) {
const shortcode = elm.title || elm.alt || ":SHORTCODE_MISSING:";
- return [pc.customEmoji(shortcode, src)];
+ // parse the link
+ const packUrl = elm.getAttribute("data-mx-pack-url");
+ let permalinkParts: PermalinkParts | null = null;
+ if (packUrl) {
+ permalinkParts = parsePermalink(packUrl);
+ }
+ return [pc.customEmoji(shortcode, src, permalinkParts?.roomIdOrAlias, permalinkParts?.eventId)];
}
return pc.plainWithEmoji(``);
}
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 522c2679ae2..b033b722047 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -41,6 +41,8 @@ interface ISerializedPillPart {
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill | Type.CustomEmoji;
text: string;
resourceId?: string;
+ roomId?: string;
+ eventId?: string;
}
export type SerializedPart = ISerializedPart | ISerializedPillPart;
@@ -87,7 +89,12 @@ interface IPillPart extends Omit {
resourceId: string;
}
-export type Part = IBasePart | IPillCandidatePart | IPillPart;
+export interface ICustomEmojiPart extends IPillPart {
+ roomId?: string;
+ eventId?: string;
+}
+
+export type Part = IBasePart | IPillCandidatePart | IPillPart | ICustomEmojiPart;
abstract class BasePart {
protected _text: string;
@@ -418,7 +425,9 @@ export class EmojiPart extends BasePart implements IBasePart {
}
}
-class CustomEmojiPart extends PillPart implements IPillPart {
+class CustomEmojiPart extends PillPart implements ICustomEmojiPart {
+ public roomId?: string;
+ public eventId?: string;
protected get className(): string {
return "mx_CustomEmojiPill mx_Pill";
}
@@ -434,8 +443,10 @@ class CustomEmojiPart extends PillPart implements IPillPart {
this.setAvatarVars(node, url, this.text[0]);
}
- public constructor(shortCode: string, url: string) {
+ public constructor(shortCode: string, url: string, roomId?: string, eventId?: string) {
super(url, shortCode);
+ this.roomId = roomId;
+ this.eventId = eventId;
}
protected acceptsInsertion(chr: string): boolean {
return false;
@@ -452,6 +463,14 @@ class CustomEmojiPart extends PillPart implements IPillPart {
public get canEdit(): boolean {
return false;
}
+
+ public serialize(): ISerializedPillPart {
+ return {
+ ...super.serialize(),
+ roomId: this.roomId,
+ eventId: this.eventId,
+ };
+ }
}
class RoomPillPart extends PillPart {
@@ -622,7 +641,7 @@ export class PartCreator {
case Type.Emoji:
return this.emoji(part.text);
case Type.CustomEmoji:
- return this.customEmoji(part.text, part.resourceId);
+ return this.customEmoji(part.text, part.resourceId!, part.roomId!, part.eventId!);
case Type.AtRoomPill:
return this.atRoomPill(part.text);
case Type.PillCandidate:
@@ -701,8 +720,13 @@ export class PartCreator {
return parts;
}
- public customEmoji(shortcode: string, url: string): CustomEmojiPart {
- return new CustomEmojiPart(shortcode, url);
+ public customEmoji(
+ shortcode: string,
+ url: string,
+ roomId?: string | null,
+ eventId?: string | null,
+ ): CustomEmojiPart {
+ return new CustomEmojiPart(shortcode, url, roomId!, eventId!);
}
public createMentionParts(
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 30df421e042..427f34aacd8 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -21,11 +21,11 @@ import escapeHtml from "escape-html";
import _ from "lodash";
import Markdown from "../Markdown";
-import { makeGenericPermalink } from "../utils/permalinks/Permalinks";
+import { makeGenericPermalink, makeRoomPermalink } from "../utils/permalinks/Permalinks";
import EditorModel from "./model";
import SettingsStore from "../settings/SettingsStore";
import SdkConfig from "../SdkConfig";
-import { Type } from "./parts";
+import { ICustomEmojiPart, Type } from "./parts";
export function mdSerialize(model: EditorModel): string {
return model.parts.reduce((html, part) => {
@@ -53,6 +53,18 @@ export function mdSerialize(model: EditorModel): string {
`[${part.text.replace(/[[\\\]]/g, (c) => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`
);
case Type.CustomEmoji:
+ if ((part as ICustomEmojiPart).roomId) {
+ const permalink = makeRoomPermalink(
+ (part as ICustomEmojiPart).roomId!,
+ (part as ICustomEmojiPart).eventId,
+ );
+ return (
+ html +
+ `
`
+ );
+ }
return (
html +
`})