diff --git a/src-tauri/src/commands/accounts.rs b/src-tauri/src/commands/accounts.rs index c5ff008..18043f1 100644 --- a/src-tauri/src/commands/accounts.rs +++ b/src-tauri/src/commands/accounts.rs @@ -6,6 +6,7 @@ use tauri::{AppHandle, Manager}; #[serde(rename_all = "camelCase")] pub struct Account { pub display_name: String, + pub avatar: Option, pub private_key: String, } diff --git a/src/App.tsx b/src/App.tsx index cbb8fb0..3f7d97e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,18 +29,26 @@ export default function App() { return ( -
+
{appRef.current?.accounts && appRef.current.accounts.accounts.length === 0 ? ( ) : ( <> - - - + +
+ + +
+
diff --git a/src/app/app.ts b/src/app/app.ts index 8eb46a6..62f033d 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -5,7 +5,7 @@ import { sha512 } from "@noble/hashes/sha2.js"; import { base58 } from "@scure/base"; import axios from "axios"; -import { AccountsFile, getAccounts } from "@/lib/accounts"; +import { Account, AccountsFile, getAccounts } from "@/lib/accounts"; ed.hashes.sha512 = sha512; @@ -38,6 +38,10 @@ export default class Enclave { this.accounts = await getAccounts(); } + public getAccount(): Account | null { + return this.accounts?.accounts[this.accounts.activeAccount] || null; + } + public async connectToServer(hostname: string, isSecure: boolean) { if (this.server) { this.server.disconnect(); @@ -87,7 +91,7 @@ export default class Enclave { public getClientSecretKey() { if (!this.accounts) return null; - const account = this.accounts?.accounts[this.accounts.activeAccount]; + const account = this.getAccount(); return account ? base58.decode(account.privateKey) : null; } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..e92a2f4 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: AvatarPrimitive.Root.Props & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..4458dae --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/view/AccountCard.tsx b/src/components/view/AccountCard.tsx new file mode 100644 index 0000000..2fefd43 --- /dev/null +++ b/src/components/view/AccountCard.tsx @@ -0,0 +1,51 @@ +import { Card } from "../ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; +import { CopyIcon, Settings2Icon } from "lucide-react"; +import { Button } from "../ui/button"; +import Enclave from "@/app/app"; +import { base58 } from "@scure/base"; +import * as ed from "@noble/ed25519"; + +export default function AccountCard({ + appRef, +}: { + appRef: React.RefObject; +}) { + const account = appRef.current?.getAccount(); + + if (!account) return null; + + return ( +
+ +
+ + + {account.displayName[0]} + +
+ { + navigator.clipboard.writeText( + base58.encode( + ed.getPublicKey(base58.decode(account.privateKey)), + ), + ); + }} + > + {account.displayName} + + + Online +
+
+
+ +
+
+
+ ); +} diff --git a/src/components/view/NewProfileView.tsx b/src/components/view/NewProfileView.tsx index 49dba06..a5c06d8 100644 --- a/src/components/view/NewProfileView.tsx +++ b/src/components/view/NewProfileView.tsx @@ -7,6 +7,16 @@ import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { saveAccounts } from "@/lib/accounts"; import Enclave from "@/app/app"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; + +function readFileAsDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(new Error("Failed to read image")); + reader.readAsDataURL(file); + }); +} export function NewProfilePage({ appRef, @@ -14,9 +24,28 @@ export function NewProfilePage({ appRef: React.RefObject; }) { const [displayName, setDisplayName] = useState(""); + const [avatar, setAvatar] = useState(null); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); + async function handleAvatarChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith("image/")) { + setError("Please select an image file"); + return; + } + + try { + const dataUrl = await readFileAsDataUrl(file); + setAvatar(dataUrl); + setError(null); + } catch { + setError("Failed to load image"); + } + } + async function handleCreate() { const name = displayName.trim(); if (!name) { @@ -42,6 +71,7 @@ export function NewProfilePage({ const newAccount = { displayName: name, privateKey: base58.encode(secretKey), + avatar: avatar ?? undefined, }; appRef.current.accounts.activeAccount = @@ -54,11 +84,13 @@ export function NewProfilePage({ setError(e instanceof Error ? e.message : "Failed to create profile"); } finally { setSubmitting(false); + + window.location.reload(); } } return ( -
+

Create your profile

@@ -68,6 +100,24 @@ export function NewProfilePage({

+
+ + +
+
+
setOpen((v) => !v)} + > + {channel.name} + {open ? ( + + ) : ( + + )} +
+ {open && ( +
+ +
+ )} +
+ ); +} + export function RenderChannels({ channels }: { channels: Channel[] }) { return channels.map((channel) => channel.kind === "category" ? ( -
-
- {channel.name} -
-
- -
-
+ ) : ( -
+
{channel.name}
@@ -37,16 +59,20 @@ export default function Sidebar({ appRef: React.RefObject; }) { return ( - appRef.current?.server?.meta && ( -
-
-

{appRef.current.server.meta.name}

-
+
+ {appRef.current?.server?.meta && ( + <> +
+

{appRef.current.server.meta.name}

+
-
- -
-
- ) +
+ +
+ + )} + + +
); } diff --git a/src/components/view/serverList.tsx b/src/components/view/serverList.tsx index 2ff8bc8..ae84df4 100644 --- a/src/components/view/serverList.tsx +++ b/src/components/view/serverList.tsx @@ -10,7 +10,7 @@ export default function ServerList({ if (!appRef.current) return null; return ( -