Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/pages/instance/Chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./style.css";
import { useTranslation } from "react-i18next";
import { User, MessageCircle, PlusIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
Expand Down Expand Up @@ -28,6 +29,7 @@ const formatJid = (remoteJid: string): string => {
};

function Chat() {
const { t } = useTranslation();
const isMD = useMediaQuery("(min-width: 768px)");
const lastMessageRef = useRef<HTMLDivElement | null>(null);
const [textareaHeight] = useState("auto");
Expand All @@ -49,7 +51,7 @@ function Chat() {
const chatMap = new Map();

// First add all chats from React Query
chats.forEach((chat) => chatMap.set(chat.remoteJid, chat));
chats.forEach((chat: { remoteJid: any; }) => chatMap.set(chat.remoteJid, chat));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Replace the inline { remoteJid: any } typing with the existing ChatType (or equivalent).

Using { remoteJid: any } here throws away the stronger typing you already have and reintroduces any into this path. Relying on ChatType (or a shared interface) keeps the type information intact and the chat typing consistent across the codebase.

Suggested implementation:

  const lastMessageRef = useRef<HTMLDivElement | null>(null);
  const [textareaHeight] = useState("auto");
    const chatMap = new Map<ChatType["remoteJid"], ChatType>();

    // First add all chats from React Query
    chats.forEach((chat: ChatType) => chatMap.set(chat.remoteJid, chat));
  1. Ensure ChatType is imported into src/pages/instance/Chat/index.tsx from the module where it is defined, e.g.:

    import type { ChatType } from "@/types/chat";

    or the correct relative path used elsewhere in the codebase.

  2. If the array chats is not yet typed as ChatType[], update its typing at the source (e.g., React Query hook return type) to ChatType[] so TypeScript can infer the correct type in the forEach without needing an explicit annotation.


// Then add/update with real-time chats
realtimeChats.forEach((chat) => {
Expand Down Expand Up @@ -181,19 +183,19 @@ function Chat() {
<div className="flex h-7 w-7 items-center justify-center rounded-full">
<MessageCircle className="h-4 w-4" />
</div>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">Chat</div>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">{t("chat.title")}</div>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
<Tabs defaultValue="contacts" className="flex flex-col flex-1 min-h-0">
<TabsList className="tabs-chat flex-shrink-0">
<TabsTrigger value="contacts">Contatos</TabsTrigger>
<TabsTrigger value="groups">Grupos</TabsTrigger>
<TabsTrigger value="contacts">{t("chat.contacts")}</TabsTrigger>
<TabsTrigger value="groups">{t("chat.groups")}</TabsTrigger>
</TabsList>
<TabsContent value="contacts" className="flex-1 overflow-hidden">
<div className="h-full overflow-auto">
<div className="grid gap-1 p-2 text-foreground">
<div className="px-2 text-xs font-medium text-muted-foreground">Contatos</div>
<div className="px-2 text-xs font-medium text-muted-foreground">{t("chat.contacts")}</div>
{chats?.map(
(chat: ChatType) =>
chat.remoteJid.includes("@s.whatsapp.net") && (
Expand Down
49 changes: 27 additions & 22 deletions src/pages/instance/Chat/messages.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";

import { DropdownMenu, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { ArrowRightIcon, ChevronDownIcon, SparkleIcon, User, ZapIcon } from "lucide-react";
import { RefObject, useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -32,7 +34,7 @@ type MessagesProps = {
};

// Utility function to format dates like WhatsApp
const formatDateSeparator = (date: Date): string => {
const formatDateSeparator = (date: Date, t: any): string => {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
Comment on lines +37 to 40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Tighten typing for t and consider whether a translation key is the right source for the locale string.

t is currently typed as any and t("chat.date.location") is passed to toLocaleDateString as the locale. Please type t as the translation function from useTranslation (or at least t: (key: string) => string) and consider sourcing the locale from i18n.language or config instead of a translated string, to avoid issues when the key is missing or its value is not a valid locale.

Suggested implementation:

import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";

import { DropdownMenu, DropdownMenuTrigger } from "@radix-ui/react-dropdown_menu";
import { ArrowRightIcon, ChevronDownIcon, SparkleIcon, User, ZapIcon } from "lucide-react";
import { RefObject, useEffect, useMemo, useState } from "react";
};
 // Utility function to format dates like WhatsApp
const formatDateSeparator = (date: Date, t: TFunction, locale: string): string => {
  // Check if it's today
  if (messageDate.toDateString() === today.toDateString()) {
    return t("chat.date.today");
  }

  // Check if it's yesterday
  if (messageDate.toDateString() === yesterday.toDateString()) {
  return messageDate.toLocaleDateString(locale, {

Because only part of the file is shown, you’ll also need to:

  1. Update all call sites of formatDateSeparator to pass the locale explicitly, e.g. where you currently have:

    const { t, i18n } = useTranslation();
    // ...
    const separator = formatDateSeparator(messageDate, t);

    change it to:

    const { t, i18n } = useTranslation();
    // ...
    const separator = formatDateSeparator(messageDate, t, i18n.language);
  2. Ensure the exact SEARCH snippet for toLocaleDateString matches your implementation. If your current code differs slightly (e.g. different options object or spacing), apply the same replacement idea: replace t("chat.date.location") with a locale parameter and pass i18n.language from the caller.

  3. If you use a namespace with useTranslation (e.g. useTranslation("chat")), the type TFunction remains valid, but if you want to be even stricter you can specialise it: TFunction<"chat">. That would require aligning the type with how useTranslation is invoked across the file.

Expand All @@ -41,22 +43,22 @@ const formatDateSeparator = (date: Date): string => {

// Check if it's today
if (messageDate.toDateString() === today.toDateString()) {
return "Hoje";
return t("chat.date.today");
}

// Check if it's yesterday
if (messageDate.toDateString() === yesterday.toDateString()) {
return "Ontem";
return t("chat.date.yesterday");
}

// Check if it's within the last week
const daysDiff = Math.floor((today.getTime() - messageDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysDiff < 7) {
return messageDate.toLocaleDateString("pt-BR", { weekday: "long" });
return messageDate.toLocaleDateString(t("chat.date.location"), { weekday: "long" });
}

// For older dates, show the full date
return messageDate.toLocaleDateString("pt-BR", {
return messageDate.toLocaleDateString(t("chat.date.location"), {
day: "2-digit",
month: "2-digit",
year: "numeric",
Expand Down Expand Up @@ -139,6 +141,8 @@ const getMessageText = (messageObj: any): string => {

// Component to render different message types based on messageType
const MessageContent = ({ message }: { message: Message }) => {
const { t } = useTranslation();

const messageType = message.messageType as string;

switch (messageType) {
Expand All @@ -149,10 +153,10 @@ const MessageContent = ({ message }: { message: Message }) => {
<div className="p-3 bg-muted rounded-lg max-w-xs">
<div className="flex items-center gap-2 mb-2">
<div className="text-xl">πŸ‘€</div>
<span className="font-medium">Contact</span>
<span className="font-medium">{t("chat.contact")}</span>
</div>
{contactMsg.displayName && <p className="text-sm font-medium">{contactMsg.displayName}</p>}
{contactMsg.vcard && <p className="text-xs text-muted-foreground">Contact card</p>}
{contactMsg.vcard && <p className="text-xs text-muted-foreground">{t("chat.contactCard")}</p>}
</div>
);
}
Expand All @@ -163,7 +167,7 @@ const MessageContent = ({ message }: { message: Message }) => {
<div className="p-3 bg-muted rounded-lg max-w-xs">
<div className="flex items-center gap-2 mb-2">
<div className="text-xl">πŸ“</div>
<span className="font-medium">Location</span>
<span className="font-medium">{t("chat.messageType.location.title")}</span>
</div>
{locationMsg.name && <p className="text-sm font-medium">{locationMsg.name}</p>}
{locationMsg.address && <p className="text-xs text-muted-foreground">{locationMsg.address}</p>}
Expand All @@ -173,7 +177,7 @@ const MessageContent = ({ message }: { message: Message }) => {
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm mt-1 inline-block">
View on Maps
{t("chat.messageType.location.viewOnMaps")}
</a>
)}
</div>
Expand Down Expand Up @@ -207,8 +211,8 @@ const MessageContent = ({ message }: { message: Message }) => {
/>
) : (
<div className="rounded bg-muted p-4 max-w-xs">
<p className="text-center text-muted-foreground">Image couldn't be loaded</p>
<p className="text-center text-xs text-muted-foreground mt-1">Missing base64 data and mediaUrl</p>
<p className="text-center text-muted-foreground">{t("chat.messageType.image.notFound")}</p>
<p className="text-center text-xs text-muted-foreground mt-1">{t("chat.media.missingData")}</p>
</div>
)}
{message.message.imageMessage?.caption && <p className="text-sm">{message.message.imageMessage.caption}</p>}
Expand All @@ -235,8 +239,8 @@ const MessageContent = ({ message }: { message: Message }) => {
/>
) : (
<div className="rounded bg-muted p-4 max-w-xs">
<p className="text-center text-muted-foreground">Video couldn't be loaded</p>
<p className="text-center text-xs text-muted-foreground mt-1">Missing base64 data and mediaUrl</p>
<p className="text-center text-muted-foreground">{t("chat.messageType.video.notFound")}</p>
<p className="text-center text-xs text-muted-foreground mt-1">{t("chat.media.missingData")}</p>
</div>
)}
{message.message.videoMessage?.caption && <p className="text-sm">{message.message.videoMessage.caption}</p>}
Expand All @@ -252,12 +256,12 @@ const MessageContent = ({ message }: { message: Message }) => {
return audioSrc ? (
<audio controls className="w-full max-w-xs">
<source src={audioSrc} type="audio/mpeg" />
Your browser does not support the audio element.
{t("chat.messageType.audio.notSupported")}.
</audio>
) : (
<div className="rounded bg-muted p-4 max-w-xs">
<p className="text-center text-muted-foreground">Audio couldn't be loaded</p>
<p className="text-center text-xs text-muted-foreground mt-1">Missing base64 data and mediaUrl</p>
<p className="text-center text-muted-foreground">{t("chat.messageType.audio.notFound")}</p>
<p className="text-center text-xs text-muted-foreground mt-1">{t("chat.media.missingData")}</p>
</div>
);

Expand All @@ -280,7 +284,7 @@ const MessageContent = ({ message }: { message: Message }) => {
return (
<div className="text-xs text-muted-foreground bg-muted p-2 rounded max-w-xs">
<details>
<summary>Unknown message type: {messageType}</summary>
<summary>{t("chat.messageType.unknownMessage")}: {messageType}</summary>
<pre className="mt-2 whitespace-pre-wrap break-all text-xs">{JSON.stringify(message.message, null, 2)}</pre>
</details>
</div>
Expand All @@ -289,6 +293,7 @@ const MessageContent = ({ message }: { message: Message }) => {
};

function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessageRef, scrollToBottom }: MessagesProps) {
const { t } = useTranslation();
const { instance } = useInstance();
const [messageText, setMessageText] = useState("");
const [isSending, setIsSending] = useState(false);
Expand Down Expand Up @@ -416,7 +421,7 @@ function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessa
const messageMap = new Map();

// First add all messages from React Query
messages.forEach((message) => messageMap.set(message.key.id, message));
messages.forEach((message: { key: { id: any; }; }) => messageMap.set(message.key.id, message));

// Then add/update with real-time messages
realtimeMessages.forEach((message) => {
Expand Down Expand Up @@ -523,7 +528,7 @@ function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessa
if (dateString !== currentDate) {
if (currentGroup.length > 0) {
grouped.push({
date: formatDateSeparator(new Date(currentDate)),
date: formatDateSeparator(new Date(currentDate), t),
messages: currentGroup,
});
}
Expand All @@ -536,7 +541,7 @@ function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessa

if (currentGroup.length > 0) {
grouped.push({
date: formatDateSeparator(new Date(currentDate)),
date: formatDateSeparator(new Date(currentDate), t),
messages: currentGroup,
});
}
Expand Down Expand Up @@ -651,7 +656,7 @@ function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessa
<div className="flex items-center rounded-3xl border border-border bg-background px-2 py-1">
{instance && <MediaOptions instance={instance} setSelectedMedia={setSelectedMedia} />}
<Textarea
placeholder="Enviar mensagem..."
placeholder={t("chat.message.placeholder")}
name="message"
id="message"
rows={1}
Expand All @@ -665,7 +670,7 @@ function Messages({ textareaRef, handleTextareaChange, textareaHeight, lastMessa
/>
<Button type="button" size="icon" onClick={sendMessage} disabled={(!messageText.trim() && !selectedMedia) || isSending} className="rounded-full p-2 disabled:opacity-50">
<ArrowRightIcon className="h-6 w-6" />
<span className="sr-only">Enviar</span>
<span className="sr-only">{t("chat.message.send")}</span>
</Button>
</div>
</div>
Expand Down
72 changes: 72 additions & 0 deletions src/translate/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,79 @@
}
},
"chat": {
"title": "Chat",
"contacts": "Contacts",
"contact": "Contact",
"contactCard": "Contact card",
"recent": "Recent",
"noInstance": "No instance selected",
"messageType": {
"location": {
"title": "Location",
"viewOnMaps": "View on Maps"
},
"image": {
"notFound": "Unable to load the image"
},
"video": {
"notFound": "Unable to load the video"
},
"audio": {
"notFound": "Unable to load the audio",
"notSupported": "Your browser does not support audio files"
},
"unknownMessage": "Unknown message type"
},
"groups": "Groups",
"date": {
"today": "Today",
"yesterday": "Yesterday",
"location": "en-US"
},
"message": {
"placeholder": "Type the message",
"options": "Options",
"reply": "Forward",
"delete": "Delete",
"send": "Send"
},
"newChat": {
"title": "New Chat",
"description": "Start a new chat",
"contact": "Contact",
"placeholder": "Type the message",
"submit": "Send"
},
"buttons": {
"preview": {
"title": "Preview",
"buttonPlaceholder": "Preview"
},
"form": {
"validation": {
"titleRequired": "Title required",
"descriptionRequired": "Description required",
"buttonTypeRequired": "Button type required",
"buttonTextRequired": "Button text required",
"minimumOneButton": "At least one button",
"replyIdRequired": "Reply ID required",
"copyTextRequired": "Copy text required",
"urlRequired": "URL required",
"phoneRequired": "Phone number required"
}
}
},
"toast": {
"error": "Error",
"recordingError": "Recording error",
"sendError": "Send error"
},
"window": {
"expired": "Expired",
"expiresIn": "Expires in"
},
"media": {
"missingData": "Missing base64 data and/or media URL",
"attach": "Attach file",
"document": "Document",
"photosAndVideos": "Photos and Videos",
Expand Down
Loading