Added enter animation, added loading and error state to conversations and communities, updated other small stuff

This commit is contained in:
Alois 2026-03-29 11:31:44 +02:00
commit ed53c17597
10 changed files with 116 additions and 53 deletions

View file

@ -12,33 +12,34 @@
"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:*",
"@tensamin/storage": "workspace:*",
"@tensamin/user": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"@fontsource-variable/inter": "^5.2.8",
"@noble/curves": "^2.0.1",
"@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:*",
"@tensamin/storage": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/ui": "workspace:*",
"@tensamin/user": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"comlink": "^4.4.2",
"framer-motion": "^12.38.0",
"lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"shadcn": "^4.0.7",
"sonner": "^1.0.0",
"tailwind-merge": "^3.5.0",
"tailwind-scrollbar-hide": "^4.0.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"zod": "^4.3.6",
"@fontsource-variable/inter": "^5.2.8",
"shadcn": "^4.0.7"
"zod": "^4.3.6"
},
"devDependencies": {
"@types/react": "^19.2.2",

View file

@ -21,7 +21,7 @@ export default function Navbar() {
const { pathname } = useLocation();
const { id } = useSearch({ strict: false });
const currentCalls = id ? conversations[id]?.calls || [] : [];
const currentCalls = id ? conversations?.[id]?.calls || [] : [];
return (
<div className="w-full h-13.5 flex items-center justify-center">

View file

@ -8,8 +8,8 @@ import type {
} from "@tensamin/shared/features/conversation/schema";
interface contextValue {
conversations: Conversation[];
communities: Community[];
conversations: Conversation[] | null;
communities: Community[] | null;
}
const ConversationContext = React.createContext<contextValue | undefined>(
@ -24,8 +24,12 @@ const ConversationContext = React.createContext<contextValue | undefined>(
export default function ConversationProvider(props: {
children: React.ReactNode;
}) {
const [conversations, setConversations] = React.useState<Conversation[]>([]);
const [communities, setCommunities] = React.useState<Community[]>([]);
const [conversations, setConversations] = React.useState<
Conversation[] | null
>(null);
const [communities, setCommunities] = React.useState<Community[] | null>(
null,
);
const { send } = useSocket();
@ -39,6 +43,12 @@ export default function ConversationProvider(props: {
}
})
.catch(() => {
setConversations([
{
last_message_at: 0,
user_id: 0,
},
]);
toast("error", "Failed to load conversations");
});
@ -49,6 +59,13 @@ export default function ConversationProvider(props: {
}
})
.catch(() => {
setCommunities([
{
community_address: "none",
community_title: "Failed",
position: "",
},
]);
toast("error", "Failed to load communities");
});
@ -57,13 +74,13 @@ export default function ConversationProvider(props: {
};
}, [send]);
const value = React.useMemo(
() => ({ conversations, communities }),
[communities, conversations],
);
return (
<ConversationContext.Provider value={value}>
<ConversationContext.Provider
value={{
conversations,
communities,
}}
>
{props.children}
</ConversationContext.Provider>
);

View file

@ -5,6 +5,7 @@ import { useConversation } from "../context";
import Switch from "./switch";
import ConversationModal from "../modal/conversation";
import CommunityModal from "../modal/community";
import { Loader2 } from "lucide-react";
/**
* Executes List.
@ -22,7 +23,9 @@ export default function List() {
const virtualizer = useVirtualizer({
count:
category === "conversations" ? conversations.length : communities.length,
category === "conversations"
? conversations?.length || 0
: communities?.length || 0,
estimateSize: () => 60,
getScrollElement: () => scrollRef.current,
});
@ -41,32 +44,45 @@ export default function List() {
height: `${virtualizer.getTotalSize()}px`,
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const itemKey = `${category}-${virtualItem.index}`;
{communities === null || conversations === null ? (
<div className="flex items-center justify-center h-full pt-5">
<p className="text-foreground/45 flex gap-1 items-center justify-center">
<Loader2 size={18} className="animate-spin" />
Loading...
</p>
</div>
) : null}
return (
<div
key={itemKey}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualItem.start}px)`,
}}
>
{category === "conversations" ? (
conversations[virtualItem.index] ? (
<ConversationModal
userId={conversations[virtualItem.index].user_id}
{communities &&
conversations &&
virtualizer.getVirtualItems().map((virtualItem) => {
const itemKey = `${category}-${virtualItem.index}`;
return (
<div
key={itemKey}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualItem.start}px)`,
}}
>
{category === "conversations" ? (
conversations[virtualItem.index] ? (
<ConversationModal
userId={conversations[virtualItem.index].user_id}
/>
) : null
) : communities[virtualItem.index] ? (
<CommunityModal
community={communities[virtualItem.index]}
/>
) : null
) : communities[virtualItem.index] ? (
<CommunityModal community={communities[virtualItem.index]} />
) : null}
</div>
);
})}
) : null}
</div>
);
})}
</div>
</div>
</div>

View file

@ -79,7 +79,7 @@
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-black text-foreground;
}
html {
@apply font-sans;

View file

@ -5,6 +5,8 @@ import Sidebar from "@/components/sidebar";
import Navbar from "@/components/navbar";
import { useStorage } from "@tensamin/storage/context";
import { motion } from "framer-motion";
/**
* Executes Layout.
* @param props Parameter props.
@ -46,7 +48,20 @@ export default function Layout(props: { children: ReactNode }) {
}
return (
<div className="w-full h-full flex bg-sidebar">
<motion.div
className="w-full h-full flex bg-sidebar"
initial={{ y: -40, scale: 0.95, borderRadius: "1rem" }}
animate={{ y: 0, scale: 1, borderRadius: 0 }}
transition={{
borderRadius: { duration: animationTiming.first, ease: "easeOut" },
y: { duration: animationTiming.first, ease: "easeOut" },
scale: {
delay: animationTiming.first,
duration: animationTiming.second,
ease: "easeOut",
},
}}
>
<Sidebar />
<div className="w-full h-full flex flex-col">
<Navbar />
@ -54,6 +69,11 @@ export default function Layout(props: { children: ReactNode }) {
{props.children}
</div>
</div>
</div>
</motion.div>
);
}
const animationTiming = {
first: 0.18,
second: 0.1,
};