sync: phase 1 backend + frontend from server
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useServerStore } from "../stores/server.ts";
|
||||
import { useAuthStore } from "../stores/auth.ts";
|
||||
import { useMemberStore, type Member } from "../stores/member.ts";
|
||||
import { usePresenceStore } from "../stores/presence.ts";
|
||||
import type { UserStatus } from "../stores/auth.ts";
|
||||
import { MemberContextMenu } from "./MemberContextMenu.tsx";
|
||||
import { useConversationStore } from "../stores/conversation.ts";
|
||||
|
||||
function statusIcon(status: UserStatus): string {
|
||||
switch (status) {
|
||||
@@ -46,13 +49,63 @@ function usernameColor(status: UserStatus): string {
|
||||
function MemberRow({ member }: { member: Member }) {
|
||||
const presence = usePresenceStore((s) => s.presences[member.id]);
|
||||
const status = presence?.status ?? member.status;
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const currentUser = useAuthStore((s) => s.user);
|
||||
const userPerms = useServerStore((s) => s.userPermissions);
|
||||
const activeServerId = useServerStore((s) => s.activeServerId);
|
||||
const createConversation = useConversationStore((s) => s.createConversation);
|
||||
|
||||
const canKick = userPerms?.kick_members ?? false;
|
||||
const canBan = userPerms?.ban_members ?? false;
|
||||
const canMute = userPerms?.mute_members ?? false;
|
||||
const isSelf = currentUser?.id === member.id;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-2 py-1">
|
||||
<span className={statusColor(status)}>{statusIcon(status)}</span>
|
||||
<span className={`truncate ${usernameColor(status)}`}>
|
||||
{member.display_name || member.username}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1 text-left hover:bg-gb-bg-t"
|
||||
>
|
||||
<span className={statusColor(status)}>{statusIcon(status)}</span>
|
||||
<span className={`truncate ${usernameColor(status)}`}>
|
||||
{member.display_name || member.username}
|
||||
</span>
|
||||
<span className="ml-auto text-gb-fg-f text-xs">[...]</span>
|
||||
</button>
|
||||
{menuOpen && !isSelf && activeServerId && (
|
||||
<MemberContextMenu
|
||||
memberId={member.id}
|
||||
username={member.username}
|
||||
canKick={canKick}
|
||||
canBan={canBan}
|
||||
canMute={canMute}
|
||||
onMessage={() => {
|
||||
createConversation([member.id]);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
onKick={(_reason) => {
|
||||
fetch(`/api/v1/servers/${activeServerId}/members/${member.id}`, { method: "DELETE", credentials: "include" })
|
||||
.then(() => setMenuOpen(false));
|
||||
}}
|
||||
onBan={(reason) => {
|
||||
fetch(`/api/v1/servers/${activeServerId}/bans`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: member.id, reason }),
|
||||
}).then(() => setMenuOpen(false));
|
||||
}}
|
||||
onMute={(duration, reason) => {
|
||||
fetch(`/api/v1/servers/${activeServerId}/mutes`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: member.id, duration, reason }),
|
||||
}).then(() => setMenuOpen(false));
|
||||
}}
|
||||
onClose={() => setMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user