This commit is contained in:
2025-05-21 18:03:22 +03:00
parent 4419151510
commit 074d6674b8
34 changed files with 99 additions and 79 deletions

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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),
); );

View File

@@ -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();
}; };

View File

@@ -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,
}); });

View File

@@ -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,
}); });

View File

@@ -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>

View File

@@ -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]);

View File

@@ -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[]) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
} }
} }

View File

@@ -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;
}; };
} }

View File

@@ -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;", ""]);

View File

@@ -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");
} }
} }

View File

@@ -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">
{" "} {" "}

View File

@@ -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>

View File

@@ -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

View File

@@ -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&apos;t have an account?</span>
<Link <Link
className={buttonVariants({ className={buttonVariants({
variant: "link", variant: "link",

View File

@@ -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),

View File

@@ -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" }];
} }

View File

@@ -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) => {

View File

@@ -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);
}); });

View File

@@ -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) => {

View File

@@ -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 }),

View File

@@ -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),
})), })),
); );

View File

@@ -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 }),

View File

@@ -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",
},
},
]); ]);