From 3c9ff36ca1f4e8276c0296e143be3ecc214704c1 Mon Sep 17 00:00:00 2001 From: louzt <179385168+louzt@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:23:07 -0600 Subject: [PATCH 1/2] fix(connection): allow cancel/delete while server is connecting Normalize stored server URLs back into editable host/port fields so Edit Server no longer re-saves values like ircs://host:6697:6697. Keep the loading overlay non-blocking, clear global connecting state when a connecting server is deleted or disconnected, and avoid sending QUIT before a socket is actually open.\n\nAlso adds focused regressions for URL normalization, safe disconnect during CONNECTING, and deleting a server while the UI is still in a connecting state. --- src/components/ui/AddServerModal.tsx | 56 ++++++------ src/components/ui/EditServerModal.tsx | 54 +++++++++-- src/components/ui/LoadingOverlay.tsx | 2 +- src/lib/irc/IRCClient.ts | 16 +++- src/lib/serverConnectionUrl.ts | 111 +++++++++++++++++++++++ src/store/index.ts | 27 ++++-- tests/lib/ircClient.test.ts | 23 +++++ tests/lib/serverConnectionUrl.test.ts | 64 +++++++++++++ tests/store/serverConnectionFlow.test.ts | 49 ++++++++++ 9 files changed, 356 insertions(+), 46 deletions(-) create mode 100644 src/lib/serverConnectionUrl.ts create mode 100644 tests/lib/serverConnectionUrl.test.ts create mode 100644 tests/store/serverConnectionFlow.test.ts diff --git a/src/components/ui/AddServerModal.tsx b/src/components/ui/AddServerModal.tsx index 62fba956..440c4fc3 100644 --- a/src/components/ui/AddServerModal.tsx +++ b/src/components/ui/AddServerModal.tsx @@ -4,6 +4,10 @@ import { FaQuestionCircle } from "react-icons/fa"; import BaseModal from "../../lib/modal/BaseModal"; import { Button, ModalBody, ModalFooter } from "../../lib/modal/components"; import { isTauri } from "../../lib/platformUtils"; +import { + buildServerConnectionUrl, + getServerConnectionFields, +} from "../../lib/serverConnectionUrl"; import useStore from "../../store"; import { TextInput } from "./TextInput"; @@ -16,15 +20,17 @@ export const AddServerModal: React.FC = () => { ui: { prefillServerDetails, isAddServerModalOpen }, } = useStore(); - const [serverName, setServerName] = useState( - prefillServerDetails?.name || "", - ); - const [serverHost, setServerHost] = useState( + const initialConnectionFields = getServerConnectionFields( prefillServerDetails?.host || "", - ); - const [serverPort, setServerPort] = useState( prefillServerDetails?.port || (isTauri() ? "6697" : "443"), + prefillServerDetails?.useWebSocket ?? false, + ); + + const [serverName, setServerName] = useState( + prefillServerDetails?.name || "", ); + const [serverHost, setServerHost] = useState(initialConnectionFields.host); + const [serverPort, setServerPort] = useState(initialConnectionFields.port); const [nickname, setNickname] = useState( prefillServerDetails?.nickname || `user${Math.floor(Math.random() * 1000)}`, ); @@ -36,7 +42,7 @@ export const AddServerModal: React.FC = () => { const [showAccount, setShowAccount] = useState(false); const [registerAccount, setRegisterAccount] = useState(false); const [useWebSocket, setUseWebSocket] = useState( - prefillServerDetails?.useWebSocket ?? false, + initialConnectionFields.useWebSocket, ); const [registerEmail, setRegisterEmail] = useState(""); const [registerPassword, setRegisterPassword] = useState(""); @@ -44,14 +50,20 @@ export const AddServerModal: React.FC = () => { const [error, setError] = useState(""); useEffect(() => { + const nextFields = getServerConnectionFields( + prefillServerDetails?.host || "", + prefillServerDetails?.port || (isTauri() ? "6697" : "443"), + prefillServerDetails?.useWebSocket ?? false, + ); + setServerName(prefillServerDetails?.name || ""); - setServerHost(prefillServerDetails?.host || ""); - setServerPort(prefillServerDetails?.port || (isTauri() ? "6697" : "443")); + setServerHost(nextFields.host); + setServerPort(nextFields.port); setNickname( prefillServerDetails?.nickname || `user${Math.floor(Math.random() * 1000)}`, ); - setUseWebSocket(prefillServerDetails?.useWebSocket || false); + setUseWebSocket(nextFields.useWebSocket); }, [prefillServerDetails]); useEffect(() => { @@ -96,22 +108,16 @@ export const AddServerModal: React.FC = () => { } try { - let finalHost = serverHost; - if (isTauri()) { - const port = Number.parseInt(serverPort, 10); - const cleanHost = serverHost.replace( - /^(https?|wss|ircs?|irc):\/\//, - "", - ); - finalHost = useWebSocket - ? `wss://${cleanHost}:${port}` - : `ircs://${cleanHost}:${port}`; - } + const port = Number.parseInt(serverPort, 10); + const finalHost = buildServerConnectionUrl(serverHost, port, { + isTauri: isTauri(), + useWebSocket, + }); await connect( finalServerName, finalHost, - Number.parseInt(serverPort, 10), + port, nickname, !!saslPassword, password, @@ -167,11 +173,7 @@ export const AddServerModal: React.FC = () => { setServerHost(e.target.value)} placeholder="irc.example.com" className={`w-full rounded px-3 py-2 focus:outline-none focus:ring-1 focus:ring-discord-primary ${ diff --git a/src/components/ui/EditServerModal.tsx b/src/components/ui/EditServerModal.tsx index e824a786..3b4dc6da 100644 --- a/src/components/ui/EditServerModal.tsx +++ b/src/components/ui/EditServerModal.tsx @@ -1,6 +1,11 @@ import type React from "react"; import { useState } from "react"; import { FaQuestionCircle, FaTimes } from "react-icons/fa"; +import { isTauri } from "../../lib/platformUtils"; +import { + buildServerConnectionUrl, + getServerConnectionFields, +} from "../../lib/serverConnectionUrl"; import useStore, { loadSavedServers } from "../../store"; import type { ServerConfig } from "../../types"; import { TextInput } from "./TextInput"; @@ -19,16 +24,22 @@ export const EditServerModal: React.FC = ({ const server = servers.find((s) => s.id === serverId); const savedServers = loadSavedServers(); const serverConfig = savedServers.find((s) => s.id === serverId); + const initialConnectionFields = getServerConnectionFields( + serverConfig?.host || server?.host || "", + serverConfig?.port?.toString() || + server?.port?.toString() || + (isTauri() ? "6697" : "443"), + false, + ); // Initialize state with current server values const [serverName, setServerName] = useState( serverConfig?.name || server?.name || "", ); - const [serverHost, setServerHost] = useState( - serverConfig?.host || server?.host || "", - ); - const [serverPort, setServerPort] = useState( - serverConfig?.port?.toString() || server?.port?.toString() || "443", + const [serverHost, setServerHost] = useState(initialConnectionFields.host); + const [serverPort, setServerPort] = useState(initialConnectionFields.port); + const [useWebSocket, setUseWebSocket] = useState( + initialConnectionFields.useWebSocket, ); const [nickname, setNickname] = useState(serverConfig?.nickname || ""); const [password, setPassword] = useState(""); @@ -87,11 +98,16 @@ export const EditServerModal: React.FC = ({ } try { + const parsedPort = Number.parseInt(serverPort, 10); + // Update server configuration const updatedConfig: Partial = { name: finalServerName, - host: serverHost.trim(), - port: Number.parseInt(serverPort, 10), + host: buildServerConnectionUrl(serverHost, parsedPort, { + isTauri: isTauri(), + useWebSocket, + }), + port: parsedPort, nickname: nickname.trim(), password: password.trim() || undefined, saslAccountName: finalSaslAccountName || undefined, @@ -163,6 +179,7 @@ export const EditServerModal: React.FC = ({ value={serverHost} onChange={(e) => setServerHost(e.target.value)} placeholder="irc.example.com" + inputMode="url" className="w-full bg-discord-dark-400 text-discord-text-normal rounded px-3 py-2 focus:outline-none focus:ring-1 focus:ring-discord-primary" /> @@ -176,6 +193,7 @@ export const EditServerModal: React.FC = ({ /> setServerPort(e.target.value)} placeholder="443" @@ -184,6 +202,28 @@ export const EditServerModal: React.FC = ({ + {isTauri() && ( +
+ setUseWebSocket(!useWebSocket)} + className="accent-discord-accent rounded" + /> + +
+ )} +