diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh old mode 100644 new mode 100755 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8b28612..c168ca9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "mode": "debug", "program": "cmd/openbooks", - "args": ["server", "--log", "--server", "localhost", "--persist"] + "args": ["server", "--log", "--tls", "false", "--server", "localhost:6667", "--name", "dev-user"] }, { "name": "Launch OpenBooks CLI", diff --git a/go.mod b/go.mod index 2ce44e2..f54695c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/go-chi/chi/v5 v5.0.7 github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.5.0 github.com/mholt/archiver/v3 v3.5.1 github.com/nwaples/rardecode v1.1.3 // indirect github.com/rs/cors v1.8.2 @@ -28,6 +27,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 github.com/inkeliz/gowebview v1.0.1 + github.com/r3labs/sse/v2 v2.10.0 github.com/testcontainers/testcontainers-go v0.17.0 gopkg.in/irc.v4 v4.0.0 ) @@ -73,5 +73,6 @@ require ( google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 58c87b2..3cf41e8 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,6 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= @@ -160,6 +158,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -228,6 +228,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -318,6 +319,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/irc.v4 v4.0.0 h1:5jsLkU2Tg+R2nGNqmkGCrciasyi4kNkDXhyZD+C31yY= diff --git a/mock/operator.go b/mock/operator.go index aa5b8d0..654954e 100644 --- a/mock/operator.go +++ b/mock/operator.go @@ -2,13 +2,14 @@ package mock import ( "context" - "gopkg.in/irc.v4" "log" "math/rand" "net" "os" "strings" "time" + + "gopkg.in/irc.v4" ) type Config struct { @@ -58,8 +59,11 @@ func NewOperator(config *Config) *Operator { func (o *Operator) Handler(client *irc.Client, message *irc.Message) { if strings.HasPrefix(message.Trailing(), "@search") { - query := strings.SplitN(message.Trailing(), " ", 2)[1] - o.log.Printf("Search for '%s'\n", query) + queryParts := strings.SplitN(message.Trailing(), " ", 2) + if len(queryParts) == 1 { + return + } + o.log.Printf("Search for '%s'\n", queryParts[1]) dccString := o.dccManager.ServeFile("SearchBot_results_for_ the great gatsby.txt.zip") diff --git a/server/app/package-lock.json b/server/app/package-lock.json index 25fee82..b387bb8 100644 --- a/server/app/package-lock.json +++ b/server/app/package-lock.json @@ -13,7 +13,7 @@ "@mantine/hooks": "^7.1.2", "@mantine/notifications": "^7.1.2", "@phosphor-icons/react": "^2.0.13", - "@reduxjs/toolkit": "^1.9.6", + "@reduxjs/toolkit": "^2.2.6", "@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/react-table": "^8.10.3", "@tanstack/react-virtual": "^3.0.0-beta.62", @@ -1106,18 +1106,18 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.6.tgz", - "integrity": "sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.6.tgz", + "integrity": "sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==", "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { "react": { @@ -3432,9 +3432,9 @@ } }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -7965,19 +7965,16 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "peerDependencies": { - "redux": "^4" + "redux": "^5.0.0" } }, "node_modules/reflect.getprototypeof": { @@ -8028,9 +8025,9 @@ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" }, "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "node_modules/resolve": { "version": "2.0.0-next.4", @@ -9733,14 +9730,14 @@ } }, "@reduxjs/toolkit": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.6.tgz", - "integrity": "sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.6.tgz", + "integrity": "sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==", "requires": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" } }, "@remix-run/router": { @@ -11418,9 +11415,9 @@ "dev": true }, "immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==" }, "import-fresh": { "version": "3.3.0", @@ -14351,17 +14348,14 @@ } }, "redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "requires": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "requires": {} }, "reflect.getprototypeof": { @@ -14400,9 +14394,9 @@ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" }, "reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "resolve": { "version": "2.0.0-next.4", diff --git a/server/app/package.json b/server/app/package.json index 89f1b70..29001ea 100644 --- a/server/app/package.json +++ b/server/app/package.json @@ -15,7 +15,7 @@ "@mantine/hooks": "^7.1.2", "@mantine/notifications": "^7.1.2", "@phosphor-icons/react": "^2.0.13", - "@reduxjs/toolkit": "^1.9.6", + "@reduxjs/toolkit": "^2.2.6", "@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/react-table": "^8.10.3", "@tanstack/react-virtual": "^3.0.0-beta.62", diff --git a/server/app/src/components/sidebar/History.tsx b/server/app/src/components/sidebar/History.tsx index 0c4d6a2..18c09f2 100644 --- a/server/app/src/components/sidebar/History.tsx +++ b/server/app/src/components/sidebar/History.tsx @@ -1,3 +1,4 @@ +import { useAutoAnimate } from "@formkit/auto-animate/react"; import { Badge, Button, @@ -9,22 +10,18 @@ import { Tooltip } from "@mantine/core"; import { Eye, EyeSlash, MagnifyingGlass, Trash } from "@phosphor-icons/react"; -import { useSelector } from "react-redux"; import { - deleteHistoryItem, HistoryItem, - selectHistory + removeResults, + setActiveItem } from "../../state/historySlice"; -import { setActiveItem } from "../../state/stateSlice"; import { AppDispatch, useAppDispatch, useAppSelector } from "../../state/store"; -import classes from "./SidebarButton.module.css"; import { conditionalAttribute } from "../../utils/attribute-helper"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; +import classes from "./SidebarButton.module.css"; export default function History() { - const history = useSelector(selectHistory); - const activeTS = - useAppSelector((store) => store.state.activeItem?.timestamp) ?? -1; + const history = useAppSelector((store) => store.history.items); + const activeTS = useAppSelector((store) => store.history.active); const dispatch = useAppDispatch(); const [parent] = useAutoAnimate(/* optional config */); @@ -51,7 +48,7 @@ export default function History() { } type Props = { - activeTS: number; + activeTS: number | undefined; item: HistoryItem; dispatch: AppDispatch; }; @@ -107,7 +104,7 @@ function HistoryCard({ activeTS, item, dispatch }: Props) { } - onClick={() => dispatch(deleteHistoryItem(item.timestamp))}> + onClick={() => dispatch(removeResults(item.timestamp))}> Delete item diff --git a/server/app/src/components/sidebar/Library.tsx b/server/app/src/components/sidebar/Library.tsx index 0bf99e2..5e05ac6 100644 --- a/server/app/src/components/sidebar/Library.tsx +++ b/server/app/src/components/sidebar/Library.tsx @@ -15,7 +15,7 @@ import classes from "./SidebarButton.module.css"; import { useAutoAnimate } from "@formkit/auto-animate/react"; export default function Library() { - const { data, isLoading, isSuccess, isError } = useGetBooksQuery(null); + const { data, isLoading, isSuccess, isError } = useGetBooksQuery(); const [parent] = useAutoAnimate(/* optional config */); if (isLoading && !data) { diff --git a/server/app/src/components/tables/BookTable.tsx b/server/app/src/components/tables/BookTable.tsx index a400515..efff738 100644 --- a/server/app/src/components/tables/BookTable.tsx +++ b/server/app/src/components/tables/BookTable.tsx @@ -1,11 +1,10 @@ import { Button, Indicator, Loader, Text, Tooltip } from "@mantine/core"; -import { createColumnHelper, Table } from "@tanstack/react-table"; -import React, { useCallback, useMemo, useState } from "react"; +import { Table, createColumnHelper } from "@tanstack/react-table"; +import { useCallback, useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import { useGetServersQuery } from "../../state/api"; +import { useDownloadMutation, useGetServersQuery } from "../../state/api"; import { BookDetail } from "../../state/messages"; -import { sendDownload } from "../../state/stateSlice"; -import { RootState, useAppDispatch } from "../../state/store"; +import { RootState } from "../../state/store"; import { DataTableColumnHeader } from "./DataTable/ColumnHeader"; import DataTable from "./DataTable/DataTable"; import ToolbarFacetFilter from "./DataTable/ToolbarFacetFilter"; @@ -18,7 +17,10 @@ interface BookTableProps { } export default function BookTable({ books }: BookTableProps) { - const { data: servers } = useGetServersQuery(null); + const { data: servers } = useGetServersQuery(undefined, { + pollingInterval: 5000, + skipPollingIfUnfocused: true + }); const columns = useMemo( () => [ @@ -27,7 +29,7 @@ export default function BookTable({ books }: BookTableProps) { ), cell: (props) => { - const online = servers?.includes(props.getValue()); + const online = servers && servers.has(props.getValue()); return ( @@ -133,7 +135,7 @@ function DownloadButton({ book }: { book: string }) { // Prevent hitting the same button multiple times const onClick = () => { if (clicked) return; - dispatch(sendDownload(book)); + downloadMutation(book); setClicked(true); }; diff --git a/server/app/src/components/tables/Facets.tsx b/server/app/src/components/tables/Facets.tsx index 0d63798..8a89d7b 100644 --- a/server/app/src/components/tables/Facets.tsx +++ b/server/app/src/components/tables/Facets.tsx @@ -12,8 +12,8 @@ export function ServerFacetEntry({ selected, style }: FacetEntryProps) { - const { data: servers } = useGetServersQuery(null); - const serverOnline = servers?.includes(entry) ?? false; + const { data: servers } = useGetServersQuery(); + const serverOnline = servers && servers.has(entry); return (