Skip to content

Commit c176278

Browse files
authored
Merge pull request #1228 from Entrivax/feat/userVodChatHistory
feat(chat): display all user messages of vod in floating window on username click
2 parents 6c908f0 + 9a2c95d commit c176278

10 files changed

Lines changed: 546 additions & 149 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Comment, useGetChatForChatterInVideo } from "@/app/hooks/useChat";
2+
import {
3+
CloseButton,
4+
FloatingWindow,
5+
Group,
6+
ScrollArea,
7+
Text,
8+
} from "@mantine/core";
9+
import GanymedeLoadingText from "../utils/GanymedeLoadingText";
10+
import { useTranslations } from "next-intl";
11+
import ChatMessage from "./ChatMessage";
12+
import { UseFloatingWindowOptions } from "@mantine/hooks"
13+
import { RefObject, useEffect, useMemo, useRef } from "react"
14+
import { processComment, type ChatProcessingMaps } from "@/app/util/chat"
15+
16+
interface Params {
17+
videoId: string;
18+
chatterId: string;
19+
chatterLogin: string;
20+
chatterName: string;
21+
isLiveArchive: boolean;
22+
initialScrollMessageId?: string;
23+
timestampSeconds: ((comment: Comment) => number | null) | null;
24+
onTimestampClick: (timestamp: number) => void;
25+
onClose: () => void;
26+
initialPosition: UseFloatingWindowOptions['initialPosition'];
27+
chatMapsRef: RefObject<ChatProcessingMaps>;
28+
}
29+
30+
const ChatChatterMessages = ({
31+
videoId,
32+
chatterId,
33+
chatterLogin,
34+
chatterName,
35+
isLiveArchive,
36+
initialScrollMessageId,
37+
timestampSeconds,
38+
onTimestampClick,
39+
onClose,
40+
initialPosition,
41+
chatMapsRef,
42+
}: Params) => {
43+
const {
44+
data: comments,
45+
isLoading,
46+
isError,
47+
} = useGetChatForChatterInVideo(videoId, chatterId, chatterLogin, isLiveArchive);
48+
const t = useTranslations("VideoComponents");
49+
const messagesContainerRef = useRef<HTMLDivElement>(null);
50+
51+
const processedComments = useMemo(() => {
52+
if (!comments) return null;
53+
return comments.map((comment) => processComment(
54+
comment,
55+
chatMapsRef.current,
56+
(error) => {
57+
console.error(error);
58+
}
59+
));
60+
}, [comments, chatMapsRef]);
61+
62+
useEffect(() => {
63+
const handle = requestAnimationFrame(() => {
64+
if (!messagesContainerRef.current || !initialScrollMessageId || !processedComments?.length) return;
65+
const messageElement = messagesContainerRef.current.querySelector(`[data-message-id="${CSS.escape(initialScrollMessageId)}"]`);
66+
if (messageElement && 'scrollIntoView' in messageElement) {
67+
messageElement.scrollIntoView({ behavior: "smooth" });
68+
}
69+
})
70+
return () => cancelAnimationFrame(handle);
71+
}, [isLoading, processedComments, initialScrollMessageId]);
72+
73+
return (
74+
<FloatingWindow
75+
w={340}
76+
withBorder
77+
dragHandleSelector=".drag-handle"
78+
initialPosition={initialPosition}
79+
constrainToViewport={true}
80+
>
81+
<Group
82+
justify="space-between"
83+
px="md"
84+
py="sm"
85+
className="drag-handle"
86+
style={{ cursor: "move" }}
87+
>
88+
<Text>{t("chatterMessages", { name: chatterName })}</Text>
89+
<CloseButton onClick={onClose} />
90+
</Group>
91+
<ScrollArea.Autosize mah={300} px="md" pb="sm" ref={messagesContainerRef}>
92+
{isLoading && <GanymedeLoadingText message={t("loadingChatterMessages")} />}
93+
{!isLoading && isError && <Text color="red">{t("chatError")}</Text>}
94+
{!isLoading &&
95+
!isError &&
96+
(!processedComments || processedComments.length === 0 ? (
97+
<Text>{t("noChat")}</Text>
98+
) : (
99+
processedComments.map((comment) => (
100+
<ChatMessage
101+
key={comment._id}
102+
highlightAnimation={comment._id === initialScrollMessageId}
103+
comment={comment}
104+
showTimestamp={true}
105+
timestampSeconds={timestampSeconds?.(comment) ?? null}
106+
onTimestampClick={() => {
107+
onTimestampClick(comment.content_offset_seconds);
108+
}}
109+
/>
110+
))
111+
))}
112+
</ScrollArea.Autosize>
113+
</FloatingWindow>
114+
);
115+
};
116+
117+
export default ChatChatterMessages;

frontend/app/components/videos/ChatMessage.module.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@
1212
border-radius: 4px;
1313
padding: 2px 4px 2px 2px;
1414
}
15+
.highlightAnimation {
16+
animation: highlightAnimation 3s ease-in-out;
17+
}
18+
@keyframes highlightAnimation {
19+
0% {
20+
background-color: transparent;
21+
}
22+
10% {
23+
background-color: transparent;
24+
}
25+
30% {
26+
background-color: color-mix(in srgb, var(--mantine-color-yellow-6) 14%, transparent);
27+
}
28+
60% {
29+
background-color: color-mix(in srgb, var(--mantine-color-yellow-6) 14%, transparent);
30+
}
31+
100% {
32+
background-color: transparent;
33+
}
34+
}
1535
.chatMessageNoTimestamp {
1636
display: block;
1737
}

frontend/app/components/videos/ChatMessage.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
import { Comment, GanymedeChatMessageKind, GanymedeFormattedBadge, GanymedeFormattedMessageFragment, GanymedeFormattedMessageType } from "@/app/hooks/useChat";
33
import { durationToTime } from "@/app/util/util";
44
import classes from "./ChatMessage.module.css"
5-
import { Text, Tooltip } from "@mantine/core"
5+
import { Text, Tooltip, UnstyledButton } from "@mantine/core"
66
import { useTranslations } from "next-intl";
77
import { IconBolt, IconMessageCircle, IconStar } from "@tabler/icons-react";
8+
import { MouseEvent } from "react"
89

910
interface Params {
1011
comment: Comment;
12+
highlightAnimation?: boolean;
1113
showTimestamp: boolean;
1214
timestampSeconds: number | null;
1315
onTimestampClick: () => void;
16+
onUserNameClick?: (event: MouseEvent<HTMLButtonElement>) => void;
1417
}
1518

16-
const ChatMessage = ({ comment, showTimestamp, timestampSeconds, onTimestampClick }: Params) => {
19+
const ChatMessage = ({ comment, highlightAnimation, showTimestamp, timestampSeconds, onTimestampClick, onUserNameClick }: Params) => {
1720
const t = useTranslations("VideoComponents");
1821
const hasTimestamp = timestampSeconds !== null;
1922
const timestampLabel = hasTimestamp ? durationToTime(Math.floor(timestampSeconds)) : "";
@@ -39,6 +42,7 @@ const ChatMessage = ({ comment, showTimestamp, timestampSeconds, onTimestampClic
3942
isFirstMessage ? classes.firstMessage : "",
4043
isHighlighted ? classes.highlightedMessage : "",
4144
isAction ? classes.actionMessage : "",
45+
highlightAnimation ? classes.highlightAnimation : "",
4246
].filter(Boolean).join(" ");
4347

4448
const renderFormattedMessage = () => (
@@ -84,8 +88,20 @@ const ChatMessage = ({ comment, showTimestamp, timestampSeconds, onTimestampClic
8488
)
8589
);
8690

91+
const usernameComponent = (
92+
<Text
93+
fw={700}
94+
lh={1}
95+
size="sm"
96+
style={{ color: comment.message.user_color }}
97+
span
98+
>
99+
{comment.commenter.display_name}
100+
</Text>
101+
);
102+
87103
return (
88-
<div key={comment._id} className={rowClassName}>
104+
<div key={comment._id} className={rowClassName} data-message-id={comment._id}>
89105
{showTimestamp && (
90106
hasTimestamp ? (
91107
<button
@@ -144,15 +160,15 @@ const ChatMessage = ({ comment, showTimestamp, timestampSeconds, onTimestampClic
144160
</span>
145161
)}
146162
{/* username */}
147-
<Text
148-
fw={700}
149-
lh={1}
150-
size="sm"
151-
style={{ color: comment.message.user_color }}
152-
span
153-
>
154-
{comment.commenter.display_name}
155-
</Text>
163+
{
164+
onUserNameClick ? (
165+
<UnstyledButton onClick={onUserNameClick} type="button">
166+
{usernameComponent}
167+
</UnstyledButton>
168+
) : (
169+
usernameComponent
170+
)
171+
}
156172
<Text className={classes.message} span>
157173
{isAction ? " " : ": "}
158174
</Text>

0 commit comments

Comments
 (0)