Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arc/msc3971 #15

Open
wants to merge 1 commit into
base: sc
Choose a base branch
from
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
30 changes: 18 additions & 12 deletions src/HtmlUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down 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 @@ -596,14 +596,24 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, 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) {
Expand Down Expand Up @@ -633,11 +643,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
36 changes: 30 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,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(
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