321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
import Wrapper from "@tensamin/user/wrapper";
|
|
import { Basic, Loading } from "./modals/basic";
|
|
import List from "@/features/conversation/list/body";
|
|
|
|
import {
|
|
Sidebar as SidebarRoot,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
useSidebar,
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuGroup,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Input,
|
|
DialogClose,
|
|
Button,
|
|
Label,
|
|
Select,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
SelectContent,
|
|
SelectItem,
|
|
} from "@methanium/ui";
|
|
import { useIsMobile } from "@methanium/ui";
|
|
import { MobileNavbar } from "./navbar";
|
|
|
|
import SidebarBox from "@tensamin/call/sidebarBox";
|
|
import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar";
|
|
import { Ellipsis, Check } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useUser, type User } from "@tensamin/user/context";
|
|
import { mtp, userPresencePreferenceSchema } from "@tensamin/shared/data";
|
|
import { useMTP } from "@tensamin/mtp";
|
|
import {
|
|
onlineStatusLabels,
|
|
onlineStatusOptions,
|
|
type OnlineStatus,
|
|
} from "./status-options";
|
|
|
|
function accountPreference(status: User["OnlineStatus"]): OnlineStatus {
|
|
return userPresencePreferenceSchema.safeParse(status).success
|
|
? (status as OnlineStatus)
|
|
: "user_online";
|
|
}
|
|
|
|
function StatusDialog({
|
|
user,
|
|
open,
|
|
onOpenChange,
|
|
send,
|
|
draftStatus,
|
|
setDraftStatus,
|
|
draftOnlineStatus,
|
|
setDraftOnlineStatus,
|
|
errorMessage,
|
|
setErrorMessage,
|
|
saveSucceeded,
|
|
setSaveSucceeded,
|
|
}: {
|
|
user: Pick<User, "UserId" | "OnlineStatus" | "Status">;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
send: ReturnType<typeof useMTP>["send"];
|
|
draftStatus: string;
|
|
setDraftStatus: (value: string) => void;
|
|
draftOnlineStatus: OnlineStatus;
|
|
setDraftOnlineStatus: (value: OnlineStatus) => void;
|
|
errorMessage: string;
|
|
setErrorMessage: (value: string) => void;
|
|
saveSucceeded: boolean;
|
|
setSaveSucceeded: (value: boolean) => void;
|
|
}) {
|
|
const { updateProfile, updateState } = useUser();
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(nextOpen) => {
|
|
if (!nextOpen) {
|
|
setDraftStatus(user.Status ?? "");
|
|
setDraftOnlineStatus(accountPreference(user.OnlineStatus));
|
|
setErrorMessage("");
|
|
setSaveSucceeded(false);
|
|
}
|
|
|
|
onOpenChange(nextOpen);
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Update Status</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-2">
|
|
<Label htmlFor="user-status">Status message</Label>
|
|
<Input
|
|
id="user-status"
|
|
placeholder="Getting snacks..."
|
|
value={draftStatus}
|
|
onChange={(e) => {
|
|
setSaveSucceeded(false);
|
|
setErrorMessage("");
|
|
setDraftStatus(e.target.value);
|
|
}}
|
|
/>
|
|
<Label htmlFor="user-online-status">Online status</Label>
|
|
<Select
|
|
value={draftOnlineStatus}
|
|
onValueChange={(value) => {
|
|
setSaveSucceeded(false);
|
|
setErrorMessage("");
|
|
setDraftOnlineStatus(value ?? "user_online");
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full" id="user-online-status">
|
|
<SelectValue placeholder="Online">
|
|
{onlineStatusLabels[draftOnlineStatus]}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent className="p-1">
|
|
{onlineStatusOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{errorMessage && (
|
|
<p className="text-sm text-destructive">{errorMessage}</p>
|
|
)}
|
|
<DialogFooter>
|
|
<DialogClose render={<Button variant="destructive">Cancel</Button>} />
|
|
<Button
|
|
onClick={async () => {
|
|
const validation = mtp.ChangeUserData.request.safeParse({
|
|
OnlineStatus: draftOnlineStatus,
|
|
Status: draftStatus,
|
|
});
|
|
|
|
if (!validation.success) {
|
|
setSaveSucceeded(false);
|
|
setErrorMessage(
|
|
validation.error.issues[0]?.message ?? "Invalid status data",
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await send("ChangeUserData", validation.data);
|
|
updateState(user.UserId, draftOnlineStatus);
|
|
await updateProfile(user.UserId, { Status: draftStatus });
|
|
setSaveSucceeded(true);
|
|
setErrorMessage("");
|
|
} catch (err) {
|
|
setSaveSucceeded(false);
|
|
setErrorMessage("Failed to update status: " + err);
|
|
}
|
|
}}
|
|
>
|
|
{saveSucceeded ? (
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<Check className="size-4" />
|
|
Saved
|
|
</span>
|
|
) : (
|
|
"Save"
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
export default function Sidebar() {
|
|
const isMobile = useIsMobile();
|
|
const showMobileNavbar = useShowMobileNavbar();
|
|
const { openMobile, setOpenMobile } = useSidebar();
|
|
const [draftStatus, setDraftStatus] = useState("");
|
|
const [draftOnlineStatus, setDraftOnlineStatus] =
|
|
useState<OnlineStatus>("user_online");
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [statusErrorMessage, setStatusErrorMessage] = useState("");
|
|
const [statusSaveSucceeded, setStatusSaveSucceeded] = useState(false);
|
|
|
|
const { send } = useMTP();
|
|
|
|
const content = (
|
|
<>
|
|
<SidebarContent className="pt-2">
|
|
<div className="h-full w-full flex flex-col gap-3 p-2 pt-0!">
|
|
<div>
|
|
<Wrapper
|
|
loading={<Loading />}
|
|
userId={"own"}
|
|
fields={[
|
|
"UserId",
|
|
"Display",
|
|
"Username",
|
|
"Avatar",
|
|
"OnlineStatus",
|
|
"Status",
|
|
]}
|
|
component={(user) => (
|
|
<>
|
|
<Basic
|
|
user={user}
|
|
extra={
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<button
|
|
type="button"
|
|
aria-label="Open profile menu"
|
|
className="cursor-pointer inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
>
|
|
<Ellipsis />
|
|
</button>
|
|
}
|
|
/>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setDraftStatus(user.Status ?? "");
|
|
setDraftOnlineStatus(
|
|
accountPreference(user.OnlineStatus),
|
|
);
|
|
setStatusErrorMessage("");
|
|
setStatusSaveSucceeded(false);
|
|
setDialogOpen(true);
|
|
}}
|
|
>
|
|
Set Status
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
}
|
|
/>
|
|
<StatusDialog
|
|
user={user}
|
|
open={dialogOpen}
|
|
onOpenChange={(nextOpen) => {
|
|
if (!nextOpen) {
|
|
setDraftStatus(user.Status ?? "");
|
|
setDraftOnlineStatus(
|
|
accountPreference(user.OnlineStatus),
|
|
);
|
|
setStatusErrorMessage("");
|
|
setStatusSaveSucceeded(false);
|
|
}
|
|
|
|
setDialogOpen(nextOpen);
|
|
}}
|
|
send={send}
|
|
draftStatus={draftStatus}
|
|
setDraftStatus={setDraftStatus}
|
|
draftOnlineStatus={draftOnlineStatus}
|
|
setDraftOnlineStatus={setDraftOnlineStatus}
|
|
errorMessage={statusErrorMessage}
|
|
setErrorMessage={setStatusErrorMessage}
|
|
saveSucceeded={statusSaveSucceeded}
|
|
setSaveSucceeded={setStatusSaveSucceeded}
|
|
/>
|
|
</>
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="h-full">
|
|
<List />
|
|
</div>
|
|
</div>
|
|
</SidebarContent>
|
|
{isMobile && !showMobileNavbar && (
|
|
<SidebarFooter className="p-0!">
|
|
<MobileNavbar />
|
|
</SidebarFooter>
|
|
)}
|
|
{!isMobile && (
|
|
<SidebarFooter>
|
|
<SidebarBox />
|
|
</SidebarFooter>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<>
|
|
{openMobile && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/10"
|
|
onClick={() => setOpenMobile(false)}
|
|
/>
|
|
)}
|
|
<div
|
|
data-sidebar="sidebar"
|
|
data-slot="sidebar"
|
|
data-mobile="true"
|
|
className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar pt-[var(--ui-safe-area-top)] pr-[var(--ui-safe-area-right)] pl-[var(--ui-safe-area-left)] text-sidebar-foreground"
|
|
style={{
|
|
transform: openMobile ? "translateX(0)" : "translateX(-100%)",
|
|
opacity: openMobile ? 1 : 0,
|
|
pointerEvents: openMobile ? "auto" : "none",
|
|
}}
|
|
>
|
|
<div className="flex h-full w-full flex-col">{content}</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return <SidebarRoot className="border-0!">{content}</SidebarRoot>;
|
|
}
|