Big chat improvements, adjusted mobile ui.

This commit is contained in:
Alois 2026-04-04 22:49:34 +02:00
commit 7a922a5978
15 changed files with 201 additions and 101 deletions

View file

@ -15,7 +15,9 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2" "@tauri-apps/plugin-opener": "^2",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2"

View file

@ -13,7 +13,7 @@
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "tensamin", "title": "Tensamin",
"width": 800, "width": 800,
"height": 600 "height": 600
} }

View file

@ -1,25 +1,16 @@
import { isTauri } from "@tauri-apps/api/core"; import { createContext } from "react";
import { createContext, useEffect } from "react";
type contextType = { type contextType = {
isTauri: boolean; nothing: undefined;
}; };
export const context = createContext<contextType | undefined>(undefined); export const context = createContext<contextType | undefined>(undefined);
export default function Provider({ children }: { children: React.ReactNode }) { export default function Provider({ children }: { children: React.ReactNode }) {
const isTauriVar = isTauri();
useEffect(() => {
if (!isTauriVar) return;
document.body.classList.add("");
}, [isTauriVar]);
return ( return (
<context.Provider <context.Provider
value={{ value={{
isTauri: isTauriVar, nothing: undefined,
}} }}
> >
{children} {children}

View file

@ -26,6 +26,7 @@
"@tensamin/ui": "workspace:*", "@tensamin/ui": "workspace:*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"@tensamin/mobile": "workspace:*", "@tensamin/mobile": "workspace:*",
"@tauri-apps/api": "^2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comlink": "^4.4.2", "comlink": "^4.4.2",

View file

@ -1,5 +1,5 @@
import { Button } from "@tensamin/ui/cmp/button"; import { Button } from "@tensamin/ui/cmp/button";
import { House, Phone, User } from "lucide-react"; import { ArrowLeft, House, Phone, User } from "lucide-react";
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import { useCall } from "@tensamin/call/context"; import { useCall } from "@tensamin/call/context";
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/user/wrapper";
@ -15,7 +15,7 @@ import { displayCallId } from "@tensamin/call/utils";
import { useState } from "react"; import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar"; import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar";
export default function Navbar() { export default function Navbar({ forMobile }: { forMobile: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { joinCall, state } = useCall(); const { joinCall, state } = useCall();
const { conversations } = useConversation(); const { conversations } = useConversation();
@ -29,7 +29,17 @@ export default function Navbar() {
const [selectOpen, setSelectOpen] = useState(false); const [selectOpen, setSelectOpen] = useState(false);
return ( return (
<div className="w-full gap-2 h-13.5 flex items-center justify-center"> <div
className={`${forMobile && "border-b"} w-full gap-2 h-13.5 flex items-center justify-center`}
>
{forMobile ? (
<SidebarTrigger
className="w-9 h-9 aspect-square rounded-lg ml-2"
variant="outline"
>
<ArrowLeft className="size-4.5" />
</SidebarTrigger>
) : (
<Button <Button
onClick={() => navigate({ to: "/" })} onClick={() => navigate({ to: "/" })}
className="w-9 h-9 aspect-square rounded-lg" className="w-9 h-9 aspect-square rounded-lg"
@ -37,6 +47,7 @@ export default function Navbar() {
> >
<House className="size-4.5" /> <House className="size-4.5" />
</Button> </Button>
)}
{pathname === "/chat" && ( {pathname === "/chat" && (
<Wrapper <Wrapper
userId={id} userId={id}
@ -107,7 +118,8 @@ export function MobileNavbar() {
const { setOpenMobile } = useSidebar(); const { setOpenMobile } = useSidebar();
return ( return (
<div className="z-100 w-full h-16 flex justify-center items-center gap-8 bg-card border-t"> <div className="z-100 w-full pb-[env(safe-area-inset-bottom)] bg-card border-t">
<div className="z-100 w-full h-14 flex justify-center items-center gap-8">
<SidebarTrigger <SidebarTrigger
className="text-foreground w-12! h-12! aspect-square rounded-xl" className="text-foreground w-12! h-12! aspect-square rounded-xl"
variant="link" variant="link"
@ -125,5 +137,6 @@ export function MobileNavbar() {
<House className="size-5.5" /> <House className="size-5.5" />
</Button> </Button>
</div> </div>
</div>
); );
} }

View file

@ -8,6 +8,7 @@ import {
Sidebar as SidebarRoot, Sidebar as SidebarRoot,
SidebarContent, SidebarContent,
} from "@tensamin/ui/cmp/sidebar"; } from "@tensamin/ui/cmp/sidebar";
import { isTauri } from "@tauri-apps/api/core";
/** /**
* Renders the conversation sidebar with account summary and conversation list. * Renders the conversation sidebar with account summary and conversation list.
@ -23,8 +24,8 @@ export default function Sidebar() {
return ( return (
<SidebarRoot> <SidebarRoot>
<SidebarContent> <SidebarContent className={isTauri() ? "pt-[env(safe-area-inset-top)]" : "pt-2"}>
<div className="h-full w-full flex flex-col gap-3 p-2"> <div className="h-full w-full flex flex-col gap-3 p-2 pt-0!">
<div> <div>
<Wrapper <Wrapper
loading={<Loading />} loading={<Loading />}

View file

@ -8,9 +8,11 @@ import {
ContextMenuTrigger, ContextMenuTrigger,
} from "@tensamin/ui/cmp/context-menu"; } from "@tensamin/ui/cmp/context-menu";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useSidebar } from "@tensamin/ui/cmp/sidebar";
export default function ConversationModal({ userId }: { userId: number }) { export default function ConversationModal({ userId }: { userId: number }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { setOpenMobile } = useSidebar();
return ( return (
<ContextMenu> <ContextMenu>
@ -18,7 +20,10 @@ export default function ConversationModal({ userId }: { userId: number }) {
render={ render={
<div <div
className="select-none cursor-pointer" className="select-none cursor-pointer"
onClick={() => navigate({ to: "/chat", search: { id: userId } })} onClick={() => {
navigate({ to: "/chat", search: { id: userId } });
setOpenMobile(false);
}}
> >
<Wrapper <Wrapper
loading={<Loading />} loading={<Loading />}

View file

@ -14,13 +14,17 @@ import { toast } from "@tensamin/shared/log";
import { useTTP } from "@tensamin/ttp/context"; import { useTTP } from "@tensamin/ttp/context";
import { useState } from "react"; import { useState } from "react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core";
// The page // The page
export default function Page() { export default function Page() {
return ( return (
<div className="p-3 flex gap-2"> <div className={`px-3 flex gap-2 ${!isTauri() && "pt-3"}`}>
<AddConversationButton /> <AddConversationButton />
<Button disabled>Add Community</Button> <Button disabled>Add Community</Button>
<Button variant="outline" onClick={() => location.reload()}>
Reload
</Button>
</div> </div>
); );
} }

View file

@ -7,6 +7,8 @@ import { SidebarProvider } from "@tensamin/ui/cmp/sidebar";
import { useIsMobile, cn } from "@tensamin/ui/utils"; import { useIsMobile, cn } from "@tensamin/ui/utils";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { isTauri } from "@tauri-apps/api/core";
import { useLocation } from "@tanstack/react-router";
/** /**
* Executes Layout. * Executes Layout.
@ -15,6 +17,7 @@ import { motion } from "framer-motion";
*/ */
export default function Layout(props: { children: ReactNode }) { export default function Layout(props: { children: ReactNode }) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const location = useLocation();
return ( return (
<motion.div <motion.div
@ -33,8 +36,17 @@ export default function Layout(props: { children: ReactNode }) {
> >
<SidebarProvider> <SidebarProvider>
<Sidebar /> <Sidebar />
<div className="w-full h-full flex flex-col"> <div
{!isMobile && <Navbar />} className={cn(
"w-full h-full flex flex-col",
isTauri() && "pt-[env(safe-area-inset-top)]",
)}
>
{!isMobile && <Navbar forMobile={false} />}
{isMobile && location.pathname === "/chat" && (
<Navbar forMobile={true} />
)}
<div <div
className={cn( className={cn(
"bg-background h-full w-full", "bg-background h-full w-full",
@ -43,7 +55,7 @@ export default function Layout(props: { children: ReactNode }) {
> >
{props.children} {props.children}
</div> </div>
{isMobile && <MobileNavbar />} {isMobile && location.pathname !== "/chat" && <MobileNavbar />}
</div> </div>
</SidebarProvider> </SidebarProvider>
</motion.div> </motion.div>

View file

@ -12,6 +12,7 @@ import { TooltipProvider } from "@tensamin/ui/cmp/tooltip";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { useIsMobile } from "@tensamin/ui/utils"; import { useIsMobile } from "@tensamin/ui/utils";
import { isTauri } from "@tauri-apps/api/core";
/** /**
* Executes Layout. * Executes Layout.
@ -51,7 +52,16 @@ export default function Layout(props: { children: ReactNode }) {
</div> </div>
)} )}
</div> </div>
<Toaster position={isMobile ? "top-center" : "bottom-right"} /> <Toaster
position={isMobile ? "top-center" : "bottom-right"}
{...(isTauri()
? {
mobileOffset: {
top: "env(safe-area-inset-top)",
},
}
: {})}
/>
<TooltipProvider> <TooltipProvider>
<Storage> <Storage>
<LoginWrapper> <LoginWrapper>

View file

@ -35,6 +35,8 @@
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
@ -49,6 +51,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",
"@tauri-apps/api": "^2",
"@tensamin/call": "workspace:*", "@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",

View file

@ -10,8 +10,13 @@ import { useTTP } from "@tensamin/ttp/context";
import { useCrypto } from "@tensamin/crypto/context"; import { useCrypto } from "@tensamin/crypto/context";
import { log, toast } from "@tensamin/shared/log"; import { log, toast } from "@tensamin/shared/log";
export default function InputComponent() { export default function InputComponent({
const [value, setValue] = React.useState(""); value,
setValue,
}: {
value: string;
setValue: (value: string) => void;
}) {
const [invertEnterBehavior, setInvertEnterBehavior] = React.useState(false); const [invertEnterBehavior, setInvertEnterBehavior] = React.useState(false);
const { encrypt } = useCrypto(); const { encrypt } = useCrypto();
@ -35,15 +40,13 @@ export default function InputComponent() {
const time = Date.now(); const time = Date.now();
const currentValue = value; const currentValue = value;
const currentUserId = userId();
const currentSharedSecret = sharedSecret();
if (!Number.isSafeInteger(currentUserId) || currentUserId <= 0) { if (!Number.isSafeInteger(userId) || userId <= 0) {
toast("error", "No conversation selected"); toast("error", "No conversation selected");
return; return;
} }
if (!currentSharedSecret) { if (!sharedSecret) {
toast("error", "Still getting shared secret..."); toast("error", "Still getting shared secret...");
return; return;
} }
@ -59,18 +62,18 @@ export default function InputComponent() {
message_state: "awaiting", message_state: "awaiting",
}); });
const encryptedContext = await encrypt(currentSharedSecret, currentValue); const encryptedContext = await encrypt(sharedSecret, currentValue);
send("message_send", { send("message_send", {
height, height,
content: encryptedContext, content: encryptedContext,
receiver_id: currentUserId, receiver_id: userId,
timestamp: time, timestamp: time,
}).catch((e) => { }).catch((e) => {
log(0, "Chat", "red", "Failed to send message", e, { log(0, "Chat", "red", "Failed to send message", e, {
content: currentValue, content: currentValue,
encryptedContext, encryptedContext,
receiver_id: currentUserId, receiver_id: userId,
timestamp: time, timestamp: time,
}); });
reference.setFailed(true); reference.setFailed(true);
@ -81,7 +84,7 @@ export default function InputComponent() {
} }
return ( return (
<Card className="rounded-none rounded-t-xl border-b-0 pt-0"> <Card className="rounded-none rounded-t-xl border-b-0 pt-0 pb-[env(safe-area-inset-bottom)]">
<CardHeader className="p-0 flex flex-col"> <CardHeader className="p-0 flex flex-col">
<Input <Input
placeholder="Send a message..." placeholder="Send a message..."

View file

@ -25,7 +25,6 @@ export default function Message(props: {
}) { }) {
const { sharedSecret } = useChat(); const { sharedSecret } = useChat();
const { decrypt } = useCrypto(); const { decrypt } = useCrypto();
const sharedSecretValue = sharedSecret();
const [decodedContent, setDecodedContent] = React.useState(""); const [decodedContent, setDecodedContent] = React.useState("");
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
@ -38,7 +37,7 @@ export default function Message(props: {
} }
const content = props.message.content; const content = props.message.content;
const secret = sharedSecretValue; const secret = sharedSecret;
let active = true; let active = true;
if (!secret) { if (!secret) {
@ -72,7 +71,7 @@ export default function Message(props: {
return () => { return () => {
active = false; active = false;
}; };
}, [decrypt, props.message.content, props.notEncrypted, sharedSecretValue]); }, [decrypt, props.message.content, props.notEncrypted, sharedSecret]);
return ( return (
<div <div
@ -83,9 +82,9 @@ export default function Message(props: {
props.message.sent_by_self props.message.sent_by_self
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-muted" : "bg-muted"
}`} } ${!isReady && "animate-pulse"}`}
> >
{props.message.failed && ( {props.message.failed && props.message.message_state === "awaiting" && (
<Tooltip> <Tooltip>
<TooltipContent> <TooltipContent>
<p>Failed to send message</p> <p>Failed to send message</p>
@ -93,9 +92,18 @@ export default function Message(props: {
<TooltipTrigger render={<AlertTriangle size={17} />} /> <TooltipTrigger render={<AlertTriangle size={17} />} />
</Tooltip> </Tooltip>
)} )}
{props.message.message_state === "awaiting" && {!props.message.failed &&
!props.message.failed && <Clock size={17} />} props.message.message_state === "awaiting" && <Clock size={17} />}
{isReady ? <Text value={decodedContent} /> : null} {isReady ? (
<Text value={decodedContent} />
) : (
<div
className="h-8"
style={{
width: Math.random() * 100 + 50 + "px",
}}
/>
)}
</div> </div>
</div> </div>
); );

View file

@ -6,6 +6,7 @@ import {
useState, useState,
useContext, useContext,
type ReactNode, type ReactNode,
useRef,
} from "react"; } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRouterState } from "@tanstack/react-router"; import { useRouterState } from "@tanstack/react-router";
@ -15,6 +16,7 @@ import { useCrypto } from "@tensamin/crypto/context";
import { useUser } from "@tensamin/user/context"; import { useUser } from "@tensamin/user/context";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useTTP } from "@tensamin/ttp/context"; import { useTTP } from "@tensamin/ttp/context";
import { log } from "@tensamin/shared/log";
export const context = createContext<contextType | undefined>(undefined); export const context = createContext<contextType | undefined>(undefined);
@ -61,28 +63,38 @@ export default function Provider(props: { children: ReactNode }) {
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]); const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
const [currentSharedSecret, setCurrentSharedSecret] = useState(""); const [currentSharedSecret, setCurrentSharedSecret] = useState("");
const inputBoxRef = useRef<HTMLDivElement>(null);
const locationSearch = useRouterState({ const locationSearch = useRouterState({
select: (state) => state.location.search, select: (state) => state.location.search,
}); });
// User ID compare to clear shared secret
const userIdValue = useMemo(() => { const userIdValue = useMemo(() => {
const rawId = (locationSearch as unknown as { id?: unknown })?.id; const rawId = (locationSearch as unknown as { id?: unknown })?.id;
return Number(rawId ?? 0); return Number(rawId ?? 0);
}, [locationSearch]); }, [locationSearch]);
useEffect(() => { const userIdValueFromLastRender = useRef(userIdValue);
const recipientId = userIdValue;
if (!recipientId) { useEffect(() => {
setCurrentSharedSecret(""); if (userIdValue === userIdValueFromLastRender.current) {
return; return;
} }
userIdValueFromLastRender.current = userIdValue;
setCurrentSharedSecret("");
}, [userIdValue]);
// Load shared secret
useEffect(() => {
if (!userIdValue) return;
let active = true; let active = true;
void (async () => { void (async () => {
try { try {
const recipientData = await get(recipientId); const recipientData = await get(userIdValue);
const ownId = await load("user_id"); const ownId = await load("user_id");
const privateKey = await load("private_key"); const privateKey = await load("private_key");
const ownData = await get(ownId); const ownData = await get(ownId);
@ -193,10 +205,26 @@ export default function Provider(props: { children: ReactNode }) {
!Number.isFinite(nextState.chat_partner_id) || !Number.isFinite(nextState.chat_partner_id) ||
!Number.isFinite(nextState.timestamp) !Number.isFinite(nextState.timestamp)
) { ) {
log(
3,
"chat",
"yellow",
"Cancel message state update due to invalid data",
);
return; return;
} }
if (nextState.chat_partner_id !== userIdValue) { if (nextState.chat_partner_id !== userIdValue) {
log(
3,
"chat",
"yellow",
"Cancel message state update due to user ID mismatch",
{
expected: userIdValue,
received: nextState.chat_partner_id,
}
);
return; return;
} }
@ -246,28 +274,21 @@ export default function Provider(props: { children: ReactNode }) {
}); });
}, [subscribePush, userIdValue]); }, [subscribePush, userIdValue]);
const value = useMemo<contextType>( return (
() => ({ <QueryClientProvider client={queryClient}>
<context.Provider
value={{
getMessages: customGetMessages, getMessages: customGetMessages,
liveMessages: () => liveMessagesState, liveMessages: () => liveMessagesState,
addLiveMessage, addLiveMessage,
clearLiveMessages, clearLiveMessages,
sharedSecret: () => currentSharedSecret, sharedSecret: currentSharedSecret,
userId: () => userIdValue, userId: userIdValue,
}), inputBoxRef,
[ }}
addLiveMessage, >
clearLiveMessages, {props.children}
currentSharedSecret, </context.Provider>
customGetMessages,
liveMessagesState,
userIdValue,
],
);
return (
<QueryClientProvider client={queryClient}>
<context.Provider value={value}>{props.children}</context.Provider>
</QueryClientProvider> </QueryClientProvider>
); );
} }
@ -279,8 +300,9 @@ type contextType = {
setFailed: (failed: boolean) => void; setFailed: (failed: boolean) => void;
}; };
clearLiveMessages: () => void; clearLiveMessages: () => void;
sharedSecret: () => string; sharedSecret: string;
userId: () => number; userId: number;
inputBoxRef: React.RefObject<HTMLDivElement | null>;
}; };
/** /**

View file

@ -7,6 +7,7 @@ import InputComponent from "./components/input";
import Message from "./components/message"; import Message from "./components/message";
import { PAGE_SIZE } from "./values"; import { PAGE_SIZE } from "./values";
import { useLayoutEffect } from "react";
/** /**
* Renders the chat screen with virtualized history and live message updates. * Renders the chat screen with virtualized history and live message updates.
@ -15,6 +16,7 @@ import { PAGE_SIZE } from "./values";
export default function Screen() { export default function Screen() {
const { getMessages, liveMessages, clearLiveMessages, userId } = useChat(); const { getMessages, liveMessages, clearLiveMessages, userId } = useChat();
const inputBoxRef = React.useRef<HTMLDivElement | null>(null);
const scrollRef = React.useRef<HTMLDivElement | null>(null); const scrollRef = React.useRef<HTMLDivElement | null>(null);
const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] = const [hasScrolledToBottomInitially, setHasScrolledToBottomInitially] =
@ -25,11 +27,10 @@ export default function Screen() {
scrollTop: number; scrollTop: number;
} | null>(null); } | null>(null);
const chatUserId = userId(); const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
const hasValidChatUser = Number.isSafeInteger(chatUserId) && chatUserId > 0;
const messagesQuery = useInfiniteQuery({ const messagesQuery = useInfiniteQuery({
queryKey: ["chat-messages", String(chatUserId)], queryKey: ["chat-messages", String(userId)],
initialPageParam: 0, initialPageParam: 0,
queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, Number(pageParam)), queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, Number(pageParam)),
enabled: hasValidChatUser, enabled: hasValidChatUser,
@ -52,7 +53,7 @@ export default function Screen() {
setHasScrolledToBottomInitially(false); setHasScrolledToBottomInitially(false);
setLastLiveMessageCount(0); setLastLiveMessageCount(0);
setPrependAnchor(null); setPrependAnchor(null);
}, [chatUserId, clearLiveMessages]); }, [userId, clearLiveMessages]);
const liveMessagesSnapshot = liveMessages(); const liveMessagesSnapshot = liveMessages();
@ -152,6 +153,28 @@ export default function Screen() {
void onScroll(); void onScroll();
}, [onScroll]); }, [onScroll]);
/**
* Input box height changes & state
*/
const [value, setValue] = React.useState("");
useLayoutEffect(() => {
const el = inputBoxRef.current;
const host = scrollRef.current;
if (!host || !el) return;
host.style.maxHeight = `calc(100vh - ${el.scrollHeight + 50}px)`;
}, [value]);
React.useEffect(() => {
const el = inputBoxRef.current;
const host = scrollRef.current;
if (!host || !el) return;
host.style.maxHeight = `calc(100vh - ${el.scrollHeight + 50}px)`;
}, []);
// Render
if (!hasValidChatUser) { if (!hasValidChatUser) {
return ( return (
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80"> <div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
@ -161,11 +184,11 @@ export default function Screen() {
} }
return ( return (
<div className="w-full h-full flex flex-col overflow-hidden px-2"> <div className="w-full h-full flex flex-col px-2">
<div <div
ref={scrollRef} ref={scrollRef}
id="chat_container" id="chat_container"
className="flex-1 max-h-[calc(100vh-151px)] overflow-y-auto px-2.5" className="flex-1 overflow-y-auto px-2.5"
onScroll={handleContainerScroll} onScroll={handleContainerScroll}
> >
<div <div
@ -203,7 +226,9 @@ export default function Screen() {
})} })}
</div> </div>
</div> </div>
<InputComponent /> <div ref={inputBoxRef}>
<InputComponent setValue={setValue} value={value} />
</div>
</div> </div>
); );
} }