Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 29 additions & 27 deletions src/components/ui/AddServerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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)}`,
);
Expand All @@ -36,22 +42,28 @@ 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("");

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(() => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -167,11 +173,7 @@ export const AddServerModal: React.FC = () => {
</label>
<TextInput
inputMode="url"
value={
disableServerConnectionInfo && serverHost.includes("://")
? new URL(serverHost).hostname
: serverHost || ""
}
value={serverHost || ""}
onChange={(e) => 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 ${
Expand Down
54 changes: 47 additions & 7 deletions src/components/ui/EditServerModal.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,16 +24,22 @@ export const EditServerModal: React.FC<EditServerModalProps> = ({
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,
);
Comment on lines +27 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether ServerConfig carries any useWebSocket / websocket-related flag that could serve as a better fallback.
rg -nP -C2 '\buseWebSocket\b|\bwebSocket\b|\bwss\b' --type=ts -g '!tests/**' -g '!**/*.test.*'
echo '---'
ast-grep --pattern 'interface ServerConfig {
  $$$
}'

Repository: ObsidianIRC/ObsidianIRC

Length of output: 15586


🏁 Script executed:

cat -n src/components/ui/EditServerModal.tsx | head -50

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2231


🏁 Script executed:

# Find getServerConnectionFields implementation and its signature
rg -n 'getServerConnectionFields|function getServerConnectionFields' --type=ts

Repository: ObsidianIRC/ObsidianIRC

Length of output: 1061


🏁 Script executed:

# Check what type serverConfig is in EditServerModal (props interface)
ast-grep --pattern 'interface $_EditServerModalProps {
  $$$
}'

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Also check if there's a type annotation for the serverConfig parameter
rg -B5 -A5 'function EditServerModal' src/components/ui/EditServerModal.tsx

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

cat -n src/lib/serverConnectionUrl.ts | head -100

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2927


🏁 Script executed:

# Look at the full getServerConnectionFields function
sed -n '44,90p' src/lib/serverConnectionUrl.ts

Repository: ObsidianIRC/ObsidianIRC

Length of output: 1233


🏁 Script executed:

# Check if AddServerModal uses a different type for prefillServerDetails
sed -n '1,50p' src/components/ui/AddServerModal.tsx

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2040


🏁 Script executed:

# Look at the store types to understand the relationship
cat -n src/store/types.ts | head -30

Repository: ObsidianIRC/ObsidianIRC

Length of output: 968


Add a clarifying comment for legacy config handling: ServerConfig has no useWebSocket field.

Since ServerConfig (the persisted type) lacks a useWebSocket field, fallbackUseWebSocket cannot reference it. The hardcoded false fallback will affect bare-hostname legacy configs (those without a scheme in the host field): a Tauri user with a legacy stored host like "irc.example.com" will see the WSS checkbox unchecked after opening Edit, and saving will rewrite the URL to ircs://…. Configs saved after the refactor will have schemes and round-trip correctly. Add a one-line comment (e.g., // Legacy bare hostnames default to ircs or similar) to document this intentional behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/EditServerModal.tsx` around lines 27 - 33, The code is
intentionally passing a hardcoded false for the useWebSocket fallback because
persisted ServerConfig has no useWebSocket field; add a one-line clarifying
comment next to the initialConnectionFields call (or above
getServerConnectionFields usage) noting that legacy bare hostnames (no scheme)
default to ircs/WSS behavior and that this false fallback will cause Tauri
legacy hosts like "irc.example.com" to show the WSS checkbox unchecked and be
rewritten to ircs:// on save. Reference getServerConnectionFields,
initialConnectionFields, and ServerConfig in the comment so future readers
understand this is an intentional legacy-handling choice.


// 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("");
Expand Down Expand Up @@ -87,11 +98,16 @@ export const EditServerModal: React.FC<EditServerModalProps> = ({
}

try {
const parsedPort = Number.parseInt(serverPort, 10);

// Update server configuration
const updatedConfig: Partial<ServerConfig> = {
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,
Expand Down Expand Up @@ -163,6 +179,7 @@ export const EditServerModal: React.FC<EditServerModalProps> = ({
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"
/>
</div>
Expand All @@ -176,6 +193,7 @@ export const EditServerModal: React.FC<EditServerModalProps> = ({
/>
</label>
<TextInput
inputMode="numeric"
value={serverPort}
onChange={(e) => setServerPort(e.target.value)}
placeholder="443"
Expand All @@ -184,6 +202,28 @@ export const EditServerModal: React.FC<EditServerModalProps> = ({
</div>
</div>

{isTauri() && (
<div className="mb-4 flex items-center">
<input
type="checkbox"
id="editUseWebSocket"
checked={useWebSocket}
onChange={() => setUseWebSocket(!useWebSocket)}
className="accent-discord-accent rounded"
/>
<label
htmlFor="editUseWebSocket"
className="text-discord-text-muted text-sm flex items-center ml-2"
>
WSS{" "}
<FaQuestionCircle
title="Toggle WebSocket instead of direct IRC-over-TLS"
className="inline-block text-discord-text-muted cursor-help text-xs ml-1"
/>
</label>
</div>
)}

<div className="mb-4">
<label className="block text-discord-text-muted text-sm font-medium mb-1">
Nickname
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import LoadingSpinner from "./LoadingSpinner";

export const LoadingOverlay: React.FC = () => {
return (
<div className="fixed inset-0 z-[100001] flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="pointer-events-none fixed inset-0 z-[100001] flex items-center justify-center bg-black/50 backdrop-blur-sm">
<LoadingSpinner size="lg" text="" />
</div>
);
Expand Down
16 changes: 13 additions & 3 deletions src/lib/irc/IRCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,9 +713,19 @@ export class IRCClient implements IRCClientContext {
disconnect(serverId: string, quitMessage?: string): void {
const socket = this.sockets.get(serverId);
if (socket) {
const message = quitMessage || "ObsidianIRC - Bringing IRC to the future";
socket.send(`QUIT :${message}`);
socket.close();
const CONNECTING = 0;
const OPEN = 1;

if (socket.readyState === OPEN) {
const message =
quitMessage || "ObsidianIRC - Bringing IRC to the future";
socket.send(`QUIT :${message}`);
}

if (socket.readyState === CONNECTING || socket.readyState === OPEN) {
socket.close();
}

this.sockets.delete(serverId);
}
const server = this.servers.get(serverId);
Expand Down
111 changes: 111 additions & 0 deletions src/lib/serverConnectionUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { parseIrcUrl } from "./ircUrlParser";

export interface ServerConnectionFields {
host: string;
port: string;
useWebSocket: boolean;
}

function parseStandardUrl(url: string): {
host: string;
port: string;
useWebSocket: boolean;
} | null {
try {
const parsed = new URL(url);
const useWebSocket =
parsed.protocol === "wss:" || parsed.protocol === "ws:";
return {
host: parsed.hostname,
port: parsed.port,
useWebSocket,
};
} catch {
return null;
}
}

function parseHostWithOptionalPort(value: string): {
host: string;
port?: string;
} {
const trimmed = value.trim();
const match = trimmed.match(/^([^:/?#\s]+):(\d+)$/);
if (!match) {
return { host: trimmed };
}

return {
host: match[1],
port: match[2],
};
}

export function getServerConnectionFields(
rawHost: string | null | undefined,
rawPort: string | number | null | undefined,
fallbackUseWebSocket = false,
): ServerConnectionFields {
const fallbackPort = String(rawPort ?? "").trim();
const host = rawHost?.trim() ?? "";

if (!host) {
return {
host: "",
port: fallbackPort,
useWebSocket: fallbackUseWebSocket,
};
}

if (host.startsWith("ircs://") || host.startsWith("irc://")) {
const parsed = parseIrcUrl(host);
return {
host: parsed.host,
port: String(parsed.port || fallbackPort),
useWebSocket: false,
};
}

if (host.startsWith("wss://") || host.startsWith("ws://")) {
const parsed = parseStandardUrl(host);
if (parsed) {
return {
host: parsed.host,
port: parsed.port || fallbackPort,
useWebSocket: parsed.useWebSocket,
};
}
}

const parsedHost = parseHostWithOptionalPort(host);
return {
host: parsedHost.host,
port: parsedHost.port || fallbackPort,
useWebSocket: fallbackUseWebSocket,
};
}

export function buildServerConnectionUrl(
hostInput: string,
port: number,
options: {
isTauri: boolean;
useWebSocket: boolean;
},
): string {
const { host } = getServerConnectionFields(
hostInput,
String(port),
options.useWebSocket,
);
const trimmedHost = host.trim();
if (!trimmedHost) return "";

const scheme = options.isTauri
? options.useWebSocket
? "wss"
: "ircs"
: "wss";

return `${scheme}://${trimmedHost}:${port}`;
}
Loading
Loading