Display reactions below messages
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
matrixgw_frontend/src/widgets/EmojiIcon.tsx
Normal file
24
matrixgw_frontend/src/widgets/EmojiIcon.tsx
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
.get(p.emojiKey)
|
||||||
|
?.find(
|
||||||
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id
|
(r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id
|
||||||
) !== undefined
|
) !== undefined
|
||||||
)
|
)
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
||||||
return <Button onClick={sendEmoji}>{p.emojiKey}</Button>;
|
return (
|
||||||
|
<Button onClick={sendEmoji}>
|
||||||
|
<EmojiIcon {...p} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user