Skip to content

Commit 7f52980

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 7b07588 commit 7f52980

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,11 +4,12 @@ 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";
1011

11-
import Instance, { DefaultURL } from "../instance/Instance";
12+
import Instance from "../instance/Instance";
1213
import { killServiceWorkerSubscription } from "./NotificationsController";
1314

1415
export enum State {
@@ -620,10 +621,32 @@ export default class ClientController {
620621
}
621622

622623
/** Check if instance matches auth, and switch if it doesn't */
623-
#checkSwapInstance() {
624-
const ses = this.state.auth.getSession();
625-
if (ses) console.log("SES CHECK", ses?.host, this.instance.host);
626-
if (ses && (ses.host || null) !== (this.instance.host || null)) {
624+
#checkSwapInstance(swapUser = true) {
625+
const host = this.instance.host || DefaultHost,
626+
ses = this.state.auth.getSession();
627+
if (ses)
628+
console.log(
629+
"SES CHECK",
630+
(ses.host || DefaultHost) !== host,
631+
ses.host || DefaultHost,
632+
host,
633+
);
634+
if (ses && (ses.host || DefaultHost) !== host) {
635+
//First try to find an account that fits this instance
636+
if (swapUser) {
637+
const sl = this.state.auth.getSaved();
638+
//Itterate backwards to prefer last-used
639+
for (let i = sl.length - 1; i >= 0; --i)
640+
if ((sl[i].host || DefaultHost) === host) {
641+
console.log("SES SWAP TO BETTER FIT", sl[i]);
642+
this.state.auth.swapSession(sl[i].userId);
643+
this.lifecycle.transition({
644+
type: TransitionType.LoginCached,
645+
session: this.state.auth.getSession()!,
646+
});
647+
return true;
648+
}
649+
}
627650
//Delay to ensure auth is written to disk
628651
setTimeout(
629652
() =>
@@ -639,7 +662,7 @@ export default class ClientController {
639662
swapAccount(userId: string) {
640663
this.#cacheUserInfo();
641664
this.state.auth.swapSession(userId);
642-
if (this.#checkSwapInstance()) return;
665+
if (this.#checkSwapInstance(false)) return;
643666
this.lifecycle.transition({
644667
type: TransitionType.Logout,
645668
});

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,10 +1,11 @@
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 { MessageContextMenu, useMessage } from "@revolt/app";
77
import { useClient } from "@revolt/client";
8+
import { DefaultHost, useInstance } from "@revolt/instance";
89
import { useModals } from "@revolt/modal";
910
import { paramsFromPathname } from "@revolt/routing";
1011
import { useState } from "@revolt/state";
@@ -15,6 +16,7 @@ import { Symbol } from "@revolt/ui/components/utils/Symbol";
1516
import MdChat from "@material-design-icons/svg/outlined/chat.svg?component-solid";
1617
import MdChevronRight from "@material-design-icons/svg/outlined/chevron_right.svg?component-solid";
1718
import MdPeople from "@material-design-icons/svg/outlined/people.svg?component-solid";
19+
1820
// import { determineLink } from "../../../lib/links";
1921
// import { modalController } from "../../../controllers/modals/ModalController";
2022

@@ -45,16 +47,16 @@ const internalLink = cva({
4547
},
4648
});
4749

48-
function inAppScope(link: URL): boolean {
50+
function inAppScope(link: URL, root: string): boolean {
4951
return (
5052
[
51-
location.origin,
53+
root,
5254
"https://old.stoat.chat",
5355
"https://revolt.chat",
5456
"https://app.revolt.chat",
5557
"https://stoat.chat",
5658
].includes(link.origin) &&
57-
/\/(app|home|pwa|dev|invite|bot|friends|server|channel)\/?/.test(
59+
/\/(i|app|home|pwa|dev|invite|bot|friends|server|channel)\/?/.test(
5860
link.pathname,
5961
)
6062
);
@@ -72,6 +74,10 @@ export function RenderAnchor(
7274
"disabled",
7375
]);
7476

77+
const instance = useInstance(),
78+
host = instance.host || undefined,
79+
root = new URL(instance.apiUrl).origin;
80+
7581
// Handle case where there is no link
7682
if (!localProps.href) return <span>{remoteProps.children}</span>;
7783

@@ -91,82 +97,93 @@ export function RenderAnchor(
9197
// Remap discover links to native links
9298
if (url.origin === "https://rvlt.gg" || url.origin === "https://stt.gg") {
9399
if (/^\/[\w\d]+$/.test(url.pathname)) {
94-
url = new URL(`/invite${url.pathname}`, location.origin);
100+
url = new URL(`/invite${url.pathname}`, root);
95101
} else if (url.pathname.startsWith("/discover")) {
96-
url = new URL(url.pathname, location.origin);
102+
url = new URL(url.pathname, root);
97103
}
98104
}
99105

100106
// Determine whether it's in our scope
101-
if (inAppScope(url)) {
102-
const client = useClient();
103-
const params = paramsFromPathname(url.pathname);
107+
if (inAppScope(url, root)) {
108+
const client = useClient(),
109+
params = paramsFromPathname(url.pathname),
110+
remote = params.host !== host;
104111

105112
if (params.exactChannel) {
106113
const channel = () => client().channels.get(params.channelId!);
107-
108114
const internalUrl = () =>
109115
new URL(
110-
(channel()!.serverId
111-
? `/server/${channel()!.serverId}/channel/${channel()!.id}`
112-
: `/channel/${channel()!.id}`) +
116+
`/i/${params.host || DefaultHost}` +
117+
(channel()?.serverId ? `/server/${channel()!.serverId}` : "") +
118+
`/channel/${params.channelId}` +
113119
(params.exactMessage && params.messageId
114120
? `/${params.messageId}`
115121
: ""),
116122
location.origin,
117-
).toString();
123+
).href;
118124

119125
return (
120-
<Switch
126+
<Show
127+
when={remote || channel()}
121128
fallback={
122129
<span class={internalLink()}>
123130
<Symbol>tag</Symbol>
124131
<Trans>Private Channel</Trans>
125132
</span>
126133
}
127134
>
128-
<Match when={channel()}>
129-
<LinkComponent
130-
class={internalLink()}
131-
disabled={localProps.disabled}
132-
href={internalUrl()}
133-
>
134-
<Symbol>tag</Symbol>
135-
{channel()!.name}
136-
{params.exactMessage && (
137-
<>
138-
<MdChevronRight {...iconSize("1em")} />
139-
<MdChat {...iconSize("1em")} />
140-
</>
141-
)}
142-
</LinkComponent>
143-
</Match>
144-
</Switch>
135+
<LinkComponent
136+
class={internalLink()}
137+
disabled={props.disabled}
138+
href={internalUrl()}
139+
>
140+
<Symbol>tag</Symbol>
141+
{remote ? <Trans>Remote Channel</Trans> : channel()!.name}
142+
{params.exactMessage && (
143+
<>
144+
<MdChevronRight {...iconSize("1em")} />
145+
<MdChat {...iconSize("1em")} />
146+
</>
147+
)}
148+
</LinkComponent>
149+
</Show>
145150
);
146151
} else if (params.exactServer) {
147152
const server = () => client().servers.get(params.serverId!);
148153
const internalUrl = () =>
149-
new URL(`/server/${server()!.id}`, location.origin).toString();
154+
new URL(
155+
`/i/${params.host || DefaultHost}/server/${params.serverId}`,
156+
location.origin,
157+
).href;
150158

151159
return (
152-
<Switch
160+
<Show
161+
when={remote || server()}
153162
fallback={
154163
<span class={internalLink()}>
155164
<MdPeople {...iconSize("1em")} />
156165
<Trans>Unknown Server</Trans>
157166
</span>
158167
}
159168
>
160-
<Match when={server()}>
161-
<LinkComponent
162-
class={internalLink()}
163-
disabled={localProps.disabled}
164-
href={internalUrl()}
165-
>
166-
<Avatar size={16} src={server()?.iconURL} /> {server()?.name}
167-
</LinkComponent>
168-
</Match>
169-
</Switch>
169+
<LinkComponent
170+
class={internalLink()}
171+
disabled={props.disabled}
172+
href={internalUrl()}
173+
>
174+
{remote ? (
175+
<>
176+
<MdPeople {...iconSize("1em")} />
177+
<Trans>Remote Server</Trans>
178+
</>
179+
) : (
180+
<>
181+
<Avatar size={16} src={server()!.iconURL} />
182+
{server()!.name}
183+
</>
184+
)}
185+
</LinkComponent>
186+
</Show>
170187
);
171188
} else if (
172189
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)