Account and avatar
This commit is contained in:
@@ -6,6 +6,7 @@ use tauri::{AppHandle, Manager};
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Account {
|
||||
pub display_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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<Enclave | null>;
|
||||
}) {
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function handleAvatarChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
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 (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<div className="w-full max-w-sm space-y-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold">Create your profile</h1>
|
||||
@@ -68,6 +100,24 @@ export function NewProfilePage({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<label htmlFor="avatar-upload" className="cursor-pointer">
|
||||
<div className="h-20 w-20 overflow-hidden rounded-full border border-border bg-muted flex items-center justify-center hover:opacity-80 transition-opacity">
|
||||
<Avatar className="h-full w-auto aspect-square">
|
||||
{avatar && <AvatarImage src={avatar} />}
|
||||
<AvatarFallback>{displayName[0] || "J"}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
id="avatar-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleAvatarChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display-name">Display name</Label>
|
||||
<Input
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Channel, ChannelKind } from "@/app/protocol";
|
||||
import { ChevronDown, ChevronUp, HashIcon, Settings2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Card } from "../ui/card";
|
||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
||||
@@ -60,6 +60,8 @@ export default function Sidebar({
|
||||
}: {
|
||||
appRef: React.RefObject<Enclave | null>;
|
||||
}) {
|
||||
const account = appRef.current?.getAccount();
|
||||
|
||||
return (
|
||||
<div className="h-screen relative w-full @container">
|
||||
{appRef.current?.server?.meta && (
|
||||
@@ -74,24 +76,27 @@ export default function Sidebar({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="absolute -left-16 right-16 bottom-0 h-20 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
||||
<Card className="px-3 py-3 h-full w-full flex flex-row">
|
||||
<div className="flex overflow-hidden gap-2.5 items-center">
|
||||
<Avatar className="h-full w-auto aspect-square">
|
||||
<AvatarImage src="https://avatars.githubusercontent.com/u/103524696?v=4" />
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span>Klesti Selimaj</span>
|
||||
<span className="text-muted-foreground">Online</span>
|
||||
{account && (
|
||||
<div className="absolute -left-16 right-16 bottom-0 h-20 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
||||
<Card className="px-3 py-3 h-full w-full flex flex-row">
|
||||
<div className="flex gap-2.5 items-center">
|
||||
<Avatar className="h-full w-auto aspect-square">
|
||||
<AvatarImage src={account.avatar} />
|
||||
<AvatarFallback>{account.displayName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span>{account.displayName}</span>
|
||||
<span className="text-muted-foreground">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center text-muted-foreground">
|
||||
<Button variant="ghost" className="size-10">
|
||||
<Settings2Icon className="size-5.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center text-muted-foreground">
|
||||
<Button variant="ghost" className="size-10">
|
||||
<Settings2Icon className="size-5.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function ServerList({
|
||||
if (!appRef.current) return null;
|
||||
|
||||
return (
|
||||
<aside className="bg-card w-16 h-screen p-2 flex flex-col gap-2.5 border-r border-r-border shrink-0">
|
||||
<aside className="w-16 h-screen p-2 flex flex-col gap-2.5 border-r border-r-border shrink-0">
|
||||
{Object.entries(appRef.current.serverList).map(([hostname, server]) => {
|
||||
return (
|
||||
<img
|
||||
|
||||
@@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface Account {
|
||||
displayName: string;
|
||||
avatar?: string;
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user