diff --git a/src/app/organisms/emoji-board/custom-emoji.js b/src/app/organisms/emoji-board/custom-emoji.js
index b847bd4..650a962 100644
--- a/src/app/organisms/emoji-board/custom-emoji.js
+++ b/src/app/organisms/emoji-board/custom-emoji.js
@@ -10,26 +10,114 @@ import { emojis } from './emoji';
// globally, while emojis and packs in rooms and spaces should only be available within
// those spaces and rooms
-// Retrieve a list of user emojis
-//
-// Result is a list of objects, each with a shortcode and an mxc property
-//
-// Accepts a reference to a matrix client as the only argument
-function getUserEmoji(mx) {
- const accountDataEmoji = mx.getAccountData('im.ponies.user_emotes');
- if (!accountDataEmoji) {
- return [];
+class ImagePack {
+ // Convert a raw image pack into a more maliable format
+ //
+ // Takes an image pack as per MSC 2545 (e.g. as in the Matrix spec), and converts it to a
+ // format used here, while filling in defaults.
+ //
+ // The room argument is the room the pack exists in, which is used as a fallback for
+ // missing properties
+ //
+ // Returns `null` if the rawPack is not a properly formatted image pack, although there
+ // is still a fair amount of tolerance for malformed packs.
+ static parsePack(rawPack, room) {
+ if (typeof rawPack.images === 'undefined') {
+ return null;
+ }
+
+ const pack = rawPack.pack ?? {};
+
+ const displayName = pack.display_name ?? (room ? room.name : undefined);
+ const avatar = pack.avatar_url ?? (room ? room.getMxcAvatarUrl() : undefined);
+ const usage = pack.usage ?? ['emoticon', 'sticker'];
+ const { attribution } = pack;
+ const images = Object.entries(rawPack.images).flatMap((e) => {
+ const data = e[1];
+ const shortcode = e[0];
+ const mxc = data.url;
+ const body = data.body ?? shortcode;
+ const { info } = data;
+ const usage_ = data.usage ?? usage;
+
+ if (mxc) {
+ return [{
+ shortcode, mxc, body, info, usage: usage_,
+ }];
+ }
+ return [];
+ });
+
+ return new ImagePack(displayName, avatar, usage, attribution, images);
}
- const { images } = accountDataEmoji.event.content;
- const mapped = Object.entries(images).map((e) => ({
- shortcode: e[0],
- mxc: e[1].url,
- }));
- return mapped;
+ constructor(displayName, avatar, usage, attribution, images) {
+ this.displayName = displayName;
+ this.avatar = avatar;
+ this.usage = usage;
+ this.attribution = attribution;
+ this.images = images;
+ }
+
+ // Produce a list of emoji in this image pack
+ getEmojis() {
+ return this.images.filter((i) => i.usage.indexOf('emoticon') !== -1);
+ }
+
+ // Produce a list of stickers in this image pack
+ getStickers() {
+ return this.images.filter((i) => i.usage.indexOf('sticker') !== -1);
+ }
}
-// Returns all user emojis and all standard unicode emojis
+// Retrieve a list of user emojis
+//
+// Result is an ImagePack, or null if the user hasn't set up or has deleted their personal
+// image pack.
+//
+// Accepts a reference to a matrix client as the only argument
+function getUserImagePack(mx) {
+ const accountDataEmoji = mx.getAccountData('im.ponies.user_emotes');
+ if (!accountDataEmoji) {
+ return null;
+ }
+
+ const userImagePack = ImagePack.parsePack(accountDataEmoji.event.content);
+ if (userImagePack) userImagePack.displayName ??= 'Your Emoji';
+ return userImagePack;
+}
+
+// Produces a list of all of the emoji packs in a room
+//
+// Returns a list of `ImagePack`s. This does not include packs in spaces that contain
+// this room.
+function getPacksInRoom(room) {
+ const packs = room.currentState.getStateEvents('im.ponies.room_emotes');
+
+ return packs
+ .map((p) => ImagePack.parsePack(p.event.content, room))
+ .filter((p) => p !== null);
+}
+
+// Produce a list of all image packs which should be shown for a given room
+//
+// This includes packs in that room, the user's personal images, and will eventually
+// include the user's enabled global image packs and space-level packs.
+//
+// This differs from getPacksInRoom, as the former only returns packs that are directly in
+// a room, whereas this function returns all packs which should be shown to the user while
+// they are in this room.
+//
+// Packs will be returned in the order that shortcode conflicts should be resolved, with
+// higher priority packs coming first.
+function getRelevantPacks(room) {
+ return [].concat(
+ getUserImagePack(room.client) ?? [],
+ getPacksInRoom(room),
+ );
+}
+
+// Returns all user+room emojis and all standard unicode emojis
//
// Accepts a reference to a matrix client as the only argument
//
@@ -37,7 +125,7 @@ function getUserEmoji(mx) {
// shortcode, only one will be presented, with priority given to custom emoji.
//
// Will eventually be expanded to include all emojis revelant to a room and the user
-function getShortcodeToEmoji(mx) {
+function getShortcodeToEmoji(room) {
const allEmoji = new Map();
emojis.forEach((emoji) => {
@@ -50,9 +138,11 @@ function getShortcodeToEmoji(mx) {
}
});
- getUserEmoji(mx).forEach((emoji) => {
- allEmoji.set(emoji.shortcode, emoji);
- });
+ getRelevantPacks(room).reverse()
+ .flatMap((pack) => pack.getEmojis())
+ .forEach((emoji) => {
+ allEmoji.set(emoji.shortcode, emoji);
+ });
return allEmoji;
}
@@ -64,14 +154,16 @@ function getShortcodeToEmoji(mx) {
// shortcodes for the standard emoji will not be considered.
//
// Standard emoji are guaranteed to be earlier in the list than custom emoji
-function getEmojiForCompletion(mx) {
+function getEmojiForCompletion(room) {
const allEmoji = new Map();
- getUserEmoji(mx).forEach((emoji) => {
- allEmoji.set(emoji.shortcode, emoji);
- });
+ getRelevantPacks(room).reverse()
+ .flatMap((pack) => pack.getEmojis())
+ .forEach((emoji) => {
+ allEmoji.set(emoji.shortcode, emoji);
+ });
return emojis.filter((e) => !allEmoji.has(e.shortcode))
.concat(Array.from(allEmoji.values()));
}
-export { getUserEmoji, getShortcodeToEmoji, getEmojiForCompletion };
+export { getUserImagePack, getShortcodeToEmoji, getEmojiForCompletion };
diff --git a/src/app/organisms/room/RoomViewCmdBar.jsx b/src/app/organisms/room/RoomViewCmdBar.jsx
index cc4a6bb..8a9feb5 100644
--- a/src/app/organisms/room/RoomViewCmdBar.jsx
+++ b/src/app/organisms/room/RoomViewCmdBar.jsx
@@ -210,7 +210,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
setCmd({ prefix, suggestions: commands });
},
':': () => {
- const emojis = getEmojiForCompletion(mx);
+ const emojis = getEmojiForCompletion(mx.getRoom(roomId));
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
setCmd({ prefix, suggestions: emojis.slice(26, 46) });
},
diff --git a/src/client/state/RoomsInput.js b/src/client/state/RoomsInput.js
index 6a65c1b..1f0f8a4 100644
--- a/src/client/state/RoomsInput.js
+++ b/src/client/state/RoomsInput.js
@@ -113,6 +113,54 @@ function bindReplyToContent(roomId, reply, content) {
return newContent;
}
+// Apply formatting to a plain text message
+//
+// This includes inserting any custom emoji that might be relevant, and (only if the
+// user has enabled it in their settings) formatting the message using markdown.
+function formatAndEmojifyText(room, text) {
+ const allEmoji = getShortcodeToEmoji(room);
+
+ // Start by applying markdown formatting (if relevant)
+ let formattedText;
+ if (settings.isMarkdown) {
+ formattedText = getFormattedBody(text);
+ } else {
+ formattedText = text;
+ }
+
+ // Check to see if there are any :shortcode-style-tags: in the message
+ Array.from(formattedText.matchAll(/\B:([\w-]+):\B/g))
+ // Then filter to only the ones corresponding to a valid emoji
+ .filter((match) => allEmoji.has(match[1]))
+ // Reversing the array ensures that indices are preserved as we start replacing
+ .reverse()
+ // Replace each :shortcode: with an tag
+ .forEach((shortcodeMatch) => {
+ const emoji = allEmoji.get(shortcodeMatch[1]);
+
+ // Render the tag that will replace the shortcode
+ let tag;
+ if (emoji.mxc) {
+ tag = ``;
+ } else {
+ tag = emoji.unicode;
+ }
+
+ // Splice the tag into the text
+ formattedText = formattedText.substr(0, shortcodeMatch.index)
+ + tag
+ + formattedText.substr(shortcodeMatch.index + shortcodeMatch[0].length);
+ });
+
+ return formattedText;
+}
+
class RoomsInput extends EventEmitter {
constructor(mx) {
super();
@@ -201,54 +249,6 @@ class RoomsInput extends EventEmitter {
return this.roomIdToInput.get(roomId)?.isSending || false;
}
- // Apply formatting to a plain text message
- //
- // This includes inserting any custom emoji that might be relevant, and (only if the
- // user has enabled it in their settings) formatting the message using markdown.
- formatAndEmojifyText(text) {
- const allEmoji = getShortcodeToEmoji(this.matrixClient);
-
- // Start by applying markdown formatting (if relevant)
- let formattedText;
- if (settings.isMarkdown) {
- formattedText = getFormattedBody(text);
- } else {
- formattedText = text;
- }
-
- // Check to see if there are any :shortcode-style-tags: in the message
- Array.from(formattedText.matchAll(/\B:([\w-]+):\B/g))
- // Then filter to only the ones corresponding to a valid emoji
- .filter((match) => allEmoji.has(match[1]))
- // Reversing the array ensures that indices are preserved as we start replacing
- .reverse()
- // Replace each :shortcode: with an tag
- .forEach((shortcodeMatch) => {
- const emoji = allEmoji.get(shortcodeMatch[1]);
-
- // Render the tag that will replace the shortcode
- let tag;
- if (emoji.mxc) {
- tag = ``;
- } else {
- tag = emoji.unicode;
- }
-
- // Splice the tag into the text
- formattedText = formattedText.substr(0, shortcodeMatch.index)
- + tag
- + formattedText.substr(shortcodeMatch.index + shortcodeMatch[0].length);
- });
-
- return formattedText;
- }
-
async sendInput(roomId) {
const input = this.getInput(roomId);
input.isSending = true;
@@ -265,7 +265,10 @@ class RoomsInput extends EventEmitter {
};
// Apply formatting if relevant
- const formattedBody = this.formatAndEmojifyText(input.message);
+ const formattedBody = formatAndEmojifyText(
+ this.matrixClient.getRoom(roomId),
+ input.message,
+ );
if (formattedBody !== input.message) {
// Formatting was applied, and we need to switch to custom HTML
content.format = 'org.matrix.custom.html';
@@ -401,7 +404,10 @@ class RoomsInput extends EventEmitter {
};
// Apply formatting if relevant
- const formattedBody = this.formatAndEmojifyText(editedBody);
+ const formattedBody = formatAndEmojifyText(
+ this.matrixClient.getRoom(roomId),
+ editedBody
+ );
if (formattedBody !== editedBody) {
content.formatted_body = ` * ${formattedBody}`;
content.format = 'org.matrix.custom.html';