/* * Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors. * SPDX-License-Identifier: GPL-2.0-or-later */ import { Fragment, MouseEvent, useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { ArrowPathIcon, LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid"; import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react"; import { toast } from "react-hot-toast"; import { ArrowsPointingInIcon, ArrowsPointingOutIcon, Cog6ToothIcon, 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 { IrcKeys } from "@api/query_keys"; import { IrcQueryOptions } from "@api/queries"; import { EmptySimple } from "@components/emptystates"; import { DeleteModal } from "@components/modals"; import Toast from "@components/notifications/Toast"; import { SettingsContext } from "@utils/Context"; import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; import { RingResizeSpinner } from "@components/Icons.tsx"; 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 ircQuery = useSuspenseQuery(IrcQueryOptions()) const sortedNetworks = useSort(ircQuery.data || []); return (
Add new } >
  • Network healthy
  • Network unhealthy
  • Network disabled
{ircQuery.data && ircQuery.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")}
  • {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 (
  • { if (e.defaultPrevented) return; e.preventDefault(); toggleEdit(); }} >
    {network.enabled ? ( network.healthy ? ( ) : ( ) ) : ( )}
    {network.name}
    {network.tls ? ( ) : ( )}

    {network.server}:{network.port}

    {network.nick}
    {(edit || expanded) && (
    {network.channels.length > 0 ? (
    • Channel
      Monitoring since
      Last announce
    • {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.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); }} > { 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 }) => ( )}
    ); }; interface ReprocessAnnounceProps { networkId: number; channel: string; msg: string; } const ReprocessAnnounceButton = ({ networkId, channel, msg }: ReprocessAnnounceProps) => { const mutation = useMutation({ mutationFn: (req: IrcProcessManualRequest) => APIClient.irc.reprocessAnnounce(req.network_id, req.channel, req.msg), onSuccess: () => { toast.custom((t) => ( )); } }); const reprocessAnnounce = () => { const req: IrcProcessManualRequest = { network_id: networkId, msg: msg, channel: channel, } if (channel.startsWith("#")) { req.channel = channel.replace("#", "") } mutation.mutate(req); }; 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 [logs, setLogs] = useState([]); const [settings] = SettingsContext.use(); 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]); }; return () => es.close(); }, [channel, network.id, settings]); const [isFullscreen, toggleFullscreen] = useToggle(false); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape" && isFullscreen) { toggleFullscreen(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [isFullscreen, toggleFullscreen]); const messagesEndRef = useRef(null); useEffect(() => { const scrollToBottom = () => { if (messagesEndRef.current) { messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight; } }; if (settings.scrollOnNewLog) scrollToBottom(); }, [logs, settings.scrollOnNewLog]); // Add a useEffect to clear logs div when settings.scrollOnNewLog changes to prevent duplicate entries. useEffect(() => { setLogs([]); }, [settings.scrollOnNewLog]); useEffect(() => { document.body.classList.toggle("overflow-hidden", isFullscreen); return () => { // Clean up by removing the class when the component unmounts document.body.classList.remove("overflow-hidden"); }; }, [isFullscreen]); return (
    {logs.map((entry, idx) => (
    [{simplifyDate(entry.time)}] {entry.nick}: {entry.msg}
    ))}
    ); }; export default IrcSettings; const IRCLogsDropdown = () => { const [settings, setSettings] = SettingsContext.use(); const onSetValue = ( key: "scrollOnNewLog", newValue: boolean ) => setSettings((prevState) => ({ ...prevState, [key]: newValue })); // // FIXME: Warning: Function components cannot be given refs. Attempts to access this ref will fail. // Did you mean to use React.forwardRef()? // // Check the render method of `Pe2`. // at Checkbox (http://localhost:3000/src/components/Checkbox.tsx:14:28) // at Pe2 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:2164:12) // at div // at Ee (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:2106:12) // at c5 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:592:22) // at De4 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:3016:22) // at He5 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:3053:15) // at div // at c5 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:592:22) // at Me2 (http://localhost:3000/node_modules/.vite/deps/@headlessui_react.js?v=e8629745:2062:21) // at IRCLogsDropdown (http://localhost:3000/src/screens/settings/Irc.tsx?t=1694269937935:1354:53) return ( Options {() => ( onSetValue("scrollOnNewLog", newValue)} /> )} ); };