feat(reactions): add message reactions API and web quick reactions
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { http } from "./http";
|
||||
import type { Chat, ChatDetail, ChatMember, ChatMemberRole, ChatType, DiscoverChat, Message, MessageType } from "../chat/types";
|
||||
import type { Chat, ChatDetail, ChatMember, ChatMemberRole, ChatType, DiscoverChat, Message, MessageReaction, MessageType } from "../chat/types";
|
||||
import axios from "axios";
|
||||
|
||||
export interface ChatNotificationSettings {
|
||||
@@ -160,6 +160,16 @@ export async function forwardMessage(messageId: number, targetChatId: number): P
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function listMessageReactions(messageId: number): Promise<MessageReaction[]> {
|
||||
const { data } = await http.get<MessageReaction[]>(`/messages/${messageId}/reactions`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function toggleMessageReaction(messageId: number, emoji: string): Promise<MessageReaction[]> {
|
||||
const { data } = await http.post<MessageReaction[]>(`/messages/${messageId}/reactions/toggle`, { emoji });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function pinMessage(chatId: number, messageId: number | null): Promise<Chat> {
|
||||
const { data } = await http.post<Chat>(`/chats/${chatId}/pin`, {
|
||||
message_id: messageId
|
||||
|
||||
@@ -58,6 +58,12 @@ export interface Message {
|
||||
is_pending?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageReaction {
|
||||
emoji: string;
|
||||
count: number;
|
||||
reacted: boolean;
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
id: number;
|
||||
email: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { deleteMessage, forwardMessage, pinMessage } from "../api/chats";
|
||||
import type { Message } from "../chat/types";
|
||||
import { deleteMessage, forwardMessage, listMessageReactions, pinMessage, toggleMessageReaction } from "../api/chats";
|
||||
import type { Message, MessageReaction } from "../chat/types";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import { formatTime } from "../utils/format";
|
||||
@@ -45,6 +45,7 @@ export function MessageList() {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||
const [pendingDelete, setPendingDelete] = useState<PendingDeleteState>(null);
|
||||
const [undoTick, setUndoTick] = useState(0);
|
||||
const [reactionsByMessage, setReactionsByMessage] = useState<Record<number, MessageReaction[]>>({});
|
||||
|
||||
const messages = useMemo(() => {
|
||||
if (!activeChatId) {
|
||||
@@ -97,6 +98,7 @@ export function MessageList() {
|
||||
setCtx(null);
|
||||
setDeleteMessageId(null);
|
||||
setForwardMessageId(null);
|
||||
setReactionsByMessage({});
|
||||
}, [activeChatId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,6 +164,27 @@ export function MessageList() {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureReactionsLoaded(messageId: number) {
|
||||
if (reactionsByMessage[messageId]) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const rows = await listMessageReactions(messageId);
|
||||
setReactionsByMessage((state) => ({ ...state, [messageId]: rows }));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleReaction(messageId: number, emoji: string) {
|
||||
try {
|
||||
const rows = await toggleMessageReaction(messageId, emoji);
|
||||
setReactionsByMessage((state) => ({ ...state, [messageId]: rows }));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelected(messageId: number) {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -314,6 +337,7 @@ export function MessageList() {
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
void ensureReactionsLoaded(message.id);
|
||||
const pos = getSafeContextPosition(e.clientX, e.clientY, 160, 108);
|
||||
setCtx({ x: pos.x, y: pos.y, messageId: message.id });
|
||||
}}
|
||||
@@ -335,6 +359,24 @@ export function MessageList() {
|
||||
</div>
|
||||
) : null}
|
||||
{renderContent(message.type, message.text)}
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{["👍", "❤️", "🔥"].map((emoji) => {
|
||||
const items = reactionsByMessage[message.id] ?? [];
|
||||
const item = items.find((reaction) => reaction.emoji === emoji);
|
||||
return (
|
||||
<button
|
||||
className={`rounded-full border px-2 py-0.5 text-[11px] ${
|
||||
item?.reacted ? "border-sky-300 bg-sky-500/30" : "border-slate-500/60 bg-slate-700/40"
|
||||
}`}
|
||||
key={`${message.id}-${emoji}`}
|
||||
onClick={() => void handleToggleReaction(message.id, emoji)}
|
||||
type="button"
|
||||
>
|
||||
{emoji}{item ? ` ${item.count}` : ""}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className={`mt-1 flex items-center justify-end gap-1 text-[11px] ${own ? "text-slate-900/85" : "text-slate-400"}`}>
|
||||
<span>{formatTime(message.created_at)}</span>
|
||||
{own ? <span className={message.delivery_status === "read" ? "text-cyan-900" : ""}>{renderStatus(message.delivery_status)}</span> : null}
|
||||
|
||||
Reference in New Issue
Block a user