Improve chat view

This commit is contained in:
Arthur Belleville 2025-06-19 22:33:03 +02:00
parent b7d5c08ac0
commit df0bb73305
No known key found for this signature in database
6 changed files with 152 additions and 111 deletions

View file

@ -28,12 +28,7 @@ export const App = () => {
<ThemeProvider>
<SessionProvider>
<Router>
<div
className={twMerge(
"min-h-screen bg-gradient-to-br from-emerald-100 via-green-100 to-white",
"dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black"
)}
>
<div className={twMerge("min-h-screen bg-white", "dark:bg-white")}>
<Routes>
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
<Route

View file

@ -39,7 +39,7 @@ export function Layout({ children }: LayoutProps) {
<SideNavigation isMobileMenuOpen={isMobileMenuOpen} />
</div>
<main className="flex-1 overflow-auto p-4 md:p-6">{children}</main>
<main className="flex-1 overflow-auto">{children}</main>
</div>
);
}

View file

@ -277,7 +277,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
},
{
path: "/chat",
label: "Messages",
label: "Discussions",
icon: <MessageCircleIcon className="w-5 h-5" />,
},
];

9
ui/src/main.css Normal file
View file

@ -0,0 +1,9 @@
/* .str-chat {
--str-chat__primary-color: #000;
--str-chat__active-primary-color: #000;
--str-chat__surface-color: #000;
--str-chat__secondary-surface-color: #000;
--str-chat__primary-surface-color: #000;
--str-chat__primary-surface-color-low-emphasis: #000;
--str-chat__border-radius-circle: 6px;
} */

View file

@ -6,6 +6,7 @@ import { queryClient } from "./lib/api";
import { GlobalToastRegion } from "./ui-library/toast/toast-region";
import "stream-chat-react/dist/css/v2/index.css";
import "./main.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>

View file

@ -1,134 +1,170 @@
import { useState } from "react";
import {
Channel,
ChannelList,
MessageList,
MessageInput,
Window,
Thread,
ChannelPreviewUIComponentProps,
useChatContext,
SendButton,
Channel,
ChatView,
Thread,
ThreadList,
ChannelPreviewUIComponentProps,
} from "stream-chat-react";
export function ChatPage() {
const { client, channel: activeChannel, setActiveChannel } = useChatContext();
const [creating, setCreating] = useState(false);
const [showSidebar, setShowSidebar] = useState(true);
interface ChannelData {
name?: string;
image?: string;
members?: string[];
}
// Custom Preview component for ChannelList
const CustomChannelPreview = (props: ChannelPreviewUIComponentProps) => {
const {
channel,
activeChannel: currentActiveChannel,
setActiveChannel: setActive,
} = props;
const isSelected = channel?.id === currentActiveChannel?.id;
const channelName =
channel.data && "name" in channel.data ? channel.data.name : channel.id;
const lastMessage = channel.state?.messages?.length
? channel.state.messages[channel.state.messages.length - 1]?.text
: undefined;
return (
<button
className={`w-full text-left px-4 py-2 rounded transition-colors duration-100 hover:bg-blue-50 ${
isSelected ? "bg-blue-100 font-bold" : ""
}`}
disabled={isSelected}
onClick={() => {
setActive?.(channel);
setShowSidebar(false); // Hide sidebar on mobile when channel is selected
}}
>
<div className="truncate">{String(channelName)}</div>
<div className="text-xs text-gray-500 truncate">
{lastMessage || "No messages yet"}
</div>
</button>
);
};
export function ChatPage() {
const { client } = useChatContext();
const [isCreating, setIsCreating] = useState(false);
const handleCreateChannel = async () => {
const name = window.prompt("Enter channel name:");
if (!name) return;
setCreating(true);
setIsCreating(true);
try {
const newChannel = client.channel("messaging", name);
const channelData: ChannelData = {
name,
members: [client.userID || ""],
};
const newChannel = client.channel("messaging", name, channelData);
await newChannel.create();
setActiveChannel(newChannel);
setShowSidebar(false); // Hide sidebar on mobile after creating channel
} catch {
alert("Failed to create channel");
} finally {
setCreating(false);
setIsCreating(false);
}
};
// Responsive: show sidebar only if no channel is selected or on large screens
return (
<div className="flex h-screen w-screen bg-zinc-50 dark:bg-zinc-900">
{/* Sidebar: hidden on mobile if a channel is selected */}
<aside
className={`w-72 h-full border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 flex flex-col transition-transform duration-200 z-10
${activeChannel && !showSidebar ? "hidden" : "block"} md:block`}
>
<div className="px-4 py-5 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between">
<span className="text-lg font-semibold tracking-tight text-blue-700 dark:text-blue-300">
Channels
</span>
<button
className="ml-2 px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 transition disabled:opacity-50 text-sm"
onClick={handleCreateChannel}
disabled={creating}
>
{creating ? "..." : "+"}
</button>
const CustomChannelPreview = ({
channel,
}: ChannelPreviewUIComponentProps) => {
const channelData = channel.data as ChannelData;
return (
<div className="px-4 py-3 hover:bg-gray-50 cursor-pointer">
<div className="font-medium text-gray-900">
{channelData?.name || channel.id || "Unnamed Channel"}
</div>
<div className="flex-1 overflow-y-auto">
<ChannelList
Preview={(props) => (
<CustomChannelPreview
{...props}
activeChannel={activeChannel}
setActiveChannel={setActiveChannel}
/>
)}
/>
<div className="text-sm text-gray-500 truncate">
{channel.state.messages?.[channel.state.messages.length - 1]?.text ||
"No messages yet"}
</div>
</aside>
</div>
);
};
{/* Main Chat Area: takes full width if a channel is selected */}
{activeChannel ? (
<div className="h-full">
<Channel channel={activeChannel} key={activeChannel.id}>
<Window>
{/* Header with back button on mobile */}
<div className="border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 flex items-center">
<button
className="md:hidden px-4 py-2 text-blue-600 hover:text-blue-800 focus:outline-none"
onClick={() => setShowSidebar(true)}
>
Back
</button>
<div className="px-6 py-4 text-lg font-semibold text-zinc-800 dark:text-zinc-100 truncate">
{activeChannel.data &&
typeof activeChannel.data === "object" &&
"name" in activeChannel.data
? (activeChannel.data as { name?: string }).name ||
activeChannel.id
: activeChannel.id}
const actions = [
"delete",
"edit",
"flag",
"markUnread",
"mute",
"react",
"reply",
];
return (
<div className="h-screen w-screen bg-[#efeae2]">
<ChatView>
<ChatView.Selector />
<div className="flex h-full overflow-hidden">
<ChatView.Channels>
<div className="flex h-full">
<div className="flex h-full flex-col w-[360px] shrink-0 border-r border-gray-200 bg-amber-50 overflow-hidden">
<div className="h-16 px-4 bg-[#008069] flex items-center justify-between">
<span className="text-lg font-semibold tracking-tight text-white">
Discussions
</span>
<button
className="ml-2 p-2 text-white rounded-full hover:bg-[#0c977d] transition disabled:opacity-50 text-sm"
onClick={handleCreateChannel}
disabled={isCreating}
>
{isCreating ? "..." : "+"}
</button>
</div>
<div className="flex-1 overflow-y-auto bg-white">
<ChannelList Preview={CustomChannelPreview} />
</div>
</div>
<MessageList />
<MessageInput />
<Thread />
</Window>
</Channel>
<div className="flex-1 bg-zinc-100 w-full">
<Channel SendButton={SendButton}>
<Window>
<div className="flex flex-col h-full w-full">
<div className="h-16 px-4 bg-[#008069] flex items-center justify-between border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
<span className="text-white font-semibold text-sm">
#
</span>
</div>
<div>
<h2 className="text-white font-semibold text-lg">
Channel Name
</h2>
<p className="text-white/70 text-sm">Active now</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button className="p-2 text-white/80 hover:text-white hover:bg-white/10 rounded-full transition">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
<button className="p-2 text-white/80 hover:text-white hover:bg-white/10 rounded-full transition">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
</button>
</div>
</div>
<div className="flex-1 flex flex-col min-h-0 w-full">
<MessageList messageActions={actions} />
<MessageInput
focus
audioRecordingEnabled
asyncMessagesMultiSendEnabled
/>
</div>
</div>
</Window>
</Channel>
</div>
</div>
</ChatView.Channels>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-zinc-400 text-lg select-none">
Select a channel to start chatting
</div>
)}
<ChatView.Threads>
<ThreadList />
<ChatView.ThreadAdapter>
<Thread virtualized />
</ChatView.ThreadAdapter>
</ChatView.Threads>
</ChatView>
</div>
);
}