Custom emoji & Sticker support (#686)
* Remove comments * Show custom emoji first in suggestions * Show global image packs in emoji picker * Display emoji and sticker in room settings * Fix some pack not visible in emojiboard * WIP * Add/delete/rename images to exisitng packs * Change pack avatar, name & attribution * Add checkbox to make pack global * Bug fix * Create or delete pack * Add personal emoji in settings * Show global pack selector in settings * Show space emoji in emojiboard * Send custom emoji reaction as mxc * Render stickers as stickers * Fix sticker jump bug * Fix reaction width * Fix stretched custom emoji * Fix sending space emoji in message * Remove unnessesary comments * Send user pills * Fix pill generating regex * Add support for sending stickers
This commit is contained in:
@@ -70,7 +70,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||
unicode={`:${emoji.shortcode}:`}
|
||||
shortcodes={emoji.shortcode}
|
||||
src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon
|
||||
data-mx-emoticon={emoji.mxc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -141,10 +141,13 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
function getEmojiDataFromTarget(target) {
|
||||
const unicode = target.getAttribute('unicode');
|
||||
const hexcode = target.getAttribute('hexcode');
|
||||
const mxc = target.getAttribute('data-mx-emoticon');
|
||||
let shortcodes = target.getAttribute('shortcodes');
|
||||
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
||||
else shortcodes = shortcodes.split(',');
|
||||
return { unicode, hexcode, shortcodes };
|
||||
return {
|
||||
unicode, hexcode, shortcodes, mxc,
|
||||
};
|
||||
}
|
||||
|
||||
function selectEmoji(e) {
|
||||
@@ -202,21 +205,23 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
setAvailableEmojis([]);
|
||||
return;
|
||||
}
|
||||
// Retrieve the packs for the new room
|
||||
// Remove packs that aren't marked as emoji packs
|
||||
// Remove packs without emojis
|
||||
const packs = getRelevantPacks(
|
||||
initMatrix.matrixClient.getRoom(selectedRoomId),
|
||||
)
|
||||
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
|
||||
.filter((pack) => pack.getEmojis().length !== 0);
|
||||
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(selectedRoomId);
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
if (room) {
|
||||
const packs = getRelevantPacks(
|
||||
room.client,
|
||||
[room, ...parentRooms],
|
||||
).filter((pack) => pack.getEmojis().length !== 0);
|
||||
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
}
|
||||
setAvailableEmojis(packs);
|
||||
}
|
||||
|
||||
setAvailableEmojis(packs);
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
@@ -260,7 +265,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
{
|
||||
availableEmojis.map((pack) => (
|
||||
<EmojiGroup
|
||||
name={pack.displayName}
|
||||
name={pack.displayName ?? 'Unknown'}
|
||||
key={pack.packIndex}
|
||||
groupEmojis={pack.getEmojis()}
|
||||
className="custom-emoji-group"
|
||||
@@ -293,13 +298,14 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="right"
|
||||
isImage
|
||||
/>
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
& > p:last-child {
|
||||
@@ -123,6 +124,7 @@
|
||||
& .emoji {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
padding: var(--emoji-padding);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
|
||||
@@ -1,135 +1,224 @@
|
||||
import { emojis } from './emoji';
|
||||
|
||||
// Custom emoji are stored in one of three places:
|
||||
// - User emojis, which are stored in account data
|
||||
// - Room emojis, which are stored in state events in a room
|
||||
// - Emoji packs, which are rooms of emojis referenced in the account data or in a room's
|
||||
// cannonical space
|
||||
//
|
||||
// Emojis and packs referenced from within a user's account data should be available
|
||||
// globally, while emojis and packs in rooms and spaces should only be available within
|
||||
// those spaces and rooms
|
||||
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
|
||||
|
||||
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') {
|
||||
static parsePack(eventId, packContent) {
|
||||
if (!eventId || typeof packContent?.images !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pack = rawPack.pack ?? {};
|
||||
return new ImagePack(eventId, packContent);
|
||||
}
|
||||
|
||||
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];
|
||||
constructor(eventId, content) {
|
||||
this.id = eventId;
|
||||
this.content = JSON.parse(JSON.stringify(content));
|
||||
|
||||
this.applyPack(content);
|
||||
this.applyImages(content);
|
||||
}
|
||||
|
||||
applyPack(content) {
|
||||
const pack = content.pack ?? {};
|
||||
|
||||
this.displayName = pack.display_name;
|
||||
this.avatarUrl = pack.avatar_url;
|
||||
this.usage = pack.usage ?? ['emoticon', 'sticker'];
|
||||
this.attribution = pack.attribution;
|
||||
}
|
||||
|
||||
applyImages(content) {
|
||||
this.images = new Map();
|
||||
this.emoticons = [];
|
||||
this.stickers = [];
|
||||
|
||||
Object.entries(content.images).forEach(([shortcode, data]) => {
|
||||
const mxc = data.url;
|
||||
const body = data.body ?? shortcode;
|
||||
const usage = data.usage ?? this.usage;
|
||||
const { info } = data;
|
||||
const usage_ = data.usage ?? usage;
|
||||
|
||||
if (mxc) {
|
||||
return [{
|
||||
shortcode, mxc, body, info, usage: usage_,
|
||||
}];
|
||||
if (!mxc) return;
|
||||
const image = {
|
||||
shortcode, mxc, body, usage, info,
|
||||
};
|
||||
|
||||
this.images.set(shortcode, image);
|
||||
if (usage.includes('emoticon')) {
|
||||
this.emoticons.push(image);
|
||||
}
|
||||
if (usage.includes('sticker')) {
|
||||
this.stickers.push(image);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return new ImagePack(displayName, avatar, usage, attribution, images);
|
||||
}
|
||||
|
||||
constructor(displayName, avatar, usage, attribution, images) {
|
||||
this.displayName = displayName;
|
||||
this.avatar = avatar;
|
||||
this.usage = usage;
|
||||
this.attribution = attribution;
|
||||
this.images = images;
|
||||
getImages() {
|
||||
return this.images;
|
||||
}
|
||||
|
||||
// Produce a list of emoji in this image pack
|
||||
getEmojis() {
|
||||
return this.images.filter((i) => i.usage.indexOf('emoticon') !== -1);
|
||||
return this.emoticons;
|
||||
}
|
||||
|
||||
// Produce a list of stickers in this image pack
|
||||
getStickers() {
|
||||
return this.images.filter((i) => i.usage.indexOf('sticker') !== -1);
|
||||
return this.stickers;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
_updatePackProperty(property, value) {
|
||||
if (this.content.pack === undefined) {
|
||||
this.content.pack = {};
|
||||
}
|
||||
this.content.pack[property] = value;
|
||||
this.applyPack(this.content);
|
||||
}
|
||||
|
||||
setAvatarUrl(avatarUrl) {
|
||||
this._updatePackProperty('avatar_url', avatarUrl);
|
||||
}
|
||||
|
||||
setDisplayName(displayName) {
|
||||
this._updatePackProperty('display_name', displayName);
|
||||
}
|
||||
|
||||
setAttribution(attribution) {
|
||||
this._updatePackProperty('attribution', attribution);
|
||||
}
|
||||
|
||||
setUsage(usage) {
|
||||
this._updatePackProperty('usage', usage);
|
||||
}
|
||||
|
||||
addImage(key, imgContent) {
|
||||
this.content.images = {
|
||||
[key]: imgContent,
|
||||
...this.content.images,
|
||||
};
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
removeImage(key) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
delete this.content.images[key];
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
updateImageKey(key, newKey) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
const copyImages = {};
|
||||
Object.keys(this.content.images).forEach((imgKey) => {
|
||||
copyImages[imgKey === key ? newKey : imgKey] = this.content.images[imgKey];
|
||||
});
|
||||
this.content.images = copyImages;
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
_updateImageProperty(key, property, value) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
this.content.images[key][property] = value;
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
setImageUrl(key, url) {
|
||||
this._updateImageProperty(key, 'url', url);
|
||||
}
|
||||
|
||||
setImageBody(key, body) {
|
||||
this._updateImageProperty(key, 'body', body);
|
||||
}
|
||||
|
||||
setImageInfo(key, info) {
|
||||
this._updateImageProperty(key, 'info', info);
|
||||
}
|
||||
|
||||
setImageUsage(key, usage) {
|
||||
this._updateImageProperty(key, 'usage', usage);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 getGlobalImagePacks(mx) {
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return [];
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return [];
|
||||
|
||||
const roomIds = Object.keys(rooms);
|
||||
|
||||
const packs = roomIds.flatMap((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return [];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return [];
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
|
||||
return stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
|
||||
if (pack) {
|
||||
pack.displayName ??= room.name;
|
||||
pack.avatarUrl ??= room.getMxcAvatarUrl();
|
||||
}
|
||||
return pack;
|
||||
}).filter((pack) => pack !== null);
|
||||
});
|
||||
|
||||
return packs;
|
||||
}
|
||||
|
||||
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';
|
||||
const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
|
||||
userImagePack.displayName ??= 'Personal 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');
|
||||
function getRoomImagePacks(room) {
|
||||
const dataEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
|
||||
return packs
|
||||
.map((p) => ImagePack.parsePack(p.event.content, room))
|
||||
.filter((p) => p !== null);
|
||||
return dataEvents
|
||||
.map((data) => {
|
||||
const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
|
||||
if (pack) {
|
||||
pack.displayName ??= room.name;
|
||||
pack.avatarUrl ??= room.getMxcAvatarUrl();
|
||||
}
|
||||
return pack;
|
||||
})
|
||||
.filter((pack) => pack !== 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) {
|
||||
/**
|
||||
* @param {MatrixClient} mx Provide if you want to include user personal/global pack
|
||||
* @param {Room[]} rooms Provide rooms if you want to include rooms pack
|
||||
* @returns {ImagePack[]} packs
|
||||
*/
|
||||
function getRelevantPacks(mx, rooms) {
|
||||
const userPack = mx ? getUserImagePack(mx) : [];
|
||||
const globalPacks = mx ? getGlobalImagePacks(mx) : [];
|
||||
const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
|
||||
const roomsPack = rooms?.flatMap(getRoomImagePacks) ?? [];
|
||||
|
||||
return [].concat(
|
||||
getUserImagePack(room.client) ?? [],
|
||||
getPacksInRoom(room),
|
||||
userPack ?? [],
|
||||
globalPacks,
|
||||
roomsPack.filter((pack) => !globalPackIds.has(pack.id)),
|
||||
);
|
||||
}
|
||||
|
||||
// Returns all user+room emojis and all standard unicode emojis
|
||||
//
|
||||
// Accepts a reference to a matrix client as the only argument
|
||||
//
|
||||
// Result is a map from shortcode to the corresponding emoji. If two emoji share a
|
||||
// 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(room) {
|
||||
function getShortcodeToEmoji(mx, rooms) {
|
||||
const allEmoji = new Map();
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
if (emoji.shortcodes.constructor.name === 'Array') {
|
||||
if (Array.isArray(emoji.shortcodes)) {
|
||||
emoji.shortcodes.forEach((shortcode) => {
|
||||
allEmoji.set(shortcode, emoji);
|
||||
});
|
||||
@@ -138,7 +227,7 @@ function getShortcodeToEmoji(room) {
|
||||
}
|
||||
});
|
||||
|
||||
getRelevantPacks(room).reverse()
|
||||
getRelevantPacks(mx, rooms)
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
@@ -150,7 +239,7 @@ function getShortcodeToEmoji(room) {
|
||||
function getShortcodeToCustomEmoji(room) {
|
||||
const allEmoji = new Map();
|
||||
|
||||
getRelevantPacks(room).reverse()
|
||||
getRelevantPacks(room.client, [room])
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
@@ -159,27 +248,20 @@ function getShortcodeToCustomEmoji(room) {
|
||||
return allEmoji;
|
||||
}
|
||||
|
||||
// Produces a special list of emoji specifically for auto-completion
|
||||
//
|
||||
// This list contains each emoji once, with all emoji being deduplicated by shortcode.
|
||||
// However, the order of the standard emoji will have been preserved, and alternate
|
||||
// shortcodes for the standard emoji will not be considered.
|
||||
//
|
||||
// Standard emoji are guaranteed to be earlier in the list than custom emoji
|
||||
function getEmojiForCompletion(room) {
|
||||
function getEmojiForCompletion(mx, rooms) {
|
||||
const allEmoji = new Map();
|
||||
getRelevantPacks(room).reverse()
|
||||
getRelevantPacks(mx, rooms)
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
});
|
||||
|
||||
return emojis.filter((e) => !allEmoji.has(e.shortcode))
|
||||
.concat(Array.from(allEmoji.values()));
|
||||
return Array.from(allEmoji.values()).concat(emojis.filter((e) => !allEmoji.has(e.shortcode)));
|
||||
}
|
||||
|
||||
export {
|
||||
getUserImagePack,
|
||||
ImagePack,
|
||||
getUserImagePack, getGlobalImagePacks, getRoomImagePacks,
|
||||
getShortcodeToEmoji, getShortcodeToCustomEmoji,
|
||||
getRelevantPacks, getEmojiForCompletion,
|
||||
};
|
||||
|
||||
@@ -25,9 +25,11 @@ import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomH
|
||||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
@@ -42,6 +44,7 @@ const tabText = {
|
||||
GENERAL: 'General',
|
||||
SEARCH: 'Search',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
SECURITY: 'Security',
|
||||
};
|
||||
@@ -58,6 +61,10 @@ const tabItems = [{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
@@ -197,6 +204,7 @@ function RoomSettings({ roomId }) {
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||
</div>
|
||||
@@ -210,7 +218,5 @@ RoomSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export {
|
||||
RoomSettings as default,
|
||||
tabText,
|
||||
};
|
||||
export default RoomSettings;
|
||||
export { tabText };
|
||||
|
||||
@@ -21,7 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
|
||||
import { addRecentEmoji } from '../emoji-board/recent';
|
||||
import { addRecentEmoji, getRecentEmojis } from '../emoji-board/recent';
|
||||
|
||||
const commands = [{
|
||||
name: 'markdown',
|
||||
@@ -213,9 +213,15 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
setCmd({ prefix, suggestions: commands });
|
||||
},
|
||||
':': () => {
|
||||
const emojis = getEmojiForCompletion(mx.getRoom(roomId));
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const emojis = getEmojiForCompletion(mx, [mx.getRoom(roomId), ...parentRooms]);
|
||||
const recentEmoji = getRecentEmojis(20);
|
||||
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
|
||||
setCmd({ prefix, suggestions: emojis.slice(26, 46) });
|
||||
setCmd({
|
||||
prefix,
|
||||
suggestions: recentEmoji.length > 0 ? recentEmoji : emojis.slice(26, 46),
|
||||
});
|
||||
},
|
||||
'@': () => {
|
||||
const members = mx.getRoom(roomId).getJoinedMembers().map((member) => ({
|
||||
@@ -247,7 +253,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
}
|
||||
if (myCmd.prefix === '@') {
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: myCmd.result.name,
|
||||
replace: `@${myCmd.result.userId}`,
|
||||
});
|
||||
}
|
||||
deactivateCmd();
|
||||
|
||||
@@ -8,7 +8,7 @@ import TextareaAutosize from 'react-autosize-textarea';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import { openEmojiBoard } from '../../../client/action/navigation';
|
||||
import { openEmojiBoard, openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { bytesToSize, getEventCords } from '../../../util/common';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
@@ -20,9 +20,12 @@ import IconButton from '../../atoms/button/IconButton';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import { MessageReply } from '../../molecules/message/Message';
|
||||
|
||||
import StickerBoard from '../sticker-board/StickerBoard';
|
||||
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SendIC from '../../../../public/res/ic/outlined/send.svg';
|
||||
import StickerIC from '../../../../public/res/ic/outlined/sticker.svg';
|
||||
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
|
||||
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
|
||||
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
|
||||
@@ -128,7 +131,11 @@ function RoomViewInput({
|
||||
}
|
||||
function firedCmd(cmdData) {
|
||||
const msg = textAreaRef.current.value;
|
||||
textAreaRef.current.value = replaceCmdWith(msg, cmdCursorPos, typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '');
|
||||
textAreaRef.current.value = replaceCmdWith(
|
||||
msg,
|
||||
cmdCursorPos,
|
||||
typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
|
||||
);
|
||||
deactivateCmd();
|
||||
}
|
||||
|
||||
@@ -199,6 +206,33 @@ function RoomViewInput({
|
||||
if (replyTo !== null) setReplyTo(null);
|
||||
};
|
||||
|
||||
const handleSendSticker = async (data) => {
|
||||
const { mxc: url, body, httpUrl } = data;
|
||||
const info = {};
|
||||
|
||||
const img = new Image();
|
||||
img.src = httpUrl;
|
||||
|
||||
try {
|
||||
const res = await fetch(httpUrl);
|
||||
const blob = await res.blob();
|
||||
info.w = img.width;
|
||||
info.h = img.height;
|
||||
info.mimetype = blob.type;
|
||||
info.size = blob.size;
|
||||
info.thumbnail_info = { ...info };
|
||||
info.thumbnail_url = url;
|
||||
} catch {
|
||||
// send sticker without info
|
||||
}
|
||||
|
||||
mx.sendEvent(roomId, 'm.sticker', {
|
||||
body,
|
||||
url,
|
||||
info,
|
||||
});
|
||||
};
|
||||
|
||||
function processTyping(msg) {
|
||||
const isEmptyMsg = msg === '';
|
||||
|
||||
@@ -338,6 +372,29 @@ function RoomViewInput({
|
||||
{isMarkdown && <RawIcon size="extra-small" src={MarkdownIC} />}
|
||||
</div>
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
openReusableContextMenu(
|
||||
'top',
|
||||
(() => {
|
||||
const cords = getEventCords(e);
|
||||
cords.y -= 20;
|
||||
return cords;
|
||||
})(),
|
||||
(closeMenu) => (
|
||||
<StickerBoard
|
||||
roomId={roomId}
|
||||
onSelect={(data) => {
|
||||
handleSendSticker(data);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
src={StickerIC}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
|
||||
@@ -24,6 +24,7 @@ import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
|
||||
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
|
||||
import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack';
|
||||
|
||||
import ProfileEditor from '../profile-editor/ProfileEditor';
|
||||
import CrossSigning from './CrossSigning';
|
||||
@@ -31,6 +32,7 @@ import KeyBackup from './KeyBackup';
|
||||
import DeviceManage from './DeviceManage';
|
||||
|
||||
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
|
||||
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
||||
@@ -169,6 +171,15 @@ function NotificationsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiSection() {
|
||||
return (
|
||||
<>
|
||||
<div className="settings-emoji__card"><ImagePackUser /></div>
|
||||
<div className="settings-emoji__card"><ImagePackGlobal /></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SecuritySection() {
|
||||
return (
|
||||
<div className="settings-security">
|
||||
@@ -250,6 +261,7 @@ function AboutSection() {
|
||||
export const tabText = {
|
||||
APPEARANCE: 'Appearance',
|
||||
NOTIFICATIONS: 'Notifications',
|
||||
EMOJI: 'Emoji',
|
||||
SECURITY: 'Security',
|
||||
ABOUT: 'About',
|
||||
};
|
||||
@@ -263,6 +275,11 @@ const tabItems = [{
|
||||
iconSrc: BellIC,
|
||||
disabled: false,
|
||||
render: () => <NotificationsSection />,
|
||||
}, {
|
||||
text: tabText.EMOJI,
|
||||
iconSrc: EmojiIC,
|
||||
disabled: false,
|
||||
render: () => <EmojiSection />,
|
||||
}, {
|
||||
text: tabText.SECURITY,
|
||||
iconSrc: LockIC,
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
.settings-notifications,
|
||||
.settings-security__card,
|
||||
.settings-security .device-manage,
|
||||
.settings-about__card {
|
||||
.settings-about__card,
|
||||
.settings-emoji__card {
|
||||
@extend .settings-window__card;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
@@ -35,6 +36,7 @@ import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
||||
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
|
||||
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
@@ -42,6 +44,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
};
|
||||
|
||||
@@ -53,6 +56,10 @@ const tabItems = [{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
@@ -178,6 +185,7 @@ function SpaceSettings() {
|
||||
<div className="space-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
88
src/app/organisms/sticker-board/StickerBoard.jsx
Normal file
88
src/app/organisms/sticker-board/StickerBoard.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './StickerBoard.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getRelevantPacks } from '../emoji-board/custom-emoji';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
|
||||
function StickerBoard({ roomId, onSelect }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
|
||||
const packs = getRelevantPacks(
|
||||
mx,
|
||||
[room, ...parentRooms],
|
||||
).filter((pack) => pack.getStickers().length !== 0);
|
||||
|
||||
function isTargetNotSticker(target) {
|
||||
return target.classList.contains('sticker-board__sticker') === false;
|
||||
}
|
||||
function getStickerData(target) {
|
||||
const mxc = target.getAttribute('data-mx-sticker');
|
||||
const body = target.getAttribute('title');
|
||||
const httpUrl = target.getAttribute('src');
|
||||
return { mxc, body, httpUrl };
|
||||
}
|
||||
const handleOnSelect = (e) => {
|
||||
if (isTargetNotSticker(e.target)) return;
|
||||
|
||||
const stickerData = getStickerData(e.target);
|
||||
onSelect(stickerData);
|
||||
};
|
||||
|
||||
const renderPack = (pack) => (
|
||||
<div className="sticker-board__pack" key={pack.id}>
|
||||
<Text className="sticker-board__pack-header" variant="b2" weight="bold">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<div className="sticker-board__pack-items">
|
||||
{pack.getStickers().map((sticker) => (
|
||||
<img
|
||||
key={sticker.shortcode}
|
||||
className="sticker-board__sticker"
|
||||
src={mx.mxcUrlToHttp(sticker.mxc)}
|
||||
alt={sticker.shortcode}
|
||||
title={sticker.body ?? sticker.shortcode}
|
||||
data-mx-sticker={sticker.mxc}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="sticker-board">
|
||||
<div className="sticker-board__container">
|
||||
<ScrollView autoHide>
|
||||
<div
|
||||
onClick={handleOnSelect}
|
||||
className="sticker-board__content"
|
||||
>
|
||||
{
|
||||
packs.length > 0
|
||||
? packs.map(renderPack)
|
||||
: (
|
||||
<div className="sticker-board__empty">
|
||||
<Text>There is no sticker pack.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
StickerBoard.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StickerBoard;
|
||||
60
src/app/organisms/sticker-board/StickerBoard.scss
Normal file
60
src/app/organisms/sticker-board/StickerBoard.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.sticker-board {
|
||||
--sticker-board-height: 390px;
|
||||
--sticker-board-width: 286px;
|
||||
display: flex;
|
||||
height: var(--sticker-board-height);
|
||||
|
||||
&__container {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
width: var(--sticker-board-width);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__pack {
|
||||
margin-bottom: var(--sp-normal);
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background-color: var(--bg-surface);
|
||||
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
padding: var(--sp-extra-tight) var(--sp-ultra-tight);
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 -4px 0 0 var(--bg-surface);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
&-items {
|
||||
margin: var(--sp-tight);
|
||||
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-normal) var(--sp-tight);
|
||||
|
||||
img {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
width: 100%;
|
||||
height: var(--sticker-board-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user