mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-25 07:02:10 -05:00
Merge branch 'revolt-bree/revite-cleanup'
This commit is contained in:
commit
56058c1e75
30 changed files with 169 additions and 119 deletions
|
@ -94,7 +94,7 @@
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scroll": "^1.8.2",
|
"react-scroll": "^1.8.2",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.0",
|
||||||
"revolt.js": "4.3.3-alpha.7",
|
"revolt.js": "4.3.3-alpha.8",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
"shade-blend-color": "^1.0.0",
|
"shade-blend-color": "^1.0.0",
|
||||||
|
|
|
@ -384,7 +384,7 @@ export default function AutoComplete({ detached, state, setState, onClick }: Pic
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
<Emoji emoji={(emojiDictionary as any)[match]} size={20} />
|
<Emoji emoji={(emojiDictionary as Record<string, string>)[match]} size={20} />
|
||||||
:{match}:
|
:{match}:
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ComboBox from "../ui/ComboBox";
|
import ComboBox from "../ui/ComboBox";
|
||||||
import { dispatch } from "../../redux";
|
import { dispatch } from "../../redux";
|
||||||
import { connectState } from "../../redux/connector";
|
import { connectState } from "../../redux/connector";
|
||||||
import { LanguageEntry, Languages } from "../../context/Locale";
|
import { Language, LanguageEntry, Languages } from "../../context/Locale";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
@ -14,12 +14,12 @@ export function LocaleSelector(props: Props) {
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_LOCALE",
|
type: "SET_LOCALE",
|
||||||
locale: e.currentTarget.value as any
|
locale: e.currentTarget.value as Language
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{Object.keys(Languages).map(x => {
|
{Object.keys(Languages).map(x => {
|
||||||
const l = (Languages as any)[x] as LanguageEntry;
|
const l = Languages[x as keyof typeof Languages];
|
||||||
return (
|
return (
|
||||||
<option value={x}>
|
<option value={x}>
|
||||||
{l.emoji} {l.display}
|
{l.emoji} {l.display}
|
||||||
|
|
|
@ -35,7 +35,12 @@ function Message({ attachContext, message, contrast, content: replacement, head:
|
||||||
const content = message.content as string;
|
const content = message.content as string;
|
||||||
const head = preferHead || (message.replies && message.replies.length > 0);
|
const head = preferHead || (message.replies && message.replies.length > 0);
|
||||||
|
|
||||||
const userContext = attachContext ? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) : undefined as any; // ! FIXME: tell fatal to make this type generic
|
// ! FIXME: tell fatal to make this type generic
|
||||||
|
// bree: Fatal please...
|
||||||
|
const userContext = attachContext
|
||||||
|
? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) as any
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const openProfile = () => openScreen({ id: 'profile', user_id: message.author });
|
const openProfile = () => openScreen({ id: 'profile', user_id: message.author });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -19,7 +19,8 @@ export default function AttachmentActions({ attachment }: Props) {
|
||||||
const open_url = `${url}/${filename}`;
|
const open_url = `${url}/${filename}`;
|
||||||
const download_url = url.replace('attachments', 'attachments/download')
|
const download_url = url.replace('attachments', 'attachments/download')
|
||||||
|
|
||||||
const filesize = determineFileSize(size as any);
|
// for some reason revolt.js says the size is a string even though it's a number
|
||||||
|
const filesize = determineFileSize(size);
|
||||||
|
|
||||||
switch (metadata.type) {
|
switch (metadata.type) {
|
||||||
case 'Image':
|
case 'Image':
|
||||||
|
|
|
@ -22,13 +22,21 @@ import MarkdownSup from "markdown-it-sup";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import MarkdownSub from "markdown-it-sub";
|
import MarkdownSub from "markdown-it-sub";
|
||||||
|
|
||||||
|
// TODO: global.d.ts file for defining globals
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
copycode: (element: HTMLDivElement) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handler for code block copy.
|
// Handler for code block copy.
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).copycode = function(element: HTMLDivElement) {
|
window.copycode = function(element: HTMLDivElement) {
|
||||||
try {
|
try {
|
||||||
let code = element.parentElement?.parentElement?.children[1];
|
let code = element.parentElement?.parentElement?.children[1];
|
||||||
if (code) {
|
if (code) {
|
||||||
navigator.clipboard.writeText((code as any).innerText.trim());
|
navigator.clipboard.writeText(code.textContent?.trim() ?? '');
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
@ -65,10 +73,17 @@ const defaultRender =
|
||||||
return self.renderToken(tokens, idx, options);
|
return self.renderToken(tokens, idx, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: global.d.ts file for defining globals
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
internalHandleURL: (element: HTMLAnchorElement) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handler for internal links, pushes events to React using magic.
|
// Handler for internal links, pushes events to React using magic.
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).internalHandleURL = function(element: HTMLAnchorElement) {
|
window.internalHandleURL = function(element: HTMLAnchorElement) {
|
||||||
const url = new URL(element.href, location as any);
|
const url = new URL(element.href, location.href);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
if (pathname.startsWith("/@")) {
|
if (pathname.startsWith("/@")) {
|
||||||
|
@ -87,7 +102,7 @@ md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
|
||||||
// For internal links, we should use our own handler to use react-router history.
|
// For internal links, we should use our own handler to use react-router history.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const href = tokens[idx].attrs[hIndex][1];
|
const href = tokens[idx].attrs[hIndex][1];
|
||||||
const url = new URL(href, location as any);
|
const url = new URL(href, location.href);
|
||||||
|
|
||||||
if (url.hostname === location.hostname) {
|
if (url.hostname === location.hostname) {
|
||||||
internal = true;
|
internal = true;
|
||||||
|
@ -161,7 +176,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||||
data-large-emojis={useLargeEmojis}
|
data-large-emojis={useLargeEmojis}
|
||||||
onClick={ev => {
|
onClick={ev => {
|
||||||
if (ev.target) {
|
if (ev.target) {
|
||||||
let element: Element = ev.target as any;
|
let element = ev.currentTarget;
|
||||||
if (element.classList.contains("spoiler")) {
|
if (element.classList.contains("spoiler")) {
|
||||||
element.classList.add("shown");
|
element.classList.add("shown");
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,12 +56,8 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
|
||||||
|
|
||||||
members.sort((a, b) => {
|
members.sort((a, b) => {
|
||||||
// ! FIXME: should probably rewrite all this code
|
// ! FIXME: should probably rewrite all this code
|
||||||
let l = ((a.online &&
|
let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
|
||||||
a.status?.presence !== Users.Presence.Invisible) ??
|
let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
|
||||||
false) as any | 0;
|
|
||||||
let r = ((b.online &&
|
|
||||||
b.status?.presence !== Users.Presence.Invisible) ??
|
|
||||||
false) as any | 0;
|
|
||||||
|
|
||||||
let n = r - l;
|
let n = r - l;
|
||||||
if (n !== 0) {
|
if (n !== 0) {
|
||||||
|
@ -161,12 +157,8 @@ export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channel
|
||||||
// copy paste from above
|
// copy paste from above
|
||||||
users.sort((a, b) => {
|
users.sort((a, b) => {
|
||||||
// ! FIXME: should probably rewrite all this code
|
// ! FIXME: should probably rewrite all this code
|
||||||
let l = ((a.online &&
|
let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
|
||||||
a.status?.presence !== Users.Presence.Invisible) ??
|
let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
|
||||||
false) as any | 0;
|
|
||||||
let r = ((b.online &&
|
|
||||||
b.status?.presence !== Users.Presence.Invisible) ??
|
|
||||||
false) as any | 0;
|
|
||||||
|
|
||||||
let n = r - l;
|
let n = r - l;
|
||||||
if (n !== 0) {
|
if (n !== 0) {
|
||||||
|
|
|
@ -114,9 +114,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Locale({ children, locale }: Props) {
|
function Locale({ children, locale }: Props) {
|
||||||
const [defns, setDefinition] = useState(definition);
|
// TODO: create and use LanguageDefinition type here
|
||||||
|
const [defns, setDefinition] = useState<Record<string, unknown>>(definition);
|
||||||
const lang = Languages[locale];
|
const lang = Languages[locale];
|
||||||
|
|
||||||
|
// TODO: clean this up and use the built in Intl API
|
||||||
function transformLanguage(source: { [key: string]: any }) {
|
function transformLanguage(source: { [key: string]: any }) {
|
||||||
const obj = defaultsDeep(source, definition);
|
const obj = defaultsDeep(source, definition);
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ import { Children } from "../types/Preact";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useMemo } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
|
|
||||||
export const SettingsContext = createContext<Settings>({} as any);
|
export const SettingsContext = createContext<Settings>({});
|
||||||
export const SoundContext = createContext<(sound: Sounds) => void>({} as any);
|
export const SoundContext = createContext<((sound: Sounds) => void)>(null!);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Children,
|
children?: Children,
|
||||||
|
|
|
@ -30,7 +30,15 @@ export type Variables =
|
||||||
| "status-away"
|
| "status-away"
|
||||||
| "status-busy"
|
| "status-busy"
|
||||||
| "status-streaming"
|
| "status-streaming"
|
||||||
| "status-invisible";
|
| "status-invisible"
|
||||||
|
|
||||||
|
// While this isn't used, it'd be good to keep this up to date as a reference or for future use
|
||||||
|
export type HiddenVariables =
|
||||||
|
| "font"
|
||||||
|
| "ligatures"
|
||||||
|
| "app-height"
|
||||||
|
| "sidebar-active"
|
||||||
|
| "monospace-font"
|
||||||
|
|
||||||
export type Fonts = 'Open Sans' | 'Inter' | 'Atkinson Hyperlegible' | 'Roboto' | 'Noto Sans' | 'Lato' | 'Bree Serif' | 'Montserrat' | 'Poppins' | 'Raleway' | 'Ubuntu' | 'Comic Neue';
|
export type Fonts = 'Open Sans' | 'Inter' | 'Atkinson Hyperlegible' | 'Roboto' | 'Noto Sans' | 'Lato' | 'Bree Serif' | 'Montserrat' | 'Poppins' | 'Raleway' | 'Ubuntu' | 'Comic Neue';
|
||||||
export type MonoscapeFonts = 'Fira Code' | 'Roboto Mono' | 'Source Code Pro' | 'Space Mono' | 'Ubuntu Mono';
|
export type MonoscapeFonts = 'Fira Code' | 'Roboto Mono' | 'Source Code Pro' | 'Space Mono' | 'Ubuntu Mono';
|
||||||
|
@ -191,7 +199,7 @@ export const DEFAULT_FONT = 'Open Sans';
|
||||||
export const DEFAULT_MONO_FONT = 'Fira Code';
|
export const DEFAULT_MONO_FONT = 'Fira Code';
|
||||||
|
|
||||||
// Generated from https://gitlab.insrt.uk/revolt/community/themes
|
// Generated from https://gitlab.insrt.uk/revolt/community/themes
|
||||||
export const PRESETS: { [key: string]: Theme } = {
|
export const PRESETS: Record<string, Theme> = {
|
||||||
light: {
|
light: {
|
||||||
light: true,
|
light: true,
|
||||||
accent: "#FD6671",
|
accent: "#FD6671",
|
||||||
|
@ -217,7 +225,7 @@ export const PRESETS: { [key: string]: Theme } = {
|
||||||
"status-away": "#F39F00",
|
"status-away": "#F39F00",
|
||||||
"status-busy": "#F84848",
|
"status-busy": "#F84848",
|
||||||
"status-streaming": "#977EFF",
|
"status-streaming": "#977EFF",
|
||||||
"status-invisible": "#A5A5A5",
|
"status-invisible": "#A5A5A5"
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
light: false,
|
light: false,
|
||||||
|
@ -244,7 +252,7 @@ export const PRESETS: { [key: string]: Theme } = {
|
||||||
"status-away": "#F39F00",
|
"status-away": "#F39F00",
|
||||||
"status-busy": "#F84848",
|
"status-busy": "#F84848",
|
||||||
"status-streaming": "#977EFF",
|
"status-streaming": "#977EFF",
|
||||||
"status-invisible": "#A5A5A5",
|
"status-invisible": "#A5A5A5"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -259,7 +267,8 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ThemeContext = createContext<Theme>({} as any);
|
// Load the default default them and apply extras later
|
||||||
|
export const ThemeContext = createContext<Theme>(PRESETS['dark']);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Children;
|
children: Children;
|
||||||
|
@ -269,7 +278,7 @@ interface Props {
|
||||||
function Theme({ children, options }: Props) {
|
function Theme({ children, options }: Props) {
|
||||||
const theme: Theme = {
|
const theme: Theme = {
|
||||||
...PRESETS["dark"],
|
...PRESETS["dark"],
|
||||||
...(PRESETS as any)[options?.preset as any],
|
...PRESETS[options?.preset ?? ''],
|
||||||
...options?.custom
|
...options?.custom
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,9 @@ export interface VoiceState {
|
||||||
participants?: Readonly<Map<string, VoiceUser>>;
|
participants?: Readonly<Map<string, VoiceUser>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VoiceContext = createContext<VoiceState>(undefined as any);
|
// They should be present from first render. - insert's words
|
||||||
export const VoiceOperationsContext = createContext<VoiceOperations>(undefined as any);
|
export const VoiceContext = createContext<VoiceState>(null!);
|
||||||
|
export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: Children;
|
children: Children;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import styles from "./Onboarding.module.scss";
|
import styles from "./Onboarding.module.scss";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
|
@ -14,12 +14,16 @@ interface Props {
|
||||||
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormInputs {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
export function OnboardingModal({ onClose, callback }: Props) {
|
export function OnboardingModal({ onClose, callback }: Props) {
|
||||||
const { handleSubmit, register } = useForm();
|
const { handleSubmit, register } = useForm<FormInputs>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
async function onSubmit({ username }: { username: string }) {
|
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
callback(username, true)
|
callback(username, true)
|
||||||
.then(onClose)
|
.then(onClose)
|
||||||
|
@ -45,7 +49,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
|
||||||
<p>
|
<p>
|
||||||
<Text id="app.special.modals.onboarding.pick" />
|
<Text id="app.special.modals.onboarding.pick" />
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={handleSubmit(onSubmit) as any}>
|
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
|
||||||
<div>
|
<div>
|
||||||
<FormField
|
<FormField
|
||||||
type="username"
|
type="username"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import Modal from "../../../components/ui/Modal";
|
import Modal from "../../../components/ui/Modal";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useContext, useState } from "preact/hooks";
|
||||||
|
@ -12,22 +12,28 @@ interface Props {
|
||||||
field: "username" | "email" | "password";
|
field: "username" | "email" | "password";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormInputs {
|
||||||
|
password: string,
|
||||||
|
new_email: string,
|
||||||
|
new_username: string,
|
||||||
|
new_password: string,
|
||||||
|
|
||||||
|
// TODO: figure out if this is correct or not
|
||||||
|
// it wasn't in the types before this was typed but the element itself was there
|
||||||
|
current_password?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function ModifyAccountModal({ onClose, field }: Props) {
|
export function ModifyAccountModal({ onClose, field }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const { handleSubmit, register, errors } = useForm();
|
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
async function onSubmit({
|
const onSubmit: SubmitHandler<FormInputs> = async ({
|
||||||
password,
|
password,
|
||||||
new_username,
|
new_username,
|
||||||
new_email,
|
new_email,
|
||||||
new_password
|
new_password
|
||||||
}: {
|
}) => {
|
||||||
password: string;
|
|
||||||
new_username: string;
|
|
||||||
new_email: string;
|
|
||||||
new_password: string;
|
|
||||||
}) {
|
|
||||||
try {
|
try {
|
||||||
if (field === "email") {
|
if (field === "email") {
|
||||||
await client.req("POST", "/auth/change/email", {
|
await client.req("POST", "/auth/change/email", {
|
||||||
|
@ -75,7 +81,8 @@ export function ModifyAccountModal({ onClose, field }: Props) {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onSubmit) as any}>
|
{/* Preact / React typing incompatabilities */}
|
||||||
|
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
|
||||||
{field === "email" && (
|
{field === "email" && (
|
||||||
<FormField
|
<FormField
|
||||||
type="email"
|
type="email"
|
||||||
|
|
|
@ -44,8 +44,8 @@ export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooL
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.multiple = multiple ?? false;
|
input.multiple = multiple ?? false;
|
||||||
|
|
||||||
input.onchange = async e => {
|
input.onchange = async (e) => {
|
||||||
const files = (e.target as any)?.files;
|
const files = (e.currentTarget as HTMLInputElement)?.files;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
if (file.size > maxFileSize) {
|
if (file.size > maxFileSize) {
|
||||||
|
|
|
@ -216,7 +216,7 @@ function Notifier({ options, notifs }: Props) {
|
||||||
document.removeEventListener("visibilitychange", visChange);
|
document.removeEventListener("visibilitychange", visChange);
|
||||||
}, [guild_id, channel_id]);
|
}, [guild_id, channel_id]);
|
||||||
|
|
||||||
return <></>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotifierComponent = connectState(
|
const NotifierComponent = connectState(
|
||||||
|
|
|
@ -34,9 +34,12 @@ export interface ClientOperations {
|
||||||
openDM: (user_id: string) => Promise<string>;
|
openDM: (user_id: string) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext<Client>(undefined as any);
|
// By the time they are used, they should all be initialized.
|
||||||
export const StatusContext = createContext<ClientStatus>(undefined as any);
|
// Currently the app does not render until a client is built and the other two are always initialized on first render.
|
||||||
export const OperationsContext = createContext<ClientOperations>(undefined as any);
|
// - insert's words
|
||||||
|
export const AppContext = createContext<Client>(null!);
|
||||||
|
export const StatusContext = createContext<ClientStatus>(null!);
|
||||||
|
export const OperationsContext = createContext<ClientOperations>(null!);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
auth: AuthState;
|
auth: AuthState;
|
||||||
|
@ -93,16 +96,14 @@ function Context({ auth, children }: Props) {
|
||||||
const login = () =>
|
const login = () =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "LOGIN",
|
type: "LOGIN",
|
||||||
session: client.session as any
|
session: client.session! // This [null assertion] is ok, we should have a session by now. - insert's words
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onboarding) {
|
if (onboarding) {
|
||||||
openScreen({
|
openScreen({
|
||||||
id: "onboarding",
|
id: "onboarding",
|
||||||
callback: async (username: string) => {
|
callback: (username: string) =>
|
||||||
await (onboarding as any)(username, true);
|
onboarding(username, true).then(login)
|
||||||
login();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
login();
|
login();
|
||||||
|
|
|
@ -63,7 +63,7 @@ function StateMonitor(props: Props) {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [ props.typing ]);
|
}, [ props.typing ]);
|
||||||
|
|
||||||
return <></>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connectState(
|
export default connectState(
|
||||||
|
|
|
@ -23,11 +23,11 @@ type Props = {
|
||||||
|
|
||||||
var lastValues: { [key in SyncKeys]?: any } = { };
|
var lastValues: { [key in SyncKeys]?: any } = { };
|
||||||
|
|
||||||
export function mapSync(packet: Sync.UserSettings, revision?: { [key: string]: number }) {
|
export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
|
||||||
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
|
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
|
||||||
for (let key of Object.keys(packet)) {
|
for (let key of Object.keys(packet)) {
|
||||||
let [ timestamp, obj ] = packet[key];
|
let [ timestamp, obj ] = packet[key];
|
||||||
if (timestamp < (revision ?? {} as any)[key] ?? 0) {
|
if (timestamp < (revision ?? {})[key] ?? 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ function SyncManager(props: Props) {
|
||||||
return () => client.removeListener('packet', onPacket);
|
return () => client.removeListener('packet', onPacket);
|
||||||
}, [ disabled, props.sync ]);
|
}, [ disabled, props.sync ]);
|
||||||
|
|
||||||
return <></>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connectState(
|
export default connectState(
|
||||||
|
|
|
@ -31,7 +31,7 @@ export function registerEvents({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners = {
|
let listeners: Record<string, (...args: any[]) => void> = {
|
||||||
connecting: () =>
|
connecting: () =>
|
||||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||||
|
|
||||||
|
@ -86,21 +86,18 @@ export function registerEvents({
|
||||||
ready: () => setStatus(ClientStatus.ONLINE)
|
ready: () => setStatus(ClientStatus.ONLINE)
|
||||||
};
|
};
|
||||||
|
|
||||||
let listenerFunc: { [key: string]: Function };
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
listenerFunc = {};
|
listeners = new Proxy(listeners, {
|
||||||
for (const listener of Object.keys(listeners)) {
|
get: (target, listener, receiver) => (...args: unknown[]) => {
|
||||||
listenerFunc[listener] = (...args: any[]) => {
|
console.debug(`Calling ${listener.toString()} with`, args);
|
||||||
console.debug(`Calling ${listener} with`, args);
|
Reflect.get(target, listener)(...args)
|
||||||
(listeners as any)[listener](...args);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
listenerFunc = listeners;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const listener of Object.keys(listenerFunc)) {
|
// TODO: clean this a bit and properly handle types
|
||||||
client.addListener(listener, (listenerFunc as any)[listener]);
|
for (const listener in listeners) {
|
||||||
|
client.addListener(listener, listeners[listener]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logMutation(target: string, key: string) {
|
function logMutation(target: string, key: string) {
|
||||||
|
@ -134,8 +131,8 @@ export function registerEvents({
|
||||||
window.addEventListener("offline", offline);
|
window.addEventListener("offline", offline);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const listener of Object.keys(listenerFunc)) {
|
for (const listener in listeners) {
|
||||||
client.removeListener(listener, (listenerFunc as any)[listener]);
|
client.removeListener(listener, listeners[listener as keyof typeof listeners]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||||
import { Client, PermissionCalculator } from 'revolt.js';
|
import { Client, PermissionCalculator } from 'revolt.js';
|
||||||
import { AppContext } from "./RevoltClient";
|
import { AppContext } from "./RevoltClient";
|
||||||
|
import Collection from "revolt.js/dist/maps/Collection";
|
||||||
|
|
||||||
export interface HookContext {
|
export interface HookContext {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
@ -25,7 +26,16 @@ export function useForceUpdate(context?: HookContext): HookContext {
|
||||||
return { client, forceUpdate: () => updateState(Math.random()) };
|
return { client, forceUpdate: () => updateState(Math.random()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function useObject(type: string, id?: string | string[], context?: HookContext) {
|
// TODO: utils.d.ts maybe?
|
||||||
|
type PickProperties<T, U> = Pick<T, {
|
||||||
|
[K in keyof T]: T[K] extends U ? K : never
|
||||||
|
}[keyof T]>
|
||||||
|
|
||||||
|
// The keys in Client that are an object
|
||||||
|
// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
|
||||||
|
type ClientCollectionKey = Exclude<keyof PickProperties<Client, Collection<any>>, undefined>;
|
||||||
|
|
||||||
|
function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
|
||||||
const ctx = useForceUpdate(context);
|
const ctx = useForceUpdate(context);
|
||||||
|
|
||||||
function update(target: any) {
|
function update(target: any) {
|
||||||
|
@ -35,7 +45,7 @@ function useObject(type: string, id?: string | string[], context?: HookContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = (ctx.client as any)[type];
|
const map = ctx.client[type];
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map.addListener("update", update);
|
map.addListener("update", update);
|
||||||
return () => map.removeListener("update", update);
|
return () => map.removeListener("update", update);
|
||||||
|
|
|
@ -307,11 +307,15 @@ function ContextMenus(props: Props) {
|
||||||
case "delete_server":
|
case "delete_server":
|
||||||
case "delete_message":
|
case "delete_message":
|
||||||
case "create_channel":
|
case "create_channel":
|
||||||
// @ts-expect-error
|
case "create_invite":
|
||||||
case "create_invite": openScreen({ id: "special_prompt", type: data.action, target: data.target }); break;
|
// The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
|
||||||
|
openScreen({ id: "special_prompt", type: data.action, target: data.target as any });
|
||||||
|
break;
|
||||||
|
|
||||||
case "ban_member":
|
case "ban_member":
|
||||||
case "kick_member": openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user }); break;
|
case "kick_member":
|
||||||
|
openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user });
|
||||||
|
break;
|
||||||
|
|
||||||
case "open_notification_options": {
|
case "open_notification_options": {
|
||||||
openContextMenu("NotificationOptions", { channel: data.channel });
|
openContextMenu("NotificationOptions", { channel: data.channel });
|
||||||
|
@ -427,7 +431,7 @@ function ContextMenus(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
let actions: string[];
|
let actions: Action['action'][];
|
||||||
switch (user.relationship) {
|
switch (user.relationship) {
|
||||||
case Users.Relationship.User: actions = []; break;
|
case Users.Relationship.User: actions = []; break;
|
||||||
case Users.Relationship.Friend:
|
case Users.Relationship.Friend:
|
||||||
|
@ -461,11 +465,9 @@ function ContextMenus(props: Props) {
|
||||||
generateAction({ action: 'message_user', user: user._id });
|
generateAction({ action: 'message_user', user: user._id });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const action of actions) {
|
for(let i = 0; i < actions.length; i++) {
|
||||||
generateAction({
|
// The any here is because typescript can't determine that user the actions are linked together correctly
|
||||||
action: action as any,
|
generateAction({ action: actions[i] as any, user })
|
||||||
user
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ export default function FormField({
|
||||||
)}
|
)}
|
||||||
<Localizer>
|
<Localizer>
|
||||||
<InputBox
|
<InputBox
|
||||||
|
// Styled uses React typing while we use Preact
|
||||||
|
// this leads to inconsistances where things need to be typed oddly
|
||||||
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
||||||
name={
|
name={
|
||||||
type === "current_password" ? "password" : name ?? type
|
type === "current_password" ? "password" : name ?? type
|
||||||
|
|
|
@ -35,6 +35,12 @@ function getInviteCode() {
|
||||||
return code ?? '';
|
return code ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormInputs {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
invite: string
|
||||||
|
}
|
||||||
|
|
||||||
export function Form({ page, callback }: Props) {
|
export function Form({ page, callback }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
|
|
||||||
|
@ -43,7 +49,7 @@ export function Form({ page, callback }: Props) {
|
||||||
const [error, setGlobalError] = useState<string | undefined>(undefined);
|
const [error, setGlobalError] = useState<string | undefined>(undefined);
|
||||||
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
|
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
|
||||||
|
|
||||||
const { handleSubmit, register, errors, setError } = useForm({
|
const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
@ -51,11 +57,7 @@ export function Form({ page, callback }: Props) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(data: {
|
async function onSubmit(data: FormInputs) {
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
invite: string;
|
|
||||||
}) {
|
|
||||||
setGlobalError(undefined);
|
setGlobalError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
@ -143,7 +145,8 @@ export function Form({ page, callback }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.form}>
|
<div className={styles.form}>
|
||||||
<img src={wideSVG} />
|
<img src={wideSVG} />
|
||||||
<form onSubmit={handleSubmit(onSubmit) as any}>
|
{/* Preact / React typing incompatabilities */}
|
||||||
|
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
|
||||||
{page !== "reset" && (
|
{page !== "reset" && (
|
||||||
<FormField
|
<FormField
|
||||||
type="email"
|
type="email"
|
||||||
|
|
|
@ -25,9 +25,9 @@ export function FormReset() {
|
||||||
<Form
|
<Form
|
||||||
page="reset"
|
page="reset"
|
||||||
callback={async data => {
|
callback={async data => {
|
||||||
await client.req("POST", "/auth/reset" as any, {
|
await client.req("POST", "/auth/reset", {
|
||||||
token,
|
token,
|
||||||
...(data as any)
|
...data
|
||||||
});
|
});
|
||||||
history.push("/login");
|
history.push("/login");
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -52,11 +52,11 @@ export function Account() {
|
||||||
<div className={styles.username}>@{user.username}</div>
|
<div className={styles.username}>@{user.username}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
{[
|
{([
|
||||||
["username", user.username, <At size={24} />],
|
["username", user.username, <At size={24} />],
|
||||||
["email", email, <Envelope size={24} />],
|
["email", email, <Envelope size={24} />],
|
||||||
["password", "*****", <Key size={24} />]
|
["password", "*****", <Key size={24} />]
|
||||||
].map(([field, value, icon]) => (
|
] as const).map(([field, value, icon]) => (
|
||||||
<div>
|
<div>
|
||||||
{icon}
|
{icon}
|
||||||
<div className={styles.detail}>
|
<div className={styles.detail}>
|
||||||
|
@ -70,7 +70,7 @@ export function Account() {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
openScreen({
|
||||||
id: "modify_account",
|
id: "modify_account",
|
||||||
field: field as any
|
field: field
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
contrast
|
contrast
|
||||||
|
|
|
@ -226,7 +226,7 @@ export function Component(props: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.overrides}>
|
<div className={styles.overrides}>
|
||||||
{[
|
{([
|
||||||
"accent",
|
"accent",
|
||||||
"background",
|
"background",
|
||||||
"foreground",
|
"foreground",
|
||||||
|
@ -240,7 +240,6 @@ export function Component(props: Props) {
|
||||||
"block",
|
"block",
|
||||||
"message-box",
|
"message-box",
|
||||||
"mention",
|
"mention",
|
||||||
"sidebar-active",
|
|
||||||
"scrollbar-thumb",
|
"scrollbar-thumb",
|
||||||
"scrollbar-track",
|
"scrollbar-track",
|
||||||
"status-online",
|
"status-online",
|
||||||
|
@ -252,15 +251,15 @@ export function Component(props: Props) {
|
||||||
"warning",
|
"warning",
|
||||||
"error",
|
"error",
|
||||||
"hover"
|
"hover"
|
||||||
].map(x => (
|
] as const).map(x => (
|
||||||
<div className={styles.entry} key={x}>
|
<div className={styles.entry} key={x}>
|
||||||
<span>{x}</span>
|
<span>{x}</span>
|
||||||
<div className={styles.override}>
|
<div className={styles.override}>
|
||||||
<div className={styles.picker}
|
<div className={styles.picker}
|
||||||
style={{ backgroundColor: (theme as any)[x as any] }}>
|
style={{ backgroundColor: theme[x] }}>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={(theme as any)[x as any]}
|
value={theme[x]}
|
||||||
onChange={v =>
|
onChange={v =>
|
||||||
setOverride({
|
setOverride({
|
||||||
[x]: v.currentTarget.value
|
[x]: v.currentTarget.value
|
||||||
|
@ -270,7 +269,7 @@ export function Component(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<InputBox
|
<InputBox
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
value={(theme as any)[x as any]}
|
value={theme[x]}
|
||||||
onChange={y =>
|
onChange={y =>
|
||||||
setOverride({
|
setOverride({
|
||||||
[x]: y.currentTarget.value
|
[x]: y.currentTarget.value
|
||||||
|
|
|
@ -77,11 +77,11 @@ export function Component({ options }: Props) {
|
||||||
|
|
||||||
// tell the server we just subscribed
|
// tell the server we just subscribed
|
||||||
const json = sub.toJSON();
|
const json = sub.toJSON();
|
||||||
if (json.keys) {
|
if (json.keys) {;
|
||||||
client.req("POST", "/push/subscribe", {
|
client.req("POST", "/push/subscribe", {
|
||||||
endpoint: sub.endpoint,
|
endpoint: sub.endpoint,
|
||||||
...json.keys
|
...(json.keys as { p256dh: string, auth: string })
|
||||||
} as any);
|
});
|
||||||
setPushEnabled(true);
|
setPushEnabled(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltCli
|
||||||
import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const { intl } = useContext(IntlContext) as any;
|
const { intl } = useContext(IntlContext);
|
||||||
const status = useContext(StatusContext);
|
const status = useContext(StatusContext);
|
||||||
|
|
||||||
const ctx = useForceUpdate();
|
const ctx = useForceUpdate();
|
||||||
|
@ -121,7 +121,7 @@ export function Profile() {
|
||||||
: "placeholder"
|
: "placeholder"
|
||||||
}`,
|
}`,
|
||||||
"",
|
"",
|
||||||
intl.dictionary
|
(intl as any).dictionary as Record<string, unknown>
|
||||||
)}
|
)}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|
|
@ -155,7 +155,7 @@ export function Sessions() {
|
||||||
]);
|
]);
|
||||||
await client.req(
|
await client.req(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
`/auth/sessions/${session.id}` as any
|
`/auth/sessions/${session.id}` as '/auth/sessions'
|
||||||
);
|
);
|
||||||
setSessions(
|
setSessions(
|
||||||
sessions?.filter(
|
sessions?.filter(
|
||||||
|
|
|
@ -3590,10 +3590,10 @@ reusify@^1.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||||
|
|
||||||
revolt.js@4.3.3-alpha.7:
|
revolt.js@4.3.3-alpha.8:
|
||||||
version "4.3.3-alpha.7"
|
version "4.3.3-alpha.8"
|
||||||
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.7.tgz#de6ecef444e8368aac3753761e2e10f516f50712"
|
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.8.tgz#2a191ffa9d4c304e328b5eb8d9dc1e13e1f99d9a"
|
||||||
integrity sha512-oi76A+EIxrD+tVRTU8s2LISFBpvMf0kpinw5rdukoc1VWpl0bCC6Kko26yC7lhVkWGLTZxHMOKaUkgbOgy0flA==
|
integrity sha512-A6sjZ7cmeQuqS9otzANv+Rg4CfvpsTMoDARBwQuez4O7NPRopdWNHylUPo20UutAPzW9xoqVbF8673VlTu5Jag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@insertish/mutable" "1.1.0"
|
"@insertish/mutable" "1.1.0"
|
||||||
axios "^0.19.2"
|
axios "^0.19.2"
|
||||||
|
|
Loading…
Reference in a new issue