diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index ccaa38f9..cb696b7c 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -98,7 +98,24 @@ "groupChat": "مجموعة", "directMessage": "محادثة خاصة", "participants": "المشاركون", - "searchUsers": "البحث عن مستخدمين" + "searchUsers": "البحث عن مستخدمين", + "modeDirect": "محادثة خاصة", + "modeGroup": "مجموعة", + "groupNameAr": "اسم المجموعة (عربي)", + "groupNameEn": "اسم المجموعة (إنجليزي)", + "groupNamePlaceholderAr": "مثلاً: فريق المبيعات", + "groupNamePlaceholderEn": "e.g. Sales team", + "searchUsersPlaceholder": "ابحث بالاسم أو اسم المستخدم", + "participantsCount_one": "{{count}} شخص مختار", + "participantsCount_other": "{{count}} أشخاص مختارين", + "participantsCount_zero": "لم يتم اختيار أحد", + "noUsersFound": "لا يوجد مستخدمون مطابقون", + "validation": { + "needGroupName": "أدخل اسم المجموعة بالعربية أو الإنجليزية.", + "minTwoMembers": "اختر شخصين على الأقل لإنشاء مجموعة.", + "pickOnePerson": "اختر شخصاً واحداً للمحادثة." + }, + "create": "إنشاء" }, "notifications": { "title": "الإشعارات", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 4cc5c135..4add5209 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -95,10 +95,27 @@ "send": "Send", "noConversations": "No conversations", "createConversation": "New Conversation", - "groupChat": "Group Chat", + "groupChat": "Group", "directMessage": "Direct Message", "participants": "Participants", - "searchUsers": "Search users" + "searchUsers": "Search users", + "modeDirect": "Direct", + "modeGroup": "Group", + "groupNameAr": "Group name (Arabic)", + "groupNameEn": "Group name (English)", + "groupNamePlaceholderAr": "مثلاً: فريق المبيعات", + "groupNamePlaceholderEn": "e.g. Sales team", + "searchUsersPlaceholder": "Search by name or username", + "participantsCount_one": "{{count}} person selected", + "participantsCount_other": "{{count}} people selected", + "participantsCount_zero": "No one selected", + "noUsersFound": "No matching users", + "validation": { + "needGroupName": "Enter a group name in Arabic or English.", + "minTwoMembers": "Pick at least 2 people for a group.", + "pickOnePerson": "Pick one person to chat with." + }, + "create": "Create" }, "notifications": { "title": "Notifications", diff --git a/artifacts/teaboy-os/src/pages/chat.tsx b/artifacts/teaboy-os/src/pages/chat.tsx index c6d49a28..a2322b5f 100644 --- a/artifacts/teaboy-os/src/pages/chat.tsx +++ b/artifacts/teaboy-os/src/pages/chat.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "wouter"; import { @@ -13,7 +13,16 @@ import { import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "@/contexts/AuthContext"; import { io } from "socket.io-client"; -import { ArrowRight, ArrowLeft, MessageCircle, Plus, Send } from "lucide-react"; +import { + ArrowRight, + ArrowLeft, + MessageCircle, + Plus, + Send, + Search, + Users, + User as UserIcon, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { formatTime } from "@/lib/i18n-format"; @@ -29,6 +38,8 @@ function getSocket() { }); } +type ConvMode = "direct" | "group"; + export default function ChatPage() { const { t, i18n } = useTranslation(); const [, setLocation] = useLocation(); @@ -41,7 +52,11 @@ export default function ChatPage() { const [selectedConvId, setSelectedConvId] = useState(null); const [messageText, setMessageText] = useState(""); const [showNewConv, setShowNewConv] = useState(false); + const [mode, setMode] = useState("direct"); const [selectedUserIds, setSelectedUserIds] = useState([]); + const [groupNameAr, setGroupNameAr] = useState(""); + const [groupNameEn, setGroupNameEn] = useState(""); + const [userSearch, setUserSearch] = useState(""); const bottomRef = useRef(null); const { data: conversations, isLoading: convsLoading } = useListConversations({ @@ -136,29 +151,111 @@ export default function ChatPage() { ); }; + const resetNewConvState = () => { + setMode("direct"); + setSelectedUserIds([]); + setGroupNameAr(""); + setGroupNameEn(""); + setUserSearch(""); + }; + + const openNewConv = () => { + resetNewConvState(); + setShowNewConv(true); + }; + + const closeNewConv = () => { + setShowNewConv(false); + resetNewConvState(); + }; + + const switchMode = (next: ConvMode) => { + setMode(next); + if (next === "direct") { + if (selectedUserIds.length > 1) { + setSelectedUserIds(selectedUserIds.slice(0, 1)); + } + setGroupNameAr(""); + setGroupNameEn(""); + } + }; + + const toggleUser = (uid: number) => { + if (mode === "direct") { + setSelectedUserIds(selectedUserIds.includes(uid) ? [] : [uid]); + } else { + setSelectedUserIds( + selectedUserIds.includes(uid) + ? selectedUserIds.filter((id) => id !== uid) + : [...selectedUserIds, uid], + ); + } + }; + + const filteredUsers = useMemo(() => { + const list = (users ?? []).filter((u) => u.id !== user?.id); + const q = userSearch.trim().toLowerCase(); + if (!q) return list; + return list.filter((u) => { + const fields = [u.username, u.displayNameAr ?? "", u.displayNameEn ?? ""]; + return fields.some((f) => f.toLowerCase().includes(q)); + }); + }, [users, userSearch, user?.id]); + + const validation: { ok: boolean; message: string | null } = useMemo(() => { + if (mode === "direct") { + if (selectedUserIds.length !== 1) { + return { ok: false, message: t("chat.validation.pickOnePerson") }; + } + return { ok: true, message: null }; + } + if (selectedUserIds.length < 2) { + return { ok: false, message: t("chat.validation.minTwoMembers") }; + } + if (!groupNameAr.trim() && !groupNameEn.trim()) { + return { ok: false, message: t("chat.validation.needGroupName") }; + } + return { ok: true, message: null }; + }, [mode, selectedUserIds, groupNameAr, groupNameEn, t]); + const handleCreateConv = () => { - if (!selectedUserIds.length) return; + if (!validation.ok) return; + const isGroup = mode === "group"; createConv.mutate( - { data: { participantIds: selectedUserIds, isGroup: selectedUserIds.length > 1 } }, + { + data: { + participantIds: selectedUserIds, + isGroup, + nameAr: isGroup ? (groupNameAr.trim() || null) : null, + nameEn: isGroup ? (groupNameEn.trim() || null) : null, + }, + }, { onSuccess: (conv) => { queryClient.invalidateQueries({ queryKey: getListConversationsQueryKey() }); setSelectedConvId(conv.id); - setShowNewConv(false); - setSelectedUserIds([]); + closeNewConv(); }, }, ); }; const selectedConv = conversations?.find((c) => c.id === selectedConvId); - const convName = selectedConv - ? (lang === "ar" ? selectedConv.nameAr : selectedConv.nameEn) || - selectedConv.participants - ?.filter((p) => p.id !== user?.id) - .map((p) => (lang === "ar" ? p.displayNameAr : p.displayNameEn) ?? p.username) - .join(", ") - : ""; + const convDisplayName = ( + conv: NonNullable, + ): string => { + const langName = lang === "ar" ? conv.nameAr : conv.nameEn; + const otherName = lang === "ar" ? conv.nameEn : conv.nameAr; + if (langName) return langName; + if (otherName) return otherName; + const others = conv.participants + ?.filter((p) => p.id !== user?.id) + .map((p) => (lang === "ar" ? p.displayNameAr : p.displayNameEn) ?? p.username); + if (others?.length) return others.join(", "); + return t("chat.directMessage"); + }; + + const convName = selectedConv ? convDisplayName(selectedConv) : ""; return (
@@ -170,16 +267,22 @@ export default function ChatPage() { > -
- -

+
+ +

{selectedConvId ? convName : t("chat.title")}

+ {selectedConvId && selectedConv?.isGroup && ( + + {t("chat.groupChat")} + + )}
{!selectedConvId && ( @@ -188,51 +291,178 @@ export default function ChatPage() { {/* New Conversation Dialog */} {showNewConv && ( -
-
-

{t("chat.createConversation")}

-

{t("chat.searchUsers")}

- +
+
e.stopPropagation()} + > +

+ {t("chat.createConversation")} +

+ + {/* Mode tabs */} +
+ + +
+ + {/* Group name fields */} + {mode === "group" && (
- {users?.filter((u) => u.id !== user?.id).map((u) => ( - - ))} + setGroupNameAr(e.target.value)} + placeholder={t("chat.groupNamePlaceholderAr")} + aria-label={t("chat.groupNameAr")} + dir="rtl" + className="bg-white/70 border-slate-200" + /> + setGroupNameEn(e.target.value)} + placeholder={t("chat.groupNamePlaceholderEn")} + aria-label={t("chat.groupNameEn")} + dir="ltr" + className="bg-white/70 border-slate-200" + /> +
+ )} + + {/* Search */} +
+ + setUserSearch(e.target.value)} + placeholder={t("chat.searchUsersPlaceholder")} + className={`bg-white/70 border-slate-200 ${isRtl ? "pr-9" : "pl-9"}`} + /> +
+ + {/* Counter */} +
+ + {t("chat.participantsCount", { count: selectedUserIds.length })} + +
+ + {/* User list */} + +
+ {filteredUsers.length === 0 ? ( +
+ {t("chat.noUsersFound")} +
+ ) : ( + filteredUsers.map((u) => { + const checked = selectedUserIds.includes(u.id); + const display = + (lang === "ar" ? u.displayNameAr : u.displayNameEn) ?? u.username; + return ( + + ); + }) + )}
+ + {/* Validation message */} + {validation.message && ( +

+ {validation.message} +

+ )} + + {/* Actions */}
-
@@ -310,14 +540,7 @@ export default function ChatPage() {
) : ( conversations.map((conv) => { - const name = - (lang === "ar" ? conv.nameAr : conv.nameEn) || - conv.participants - ?.filter((p) => p.id !== user?.id) - .map((p) => (lang === "ar" ? p.displayNameAr : p.displayNameEn) ?? p.username) - .join(", ") || - t("chat.directMessage"); - + const name = convDisplayName(conv); const lastMsg = conv.lastMessage; return ( @@ -328,12 +551,25 @@ export default function ChatPage() { > - {String(name).slice(0, 2)} + {conv.isGroup ? ( + + ) : ( + String(name).slice(0, 2) + )}
-
- {name} +
+
+ + {name} + + {conv.isGroup && ( + + {t("chat.groupChat")} + + )} +
{conv.unreadCount > 0 && ( {conv.unreadCount}