diff --git a/src/app/app.ts b/src/app/app.ts
index 58924f1..39b1115 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -7,6 +7,8 @@ import { base58 } from "@scure/base";
import axios from "axios";
import { Account, AccountsFile, getAccounts } from "@/lib/accounts";
import { Page } from "@/components/page/PageView";
+import { Channel } from "@/lib/types";
+import { invoke } from "@tauri-apps/api/core";
import { ClientMethod } from "./protocol";
import { Config, getConfig } from "@/lib/config";
@@ -152,6 +154,44 @@ export default class Enclave
{
this.forceRender();
}
+ public joinVoice(channel: Channel) {
+ const server = this.server;
+
+ if (!server || channel.kind !== "voice") return;
+
+ server.voiceJoin = (pin, channelId) => {
+ invoke("connect_to_vc", { hostname: server.hostname, pin, channelId })
+ .then(() => {
+ server.isInVoiceChat = true;
+ server.voiceChannelId = channelId;
+ this.forceRender();
+ })
+ .catch(console.error);
+ };
+
+ server.isInVoiceChat = true;
+ server.voiceChannelId = channel.id;
+
+ server.websocket?.send({ method: "JoinVoice", channel_id: channel.id });
+
+ this.forceRender();
+ }
+
+ public leaveVoice() {
+ const server = this.server;
+
+ if (!server || !server.isInVoiceChat) return;
+
+ server.websocket?.send({ method: "LeaveVoice" });
+ invoke("disconnect_from_vc").catch(console.error);
+
+ server.isInVoiceChat = false;
+ server.voiceChannelId = undefined;
+ server.voiceChatSpeakers = {};
+
+ this.forceRender();
+ }
+
public async onMessage(msg: ClientMethod) {
const server = this.server;
diff --git a/src/app/server.ts b/src/app/server.ts
index a3e56d1..c4e20f5 100644
--- a/src/app/server.ts
+++ b/src/app/server.ts
@@ -36,6 +36,8 @@ export default class EnclaveServer {
public voiceJoin?: (pin: bigint, channelId: string) => void;
public voiceChatUsers: Record;
public voiceChatSpeakers: Record;
+ public isInVoiceChat: boolean;
+ public voiceChannelId?: string;
public constructor(hostname: string, isSecure: boolean) {
this.hostname = hostname;
@@ -44,6 +46,7 @@ export default class EnclaveServer {
this.users = {};
this.voiceChatUsers = {};
this.voiceChatSpeakers = {};
+ this.isInVoiceChat = false;
}
public disconnect() {
diff --git a/src/components/page/channel/VoiceChannel.tsx b/src/components/page/channel/VoiceChannel.tsx
index 52a7ed0..25e2366 100644
--- a/src/components/page/channel/VoiceChannel.tsx
+++ b/src/components/page/channel/VoiceChannel.tsx
@@ -1,11 +1,9 @@
import Enclave from "@/app/app";
import { ChannelPageProps } from "../PageView";
-import { useEffect, useRef, useState } from "react";
-import { invoke } from "@tauri-apps/api/core";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
-import { Phone, PhoneOff } from "lucide-react";
+import { Phone } from "lucide-react";
export default function VoiceChannel({
appRef,
@@ -15,50 +13,9 @@ export default function VoiceChannel({
const channel = appRef.current?.page?.channel;
if (!channel) return null;
- const [isConnected, setIsConnected] = useState(false);
- const lastChannelId = useRef(null);
-
- // Leave current channel setup
- const handleLeave = () => {
- appRef.current?.server?.websocket?.send({ method: "LeaveVoice" });
- invoke("disconnect_from_vc").catch(console.error);
- setIsConnected(false);
- };
-
- // Explicit action to join the voice channel
- const handleJoin = () => {
- const hostname = appRef.current?.server?.hostname;
- if (!hostname || channel.kind !== "voice" || !appRef.current?.server)
- return;
-
- appRef.current.server.voiceJoin = (pin, channelId) => {
- invoke("connect_to_vc", { hostname, pin, channelId })
- .then(() => setIsConnected(true))
- .catch(console.error);
- };
-
- appRef.current.server.websocket?.send({
- method: "JoinVoice",
- channel_id: channel.id,
- });
-
- setIsConnected(true);
- };
-
- // Clean up when channel changes or component unmounts
- useEffect(() => {
- if (lastChannelId.current !== channel.id) {
- if (isConnected) {
- invoke("disconnect_from_vc").catch(console.error);
- setIsConnected(false);
- }
- lastChannelId.current = channel.id;
- }
-
- return () => {
- invoke("disconnect_from_vc").catch(console.error);
- };
- }, [channel.id]);
+ const isConnected =
+ appRef.current?.server?.isInVoiceChat &&
+ appRef.current.server.voiceChannelId === channel.id;
const speakers =
appRef.current?.server?.voiceChatSpeakers &&
@@ -141,27 +98,18 @@ export default function VoiceChannel({
{/* Bottom Control Bar */}
-
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/src/components/view/AccountCard.tsx b/src/components/view/AccountCard.tsx
deleted file mode 100644
index d5d63e4..0000000
--- a/src/components/view/AccountCard.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-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.meta.displayName[0]}
-
-
-
{
- navigator.clipboard.writeText(
- base58.encode(
- ed.getPublicKey(base58.decode(account.privateKey)),
- ),
- );
- }}
- >
- {account.meta.displayName}
-
-
-
Online
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/view/Sidebar.tsx b/src/components/view/Sidebar.tsx
index 1fe61fa..5113fb0 100644
--- a/src/components/view/Sidebar.tsx
+++ b/src/components/view/Sidebar.tsx
@@ -2,7 +2,7 @@ import Enclave from "@/app/app";
import { Channel, ChannelKind } from "@/lib/types";
import { ChevronDown, ChevronUp, HashIcon, Volume2Icon } from "lucide-react";
import { useState } from "react";
-import AccountCard from "./AccountCard";
+import StatusCard from "./StatusCard";
import { cn } from "@/lib/utils";
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
@@ -106,7 +106,7 @@ export default function Sidebar({
>
)}
-
+
);
}
diff --git a/src/components/view/StatusCard.tsx b/src/components/view/StatusCard.tsx
new file mode 100644
index 0000000..a9fa701
--- /dev/null
+++ b/src/components/view/StatusCard.tsx
@@ -0,0 +1,102 @@
+import { Card } from "../ui/card";
+import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
+import { CopyIcon, PhoneOff, Settings2Icon, Signal } from "lucide-react";
+import { Button } from "../ui/button";
+import Enclave from "@/app/app";
+import { base58 } from "@scure/base";
+import * as ed from "@noble/ed25519";
+import { Channel } from "@/lib/types";
+
+function findChannel(channels: Channel[], id: string): Channel | undefined {
+ for (const channel of channels) {
+ if (channel.id === id) return channel;
+
+ if (channel.kind === "category") {
+ const found = findChannel(channel.channels, id);
+ if (found) return found;
+ }
+ }
+
+ return undefined;
+}
+
+export default function StatusCard({
+ appRef,
+}: {
+ appRef: React.RefObject;
+}) {
+ const account = appRef.current?.getAccount();
+ const server = appRef.current?.server;
+
+ const voiceChannelId = server?.voiceChannelId;
+ const voiceChannel =
+ voiceChannelId && server?.meta
+ ? findChannel(server.meta.channels, voiceChannelId)
+ : undefined;
+
+ if (!account) return null;
+
+ return (
+
+
+ {server?.isInVoiceChat && voiceChannel && (
+
+
+
+ Voice Connected
+
+ {voiceChannel.name}
+
+
+
+
+ )}
+
+
+
+
+ {account.meta.displayName[0]}
+
+
+
{
+ navigator.clipboard.writeText(
+ base58.encode(
+ ed.getPublicKey(base58.decode(account.privateKey)),
+ ),
+ );
+ }}
+ >
+ {account.meta.displayName}
+
+
+
Online
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file