{ msgType === 'm.emote' && (
<>
{'* '}
@@ -277,7 +289,7 @@ function MessageEdit({ body, onSave, onCancel }) {
}, []);
const handleKeyDown = (e) => {
- if (e.keyCode === 13 && e.shiftKey === false) {
+ if (e.key === 'Enter' && e.shiftKey === false) {
e.preventDefault();
onSave(editInputRef.current.value);
}
@@ -322,7 +334,7 @@ function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
return rEvent;
}
-function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
+function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
if (myAlreadyReactEvent) {
const rId = myAlreadyReactEvent.getId();
@@ -330,17 +342,17 @@ function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
redactEvent(roomId, rId);
return;
}
- sendReaction(roomId, eventId, emojiKey);
+ sendReaction(roomId, eventId, emojiKey, shortcode);
}
function pickEmoji(e, roomId, eventId, roomTimeline) {
openEmojiBoard(getEventCords(e), (emoji) => {
- toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
+ toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
e.target.click();
});
}
-function genReactionMsg(userIds, reaction) {
+function genReactionMsg(userIds, reaction, shortcode) {
return (
<>
{userIds.map((userId, index) => (
@@ -354,24 +366,22 @@ function genReactionMsg(userIds, reaction) {
))}
{' reacted with '}
- {twemojify(reaction, { className: 'react-emoji' })}
+ {twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
>
);
}
function MessageReaction({
- shortcodeToEmoji, reaction, count, users, isActive, onClick,
+ reaction, shortcode, count, users, isActive, onClick,
}) {
- const customEmojiMatch = reaction.match(/^:(\S+):$/);
let customEmojiUrl = null;
- if (customEmojiMatch) {
- const customEmoji = shortcodeToEmoji.get(customEmojiMatch[1]);
- customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(customEmoji?.mxc);
+ if (reaction.match(/^mxc:\/\/\S+$/)) {
+ customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
}
return (
{users.length > 0 ? genReactionMsg(users, reaction) : 'Unable to load who has reacted'}}
+ content={{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}}
>
);
}
+MessageReaction.defaultProps = {
+ shortcode: undefined,
+};
MessageReaction.propTypes = {
- shortcodeToEmoji: PropTypes.shape({}).isRequired,
reaction: PropTypes.node.isRequired,
+ shortcode: PropTypes.string,
count: PropTypes.number.isRequired,
users: PropTypes.arrayOf(PropTypes.string).isRequired,
isActive: PropTypes.bool.isRequired,
@@ -401,11 +414,10 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
const { roomId, room, reactionTimeline } = roomTimeline;
const mx = initMatrix.matrixClient;
const reactions = {};
- const shortcodeToEmoji = getShortcodeToCustomEmoji(room);
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
const eventReactions = reactionTimeline.get(mEvent.getId());
- const addReaction = (key, count, senderId, isActive) => {
+ const addReaction = (key, shortcode, count, senderId, isActive) => {
let reaction = reactions[key];
if (reaction === undefined) {
reaction = {
@@ -414,6 +426,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
isActive: false,
};
}
+ if (shortcode) reaction.shortcode = shortcode;
if (count) {
reaction.count = count;
} else {
@@ -429,9 +442,10 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
if (rEvent.getRelation() === null) return;
const reaction = rEvent.getRelation();
const senderId = rEvent.getSender();
+ const { shortcode } = rEvent.getContent();
const isActive = senderId === mx.getUserId();
- addReaction(reaction.key, undefined, senderId, isActive);
+ addReaction(reaction.key, shortcode, undefined, senderId, isActive);
});
} else {
// Use aggregated reactions
@@ -439,7 +453,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
if (!aggregatedReaction) return null;
aggregatedReaction.forEach((reaction) => {
if (reaction.type !== 'm.reaction') return;
- addReaction(reaction.key, reaction.count, undefined, false);
+ addReaction(reaction.key, undefined, reaction.count, undefined, false);
});
}
@@ -449,13 +463,13 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
Object.keys(reactions).map((key) => (
{
- toggleEmoji(roomId, mEvent.getId(), key, roomTimeline);
+ toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
}}
/>
))
@@ -607,7 +621,9 @@ function genMediaContent(mE) {
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return Malformed event;
let msgType = mE.getContent()?.msgtype;
- if (mE.getType() === 'm.sticker') msgType = 'm.image';
+ if (mE.getType() === 'm.sticker') msgType = 'm.sticker';
+
+ const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
switch (msgType) {
case 'm.file':
@@ -628,6 +644,18 @@ function genMediaContent(mE) {
link={mx.mxcUrlToHttp(mediaMXC)}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
+ blurhash={blurhash}
+ />
+ );
+ case 'm.sticker':
+ return (
+
);
case 'm.audio':
@@ -654,6 +682,7 @@ function genMediaContent(mE) {
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
+ blurhash={blurhash}
/>
);
default:
@@ -674,7 +703,7 @@ function getEditedBody(editedMEvent) {
}
function Message({
- mEvent, isBodyOnly, roomTimeline, focus, time,
+ mEvent, isBodyOnly, roomTimeline, focus, fullTime,
}) {
const [isEditing, setIsEditing] = useState(false);
const roomId = mEvent.getRoomId();
@@ -735,7 +764,12 @@ function Message({
}
{!isBodyOnly && (
-
+
)}
{roomTimeline && isReply && (
- {time}
+
+
+
);
@@ -68,7 +71,7 @@ TimelineChange.propTypes = {
PropTypes.string,
PropTypes.node,
]).isRequired,
- time: PropTypes.string.isRequired,
+ timestamp: PropTypes.number.isRequired,
onClick: PropTypes.func,
};
diff --git a/src/app/molecules/room-emojis/RoomEmojis.jsx b/src/app/molecules/room-emojis/RoomEmojis.jsx
new file mode 100644
index 00000000..81cee0a8
--- /dev/null
+++ b/src/app/molecules/room-emojis/RoomEmojis.jsx
@@ -0,0 +1,130 @@
+import React, { useReducer, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import './RoomEmojis.scss';
+
+import initMatrix from '../../../client/initMatrix';
+import { suffixRename } from '../../../util/common';
+
+import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
+import Text from '../../atoms/text/Text';
+import Input from '../../atoms/input/Input';
+import Button from '../../atoms/button/Button';
+import ImagePack from '../image-pack/ImagePack';
+
+function useRoomPacks(room) {
+ const mx = initMatrix.matrixClient;
+ const [, forceUpdate] = useReducer((count) => count + 1, 0);
+
+ const packEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
+ const unUsablePacks = [];
+ const usablePacks = packEvents.filter((mEvent) => {
+ if (typeof mEvent.getContent()?.images !== 'object') {
+ unUsablePacks.push(mEvent);
+ return false;
+ }
+ return true;
+ });
+
+ useEffect(() => {
+ const handleEvent = (event, state, prevEvent) => {
+ if (event.getRoomId() !== room.roomId) return;
+ if (event.getType() !== 'im.ponies.room_emotes') return;
+ if (!prevEvent?.getContent()?.images || !event.getContent().images) {
+ forceUpdate();
+ }
+ };
+
+ mx.on('RoomState.events', handleEvent);
+ return () => {
+ mx.removeListener('RoomState.events', handleEvent);
+ };
+ }, [room, mx]);
+
+ const isStateKeyAvailable = (key) => !room.currentState.getStateEvents('im.ponies.room_emotes', key);
+
+ const createPack = async (name) => {
+ const packContent = {
+ pack: { display_name: name },
+ images: {},
+ };
+ let stateKey = '';
+ if (unUsablePacks.length > 0) {
+ const mEvent = unUsablePacks[0];
+ stateKey = mEvent.getStateKey();
+ } else {
+ stateKey = packContent.pack.display_name.replace(/\s/g, '-');
+ if (!isStateKeyAvailable(stateKey)) {
+ stateKey = suffixRename(
+ stateKey,
+ isStateKeyAvailable,
+ );
+ }
+ }
+ await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', packContent, stateKey);
+ };
+
+ const deletePack = async (stateKey) => {
+ await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', {}, stateKey);
+ };
+
+ return {
+ usablePacks,
+ createPack,
+ deletePack,
+ };
+}
+
+function RoomEmojis({ roomId }) {
+ const mx = initMatrix.matrixClient;
+ const room = mx.getRoom(roomId);
+
+ const { usablePacks, createPack, deletePack } = useRoomPacks(room);
+
+ const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
+ const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
+
+ const handlePackCreate = (e) => {
+ e.preventDefault();
+ const { nameInput } = e.target;
+ const name = nameInput.value.trim();
+ if (name === '') return;
+ nameInput.value = '';
+
+ createPack(name);
+ };
+
+ return (
+
+ { canChange && (
+
+ Create Pack
+
+
+ )}
+ {
+ usablePacks.length > 0
+ ? usablePacks.reverse().map((mEvent) => (
+
+ )) : (
+
+ No emoji or sticker pack.
+
+ )
+ }
+
+ );
+}
+
+RoomEmojis.propTypes = {
+ roomId: PropTypes.string.isRequired,
+};
+
+export default RoomEmojis;
diff --git a/src/app/molecules/room-emojis/RoomEmojis.scss b/src/app/molecules/room-emojis/RoomEmojis.scss
new file mode 100644
index 00000000..7ba2b494
--- /dev/null
+++ b/src/app/molecules/room-emojis/RoomEmojis.scss
@@ -0,0 +1,29 @@
+.room-emojis {
+ .image-pack,
+ .room-emojis__add-pack,
+ .room-emojis__empty {
+ margin: var(--sp-normal) 0;
+ background-color: var(--bg-surface);
+ border-radius: var(--bo-radius);
+ box-shadow: var(--bs-surface-border);
+ overflow: hidden;
+
+ & > .context-menu__header:first-child {
+ margin-top: 2px;
+ }
+ }
+ &__add-pack {
+ & form {
+ margin: var(--sp-normal);
+ display: flex;
+ gap: var(--sp-normal);
+ & .input-container {
+ flex-grow: 1;
+ }
+ }
+ }
+ &__empty {
+ padding: var(--sp-extra-loose) var(--sp-normal);
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/src/app/molecules/room-permissions/RoomPermissions.jsx b/src/app/molecules/room-permissions/RoomPermissions.jsx
index 989a9396..da8720cd 100644
--- a/src/app/molecules/room-permissions/RoomPermissions.jsx
+++ b/src/app/molecules/room-permissions/RoomPermissions.jsx
@@ -237,12 +237,12 @@ function RoomPermissions({ roomId }) {
? permissions[permInfo.parent]?.[permKey]
: permissions[permKey];
- if (!permValue) permValue = permInfo.default;
+ if (permValue === undefined) permValue = permInfo.default;
if (typeof permValue === 'number') {
powerLevel = permValue;
} else if (permKey === 'notifications') {
- powerLevel = permValue.room || 50;
+ powerLevel = permValue.room ?? 50;
}
return (
(