feat(web): add banned users section in chat info moderation
Some checks failed
CI / test (push) Failing after 2m12s
Some checks failed
CI / test (push) Failing after 2m12s
This commit is contained in:
@@ -2,6 +2,7 @@ import { http } from "./http";
|
||||
import type {
|
||||
Chat,
|
||||
ChatAttachment,
|
||||
ChatBan,
|
||||
ChatDetail,
|
||||
ChatInviteLink,
|
||||
ChatMember,
|
||||
@@ -299,6 +300,11 @@ export async function listChatMembers(chatId: number): Promise<ChatMember[]> {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function listChatBans(chatId: number): Promise<ChatBan[]> {
|
||||
const { data } = await http.get<ChatBan[]>(`/chats/${chatId}/bans`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateChatTitle(chatId: number, title: string): Promise<Chat> {
|
||||
const { data } = await http.patch<Chat>(`/chats/${chatId}/title`, { title });
|
||||
return data;
|
||||
|
||||
@@ -48,6 +48,13 @@ export interface ChatMember {
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
export interface ChatBan {
|
||||
chat_id: number;
|
||||
user_id: number;
|
||||
banned_by_user_id: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ChatDetail extends Chat {
|
||||
members: ChatMember[];
|
||||
}
|
||||
|
||||
@@ -9,16 +9,18 @@ import {
|
||||
getChatNotificationSettings,
|
||||
getChatDetail,
|
||||
leaveChat,
|
||||
listChatBans,
|
||||
listChatMembers,
|
||||
requestUploadUrl,
|
||||
removeChatMember,
|
||||
unbanChatMember,
|
||||
updateChatProfile,
|
||||
updateChatNotificationSettings,
|
||||
updateChatMemberRole,
|
||||
uploadToPresignedUrl
|
||||
} from "../api/chats";
|
||||
import { blockUser, getUserById, listBlockedUsers, searchUsers, unblockUser } from "../api/users";
|
||||
import type { AuthUser, ChatAttachment, ChatDetail, ChatMember, Message, UserSearchItem } from "../chat/types";
|
||||
import type { AuthUser, ChatAttachment, ChatBan, ChatDetail, ChatMember, Message, UserSearchItem } from "../chat/types";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import { useUiStore } from "../store/uiStore";
|
||||
@@ -59,6 +61,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
const [linkItems, setLinkItems] = useState<Array<{ url: string; messageId: number; createdAt: string }>>([]);
|
||||
const [attachmentCtx, setAttachmentCtx] = useState<{ x: number; y: number; url: string; messageId: number } | null>(null);
|
||||
const [memberCtx, setMemberCtx] = useState<{ x: number; y: number; member: ChatMember } | null>(null);
|
||||
const [bans, setBans] = useState<ChatBan[]>([]);
|
||||
const [bannedUsers, setBannedUsers] = useState<Record<number, AuthUser>>({});
|
||||
const [attachmentsTab, setAttachmentsTab] = useState<"all" | "photos" | "videos" | "audio" | "links" | "voice">("all");
|
||||
const [mediaViewer, setMediaViewer] = useState<{ items: Array<{ url: string; type: "image" | "video"; messageId: number }>; index: number } | null>(null);
|
||||
const [avatarCropFile, setAvatarCropFile] = useState<File | null>(null);
|
||||
@@ -96,7 +100,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
);
|
||||
const allAttachmentItems = useMemo(() => [...attachments].sort((a, b) => b.id - a.id), [attachments]);
|
||||
|
||||
async function refreshMembers(targetChatId: number) {
|
||||
async function refreshMembers(targetChatId: number): Promise<ChatMember[]> {
|
||||
const nextMembers = await listChatMembers(targetChatId);
|
||||
setMembers(nextMembers);
|
||||
const ids = [...new Set(nextMembers.map((m) => m.user_id))];
|
||||
@@ -106,6 +110,27 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
byId[profile.id] = profile;
|
||||
}
|
||||
setMemberUsers(byId);
|
||||
return nextMembers;
|
||||
}
|
||||
|
||||
async function refreshBans(targetChatId: number, allowFailure = true) {
|
||||
try {
|
||||
const nextBans = await listChatBans(targetChatId);
|
||||
setBans(nextBans);
|
||||
const ids = [...new Set(nextBans.map((item) => item.user_id))];
|
||||
const profiles = await Promise.all(ids.map((id) => getUserById(id)));
|
||||
const byId: Record<number, AuthUser> = {};
|
||||
for (const profile of profiles) {
|
||||
byId[profile.id] = profile;
|
||||
}
|
||||
setBannedUsers(byId);
|
||||
} catch {
|
||||
setBans([]);
|
||||
setBannedUsers({});
|
||||
if (!allowFailure) {
|
||||
throw new Error("failed_to_load_bans");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPanelData(targetChatId: number, withLoading = false) {
|
||||
@@ -134,7 +159,15 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
setCounterpartProfile(null);
|
||||
setCounterpartBlocked(false);
|
||||
}
|
||||
await refreshMembers(targetChatId);
|
||||
const nextMembers = await refreshMembers(targetChatId);
|
||||
const resolvedRole = String(detail.my_role ?? nextMembers.find((m) => m.user_id === me?.id)?.role ?? "").toLowerCase();
|
||||
const canLoadBans = (detail.type === "group" || detail.type === "channel") && (resolvedRole === "owner" || resolvedRole === "admin");
|
||||
if (canLoadBans) {
|
||||
await refreshBans(targetChatId);
|
||||
} else {
|
||||
setBans([]);
|
||||
setBannedUsers({});
|
||||
}
|
||||
const chatAttachments = await getChatAttachments(targetChatId, 120);
|
||||
const messages = await getRecentMessagesForLinks(targetChatId);
|
||||
setAttachments(chatAttachments);
|
||||
@@ -202,6 +235,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
setInviteLink(null);
|
||||
setMembers([]);
|
||||
setMemberUsers({});
|
||||
setBans([]);
|
||||
setBannedUsers({});
|
||||
setSearchQuery("");
|
||||
setSearchResults([]);
|
||||
}, [chatId, open]);
|
||||
@@ -512,6 +547,41 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showMembersSection && canManageMembers ? (
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">Banned users ({bans.length})</p>
|
||||
{bans.length === 0 ? <p className="text-xs text-slate-400">No banned users</p> : null}
|
||||
<div className="tg-scrollbar max-h-44 space-y-1 overflow-auto">
|
||||
{bans.map((ban) => {
|
||||
const user = bannedUsers[ban.user_id];
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded border border-slate-700/60 bg-slate-900/60 px-2 py-1.5" key={`ban-${ban.user_id}`}>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs font-semibold text-slate-200">{user?.name || `user #${ban.user_id}`}</p>
|
||||
<p className="truncate text-[11px] text-slate-400">@{user?.username || "unknown"}</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded bg-slate-700 px-2 py-1 text-[11px] hover:bg-slate-600"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await unbanChatMember(chatId, ban.user_id);
|
||||
await refreshBans(chatId, false);
|
||||
await refreshMembers(chatId);
|
||||
showToast("User unbanned");
|
||||
} catch {
|
||||
setError("Failed to unban user");
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Unban
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">
|
||||
@@ -758,6 +828,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
try {
|
||||
await updateChatMemberRole(chatId, memberCtx.member.user_id, "admin");
|
||||
await refreshMembers(chatId);
|
||||
await refreshBans(chatId);
|
||||
} catch {
|
||||
setError("Failed to update role");
|
||||
} finally {
|
||||
@@ -776,6 +847,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
try {
|
||||
await updateChatMemberRole(chatId, memberCtx.member.user_id, "member");
|
||||
await refreshMembers(chatId);
|
||||
await refreshBans(chatId);
|
||||
} catch {
|
||||
setError("Failed to update role");
|
||||
} finally {
|
||||
@@ -794,6 +866,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
try {
|
||||
await updateChatMemberRole(chatId, memberCtx.member.user_id, "owner");
|
||||
await refreshMembers(chatId);
|
||||
await refreshBans(chatId);
|
||||
} catch {
|
||||
setError("Failed to transfer ownership");
|
||||
} finally {
|
||||
@@ -812,6 +885,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
try {
|
||||
await banChatMember(chatId, memberCtx.member.user_id);
|
||||
await refreshMembers(chatId);
|
||||
await refreshBans(chatId);
|
||||
} catch {
|
||||
setError("Failed to ban member");
|
||||
} finally {
|
||||
@@ -830,6 +904,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
try {
|
||||
await removeChatMember(chatId, memberCtx.member.user_id);
|
||||
await refreshMembers(chatId);
|
||||
await refreshBans(chatId);
|
||||
} catch {
|
||||
setError("Failed to remove member");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user