Skip to content

Commit 1f2fc4f

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 27def76 commit 1f2fc4f

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 {
@@ -616,10 +617,32 @@ export default class ClientController {
616617
}
617618

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

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|channel)\/?/.test(
58+
/\/(i|app|home|pwa|dev|invite|bot|friends|server|channel)\/?/.test(
5759
link.pathname,
5860
)
5961
);
@@ -71,6 +73,10 @@ export function RenderAnchor(
7173
"disabled",
7274
]);
7375

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

@@ -90,82 +96,93 @@ export function RenderAnchor(
9096
// Remap discover links to native links
9197
if (url.origin === "https://rvlt.gg" || url.origin === "https://stt.gg") {
9298
if (/^\/[\w\d]+$/.test(url.pathname)) {
93-
url = new URL(`/invite${url.pathname}`, location.origin);
99+
url = new URL(`/invite${url.pathname}`, root);
94100
} else if (url.pathname.startsWith("/discover")) {
95-
url = new URL(url.pathname, location.origin);
101+
url = new URL(url.pathname, root);
96102
}
97103
}
98104

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

104111
if (params.exactChannel) {
105112
const channel = () => client().channels.get(params.channelId!);
106-
107113
const internalUrl = () =>
108114
new URL(
109-
(channel()!.serverId
110-
? `/server/${channel()!.serverId}/channel/${channel()!.id}`
111-
: `/channel/${channel()!.id}`) +
115+
`/i/${params.host || DefaultHost}` +
116+
(channel()?.serverId ? `/server/${channel()!.serverId}` : "") +
117+
`/channel/${params.channelId}` +
112118
(params.exactMessage && params.messageId
113119
? `/${params.messageId}`
114120
: ""),
115121
location.origin,
116-
).toString();
122+
).href;
117123

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

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