Completed Admin Users form
This commit is contained in:
@@ -8,6 +8,8 @@ import { dispatchSocket } from "dispatch/socket";
|
||||
import { Mission, NotificationPayload } from "@repo/db";
|
||||
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||
import { useMapStore } from "_store/mapStore";
|
||||
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
|
||||
import { pilotSocket } from "pilot/socket";
|
||||
|
||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
const mapStore = useMapStore((s) => s);
|
||||
@@ -39,6 +41,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["aircrafts"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["dispatchers"],
|
||||
});
|
||||
};
|
||||
|
||||
const invalidateConenctedAircrafts = () => {
|
||||
@@ -58,13 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
toast.custom(
|
||||
(t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />,
|
||||
{
|
||||
duration: 9999,
|
||||
duration: 99999,
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
case "admin-message":
|
||||
toast.custom((t) => <AdminMessageToast event={notification} t={t} />, {
|
||||
duration: 999999,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast(notification.message);
|
||||
toast("unbekanntes Notification-Event");
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -76,6 +86,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
||||
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||
dispatchSocket.on("notification", handleNotification);
|
||||
pilotSocket.on("notification", handleNotification);
|
||||
|
||||
return () => {
|
||||
dispatchSocket.off("update-mission", invalidateMission);
|
||||
|
||||
34
apps/dispatch/app/_components/customToasts/AdminMessage.tsx
Normal file
34
apps/dispatch/app/_components/customToasts/AdminMessage.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AdminMessage } from "@repo/db";
|
||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||
import { cn } from "_helpers/cn";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import toast, { Toast } from "react-hot-toast";
|
||||
|
||||
export const AdminMessageToast = ({ event, t }: { event: AdminMessage; t: Toast }) => {
|
||||
const handleClick = () => {
|
||||
toast.dismiss(t.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNotification icon={<TriangleAlert />} className="flex flex-row">
|
||||
<div className="flex-1">
|
||||
<h1
|
||||
className={cn(
|
||||
"font-bold",
|
||||
event.status == "ban" && "text-red-500 ",
|
||||
event.status == "kick" && "text-yellow-500 ",
|
||||
)}
|
||||
>
|
||||
Du wurdes durch den Admin {event.data?.admin.publicId}{" "}
|
||||
{event.status == "ban" ? "gebannt" : "gekickt"}!
|
||||
</h1>
|
||||
<p>{event.message}</p>
|
||||
</div>
|
||||
<div className="ml-11">
|
||||
<button className="btn" onClick={handleClick}>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</BaseNotification>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NotificationPayload } from "@repo/db";
|
||||
import { NotificationPayload, ValidationFailed, ValidationSuccess } from "@repo/db";
|
||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||
import { MapStore, useMapStore } from "_store/mapStore";
|
||||
import { Check, Cross } from "lucide-react";
|
||||
@@ -9,7 +9,7 @@ export const HPGnotificationToast = ({
|
||||
t,
|
||||
mapStore,
|
||||
}: {
|
||||
event: NotificationPayload;
|
||||
event: ValidationFailed | ValidationSuccess;
|
||||
t: Toast;
|
||||
mapStore: MapStore;
|
||||
}) => {
|
||||
|
||||
@@ -1,21 +1,102 @@
|
||||
"use client";
|
||||
import { PublicUser } from "@repo/db";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getConnectedAircraftsAPI, kickAircraftAPI } from "_querys/aircrafts";
|
||||
import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/connected-user";
|
||||
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
|
||||
import { editUserAPI } from "_querys/user";
|
||||
import { ParticipantInfo } from "livekit-server-sdk";
|
||||
import {
|
||||
ArrowLeftRight,
|
||||
Eye,
|
||||
LockKeyhole,
|
||||
Plane,
|
||||
RedoDot,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
Speaker,
|
||||
User,
|
||||
UserCheck,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function AdminPanel() {
|
||||
const path = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: pilots } = useQuery({
|
||||
queryKey: ["pilots"],
|
||||
queryFn: () => getConnectedAircraftsAPI(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
const { data: dispatcher } = useQuery({
|
||||
queryKey: ["dispatcher"],
|
||||
|
||||
queryFn: () => getConnectedDispatcherAPI(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["connected-audio-users"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
const kickLivekitParticipantMutation = useMutation({
|
||||
mutationFn: kickLivekitParticipant,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["connected-audio-users"] });
|
||||
},
|
||||
});
|
||||
const editUSerMutation = useMutation({
|
||||
mutationFn: editUserAPI,
|
||||
});
|
||||
const kickPilotMutation = useMutation({
|
||||
mutationFn: kickAircraftAPI,
|
||||
onSuccess: () => {
|
||||
toast.success("Pilot wurde erfolgreich gekickt");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["aircrafts"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["connected-audio-users"],
|
||||
});
|
||||
},
|
||||
});
|
||||
const kickDispatchMutation = useMutation({
|
||||
mutationFn: kickDispatcherAPI,
|
||||
onSuccess: () => {
|
||||
toast.success("Disponent wurde erfolgreich gekickt");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["dispatcher"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["connected-audio-users"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const participants: { participant: ParticipantInfo; room: string }[] = [];
|
||||
|
||||
if (livekitRooms) {
|
||||
livekitRooms?.forEach((room) => {
|
||||
room.participants.forEach((participant) => {
|
||||
participants.push({
|
||||
participant,
|
||||
room: room.room.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const livekitUserNotConnected = participants.filter((p) => {
|
||||
const pilot = pilots?.find(
|
||||
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
||||
);
|
||||
const fDispatcher = dispatcher?.find(
|
||||
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
||||
);
|
||||
return !pilot && !fDispatcher;
|
||||
});
|
||||
|
||||
console.log("Livekit Rooms", livekitRooms);
|
||||
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
return (
|
||||
@@ -29,7 +110,7 @@ export default function AdminPanel() {
|
||||
>
|
||||
<Shield size={18} /> Admin Panel
|
||||
</button>
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<dialog ref={modalRef} className="modal min-w-[500px]">
|
||||
<div className="modal-box w-11/12 max-w-7xl">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
@@ -50,203 +131,171 @@ export default function AdminPanel() {
|
||||
<th>Name</th>
|
||||
<th>Station</th>
|
||||
<th>Voice</th>
|
||||
<th>Dispatch</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VAR0124</td>
|
||||
<td>Max Mustermann</td>
|
||||
<td>Christoph 31</td>
|
||||
<td className="text-error">
|
||||
<span>Nicht verbunden</span>
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<span>Verbunden</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{pilots?.map((p) => {
|
||||
const publicUser = p.publicUser as unknown as PublicUser;
|
||||
const livekitParticipant = participants.find(
|
||||
(p) => p.participant.identity === publicUser.publicId,
|
||||
);
|
||||
return (
|
||||
<tr key={p.id}>
|
||||
<td className="flex items-center gap-2">
|
||||
<Plane /> {publicUser.publicId}
|
||||
</td>
|
||||
<td>{publicUser.fullName}</td>
|
||||
<td>{p.Station.bosCallsign}</td>
|
||||
<td>
|
||||
{!livekitParticipant ? (
|
||||
<span className="text-error">Nicht verbunden</span>
|
||||
) : (
|
||||
<span className="text-success">{livekitParticipant.room}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
onClick={() => kickPilotMutation.mutate({ id: p.id })}
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
onClick={() => {
|
||||
kickPilotMutation.mutate({ id: p.id, bann: true });
|
||||
}}
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{dispatcher?.map((d) => {
|
||||
const publicUser = d.publicUser as unknown as PublicUser;
|
||||
const livekitParticipant = participants.find(
|
||||
(p) => p.participant.identity === publicUser.publicId,
|
||||
);
|
||||
return (
|
||||
<tr key={d.id}>
|
||||
<td className="flex items-center gap-2">
|
||||
<Workflow /> {publicUser.publicId}
|
||||
</td>
|
||||
<td>{publicUser.fullName}</td>
|
||||
<td>{d.zone}</td>
|
||||
<td>
|
||||
{!livekitParticipant ? (
|
||||
<span className="text-error">Nicht verbunden</span>
|
||||
) : (
|
||||
<span className="text-success">{livekitParticipant.room}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
onClick={() => kickDispatchMutation.mutate({ id: d.id })}
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
onClick={() => {
|
||||
kickDispatchMutation.mutate({ id: d.id, bann: true });
|
||||
}}
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${d.userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{livekitUserNotConnected.map((p) => {
|
||||
const publicUser = JSON.parse(
|
||||
p.participant.attributes.publicUser || "{}",
|
||||
) as PublicUser;
|
||||
return (
|
||||
<tr key={p.participant.identity}>
|
||||
<td className="flex items-center gap-2">
|
||||
<Speaker /> {p.participant.identity}
|
||||
</td>
|
||||
<td>{publicUser?.fullName}</td>
|
||||
<td>
|
||||
<span className="text-error">Nicht verbunden</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="text-success">{p.room}</span>
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
onClick={() =>
|
||||
kickLivekitParticipantMutation.mutate({
|
||||
roomName: p.room,
|
||||
identity: p.participant.identity,
|
||||
})
|
||||
}
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||
data-tip="Profil"
|
||||
>
|
||||
<User size={15} />
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
|
||||
{/* <div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
|
||||
<div className="card-body">
|
||||
<div className="card-title flex items-center gap-2">
|
||||
<ShieldAlert size={20} /> Allgemeine Befehle
|
||||
@@ -266,7 +315,7 @@ export default function AdminPanel() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
<button>close</button>
|
||||
|
||||
@@ -19,8 +19,7 @@ export const SettingsBtn = () => {
|
||||
const testSoundRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const editUserMutation = useMutation({
|
||||
mutationFn: ({ user }: { user: Prisma.UserUpdateInput }) =>
|
||||
editUserAPI(session.data!.user.id, user),
|
||||
mutationFn: editUserAPI,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -201,7 +200,8 @@ export const SettingsBtn = () => {
|
||||
onSubmit={() => false}
|
||||
onClick={async () => {
|
||||
testSoundRef.current?.pause();
|
||||
const res = await editUserMutation.mutateAsync({
|
||||
await editUserMutation.mutateAsync({
|
||||
id: session.data!.user.id,
|
||||
user: {
|
||||
settingsMicDevice: selectedDevice,
|
||||
settingsMicVolume: micVol,
|
||||
|
||||
Reference in New Issue
Block a user