Skip to content

Commit bf152e3

Browse files
committed
feat(chat): Improve chat UI and conversation handling
- Replace creatingConversation state with a ref to avoid rerenders - Skip adding duplicate conversations and verify user is in convo - Add conversations to realtime effect deps to keep list up to date - Pass conversationId to Conversations; highlight active item, shrink avatars and truncate long usernames - Add scroll-to-bottom button in Messages with scroll listener - Tweak chat header spacing and wrap conversations in scroll area - Expand TopLeaderboard grid columns - Add Vercel rewrite for /api/sentry-example-api
1 parent db3f374 commit bf152e3

5 files changed

Lines changed: 94 additions & 26 deletions

File tree

app/components/Chat.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export default function Chat({ user }: { user: User }) {
5050
const [allUsers, setAllUsers] = useState<ChatUser[]>([]);
5151
const channelRef = useRef<RealtimeChannel>(null);
5252
const textareaRef = useRef<HTMLTextAreaElement>(null);
53-
const [creatingConversation, setCreatingConversation] = useState(false);
5453
const bottomRef = useRef<HTMLDivElement | null>(null);
54+
const creatingRef = useRef(false);
5555

5656
useEffect(() => {
5757
if (textareaRef.current) {
@@ -124,6 +124,16 @@ export default function Chat({ user }: { user: User }) {
124124
.then(({ data }) => {
125125
if (!data || data.length === 0) return;
126126
const convo = data[0];
127+
// Double check the user is part of the conversation (should always be true)
128+
if (
129+
!convo.users.some(
130+
(u: { user_id: string }) => u.user_id === user.id,
131+
)
132+
)
133+
return;
134+
// Check if we already have this conversation in state
135+
if (conversations.some((c) => c.id === convo.id)) return;
136+
127137
setConversations((prev) => [
128138
...prev,
129139
{
@@ -141,7 +151,7 @@ export default function Chat({ user }: { user: User }) {
141151
return () => {
142152
channel.unsubscribe();
143153
};
144-
}, [user.id]);
154+
}, [user.id, conversations]);
145155

146156
useEffect(() => {
147157
if (!conversationId) return;
@@ -223,9 +233,9 @@ export default function Chat({ user }: { user: User }) {
223233
}, [showModal, user.id]);
224234

225235
const createConversation = async (otherUser: ChatUser) => {
226-
if (creatingConversation) return;
236+
if (creatingRef.current) return;
237+
creatingRef.current = true;
227238

228-
setCreatingConversation(true);
229239
const existing = conversations.find((conv) =>
230240
conv.users.some((u) => u.id === otherUser.user_id),
231241
);
@@ -274,8 +284,9 @@ export default function Chat({ user }: { user: User }) {
274284
],
275285
},
276286
]);
277-
setCreatingConversation(false);
287+
278288
setShowModal(false);
289+
creatingRef.current = false;
279290
};
280291

281292
const sendMessage = async () => {
@@ -296,22 +307,25 @@ export default function Chat({ user }: { user: User }) {
296307

297308
return (
298309
<div className="flex flex-col h-screen">
299-
<div className="p-4 flex gap-4 overflow-x-auto border-b border-neutral-700">
310+
<div className="px-3 pt-3 flex border-neutral-700">
300311
<button
301312
onClick={() => setShowModal(true)}
302313
className="flex flex-col items-center min-w-15"
303314
>
304-
<div className="w-12 h-12 rounded-full bg-indigo-500 flex items-center justify-center">
315+
<div className="w-10 h-10 rounded-full bg-indigo-500 flex items-center justify-center">
305316
<FontAwesomeIcon icon={faPlus} className="text-white" />
306317
</div>
307318
<span className="text-xs mt-1">New</span>
308319
</button>
309320

310-
<Conversations
311-
conversations={conversations}
312-
user={user}
313-
setConversationId={setConversationId}
314-
/>
321+
<div className="flex-1 flex gap-4 overflow-x-auto">
322+
<Conversations
323+
conversations={conversations}
324+
user={user}
325+
conversationId={conversationId}
326+
setConversationId={setConversationId}
327+
/>
328+
</div>
315329
</div>
316330

317331
{conversationId ? (

app/components/chat/Conversations.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,48 @@
1-
"use client";
21
import { User } from "@supabase/supabase-js";
32
import { Conversation } from "../Chat";
43

54
export default function Conversations({
65
conversations,
76
user,
7+
conversationId,
88
setConversationId,
99
}: {
1010
conversations: Conversation[];
1111
user: User;
12+
conversationId: string | null;
1213
setConversationId: (id: string) => void;
1314
}) {
1415
return (
1516
<>
1617
{conversations.map((conv, idx) => {
1718
const otherUser = conv.users.find((u) => u.id !== user.id);
19+
const isActive = conv.id === conversationId; // check active
20+
1821
return (
1922
<div
2023
key={idx}
2124
onClick={() => setConversationId(conv.id)}
2225
className="flex flex-col items-center min-w-15 cursor-pointer"
2326
>
24-
<div className="flex justify-center items-center w-12 h-12 rounded-full bg-neutral-600">
25-
{otherUser?.email[0].toUpperCase()}
27+
<div
28+
className={`
29+
flex justify-center items-center w-10 h-10 rounded-full border border-white/10
30+
${isActive ? "bg-indigo-500 text-white" : "bg-white/5 text-gray-300"}
31+
`}
32+
>
33+
{otherUser?.email[0]?.toUpperCase()}
2634
</div>
27-
<span className="text-xs mt-1">
28-
{otherUser?.email.split("@")[0]}
35+
<span
36+
className={`text-xs mt-1 ${isActive ? "text-white" : "text-gray-300"}`}
37+
>
38+
{(() => {
39+
const name = otherUser?.email?.split("@")[0] || "";
40+
return name.length > 10 ? name.slice(0, 8) + "..." : name;
41+
})()}
2942
</span>
3043
</div>
3144
);
3245
})}
3346
</>
3447
);
3548
}
36-

app/components/chat/Messages.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
"use client";
2+
13
import { User } from "@supabase/supabase-js";
24
import ReactMarkdown from "react-markdown";
35
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
46
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
57
import { Conversation, Message } from "../Chat";
68
import { timeAgo } from "@/app/utils/time";
9+
import { useEffect, useState } from "react";
710

811
export default function Messages({
912
messages,
@@ -16,9 +19,42 @@ export default function Messages({
1619
conversations: Conversation[];
1720
bottomRef: React.RefObject<HTMLDivElement | null>;
1821
}) {
22+
const [showScrollBtn, setShowScrollBtn] = useState(false);
23+
24+
useEffect(() => {
25+
const container = document.getElementById("chat-container");
26+
27+
if (!container) return;
28+
29+
const handleScroll = () => {
30+
const isNearBottom =
31+
container.scrollHeight - container.scrollTop - container.clientHeight <
32+
100;
33+
34+
setShowScrollBtn(!isNearBottom);
35+
};
36+
37+
container.addEventListener("scroll", handleScroll);
38+
return () => container.removeEventListener("scroll", handleScroll);
39+
}, []);
40+
1941
return (
2042
<>
21-
<div className="flex-1 overflow-y-auto p-4 space-y-3">
43+
<div className="flex-1 overflow-y-auto p-4 space-y-3" id="chat-container">
44+
{showScrollBtn && (
45+
<button
46+
onClick={() =>
47+
bottomRef.current?.scrollIntoView({ behavior: "smooth" })
48+
}
49+
className="fixed right-4 top-1/2 -translate-y-1/2 z-50
50+
bg-white/10 hover:bg-white/20
51+
backdrop-blur-md border border-white/10
52+
text-white rounded-full p-3 shadow-lg transition"
53+
>
54+
55+
</button>
56+
)}
57+
2258
{messages.length === 0 && (
2359
<div className="text-gray-500 text-sm italic text-center mt-10">
2460
No messages yet. Start the conversation!
@@ -41,7 +77,7 @@ export default function Messages({
4177
)}
4278
<div>
4379
<div
44-
className={`px-4 py-2 rounded-2xl max-w-xs ${
80+
className={`px-4 py-2 rounded-2xl max-w-xs overflow-hidden ${
4581
msg.sender_id === user.id ? "bg-indigo-500" : "bg-neutral-700"
4682
}`}
4783
>
@@ -53,11 +89,12 @@ export default function Messages({
5389

5490
return (
5591
<>
56-
{ }
57-
92+
{}
93+
5894
<SyntaxHighlighter
59-
60-
style={atomDark as { [key: string]: React.CSSProperties }}
95+
style={
96+
atomDark as { [key: string]: React.CSSProperties }
97+
}
6198
language={match ? match[1] : "text"}
6299
PreTag="pre"
63100
className="rounded-md text-sm"
@@ -74,7 +111,7 @@ export default function Messages({
74111
</ReactMarkdown>
75112
</div>
76113

77-
<div className="text-muted text-sm">
114+
<div className="text-muted text-xs mt-1">
78115
{timeAgo(msg.created_at)}
79116
</div>
80117
</div>

app/components/landing-page/TopLeaderbord.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function TopLeaderboard({
2424
Celebrating the most dedicated coders in our community.
2525
</p>
2626

27-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
27+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
2828
{top_members.map(
2929
(
3030
member: { email: string; total_seconds: number },

vercel.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"source": "/sentry-example-page",
55
"destination": "/",
66
"permanent": true
7+
},
8+
{
9+
"source": "/api/sentry-example-api",
10+
"destination": "/",
11+
"permanent": true
712
}
813
]
914
}

0 commit comments

Comments
 (0)