feat: add reply msg
This commit is contained in:
+119
-36
@@ -1,5 +1,10 @@
|
||||
import { useWAClient } from "./context";
|
||||
import { getFilesAsBase64, getFormattedName, getId } from "@/lib/utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
getFilesAsBase64,
|
||||
getFormattedName,
|
||||
getId,
|
||||
} from "@/lib/utils";
|
||||
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { cn } from "cn";
|
||||
import {
|
||||
@@ -7,11 +12,13 @@ import {
|
||||
CheckCheckIcon,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
CopyIcon,
|
||||
CornerUpRightIcon,
|
||||
Loader2Icon,
|
||||
MousePointer2Icon,
|
||||
PaperclipIcon,
|
||||
PhoneCallIcon,
|
||||
ReplyIcon,
|
||||
SendIcon,
|
||||
Trash2Icon,
|
||||
XIcon,
|
||||
@@ -39,6 +46,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { forwardDlg } from "./stores";
|
||||
import type { ContactDTO, MessageDTO } from "@shared/dto";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -47,6 +55,11 @@ type Props = {
|
||||
maxCount?: number;
|
||||
};
|
||||
|
||||
type MessageDTOWithStrID = Omit<MessageDTO, "id" | "from"> & {
|
||||
id: string;
|
||||
from?: ContactDTO;
|
||||
};
|
||||
|
||||
export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
const { client, isReady } = useWAClient();
|
||||
const router = useHashRouter();
|
||||
@@ -58,6 +71,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
const { data: contacts } = useContacts();
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string> | null>(null);
|
||||
const [replyTo, setReplyTo] = useState<MessageDTOWithStrID | null>(null);
|
||||
|
||||
const {
|
||||
data: messages,
|
||||
@@ -153,6 +167,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
const quotedMsg = i.quotedMsg
|
||||
? {
|
||||
...i.quotedMsg,
|
||||
fromMe: i.fromMe,
|
||||
from: contacts?.find((c) => {
|
||||
return (
|
||||
getId(c.id) ===
|
||||
@@ -349,10 +364,31 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
<ChevronDown />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setReplyTo(item as never)}>
|
||||
<ReplyIcon />
|
||||
Reply
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={() => onSelectMsg(item.id)}>
|
||||
<MousePointer2Icon />
|
||||
Select
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
item.caption ||
|
||||
(item.type === "chat" ? item.body : null) ||
|
||||
"",
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIcon />
|
||||
Copy
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
forwardDlg.onOpen({ messageIds: [item.id] })
|
||||
@@ -361,6 +397,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
<CornerUpRightIcon />
|
||||
Forward
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onDeleteMsg(item.id)}>
|
||||
<Trash2Icon />
|
||||
@@ -383,39 +420,11 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
["chat", "image", "document"].includes(
|
||||
item.quotedMsg.type as string,
|
||||
) ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const id = getId(item.quotedMsgId).replace(
|
||||
/^(?:false_|true_)/,
|
||||
"",
|
||||
);
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"bg-card/30 px-2 py-1.5 rounded-lg mb-2 mt-1 cursor-pointer block text-left",
|
||||
!item.fromMe && "bg-[#d8f5e8]/30 dark:bg-[#26855c]/30",
|
||||
)}
|
||||
>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getFormattedName(item.quotedMsg.from)}
|
||||
</p>
|
||||
|
||||
<Paragraph
|
||||
text={
|
||||
item.quotedMsg.filename ||
|
||||
item.quotedMsg.caption ||
|
||||
(item.quotedMsg.type === "chat"
|
||||
? item.quotedMsg.body
|
||||
: "Quoted")
|
||||
}
|
||||
getMentionLabel={getMentionLabel}
|
||||
onMentionClick={onMentionClick}
|
||||
/>
|
||||
</button>
|
||||
<QuotedMsg
|
||||
data={item.quotedMsg as never}
|
||||
getMentionLabel={getMentionLabel}
|
||||
onMentionClick={onMentionClick}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.type === "image" ||
|
||||
@@ -501,17 +510,72 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MessageInputSection chatId={chatId} refetch={refetch} />
|
||||
<MessageInputSection
|
||||
chatId={chatId}
|
||||
refetch={refetch}
|
||||
replyTo={replyTo}
|
||||
setReplyTo={setReplyTo}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function QuotedMsg({
|
||||
data,
|
||||
getMentionLabel,
|
||||
onMentionClick,
|
||||
className,
|
||||
}: {
|
||||
data: MessageDTOWithStrID;
|
||||
getMentionLabel?: (username: string) => string;
|
||||
onMentionClick?: (username: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const id = getId(data.id).replace(/^(?:false_|true_)/, "");
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"bg-card/30 px-2 py-1.5 rounded-lg mb-2 mt-1 cursor-pointer block text-left",
|
||||
!data.fromMe && "bg-[#d8f5e8]/30 dark:bg-[#26855c]/30",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getFormattedName(data.from)}
|
||||
</p>
|
||||
|
||||
<Paragraph
|
||||
text={
|
||||
data.filename ||
|
||||
data.caption ||
|
||||
(data.type === "chat" ? data.body : "Quoted")
|
||||
}
|
||||
getMentionLabel={getMentionLabel}
|
||||
onMentionClick={onMentionClick}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageInputSection({
|
||||
chatId,
|
||||
refetch,
|
||||
replyTo,
|
||||
setReplyTo,
|
||||
getMentionLabel,
|
||||
}: {
|
||||
chatId?: string | null;
|
||||
refetch: () => void;
|
||||
replyTo?: MessageDTOWithStrID | null;
|
||||
setReplyTo?: (id: MessageDTOWithStrID | null) => void;
|
||||
getMentionLabel?: (username: string) => string;
|
||||
}) {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const { client, isReady } = useWAClient();
|
||||
@@ -559,7 +623,7 @@ function MessageInputSection({
|
||||
|
||||
try {
|
||||
setPending(true);
|
||||
const options = { waitForAck: true };
|
||||
const options = { waitForAck: true, quotedMsg: replyTo?.id || undefined };
|
||||
|
||||
if (files.length > 0) {
|
||||
const fileList = await getFilesAsBase64(files.map((f) => f.file));
|
||||
@@ -583,6 +647,7 @@ function MessageInputSection({
|
||||
setTimeout(refetch, 1000);
|
||||
setFiles([]);
|
||||
setMessage("");
|
||||
setReplyTo?.(null);
|
||||
} catch (err) {
|
||||
alert((err as Error).message);
|
||||
} finally {
|
||||
@@ -596,6 +661,24 @@ function MessageInputSection({
|
||||
|
||||
return (
|
||||
<div className="border-t border-border bg-card px-3 py-3 sm:py-4">
|
||||
{replyTo != null && (
|
||||
<div className="flex items-center gap-1 -mt-2 mb-1">
|
||||
<ReplyIcon className="size-5" />
|
||||
<QuotedMsg
|
||||
data={replyTo}
|
||||
getMentionLabel={getMentionLabel}
|
||||
className="text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setReplyTo?.(null)}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileRef}
|
||||
|
||||
@@ -59,3 +59,13 @@ export function createDisclosure<T = any>(data?: T | null) {
|
||||
useStore: () => useStore(store),
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user