Idk, quick backup
This commit is contained in:
parent
04f37be8f6
commit
396defef64
19 changed files with 519 additions and 108 deletions
|
|
@ -12,6 +12,7 @@
|
|||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensamin/call": "workspace:*",
|
||||
"@tensamin/chat": "workspace:*",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Skeleton } from "@tensamin/ui/cmp/skeleton";
|
|||
|
||||
export function Basic(props: { user: User }) {
|
||||
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">
|
||||
<Avatar>
|
||||
<AvatarImage src={props.user.avatar} />
|
||||
|
|
@ -21,5 +21,5 @@ export function Basic(props: { user: User }) {
|
|||
}
|
||||
|
||||
export function Loading() {
|
||||
return <Skeleton className="h-12.5 rounded-xl" />;
|
||||
return <Skeleton className="h-12.5 rounded-2xl" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,95 @@
|
|||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
import { House } from "lucide-react";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import * as React from "react";
|
||||
import { useUser, type User } from "@tensamin/user/context";
|
||||
import { House, Phone } from "lucide-react";
|
||||
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useCall } from "@tensamin/call/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() {
|
||||
const navigate = useNavigate();
|
||||
const { get } = useUser();
|
||||
const search = useRouterState({ select: (state) => state.location.search });
|
||||
const { joinCall, state } = useCall();
|
||||
const { conversations } = useConversation();
|
||||
|
||||
/**
|
||||
* Delegates navbar home button click to navigation helper.
|
||||
* @returns Void.
|
||||
*/
|
||||
const onHomeButtonClick = React.useCallback(() => {
|
||||
handleHomeButtonClick(navigate);
|
||||
}, [navigate]);
|
||||
const { pathname } = useLocation();
|
||||
const { id } = useSearch({ strict: false });
|
||||
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
|
||||
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]);
|
||||
const currentCalls = id ? conversations[id]?.calls || [] : [];
|
||||
|
||||
return (
|
||||
<div className="w-full h-13.5 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={onHomeButtonClick}
|
||||
onClick={() => navigate({ to: "/" })}
|
||||
className="w-9 h-9 aspect-square rounded-lg"
|
||||
variant="outline"
|
||||
>
|
||||
<House className="size-4.5" />
|
||||
</Button>
|
||||
{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" />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ export default function Sidebar() {
|
|||
|
||||
return (
|
||||
<div className="w-65 h-full flex flex-col gap-3 p-2">
|
||||
<div>
|
||||
<Wrapper
|
||||
loading={<Loading />}
|
||||
userId={userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<List />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,11 +15,18 @@ import AppLayout from "./routes/app/layout";
|
|||
|
||||
import Home from "@/routes/app/home";
|
||||
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 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 z from "zod";
|
||||
|
||||
const wrapper = document.getElementById("root");
|
||||
|
||||
|
|
@ -33,11 +40,6 @@ window.setLogLevelToMax = () => {
|
|||
location.reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes RootShell.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function RootShell() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
|
|
@ -50,24 +52,22 @@ function RootShell() {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes AppShell.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function AppShell() {
|
||||
return (
|
||||
<SocketContext>
|
||||
<UserContext>
|
||||
<CallContext>
|
||||
<ConversationContext>
|
||||
<AppLayout>
|
||||
<Outlet />
|
||||
</AppLayout>
|
||||
</ConversationContext>
|
||||
</CallContext>
|
||||
</UserContext>
|
||||
</SocketContext>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Chat.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function Chat() {
|
||||
return (
|
||||
<ChatContext>
|
||||
|
|
@ -77,6 +77,7 @@ function Chat() {
|
|||
}
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
|
||||
component: RootShell,
|
||||
});
|
||||
|
||||
|
|
@ -96,6 +97,15 @@ const chatRoute = createRoute({
|
|||
getParentRoute: () => appRoute,
|
||||
path: "chat",
|
||||
component: Chat,
|
||||
validateSearch: z.object({
|
||||
id: z.number().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
const callRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: "call",
|
||||
component: CallScreen,
|
||||
});
|
||||
|
||||
const loginRoute = createRoute({
|
||||
|
|
@ -111,7 +121,7 @@ const signupRoute = createRoute({
|
|||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
appRoute.addChildren([homeRoute, chatRoute]),
|
||||
appRoute.addChildren([homeRoute, chatRoute, callRoute]),
|
||||
loginRoute,
|
||||
signupRoute,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import Socket from "@tensamin/ttp/context";
|
||||
import User from "@tensamin/user/context";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import Conversation from "@/features/conversation/context";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
|
||||
|
|
@ -49,9 +46,6 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Socket>
|
||||
<User>
|
||||
<Conversation>
|
||||
<div className="w-full h-full flex bg-sidebar">
|
||||
<Sidebar />
|
||||
<div className="w-full h-full flex flex-col">
|
||||
|
|
@ -61,8 +55,5 @@ export default function Layout(props: { children: ReactNode }) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conversation>
|
||||
</User>
|
||||
</Socket>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
23
bun.lock
23
bun.lock
|
|
@ -38,6 +38,7 @@
|
|||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tanstack/react-virtual": "^3.0.0",
|
||||
"@tensamin/call": "workspace:*",
|
||||
"@tensamin/chat": "workspace:*",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
|
|
@ -72,6 +73,26 @@
|
|||
"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": {
|
||||
"name": "@tensamin/chat",
|
||||
"version": "0.0.0",
|
||||
|
|
@ -590,6 +611,8 @@
|
|||
|
||||
"@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/crypto": ["@tensamin/crypto@workspace:packages/crypto"],
|
||||
|
|
|
|||
32
packages/call/package.json
Normal file
32
packages/call/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
76
packages/call/src/context.tsx
Normal file
76
packages/call/src/context.tsx
Normal 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;
|
||||
}
|
||||
3
packages/call/src/screen.tsx
Normal file
3
packages/call/src/screen.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function Screen() {
|
||||
return <div></div>;
|
||||
}
|
||||
3
packages/call/src/utils.ts
Normal file
3
packages/call/src/utils.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function displayCallId(callId: string) {
|
||||
return callId.slice(0, 4) + "..." + callId.slice(-4);
|
||||
}
|
||||
13
packages/call/src/values.ts
Normal file
13
packages/call/src/values.ts
Normal 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;
|
||||
};
|
||||
12
packages/call/tsconfig.json
Normal file
12
packages/call/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -21,6 +21,18 @@ const message = z.object({
|
|||
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
|
||||
export const socket = {
|
||||
identification: {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ import { toast as sonnerToast } from "sonner";
|
|||
import { Ban, Check, Info, TriangleAlert } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Executes log.
|
||||
* @param logLevel Parameter logLevel.
|
||||
* @param logger Parameter logger.
|
||||
* @param color Parameter color.
|
||||
* @param args Parameter args.
|
||||
* @returns unknown.
|
||||
* Log Levels
|
||||
* - 0: Always logged
|
||||
* - 1: Errors and warnings
|
||||
* - 2: Useful information
|
||||
* - 3: Debug messages
|
||||
*/
|
||||
export function log(
|
||||
logLevel: number,
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ const DATA_TYPES = [
|
|||
"timeout",
|
||||
"has_admin",
|
||||
"last_message_at",
|
||||
"height"
|
||||
] as const;
|
||||
|
||||
const COMMUNICATION_TYPE_BY_NAME = createIndexMap(COMMUNICATION_TYPES);
|
||||
|
|
@ -224,6 +225,7 @@ registerDataKinds("number", [
|
|||
"sub_level",
|
||||
"sub_end",
|
||||
"last_message_at",
|
||||
"height"
|
||||
]);
|
||||
|
||||
registerDataKinds("string", [
|
||||
|
|
@ -276,7 +278,7 @@ registerDataKinds({ array: "number" }, [
|
|||
"last_ping",
|
||||
"ping_iota",
|
||||
"get_time",
|
||||
"omikron_connections",
|
||||
"omikron_connections"
|
||||
]);
|
||||
|
||||
registerDataKinds("container", [
|
||||
|
|
|
|||
199
packages/ui/src/cmp/select.tsx
Normal file
199
packages/ui/src/cmp/select.tsx
Normal 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,
|
||||
}
|
||||
|
|
@ -1,25 +1,26 @@
|
|||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser, type User } from "./context";
|
||||
|
||||
/**
|
||||
* Executes Wrapper.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
import { failedUser } from "@tensamin/shared/data";
|
||||
|
||||
// Wrapper function to pass user data to some component
|
||||
export default function Wrapper(props: {
|
||||
userId?: number;
|
||||
loading: React.ReactNode;
|
||||
component: (user: User) => React.ReactNode;
|
||||
}) {
|
||||
const { get } = useUser();
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!props.userId) return;
|
||||
useEffect(() => {
|
||||
if (!props.userId) {
|
||||
setUser(failedUser);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
|
||||
void get(props.userId)
|
||||
get(props.userId)
|
||||
.then((value) => {
|
||||
if (active) {
|
||||
setUser(value);
|
||||
|
|
@ -27,7 +28,7 @@ export default function Wrapper(props: {
|
|||
})
|
||||
.catch(() => {
|
||||
if (active) {
|
||||
setUser(null);
|
||||
setUser(failedUser);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
1
todo.md
1
todo.md
|
|
@ -1,3 +1,4 @@
|
|||
- Calculate message height and pass to virtualizer (-> packages/chat/src/components/input.tsx)
|
||||
- Split ttp codec into separate file
|
||||
- Add "Rename Conversations to Friends" option in settings
|
||||
- Handle live messages in notification context
|
||||
|
|
|
|||
Loading…
Reference in a new issue