Display reactions below messages

This commit is contained in:
2025-11-28 14:37:17 +01:00
parent 9f0bc3303c
commit 6c11979ef2
3 changed files with 108 additions and 9 deletions

View File

@@ -17,7 +17,7 @@ export interface Message {
time_sent: number; time_sent: number;
time_sent_dayjs: dayjs.Dayjs; time_sent_dayjs: dayjs.Dayjs;
modified: boolean; modified: boolean;
reactions: MessageReaction[]; reactions: Map<string, MessageReaction[]>;
content: string; content: string;
image?: string; image?: string;
} }
@@ -80,7 +80,7 @@ export class RoomEventsManager {
event_id: evt.id, event_id: evt.id,
account: evt.sender, account: evt.sender,
modified: false, modified: false,
reactions: [], reactions: new Map(),
time_sent: evt.time, time_sent: evt.time,
time_sent_dayjs: dayjs.unix(evt.time / 1000), time_sent_dayjs: dayjs.unix(evt.time / 1000),
image: data.content.file?.url, image: data.content.file?.url,
@@ -93,12 +93,16 @@ export class RoomEventsManager {
const message = this.messages.find( const message = this.messages.find(
(m) => m.event_id === data.content["m.relates_to"].event_id (m) => m.event_id === data.content["m.relates_to"].event_id
); );
const key = data.content["m.relates_to"].key;
if (!message) continue; if (!message) continue;
message.reactions.push({
if (!message.reactions.has(key)) message.reactions.set(key, []);
message.reactions.get(key)!.push({
account: evt.sender, account: evt.sender,
event_id: evt.id, event_id: evt.id,
key: data.content["m.relates_to"].key, key,
}); });
} }
} }

View File

@@ -0,0 +1,24 @@
import { Emoji, EmojiStyle } from "emoji-picker-react";
function emojiUnicode(emoji: string): string {
let comp;
if (emoji.length === 1) {
comp = emoji.charCodeAt(0);
}
comp =
(emoji.charCodeAt(0) - 0xd800) * 0x400 +
(emoji.charCodeAt(1) - 0xdc00) +
0x10000;
if (comp < 0) {
comp = emoji.charCodeAt(0);
}
const s = comp.toString(16);
return s.includes("f") ? s : `${s}-fe0f`;
}
export function EmojiIcon(p: { emojiKey: string }): React.ReactElement {
const unified = emojiUnicode(p.emojiKey);
return (
<Emoji unified={unified ?? ""} emojiStyle={EmojiStyle.GOOGLE} size={18} />
);
}

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
ButtonGroup, ButtonGroup,
Chip,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@@ -22,8 +23,14 @@ import type { Room } from "../../api/matrix/MatrixApiRoom";
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider";
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
import type {
Message,
MessageReaction,
RoomEventsManager,
} from "../../utils/RoomEventsManager";
import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage";
import { EmojiIcon } from "../EmojiIcon";
import { AccountIcon } from "./AccountIcon"; import { AccountIcon } from "./AccountIcon";
export function RoomMessagesList(p: { export function RoomMessagesList(p: {
@@ -83,6 +90,7 @@ function RoomMessage(p: {
const user = useUserInfo(); const user = useUserInfo();
const alert = useAlert(); const alert = useAlert();
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar();
const loadingMessage = useLoadingMessage(); const loadingMessage = useLoadingMessage();
const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); const [showImageFullScreen, setShowImageFullScreen] = React.useState(false);
@@ -140,6 +148,20 @@ function RoomMessage(p: {
} }
}; };
const handleToggleReaction = async (
key: string,
reaction: MessageReaction
) => {
try {
if (!reaction)
await MatrixApiEvent.ReactToEvent(p.room, p.message.event_id, key);
else await MatrixApiEvent.DeleteEvent(p.room, reaction.event_id);
} catch (e) {
console.error(`Failed to toggle reaction!`, e);
snackbar(`Failed to toggle reaction! ${e}`);
}
};
return ( return (
<> <>
{/* Print date if required */} {/* Print date if required */}
@@ -243,6 +265,49 @@ function RoomMessage(p: {
</ButtonGroup> </ButtonGroup>
</Box> </Box>
{/* Reaction */}
<Box sx={{ marginLeft: "50px" }}>
{[...p.message.reactions.keys()].map((r) => {
const userReaction = p.message.reactions
.get(r)!
.find((r) => r.account === user.info.matrix_user_id);
return (
<Chip
size="small"
style={{
height: "2em",
marginRight: "5px",
maxHeight: "unset",
cursor: "pointer",
}}
slotProps={{
root: { onClick: () => handleToggleReaction(r, userReaction) },
label: { style: { height: "2em" } },
}}
color={userReaction !== undefined ? "success" : undefined}
variant="filled"
label={
<span
style={{
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
}}
>
<div style={{ margin: "3px 3px" }}>
<EmojiIcon emojiKey={r} />
</div>
<div style={{ height: "2em", marginLeft: "2px" }}>
{p.message.reactions.get(r)?.length}
</div>
</span>
}
/>
);
})}
</Box>
{/* Full screen image dialog */} {/* Full screen image dialog */}
<Dialog open={showImageFullScreen} onClose={closeImageFullScreen}> <Dialog open={showImageFullScreen} onClose={closeImageFullScreen}>
<img <img
@@ -315,11 +380,17 @@ function ReactionButton(p: {
// Do not offer to react to existing reactions // Do not offer to react to existing reactions
if ( if (
p.message.reactions.find( p.message.reactions
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id .get(p.emojiKey)
) !== undefined ?.find(
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id
) !== undefined
) )
return <></>; return <></>;
return <Button onClick={sendEmoji}>{p.emojiKey}</Button>; return (
<Button onClick={sendEmoji}>
<EmojiIcon {...p} />
</Button>
);
} }