From c319266c51e4166ffbfc650bbe15a092a67d5710 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 10:04:19 +0200 Subject: [PATCH 01/10] Channel view --- src/App.tsx | 3 ++- src/app/app.ts | 4 +++- src/components/page/PageView.tsx | 24 +++++++++++++++++++++ src/components/page/channel/ChannelPage.tsx | 16 ++++++++++++++ src/components/page/channel/TextChannel.tsx | 10 +++++++++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/components/page/PageView.tsx create mode 100644 src/components/page/channel/ChannelPage.tsx create mode 100644 src/components/page/channel/TextChannel.tsx diff --git a/src/App.tsx b/src/App.tsx index 3f7d97e..45fbf04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { } from "./components/ui/resizable"; import Sidebar from "./components/view/Sidebar"; import { ThemeProvider } from "next-themes"; +import Page from "./components/page/PageView"; export default function App() { const appRef = useRef(null); @@ -50,7 +51,7 @@ export default function App() { -
+
diff --git a/src/app/app.ts b/src/app/app.ts index 98eb5b2..a5faf56 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -6,6 +6,7 @@ import { base58 } from "@scure/base"; import axios from "axios"; import { Account, AccountsFile, getAccounts } from "@/lib/accounts"; +import { Page } from "@/lib/page"; ed.hashes.sha512 = sha512; @@ -22,11 +23,12 @@ ed.hashes.sha512 = sha512; * should stay a pure display of what `Enclave` exposes — it should not * reach into `EnclaveServer` or `EnclaveWebSocket` directly. */ -export default class Enclave { +export default class Enclave

{ public accounts?: AccountsFile; public server?: EnclaveServer; public serverList: ServerList; public forceRender: () => void; + public page?: P; public constructor() { this.serverList = {}; diff --git a/src/components/page/PageView.tsx b/src/components/page/PageView.tsx new file mode 100644 index 0000000..311fe5a --- /dev/null +++ b/src/components/page/PageView.tsx @@ -0,0 +1,24 @@ +import Enclave from "@/app/app"; +import ChannelPage from "./channel/ChannelPage"; +import { Channel } from "@/app/protocol"; + +export type ChannelPageProps = { + kind: "channel"; + channel: Channel; +}; + +export type Page = ChannelPageProps; + +export default function Page({ + appRef, +}: { + appRef: React.RefObject; +}) { + switch (appRef.current?.page?.kind) { + case "channel": + return ; + + default: + return null; + } +} diff --git a/src/components/page/channel/ChannelPage.tsx b/src/components/page/channel/ChannelPage.tsx new file mode 100644 index 0000000..35ef626 --- /dev/null +++ b/src/components/page/channel/ChannelPage.tsx @@ -0,0 +1,16 @@ +import Enclave from "@/app/app"; +import { ChannelPageProps } from "@/lib/page"; + +export default function ChannelPage({ + appRef, +}: { + appRef: React.RefObject | null>; +}) { + switch (appRef.current?.page?.channel.kind) { + case "text": + return ; + + default: + return null; + } +} diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx new file mode 100644 index 0000000..e57dc4a --- /dev/null +++ b/src/components/page/channel/TextChannel.tsx @@ -0,0 +1,10 @@ +import Enclave from "@/app/app"; +import { ChannelPageProps } from "@/components/page/PageView"; + +export default function TextChannel({ + appRef, +}: { + appRef: React.RefObject | null>; +}) { + return

{appRef.current?.page?.channel.name}
; +} From 32271289f37081c16490681c994afd0791c620cc Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 10:17:42 +0200 Subject: [PATCH 02/10] Channel selector --- src/app/app.ts | 2 +- src/components/page/channel/ChannelPage.tsx | 2 +- src/components/view/Sidebar.tsx | 44 ++++++++++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index a5faf56..476962c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -6,7 +6,7 @@ import { base58 } from "@scure/base"; import axios from "axios"; import { Account, AccountsFile, getAccounts } from "@/lib/accounts"; -import { Page } from "@/lib/page"; +import { Page } from "@/components/page/PageView"; ed.hashes.sha512 = sha512; diff --git a/src/components/page/channel/ChannelPage.tsx b/src/components/page/channel/ChannelPage.tsx index 35ef626..0287f87 100644 --- a/src/components/page/channel/ChannelPage.tsx +++ b/src/components/page/channel/ChannelPage.tsx @@ -1,5 +1,5 @@ import Enclave from "@/app/app"; -import { ChannelPageProps } from "@/lib/page"; +import { ChannelPageProps } from "@/components/page/PageView"; export default function ChannelPage({ appRef, diff --git a/src/components/view/Sidebar.tsx b/src/components/view/Sidebar.tsx index 5c959f5..eb0c94d 100644 --- a/src/components/view/Sidebar.tsx +++ b/src/components/view/Sidebar.tsx @@ -3,6 +3,7 @@ import { Channel, ChannelKind } from "@/app/protocol"; import { ChevronDown, ChevronUp, HashIcon } from "lucide-react"; import { useState } from "react"; import AccountCard from "./AccountCard"; +import { cn } from "@/lib/utils"; export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) { switch (kind) { @@ -13,7 +14,13 @@ export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) { } } -export function RenderCategory({ channel }: { channel: Channel }) { +export function RenderCategory({ + appRef, + channel, +}: { + appRef: React.RefObject; + channel: Channel; +}) { if (channel.kind !== "category") return null; const [open, setOpen] = useState(true); @@ -33,19 +40,41 @@ export function RenderCategory({ channel }: { channel: Channel }) { {open && (
- +
)} ); } -export function RenderChannels({ channels }: { channels: Channel[] }) { +export function RenderChannels({ + appRef, + channels, +}: { + appRef: React.RefObject; + channels: Channel[]; +}) { return channels.map((channel) => channel.kind === "category" ? ( - + ) : ( -
+
{ + if (!appRef.current) { + console.error("AppRef is not initialized yet"); + return; + } + + appRef.current.page = { kind: "channel", channel }; + + appRef.current.forceRender(); + }} + > {channel.name}
@@ -67,7 +96,10 @@ export default function Sidebar({
- +
)} From fc1c550a2ef4131ef4697b295d82a50f44456377 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 10:24:13 +0200 Subject: [PATCH 03/10] Fix recursion --- src/components/page/PageView.tsx | 5 ++--- src/components/page/channel/ChannelPage.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/page/PageView.tsx b/src/components/page/PageView.tsx index 311fe5a..9dfb093 100644 --- a/src/components/page/PageView.tsx +++ b/src/components/page/PageView.tsx @@ -14,11 +14,10 @@ export default function Page({ }: { appRef: React.RefObject; }) { + if (!appRef.current?.page?.kind) return null; + switch (appRef.current?.page?.kind) { case "channel": return ; - - default: - return null; } } diff --git a/src/components/page/channel/ChannelPage.tsx b/src/components/page/channel/ChannelPage.tsx index 0287f87..f9b811b 100644 --- a/src/components/page/channel/ChannelPage.tsx +++ b/src/components/page/channel/ChannelPage.tsx @@ -1,16 +1,16 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "@/components/page/PageView"; +import TextChannel from "./TextChannel"; export default function ChannelPage({ appRef, }: { appRef: React.RefObject | null>; }) { + if (!appRef.current?.page?.channel.kind) return null; + switch (appRef.current?.page?.channel.kind) { case "text": - return ; - - default: - return null; + return ; } } From 8bdae4ceb88622fef359f2f2f47d3f924988b630 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 10:53:52 +0200 Subject: [PATCH 04/10] Message sending --- src/app/app.ts | 25 +++++++++++++++++++++ src/app/protocol.ts | 16 +++++++++++++ src/components/page/channel/TextChannel.tsx | 14 +++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/app/app.ts b/src/app/app.ts index 476962c..dab6286 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -44,6 +44,31 @@ export default class Enclave

{ return this.accounts?.accounts[this.accounts.activeAccount] || null; } + public sendMessage(content: string, channelId: string) { + const clientSecretKey = this.getClientSecretKey(); + + if (!clientSecretKey) { + console.error("Acccount not found"); + return; + } + + const timestamp = Date.now(); + + const signature = new TextEncoder().encode( + `${timestamp}@${this.server?.hostname}@${content}`, + ); + + this.server?.websocket?.send({ + method: "SendMessage", + channel_id: channelId, + data: { + content, + timestamp, + signature: base58.encode(ed.sign(signature, clientSecretKey)), + }, + }); + } + public async connectToServer(hostname: string, isSecure: boolean) { if (this.server) { this.server.disconnect(); diff --git a/src/app/protocol.ts b/src/app/protocol.ts index 5d51ed8..23c206f 100644 --- a/src/app/protocol.ts +++ b/src/app/protocol.ts @@ -6,6 +6,17 @@ export interface ServerMeta { channels: Channel[]; } +export interface MessageData { + content: string; + timestamp: number; + signature: string; +} + +export interface StoredMessage extends MessageData { + id: string; + author: string; +} + export type ClientMethod = | { method: "Initialized"; @@ -30,6 +41,11 @@ export type ServerMethod = | { method: "Meta"; } + | { + method: "SendMessage"; + channel_id: string; + data: MessageData; + } | { method: "Error"; error: string; diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index e57dc4a..b584951 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -6,5 +6,17 @@ export default function TextChannel({ }: { appRef: React.RefObject | null>; }) { - return

{appRef.current?.page?.channel.name}
; + const channel = appRef.current?.page?.channel; + + if (!channel) return null; + + return ( +
{ + appRef.current?.sendMessage("Hello, World", channel.id); + }} + > + {channel.name} +
+ ); } From 9c448049bc9ce3343457e33e3355942371fa8673 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 11:06:35 +0200 Subject: [PATCH 05/10] Meta and refactor --- src/app/app.ts | 25 +++++++++++++++++-------- src/app/protocol.ts | 28 +++------------------------- src/app/server.ts | 2 +- src/lib/accounts.ts | 4 ++-- src/lib/serverList.ts | 2 +- src/lib/types.ts | 26 ++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 37 deletions(-) create mode 100644 src/lib/types.ts diff --git a/src/app/app.ts b/src/app/app.ts index dab6286..5c572e9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -44,6 +44,14 @@ export default class Enclave

{ return this.accounts?.accounts[this.accounts.activeAccount] || null; } + public getClientSecretKey() { + if (!this.accounts) return null; + + const account = this.getAccount(); + + return account ? base58.decode(account.privateKey) : null; + } + public sendMessage(content: string, channelId: string) { const clientSecretKey = this.getClientSecretKey(); @@ -104,6 +112,15 @@ export default class Enclave

{ return; } + const account = this.getAccount(); + + if (!account) { + console.error("Failed to get account"); + return; + } + + this.server.websocket?.send({ method: "Meta", ...account.meta }); + const metaResponse = await axios.get( getHTTPUrl(hostname, isSecure, "/meta"), ); @@ -118,12 +135,4 @@ export default class Enclave

{ this.forceRender(); } - - public getClientSecretKey() { - if (!this.accounts) return null; - - const account = this.getAccount(); - - return account ? base58.decode(account.privateKey) : null; - } } diff --git a/src/app/protocol.ts b/src/app/protocol.ts index 23c206f..b509425 100644 --- a/src/app/protocol.ts +++ b/src/app/protocol.ts @@ -1,21 +1,4 @@ -export interface ClientMeta {} - -export interface ServerMeta { - name: string; - description: string; - channels: Channel[]; -} - -export interface MessageData { - content: string; - timestamp: number; - signature: string; -} - -export interface StoredMessage extends MessageData { - id: string; - author: string; -} +import { ClientMeta, MessageData } from "@/lib/types"; export type ClientMethod = | { @@ -38,9 +21,9 @@ export type ServerMethod = timestamp: number; hostname: string; } - | { + | ({ method: "Meta"; - } + } & ClientMeta) | { method: "SendMessage"; channel_id: string; @@ -50,8 +33,3 @@ export type ServerMethod = method: "Error"; error: string; }; - -export type Channel = { id: string; name: string } & ChannelKind; - -export type ChannelKind = - { kind: "text" } | { kind: "category"; channels: Channel[] }; diff --git a/src/app/server.ts b/src/app/server.ts index 8410579..0ebfcd4 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -3,7 +3,7 @@ import * as ed from "@noble/ed25519"; import EnclaveWebSocket from "./ws"; import { sha512 } from "@noble/hashes/sha2.js"; import { getWSUrl } from "@/lib/serverList"; -import { ServerMeta } from "./protocol"; +import { ServerMeta } from "@/lib/types"; ed.hashes.sha512 = sha512; diff --git a/src/lib/accounts.ts b/src/lib/accounts.ts index f9ff4ac..6278ec4 100644 --- a/src/lib/accounts.ts +++ b/src/lib/accounts.ts @@ -1,8 +1,8 @@ import { invoke } from "@tauri-apps/api/core"; +import { ClientMeta } from "./types"; export interface Account { - displayName: string; - avatar?: string; + meta: ClientMeta; privateKey: string; } diff --git a/src/lib/serverList.ts b/src/lib/serverList.ts index 18e1f81..0517bc6 100644 --- a/src/lib/serverList.ts +++ b/src/lib/serverList.ts @@ -1,5 +1,5 @@ -import { ServerMeta } from "@/app/protocol"; import { invoke } from "@tauri-apps/api/core"; +import { ServerMeta } from "./types"; export interface KnownServer { meta: ServerMeta; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..1c77ea6 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,26 @@ +export interface ClientMeta { + displayName: string; + avatar?: string; +} + +export interface ServerMeta { + name: string; + description: string; + channels: Channel[]; +} + +export interface MessageData { + content: string; + timestamp: number; + signature: string; +} + +export interface StoredMessage extends MessageData { + id: string; + author: string; +} + +export type Channel = { id: string; name: string } & ChannelKind; + +export type ChannelKind = + { kind: "text" } | { kind: "category"; channels: Channel[] }; From cce9756a1b142fc466d0311f865e1f21c08708c3 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 11:24:59 +0200 Subject: [PATCH 06/10] Working send and updated account structure --- package.json | 1 + src-tauri/src/commands/accounts.rs | 5 +++-- src-tauri/src/protocol.rs | 7 ++++++- src/app/app.ts | 7 ++++++- src/components/page/PageView.tsx | 2 +- src/components/view/AccountCard.tsx | 6 +++--- src/components/view/NewProfileView.tsx | 14 +++++++------- src/components/view/Sidebar.tsx | 2 +- 8 files changed, 28 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index e29bc23..0eb2196 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "typecheck": "tsc --noEmit", "build": "tsc && vite build", "preview": "vite preview", "tauri": "tauri" diff --git a/src-tauri/src/commands/accounts.rs b/src-tauri/src/commands/accounts.rs index a6862e8..8124468 100644 --- a/src-tauri/src/commands/accounts.rs +++ b/src-tauri/src/commands/accounts.rs @@ -2,11 +2,12 @@ use serde::{Deserialize, Serialize}; use std::fs; use tauri::{AppHandle, Manager}; +use crate::protocol::ClientMeta; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Account { - pub display_name: String, - pub avatar: Option, + pub meta: ClientMeta, pub private_key: String, } diff --git a/src-tauri/src/protocol.rs b/src-tauri/src/protocol.rs index 3518cf2..163e0eb 100644 --- a/src-tauri/src/protocol.rs +++ b/src-tauri/src/protocol.rs @@ -3,9 +3,14 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClientMeta {} +#[serde(rename_all = "camelCase")] +pub struct ClientMeta { + pub display_name: String, + pub avatar: Option, +} #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct ServerMeta { pub name: String, pub description: String, diff --git a/src/app/app.ts b/src/app/app.ts index 5c572e9..a103451 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -62,8 +62,13 @@ export default class Enclave

{ const timestamp = Date.now(); + if (!this.server?.serverPublicKey) { + console.error("Server pubkey not initialized"); + return; + } + const signature = new TextEncoder().encode( - `${timestamp}@${this.server?.hostname}@${content}`, + `${timestamp}@${base58.encode(this.server?.serverPublicKey)}@${content}`, ); this.server?.websocket?.send({ diff --git a/src/components/page/PageView.tsx b/src/components/page/PageView.tsx index 9dfb093..7ba5401 100644 --- a/src/components/page/PageView.tsx +++ b/src/components/page/PageView.tsx @@ -1,6 +1,6 @@ import Enclave from "@/app/app"; import ChannelPage from "./channel/ChannelPage"; -import { Channel } from "@/app/protocol"; +import { Channel } from "@/lib/types"; export type ChannelPageProps = { kind: "channel"; diff --git a/src/components/view/AccountCard.tsx b/src/components/view/AccountCard.tsx index 2961dd7..67b8a9a 100644 --- a/src/components/view/AccountCard.tsx +++ b/src/components/view/AccountCard.tsx @@ -20,8 +20,8 @@ export default function AccountCard({

- - {account.displayName[0]} + + {account.meta.displayName[0]}
-

{account.displayName}

+

{account.meta.displayName}

Online diff --git a/src/components/view/NewProfileView.tsx b/src/components/view/NewProfileView.tsx index a5c06d8..4b3c07a 100644 --- a/src/components/view/NewProfileView.tsx +++ b/src/components/view/NewProfileView.tsx @@ -68,16 +68,16 @@ export function NewProfilePage({ const { secretKey } = ed.keygen(); - const newAccount = { - displayName: name, - privateKey: base58.encode(secretKey), - avatar: avatar ?? undefined, - }; - appRef.current.accounts.activeAccount = appRef.current.accounts.accounts.length; - appRef.current.accounts.accounts.push(newAccount); + appRef.current.accounts.accounts.push({ + meta: { + displayName: name, + avatar: avatar ?? undefined, + }, + privateKey: base58.encode(secretKey), + }); saveAccounts(appRef.current.accounts); } catch (e) { diff --git a/src/components/view/Sidebar.tsx b/src/components/view/Sidebar.tsx index eb0c94d..d786dc2 100644 --- a/src/components/view/Sidebar.tsx +++ b/src/components/view/Sidebar.tsx @@ -1,5 +1,5 @@ import Enclave from "@/app/app"; -import { Channel, ChannelKind } from "@/app/protocol"; +import { Channel, ChannelKind } from "@/lib/types"; import { ChevronDown, ChevronUp, HashIcon } from "lucide-react"; import { useState } from "react"; import AccountCard from "./AccountCard"; From 8cf69d5b02778eb267819ea2c5ae73c9baa9f39a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 15:10:25 +0200 Subject: [PATCH 07/10] Messages --- src-tauri/src/commands/accounts.rs | 2 +- src-tauri/src/commands/server_list.rs | 2 +- src-tauri/src/lib.rs | 2 +- src-tauri/src/protocol.rs | 51 --------------------- src-tauri/src/types.rs | 15 ++++++ src/app/app.ts | 41 ++++++++++++++++- src/app/protocol.ts | 15 ++++-- src/app/server.ts | 4 +- src/components/page/channel/TextChannel.tsx | 19 +++++--- 9 files changed, 85 insertions(+), 66 deletions(-) delete mode 100644 src-tauri/src/protocol.rs create mode 100644 src-tauri/src/types.rs diff --git a/src-tauri/src/commands/accounts.rs b/src-tauri/src/commands/accounts.rs index 8124468..fa8461d 100644 --- a/src-tauri/src/commands/accounts.rs +++ b/src-tauri/src/commands/accounts.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use tauri::{AppHandle, Manager}; -use crate::protocol::ClientMeta; +use crate::types::ClientMeta; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/commands/server_list.rs b/src-tauri/src/commands/server_list.rs index e66e0ab..b084088 100644 --- a/src-tauri/src/commands/server_list.rs +++ b/src-tauri/src/commands/server_list.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs}; use tauri::{AppHandle, Manager}; -use crate::protocol::ServerMeta; +use crate::types::ServerMeta; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 51cbccc..6a7af70 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,5 @@ pub mod commands; -pub mod protocol; +pub mod types; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { diff --git a/src-tauri/src/protocol.rs b/src-tauri/src/protocol.rs deleted file mode 100644 index 163e0eb..0000000 --- a/src-tauri/src/protocol.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::borrow::Cow; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientMeta { - pub display_name: String, - pub avatar: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ServerMeta { - pub name: String, - pub description: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "method")] -pub enum ClientMethod { - Initialized { - public_key: String, - signature: String, - - timestamp: u64, - hostname: String, - }, - - Error { - error: Cow<'static, str>, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "method")] -pub enum ServerMethod { - Initialize { - public_key: String, - signature: String, - - timestamp: u64, - hostname: String, - }, - - Meta(ClientMeta), - - Error { - error: String, - }, -} diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs new file mode 100644 index 0000000..46497db --- /dev/null +++ b/src-tauri/src/types.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientMeta { + pub display_name: String, + pub avatar: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerMeta { + pub name: String, + pub description: String, +} diff --git a/src/app/app.ts b/src/app/app.ts index a103451..58a752c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -7,6 +7,7 @@ import { base58 } from "@scure/base"; import axios from "axios"; import { Account, AccountsFile, getAccounts } from "@/lib/accounts"; import { Page } from "@/components/page/PageView"; +import { ClientMethod } from "./protocol"; ed.hashes.sha512 = sha512; @@ -124,7 +125,12 @@ export default class Enclave

{ return; } - this.server.websocket?.send({ method: "Meta", ...account.meta }); + if (this.server.websocket) { + this.server.websocket.send({ method: "Meta", ...account.meta }); + this.server.websocket.websocket.onmessage = (msg) => { + this.onMessage(JSON.parse(msg.data)); + }; + } const metaResponse = await axios.get( getHTTPUrl(hostname, isSecure, "/meta"), @@ -140,4 +146,37 @@ export default class Enclave

{ this.forceRender(); } + + public async onMessage(msg: ClientMethod) { + const server = this.server; + + if (!server) { + console.error("Unable to get server for message, MSG:", msg); + return; + } + + switch (msg.method) { + case "Initialized": + console.error("Already initialized"); + return; + + case "Error": + console.error("Server error:", msg.error); + return; + + case "Messages": + console.log("msg", msg.messages); + Object.entries(msg.messages).forEach(([channelId, messages]) => { + if (!server.messages[channelId]) { + server.messages[channelId] = []; + } + + messages.forEach((message) => { + server.messages[channelId].push(message); + }); + }); + + return; + } + } } diff --git a/src/app/protocol.ts b/src/app/protocol.ts index b509425..ed48003 100644 --- a/src/app/protocol.ts +++ b/src/app/protocol.ts @@ -1,4 +1,4 @@ -import { ClientMeta, MessageData } from "@/lib/types"; +import { ClientMeta, MessageData, StoredMessage } from "@/lib/types"; export type ClientMethod = | { @@ -11,6 +11,10 @@ export type ClientMethod = | { method: "Error"; error: string; + } + | { + method: "Messages"; + messages: Record; }; export type ServerMethod = @@ -21,6 +25,10 @@ export type ServerMethod = timestamp: number; hostname: string; } + | { + method: "Error"; + error: string; + } | ({ method: "Meta"; } & ClientMeta) @@ -30,6 +38,7 @@ export type ServerMethod = data: MessageData; } | { - method: "Error"; - error: string; + method: "GetMessages"; + channel_id: string; + chunk: number; }; diff --git a/src/app/server.ts b/src/app/server.ts index 0ebfcd4..c54efd1 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -3,7 +3,7 @@ import * as ed from "@noble/ed25519"; import EnclaveWebSocket from "./ws"; import { sha512 } from "@noble/hashes/sha2.js"; import { getWSUrl } from "@/lib/serverList"; -import { ServerMeta } from "@/lib/types"; +import { ServerMeta, StoredMessage } from "@/lib/types"; ed.hashes.sha512 = sha512; @@ -29,10 +29,12 @@ export default class EnclaveServer { public isSecure: boolean; public websocket?: EnclaveWebSocket; public meta?: ServerMeta; + public messages: Record; public constructor(hostname: string, isSecure: boolean) { this.hostname = hostname; this.isSecure = isSecure; + this.messages = {}; } public disconnect() { diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index b584951..166b90f 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -1,5 +1,6 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "@/components/page/PageView"; +import { useEffect } from "react"; export default function TextChannel({ appRef, @@ -10,13 +11,17 @@ export default function TextChannel({ if (!channel) return null; + useEffect(() => { + appRef.current?.server?.websocket?.send({ + method: "GetMessages", + channel_id: channel.id, + chunk: 0, + }); + + appRef.current?.forceRender(); + }, []); + return ( -

{ - appRef.current?.sendMessage("Hello, World", channel.id); - }} - > - {channel.name} -
+
{JSON.stringify(appRef.current?.server?.messages[channel.id])}
); } From 83c7926728c05aafe062531f05b19fa7e1d87b96 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 18:12:47 +0200 Subject: [PATCH 08/10] Text Channel UI --- src/app/app.ts | 4 +- src/app/server.ts | 2 +- src/components/page/channel/TextChannel.tsx | 82 +++++++++++++++++++-- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index 58a752c..169196e 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -168,11 +168,11 @@ export default class Enclave

{ console.log("msg", msg.messages); Object.entries(msg.messages).forEach(([channelId, messages]) => { if (!server.messages[channelId]) { - server.messages[channelId] = []; + server.messages[channelId] = new Set(); } messages.forEach((message) => { - server.messages[channelId].push(message); + server.messages[channelId].add(message); }); }); diff --git a/src/app/server.ts b/src/app/server.ts index c54efd1..7d23595 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -29,7 +29,7 @@ export default class EnclaveServer { public isSecure: boolean; public websocket?: EnclaveWebSocket; public meta?: ServerMeta; - public messages: Record; + public messages: Record>; public constructor(hostname: string, isSecure: boolean) { this.hostname = hostname; diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 166b90f..be60580 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -1,17 +1,21 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "@/components/page/PageView"; -import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { StoredMessage } from "@/lib/types"; +import { useEffect, useState } from "react"; export default function TextChannel({ appRef, }: { appRef: React.RefObject | null>; }) { + const [currentMessage, setCurrentMessage] = useState(""); const channel = appRef.current?.page?.channel; - if (!channel) return null; - useEffect(() => { + if (!channel) return; + appRef.current?.server?.websocket?.send({ method: "GetMessages", channel_id: channel.id, @@ -19,9 +23,77 @@ export default function TextChannel({ }); appRef.current?.forceRender(); - }, []); + }, [channel]); + + if (!channel) return null; + + const sendMessage = () => { + appRef.current?.sendMessage(currentMessage, channel.id); + setCurrentMessage(""); + }; return ( -

{JSON.stringify(appRef.current?.server?.messages[channel.id])}
+
+
+

{channel.name}

+
+ +
+ {appRef.current?.server?.messages[channel.id] && + Array.from(appRef.current?.server?.messages[channel.id]).map( + (message) => , + )} +
+ +
+ setCurrentMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }} + /> + + {currentMessage.trim() && } +
+
+ ); +} + +export function TextMessage({ + appRef, + message, +}: { + appRef: React.RefObject | null>; + message: StoredMessage; +}) { + const time = new Date(message.timestamp).toLocaleTimeString([], { + hour: "numeric", + minute: "2-digit", + }); + + return ( +
+
+ {message.author.slice(0, 2).toUpperCase()} +
+ +
+
+ + {message.author.slice(0, 8)} + + {time} +
+

+ {message.content} +

+
+
); } From 7934f209858f789357a49cd40adaf7a287e98917 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 18:21:28 +0200 Subject: [PATCH 09/10] Messages --- src/app/app.ts | 7 ++++--- src/app/server.ts | 2 +- src/components/page/channel/TextChannel.tsx | 15 ++++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index 169196e..b79a940 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -165,17 +165,18 @@ export default class Enclave

{ return; case "Messages": - console.log("msg", msg.messages); Object.entries(msg.messages).forEach(([channelId, messages]) => { if (!server.messages[channelId]) { - server.messages[channelId] = new Set(); + server.messages[channelId] = {}; } messages.forEach((message) => { - server.messages[channelId].add(message); + server.messages[channelId][message.id] = message; }); }); + this.forceRender(); + return; } } diff --git a/src/app/server.ts b/src/app/server.ts index 7d23595..be0d8fe 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -29,7 +29,7 @@ export default class EnclaveServer { public isSecure: boolean; public websocket?: EnclaveWebSocket; public meta?: ServerMeta; - public messages: Record>; + public messages: Record>; public constructor(hostname: string, isSecure: boolean) { this.hostname = hostname; diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index be60580..65fc5c0 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -1,5 +1,6 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "@/components/page/PageView"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { StoredMessage } from "@/lib/types"; @@ -40,8 +41,10 @@ export default function TextChannel({

{appRef.current?.server?.messages[channel.id] && - Array.from(appRef.current?.server?.messages[channel.id]).map( - (message) => , + Object.entries(appRef.current?.server?.messages[channel.id]).map( + ([_, message]) => ( + + ), )}
@@ -79,9 +82,11 @@ export function TextMessage({ return (
-
- {message.author.slice(0, 2).toUpperCase()} -
+ + + {message.author.slice(0, 2).toUpperCase()} + +
From 0aa1b8285bddb6f824f58a596c8bcef2935582c1 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 20 Aug 2026 19:20:24 +0200 Subject: [PATCH 10/10] fix --- src/components/page/channel/TextChannel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/page/channel/TextChannel.tsx b/src/components/page/channel/TextChannel.tsx index 65fc5c0..e613936 100644 --- a/src/components/page/channel/TextChannel.tsx +++ b/src/components/page/channel/TextChannel.tsx @@ -69,7 +69,6 @@ export default function TextChannel({ } export function TextMessage({ - appRef, message, }: { appRef: React.RefObject | null>;