Idk, quick backup

This commit is contained in:
Alois 2026-03-28 22:10:27 +01:00
commit 396defef64
19 changed files with 519 additions and 108 deletions

View file

@ -12,6 +12,7 @@
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
}, },
"dependencies": { "dependencies": {
"@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/ttp": "workspace:*", "@tensamin/ttp": "workspace:*",

View file

@ -6,7 +6,7 @@ import { Skeleton } from "@tensamin/ui/cmp/skeleton";
export function Basic(props: { user: User }) { export function Basic(props: { user: User }) {
return ( return (
<Card className="animate-in fade-in duration-300 rounded-xl py-0 m-px"> <Card className="animate-in fade-in duration-300 rounded-2xl py-0 m-px">
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2"> <CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
<Avatar> <Avatar>
<AvatarImage src={props.user.avatar} /> <AvatarImage src={props.user.avatar} />
@ -21,5 +21,5 @@ export function Basic(props: { user: User }) {
} }
export function Loading() { export function Loading() {
return <Skeleton className="h-12.5 rounded-xl" />; return <Skeleton className="h-12.5 rounded-2xl" />;
} }

View file

@ -1,64 +1,95 @@
import { Button } from "@tensamin/ui/cmp/button"; import { Button } from "@tensamin/ui/cmp/button";
import { House } from "lucide-react"; import { House, Phone } from "lucide-react";
import { useNavigate, useRouterState } from "@tanstack/react-router"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import * as React from "react"; import { useCall } from "@tensamin/call/context";
import { useUser, type User } from "@tensamin/user/context"; import Wrapper from "@tensamin/user/wrapper";
import { Skeleton } from "@tensamin/ui/cmp/skeleton";
import { useConversation } from "@/features/conversation/context";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
} from "@tensamin/ui/cmp/select";
import { displayCallId } from "@tensamin/call/utils";
/**
* Navigates to the home route when the navbar home button is clicked.
* @param navigate Router navigate function from TanStack Router.
* @returns Void.
*/
function handleHomeButtonClick(navigate: ReturnType<typeof useNavigate>): void {
void navigate({ to: "/" });
}
/**
* Renders the top navigation bar and currently selected conversation user.
* @returns Navbar JSX element.
*/
export default function Navbar() { export default function Navbar() {
const navigate = useNavigate(); const navigate = useNavigate();
const { get } = useUser(); const { joinCall, state } = useCall();
const search = useRouterState({ select: (state) => state.location.search }); const { conversations } = useConversation();
/** const { pathname } = useLocation();
* Delegates navbar home button click to navigation helper. const { id } = useSearch({ strict: false });
* @returns Void.
*/
const onHomeButtonClick = React.useCallback(() => {
handleHomeButtonClick(navigate);
}, [navigate]);
const [user, setUser] = React.useState<User | null>(null); const currentCalls = id ? conversations[id]?.calls || [] : [];
const currentId = React.useMemo(
() => Number((search as Record<string, unknown>).id ?? Number.NaN),
[search],
);
React.useEffect(() => {
if (Number.isNaN(currentId)) {
setUser(null);
return;
}
get(currentId)
.then(setUser)
.catch(() => setUser(null));
}, [currentId, get]);
return ( return (
<div className="w-full h-13.5 flex items-center justify-center"> <div className="w-full h-13.5 flex items-center justify-center">
<Button <Button
onClick={onHomeButtonClick} onClick={() => navigate({ to: "/" })}
className="w-9 h-9 aspect-square rounded-lg" className="w-9 h-9 aspect-square rounded-lg"
variant="outline" variant="outline"
> >
<House className="size-4.5" /> <House className="size-4.5" />
</Button> </Button>
<p className="font-medium pl-3 text-md">{user?.display}</p> {pathname === "/app/chat" && (
<Wrapper
userId={id}
component={(user) => (
<p className="font-medium pl-3 text-md">{user?.display}</p>
)}
loading={<Skeleton className="ml-3 w-40 h-5" />}
/>
)}
<div className="w-full" /> <div className="w-full" />
{pathname === "/app/chat" && id && (
<>
{currentCalls.length > 0 ? (
<Button
disabled={state !== "closed"}
onClick={() => {
joinCall(id);
}}
className="w-9 h-9 aspect-square rounded-lg mr-2"
variant="outline"
>
<Phone />
</Button>
) : currentCalls.length === 1 ? (
<Button
disabled={state !== "closed"}
onClick={() => {
joinCall(id, currentCalls[0]);
}}
className="w-9 h-9 aspect-square rounded-lg mr-2"
>
<Phone />
</Button>
) : (
<Select>
<SelectTrigger
render={
<Button className="w-9 h-9 aspect-square rounded-lg mr-2">
<Phone />
</Button>
}
/>
<SelectContent>
{currentCalls.map((callId) => (
<SelectItem
key={callId}
onSelect={() => {
joinCall(id, callId);
}}
>
{displayCallId(callId)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</>
)}
</div> </div>
); );
} }

View file

@ -18,11 +18,13 @@ export default function Sidebar() {
return ( return (
<div className="w-65 h-full flex flex-col gap-3 p-2"> <div className="w-65 h-full flex flex-col gap-3 p-2">
<Wrapper <div>
loading={<Loading />} <Wrapper
userId={userId} loading={<Loading />}
component={(user) => <Basic user={user} />} userId={userId}
/> component={(user) => <Basic user={user} />}
/>
</div>
<div className="h-full"> <div className="h-full">
<List /> <List />
</div> </div>

View file

@ -15,11 +15,18 @@ import AppLayout from "./routes/app/layout";
import Home from "@/routes/app/home"; import Home from "@/routes/app/home";
import ChatScreen from "@tensamin/chat/screen"; import ChatScreen from "@tensamin/chat/screen";
import ChatContext from "@tensamin/chat/context"; import CallScreen from "@tensamin/call/screen";
import Login from "@/routes/screens/login"; import Login from "@/routes/screens/login";
import Signup from "@/routes/screens/signup"; import Signup from "@/routes/screens/signup";
import ConversationContext from "@/features/conversation/context";
import ChatContext from "@tensamin/chat/context";
import CallContext from "@tensamin/call/context";
import SocketContext from "@tensamin/ttp/context";
import UserContext from "@tensamin/user/context";
import { ThemeProvider } from "@tensamin/ui/theme"; import { ThemeProvider } from "@tensamin/ui/theme";
import z from "zod";
const wrapper = document.getElementById("root"); const wrapper = document.getElementById("root");
@ -33,11 +40,6 @@ window.setLogLevelToMax = () => {
location.reload(); location.reload();
}; };
/**
* Executes RootShell.
* @param none This function has no parameters.
* @returns unknown.
*/
function RootShell() { function RootShell() {
return ( return (
<ThemeProvider> <ThemeProvider>
@ -50,24 +52,22 @@ function RootShell() {
); );
} }
/**
* Executes AppShell.
* @param none This function has no parameters.
* @returns unknown.
*/
function AppShell() { function AppShell() {
return ( return (
<AppLayout> <SocketContext>
<Outlet /> <UserContext>
</AppLayout> <CallContext>
<ConversationContext>
<AppLayout>
<Outlet />
</AppLayout>
</ConversationContext>
</CallContext>
</UserContext>
</SocketContext>
); );
} }
/**
* Executes Chat.
* @param none This function has no parameters.
* @returns unknown.
*/
function Chat() { function Chat() {
return ( return (
<ChatContext> <ChatContext>
@ -77,6 +77,7 @@ function Chat() {
} }
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: RootShell, component: RootShell,
}); });
@ -96,6 +97,15 @@ const chatRoute = createRoute({
getParentRoute: () => appRoute, getParentRoute: () => appRoute,
path: "chat", path: "chat",
component: Chat, component: Chat,
validateSearch: z.object({
id: z.number().optional(),
})
});
const callRoute = createRoute({
getParentRoute: () => appRoute,
path: "call",
component: CallScreen,
}); });
const loginRoute = createRoute({ const loginRoute = createRoute({
@ -111,7 +121,7 @@ const signupRoute = createRoute({
}); });
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
appRoute.addChildren([homeRoute, chatRoute]), appRoute.addChildren([homeRoute, chatRoute, callRoute]),
loginRoute, loginRoute,
signupRoute, signupRoute,
]); ]);

View file

@ -1,10 +1,7 @@
import Socket from "@tensamin/ttp/context";
import User from "@tensamin/user/context";
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/sidebar";
import Conversation from "@/features/conversation/context";
import Navbar from "@/components/navbar"; import Navbar from "@/components/navbar";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
@ -49,20 +46,14 @@ export default function Layout(props: { children: ReactNode }) {
} }
return ( return (
<Socket> <div className="w-full h-full flex bg-sidebar">
<User> <Sidebar />
<Conversation> <div className="w-full h-full flex flex-col">
<div className="w-full h-full flex bg-sidebar"> <Navbar />
<Sidebar /> <div className="bg-background h-full w-full rounded-tl-3xl border-t border-l">
<div className="w-full h-full flex flex-col"> {props.children}
<Navbar /> </div>
<div className="bg-background h-full w-full rounded-tl-3xl border-t border-l"> </div>
{props.children} </div>
</div>
</div>
</div>
</Conversation>
</User>
</Socket>
); );
} }

View file

@ -38,6 +38,7 @@
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-router": "^1.0.0", "@tanstack/react-router": "^1.0.0",
"@tanstack/react-virtual": "^3.0.0", "@tanstack/react-virtual": "^3.0.0",
"@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
@ -72,6 +73,26 @@
"vite": "^7.3.1", "vite": "^7.3.1",
}, },
}, },
"packages/call": {
"name": "@tensamin/call",
"version": "0.0.0",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-router": "^1.0.0",
"@tanstack/react-virtual": "^3.0.0",
"@tensamin/crypto": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/ui": "workspace:*",
"@tensamin/user": "workspace:*",
"lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.3.6",
},
},
"packages/chat": { "packages/chat": {
"name": "@tensamin/chat", "name": "@tensamin/chat",
"version": "0.0.0", "version": "0.0.0",
@ -590,6 +611,8 @@
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.22", "", {}, "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.22", "", {}, "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g=="],
"@tensamin/call": ["@tensamin/call@workspace:packages/call"],
"@tensamin/chat": ["@tensamin/chat@workspace:packages/chat"], "@tensamin/chat": ["@tensamin/chat@workspace:packages/chat"],
"@tensamin/crypto": ["@tensamin/crypto@workspace:packages/crypto"], "@tensamin/crypto": ["@tensamin/crypto@workspace:packages/crypto"],

View file

@ -0,0 +1,32 @@
{
"name": "@tensamin/call",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./screen": "./src/screen.tsx",
"./utils": "./src/utils.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-router": "^1.0.0",
"@tanstack/react-virtual": "^3.0.0",
"@tensamin/crypto": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/user": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.3.6"
}
}

View file

@ -0,0 +1,76 @@
import { createContext, useContext, useEffect, useState } from "react";
import { log } from "@tensamin/shared/log";
import { useNavigate } from "@tanstack/react-router";
export const context = createContext<contextType | undefined>(undefined);
export default function Provider(props: { children: React.ReactNode }) {
const navigate = useNavigate();
const [state, setState] = useState<"closed" | "connecting" | "open">(
"closed",
);
const [callId, setCallId] = useState<string | null>(null);
const [callSecret, setCallSecret] = useState<string | null>(null);
function connect(callId: string, callSecret: string) {
setState("connecting");
setCallId(callId);
setCallSecret(callSecret);
log(2, "Call", "purple", "Connecting to call", { callId, callSecret });
}
// Master reset function
function disconnect() {
setState("closed");
setCallId(null);
setCallSecret(null);
}
// Utils
function joinCall(userId: number, callId?: string) {
// get/generate e2ee secret
// go into convs and find call id
// generate shared secret and decrypt enc call secret
// check if user is already in call
// connect to call
connect("", "");
// navigate to call page
navigate({ to: "/call", search: { userId: userId, callId: callId } });
}
// Event listener for incoming calls
useEffect(() => {}, []);
return (
<context.Provider
value={{
state,
connect,
disconnect,
joinCall,
}}
>
{props.children}
</context.Provider>
);
}
type contextType = {
state: "closed" | "connecting" | "open";
connect: (callId: string, callSecret: string) => void;
disconnect: () => void;
joinCall: (userId: number, callId?: string) => void;
};
export function useCall(): contextType {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useCall must be used within a CallProvider");
}
return ctx;
}

View file

@ -0,0 +1,3 @@
export default function Screen() {
return <div></div>;
}

View file

@ -0,0 +1,3 @@
export function displayCallId(callId: string) {
return callId.slice(0, 4) + "..." + callId.slice(-4);
}

View file

@ -0,0 +1,13 @@
import { z } from "zod";
import { socket } from "@tensamin/shared/data";
export type RawMessages = z.infer<
typeof socket.messages_get.response
>["messages"];
export type RawMessage = RawMessages[number];
export type LiveMessage = RawMessage & {
failed?: boolean;
localId: string;
};

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -21,6 +21,18 @@ const message = z.object({
display: z.boolean().optional(), display: z.boolean().optional(),
}); });
export const failedUser = {
display: "Failed to load user",
iota_id: 0,
omikron_connections: [],
online_status: "user_borked",
public_key: "",
sub_end: 0,
sub_level: 0,
user_id: 0,
username: "unknown",
} as z.infer<typeof socket.get_user_data.response>;
// Socket // Socket
export const socket = { export const socket = {
identification: { identification: {

View file

@ -2,12 +2,11 @@ import { toast as sonnerToast } from "sonner";
import { Ban, Check, Info, TriangleAlert } from "lucide-react"; import { Ban, Check, Info, TriangleAlert } from "lucide-react";
/** /**
* Executes log. * Log Levels
* @param logLevel Parameter logLevel. * - 0: Always logged
* @param logger Parameter logger. * - 1: Errors and warnings
* @param color Parameter color. * - 2: Useful information
* @param args Parameter args. * - 3: Debug messages
* @returns unknown.
*/ */
export function log( export function log(
logLevel: number, logLevel: number,

View file

@ -196,6 +196,7 @@ const DATA_TYPES = [
"timeout", "timeout",
"has_admin", "has_admin",
"last_message_at", "last_message_at",
"height"
] as const; ] as const;
const COMMUNICATION_TYPE_BY_NAME = createIndexMap(COMMUNICATION_TYPES); const COMMUNICATION_TYPE_BY_NAME = createIndexMap(COMMUNICATION_TYPES);
@ -224,6 +225,7 @@ registerDataKinds("number", [
"sub_level", "sub_level",
"sub_end", "sub_end",
"last_message_at", "last_message_at",
"height"
]); ]);
registerDataKinds("string", [ registerDataKinds("string", [
@ -276,7 +278,7 @@ registerDataKinds({ array: "number" }, [
"last_ping", "last_ping",
"ping_iota", "ping_iota",
"get_time", "get_time",
"omikron_connections", "omikron_connections"
]); ]);
registerDataKinds("container", [ registerDataKinds("container", [

View file

@ -0,0 +1,199 @@
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -1,25 +1,26 @@
import * as React from "react"; import { useEffect, useState } from "react";
import { useUser, type User } from "./context"; import { useUser, type User } from "./context";
/** import { failedUser } from "@tensamin/shared/data";
* Executes Wrapper.
* @param props Parameter props. // Wrapper function to pass user data to some component
* @returns unknown.
*/
export default function Wrapper(props: { export default function Wrapper(props: {
userId?: number; userId?: number;
loading: React.ReactNode; loading: React.ReactNode;
component: (user: User) => React.ReactNode; component: (user: User) => React.ReactNode;
}) { }) {
const { get } = useUser(); const { get } = useUser();
const [user, setUser] = React.useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
React.useEffect(() => { useEffect(() => {
if (!props.userId) return; if (!props.userId) {
setUser(failedUser);
return;
}
let active = true; let active = true;
void get(props.userId) get(props.userId)
.then((value) => { .then((value) => {
if (active) { if (active) {
setUser(value); setUser(value);
@ -27,7 +28,7 @@ export default function Wrapper(props: {
}) })
.catch(() => { .catch(() => {
if (active) { if (active) {
setUser(null); setUser(failedUser);
} }
}); });

View file

@ -1,3 +1,4 @@
- Calculate message height and pass to virtualizer (-> packages/chat/src/components/input.tsx) - Calculate message height and pass to virtualizer (-> packages/chat/src/components/input.tsx)
- Split ttp codec into separate file - Split ttp codec into separate file
- Add "Rename Conversations to Friends" option in settings - Add "Rename Conversations to Friends" option in settings
- Handle live messages in notification context