Merge pull request #4 from recurse-chat/working-messages
Working messages
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use crate::types::ClientMeta;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub display_name: String,
|
pub meta: ClientMeta,
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::{collections::HashMap, fs};
|
use std::{collections::HashMap, fs};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
use crate::protocol::ServerMeta;
|
use crate::types::ServerMeta;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod protocol;
|
pub mod types;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ClientMeta {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ServerMeta {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
+2
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "./components/ui/resizable";
|
} from "./components/ui/resizable";
|
||||||
import Sidebar from "./components/view/Sidebar";
|
import Sidebar from "./components/view/Sidebar";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import Page from "./components/page/PageView";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const appRef = useRef<Enclave | null>(null);
|
const appRef = useRef<Enclave | null>(null);
|
||||||
@@ -50,7 +51,7 @@ export default function App() {
|
|||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
<ResizablePanel defaultSize="100%">
|
<ResizablePanel defaultSize="100%">
|
||||||
<div className="h-screen"></div>
|
<Page appRef={appRef} />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+86
-5
@@ -6,6 +6,8 @@ import { base58 } from "@scure/base";
|
|||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Account, AccountsFile, getAccounts } from "@/lib/accounts";
|
import { Account, AccountsFile, getAccounts } from "@/lib/accounts";
|
||||||
|
import { Page } from "@/components/page/PageView";
|
||||||
|
import { ClientMethod } from "./protocol";
|
||||||
|
|
||||||
ed.hashes.sha512 = sha512;
|
ed.hashes.sha512 = sha512;
|
||||||
|
|
||||||
@@ -22,11 +24,12 @@ ed.hashes.sha512 = sha512;
|
|||||||
* should stay a pure display of what `Enclave` exposes — it should not
|
* should stay a pure display of what `Enclave` exposes — it should not
|
||||||
* reach into `EnclaveServer` or `EnclaveWebSocket` directly.
|
* reach into `EnclaveServer` or `EnclaveWebSocket` directly.
|
||||||
*/
|
*/
|
||||||
export default class Enclave {
|
export default class Enclave<P = Page> {
|
||||||
public accounts?: AccountsFile;
|
public accounts?: AccountsFile;
|
||||||
public server?: EnclaveServer;
|
public server?: EnclaveServer;
|
||||||
public serverList: ServerList;
|
public serverList: ServerList;
|
||||||
public forceRender: () => void;
|
public forceRender: () => void;
|
||||||
|
public page?: P;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.serverList = {};
|
this.serverList = {};
|
||||||
@@ -42,6 +45,44 @@ export default class Enclave {
|
|||||||
return this.accounts?.accounts[this.accounts.activeAccount] || null;
|
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();
|
||||||
|
|
||||||
|
if (!clientSecretKey) {
|
||||||
|
console.error("Acccount not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
if (!this.server?.serverPublicKey) {
|
||||||
|
console.error("Server pubkey not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = new TextEncoder().encode(
|
||||||
|
`${timestamp}@${base58.encode(this.server?.serverPublicKey)}@${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) {
|
public async connectToServer(hostname: string, isSecure: boolean) {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.disconnect();
|
this.server.disconnect();
|
||||||
@@ -77,6 +118,20 @@ export default class Enclave {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const account = this.getAccount();
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
console.error("Failed to get account");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
const metaResponse = await axios.get(
|
||||||
getHTTPUrl(hostname, isSecure, "/meta"),
|
getHTTPUrl(hostname, isSecure, "/meta"),
|
||||||
);
|
);
|
||||||
@@ -92,11 +147,37 @@ export default class Enclave {
|
|||||||
this.forceRender();
|
this.forceRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getClientSecretKey() {
|
public async onMessage(msg: ClientMethod) {
|
||||||
if (!this.accounts) return null;
|
const server = this.server;
|
||||||
|
|
||||||
const account = this.getAccount();
|
if (!server) {
|
||||||
|
console.error("Unable to get server for message, MSG:", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return account ? base58.decode(account.privateKey) : null;
|
switch (msg.method) {
|
||||||
|
case "Initialized":
|
||||||
|
console.error("Already initialized");
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "Error":
|
||||||
|
console.error("Server error:", msg.error);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "Messages":
|
||||||
|
Object.entries(msg.messages).forEach(([channelId, messages]) => {
|
||||||
|
if (!server.messages[channelId]) {
|
||||||
|
server.messages[channelId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.forEach((message) => {
|
||||||
|
server.messages[channelId][message.id] = message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.forceRender();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-15
@@ -1,10 +1,4 @@
|
|||||||
export interface ClientMeta {}
|
import { ClientMeta, MessageData, StoredMessage } from "@/lib/types";
|
||||||
|
|
||||||
export interface ServerMeta {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
channels: Channel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClientMethod =
|
export type ClientMethod =
|
||||||
| {
|
| {
|
||||||
@@ -17,6 +11,10 @@ export type ClientMethod =
|
|||||||
| {
|
| {
|
||||||
method: "Error";
|
method: "Error";
|
||||||
error: string;
|
error: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: "Messages";
|
||||||
|
messages: Record<string, StoredMessage[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerMethod =
|
export type ServerMethod =
|
||||||
@@ -27,15 +25,20 @@ export type ServerMethod =
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
method: "Meta";
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
method: "Error";
|
method: "Error";
|
||||||
error: string;
|
error: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
method: "Meta";
|
||||||
|
} & ClientMeta)
|
||||||
|
| {
|
||||||
|
method: "SendMessage";
|
||||||
|
channel_id: string;
|
||||||
|
data: MessageData;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: "GetMessages";
|
||||||
|
channel_id: string;
|
||||||
|
chunk: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Channel = { id: string; name: string } & ChannelKind;
|
|
||||||
|
|
||||||
export type ChannelKind =
|
|
||||||
{ kind: "text" } | { kind: "category"; channels: Channel[] };
|
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@ import * as ed from "@noble/ed25519";
|
|||||||
import EnclaveWebSocket from "./ws";
|
import EnclaveWebSocket from "./ws";
|
||||||
import { sha512 } from "@noble/hashes/sha2.js";
|
import { sha512 } from "@noble/hashes/sha2.js";
|
||||||
import { getWSUrl } from "@/lib/serverList";
|
import { getWSUrl } from "@/lib/serverList";
|
||||||
import { ServerMeta } from "./protocol";
|
import { ServerMeta, StoredMessage } from "@/lib/types";
|
||||||
|
|
||||||
ed.hashes.sha512 = sha512;
|
ed.hashes.sha512 = sha512;
|
||||||
|
|
||||||
@@ -29,10 +29,12 @@ export default class EnclaveServer {
|
|||||||
public isSecure: boolean;
|
public isSecure: boolean;
|
||||||
public websocket?: EnclaveWebSocket;
|
public websocket?: EnclaveWebSocket;
|
||||||
public meta?: ServerMeta;
|
public meta?: ServerMeta;
|
||||||
|
public messages: Record<string, Record<string, StoredMessage>>;
|
||||||
|
|
||||||
public constructor(hostname: string, isSecure: boolean) {
|
public constructor(hostname: string, isSecure: boolean) {
|
||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
this.isSecure = isSecure;
|
this.isSecure = isSecure;
|
||||||
|
this.messages = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect() {
|
public disconnect() {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Enclave from "@/app/app";
|
||||||
|
import ChannelPage from "./channel/ChannelPage";
|
||||||
|
import { Channel } from "@/lib/types";
|
||||||
|
|
||||||
|
export type ChannelPageProps = {
|
||||||
|
kind: "channel";
|
||||||
|
channel: Channel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Page = ChannelPageProps;
|
||||||
|
|
||||||
|
export default function Page({
|
||||||
|
appRef,
|
||||||
|
}: {
|
||||||
|
appRef: React.RefObject<Enclave | null>;
|
||||||
|
}) {
|
||||||
|
if (!appRef.current?.page?.kind) return null;
|
||||||
|
|
||||||
|
switch (appRef.current?.page?.kind) {
|
||||||
|
case "channel":
|
||||||
|
return <ChannelPage appRef={appRef} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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<Enclave<ChannelPageProps> | null>;
|
||||||
|
}) {
|
||||||
|
if (!appRef.current?.page?.channel.kind) return null;
|
||||||
|
|
||||||
|
switch (appRef.current?.page?.channel.kind) {
|
||||||
|
case "text":
|
||||||
|
return <TextChannel appRef={appRef} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function TextChannel({
|
||||||
|
appRef,
|
||||||
|
}: {
|
||||||
|
appRef: React.RefObject<Enclave<ChannelPageProps> | null>;
|
||||||
|
}) {
|
||||||
|
const [currentMessage, setCurrentMessage] = useState("");
|
||||||
|
const channel = appRef.current?.page?.channel;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
appRef.current?.server?.websocket?.send({
|
||||||
|
method: "GetMessages",
|
||||||
|
channel_id: channel.id,
|
||||||
|
chunk: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
appRef.current?.forceRender();
|
||||||
|
}, [channel]);
|
||||||
|
|
||||||
|
if (!channel) return null;
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
appRef.current?.sendMessage(currentMessage, channel.id);
|
||||||
|
setCurrentMessage("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border">
|
||||||
|
<h2>{channel.name}</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="h-full flex flex-col gap-2.5 px-3 pt-4 overflow-y-scroll">
|
||||||
|
{appRef.current?.server?.messages[channel.id] &&
|
||||||
|
Object.entries(appRef.current?.server?.messages[channel.id]).map(
|
||||||
|
([_, message]) => (
|
||||||
|
<TextMessage key={message.id} appRef={appRef} message={message} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pb-6 px-4 flex flex-row gap-3">
|
||||||
|
<Input
|
||||||
|
className="h-10"
|
||||||
|
placeholder={`Message ${channel.name}`}
|
||||||
|
value={currentMessage}
|
||||||
|
onChange={(e) => setCurrentMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{currentMessage.trim() && <Button onClick={sendMessage}>Send</Button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextMessage({
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
appRef: React.RefObject<Enclave<ChannelPageProps> | null>;
|
||||||
|
message: StoredMessage;
|
||||||
|
}) {
|
||||||
|
const time = new Date(message.timestamp).toLocaleTimeString([], {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg flex gap-3 px-3 py-3 hover:bg-muted/40">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarFallback>
|
||||||
|
{message.author.slice(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{message.author.slice(0, 8)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="whitespace-pre-wrap break-words text-sm leading-snug">
|
||||||
|
{message.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ export default function AccountCard({
|
|||||||
<Card className="px-3 py-3 h-full w-full flex flex-row">
|
<Card className="px-3 py-3 h-full w-full flex flex-row">
|
||||||
<div className="flex gap-2.5 items-center w-full">
|
<div className="flex gap-2.5 items-center w-full">
|
||||||
<Avatar className="h-full w-auto aspect-square">
|
<Avatar className="h-full w-auto aspect-square">
|
||||||
<AvatarImage src={account.avatar} />
|
<AvatarImage src={account.meta.avatar} />
|
||||||
<AvatarFallback>{account.displayName[0]}</AvatarFallback>
|
<AvatarFallback>{account.meta.displayName[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<span
|
<span
|
||||||
@@ -34,7 +34,7 @@ export default function AccountCard({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>{account.displayName}</p>
|
<p>{account.meta.displayName}</p>
|
||||||
<CopyIcon className="text-muted-foreground size-3" />
|
<CopyIcon className="text-muted-foreground size-3" />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground">Online</span>
|
<span className="text-muted-foreground">Online</span>
|
||||||
|
|||||||
@@ -68,16 +68,16 @@ export function NewProfilePage({
|
|||||||
|
|
||||||
const { secretKey } = ed.keygen();
|
const { secretKey } = ed.keygen();
|
||||||
|
|
||||||
const newAccount = {
|
|
||||||
displayName: name,
|
|
||||||
privateKey: base58.encode(secretKey),
|
|
||||||
avatar: avatar ?? undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
appRef.current.accounts.activeAccount =
|
appRef.current.accounts.activeAccount =
|
||||||
appRef.current.accounts.accounts.length;
|
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);
|
saveAccounts(appRef.current.accounts);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Enclave from "@/app/app";
|
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 { ChevronDown, ChevronUp, HashIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AccountCard from "./AccountCard";
|
import AccountCard from "./AccountCard";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
||||||
switch (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<Enclave | null>;
|
||||||
|
channel: Channel;
|
||||||
|
}) {
|
||||||
if (channel.kind !== "category") return null;
|
if (channel.kind !== "category") return null;
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
@@ -33,19 +40,41 @@ export function RenderCategory({ channel }: { channel: Channel }) {
|
|||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="pt-0.5 w-full flex flex-col gap-1">
|
<div className="pt-0.5 w-full flex flex-col gap-1">
|
||||||
<RenderChannels channels={channel.channels} />
|
<RenderChannels appRef={appRef} channels={channel.channels} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenderChannels({ channels }: { channels: Channel[] }) {
|
export function RenderChannels({
|
||||||
|
appRef,
|
||||||
|
channels,
|
||||||
|
}: {
|
||||||
|
appRef: React.RefObject<Enclave | null>;
|
||||||
|
channels: Channel[];
|
||||||
|
}) {
|
||||||
return channels.map((channel) =>
|
return channels.map((channel) =>
|
||||||
channel.kind === "category" ? (
|
channel.kind === "category" ? (
|
||||||
<RenderCategory key={channel.id} channel={channel} />
|
<RenderCategory key={channel.id} appRef={appRef} channel={channel} />
|
||||||
) : (
|
) : (
|
||||||
<div key={channel.id} className="w-full hover:bg-accent px-2.5 py-1.5 rounded-md flex gap-2.5 items-center select-none cursor-default text-sm text-muted-foreground">
|
<div
|
||||||
|
key={channel.id}
|
||||||
|
className={cn(
|
||||||
|
"w-full hover:bg-accent px-2.5 py-1.5 rounded-md flex gap-2.5 items-center select-none cursor-default text-sm text-muted-foreground",
|
||||||
|
channel.id === appRef.current?.page?.channel.id,
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!appRef.current) {
|
||||||
|
console.error("AppRef is not initialized yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appRef.current.page = { kind: "channel", channel };
|
||||||
|
|
||||||
|
appRef.current.forceRender();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChannelIcon kind={channel.kind} />
|
<ChannelIcon kind={channel.kind} />
|
||||||
{channel.name}
|
{channel.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +96,10 @@ export default function Sidebar({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="px-1.5 pt-3.5 w-full flex flex-col gap-1">
|
<section className="px-1.5 pt-3.5 w-full flex flex-col gap-1">
|
||||||
<RenderChannels channels={appRef.current.server.meta.channels} />
|
<RenderChannels
|
||||||
|
appRef={appRef}
|
||||||
|
channels={appRef.current.server.meta.channels}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { ClientMeta } from "./types";
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
displayName: string;
|
meta: ClientMeta;
|
||||||
avatar?: string;
|
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ServerMeta } from "@/app/protocol";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { ServerMeta } from "./types";
|
||||||
|
|
||||||
export interface KnownServer {
|
export interface KnownServer {
|
||||||
meta: ServerMeta;
|
meta: ServerMeta;
|
||||||
|
|||||||
@@ -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[] };
|
||||||
Reference in New Issue
Block a user