This commit is contained in:
2025-06-03 11:44:51 +03:00
parent 074d6674b8
commit 8c42b7dadd
18 changed files with 6923 additions and 190 deletions

View File

@@ -10,7 +10,7 @@ import {
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog"; // Shadcn UI Dialog
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; // Shadcn UI Tooltip
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; // Shadcn UI Tooltip
import type { UploadedFile } from "~/lib/api/types"; // Adjust path as needed
import { formatFileSize } from "~/lib/utils"; // Adjust path
import { FileIcon } from "./file-icon";
@@ -28,7 +28,6 @@ export default function ChatMessageAttachment({ file }: ChatMessageAttachmentPro
function GenericFileAttachment({ file }: ChatMessageAttachmentProps) {
return (
<TooltipProvider delayDuration={100}>
<div className="flex items-center gap-3 rounded-lg border bg-card p-3 shadow-sm ">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md">
<FileIcon className="h-8 w-8 text-muted-foreground" contentType={file.contentType} />
@@ -38,7 +37,7 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) {
<p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
</div>
<div className="flex items-center gap-1">
<Tooltip>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" asChild>
<a href={file.url} target="_blank" rel="noreferrer" download={file.filename}>
@@ -49,7 +48,7 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) {
</TooltipTrigger>
<TooltipContent>Download</TooltipContent>
</Tooltip>
<Tooltip>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" asChild>
<a href={file.url} target="_blank" rel="noreferrer">
@@ -62,17 +61,18 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) {
</Tooltip>
</div>
</div>
</TooltipProvider>
);
}
function ImageAttachment({ file }: ChatMessageAttachmentProps) {
return (
<Dialog>
<TooltipProvider delayDuration={100}>
<div className="group relative w-48 cursor-pointer sm:w-64">
<div className="relative w-48 sm:w-64">
<DialogTrigger asChild>
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-lg border bg-muted">
<AspectRatio
ratio={16 / 9}
className="group cursor-pointer overflow-hidden rounded-lg border bg-muted"
>
<img
src={file.url}
alt={file.filename}
@@ -89,7 +89,6 @@ function ImageAttachment({ file }: ChatMessageAttachmentProps) {
</p>
</div>
</div>
</TooltipProvider>
<DialogContent className="max-w-3xl p-0">
<DialogHeader className="p-4 pb-0">

View File

@@ -3,6 +3,7 @@ import { NavLink } from "react-router";
import type { Server } from "~/lib/api/types";
import { cn, getFirstLetters } from "~/lib/utils";
import { Button } from "../ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export interface ServerButtonProps {
server: Server;
@@ -12,7 +13,13 @@ export function ServerButton({ server }: ServerButtonProps) {
return (
<NavLink to={`/app/server/${server.id}`}>
{({ isActive }) => (
<Button variant="outline" size="none" className={cn("overflow-hidden", isActive ? "bg-accent" : "")}>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<Button
variant="outline"
size="none"
className={cn("overflow-hidden", isActive ? "bg-accent" : "")}
>
<div className="flex items-center justify-center size-12">
<Avatar className="rounded-none">
<AvatarImage src={server.iconUrl} className="rounded-none" />
@@ -20,6 +27,11 @@ export function ServerButton({ server }: ServerButtonProps) {
</Avatar>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p className="text-xs">{server.name}</p>
</TooltipContent>
</Tooltip>
)}
</NavLink>
);

View File

@@ -35,7 +35,7 @@ export function WebRTCConnectionManager() {
voiceState.leaveVoiceChannel();
unsubscribe();
};
});
}, []);
useEffect(() => {
if (webrtc.status === ConnectionState.DISCONNECTED) {

View File

@@ -7,7 +7,7 @@ import type { Uuid } from "~/lib/api/types"; // Adjust path
import { cn, formatFileSize } from "~/lib/utils"; // Adjust path
import TextBox from "./custom-ui/text-box"; // Adjust path, assuming TextBox is in ./custom-ui/
import { Button } from "./ui/button"; // Adjust path
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; // Adjust path
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; // Adjust path
export interface MessageBoxProps {
channelId: string;
@@ -96,11 +96,10 @@ export default function MessageBox({ channelId }: MessageBoxProps) {
>
<FileIcon contentType={file.type} className="h-7 w-7 flex-shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-card-foreground">{file.name}</p>
<p className="font-medium text-card-foreground">{file.name}</p>
<p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
</div>
<TooltipProvider delayDuration={100}>
<Tooltip>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -117,7 +116,6 @@ export default function MessageBox({ channelId }: MessageBoxProps) {
<p>Remove attachment</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
))}
</div>
@@ -133,8 +131,7 @@ export default function MessageBox({ channelId }: MessageBoxProps) {
gridTemplateRows: "auto 1fr",
}}
>
<TooltipProvider delayDuration={100}>
<Tooltip>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<div className="self-start row-start-1 col-start-1 row-span-2 col-span-1">
<input
@@ -166,7 +163,6 @@ export default function MessageBox({ channelId }: MessageBoxProps) {
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="self-center row-start-1 col-start-2 row-span-2 col-span-1">
<TextBox

View File

@@ -12,7 +12,7 @@ export default function DeleteServerConfirmModal() {
};
const onConfirm = async () => {
await import("~/lib/api/client/server").then((m) => m.default.delet((data as { serverId: string }).serverId));
await import("~/lib/api/client/server").then((m) => m.default.deleteServer((data as { serverId: string }).serverId));
onClose();
};

View File

@@ -3,6 +3,25 @@ import { getUser } from "~/lib/api/client/user";
import type { UserId } from "~/lib/api/types";
import { useUsersStore } from "~/stores/users-store";
async function fetchCurrentUser() {
const { setCurrentUserId, addUser } = useUsersStore.getState();
const user = await import("~/lib/api/client/user").then((m) => m.default.me());
setCurrentUserId(user.id);
addUser(user);
return null;
}
export const useFetchCurrentUser = () => {
const query = useQuery({
queryKey: ["users", "@me"],
queryFn: fetchCurrentUser,
});
return query;
};
export const useFetchUser = (userId: UserId) => {
const query = useQuery({
queryKey: ["users", userId],

View File

@@ -29,7 +29,7 @@ export async function get(serverId: ServerId) {
return response.data as Server;
}
export async function delet(serverId: ServerId) {
export async function deleteServer(serverId: ServerId) {
const response = await axios.delete(`/servers/${serverId}`);
return response.data as Server;
@@ -76,7 +76,7 @@ export default {
list,
listChannels,
get,
delet,
deleteServer,
createChannel,
getChannel,
deleteChannel,

View File

@@ -1 +1,3 @@
export const API_URL = "http://localhost:12345/api/v1";
export const API_URL = "http://192.168.0.133:12345/api/v1";
export const GATEWAY_URL = "ws://192.168.0.133:12345/gateway/ws";
export const VOICE_GATEWAY_URL = "ws://192.168.0.133:12345/voice/ws";

View File

@@ -0,0 +1,11 @@
import { useFetchCurrentUser } from "~/hooks/use-fetch-user";
interface WrapperProps {
children: React.ReactNode;
}
export default function CurrentUserProvider({ children }: WrapperProps) {
useFetchCurrentUser();
return <>{children}</>;
}

View File

@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query";
import { Outlet } from "react-router";
import AppLayout from "~/components/app-layout";
import { useServerListStore } from "~/stores/server-list-store";
import { useUsersStore } from "~/stores/users-store";
async function fetchServers() {
const { addServers } = useServerListStore.getState();
@@ -13,27 +12,12 @@ async function fetchServers() {
return null;
}
async function fetchCurrentUser() {
const { setCurrentUserId, addUser } = useUsersStore.getState();
const user = await import("~/lib/api/client/user").then((m) => m.default.me());
setCurrentUserId(user.id);
addUser(user);
return null;
}
export default function Layout() {
useQuery({
queryKey: ["servers"],
queryFn: fetchServers,
});
useQuery({
queryKey: ["users", "@me"],
queryFn: fetchCurrentUser,
});
return (
<AppLayout>
<Outlet />

View File

@@ -4,6 +4,7 @@ import { GatewayWebSocketConnectionManager } from "~/components/manager/gateway-
import { WebRTCConnectionManager } from "~/components/manager/webrtc-connection-manager";
import ModalProvider from "~/components/providers/modal-provider";
import { useTokenStore } from "~/stores/token-store";
import CurrentUserProvider from "./current-user-provider";
export async function clientLoader() {
const token = useTokenStore.getState().token;
@@ -29,10 +30,12 @@ export default function Layout() {
return (
<QueryClientProvider client={queryClient}>
<>
<CurrentUserProvider>
<ModalProvider />
<GatewayWebSocketConnectionManager />
<WebRTCConnectionManager />
<Outlet />
</CurrentUserProvider>
</>
</QueryClientProvider>
);

View File

@@ -1,6 +1,7 @@
import type { QueryClient } from "@tanstack/react-query";
import { create } from "zustand";
import { messageSchema, type ChannelId, type Message, type MessageId, type ServerId } from "~/lib/api/types";
import { GATEWAY_URL } from "~/lib/consts";
import { GatewayClient } from "~/lib/websocket/gateway/client";
import { ConnectionState, EventType, type EventData, type VoiceServerUpdateEvent } from "~/lib/websocket/gateway/types";
import { useChannelsVoiceStateStore } from "./channels-voice-state";
@@ -9,8 +10,6 @@ import { useServerChannelsStore } from "./server-channels-store";
import { useServerListStore } from "./server-list-store";
import { useUsersStore } from "./users-store";
const GATEWAY_URL = "ws://localhost:12345/gateway/ws";
const HANDLERS = {
[EventType.ADD_SERVER]: (self: GatewayState, data: Extract<EventData, { type: EventType.ADD_SERVER }>["data"]) => {
useServerListStore.getState().addServer(data.server);

View File

@@ -16,7 +16,6 @@ export const usePrivateChannelsStore = create<PrivateChannelsStore>()(
set((state) => {
for (const channel of channels) {
state.channels[channel.id] = channel;
console.log("add channel", channel);
}
}),
addChannel: (channel: RecipientChannel) =>

View File

@@ -1,54 +1,20 @@
import { create as batshitCreate, keyResolver } from "@yornaath/batshit";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { getUser } from "~/lib/api/client/user";
import type { FullUser, PartialUser, UserId } from "~/lib/api/types";
type UsersStore = {
users: Record<UserId, PartialUser>;
currentUserId: UserId | undefined;
fetchUsersIfNotPresent: (userIds: UserId[]) => Promise<void>;
addUser: (user: PartialUser) => void;
removeUser: (userId: UserId) => void;
setCurrentUserId: (userId: UserId) => void;
getCurrentUser: () => FullUser | undefined;
};
const usersFetcher = batshitCreate({
fetcher: async (userIds: UserId[]) => {
const users = [];
for (const userId of userIds) {
users.push(getUser(userId));
}
return await Promise.all(users);
},
resolver: keyResolver("id"),
});
export const useUsersStore = create<UsersStore>()(
immer((set, get) => ({
users: {},
currentUserId: undefined,
fetchUsersIfNotPresent: async (userIds) => {
const userPromises: Promise<PartialUser>[] = [];
for (const userId of userIds) {
const user = get().users[userId];
if (!user) {
userPromises.push(usersFetcher.fetch(userId));
}
}
const users = await Promise.all(userPromises);
const activeUsers = users.filter(Boolean);
set((state) => {
for (const user of activeUsers) {
if (user?.id) state.users[user.id] = user;
}
});
},
addUser: (user) =>
set((state) => {
if (user.id !== get().currentUserId) state.users[user.id] = user;
@@ -68,6 +34,9 @@ export const useUsersStore = create<UsersStore>()(
state.currentUserId = userId;
}),
getCurrentUser: () => (get().currentUserId ? (get().users[get().currentUserId!] as FullUser) : undefined),
getCurrentUser: () => {
const currentUserId = get().currentUserId;
return currentUserId ? (get().users[currentUserId] as FullUser) : undefined;
},
})),
);

View File

@@ -1,9 +1,9 @@
import { create } from "zustand";
import { VOICE_GATEWAY_URL } from "~/lib/consts";
import { WebRTCClient } from "~/lib/websocket/voice/client";
import { ConnectionState } from "~/lib/websocket/voice/types";
import { useVoiceStateStore } from "./voice-state-store";
const VOICE_GATEWAY_URL = "ws://localhost:12345/voice/ws";
interface WebRTCState {
client: WebRTCClient | null;

View File

@@ -1,4 +1,4 @@
import js from "@eslint/js";
import eslint from "@eslint/js";
import pluginReact from "eslint-plugin-react";
import reactCompiler from "eslint-plugin-react-compiler";
import reactHooks from "eslint-plugin-react-hooks";
@@ -7,19 +7,22 @@ import globals from "globals";
import tseslint from "typescript-eslint";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js },
extends: ["js/recommended"],
},
eslint.configs.recommended,
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
languageOptions: { globals: globals.browser },
},
...tseslint.configs.recommended,
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
reactHooks.configs["recommended-latest"],
reactCompiler.configs.recommended,
{
settings: {
react: {
version: "detect",
},
},
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
rules: {

6737
frontend.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"dev": "react-router dev --host",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},