client/apps/web/src/components/sidebar.tsx
Alois 6e977bca7e
Some checks failed
/ build-desktop (linux) (push) Failing after 6m36s
/ build-web (push) Failing after 7m7s
/ build-mobile (push) Failing after 9m21s
/ release (push) Has been skipped
feat(mobile): use drawer for gif picker
fix(mobile): some ui bugs
fix(mobile): disable pinch zooming
2026-08-12 02:16:56 +02:00

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>;
}