feat(privacy): user blocklist with private chat enforcement
Some checks failed
CI / test (push) Failing after 21s
Some checks failed
CI / test (push) Failing after 21s
- add blocked_users table and migration - add users API: block, unblock, list blocked users - prevent private chat creation and private messaging when block relation exists - add block/unblock action in private chat info panel
This commit is contained in:
@@ -24,3 +24,16 @@ export async function getUserById(userId: number): Promise<AuthUser> {
|
||||
const { data } = await http.get<AuthUser>(`/users/${userId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function listBlockedUsers(): Promise<UserSearchItem[]> {
|
||||
const { data } = await http.get<UserSearchItem[]>("/users/blocked");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function blockUser(userId: number): Promise<void> {
|
||||
await http.post(`/users/${userId}/block`);
|
||||
}
|
||||
|
||||
export async function unblockUser(userId: number): Promise<void> {
|
||||
await http.delete(`/users/${userId}/block`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
updateChatMemberRole,
|
||||
updateChatTitle
|
||||
} from "../api/chats";
|
||||
import { getUserById, searchUsers } from "../api/users";
|
||||
import { blockUser, getUserById, listBlockedUsers, searchUsers, unblockUser } from "../api/users";
|
||||
import type { AuthUser, ChatDetail, ChatMember, UserSearchItem } from "../chat/types";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
@@ -37,6 +37,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
const [searchResults, setSearchResults] = useState<UserSearchItem[]>([]);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [savingMute, setSavingMute] = useState(false);
|
||||
const [counterpartBlocked, setCounterpartBlocked] = useState(false);
|
||||
const [savingBlock, setSavingBlock] = useState(false);
|
||||
|
||||
const myRole = useMemo(() => members.find((m) => m.user_id === me?.id)?.role, [members, me?.id]);
|
||||
const isGroupLike = chat?.type === "group" || chat?.type === "channel";
|
||||
@@ -73,6 +75,14 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
if (!cancelled) {
|
||||
setMuted(notificationSettings.muted);
|
||||
}
|
||||
if (detail.type === "private" && !detail.is_saved && detail.counterpart_user_id) {
|
||||
const blocked = await listBlockedUsers();
|
||||
if (!cancelled) {
|
||||
setCounterpartBlocked(blocked.some((u) => u.id === detail.counterpart_user_id));
|
||||
}
|
||||
} else if (!cancelled) {
|
||||
setCounterpartBlocked(false);
|
||||
}
|
||||
await refreshMembers(chatId);
|
||||
} catch {
|
||||
if (!cancelled) setError("Failed to load chat info");
|
||||
@@ -282,6 +292,31 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{chat.type === "private" && !chat.is_saved && chat.counterpart_user_id ? (
|
||||
<button
|
||||
className="mb-3 w-full rounded bg-slate-700 px-3 py-2 text-sm disabled:opacity-60"
|
||||
disabled={savingBlock}
|
||||
onClick={async () => {
|
||||
setSavingBlock(true);
|
||||
try {
|
||||
if (counterpartBlocked) {
|
||||
await unblockUser(chat.counterpart_user_id!);
|
||||
setCounterpartBlocked(false);
|
||||
} else {
|
||||
await blockUser(chat.counterpart_user_id!);
|
||||
setCounterpartBlocked(true);
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to update block status");
|
||||
} finally {
|
||||
setSavingBlock(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{counterpartBlocked ? "Unblock user" : "Block user"}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{showMembersSection && (chat.type === "group" || chat.type === "channel") ? (
|
||||
<button
|
||||
className="w-full rounded bg-slate-700 px-3 py-2 text-sm"
|
||||
|
||||
Reference in New Issue
Block a user