Make more improvements on the chat

This commit is contained in:
Arthur Belleville 2025-07-06 15:49:57 +02:00
parent 31a7a632ae
commit 99e73a582d
No known key found for this signature in database
9 changed files with 182 additions and 166 deletions

View file

@ -0,0 +1,33 @@
import { UserTablo } from "@ui/types/tablos.types";
import { twMerge } from "tailwind-merge";
export const ChannelBadge = ({
tablo,
displayTitle,
isOnline,
}: {
tablo: UserTablo | null;
displayTitle: string | undefined;
isOnline: boolean;
}) => {
const initials =
tablo?.name?.charAt(0)?.toUpperCase() ||
displayTitle?.charAt(0)?.toUpperCase() ||
"C";
return (
<div className="relative">
<div
className={twMerge(
"size-12 rounded-full flex items-center justify-center text-white font-semibold text-sm",
tablo?.color && tablo.color
)}
>
{initials}
</div>
{isOnline && (
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-white dark:border-gray-900 rounded-full" />
)}
</div>
);
};

View file

@ -3,6 +3,7 @@ import { Badge } from "@ui/ui-library/badge";
import { ReactNode } from "react";
import { Channel } from "stream-chat";
import { twMerge } from "tailwind-merge";
import { ChannelBadge } from "@ui/components/ChannelBadge";
interface ChannelPreviewProps {
channel: Channel;
@ -80,8 +81,6 @@ export function ChannelPreview({
setActiveChannel?.(channel);
};
console.log({ displayTitle, tablo });
return (
<div
className={twMerge(
@ -92,24 +91,11 @@ export function ChannelPreview({
)}
onClick={handleClick}
>
{/* Avatar with online status */}
<div className="relative">
<div className="relative">
<div
className={twMerge(
"size-12 rounded-full flex items-center justify-center text-white font-semibold text-sm",
tablo?.color && tablo.color
)}
>
{tablo?.name?.charAt(0)?.toUpperCase() ||
displayTitle?.charAt(0)?.toUpperCase() ||
"C"}
</div>
{isOnline && (
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-white dark:border-gray-900 rounded-full" />
)}
</div>
</div>
<ChannelBadge
tablo={tablo}
displayTitle={displayTitle}
isOnline={isOnline}
/>
{/* Channel info */}
<div className="flex-1 min-w-0">

View file

@ -0,0 +1,56 @@
import { ChannelHeader, useChannelStateContext } from "stream-chat-react";
import { ChannelBadge } from "./ChannelBadge";
import { UserTablo } from "@ui/types/tablos.types";
interface CustomChannelHeaderProps {
tablos: UserTablo[];
onToggleChannelList?: () => void;
isChannelListExpanded?: boolean;
}
export const CustomChannelHeader = ({
tablos,
onToggleChannelList,
isChannelListExpanded = false,
}: CustomChannelHeaderProps) => {
const { channel } = useChannelStateContext();
return (
<div className="flex items-center">
{onToggleChannelList && (
<button
onClick={onToggleChannelList}
className="mr-2 p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle channel list"
>
<svg
className={`w-5 h-5 transition-transform duration-200 ${
isChannelListExpanded ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
)}
<ChannelHeader
Avatar={() => {
return (
<ChannelBadge
tablo={tablos?.find((t) => t.id === channel?.id) ?? null}
displayTitle={channel?.data?.config?.name}
isOnline={false}
/>
);
}}
/>
</div>
);
};

View file

@ -88,7 +88,6 @@ export const CreateDevisModal = ({
<Input placeholder="Email" />
<FieldError>
{(validationState) => {
console.log(validationState);
return validationState.validationErrors
? "Veuillez entrer une adresse email valide"
: "";

View file

@ -1,17 +1,18 @@
import { useEffect, useState } from "react";
import { Channel, StreamChat } from "stream-chat";
import { useParams } from "react-router-dom";
export const useChannel = (
client: StreamChat,
channelId: string | undefined
) => {
export const useChannelFromUrl = (client: StreamChat) => {
const [channel, setChannel] = useState<Channel | null>(null);
const { channelId } = useParams();
useEffect(() => {
if (channelId) {
const channel = client.channel("messaging", channelId);
channel.watch();
setChannel(channel);
} else {
setChannel(null);
}
}, [channelId, client]);
return channel;
return { channel, isChannelInUrl: !!channelId };
};

View file

@ -1,112 +1,52 @@
import {
ChannelList,
ChannelHeader,
MessageList,
MessageInput,
Window,
Channel,
useChatContext,
} from "stream-chat-react";
import { useUser } from "@ui/providers/UserStoreProvider";
import { ChannelPreview } from "@ui/components/ChannelPreview";
import { useParams } from "react-router-dom";
import { StreamChat } from "stream-chat";
import { useChannel } from "@ui/hooks/channel";
import { useChannelFromUrl } from "@ui/hooks/channel";
import { useTablosList } from "@ui/hooks/tablos";
import { CustomChannelHeader } from "@ui/components/CustomChannelHeader";
import { useEffect, useState } from "react";
export function ChatPage({ client }: { client: StreamChat }) {
const user = useUser();
const filters = { members: { $in: [user.id] }, type: "messaging" };
const { channelId } = useParams();
const channel = useChannel(client, channelId);
const { channel: channelFromUrl, isChannelInUrl } = useChannelFromUrl(client);
const { channel, setActiveChannel } = useChatContext();
const { data: tablos } = useTablosList();
const [isChannelListExpanded, setIsChannelListExpanded] = useState(false);
if (channel) {
return (
<Channel channel={channel}>
<Window>
<div className="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-3">
<div className="size-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold text-sm">
{channel?.data?.config?.name?.charAt(0)?.toUpperCase() ||
"C"}
</div>
<div>
<h1 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{channel?.data?.config?.name || "Channel"}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
{Object.keys(channel?.state?.members || {}).length}{" "}
members
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg
className="size-5 text-gray-500 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
</button>
<button className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg
className="size-5 text-gray-500 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
<button className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg
className="size-5 text-gray-500 dark:text-gray-400"
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>
<MessageList />
<MessageInput />
</Window>
</Channel>
);
}
const toggleChannelList = () => {
setIsChannelListExpanded(!isChannelListExpanded);
};
useEffect(() => {
if (channelFromUrl) {
setActiveChannel(channelFromUrl);
} else {
setIsChannelListExpanded(true);
}
}, [channelFromUrl]);
return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<div className="w-1/3 border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div
className={`border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 transition-all duration-300 ease-in-out overflow-hidden ${
isChannelListExpanded ? "w-80" : "w-0"
}`}
>
<ChannelList
filters={filters}
setActiveChannelOnMount
setActiveChannelOnMount={isChannelInUrl ? false : true}
Preview={({
displayTitle,
channel,
@ -128,9 +68,13 @@ export function ChatPage({ client }: { client: StreamChat }) {
/>
</div>
<div className="flex-1 bg-white dark:bg-gray-800">
<Channel>
<Channel channel={channel}>
<Window>
<ChannelHeader />
<CustomChannelHeader
tablos={tablos ?? []}
onToggleChannelList={toggleChannelList}
isChannelListExpanded={isChannelListExpanded}
/>
<MessageList />
<MessageInput />
</Window>

View file

@ -291,13 +291,9 @@ export const TabloPage = () => {
className="relative"
onContextMenu={(e) => {
e.preventDefault();
// Only show context menu if user is admin
if (isAdmin) {
setContextMenuTablo(
contextMenuTablo === tablo.id ? null : tablo.id
);
setContextMenuPosition({ x: e.clientX, y: e.clientY });
}
// Show context menu for all users
setContextMenuTablo(contextMenuTablo === tablo.id ? null : tablo.id);
setContextMenuPosition({ x: e.clientX, y: e.clientY });
}}
>
<div
@ -424,8 +420,8 @@ export const TabloPage = () => {
</div>
</div>
{/* Contextual Menu - Only show for admins */}
{isAdmin && contextMenuTablo === tablo.id && contextMenuPosition && (
{/* Contextual Menu */}
{contextMenuTablo === tablo.id && contextMenuPosition && (
<div
className="fixed bg-gray-50 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-30 min-w-36"
style={{
@ -482,47 +478,51 @@ export const TabloPage = () => {
<span>Ouvrir le tablo</span>
</button> */}
{/* Status change options */}
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
Changer le statut
</div>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "todo");
}}
>
<span>À faire</span>
{tablo.status === "todo" && (
<span className="text-blue-500"></span>
)}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "in_progress");
}}
>
<span>En cours</span>
{tablo.status === "in_progress" && (
<span className="text-blue-500"></span>
)}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "done");
}}
>
<span>Terminé</span>
{tablo.status === "done" && (
<span className="text-blue-500"></span>
)}
</button>
{/* Status change options - Only for admins */}
{isAdmin && (
<>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
Changer le statut
</div>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "todo");
}}
>
<span>À faire</span>
{tablo.status === "todo" && (
<span className="text-blue-500"></span>
)}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "in_progress");
}}
>
<span>En cours</span>
{tablo.status === "in_progress" && (
<span className="text-blue-500"></span>
)}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "done");
}}
>
<span>Terminé</span>
{tablo.status === "done" && (
<span className="text-blue-500"></span>
)}
</button>
</>
)}
</div>
)}
</div>

View file

@ -12,8 +12,6 @@ export default function ProfileScreen() {
const [displayName, setDisplayName] = useState(user?.full_name || "");
const [isEditing, setIsEditing] = useState(false);
console.log({ user });
return (
<View style={styles.container}>
<Card containerStyle={styles.card}>

View file

@ -9,7 +9,6 @@ export const useGetUser = (): { user: User | null; isPending: boolean } => {
queryKey: ["user"],
queryFn: async () => {
const { data } = await supabase.from("profiles").select("*");
console.log(data);
return data;
},
});