/* * 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.

  1. Network healthy
  2. Network unhealthy
  3. Network disabled
{data && data.length > 0 ? (
  1. 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")}
  2. {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}

    {network.nick}
    {(edit || expanded) && (
    {network.channels.length > 0 ? (
    1. Channel
      Monitoring since
      Last announce
    2. {network.channels.map((c) => ( ))}
    ) : (

    No channels!

    )}
    )}
  • ); }; 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 ( e.stopPropagation()} > { deleteMutation.mutate(network.id); toggleDeleteModal(); }} title={`Remove network: ${network.name}`} text="Are you sure you want to remove this network? This action cannot be undone." />
    {({ active }) => ( )} {/**/} {/* {({ active }) => (*/} {/* onToggle(!network.enabled)}*/} {/* >*/} {/* */} {({ active }) => ( )}
    {({ active }) => ( )}
    ); }; 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;