Make more improvements on the chat
This commit is contained in:
parent
31a7a632ae
commit
99e73a582d
9 changed files with 182 additions and 166 deletions
33
ui/src/components/ChannelBadge.tsx
Normal file
33
ui/src/components/ChannelBadge.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
56
ui/src/components/CustomChannelHeader.tsx
Normal file
56
ui/src/components/CustomChannelHeader.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -88,7 +88,6 @@ export const CreateDevisModal = ({
|
|||
<Input placeholder="Email" />
|
||||
<FieldError>
|
||||
{(validationState) => {
|
||||
console.log(validationState);
|
||||
return validationState.validationErrors
|
||||
? "Veuillez entrer une adresse email valide"
|
||||
: "";
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue