.
This commit is contained in:
@@ -14,11 +14,11 @@ interface AppLayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AppLayout({ children }: AppLayoutProps) {
|
export default function AppLayout({ children }: AppLayoutProps) {
|
||||||
let servers = Object.values(useServerListStore(useShallow((state) => state.servers)));
|
const servers = Object.values(useServerListStore(useShallow((state) => state.servers)));
|
||||||
|
|
||||||
const matches = useMatches();
|
const matches = useMatches();
|
||||||
|
|
||||||
let list = React.useMemo(() => {
|
const list = React.useMemo(() => {
|
||||||
return matches
|
return matches
|
||||||
.map(
|
.map(
|
||||||
(match) =>
|
(match) =>
|
||||||
@@ -46,7 +46,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
|
|||||||
<aside className="flex flex-col gap-2 p-2 h-full">
|
<aside className="flex flex-col gap-2 p-2 h-full">
|
||||||
<HomeButton />
|
<HomeButton />
|
||||||
<Separator />
|
<Separator />
|
||||||
{servers.map((server, _) => (
|
{servers.map((server) => (
|
||||||
<React.Fragment key={server.id}>
|
<React.Fragment key={server.id}>
|
||||||
<ServerButton server={server} />
|
<ServerButton server={server} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ export default function ChannelArea({ channel }: ChannelAreaProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isPending, status } =
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending, status } = useInfiniteQuery({
|
||||||
useInfiniteQuery({
|
queryKey: ["messages", channelId],
|
||||||
queryKey: ["messages", channelId],
|
initialPageParam: undefined,
|
||||||
initialPageParam: undefined,
|
queryFn: fetchMessages,
|
||||||
queryFn: fetchMessages,
|
getNextPageParam: (lastPage) => (lastPage.length < 50 ? undefined : lastPage[lastPage.length - 1]?.id),
|
||||||
getNextPageParam: (lastPage) => (lastPage.length < 50 ? undefined : lastPage[lastPage.length - 1]?.id),
|
staleTime: Infinity,
|
||||||
staleTime: Infinity,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const fetchNextPageVisible = () => {
|
const fetchNextPageVisible = () => {
|
||||||
if (!isFetchingNextPage && hasNextPage) fetchNextPage();
|
if (!isFetchingNextPage && hasNextPage) fetchNextPage();
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
<div className="row-start-2 col-start-2 row-span-1 col-span-1">
|
<div className="row-start-2 col-start-2 row-span-1 col-span-1">
|
||||||
<div className="wrap-break-word contain-inline-size">{message.content}</div>
|
<div className="wrap-break-word contain-inline-size">{message.content}</div>
|
||||||
<div className="grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 justify-start">
|
<div className="grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 justify-start">
|
||||||
{message.attachments.map((file, i) => (
|
{message.attachments.map((file) => (
|
||||||
<div key={file.id}>
|
<div key={file.id}>
|
||||||
<ChatMessageAttachment file={file} />
|
<ChatMessageAttachment file={file} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface ChannelListItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onDeleteChannel = async (channel: ServerChannel) => {
|
const onDeleteChannel = async (channel: ServerChannel) => {
|
||||||
const response = await deleteChannel(channel.serverId, channel.id);
|
await deleteChannel(channel.serverId, channel.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChannelItemWrapperProps {
|
interface ChannelItemWrapperProps {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { ModalType, useModalStore } from "~/stores/modal-store";
|
|
||||||
import { useTokenStore } from "~/stores/token-store";
|
import { useTokenStore } from "~/stores/token-store";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -13,13 +12,8 @@ import {
|
|||||||
|
|
||||||
export function SettingsButton() {
|
export function SettingsButton() {
|
||||||
const setToken = useTokenStore((state) => state.setToken);
|
const setToken = useTokenStore((state) => state.setToken);
|
||||||
const onOpen = useModalStore((state) => state.onOpen);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onUpdateProfile = () => {
|
|
||||||
onOpen(ModalType.UPDATE_PROFILE);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenSettings = () => {
|
const onOpenSettings = () => {
|
||||||
navigate("/app/settings");
|
navigate("/app/settings");
|
||||||
};
|
};
|
||||||
@@ -33,7 +27,7 @@ export function SettingsButton() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<Settings className="size-5 m-1.5" />
|
<Settings className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PhoneMissed, Signal } from "lucide-react";
|
import { PhoneMissed, Signal } from "lucide-react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useServerChannelsStore } from "~/stores/server-channels-store";
|
import { useServerChannelsStore } from "~/stores/server-channels-store";
|
||||||
import { useServerListStore } from "~/stores/server-list-store";
|
import { useServerListStore } from "~/stores/server-list-store";
|
||||||
import { useUsersStore } from "~/stores/users-store";
|
import { useUsersStore } from "~/stores/users-store";
|
||||||
@@ -11,14 +12,14 @@ import { OnlineStatus } from "./online-status";
|
|||||||
import { SettingsButton } from "./settings-button";
|
import { SettingsButton } from "./settings-button";
|
||||||
|
|
||||||
function VoiceStatus({ voiceState }: { voiceState: { serverId: string; channelId: string } }) {
|
function VoiceStatus({ voiceState }: { voiceState: { serverId: string; channelId: string } }) {
|
||||||
// const webrtcState = useWebRTCStore(state => state.status)
|
|
||||||
|
|
||||||
const leaveVoiceChannel = () => {
|
const leaveVoiceChannel = () => {
|
||||||
useVoiceStateStore.getState().leaveVoiceChannel();
|
useVoiceStateStore.getState().leaveVoiceChannel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const channel = useServerChannelsStore((state) => state.channels[voiceState.serverId]?.[voiceState.channelId]);
|
const channel = useServerChannelsStore(
|
||||||
const server = useServerListStore((state) => state.servers[voiceState.serverId]);
|
useShallow((state) => state.channels[voiceState.serverId]?.[voiceState.channelId]),
|
||||||
|
);
|
||||||
|
const server = useServerListStore(useShallow((state) => state.servers[voiceState.serverId]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gap-1 flex justify-between items-center ">
|
<div className="gap-1 flex justify-between items-center ">
|
||||||
@@ -29,8 +30,8 @@ function VoiceStatus({ voiceState }: { voiceState: { serverId: string; channelId
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="secondary" size="none" onClick={leaveVoiceChannel}>
|
<Button variant="destructive" size="icon" onClick={leaveVoiceChannel}>
|
||||||
<PhoneMissed className="size-5 m-1.5" />
|
<PhoneMissed className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTokenStore } from "~/stores/token-store";
|
|||||||
export function GatewayWebSocketConnectionManager() {
|
export function GatewayWebSocketConnectionManager() {
|
||||||
const token = useTokenStore((state) => state.token);
|
const token = useTokenStore((state) => state.token);
|
||||||
|
|
||||||
const { setQueryClient } = useGatewayStore();
|
const setQueryClient = useGatewayStore((state) => state.setQueryClient);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -17,7 +17,7 @@ export function GatewayWebSocketConnectionManager() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { status, connect, disconnect } = useGatewayStore.getState();
|
const { status, connect, disconnect } = useGatewayStore.getState();
|
||||||
|
|
||||||
if (!!token) {
|
if (token) {
|
||||||
connect(token);
|
connect(token);
|
||||||
} else {
|
} else {
|
||||||
if (status === ConnectionState.CONNECTED) {
|
if (status === ConnectionState.CONNECTED) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function WebRTCConnectionManager() {
|
|||||||
voiceState.leaveVoiceChannel();
|
voiceState.leaveVoiceChannel();
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webrtc.status === ConnectionState.DISCONNECTED) {
|
if (webrtc.status === ConnectionState.DISCONNECTED) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function CreateServerChannelModal() {
|
|||||||
|
|
||||||
const isModalOpen = type === ModalType.CREATE_SERVER_CHANNEL && isOpen;
|
const isModalOpen = type === ModalType.CREATE_SERVER_CHANNEL && isOpen;
|
||||||
|
|
||||||
let form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: ChannelType.SERVER_TEXT,
|
type: ChannelType.SERVER_TEXT,
|
||||||
@@ -40,7 +40,7 @@ export default function CreateServerChannelModal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof schema>) => {
|
const onSubmit = async (values: z.infer<typeof schema>) => {
|
||||||
const response = await import("~/lib/api/client/server").then((m) =>
|
await import("~/lib/api/client/server").then((m) =>
|
||||||
m.default.createChannel((data as CreateServerChannelModalData["data"]).serverId, values),
|
m.default.createChannel((data as CreateServerChannelModalData["data"]).serverId, values),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function CreateServerInviteModal() {
|
|||||||
const isModalOpen = type === ModalType.CREATE_SERVER_INVITE && isOpen;
|
const isModalOpen = type === ModalType.CREATE_SERVER_INVITE && isOpen;
|
||||||
const inviteLink = `${origin}/app/invite/${inviteCode}`;
|
const inviteLink = `${origin}/app/invite/${inviteCode}`;
|
||||||
|
|
||||||
const onOpenChange = (openState: boolean) => {
|
const onOpenChange = () => {
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export default function CreateServerModal() {
|
|||||||
|
|
||||||
const isModalOpen = type === ModalType.CREATE_SERVER && isOpen;
|
const isModalOpen = type === ModalType.CREATE_SERVER && isOpen;
|
||||||
|
|
||||||
let form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onOpenChange = (openState: boolean) => {
|
const onOpenChange = () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ export default function CreateServerModal() {
|
|||||||
iconId = (await file.uploadFile(values.icon))[0];
|
iconId = (await file.uploadFile(values.icon))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await server.create({
|
await server.create({
|
||||||
name: values.name,
|
name: values.name,
|
||||||
iconId,
|
iconId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function UpdateProfileModal() {
|
|||||||
|
|
||||||
const isModalOpen = type === ModalType.UPDATE_PROFILE && isOpen;
|
const isModalOpen = type === ModalType.UPDATE_PROFILE && isOpen;
|
||||||
|
|
||||||
let form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export default function UpdateProfileModal() {
|
|||||||
avatarId = (await file.uploadFile(values.avatar))[0];
|
avatarId = (await file.uploadFile(values.avatar))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await patchUser({
|
await patchUser({
|
||||||
displayName: values.displayName,
|
displayName: values.displayName,
|
||||||
avatarId,
|
avatarId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export function ThemeToggle() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<Sun className="size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const useFetchUser = (userId: UserId) => {
|
|||||||
|
|
||||||
export const useFetchUsers = (userIds: UserId[]) => {
|
export const useFetchUsers = (userIds: UserId[]) => {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["users", userIds],
|
queryKey: ["users", ...userIds],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const userIdsToFetch = userIds.filter((userId) => !useUsersStore.getState().users[userId]);
|
const userIdsToFetch = userIds.filter((userId) => !useUsersStore.getState().users[userId]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function paginatedMessages(channelId: ChannelId, limit: number, bef
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (response.data as any[]).map((value, _) => messageSchema.parse(value));
|
return (response.data as unknown[]).map((value) => messageSchema.parse(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendMessage(channelId: ChannelId, content: string, attachments?: Uuid[]) {
|
export async function sendMessage(channelId: ChannelId, content: string, attachments?: Uuid[]) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export interface FullUser {
|
|||||||
email: string;
|
email: string;
|
||||||
bot: boolean;
|
bot: boolean;
|
||||||
system: boolean;
|
system: boolean;
|
||||||
settings: any;
|
settings: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Server {
|
export interface Server {
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ export function formatFileSize(bytes: number, decimals = 2): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createPrefixedLogger(prefix: string, styles?: string[]) {
|
export function createPrefixedLogger(prefix: string, styles?: string[]) {
|
||||||
const result: Record<string, (...args: any[]) => void> = {};
|
const result: Record<string, (...args: unknown[]) => void> = {};
|
||||||
|
|
||||||
const methods = ["log", "trace", "debug", "info", "warn", "error"] as const;
|
const methods = ["log", "trace", "debug", "info", "warn", "error"] as const;
|
||||||
|
|
||||||
for (const methodName of methods) {
|
for (const methodName of methods) {
|
||||||
const originalMethod = console[methodName].bind(console);
|
const originalMethod = console[methodName].bind(console);
|
||||||
|
|
||||||
result[methodName] = (...args: any[]) => {
|
result[methodName] = (...args: unknown[]) => {
|
||||||
if (typeof args[0] === "string") {
|
if (typeof args[0] === "string") {
|
||||||
originalMethod(`${prefix} ${args[0]}`, ...(styles || []), ...args.slice(1));
|
originalMethod(`${prefix} ${args[0]}`, ...(styles || []), ...args.slice(1));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class GatewayClient {
|
|||||||
this.socket.onerror = this.onSocketError.bind(this);
|
this.socket.onerror = this.onSocketError.bind(this);
|
||||||
this.socket.onclose = this.onSocketClose.bind(this);
|
this.socket.onclose = this.onSocketClose.bind(this);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emitError(new Error("Failed to create WebSocket connection"));
|
this.emitError(new Error("Failed to create WebSocket connection", { cause: error }));
|
||||||
this.setState(ConnectionState.ERROR);
|
this.setState(ConnectionState.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,6 +242,7 @@ export class GatewayClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleEventMessage(event: EventData): void {
|
private handleEventMessage(event: EventData): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.emitEvent(event.type, event.data as any);
|
this.emitEvent(event.type, event.data as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +275,7 @@ export class GatewayClient {
|
|||||||
private emitControl<K extends keyof ControlEvents>(event: K, ...args: Parameters<ControlEvents[K]>): void {
|
private emitControl<K extends keyof ControlEvents>(event: K, ...args: Parameters<ControlEvents[K]>): void {
|
||||||
const handler = this.eventHandlers[event];
|
const handler = this.eventHandlers[event];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
(handler as Function)(...args);
|
(handler as Function)(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,6 +283,7 @@ export class GatewayClient {
|
|||||||
private emitEvent<K extends keyof GatewayEvents>(event: K, ...args: Parameters<GatewayEvents[K]>): void {
|
private emitEvent<K extends keyof GatewayEvents>(event: K, ...args: Parameters<GatewayEvents[K]>): void {
|
||||||
const handler = this.serverEventHandlers[event];
|
const handler = this.serverEventHandlers[event];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
(handler as Function)(...args);
|
(handler as Function)(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import type { ChannelId, Message, MessageId, PartialUser, Server, ServerId, UserId } from "~/lib/api/types";
|
import type {
|
||||||
|
Channel,
|
||||||
type Channel = any; // TODO: Define Channel type
|
ChannelId,
|
||||||
|
Message,
|
||||||
|
MessageId,
|
||||||
|
PartialUser,
|
||||||
|
Server,
|
||||||
|
ServerChannel,
|
||||||
|
ServerId,
|
||||||
|
UserId,
|
||||||
|
} from "~/lib/api/types";
|
||||||
|
|
||||||
export enum ServerMessageType {
|
export enum ServerMessageType {
|
||||||
AUTHENTICATE_ACCEPTED = "AUTHENTICATE_ACCEPTED",
|
AUTHENTICATE_ACCEPTED = "AUTHENTICATE_ACCEPTED",
|
||||||
@@ -111,7 +119,7 @@ export interface AddDmChannelEvent {
|
|||||||
type: EventType.ADD_DM_CHANNEL;
|
type: EventType.ADD_DM_CHANNEL;
|
||||||
data: {
|
data: {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
recipients: PartialUser[];
|
recipients: UserId[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +133,7 @@ export interface RemoveDmChannelEvent {
|
|||||||
export interface AddServerChannelEvent {
|
export interface AddServerChannelEvent {
|
||||||
type: EventType.ADD_SERVER_CHANNEL;
|
type: EventType.ADD_SERVER_CHANNEL;
|
||||||
data: {
|
data: {
|
||||||
channel: Channel;
|
channel: ServerChannel;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class WebRTCClient {
|
|||||||
this.socket.onmessage = this.handleServerMessage;
|
this.socket.onmessage = this.handleServerMessage;
|
||||||
|
|
||||||
this.socket.onerror = (event) => {
|
this.socket.onerror = (event) => {
|
||||||
this.handleError(new Error("WebSocket error occurred"));
|
this.handleError(new Error("WebSocket error occurred", { cause: event }));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.socket.onclose = (e) => {
|
this.socket.onclose = (e) => {
|
||||||
@@ -264,4 +264,4 @@ export class WebRTCClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { log, warn, ...other } = createPrefixedLogger("%cWebRTC WS%c:", ["color: blue; font-weight: bold;", ""]);
|
const { log, warn } = createPrefixedLogger("%cWebRTC WS%c:", ["color: blue; font-weight: bold;", ""]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
|||||||
const response = await import("~/lib/api/client/server").then((m) => m.default.getInvite(inviteCode));
|
const response = await import("~/lib/api/client/server").then((m) => m.default.getInvite(inviteCode));
|
||||||
|
|
||||||
return redirect(`/app/server/${response.id}`);
|
return redirect(`/app/server/${response.id}`);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return redirect("/app/@me");
|
return redirect("/app/@me");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Check } from "lucide-react";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import ChannelArea from "~/components/channel-area";
|
import ChannelArea from "~/components/channel-area";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { useFetchUsers } from "~/hooks/use-fetch-user";
|
||||||
import { usePrivateChannelsStore } from "~/stores/private-channels-store";
|
import { usePrivateChannelsStore } from "~/stores/private-channels-store";
|
||||||
import { useUsersStore } from "~/stores/users-store";
|
import { useUsersStore } from "~/stores/users-store";
|
||||||
|
|
||||||
@@ -12,18 +13,26 @@ export default function Channel({ params }: Route.ComponentProps) {
|
|||||||
|
|
||||||
const nativeChannel = usePrivateChannelsStore(useShallow((state) => state.channels[channelId]));
|
const nativeChannel = usePrivateChannelsStore(useShallow((state) => state.channels[channelId]));
|
||||||
|
|
||||||
|
const recipients = nativeChannel?.recipients?.filter((recipient) => recipient !== currentUserId) || [];
|
||||||
|
|
||||||
|
useFetchUsers(recipients);
|
||||||
|
|
||||||
|
const recipientsUsers =
|
||||||
|
useUsersStore(useShallow((state) => recipients.map((recipient) => state.users[recipient]).filter(Boolean))) ||
|
||||||
|
[];
|
||||||
|
|
||||||
if (!nativeChannel) return null;
|
if (!nativeChannel) return null;
|
||||||
|
|
||||||
const recipients = nativeChannel.recipients.filter((recipient) => recipient.id !== currentUserId);
|
const renderSystemBadge = recipientsUsers.some((recipient) => recipient.system) && recipients.length === 1;
|
||||||
|
|
||||||
const renderSystemBadge = recipients.some((recipient) => recipient.system) && recipients.length === 1;
|
|
||||||
|
|
||||||
const channel = {
|
const channel = {
|
||||||
...nativeChannel,
|
...nativeChannel,
|
||||||
name: (
|
name: (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div>{recipients.map((recipient) => recipient.displayName || recipient.username).join(", ")}</div>
|
<div>
|
||||||
|
{recipientsUsers.map((recipient) => recipient.displayName || recipient.username).join(", ")}
|
||||||
|
</div>
|
||||||
{renderSystemBadge && (
|
{renderSystemBadge && (
|
||||||
<Badge variant="default">
|
<Badge variant="default">
|
||||||
{" "}
|
{" "}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function ListComponent() {
|
|||||||
<div className="p-2 flex flex-col gap-1 h-full">
|
<div className="p-2 flex flex-col gap-1 h-full">
|
||||||
{channels
|
{channels
|
||||||
.sort((a, b) => ((a.lastMessageId ?? a.id) < (b.lastMessageId ?? b.id) ? 1 : -1))
|
.sort((a, b) => ((a.lastMessageId ?? a.id) < (b.lastMessageId ?? b.id) ? 1 : -1))
|
||||||
.map((channel, _) => (
|
.map((channel) => (
|
||||||
<React.Fragment key={channel.id}>
|
<React.Fragment key={channel.id}>
|
||||||
<ServerChannelListItem channel={channel} />
|
<ServerChannelListItem channel={channel} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function Settings() {
|
|||||||
avatarId = (await file.uploadFile(values.avatar))[0];
|
avatarId = (await file.uploadFile(values.avatar))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await patchUser({
|
await patchUser({
|
||||||
displayName:
|
displayName:
|
||||||
values.displayName === user?.displayName
|
values.displayName === user?.displayName
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export async function clientLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
let navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
let setToken = useTokenStore((state) => state.setToken);
|
const setToken = useTokenStore((state) => state.setToken);
|
||||||
const { setCurrentUserId, addUser } = useUsersStore(
|
const { setCurrentUserId, addUser } = useUsersStore(
|
||||||
useShallow((state) => {
|
useShallow((state) => {
|
||||||
return {
|
return {
|
||||||
@@ -126,7 +126,7 @@ export default function Login() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter style={{ viewTransitionName: "auth-card-footer-view" }}>
|
<CardFooter style={{ viewTransitionName: "auth-card-footer-view" }}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="text-muted-foreground text-sm">Don't have an account?</span>
|
<span className="text-muted-foreground text-sm">Don't have an account?</span>
|
||||||
<Link
|
<Link
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
variant: "link",
|
variant: "link",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
let navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof schema>>({
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
import type { Route } from "./+types/index";
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta() {
|
||||||
return [{ title: "New React Router App" }];
|
return [{ title: "New React Router App" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface ChannelsVoiceState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useChannelsVoiceStateStore = create<ChannelsVoiceState>()(
|
export const useChannelsVoiceStateStore = create<ChannelsVoiceState>()(
|
||||||
immer((set, get) => ({
|
immer((set) => ({
|
||||||
channels: {},
|
channels: {},
|
||||||
addUser: (channelId, userId, userVoiceState) =>
|
addUser: (channelId, userId, userVoiceState) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ const HANDLERS = {
|
|||||||
data: Extract<EventData, { type: EventType.REMOVE_MESSAGE }>["data"],
|
data: Extract<EventData, { type: EventType.REMOVE_MESSAGE }>["data"],
|
||||||
) => {
|
) => {
|
||||||
if (self.queryClient) {
|
if (self.queryClient) {
|
||||||
self.queryClient.setQueryData(["messages", data.channelId], (oldData: any) => {
|
self.queryClient.setQueryData(["messages", data.channelId], (oldData: Message[]) => {
|
||||||
if (!oldData) return [];
|
if (!oldData) return [];
|
||||||
return oldData.filter((message: any) => message.id !== data.messageId);
|
return oldData.filter((message: Message) => message.id !== data.messageId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,6 +159,7 @@ export const useGatewayStore = create<GatewayState>()((set, get) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const [type, handler] of Object.entries(HANDLERS)) {
|
for (const [type, handler] of Object.entries(HANDLERS)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
client.onEvent(type, (data: any) => {
|
client.onEvent(type, (data: any) => {
|
||||||
handler(get(), data);
|
handler(get(), data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type ServerChannelsStore = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useServerChannelsStore = create<ServerChannelsStore>()(
|
export const useServerChannelsStore = create<ServerChannelsStore>()(
|
||||||
immer((set, get) => ({
|
immer((set) => ({
|
||||||
channels: {},
|
channels: {},
|
||||||
addServer: (serverId) =>
|
addServer: (serverId) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type TokenStore = {
|
|||||||
|
|
||||||
export const useTokenStore = create<TokenStore>()(
|
export const useTokenStore = create<TokenStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set) => ({
|
||||||
token: undefined,
|
token: undefined,
|
||||||
setToken: (token?: string) => set({ token }),
|
setToken: (token?: string) => set({ token }),
|
||||||
removeToken: () => set({ token: undefined }),
|
removeToken: () => set({ token: undefined }),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type UsersStore = {
|
|||||||
|
|
||||||
const usersFetcher = batshitCreate({
|
const usersFetcher = batshitCreate({
|
||||||
fetcher: async (userIds: UserId[]) => {
|
fetcher: async (userIds: UserId[]) => {
|
||||||
let users = [];
|
const users = [];
|
||||||
|
|
||||||
for (const userId of userIds) {
|
for (const userId of userIds) {
|
||||||
users.push(getUser(userId));
|
users.push(getUser(userId));
|
||||||
@@ -32,7 +32,7 @@ export const useUsersStore = create<UsersStore>()(
|
|||||||
users: {},
|
users: {},
|
||||||
currentUserId: undefined,
|
currentUserId: undefined,
|
||||||
fetchUsersIfNotPresent: async (userIds) => {
|
fetchUsersIfNotPresent: async (userIds) => {
|
||||||
let userPromises: Promise<PartialUser>[] = [];
|
const userPromises: Promise<PartialUser>[] = [];
|
||||||
for (const userId of userIds) {
|
for (const userId of userIds) {
|
||||||
const user = get().users[userId];
|
const user = get().users[userId];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -68,6 +68,6 @@ export const useUsersStore = create<UsersStore>()(
|
|||||||
state.currentUserId = userId;
|
state.currentUserId = userId;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCurrentUser: () => (!!get().currentUserId ? (get().users[get().currentUserId!] as FullUser) : undefined),
|
getCurrentUser: () => (get().currentUserId ? (get().users[get().currentUserId!] as FullUser) : undefined),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface WebRTCState {
|
|||||||
createOffer: (localStream: MediaStream) => Promise<void>;
|
createOffer: (localStream: MediaStream) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWebRTCStore = create<WebRTCState>()((set, get) => {
|
export const useWebRTCStore = create<WebRTCState>()((set) => {
|
||||||
const client = new WebRTCClient(
|
const client = new WebRTCClient(
|
||||||
VOICE_GATEWAY_URL,
|
VOICE_GATEWAY_URL,
|
||||||
(state) => set({ status: state }),
|
(state) => set({ status: state }),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
|
||||||
import pluginReact from "eslint-plugin-react";
|
import pluginReact from "eslint-plugin-react";
|
||||||
|
import reactCompiler from "eslint-plugin-react-compiler";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from "eslint/config";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
@@ -16,8 +16,14 @@ export default defineConfig([
|
|||||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||||
languageOptions: { globals: globals.browser },
|
languageOptions: { globals: globals.browser },
|
||||||
},
|
},
|
||||||
tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
pluginReact.configs.recommended,
|
pluginReact.configs.flat.recommended,
|
||||||
reactHooks.configs.recommended,
|
reactHooks.configs["recommended-latest"],
|
||||||
eslintConfigPrettier,
|
reactCompiler.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user