Refactored status card
This commit is contained in:
@@ -7,6 +7,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 { Page } from "@/components/page/PageView";
|
||||||
|
import { Channel } from "@/lib/types";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { ClientMethod } from "./protocol";
|
import { ClientMethod } from "./protocol";
|
||||||
import { Config, getConfig } from "@/lib/config";
|
import { Config, getConfig } from "@/lib/config";
|
||||||
|
|
||||||
@@ -152,6 +154,44 @@ export default class Enclave<P = Page> {
|
|||||||
this.forceRender();
|
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) {
|
public async onMessage(msg: ClientMethod) {
|
||||||
const server = this.server;
|
const server = this.server;
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default class EnclaveServer {
|
|||||||
public voiceJoin?: (pin: bigint, channelId: string) => void;
|
public voiceJoin?: (pin: bigint, channelId: string) => void;
|
||||||
public voiceChatUsers: Record<string, string[]>;
|
public voiceChatUsers: Record<string, string[]>;
|
||||||
public voiceChatSpeakers: Record<string, NodeJS.Timeout>;
|
public voiceChatSpeakers: Record<string, NodeJS.Timeout>;
|
||||||
|
public isInVoiceChat: boolean;
|
||||||
|
public voiceChannelId?: string;
|
||||||
|
|
||||||
public constructor(hostname: string, isSecure: boolean) {
|
public constructor(hostname: string, isSecure: boolean) {
|
||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
@@ -44,6 +46,7 @@ export default class EnclaveServer {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.voiceChatUsers = {};
|
this.voiceChatUsers = {};
|
||||||
this.voiceChatSpeakers = {};
|
this.voiceChatSpeakers = {};
|
||||||
|
this.isInVoiceChat = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect() {
|
public disconnect() {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import Enclave from "@/app/app";
|
import Enclave from "@/app/app";
|
||||||
import { ChannelPageProps } from "../PageView";
|
import { ChannelPageProps } from "../PageView";
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Phone, PhoneOff } from "lucide-react";
|
import { Phone } from "lucide-react";
|
||||||
|
|
||||||
export default function VoiceChannel({
|
export default function VoiceChannel({
|
||||||
appRef,
|
appRef,
|
||||||
@@ -15,50 +13,9 @@ export default function VoiceChannel({
|
|||||||
const channel = appRef.current?.page?.channel;
|
const channel = appRef.current?.page?.channel;
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const isConnected =
|
||||||
const lastChannelId = useRef<string | null>(null);
|
appRef.current?.server?.isInVoiceChat &&
|
||||||
|
appRef.current.server.voiceChannelId === channel.id;
|
||||||
// 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 speakers =
|
const speakers =
|
||||||
appRef.current?.server?.voiceChatSpeakers &&
|
appRef.current?.server?.voiceChatSpeakers &&
|
||||||
@@ -141,27 +98,18 @@ export default function VoiceChannel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Control Bar */}
|
{/* Bottom Control Bar */}
|
||||||
<footer className="flex items-center justify-center px-4 py-2 border-t border-border/40 bg-card/50 rounded-xl mx-2">
|
{!isConnected && (
|
||||||
{isConnected ? (
|
<footer className="flex items-center justify-center px-4 py-2 border-t border-border/40 bg-card/50 rounded-xl mx-2">
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleLeave}
|
|
||||||
className="flex items-center gap-2 font-medium px-6"
|
|
||||||
>
|
|
||||||
<PhoneOff className="w-4 h-4" />
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleJoin}
|
onClick={() => appRef.current?.joinVoice(channel)}
|
||||||
className="flex items-center gap-2 font-medium px-6 bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="flex items-center gap-2 font-medium px-6 bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
>
|
>
|
||||||
<Phone className="w-4 h-4" />
|
<Phone className="w-4 h-4" />
|
||||||
Join Voice
|
Join Voice
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</footer>
|
||||||
</footer>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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<Enclave | null>;
|
|
||||||
}) {
|
|
||||||
const account = appRef.current?.getAccount();
|
|
||||||
|
|
||||||
if (!account) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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 w-full">
|
|
||||||
<Avatar className="h-full w-auto aspect-square">
|
|
||||||
<AvatarImage src={account.meta.avatar} />
|
|
||||||
<AvatarFallback>{account.meta.displayName[0]}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<span
|
|
||||||
className="flex items-center gap-1.5 cursor-pointer w-full"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
base58.encode(
|
|
||||||
ed.getPublicKey(base58.decode(account.privateKey)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>{account.meta.displayName}</p>
|
|
||||||
<CopyIcon className="text-muted-foreground size-3" />
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground">Online</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-muted-foreground">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="size-10"
|
|
||||||
onClick={() => {
|
|
||||||
if (!appRef.current) return;
|
|
||||||
|
|
||||||
appRef.current.isSettingsOpen = !appRef.current?.isSettingsOpen;
|
|
||||||
appRef.current.forceRender();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings2Icon className="size-5.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import Enclave from "@/app/app";
|
|||||||
import { Channel, ChannelKind } from "@/lib/types";
|
import { Channel, ChannelKind } from "@/lib/types";
|
||||||
import { ChevronDown, ChevronUp, HashIcon, Volume2Icon } from "lucide-react";
|
import { ChevronDown, ChevronUp, HashIcon, Volume2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AccountCard from "./AccountCard";
|
import StatusCard from "./StatusCard";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
export function ChannelIcon({ kind }: { kind: ChannelKind["kind"] }) {
|
||||||
@@ -106,7 +106,7 @@ export default function Sidebar({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AccountCard appRef={appRef} />
|
<StatusCard appRef={appRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Enclave | null>;
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<div className="absolute -left-16 right-16 bottom-0 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-col gap-0.5">
|
||||||
|
{server?.isInVoiceChat && voiceChannel && (
|
||||||
|
<div className="text-emerald-500 flex flex-row items-center gap-2 border-b pb-2.5">
|
||||||
|
<Signal className="h-5" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Voice Connected</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{voiceChannel.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="ml-auto"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => appRef.current?.leaveVoice()}
|
||||||
|
>
|
||||||
|
<PhoneOff />
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="h-full w-full flex flex-row">
|
||||||
|
<div className="flex gap-2.5 items-center w-full">
|
||||||
|
<Avatar className="h-14 w-auto aspect-square">
|
||||||
|
<AvatarImage src={account.meta.avatar} />
|
||||||
|
<AvatarFallback>{account.meta.displayName[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-1.5 cursor-pointer w-full"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
base58.encode(
|
||||||
|
ed.getPublicKey(base58.decode(account.privateKey)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>{account.meta.displayName}</p>
|
||||||
|
<CopyIcon className="text-muted-foreground size-3" />
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-muted-foreground">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="size-10"
|
||||||
|
onClick={() => {
|
||||||
|
if (!appRef.current) return;
|
||||||
|
|
||||||
|
appRef.current.isSettingsOpen = !appRef.current?.isSettingsOpen;
|
||||||
|
appRef.current.forceRender();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings2Icon className="size-5.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user