Skip to content

Commit 18bcea6

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 679a7b6 commit 18bcea6

7 files changed

Lines changed: 154 additions & 100 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: 71 additions & 49 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,6 +46,19 @@ const internalLink = cva({
4446
},
4547
});
4648

49+
function inAppScope(link: URL, root: string): boolean {
50+
return (
51+
[
52+
root,
53+
"https://old.stoat.chat",
54+
"https://revolt.chat",
55+
"https://app.revolt.chat",
56+
"https://stoat.chat",
57+
].includes(link.origin) &&
58+
/\/(i|app|home|pwa|dev|invite|bot|friends|server)\/?/.test(link.pathname)
59+
);
60+
}
61+
4762
export function RenderAnchor(
4863
props: { disabled?: boolean } & JSX.AnchorHTMLAttributes<HTMLAnchorElement>,
4964
) {
@@ -56,6 +71,10 @@ export function RenderAnchor(
5671
"disabled",
5772
]);
5873

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

@@ -66,91 +85,94 @@ export function RenderAnchor(
6685
// Remap discover links to native links
6786
if (url.origin === "https://rvlt.gg" || url.origin === "https://stt.gg") {
6887
if (/^\/[\w\d]+$/.test(url.pathname)) {
69-
url = new URL(`/invite${url.pathname}`, location.origin);
88+
url = new URL(`/invite${url.pathname}`, root);
7089
} else if (url.pathname.startsWith("/discover")) {
71-
url = new URL(url.pathname, location.origin);
90+
url = new URL(url.pathname, root);
7291
}
7392
}
7493

7594
// Determine whether it's in our scope
76-
if (
77-
[
78-
location.origin,
79-
// legacy
80-
"https://app.revolt.chat",
81-
"https://revolt.chat",
82-
// new
83-
"https://stoat.chat",
84-
].includes(url.origin)
85-
) {
86-
const client = useClient();
87-
const params = paramsFromPathname(url.pathname);
95+
if (inAppScope(url, root)) {
96+
const client = useClient(),
97+
params = paramsFromPathname(url.pathname),
98+
remote = params.host !== host;
8899

89100
if (params.exactChannel) {
90101
const channel = () => client().channels.get(params.channelId!);
91102

92103
const internalUrl = () =>
93104
new URL(
94-
(channel()!.serverId
95-
? `/server/${channel()!.serverId}/channel/${channel()!.id}`
96-
: `/channel/${channel()!.id}`) +
105+
`/i/${params.host || DefaultHost}` +
106+
(channel()?.serverId ? `/server/${channel()!.serverId}` : "") +
107+
`/channel/${params.channelId}` +
97108
(params.exactMessage && params.messageId
98109
? `/${params.messageId}`
99110
: ""),
100111
location.origin,
101-
).toString();
112+
).href;
102113

103114
return (
104-
<Switch
115+
<Show
116+
when={remote || channel()}
105117
fallback={
106118
<span class={internalLink()}>
107119
<Symbol>tag</Symbol>
108120
<Trans>Private Channel</Trans>
109121
</span>
110122
}
111123
>
112-
<Match when={channel()}>
113-
<LinkComponent
114-
class={internalLink()}
115-
disabled={localProps.disabled}
116-
href={internalUrl()}
117-
>
118-
<Symbol>tag</Symbol>
119-
{channel()!.name}
120-
{params.exactMessage && (
121-
<>
122-
<MdChevronRight {...iconSize("1em")} />
123-
<MdChat {...iconSize("1em")} />
124-
</>
125-
)}
126-
</LinkComponent>
127-
</Match>
128-
</Switch>
124+
<LinkComponent
125+
class={internalLink()}
126+
disabled={props.disabled}
127+
href={internalUrl()}
128+
>
129+
<Symbol>tag</Symbol>
130+
{remote ? <Trans>Remote Channel</Trans> : channel()!.name}
131+
{params.exactMessage && (
132+
<>
133+
<MdChevronRight {...iconSize("1em")} />
134+
<MdChat {...iconSize("1em")} />
135+
</>
136+
)}
137+
</LinkComponent>
138+
</Show>
129139
);
130140
} else if (params.exactServer) {
131141
const server = () => client().servers.get(params.serverId!);
132142
const internalUrl = () =>
133-
new URL(`/server/${server()!.id}`, location.origin).toString();
143+
new URL(
144+
`/i/${params.host || DefaultHost}/server/${params.serverId}`,
145+
location.origin,
146+
).href;
134147

135148
return (
136-
<Switch
149+
<Show
150+
when={remote || server()}
137151
fallback={
138152
<span class={internalLink()}>
139153
<MdPeople {...iconSize("1em")} />
140154
<Trans>Unknown Server</Trans>
141155
</span>
142156
}
143157
>
144-
<Match when={server()}>
145-
<LinkComponent
146-
class={internalLink()}
147-
disabled={localProps.disabled}
148-
href={internalUrl()}
149-
>
150-
<Avatar size={16} src={server()?.iconURL} /> {server()?.name}
151-
</LinkComponent>
152-
</Match>
153-
</Switch>
158+
<LinkComponent
159+
class={internalLink()}
160+
disabled={props.disabled}
161+
href={internalUrl()}
162+
>
163+
{remote ? (
164+
<>
165+
<MdPeople {...iconSize("1em")} />
166+
<Trans>Remote Server</Trans>
167+
</>
168+
) : (
169+
<>
170+
<Avatar size={16} src={server()!.iconURL} />
171+
{server()!.name}
172+
</>
173+
)}
174+
</LinkComponent>
175+
</Show>
154176
);
155177
} else if (
156178
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)