From 9b371234da56c405d33df83243891519cb067c37 Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:16:23 +0100 Subject: [PATCH] A server saved by its LAN address offers its public one (GRYT-2007) The app remembers public_host from server:details as the saved server's publicHost. When that server isn't answering, the screen offers "Connect through ", which adds the public address and removes the LAN one. It's joined and proved like any server added by hand, so its identity check runs as normal. Co-Authored-By: Claude Opus 5.5 --- src/packages/settings/src/types/server.ts | 4 ++++ .../socket/src/components/UnreachableServer.tsx | 13 ++++++++++++- src/packages/socket/src/components/serverView.tsx | 13 ++++++++++++- .../socket/src/hooks/registerServerSocketEvents.ts | 7 +++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/packages/settings/src/types/server.ts b/src/packages/settings/src/types/server.ts index 165d12d7d..c497ad33a 100644 --- a/src/packages/settings/src/types/server.ts +++ b/src/packages/settings/src/types/server.ts @@ -81,6 +81,8 @@ export type serverDetails = { voice_capacity_max?: number | null; clients?: Record; server_info?: { + /** Where it answers from anywhere, when the operator set one (GRYT-2007). */ + public_host?: string; server_id?: string; name?: string; description?: string; @@ -160,6 +162,8 @@ export type Server = { * means outstanding; a timestamp so the entry can say how long (GRYT-289). */ approvalRequestedAt?: number; + /** The server's public address, remembered for when this one doesn't answer (GRYT-2007). */ + publicHost?: string; }; export type Servers = { diff --git a/src/packages/socket/src/components/UnreachableServer.tsx b/src/packages/socket/src/components/UnreachableServer.tsx index 9c9225c50..738864d5c 100644 --- a/src/packages/socket/src/components/UnreachableServer.tsx +++ b/src/packages/socket/src/components/UnreachableServer.tsx @@ -1,7 +1,7 @@ import { Button, Surface } from "@gryt/ui"; import { useState } from "react"; -import { PiArrowsClockwiseFill, PiPlugsFill, PiPulseBold } from "../../../../lib/icons"; +import { PiArrowsClockwiseFill, PiGlobeSimpleFill, PiPlugsFill, PiPulseBold } from "../../../../lib/icons"; import { ServerDoctor } from "./ServerDoctor"; /** @@ -14,12 +14,17 @@ export function UnreachableServer({ sfuHosts, stunHosts, onTryNow, + publicHost, + onUsePublic, }: { host: string; serverName: string; sfuHosts: string[]; stunHosts: string[]; onTryNow: () => void; + /** The address it answers on from anywhere, when this one is its LAN address (GRYT-2007). */ + publicHost?: string; + onUsePublic?: () => void; }) { const [doctorOpen, setDoctorOpen] = useState(false); @@ -73,6 +78,12 @@ export function UnreachableServer({ Check the connection + {publicHost && publicHost !== host && onUsePublic && ( + + )} {serverName !== host && {host}} diff --git a/src/packages/socket/src/components/serverView.tsx b/src/packages/socket/src/components/serverView.tsx index 92f808031..d080b648e 100644 --- a/src/packages/socket/src/components/serverView.tsx +++ b/src/packages/socket/src/components/serverView.tsx @@ -159,7 +159,14 @@ export const ServerView = ({ dmSpace = false }: { dmSpace?: boolean }) => { devFakeParticipants, devFakeMuted, devFakeScreenShare, devFakeDeafened, devFakeSpeaking, devFakeMembers, devFakeChatSeconds, } = useSettings(); - const { currentlyViewingServer, setShowRemoveServer, setLastSelectedChannelForServer, viewServerBehindDmSpace } = useServerManagement(); + const { currentlyViewingServer, setShowRemoveServer, setLastSelectedChannelForServer, viewServerBehindDmSpace, addServer, removeServer } = useServerManagement(); + /* Switches the saved server to the address it gave as public (GRYT-2007). A new address is + joined and proved like any server added by hand, so its identity check runs as normal. */ + const switchToPublicAddress = (server: { host: string; name?: string; publicHost?: string }) => { + if (!server.publicHost) return; + addServer({ host: server.publicHost, name: server.name || server.publicHost }, true); + removeServer(server.host); + }; const directory = useDirectory(); /* The conversations as the search panel's "channels", which is what it names a hit by and what `in:` offers. Only the fields it reads. */ @@ -1206,6 +1213,8 @@ export const ServerView = ({ dmSpace = false }: { dmSpace?: boolean }) => { sfuHosts={[]} stunHosts={[]} onTryNow={() => reconnectServer(currentlyViewingServer.host)} + publicHost={currentlyViewingServer.publicHost} + onUsePublic={() => switchToPublicAddress(currentlyViewingServer)} /> ); } @@ -1384,6 +1393,8 @@ forumTags={activeDm ? [] : activeChannelForumTags} sfuHosts={serverDetails.sfu_hosts ?? (serverDetails.sfu_host ? [serverDetails.sfu_host] : [])} stunHosts={serverDetails.stun_hosts ?? []} onTryNow={() => reconnectServer(host)} + publicHost={currentlyViewingServer.publicHost} + onUsePublic={() => switchToPublicAddress(currentlyViewingServer)} /> ); } diff --git a/src/packages/socket/src/hooks/registerServerSocketEvents.ts b/src/packages/socket/src/hooks/registerServerSocketEvents.ts index 6c91d8868..da12cf915 100644 --- a/src/packages/socket/src/hooks/registerServerSocketEvents.ts +++ b/src/packages/socket/src/hooks/registerServerSocketEvents.ts @@ -283,6 +283,13 @@ export function registerServerSocketEvents(socket: Socket, host: string, ctx: Se }); socket.on("server:details", (data: serverDetails) => { + const publicHost = data.server_info?.public_host; + const saved = serversRef.current[host]; + if (saved && typeof publicHost === "string" && publicHost !== host && saved.publicHost !== publicHost) { + const updated = { ...serversRef.current, [host]: { ...saved, publicHost } }; + serversRef.current = updated; + setServers(updated); + } // An older server says nothing, which reads as off. setExternalEmojisAllowed(host, data.server_info?.external_emojis === true); /* Where the sidebar arrives, and the layer deciding whether a message makes a