Skip to content

Commit 61648b4

Browse files
committed
fix: We shall unite all as one!
- Support cross-instance channel/server/invite links (Builds on work in stoatchat#1037) - Auto-redirect to appropriate instance as needed, logging into most recent saved account on instance - Fix regex for paramsFromPathname to support instance prefix - Fix annoying draft console logging Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com>
1 parent 190cfff commit 61648b4

7 files changed

Lines changed: 144 additions & 95 deletions

File tree

packages/client/components/client/Controller.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { detect } from "detect-browser";
44
import { API, Client, ConnectionState } from "stoat.js";
55
import { ProtocolV1 } from "stoat.js/lib/events/v1";
66

7+
import { DefaultHost, DefaultURL } from "@revolt/instance";
78
import { ModalControllerExtended } from "@revolt/modal";
89
import type { State as ApplicationState } from "@revolt/state";
910
import type { Session } from "@revolt/state/stores/Auth";
10-
import Instance, { DefaultURL } from "../instance/Instance";
11+
import Instance from "../instance/Instance";
1112

1213
export enum State {
1314
Ready = "Ready",
@@ -612,10 +613,32 @@ export default class ClientController {
612613
}
613614

614615
/** Check if instance matches auth, and switch if it doesn't */
615-
#checkSwapInstance() {
616-
const ses = this.state.auth.getSession();
617-
if (ses) console.log("SES CHECK", ses?.host, this.instance.host);
618-
if (ses && (ses.host || null) !== (this.instance.host || null)) {
616+
#checkSwapInstance(swapUser = true) {
617+
const host = this.instance.host || DefaultHost,
618+
ses = this.state.auth.getSession();
619+
if (ses)
620+
console.log(
621+
"SES CHECK",
622+
(ses.host || DefaultHost) !== host,
623+
ses.host || DefaultHost,
624+
host,
625+
);
626+
if (ses && (ses.host || DefaultHost) !== host) {
627+
//First try to find an account that fits this instance
628+
if (swapUser) {
629+
const sl = this.state.auth.getSaved();
630+
//Itterate backwards to prefer last-used
631+
for (let i = sl.length - 1; i >= 0; --i)
632+
if ((sl[i].host || DefaultHost) === host) {
633+
console.log("SES SWAP TO BETTER FIT", sl[i]);
634+
this.state.auth.swapSession(sl[i].userId);
635+
this.lifecycle.transition({
636+
type: TransitionType.LoginCached,
637+
session: this.state.auth.getSession()!,
638+
});
639+
return true;
640+
}
641+
}
619642
//Delay to ensure auth is written to disk
620643
setTimeout(
621644
() =>
@@ -632,7 +655,7 @@ export default class ClientController {
632655
console.log("AUTH swapAccount", userId);
633656
this.#cacheUserInfo();
634657
this.state.auth.swapSession(userId);
635-
if (this.#checkSwapInstance()) return;
658+
if (this.#checkSwapInstance(false)) return;
636659
this.lifecycle.transition({
637660
type: TransitionType.Logout,
638661
});

packages/client/components/instance/Instance.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { CONFIGURATION } from "@revolt/common";
21
import { Navigator } from "@solidjs/router";
2+
import { DefaultHost, DefaultOrigin } from ".";
33

4-
export const DefaultURL = new URL(CONFIGURATION.DEFAULT_API_URL);
5-
export const DefaultHost = DefaultURL.host;
6-
7-
const DefOrigin = DefaultURL.origin,
8-
R_RelPath = /^\/i\/[^/]+/;
4+
const R_RelPath = /^\/i\/[^/]+/;
95

106
export default class Instance {
117
readonly host?: string;
@@ -70,7 +66,9 @@ export default class Instance {
7066
* @param base Defaults to the base path of this instance
7167
*/
7268
href = (path: string, pathOnly?: boolean, base?: string) =>
73-
(pathOnly ? "" : DefOrigin) + (base ? `/i/${base}` : this.basePath) + path;
69+
(pathOnly ? "" : DefaultOrigin) +
70+
(base ? `/i/${base}` : this.basePath) +
71+
path;
7472

7573
/** Convert an instance-specific path back to relative form */
7674
static relPath = (path: string) => path.replace(R_RelPath, "");

packages/client/components/instance/index.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { Dynamic } from "solid-js/web";
1717
import { API } from "stoat.js";
1818
import Instance from "./Instance";
1919

20+
export const DefaultURL = new URL(CONFIGURATION.DEFAULT_API_URL);
21+
export const DefaultOrigin = DefaultURL.origin;
22+
export const DefaultHost = DefaultURL.host;
23+
const DefRoute = `/i/${DefaultHost}/`;
24+
2025
const instanceContext = createContext<Instance>();
2126

2227
export function InstanceContext(props: { children?: JSXElement }) {
@@ -26,23 +31,27 @@ export function InstanceContext(props: { children?: JSXElement }) {
2631

2732
createAsync(async () => {
2833
setInst(undefined);
34+
35+
//Redirect default instance
36+
if (params.host === DefaultHost) {
37+
nav(Instance.relPath(location.pathname));
38+
delete params.host;
39+
}
40+
2941
let apiUrl = CONFIGURATION.DEFAULT_API_URL as string,
3042
wsUrl = CONFIGURATION.DEFAULT_WS_URL as string,
3143
mediaUrl = CONFIGURATION.DEFAULT_MEDIA_URL as string,
3244
proxyUrl = CONFIGURATION.DEFAULT_PROXY_URL as string;
3345

3446
try {
3547
if (params.host) {
36-
// TODO: Find a way to get this other than guessing
3748
apiUrl = `https://${params.host}/api`;
3849

3950
//TODO Link safety warning modal (might need a new
4051
// variant of it) if it's an instance they've
4152
// never connected to before?
4253

43-
const api = new API.API({
44-
baseURL: apiUrl,
45-
});
54+
const api = new API.API({ baseURL: apiUrl });
4655

4756
const cfg = await api.get("/");
4857
wsUrl = cfg.ws;
@@ -56,6 +65,7 @@ export function InstanceContext(props: { children?: JSXElement }) {
5665
wsUrl,
5766
mediaUrl,
5867
proxyUrl,
68+
//TODO Detect the below options from API
5969
CONFIGURATION.DEFAULT_GIFBOX_URL,
6070
CONFIGURATION.HCAPTCHA_SITEKEY,
6171
CONFIGURATION.MAX_EMOJI,
@@ -82,13 +92,25 @@ export function InstanceContext(props: { children?: JSXElement }) {
8292
);
8393
}
8494

95+
const DEF_MARK = "_defInst";
96+
8597
function Redirect() {
8698
const inst = useInstance(),
8799
nav = useNavigate();
88100

89101
useBeforeLeave((e) => {
90-
//Redirect relative path to instance path
91-
if (inst.host && typeof e.to === "string" && !e.to.startsWith("/i/")) {
102+
if (typeof e.to !== "string") return;
103+
104+
if ((e.to + "/").startsWith(DefRoute)) {
105+
//Redirect default instance
106+
e.preventDefault();
107+
nav(Instance.relPath(e.to), { state: DEF_MARK });
108+
} else if (
109+
inst.host &&
110+
!e.to.startsWith("/i/") &&
111+
e.options?.state !== DEF_MARK
112+
) {
113+
//Redirect relative path to instance path
92114
e.preventDefault();
93115
nav(inst.href(e.to, true));
94116
}

packages/client/components/markdown/plugins/anchors.tsx

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { JSX, Match, Show, Switch, splitProps } from "solid-js";
1+
import { JSX, Show, splitProps } from "solid-js";
22

33
import { Trans } from "@lingui-solid/solid/macro";
44
import { cva } from "styled-system/css";
55

66
import { useClient } from "@revolt/client";
7+
import { DefaultHost, useInstance } from "@revolt/instance";
78
import { useModals } from "@revolt/modal";
89
import { paramsFromPathname } from "@revolt/routing";
910
import { useState } from "@revolt/state";
@@ -14,6 +15,7 @@ import { Symbol } from "@revolt/ui/components/utils/Symbol";
1415
import MdChat from "@material-design-icons/svg/outlined/chat.svg?component-solid";
1516
import MdChevronRight from "@material-design-icons/svg/outlined/chevron_right.svg?component-solid";
1617
import MdPeople from "@material-design-icons/svg/outlined/people.svg?component-solid";
18+
1719
// import { determineLink } from "../../../lib/links";
1820
// import { modalController } from "../../../controllers/modals/ModalController";
1921

@@ -44,16 +46,16 @@ const internalLink = cva({
4446
},
4547
});
4648

47-
function inAppScope(link: URL): boolean {
49+
function inAppScope(link: URL, root: string): boolean {
4850
return (
4951
[
50-
location.origin,
52+
root,
5153
"https://old.stoat.chat",
5254
"https://revolt.chat",
5355
"https://app.revolt.chat",
5456
"https://stoat.chat",
5557
].includes(link.origin) &&
56-
/\/(app|home|pwa|dev|invite|bot|friends|server)\/?/.test(link.pathname)
58+
/\/(i|app|home|pwa|dev|invite|bot|friends|server)\/?/.test(link.pathname)
5759
);
5860
}
5961

@@ -69,6 +71,10 @@ export function RenderAnchor(
6971
"disabled",
7072
]);
7173

74+
const instance = useInstance(),
75+
host = instance.host || undefined,
76+
root = new URL(instance.apiUrl).origin;
77+
7278
// Handle case where there is no link
7379
if (!localProps.href) return <span>{remoteProps.children}</span>;
7480

@@ -79,82 +85,93 @@ export function RenderAnchor(
7985
// Remap discover links to native links
8086
if (url.origin === "https://rvlt.gg" || url.origin === "https://stt.gg") {
8187
if (/^\/[\w\d]+$/.test(url.pathname)) {
82-
url = new URL(`/invite${url.pathname}`, location.origin);
88+
url = new URL(`/invite${url.pathname}`, root);
8389
} else if (url.pathname.startsWith("/discover")) {
84-
url = new URL(url.pathname, location.origin);
90+
url = new URL(url.pathname, root);
8591
}
8692
}
8793

8894
// Determine whether it's in our scope
89-
if (inAppScope(url)) {
90-
const client = useClient();
91-
const params = paramsFromPathname(url.pathname);
95+
if (inAppScope(url, root)) {
96+
const client = useClient(),
97+
params = paramsFromPathname(url.pathname),
98+
remote = params.host !== host;
9299

93100
if (params.exactChannel) {
94101
const channel = () => client().channels.get(params.channelId!);
95-
96102
const internalUrl = () =>
97103
new URL(
98-
(channel()!.serverId
99-
? `/server/${channel()!.serverId}/channel/${channel()!.id}`
100-
: `/channel/${channel()!.id}`) +
104+
`/i/${params.host || DefaultHost}` +
105+
(channel()?.serverId ? `/server/${channel()!.serverId}` : "") +
106+
`/channel/${params.channelId}` +
101107
(params.exactMessage && params.messageId
102108
? `/${params.messageId}`
103109
: ""),
104110
location.origin,
105-
).toString();
111+
).href;
106112

107113
return (
108-
<Switch
114+
<Show
115+
when={remote || channel()}
109116
fallback={
110117
<span class={internalLink()}>
111118
<Symbol>tag</Symbol>
112119
<Trans>Private Channel</Trans>
113120
</span>
114121
}
115122
>
116-
<Match when={channel()}>
117-
<LinkComponent
118-
class={internalLink()}
119-
disabled={localProps.disabled}
120-
href={internalUrl()}
121-
>
122-
<Symbol>tag</Symbol>
123-
{channel()!.name}
124-
{params.exactMessage && (
125-
<>
126-
<MdChevronRight {...iconSize("1em")} />
127-
<MdChat {...iconSize("1em")} />
128-
</>
129-
)}
130-
</LinkComponent>
131-
</Match>
132-
</Switch>
123+
<LinkComponent
124+
class={internalLink()}
125+
disabled={props.disabled}
126+
href={internalUrl()}
127+
>
128+
<Symbol>tag</Symbol>
129+
{remote ? <Trans>Remote Channel</Trans> : channel()!.name}
130+
{params.exactMessage && (
131+
<>
132+
<MdChevronRight {...iconSize("1em")} />
133+
<MdChat {...iconSize("1em")} />
134+
</>
135+
)}
136+
</LinkComponent>
137+
</Show>
133138
);
134139
} else if (params.exactServer) {
135140
const server = () => client().servers.get(params.serverId!);
136141
const internalUrl = () =>
137-
new URL(`/server/${server()!.id}`, location.origin).toString();
142+
new URL(
143+
`/i/${params.host || DefaultHost}/server/${params.serverId}`,
144+
location.origin,
145+
).href;
138146

139147
return (
140-
<Switch
148+
<Show
149+
when={remote || server()}
141150
fallback={
142151
<span class={internalLink()}>
143152
<MdPeople {...iconSize("1em")} />
144153
<Trans>Unknown Server</Trans>
145154
</span>
146155
}
147156
>
148-
<Match when={server()}>
149-
<LinkComponent
150-
class={internalLink()}
151-
disabled={localProps.disabled}
152-
href={internalUrl()}
153-
>
154-
<Avatar size={16} src={server()?.iconURL} /> {server()?.name}
155-
</LinkComponent>
156-
</Match>
157-
</Switch>
157+
<LinkComponent
158+
class={internalLink()}
159+
disabled={props.disabled}
160+
href={internalUrl()}
161+
>
162+
{remote ? (
163+
<>
164+
<MdPeople {...iconSize("1em")} />
165+
<Trans>Remote Server</Trans>
166+
</>
167+
) : (
168+
<>
169+
<Avatar size={16} src={server()!.iconURL} />
170+
{server()!.name}
171+
</>
172+
)}
173+
</LinkComponent>
174+
</Show>
158175
);
159176
} else if (
160177
params.inviteId &&

packages/client/components/modal/modals/AdvancedLoginModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { createFormControl, createFormGroup } from "solid-forms";
33
import { Trans, useLingui } from "@lingui-solid/solid/macro";
44
import { styled } from "styled-system/jsx";
55

6-
import { useInstance } from "@revolt/instance";
7-
import { DefaultURL } from "@revolt/instance/Instance";
6+
import { DefaultURL, useInstance } from "@revolt/instance";
87
import { Column, Dialog, DialogProps, Form2 } from "@revolt/ui";
98

109
import { Modals } from "../types";

0 commit comments

Comments
 (0)