Skip to content

Commit

Permalink
sending custom emoji with the new attribute
Browse files Browse the repository at this point in the history
make them clickable

fix typescript error checks
  • Loading branch information
AndrewRyanChama committed May 19, 2023
1 parent 6c4e34f commit 50b10a6
Show file tree
Hide file tree
Showing 12 changed files with 98 additions and 28 deletions.
10 changes: 3 additions & 7 deletions src/HtmlUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -597,7 +597,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
safeBodyNeedsSerialisation = true;
}
if (isAllHtmlEmoji && !opts.disableBigEmoji) { // Big emoji? Big image URLs.
(phtml.root()[0] as cheerio.TagElement).children.forEach((elm) => {
(phtml.root()[0] as cheerio.TagElement).children.forEach((elm : cheerio.TagElement) => {
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
}
Expand Down Expand Up @@ -633,11 +633,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, 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({
Expand Down
2 changes: 2 additions & 0 deletions src/autocomplete/Autocompleter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
Expand Down
5 changes: 3 additions & 2 deletions src/autocomplete/EmojiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 || [])];
}
}
Expand Down Expand Up @@ -244,6 +244,7 @@ export default class EmojiProvider extends AutocompleteProvider {
<img className="mx_customEmoji_image" src={mediaUrl} alt={c.emoji.shortcodes[0]} />
</PillCompletion>
),
customEmoji: c.emoji,
range: range!,
} as const;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/emojipicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
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 = [];
}
Expand Down
7 changes: 7 additions & 0 deletions src/components/views/messages/TextualBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,13 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
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 `<a>` may contain children, e.g. an anchor wrapping an inline code section
target = target.closest<HTMLLinkElement>("a");
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
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);
Expand Down
9 changes: 8 additions & 1 deletion src/editor/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/editor/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand Down Expand Up @@ -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(`![${escape(alt)}](${src})`);
}
Expand Down
31 changes: 25 additions & 6 deletions src/editor/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,7 +89,12 @@ interface IPillPart extends Omit<IBasePart, "type" | "resourceId"> {
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;
Expand Down Expand Up @@ -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";
}
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -701,8 +720,8 @@ 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(
Expand Down
16 changes: 14 additions & 2 deletions src/editor/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 +
`<img data-mx-emoticon height="18" src="${encodeURI(part.resourceId)}"` +
` data-mx-pack-url="${permalink}"` +
` title=":${_.escape(part.text)}:" alt=":${_.escape(part.text)}:">`
);
}
return (
html +
`<img data-mx-emoticon height="32" src="${encodeURI(part.resourceId)}"` +
Expand Down
14 changes: 12 additions & 2 deletions src/emojipicker/customemoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";

export function loadImageSet(imageSetEvent: MatrixEvent): ICustomEmoji[] {
export function loadImageSet(imageSetEvent: MatrixEvent, room?: Room): ICustomEmoji[] {
const loadedImages: ICustomEmoji[] = [];
const images = imageSetEvent?.getContent().images;
let eventId: string | undefined;
let roomId: string | undefined;
if (!images) {
return [];
}
if (room?.getJoinRule() === JoinRule.Public) {
eventId = imageSetEvent?.getId();
roomId = room?.roomId;
}
for (const imageKey in images) {
const imageData = images[imageKey];
loadedImages.push({
shortcodes: [imageKey],
url: imageData.url,
roomId: roomId,
eventId: eventId,
});
}
return loadedImages;
Expand All @@ -36,4 +44,6 @@ export interface ICustomEmoji {
shortcodes: string[];
emoticon?: string;
url: string;
roomId?: string;
eventId?: string;
}
17 changes: 13 additions & 4 deletions src/utils/permalinks/Permalinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,23 +284,32 @@ export function makeUserPermalink(userId: string): string {
return getPermalinkConstructor().forUser(userId);
}

export function makeRoomPermalink(roomId: string): string {
export function makeRoomPermalink(roomId: string, eventId?: string): string {
if (!roomId) {
throw new Error("can't permalink a falsy roomId");
}

// If the roomId isn't actually a room ID, don't try to list the servers.
// Aliases are already routable, and don't need extra information.
if (roomId[0] !== "!") return getPermalinkConstructor().forRoom(roomId, []);
if (roomId[0] !== "!")
return eventId
? getPermalinkConstructor().forEvent(roomId, eventId!, [])
: getPermalinkConstructor().forRoom(roomId, []);

const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
if (!room) {
return getPermalinkConstructor().forRoom(roomId, []);
return eventId
? getPermalinkConstructor().forEvent(roomId, eventId!, [])
: getPermalinkConstructor().forRoom(roomId, []);
}
const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load();
return permalinkCreator.forShareableRoom();
if (eventId) {
return permalinkCreator.forEvent(eventId);
} else {
return permalinkCreator.forShareableRoom();
}
}

export function isPermalinkHost(host: string): boolean {
Expand Down

0 comments on commit 50b10a6

Please sign in to comment.