Skip to content
Merged
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
40 changes: 20 additions & 20 deletions src/components/ui/AddServerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,9 @@ export const AddServerModal: React.FC = () => {
/^(https?|wss|ircs?|irc):\/\//,
"",
);

const isLocalhost =
cleanHost.toLowerCase() === "localhost" ||
cleanHost === "127.0.0.1" ||
cleanHost === "::1";

const isSSLPort =
!isLocalhost &&
(port === 6697 || port === 9999 || port === 443 || port === 993);

if (useWebSocket) {
finalHost = `${isSSLPort ? "wss" : "ws"}://${cleanHost}:${port}`;
} else {
finalHost = `${isSSLPort ? "ircs" : "irc"}://${cleanHost}:${port}`;
}
finalHost = useWebSocket
? `wss://${cleanHost}:${port}`
: `ircs://${cleanHost}:${port}`;
}

await connect(
Expand Down Expand Up @@ -145,6 +133,7 @@ export const AddServerModal: React.FC = () => {
const disableServerConnectionInfo =
prefillServerDetails?.ui?.disableServerConnectionInfo;
const hideServerInfo = prefillServerDetails?.ui?.hideServerInfo;
const lockWebSocket = prefillServerDetails?.ui?.lockWebSocket;

return (
<BaseModal
Expand Down Expand Up @@ -176,7 +165,11 @@ export const AddServerModal: React.FC = () => {
</label>
<TextInput
inputMode="url"
value={serverHost || ""}
value={
disableServerConnectionInfo && serverHost.includes("://")
? new URL(serverHost).hostname
: 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 Expand Up @@ -217,16 +210,23 @@ export const AddServerModal: React.FC = () => {
type="checkbox"
id="useWebSocket"
checked={useWebSocket}
onChange={() => setUseWebSocket(!useWebSocket)}
className="accent-discord-accent rounded"
onChange={() =>
!lockWebSocket && setUseWebSocket(!useWebSocket)
}
disabled={!!lockWebSocket}
className={`accent-discord-accent rounded ${lockWebSocket ? "opacity-50 cursor-not-allowed" : ""}`}
/>
<label
htmlFor="useWebSocket"
className="text-discord-text-muted text-sm flex items-center ml-2"
className={`text-discord-text-muted text-sm flex items-center ml-2 ${lockWebSocket ? "opacity-50" : ""}`}
>
WSS{" "}
<FaQuestionCircle
title="Use WebSocket instead of raw TCP"
title={
lockWebSocket
? "This server only supports one connection type"
: "Use WebSocket instead of raw TCP"
}
className="inline-block text-discord-text-muted cursor-help text-xs ml-1"
/>
</label>
Expand Down
58 changes: 46 additions & 12 deletions src/components/ui/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { useEffect, useState } from "react";
import { FaPlus } from "react-icons/fa";
import { isTauri } from "../../lib/platformUtils";
import useStore from "../../store";
import { TextInput } from "./TextInput";

interface DiscoverServer {
name: string;
description: string;
wss?: string;
ircs?: string;
obsidian?: boolean;
}

const DiscoverGrid = () => {
const { toggleAddServerModal, connect, isConnecting, connectionError } =
useStore();
const [query, setQuery] = useState("");
const [servers, setServers] = useState<
{ name: string; description: string; server?: string; port?: string }[]
>([]);
const [servers, setServers] = useState<DiscoverServer[]>([]);

useEffect(() => {
const fetchServers = async () => {
Expand All @@ -30,21 +37,48 @@ const DiscoverGrid = () => {
fetchServers();
}, []); // Empty dependency array ensures this runs only once

const filteredServers = servers.filter(
(server) =>
server.name.toLowerCase().includes(query.toLowerCase()) ||
server.description.toLowerCase().includes(query.toLowerCase()),
);
// Browser can only connect via WebSocket — hide servers that don't offer wss.
// Tauri supports both raw TCP (ircs) and wss, so show all servers with either.
const filteredServers = servers
.filter((server) =>
isTauri() ? !!(server.wss || server.ircs) : !!server.wss,
)
.filter(
(server) =>
server.name.toLowerCase().includes(query.toLowerCase()) ||
server.description.toLowerCase().includes(query.toLowerCase()),
);

const handleServerClick = (server: DiscoverServer) => {
const hasWss = !!server.wss;
const hasIrcs = !!server.ircs;

// Tauri: prefer raw TCP (ircs); if only wss available, use that.
// Browser: always wss (non-wss servers are already filtered out above).
const useWebSocket = isTauri() ? !hasIrcs : false;
const uri = isTauri()
? useWebSocket
? server.wss
: (server.ircs ?? server.wss)
: server.wss;
if (!uri) return;
const parsed = new URL(uri);

// Lock the WSS toggle when only one protocol is available (Tauri only).
const lockWebSocket = isTauri() && !(hasWss && hasIrcs);

const handleServerClick = (server: Record<string, string>) => {
toggleAddServerModal(true, {
name: server.name,
host: server.server || "", // Use empty string if server is undefined
port: server.port || "443", // Default to 443 if port is undefined
nickname: "", // Generate a default nickname
// Pass the full URI so ircClient picks up the correct protocol (wss/ircs).
// AddServerModal strips the scheme for display when the field is disabled.
host: uri,
port: parsed.port || (isTauri() ? "6697" : "443"),
useWebSocket,
nickname: "",
ui: {
disableServerConnectionInfo: true,
title: server.name,
lockWebSocket,
},
});
};
Expand Down
8 changes: 8 additions & 0 deletions src/lib/tauri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare global {
interface Window {
__TAURI__?: unknown;
}
}

export const isTauri = () =>
typeof window !== "undefined" && window.__TAURI__ !== undefined;
1 change: 1 addition & 0 deletions src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ConnectionDetails {
hideServerInfo?: boolean;
hideClose?: boolean;
title?: string;
lockWebSocket?: boolean;
};
}

Expand Down
200 changes: 200 additions & 0 deletions tests/components/HomeScreen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { describe, expect, it } from "vitest";

interface DiscoverServer {
name: string;
description: string;
wss?: string;
ircs?: string;
obsidian?: boolean;
}

interface PrefillResult {
host: string;
port: string;
useWebSocket: boolean;
lockWebSocket: boolean;
}

// Logic mirroring HomeScreen.handleServerClick
function buildPrefill(
server: DiscoverServer,
isTauri: boolean,
): PrefillResult | null {
const hasWss = !!server.wss;
const hasIrcs = !!server.ircs;

if (!hasWss && !hasIrcs) return null;
if (!isTauri && !hasWss) return null; // browser can't connect without wss

const useWebSocket = isTauri ? !hasIrcs : false;
// Browser always uses wss (non-wss servers are filtered before this point).
// Tauri: ircs unless forced to wss.
const uri = isTauri
? useWebSocket
? server.wss
: (server.ircs ?? server.wss)
: server.wss;
if (!uri) return null;
const parsed = new URL(uri);

return {
host: uri,
port: parsed.port || (isTauri ? "6697" : "443"),
useWebSocket,
lockWebSocket: isTauri && !(hasWss && hasIrcs),
};
}

// Logic mirroring HomeScreen filteredServers platform filter
function isVisible(server: DiscoverServer, isTauri: boolean): boolean {
return isTauri ? !!(server.wss || server.ircs) : !!server.wss;
}

// Mock servers.json examples covering all cases
const bothProtocols: DiscoverServer = {
name: "h4ks.com",
description: "Both wss and ircs available",
wss: "wss://irc.h4ks.com:443",
ircs: "ircs://irc.h4ks.com:6697",
};

const ircsOnly: DiscoverServer = {
name: "IRCsOnly Network",
description: "Raw TCP only, no WebSocket endpoint",
ircs: "ircs://irc.ircsonly.net:6697",
};

const wssOnly: DiscoverServer = {
name: "WSSOnly Network",
description: "WebSocket only, no raw TCP endpoint",
wss: "wss://irc.wssonly.net:8097",
};

const nonStandardPort: DiscoverServer = {
name: "InspIRCd Test Network",
description: "Non-standard ports on both protocols",
wss: "wss://testnet.inspircd.org:8097",
ircs: "ircs://testnet.inspircd.org:6697",
};

const noUrls: DiscoverServer = {
name: "No URLs",
description: "Missing both protocols",
};

describe("HomeScreen server list filtering", () => {
const allServers = [
bothProtocols,
ircsOnly,
wssOnly,
nonStandardPort,
noUrls,
];

it("browser: shows only servers with wss", () => {
const visible = allServers.filter((s) => isVisible(s, false));
expect(visible.map((s) => s.name)).toEqual([
"h4ks.com",
"WSSOnly Network",
"InspIRCd Test Network",
]);
});

it("browser: hides ircs-only servers (no WebSocket available)", () => {
expect(isVisible(ircsOnly, false)).toBe(false);
});

it("Tauri: shows servers with wss, ircs, or both", () => {
const visible = allServers.filter((s) => isVisible(s, true));
expect(visible.map((s) => s.name)).toEqual([
"h4ks.com",
"IRCsOnly Network",
"WSSOnly Network",
"InspIRCd Test Network",
]);
});

it("Tauri: hides servers with neither protocol", () => {
expect(isVisible(noUrls, true)).toBe(false);
});
});

describe("HomeScreen prefill building — browser (non-Tauri)", () => {
it("uses wss URL for a server with both protocols", () => {
const result = buildPrefill(bothProtocols, false);
expect(result?.host).toBe("wss://irc.h4ks.com:443");
expect(result?.port).toBe("443");
expect(result?.useWebSocket).toBe(false); // not used; wss checkbox hidden on web
expect(result?.lockWebSocket).toBe(false);
});

it("uses wss URL for a wss-only server", () => {
const result = buildPrefill(wssOnly, false);
expect(result?.host).toBe("wss://irc.wssonly.net:8097");
expect(result?.port).toBe("8097");
});

it("returns null for ircs-only server (not connectable from browser)", () => {
expect(buildPrefill(ircsOnly, false)).toBeNull();
});

it("returns null when no protocols available", () => {
expect(buildPrefill(noUrls, false)).toBeNull();
});
});

describe("HomeScreen prefill building — Tauri", () => {
it("both protocols: defaults to ircs, WSS toggle unlocked", () => {
const result = buildPrefill(bothProtocols, true);
expect(result?.host).toBe("ircs://irc.h4ks.com:6697");
expect(result?.port).toBe("6697");
expect(result?.useWebSocket).toBe(false);
expect(result?.lockWebSocket).toBe(false);
});

it("ircs-only: uses ircs, WSS toggle locked (off)", () => {
const result = buildPrefill(ircsOnly, true);
expect(result?.host).toBe("ircs://irc.ircsonly.net:6697");
expect(result?.port).toBe("6697");
expect(result?.useWebSocket).toBe(false);
expect(result?.lockWebSocket).toBe(true);
});

it("wss-only: uses wss, WSS toggle locked (on)", () => {
const result = buildPrefill(wssOnly, true);
expect(result?.host).toBe("wss://irc.wssonly.net:8097");
expect(result?.port).toBe("8097");
expect(result?.useWebSocket).toBe(true);
expect(result?.lockWebSocket).toBe(true);
});

it("non-standard ports: preserves correct port from URL", () => {
const result = buildPrefill(nonStandardPort, true);
expect(result?.host).toBe("ircs://testnet.inspircd.org:6697");
expect(result?.port).toBe("6697");
expect(result?.useWebSocket).toBe(false);
expect(result?.lockWebSocket).toBe(false);
});

it("returns null when no protocols available", () => {
expect(buildPrefill(noUrls, true)).toBeNull();
});
});

describe("AddServerModal host display (scheme stripped when field is disabled)", () => {
it("strips wss:// scheme for display", () => {
const host = "wss://irc.h4ks.com:443";
expect(new URL(host).hostname).toBe("irc.h4ks.com");
});

it("strips ircs:// scheme for display", () => {
const host = "ircs://testnet.inspircd.org:6697";
expect(new URL(host).hostname).toBe("testnet.inspircd.org");
});

it("leaves plain hostname unchanged", () => {
const host = "irc.example.com";
const display = host.includes("://") ? new URL(host).hostname : host;
expect(display).toBe("irc.example.com");
});
});
Loading