Skip to content

Commit 270b085

Browse files
sending custom emoji with the new attribute
make them clickable fix typescript error checks
1 parent 6c4e34f commit 270b085

File tree

12 files changed

+118
-33
lines changed

12 files changed

+118
-33
lines changed

src/HtmlUtils.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = {
194194
const requestedWidth = Number(attribs.width);
195195
let requestedHeight = Number(attribs.height);
196196
if ("data-mx-emoticon" in attribs) {
197-
requestedHeight = Math.floor(18*window.devicePixelRatio); // 18 is the display height of a normal small emoji
197+
requestedHeight = Math.floor(18 * window.devicePixelRatio); // 18 is the display height of a normal small emoji
198198
}
199199
const width = Math.min(requestedWidth || 800, 800);
200200
const height = Math.min(requestedHeight || 600, 600);
@@ -311,7 +311,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
311311
div: ["data-mx-maths"],
312312
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
313313
// img tags also accept width/height, we just map those to max-width & max-height during transformation
314-
img: ["src", "alt", "title", "style", "data-mx-emoticon"],
314+
img: ["src", "alt", "title", "style", "data-mx-emoticon", "data-mx-pack-url"],
315315
ol: ["start"],
316316
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
317317
},
@@ -596,14 +596,24 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
596596
});
597597
safeBodyNeedsSerialisation = true;
598598
}
599-
if (isAllHtmlEmoji && !opts.disableBigEmoji) { // Big emoji? Big image URLs.
599+
if (isAllHtmlEmoji && !opts.disableBigEmoji) {
600+
// Big emoji? Big image URLs.
600601
(phtml.root()[0] as cheerio.TagElement).children.forEach((elm) => {
601-
if (elm.name === "img" && "data-mx-emoticon" in elm.attribs && typeof elm.attribs.src === "string") {
602-
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
602+
const tagElm = elm as cheerio.TagElement;
603+
if (
604+
tagElm.name === "img" &&
605+
"data-mx-emoticon" in tagElm.attribs &&
606+
typeof tagElm.attribs.src === "string"
607+
) {
608+
tagElm.attribs.src = tagElm.attribs.src.replace(
609+
/height=[0-9]*/,
610+
`height=${Math.floor(48 * window.devicePixelRatio)}`,
611+
); // 48 is the display height of a big emoji
603612
}
604-
})
613+
});
605614
}
606-
if (safeBodyNeedsSerialisation) { // SchildiChat: all done editing emojis, can finally serialise the body
615+
if (safeBodyNeedsSerialisation) {
616+
// SchildiChat: all done editing emojis, can finally serialise the body
607617
safeBody = phtml.html();
608618
}
609619
if (bodyHasEmoji) {
@@ -633,11 +643,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
633643

634644
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
635645
const matched = match?.[0]?.length === contentBodyTrimmed.length;
636-
emojiBody =
637-
(matched || isAllHtmlEmoji) &&
638-
(strippedBody === safeBody || // replies have the html fallbacks, account for that here
639-
content.formatted_body === undefined ||
640-
(!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:")));
646+
emojiBody = matched || isAllHtmlEmoji;
641647
}
642648

643649
const className = classNames({

src/autocomplete/Autocompleter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
2828
import SpaceProvider from "./SpaceProvider";
2929
import { TimelineRenderingType } from "../contexts/RoomContext";
3030
import { filterBoolean } from "../utils/arrays";
31+
import { ICustomEmoji } from "../emojipicker/customemoji";
3132

3233
export interface ISelectionRange {
3334
beginning?: boolean; // whether the selection is in the first block of the editor or not
@@ -46,6 +47,7 @@ export interface ICompletion {
4647
// If provided, apply a LINK entity to the completion with the
4748
// data = { url: href }.
4849
href?: string;
50+
customEmoji?: ICustomEmoji;
4951
}
5052

5153
const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, CommandProvider, SpaceProvider];

src/autocomplete/EmojiProvider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
103103
// Load this room's image sets.
104104
const imageSetEvents = room?.currentState?.getStateEvents("im.ponies.room_emotes");
105105
let loadedImages: ICustomEmoji[] =
106-
imageSetEvents?.flatMap((imageSetEvent) => loadImageSet(imageSetEvent)) || [];
106+
imageSetEvents?.flatMap((imageSetEvent) => loadImageSet(imageSetEvent, room)) || [];
107107

108108
// Global emotes from rooms
109109
const cli = MatrixClientPeg.get();
@@ -115,7 +115,7 @@ export default class EmojiProvider extends AutocompleteProvider {
115115
"im.ponies.room_emotes",
116116
packRoomStateKey,
117117
);
118-
const moreLoadedImages: ICustomEmoji[] = loadImageSet(packRoomImageSetEvents);
118+
const moreLoadedImages: ICustomEmoji[] = loadImageSet(packRoomImageSetEvents, packRoom!);
119119
loadedImages = [...loadedImages, ...(moreLoadedImages || [])];
120120
}
121121
}
@@ -244,6 +244,7 @@ export default class EmojiProvider extends AutocompleteProvider {
244244
<img className="mx_customEmoji_image" src={mediaUrl} alt={c.emoji.shortcodes[0]} />
245245
</PillCompletion>
246246
),
247+
customEmoji: c.emoji,
247248
range: range!,
248249
} as const;
249250
}

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
7676
let loadedImages: ICustomEmoji[];
7777
if (props.room) {
7878
const imageSetEvents = props.room.currentState.getStateEvents("im.ponies.room_emotes");
79-
loadedImages = imageSetEvents.flatMap((imageSetEvent) => loadImageSet(imageSetEvent));
79+
loadedImages = imageSetEvents.flatMap((imageSetEvent) => loadImageSet(imageSetEvent, props.room));
8080
} else {
8181
loadedImages = [];
8282
}

src/components/views/messages/TextualBody.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,13 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
439439
let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
440440
// links processed by linkifyjs have their own handler so don't handle those here
441441
if (target.classList.contains(linkifyOpts.className as string)) return;
442+
// handle clicking packs
443+
const packUrl = target.getAttribute("data-mx-pack-url");
444+
if (packUrl) {
445+
// it could be converted to a localHref -> therefore handle locally
446+
e.preventDefault();
447+
window.location.hash = tryTransformPermalinkToLocalHref(packUrl);
448+
}
442449
if (target.nodeName !== "A") {
443450
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
444451
target = target.closest<HTMLLinkElement>("a");

src/components/views/rooms/BasicMessageComposer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
900900
if ("unicode" in emoji) {
901901
emojiPart = partCreator.emoji(emoji.unicode);
902902
} else {
903-
emojiPart = partCreator.customEmoji(emoji.shortcodes[0], emoji.url);
903+
emojiPart = partCreator.customEmoji(emoji.shortcodes[0], emoji.url, emoji.roomId, emoji.eventId);
904904
}
905905
model.transform(() => {
906906
const addedLen = model.insert([emojiPart], position);

src/editor/autocomplete.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,14 @@ export default class AutocompleteWrapperModel {
112112
// command needs special handling for auto complete, but also renders as plain texts
113113
return [(this.partCreator as CommandPartCreator).command(text)];
114114
case "customEmoji":
115-
return [this.partCreator.customEmoji(text, completionId)];
115+
return [
116+
this.partCreator.customEmoji(
117+
text,
118+
completionId!,
119+
completion.customEmoji?.roomId,
120+
completion.customEmoji?.eventId,
121+
),
122+
];
116123
default:
117124
// used for emoji and other plain text completion replacement
118125
return this.partCreator.plainWithEmoji(text);

src/editor/deserialize.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1919
import { MsgType } from "matrix-js-sdk/src/@types/event";
2020

2121
import { checkBlockNode } from "../HtmlUtils";
22-
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
22+
import { getPrimaryPermalinkEntity, parsePermalink } from "../utils/permalinks/Permalinks";
2323
import { Part, PartCreator, Type } from "./parts";
2424
import SdkConfig from "../SdkConfig";
2525
import { textToHtmlRainbow } from "../utils/colour";
2626
import { stripPlainReply } from "../utils/Reply";
27+
import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
2728

2829
const LIST_TYPES = ["UL", "OL", "LI"];
2930

@@ -97,7 +98,13 @@ function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
9798
const isCustomEmoji = elm.hasAttribute("data-mx-emoticon");
9899
if (isCustomEmoji) {
99100
const shortcode = elm.title || elm.alt || ":SHORTCODE_MISSING:";
100-
return [pc.customEmoji(shortcode, src)];
101+
// parse the link
102+
const packUrl = elm.getAttribute("data-mx-pack-url");
103+
let permalinkParts: PermalinkParts | null = null;
104+
if (packUrl) {
105+
permalinkParts = parsePermalink(packUrl);
106+
}
107+
return [pc.customEmoji(shortcode, src, permalinkParts?.roomIdOrAlias, permalinkParts?.eventId)];
101108
}
102109
return pc.plainWithEmoji(`![${escape(alt)}](${src})`);
103110
}

src/editor/parts.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ interface ISerializedPillPart {
4141
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill | Type.CustomEmoji;
4242
text: string;
4343
resourceId?: string;
44+
roomId?: string;
45+
eventId?: string;
4446
}
4547

4648
export type SerializedPart = ISerializedPart | ISerializedPillPart;
@@ -87,7 +89,12 @@ interface IPillPart extends Omit<IBasePart, "type" | "resourceId"> {
8789
resourceId: string;
8890
}
8991

90-
export type Part = IBasePart | IPillCandidatePart | IPillPart;
92+
export interface ICustomEmojiPart extends IPillPart {
93+
roomId?: string;
94+
eventId?: string;
95+
}
96+
97+
export type Part = IBasePart | IPillCandidatePart | IPillPart | ICustomEmojiPart;
9198

9299
abstract class BasePart {
93100
protected _text: string;
@@ -418,7 +425,9 @@ export class EmojiPart extends BasePart implements IBasePart {
418425
}
419426
}
420427

421-
class CustomEmojiPart extends PillPart implements IPillPart {
428+
class CustomEmojiPart extends PillPart implements ICustomEmojiPart {
429+
public roomId?: string;
430+
public eventId?: string;
422431
protected get className(): string {
423432
return "mx_CustomEmojiPill mx_Pill";
424433
}
@@ -434,8 +443,10 @@ class CustomEmojiPart extends PillPart implements IPillPart {
434443

435444
this.setAvatarVars(node, url, this.text[0]);
436445
}
437-
public constructor(shortCode: string, url: string) {
446+
public constructor(shortCode: string, url: string, roomId?: string, eventId?: string) {
438447
super(url, shortCode);
448+
this.roomId = roomId;
449+
this.eventId = eventId;
439450
}
440451
protected acceptsInsertion(chr: string): boolean {
441452
return false;
@@ -452,6 +463,14 @@ class CustomEmojiPart extends PillPart implements IPillPart {
452463
public get canEdit(): boolean {
453464
return false;
454465
}
466+
467+
public serialize(): ISerializedPillPart {
468+
return {
469+
...super.serialize(),
470+
roomId: this.roomId,
471+
eventId: this.eventId,
472+
};
473+
}
455474
}
456475

457476
class RoomPillPart extends PillPart {
@@ -622,7 +641,7 @@ export class PartCreator {
622641
case Type.Emoji:
623642
return this.emoji(part.text);
624643
case Type.CustomEmoji:
625-
return this.customEmoji(part.text, part.resourceId);
644+
return this.customEmoji(part.text, part.resourceId!, part.roomId!, part.eventId!);
626645
case Type.AtRoomPill:
627646
return this.atRoomPill(part.text);
628647
case Type.PillCandidate:
@@ -701,8 +720,13 @@ export class PartCreator {
701720
return parts;
702721
}
703722

704-
public customEmoji(shortcode: string, url: string): CustomEmojiPart {
705-
return new CustomEmojiPart(shortcode, url);
723+
public customEmoji(
724+
shortcode: string,
725+
url: string,
726+
roomId?: string | null,
727+
eventId?: string | null,
728+
): CustomEmojiPart {
729+
return new CustomEmojiPart(shortcode, url, roomId!, eventId!);
706730
}
707731

708732
public createMentionParts(

src/editor/serialize.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ import escapeHtml from "escape-html";
2121
import _ from "lodash";
2222

2323
import Markdown from "../Markdown";
24-
import { makeGenericPermalink } from "../utils/permalinks/Permalinks";
24+
import { makeGenericPermalink, makeRoomPermalink } from "../utils/permalinks/Permalinks";
2525
import EditorModel from "./model";
2626
import SettingsStore from "../settings/SettingsStore";
2727
import SdkConfig from "../SdkConfig";
28-
import { Type } from "./parts";
28+
import { ICustomEmojiPart, Type } from "./parts";
2929

3030
export function mdSerialize(model: EditorModel): string {
3131
return model.parts.reduce((html, part) => {
@@ -53,6 +53,18 @@ export function mdSerialize(model: EditorModel): string {
5353
`[${part.text.replace(/[[\\\]]/g, (c) => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`
5454
);
5555
case Type.CustomEmoji:
56+
if ((part as ICustomEmojiPart).roomId) {
57+
const permalink = makeRoomPermalink(
58+
(part as ICustomEmojiPart).roomId!,
59+
(part as ICustomEmojiPart).eventId,
60+
);
61+
return (
62+
html +
63+
`<img data-mx-emoticon height="18" src="${encodeURI(part.resourceId)}"` +
64+
` data-mx-pack-url="${permalink}"` +
65+
` title=":${_.escape(part.text)}:" alt=":${_.escape(part.text)}:">`
66+
);
67+
}
5668
return (
5769
html +
5870
`<img data-mx-emoticon height="32" src="${encodeURI(part.resourceId)}"` +

0 commit comments

Comments
 (0)