Added enter animation, added loading and error state to conversations and communities, updated other small stuff
This commit is contained in:
parent
396defef64
commit
ed53c17597
10 changed files with 116 additions and 53 deletions
|
|
@ -12,33 +12,34 @@
|
||||||
"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:*",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@tensamin/chat": "workspace:*",
|
|
||||||
"@tensamin/crypto": "workspace:*",
|
|
||||||
"@tensamin/ttp": "workspace:*",
|
|
||||||
"@tensamin/storage": "workspace:*",
|
|
||||||
"@tensamin/user": "workspace:*",
|
|
||||||
"@tensamin/shared": "workspace:*",
|
|
||||||
"@tensamin/ui": "workspace:*",
|
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@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/crypto": "workspace:*",
|
||||||
|
"@tensamin/shared": "workspace:*",
|
||||||
|
"@tensamin/storage": "workspace:*",
|
||||||
|
"@tensamin/ttp": "workspace:*",
|
||||||
|
"@tensamin/ui": "workspace:*",
|
||||||
|
"@tensamin/user": "workspace:*",
|
||||||
"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",
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"lucide-react": "^0.564.0",
|
"lucide-react": "^0.564.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"shadcn": "^4.0.7",
|
||||||
"sonner": "^1.0.0",
|
"sonner": "^1.0.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwind-scrollbar-hide": "^4.0.0",
|
"tailwind-scrollbar-hide": "^4.0.0",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
|
||||||
"shadcn": "^4.0.7"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default function Navbar() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { id } = useSearch({ strict: false });
|
const { id } = useSearch({ strict: false });
|
||||||
|
|
||||||
const currentCalls = id ? conversations[id]?.calls || [] : [];
|
const currentCalls = id ? conversations?.[id]?.calls || [] : [];
|
||||||
|
|
||||||
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">
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import type {
|
||||||
} from "@tensamin/shared/features/conversation/schema";
|
} from "@tensamin/shared/features/conversation/schema";
|
||||||
|
|
||||||
interface contextValue {
|
interface contextValue {
|
||||||
conversations: Conversation[];
|
conversations: Conversation[] | null;
|
||||||
communities: Community[];
|
communities: Community[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConversationContext = React.createContext<contextValue | undefined>(
|
const ConversationContext = React.createContext<contextValue | undefined>(
|
||||||
|
|
@ -24,8 +24,12 @@ const ConversationContext = React.createContext<contextValue | undefined>(
|
||||||
export default function ConversationProvider(props: {
|
export default function ConversationProvider(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [conversations, setConversations] = React.useState<Conversation[]>([]);
|
const [conversations, setConversations] = React.useState<
|
||||||
const [communities, setCommunities] = React.useState<Community[]>([]);
|
Conversation[] | null
|
||||||
|
>(null);
|
||||||
|
const [communities, setCommunities] = React.useState<Community[] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const { send } = useSocket();
|
const { send } = useSocket();
|
||||||
|
|
||||||
|
|
@ -39,6 +43,12 @@ export default function ConversationProvider(props: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
setConversations([
|
||||||
|
{
|
||||||
|
last_message_at: 0,
|
||||||
|
user_id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
toast("error", "Failed to load conversations");
|
toast("error", "Failed to load conversations");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -49,6 +59,13 @@ export default function ConversationProvider(props: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
setCommunities([
|
||||||
|
{
|
||||||
|
community_address: "none",
|
||||||
|
community_title: "Failed",
|
||||||
|
position: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
toast("error", "Failed to load communities");
|
toast("error", "Failed to load communities");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -57,13 +74,13 @@ export default function ConversationProvider(props: {
|
||||||
};
|
};
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
const value = React.useMemo(
|
|
||||||
() => ({ conversations, communities }),
|
|
||||||
[communities, conversations],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConversationContext.Provider value={value}>
|
<ConversationContext.Provider
|
||||||
|
value={{
|
||||||
|
conversations,
|
||||||
|
communities,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ConversationContext.Provider>
|
</ConversationContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useConversation } from "../context";
|
||||||
import Switch from "./switch";
|
import Switch from "./switch";
|
||||||
import ConversationModal from "../modal/conversation";
|
import ConversationModal from "../modal/conversation";
|
||||||
import CommunityModal from "../modal/community";
|
import CommunityModal from "../modal/community";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes List.
|
* Executes List.
|
||||||
|
|
@ -22,7 +23,9 @@ export default function List() {
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count:
|
count:
|
||||||
category === "conversations" ? conversations.length : communities.length,
|
category === "conversations"
|
||||||
|
? conversations?.length || 0
|
||||||
|
: communities?.length || 0,
|
||||||
estimateSize: () => 60,
|
estimateSize: () => 60,
|
||||||
getScrollElement: () => scrollRef.current,
|
getScrollElement: () => scrollRef.current,
|
||||||
});
|
});
|
||||||
|
|
@ -41,32 +44,45 @@ export default function List() {
|
||||||
height: `${virtualizer.getTotalSize()}px`,
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
{communities === null || conversations === null ? (
|
||||||
const itemKey = `${category}-${virtualItem.index}`;
|
<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 (
|
{communities &&
|
||||||
<div
|
conversations &&
|
||||||
key={itemKey}
|
virtualizer.getVirtualItems().map((virtualItem) => {
|
||||||
style={{
|
const itemKey = `${category}-${virtualItem.index}`;
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
return (
|
||||||
left: 0,
|
<div
|
||||||
width: "100%",
|
key={itemKey}
|
||||||
transform: `translateY(${virtualItem.start}px)`,
|
style={{
|
||||||
}}
|
position: "absolute",
|
||||||
>
|
top: 0,
|
||||||
{category === "conversations" ? (
|
left: 0,
|
||||||
conversations[virtualItem.index] ? (
|
width: "100%",
|
||||||
<ConversationModal
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
userId={conversations[virtualItem.index].user_id}
|
}}
|
||||||
|
>
|
||||||
|
{category === "conversations" ? (
|
||||||
|
conversations[virtualItem.index] ? (
|
||||||
|
<ConversationModal
|
||||||
|
userId={conversations[virtualItem.index].user_id}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
) : communities[virtualItem.index] ? (
|
||||||
|
<CommunityModal
|
||||||
|
community={communities[virtualItem.index]}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null}
|
||||||
) : communities[virtualItem.index] ? (
|
</div>
|
||||||
<CommunityModal community={communities[virtualItem.index]} />
|
);
|
||||||
) : null}
|
})}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-black text-foreground;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import Sidebar from "@/components/sidebar";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes Layout.
|
* Executes Layout.
|
||||||
* @param props Parameter props.
|
* @param props Parameter props.
|
||||||
|
|
@ -46,7 +48,20 @@ export default function Layout(props: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 />
|
<Sidebar />
|
||||||
<div className="w-full h-full flex flex-col">
|
<div className="w-full h-full flex flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
@ -54,6 +69,11 @@ export default function Layout(props: { children: ReactNode }) {
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const animationTiming = {
|
||||||
|
first: 0.18,
|
||||||
|
second: 0.1,
|
||||||
|
};
|
||||||
|
|
|
||||||
7
bun.lock
7
bun.lock
|
|
@ -49,6 +49,7 @@
|
||||||
"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",
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"lucide-react": "^0.564.0",
|
"lucide-react": "^0.564.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
@ -911,6 +912,8 @@
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
"fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
||||||
|
|
@ -1107,6 +1110,10 @@
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
||||||
|
|
||||||
|
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="],
|
"msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="],
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useChat } from "./context";
|
||||||
import InputComponent from "./components/input";
|
import InputComponent from "./components/input";
|
||||||
import Message from "./components/message";
|
import Message from "./components/message";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
import { PAGE_SIZE } from "./values";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the chat screen with virtualized history and live message updates.
|
* Renders the chat screen with virtualized history and live message updates.
|
||||||
|
|
@ -179,8 +179,8 @@ export default function Screen() {
|
||||||
|
|
||||||
if (!hasValidChatUser) {
|
if (!hasValidChatUser) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center text-sm text-muted-foreground">
|
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
|
||||||
Select a conversation to start chatting.
|
Invalid user
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,5 @@ export type LiveMessage = RawMessage & {
|
||||||
failed?: boolean;
|
failed?: boolean;
|
||||||
localId: string;
|
localId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PAGE_SIZE = 25;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const message = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const failedUser = {
|
export const failedUser = {
|
||||||
display: "Failed to load user",
|
display: "Failed",
|
||||||
iota_id: 0,
|
iota_id: 0,
|
||||||
omikron_connections: [],
|
omikron_connections: [],
|
||||||
online_status: "user_borked",
|
online_status: "user_borked",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue