/*
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { Fragment, useRef, useState, useMemo, useEffect, MouseEvent } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
import { Menu, Switch, Transition } from "@headlessui/react";
import { toast } from "react-hot-toast";
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
ExclamationCircleIcon,
PencilSquareIcon,
TrashIcon
} from "@heroicons/react/24/outline";
import { classNames, IsEmptyDate, simplifyDate } from "@utils";
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "@forms";
import { useToggle } from "@hooks/hooks";
import { APIClient } from "@api/APIClient";
import { EmptySimple } from "@components/emptystates";
import { DeleteModal } from "@components/modals";
import Toast from "@components/notifications/Toast";
import { SettingsContext } from "@utils/Context";
// import { useForm } from "react-hook-form";
export const ircKeys = {
all: ["irc_networks"] as const,
lists: () => [...ircKeys.all, "list"] as const,
// list: (indexers: string[], sortOrder: string) => [...ircKeys.lists(), { indexers, sortOrder }] as const,
details: () => [...ircKeys.all, "detail"] as const,
detail: (id: number) => [...ircKeys.details(), id] as const
};
interface SortConfig {
key: keyof ListItemProps["network"] | "enabled";
direction: "ascending" | "descending";
}
function useSort(items: ListItemProps["network"][], config?: SortConfig) {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
if (!sortConfig) {
return items;
}
const sortableItems = [...items];
sortableItems.sort((a, b) => {
const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string;
const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string;
if (aValue < bValue) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key: keyof ListItemProps["network"]) => {
let direction: "ascending" | "descending" = "ascending";
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === "ascending"
) {
direction = "descending";
}
setSortConfig({ key, direction });
};
const getSortIndicator = (key: keyof ListItemProps["network"]) => {
if (!sortConfig || sortConfig.key !== key) {
return "";
}
return sortConfig.direction === "ascending" ? "↑" : "↓";
};
return { items: sortedItems, requestSort, sortConfig, getSortIndicator };
}
const IrcSettings = () => {
const [expandNetworks, toggleExpand] = useToggle(false);
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
const { data } = useQuery({
queryKey: ircKeys.lists(),
queryFn: APIClient.irc.getNetworks,
refetchOnWindowFocus: false,
refetchInterval: 3000 // Refetch every 3 seconds
});
const sortedNetworks = useSort(data || []);
return (
IRC
IRC networks and channels. Click on a network to view channel
status.
-
Network healthy
-
Network unhealthy
-
Network disabled
{data && data.length > 0 ? (
-
sortedNetworks.requestSort("enabled")}>
Enabled {sortedNetworks.getSortIndicator("enabled")}
sortedNetworks.requestSort("name")}>
Network {sortedNetworks.getSortIndicator("name")}
sortedNetworks.requestSort("server")}>
Server {sortedNetworks.getSortIndicator("server")}
sortedNetworks.requestSort("nick")}>
Nick {sortedNetworks.getSortIndicator("nick")}
{data &&
sortedNetworks.items.map((network) => (
))}
) : (
)}
);
};
interface ListItemProps {
network: IrcNetworkWithHealth;
expanded: boolean;
}
const ListItem = ({ network, expanded }: ListItemProps) => {
const [updateIsOpen, toggleUpdate] = useToggle(false);
const [edit, toggleEdit] = useToggle(false);
const queryClient = useQueryClient();
const updateMutation = useMutation({
mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network).then(() => network),
onSuccess: (network: IrcNetwork) => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
toast.custom(t => );
}
});
const onToggleMutation = (newState: boolean) => {
updateMutation.mutate({
...network,
enabled: newState
});
};
return (
e.stopPropagation()}
checked={network.enabled}
onChange={onToggleMutation}
className={classNames(
network.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
"items-center relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)}
>
Enable
{network.enabled ? (
network.healthy ? (
) : (
)
) : (
)}
{network.name}
{network.tls ? (
) : (
)}
{network.server}:{network.port}
{(edit || expanded) && (
{network.channels.length > 0 ? (
-
Channel
Monitoring since
Last announce
{network.channels.map((c) => (
))}
) : (
)}
)}
);
};
interface ChannelItemProps {
network: IrcNetwork;
channel: IrcChannelWithHealth;
}
const ChannelItem = ({ network, channel }: ChannelItemProps) => {
const [viewChannel, toggleView] = useToggle(false);
return (
{network.enabled ? (
channel.monitoring ? (
) : (
)
) : (
)}
{channel.name}
{IsEmptyDate(channel.monitoring_since)}
{IsEmptyDate(channel.last_announce)}
{viewChannel && (
)}
);
};
interface ListItemDropdownProps {
network: IrcNetwork;
toggleUpdate: () => void;
}
const ListItemDropdown = ({
network,
toggleUpdate
}: ListItemDropdownProps) => {
const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.deleteNetwork(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
toast.custom((t) => );
toggleDeleteModal();
}
});
const restartMutation = useMutation({
mutationFn: (id: number) => APIClient.irc.restartNetwork(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
toast.custom((t) => );
}
});
const restart = (id: number) => restartMutation.mutate(id);
return (
);
};
type IrcEvent = {
channel: string;
nick: string;
msg: string;
time: string;
};
// type IrcMsg = {
// msg: string;
// };
interface EventsProps {
network: IrcNetwork;
channel: string;
}
export const Events = ({ network, channel }: EventsProps) => {
const [settings] = SettingsContext.use();
const messagesEndRef = useRef(null);
const [logs, setLogs] = useState([]);
// const scrollToBottom = () => {
// messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
// };
// const { handleSubmit, register , resetField } = useForm({
// defaultValues: { msg: "" },
// mode: "onBlur"
// });
// const cmdMutation = useMutation({
// mutationFn: (data: SendIrcCmdRequest) => APIClient.irc.sendCmd(data),
// onSuccess: (_, _variables) => {
// resetField("msg");
// },
// onError: () => {
// toast.custom((t) => (
//
// ));
// }
// });
// const onSubmit = (msg: IrcMsg) => {
// const payload = { network_id: network.id, nick: network.nick, server: network.server, channel: channel, msg: msg.msg };
// cmdMutation.mutate(payload);
// };
useEffect(() => {
// Following RFC4648
const key = window.btoa(`${network.id}${channel.toLowerCase()}`)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
const es = APIClient.irc.events(key);
es.onmessage = (event) => {
const newData = JSON.parse(event.data) as IrcEvent;
setLogs((prevState) => [...prevState, newData]);
// if (settings.scrollOnNewLog)
// scrollToBottom();
};
return () => es.close();
}, [settings]);
return (
{logs.map((entry, idx) => (
{entry.nick}: {entry.msg}
))}
{/*
*/}
{/* */}
{/*
*/}
);
};
export default IrcSettings;