mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
Run prettier on all files.
This commit is contained in:
@@ -1,186 +1,217 @@
|
||||
import { IntlProvider } from "preact-i18n";
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
import { connectState } from "../redux/connector";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import definition from "../../external/lang/en.json";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import calendar from "dayjs/plugin/calendar";
|
||||
import update from "dayjs/plugin/updateLocale";
|
||||
import format from "dayjs/plugin/localizedFormat";
|
||||
import update from "dayjs/plugin/updateLocale";
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
|
||||
import { IntlProvider } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { connectState } from "../redux/connector";
|
||||
|
||||
import definition from "../../external/lang/en.json";
|
||||
|
||||
dayjs.extend(calendar);
|
||||
dayjs.extend(format);
|
||||
dayjs.extend(update);
|
||||
|
||||
export enum Language {
|
||||
ENGLISH = "en",
|
||||
ENGLISH = "en",
|
||||
|
||||
ARABIC = "ar",
|
||||
AZERBAIJANI = "az",
|
||||
CZECH = "cs",
|
||||
GERMAN = "de",
|
||||
SPANISH = "es",
|
||||
FINNISH = "fi",
|
||||
FRENCH = "fr",
|
||||
HINDI = "hi",
|
||||
CROATIAN = "hr",
|
||||
HUNGARIAN = "hu",
|
||||
INDONESIAN = "id",
|
||||
LITHUANIAN = "lt",
|
||||
MACEDONIAN = "mk",
|
||||
DUTCH = "nl",
|
||||
POLISH = "pl",
|
||||
PORTUGUESE_BRAZIL = "pt_BR",
|
||||
ROMANIAN = "ro",
|
||||
RUSSIAN = "ru",
|
||||
SERBIAN = "sr",
|
||||
SWEDISH = "sv",
|
||||
TURKISH = "tr",
|
||||
UKRANIAN = "uk",
|
||||
CHINESE_SIMPLIFIED = "zh_Hans",
|
||||
ARABIC = "ar",
|
||||
AZERBAIJANI = "az",
|
||||
CZECH = "cs",
|
||||
GERMAN = "de",
|
||||
SPANISH = "es",
|
||||
FINNISH = "fi",
|
||||
FRENCH = "fr",
|
||||
HINDI = "hi",
|
||||
CROATIAN = "hr",
|
||||
HUNGARIAN = "hu",
|
||||
INDONESIAN = "id",
|
||||
LITHUANIAN = "lt",
|
||||
MACEDONIAN = "mk",
|
||||
DUTCH = "nl",
|
||||
POLISH = "pl",
|
||||
PORTUGUESE_BRAZIL = "pt_BR",
|
||||
ROMANIAN = "ro",
|
||||
RUSSIAN = "ru",
|
||||
SERBIAN = "sr",
|
||||
SWEDISH = "sv",
|
||||
TURKISH = "tr",
|
||||
UKRANIAN = "uk",
|
||||
CHINESE_SIMPLIFIED = "zh_Hans",
|
||||
|
||||
OWO = "owo",
|
||||
PIRATE = "pr",
|
||||
BOTTOM = "bottom",
|
||||
PIGLATIN = "piglatin",
|
||||
OWO = "owo",
|
||||
PIRATE = "pr",
|
||||
BOTTOM = "bottom",
|
||||
PIGLATIN = "piglatin",
|
||||
}
|
||||
|
||||
export interface LanguageEntry {
|
||||
display: string;
|
||||
emoji: string;
|
||||
i18n: string;
|
||||
dayjs?: string;
|
||||
rtl?: boolean;
|
||||
alt?: boolean;
|
||||
display: string;
|
||||
emoji: string;
|
||||
i18n: string;
|
||||
dayjs?: string;
|
||||
rtl?: boolean;
|
||||
alt?: boolean;
|
||||
}
|
||||
|
||||
export const Languages: { [key in Language]: LanguageEntry } = {
|
||||
en: {
|
||||
display: "English (Traditional)",
|
||||
emoji: "🇬🇧",
|
||||
i18n: "en",
|
||||
dayjs: "en-gb",
|
||||
},
|
||||
en: {
|
||||
display: "English (Traditional)",
|
||||
emoji: "🇬🇧",
|
||||
i18n: "en",
|
||||
dayjs: "en-gb",
|
||||
},
|
||||
|
||||
ar: { display: "عربي", emoji: "🇸🇦", i18n: "ar", rtl: true },
|
||||
az: { display: "Azərbaycan dili", emoji: "🇦🇿", i18n: "az" },
|
||||
cs: { display: "Čeština", emoji: "🇨🇿", i18n: "cs" },
|
||||
de: { display: "Deutsch", emoji: "🇩🇪", i18n: "de" },
|
||||
es: { display: "Español", emoji: "🇪🇸", i18n: "es" },
|
||||
fi: { display: "suomi", emoji: "🇫🇮", i18n: "fi" },
|
||||
fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
|
||||
hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
|
||||
hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
|
||||
hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" },
|
||||
id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
|
||||
lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
|
||||
mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
|
||||
nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
|
||||
pl: { display: "Polski", emoji: "🇵🇱", i18n: "pl" },
|
||||
pt_BR: {
|
||||
display: "Português (do Brasil)",
|
||||
emoji: "🇧🇷",
|
||||
i18n: "pt_BR",
|
||||
dayjs: "pt-br",
|
||||
},
|
||||
ro: { display: "Română", emoji: "🇷🇴", i18n: "ro" },
|
||||
ru: { display: "Русский", emoji: "🇷🇺", i18n: "ru" },
|
||||
sr: { display: "Српски", emoji: "🇷🇸", i18n: "sr" },
|
||||
sv: { display: "Svenska", emoji: "🇸🇪", i18n: "sv" },
|
||||
tr: { display: "Türkçe", emoji: "🇹🇷", i18n: "tr" },
|
||||
uk: { display: "Українська", emoji: "🇺🇦", i18n: "uk" },
|
||||
zh_Hans: {
|
||||
display: "中文 (简体)",
|
||||
emoji: "🇨🇳",
|
||||
i18n: "zh_Hans",
|
||||
dayjs: "zh",
|
||||
},
|
||||
ar: { display: "عربي", emoji: "🇸🇦", i18n: "ar", rtl: true },
|
||||
az: { display: "Azərbaycan dili", emoji: "🇦🇿", i18n: "az" },
|
||||
cs: { display: "Čeština", emoji: "🇨🇿", i18n: "cs" },
|
||||
de: { display: "Deutsch", emoji: "🇩🇪", i18n: "de" },
|
||||
es: { display: "Español", emoji: "🇪🇸", i18n: "es" },
|
||||
fi: { display: "suomi", emoji: "🇫🇮", i18n: "fi" },
|
||||
fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
|
||||
hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
|
||||
hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
|
||||
hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" },
|
||||
id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
|
||||
lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
|
||||
mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
|
||||
nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
|
||||
pl: { display: "Polski", emoji: "🇵🇱", i18n: "pl" },
|
||||
pt_BR: {
|
||||
display: "Português (do Brasil)",
|
||||
emoji: "🇧🇷",
|
||||
i18n: "pt_BR",
|
||||
dayjs: "pt-br",
|
||||
},
|
||||
ro: { display: "Română", emoji: "🇷🇴", i18n: "ro" },
|
||||
ru: { display: "Русский", emoji: "🇷🇺", i18n: "ru" },
|
||||
sr: { display: "Српски", emoji: "🇷🇸", i18n: "sr" },
|
||||
sv: { display: "Svenska", emoji: "🇸🇪", i18n: "sv" },
|
||||
tr: { display: "Türkçe", emoji: "🇹🇷", i18n: "tr" },
|
||||
uk: { display: "Українська", emoji: "🇺🇦", i18n: "uk" },
|
||||
zh_Hans: {
|
||||
display: "中文 (简体)",
|
||||
emoji: "🇨🇳",
|
||||
i18n: "zh_Hans",
|
||||
dayjs: "zh",
|
||||
},
|
||||
|
||||
owo: { display: "OwO", emoji: "🐱", i18n: "owo", dayjs: "en-gb", alt: true },
|
||||
pr: { display: "Pirate", emoji: "🏴☠️", i18n: "pr", dayjs: "en-gb", alt: true },
|
||||
bottom: { display: "Bottom", emoji: "🥺", i18n: "bottom", dayjs: "en-gb", alt: true },
|
||||
piglatin: {
|
||||
display: "Pig Latin",
|
||||
emoji: "🐖",
|
||||
i18n: "piglatin",
|
||||
dayjs: "en-gb",
|
||||
alt: true
|
||||
},
|
||||
owo: {
|
||||
display: "OwO",
|
||||
emoji: "🐱",
|
||||
i18n: "owo",
|
||||
dayjs: "en-gb",
|
||||
alt: true,
|
||||
},
|
||||
pr: {
|
||||
display: "Pirate",
|
||||
emoji: "🏴☠️",
|
||||
i18n: "pr",
|
||||
dayjs: "en-gb",
|
||||
alt: true,
|
||||
},
|
||||
bottom: {
|
||||
display: "Bottom",
|
||||
emoji: "🥺",
|
||||
i18n: "bottom",
|
||||
dayjs: "en-gb",
|
||||
alt: true,
|
||||
},
|
||||
piglatin: {
|
||||
display: "Pig Latin",
|
||||
emoji: "🐖",
|
||||
i18n: "piglatin",
|
||||
dayjs: "en-gb",
|
||||
alt: true,
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
locale: Language;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
locale: Language;
|
||||
}
|
||||
|
||||
function Locale({ children, locale }: Props) {
|
||||
// TODO: create and use LanguageDefinition type here
|
||||
const [defns, setDefinition] = useState<Record<string, unknown>>(definition);
|
||||
const lang = Languages[locale];
|
||||
// TODO: create and use LanguageDefinition type here
|
||||
const [defns, setDefinition] =
|
||||
useState<Record<string, unknown>>(definition);
|
||||
const lang = Languages[locale];
|
||||
|
||||
// TODO: clean this up and use the built in Intl API
|
||||
function transformLanguage(source: { [key: string]: any }) {
|
||||
const obj = defaultsDeep(source, definition);
|
||||
// TODO: clean this up and use the built in Intl API
|
||||
function transformLanguage(source: { [key: string]: any }) {
|
||||
const obj = defaultsDeep(source, definition);
|
||||
|
||||
const dayjs = obj.dayjs;
|
||||
const defaults = dayjs.defaults;
|
||||
const dayjs = obj.dayjs;
|
||||
const defaults = dayjs.defaults;
|
||||
|
||||
const twelvehour = defaults?.twelvehour === 'yes' || true;
|
||||
const separator: '/' | '-' | '.' = defaults?.date_separator ?? '/';
|
||||
const date: 'traditional' | 'simplified' | 'ISO8601' = defaults?.date_format ?? 'traditional';
|
||||
|
||||
const DATE_FORMATS = {
|
||||
traditional: `DD${separator}MM${separator}YYYY`,
|
||||
simplified: `MM${separator}DD${separator}YYYY`,
|
||||
ISO8601: 'YYYY-MM-DD'
|
||||
}
|
||||
const twelvehour = defaults?.twelvehour === "yes" || true;
|
||||
const separator: "/" | "-" | "." = defaults?.date_separator ?? "/";
|
||||
const date: "traditional" | "simplified" | "ISO8601" =
|
||||
defaults?.date_format ?? "traditional";
|
||||
|
||||
dayjs['sameElse'] = DATE_FORMATS[date];
|
||||
Object.keys(dayjs)
|
||||
.filter(k => k !== 'defaults')
|
||||
.forEach(k => dayjs[k] = dayjs[k].replace(/{{time}}/g, twelvehour ? 'LT' : 'HH:mm'));
|
||||
|
||||
return obj;
|
||||
}
|
||||
const DATE_FORMATS = {
|
||||
traditional: `DD${separator}MM${separator}YYYY`,
|
||||
simplified: `MM${separator}DD${separator}YYYY`,
|
||||
ISO8601: "YYYY-MM-DD",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (locale === "en") {
|
||||
const defn = transformLanguage(definition);
|
||||
setDefinition(defn);
|
||||
dayjs.locale("en");
|
||||
dayjs.updateLocale('en', { calendar: defn.dayjs });
|
||||
return;
|
||||
}
|
||||
dayjs["sameElse"] = DATE_FORMATS[date];
|
||||
Object.keys(dayjs)
|
||||
.filter((k) => k !== "defaults")
|
||||
.forEach(
|
||||
(k) =>
|
||||
(dayjs[k] = dayjs[k].replace(
|
||||
/{{time}}/g,
|
||||
twelvehour ? "LT" : "HH:mm",
|
||||
)),
|
||||
);
|
||||
|
||||
import(`../../external/lang/${lang.i18n}.json`).then(
|
||||
async (lang_file) => {
|
||||
const defn = transformLanguage(lang_file.default);
|
||||
const target = lang.dayjs ?? lang.i18n;
|
||||
const dayjs_locale = await import(`../../node_modules/dayjs/esm/locale/${target}.js`);
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (defn.dayjs) {
|
||||
dayjs.updateLocale(target, { calendar: defn.dayjs });
|
||||
}
|
||||
useEffect(() => {
|
||||
if (locale === "en") {
|
||||
const defn = transformLanguage(definition);
|
||||
setDefinition(defn);
|
||||
dayjs.locale("en");
|
||||
dayjs.updateLocale("en", { calendar: defn.dayjs });
|
||||
return;
|
||||
}
|
||||
|
||||
dayjs.locale(dayjs_locale.default);
|
||||
setDefinition(defn);
|
||||
}
|
||||
);
|
||||
}, [locale, lang]);
|
||||
import(`../../external/lang/${lang.i18n}.json`).then(
|
||||
async (lang_file) => {
|
||||
const defn = transformLanguage(lang_file.default);
|
||||
const target = lang.dayjs ?? lang.i18n;
|
||||
const dayjs_locale = await import(
|
||||
`../../node_modules/dayjs/esm/locale/${target}.js`
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.direction = lang.rtl ? "rtl" : "";
|
||||
}, [lang.rtl]);
|
||||
if (defn.dayjs) {
|
||||
dayjs.updateLocale(target, { calendar: defn.dayjs });
|
||||
}
|
||||
|
||||
return <IntlProvider definition={defns}>{children}</IntlProvider>;
|
||||
dayjs.locale(dayjs_locale.default);
|
||||
setDefinition(defn);
|
||||
},
|
||||
);
|
||||
}, [locale, lang]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.direction = lang.rtl ? "rtl" : "";
|
||||
}, [lang.rtl]);
|
||||
|
||||
return <IntlProvider definition={defns}>{children}</IntlProvider>;
|
||||
}
|
||||
|
||||
export default connectState<Omit<Props, "locale">>(
|
||||
Locale,
|
||||
(state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
},
|
||||
true
|
||||
Locale,
|
||||
(state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -6,44 +6,56 @@
|
||||
// if it does cause problems though.
|
||||
//
|
||||
// This now also supports Audio stuff.
|
||||
|
||||
import { DEFAULT_SOUNDS, Settings, SoundOptions } from "../redux/reducers/settings";
|
||||
import { playSound, Sounds } from "../assets/sounds/Audio";
|
||||
import { connectState } from "../redux/connector";
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
import { Children } from "../types/Preact";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import { connectState } from "../redux/connector";
|
||||
import {
|
||||
DEFAULT_SOUNDS,
|
||||
Settings,
|
||||
SoundOptions,
|
||||
} from "../redux/reducers/settings";
|
||||
|
||||
import { playSound, Sounds } from "../assets/sounds/Audio";
|
||||
import { Children } from "../types/Preact";
|
||||
|
||||
export const SettingsContext = createContext<Settings>({});
|
||||
export const SoundContext = createContext<((sound: Sounds) => void)>(null!);
|
||||
export const SoundContext = createContext<(sound: Sounds) => void>(null!);
|
||||
|
||||
interface Props {
|
||||
children?: Children,
|
||||
settings: Settings
|
||||
children?: Children;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
function Settings({ settings, children }: Props) {
|
||||
const play = useMemo(() => {
|
||||
const enabled: SoundOptions = defaultsDeep(settings.notification?.sounds ?? {}, DEFAULT_SOUNDS);
|
||||
return (sound: Sounds) => {
|
||||
if (enabled[sound]) {
|
||||
playSound(sound);
|
||||
}
|
||||
};
|
||||
}, [ settings.notification ]);
|
||||
function SettingsProvider({ settings, children }: Props) {
|
||||
const play = useMemo(() => {
|
||||
const enabled: SoundOptions = defaultsDeep(
|
||||
settings.notification?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (sound: Sounds) => {
|
||||
if (enabled[sound]) {
|
||||
playSound(sound);
|
||||
}
|
||||
};
|
||||
}, [settings.notification]);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<SoundContext.Provider value={play}>
|
||||
{ children }
|
||||
</SoundContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
)
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<SoundContext.Provider value={play}>
|
||||
{children}
|
||||
</SoundContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState<Omit<Props, 'settings'>>(Settings, state => {
|
||||
return {
|
||||
settings: state.settings
|
||||
}
|
||||
});
|
||||
export default connectState<Omit<Props, "settings">>(
|
||||
SettingsProvider,
|
||||
(state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,335 +1,360 @@
|
||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import { connectState } from "../redux/connector";
|
||||
import { Children } from "../types/Preact";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { createContext } from "preact";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||
|
||||
import { connectState } from "../redux/connector";
|
||||
|
||||
import { Children } from "../types/Preact";
|
||||
|
||||
export type Variables =
|
||||
| "accent"
|
||||
| "background"
|
||||
| "foreground"
|
||||
| "block"
|
||||
| "message-box"
|
||||
| "mention"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "hover"
|
||||
| "scrollbar-thumb"
|
||||
| "scrollbar-track"
|
||||
| "primary-background"
|
||||
| "primary-header"
|
||||
| "secondary-background"
|
||||
| "secondary-foreground"
|
||||
| "secondary-header"
|
||||
| "tertiary-background"
|
||||
| "tertiary-foreground"
|
||||
| "status-online"
|
||||
| "status-away"
|
||||
| "status-busy"
|
||||
| "status-streaming"
|
||||
| "status-invisible"
|
||||
| "accent"
|
||||
| "background"
|
||||
| "foreground"
|
||||
| "block"
|
||||
| "message-box"
|
||||
| "mention"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "hover"
|
||||
| "scrollbar-thumb"
|
||||
| "scrollbar-track"
|
||||
| "primary-background"
|
||||
| "primary-header"
|
||||
| "secondary-background"
|
||||
| "secondary-foreground"
|
||||
| "secondary-header"
|
||||
| "tertiary-background"
|
||||
| "tertiary-foreground"
|
||||
| "status-online"
|
||||
| "status-away"
|
||||
| "status-busy"
|
||||
| "status-streaming"
|
||||
| "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"
|
||||
| "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 MonoscapeFonts = 'Fira Code' | 'Roboto Mono' | 'Source Code Pro' | 'Space Mono' | 'Ubuntu Mono';
|
||||
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 Theme = {
|
||||
[variable in Variables]: string;
|
||||
[variable in Variables]: string;
|
||||
} & {
|
||||
light?: boolean;
|
||||
font?: Fonts;
|
||||
css?: string;
|
||||
monoscapeFont?: MonoscapeFonts;
|
||||
light?: boolean;
|
||||
font?: Fonts;
|
||||
css?: string;
|
||||
monoscapeFont?: MonoscapeFonts;
|
||||
};
|
||||
|
||||
export interface ThemeOptions {
|
||||
preset?: string;
|
||||
ligatures?: boolean;
|
||||
custom?: Partial<Theme>;
|
||||
preset?: string;
|
||||
ligatures?: boolean;
|
||||
custom?: Partial<Theme>;
|
||||
}
|
||||
|
||||
// import aaa from "@fontsource/open-sans/300.css?raw";
|
||||
// console.info(aaa);
|
||||
|
||||
export const FONTS: Record<Fonts, { name: string, load: () => void }> = {
|
||||
"Open Sans": {
|
||||
name: "Open Sans",
|
||||
load: async () => {
|
||||
await import("@fontsource/open-sans/300.css");
|
||||
await import("@fontsource/open-sans/400.css");
|
||||
await import("@fontsource/open-sans/600.css");
|
||||
await import("@fontsource/open-sans/700.css");
|
||||
await import("@fontsource/open-sans/400-italic.css");
|
||||
}
|
||||
},
|
||||
Inter: {
|
||||
name: "Inter",
|
||||
load: async () => {
|
||||
await import("@fontsource/inter/300.css");
|
||||
await import("@fontsource/inter/400.css");
|
||||
await import("@fontsource/inter/600.css");
|
||||
await import("@fontsource/inter/700.css");
|
||||
}
|
||||
},
|
||||
"Atkinson Hyperlegible": {
|
||||
name: "Atkinson Hyperlegible",
|
||||
load: async () => {
|
||||
await import("@fontsource/atkinson-hyperlegible/400.css");
|
||||
await import("@fontsource/atkinson-hyperlegible/700.css");
|
||||
await import("@fontsource/atkinson-hyperlegible/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Roboto": {
|
||||
name: "Roboto",
|
||||
load: async () => {
|
||||
await import("@fontsource/roboto/400.css");
|
||||
await import("@fontsource/roboto/700.css");
|
||||
await import("@fontsource/roboto/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Noto Sans": {
|
||||
name: "Noto Sans",
|
||||
load: async () => {
|
||||
await import("@fontsource/noto-sans/400.css");
|
||||
await import("@fontsource/noto-sans/700.css");
|
||||
await import("@fontsource/noto-sans/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Bree Serif": {
|
||||
name: "Bree Serif",
|
||||
load: () => import("@fontsource/bree-serif/400.css")
|
||||
},
|
||||
"Lato": {
|
||||
name: "Lato",
|
||||
load: async () => {
|
||||
await import("@fontsource/lato/300.css");
|
||||
await import("@fontsource/lato/400.css");
|
||||
await import("@fontsource/lato/700.css");
|
||||
await import("@fontsource/lato/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Montserrat": {
|
||||
name: "Montserrat",
|
||||
load: async () => {
|
||||
await import("@fontsource/montserrat/300.css");
|
||||
await import("@fontsource/montserrat/400.css");
|
||||
await import("@fontsource/montserrat/600.css");
|
||||
await import("@fontsource/montserrat/700.css");
|
||||
await import("@fontsource/montserrat/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Poppins": {
|
||||
name: "Poppins",
|
||||
load: async () => {
|
||||
await import("@fontsource/poppins/300.css");
|
||||
await import("@fontsource/poppins/400.css");
|
||||
await import("@fontsource/poppins/600.css");
|
||||
await import("@fontsource/poppins/700.css");
|
||||
await import("@fontsource/poppins/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Raleway": {
|
||||
name: "Raleway",
|
||||
load: async () => {
|
||||
await import("@fontsource/raleway/300.css");
|
||||
await import("@fontsource/raleway/400.css");
|
||||
await import("@fontsource/raleway/600.css");
|
||||
await import("@fontsource/raleway/700.css");
|
||||
await import("@fontsource/raleway/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Ubuntu": {
|
||||
name: "Ubuntu",
|
||||
load: async () => {
|
||||
await import("@fontsource/ubuntu/300.css");
|
||||
await import("@fontsource/ubuntu/400.css");
|
||||
await import("@fontsource/ubuntu/500.css");
|
||||
await import("@fontsource/ubuntu/700.css");
|
||||
await import("@fontsource/ubuntu/400-italic.css");
|
||||
}
|
||||
},
|
||||
"Comic Neue": {
|
||||
name: "Comic Neue",
|
||||
load: async () => {
|
||||
await import("@fontsource/comic-neue/300.css");
|
||||
await import("@fontsource/comic-neue/400.css");
|
||||
await import("@fontsource/comic-neue/700.css");
|
||||
await import("@fontsource/comic-neue/400-italic.css");
|
||||
}
|
||||
}
|
||||
export const FONTS: Record<Fonts, { name: string; load: () => void }> = {
|
||||
"Open Sans": {
|
||||
name: "Open Sans",
|
||||
load: async () => {
|
||||
await import("@fontsource/open-sans/300.css");
|
||||
await import("@fontsource/open-sans/400.css");
|
||||
await import("@fontsource/open-sans/600.css");
|
||||
await import("@fontsource/open-sans/700.css");
|
||||
await import("@fontsource/open-sans/400-italic.css");
|
||||
},
|
||||
},
|
||||
Inter: {
|
||||
name: "Inter",
|
||||
load: async () => {
|
||||
await import("@fontsource/inter/300.css");
|
||||
await import("@fontsource/inter/400.css");
|
||||
await import("@fontsource/inter/600.css");
|
||||
await import("@fontsource/inter/700.css");
|
||||
},
|
||||
},
|
||||
"Atkinson Hyperlegible": {
|
||||
name: "Atkinson Hyperlegible",
|
||||
load: async () => {
|
||||
await import("@fontsource/atkinson-hyperlegible/400.css");
|
||||
await import("@fontsource/atkinson-hyperlegible/700.css");
|
||||
await import("@fontsource/atkinson-hyperlegible/400-italic.css");
|
||||
},
|
||||
},
|
||||
Roboto: {
|
||||
name: "Roboto",
|
||||
load: async () => {
|
||||
await import("@fontsource/roboto/400.css");
|
||||
await import("@fontsource/roboto/700.css");
|
||||
await import("@fontsource/roboto/400-italic.css");
|
||||
},
|
||||
},
|
||||
"Noto Sans": {
|
||||
name: "Noto Sans",
|
||||
load: async () => {
|
||||
await import("@fontsource/noto-sans/400.css");
|
||||
await import("@fontsource/noto-sans/700.css");
|
||||
await import("@fontsource/noto-sans/400-italic.css");
|
||||
},
|
||||
},
|
||||
"Bree Serif": {
|
||||
name: "Bree Serif",
|
||||
load: () => import("@fontsource/bree-serif/400.css"),
|
||||
},
|
||||
Lato: {
|
||||
name: "Lato",
|
||||
load: async () => {
|
||||
await import("@fontsource/lato/300.css");
|
||||
await import("@fontsource/lato/400.css");
|
||||
await import("@fontsource/lato/700.css");
|
||||
await import("@fontsource/lato/400-italic.css");
|
||||
},
|
||||
},
|
||||
Montserrat: {
|
||||
name: "Montserrat",
|
||||
load: async () => {
|
||||
await import("@fontsource/montserrat/300.css");
|
||||
await import("@fontsource/montserrat/400.css");
|
||||
await import("@fontsource/montserrat/600.css");
|
||||
await import("@fontsource/montserrat/700.css");
|
||||
await import("@fontsource/montserrat/400-italic.css");
|
||||
},
|
||||
},
|
||||
Poppins: {
|
||||
name: "Poppins",
|
||||
load: async () => {
|
||||
await import("@fontsource/poppins/300.css");
|
||||
await import("@fontsource/poppins/400.css");
|
||||
await import("@fontsource/poppins/600.css");
|
||||
await import("@fontsource/poppins/700.css");
|
||||
await import("@fontsource/poppins/400-italic.css");
|
||||
},
|
||||
},
|
||||
Raleway: {
|
||||
name: "Raleway",
|
||||
load: async () => {
|
||||
await import("@fontsource/raleway/300.css");
|
||||
await import("@fontsource/raleway/400.css");
|
||||
await import("@fontsource/raleway/600.css");
|
||||
await import("@fontsource/raleway/700.css");
|
||||
await import("@fontsource/raleway/400-italic.css");
|
||||
},
|
||||
},
|
||||
Ubuntu: {
|
||||
name: "Ubuntu",
|
||||
load: async () => {
|
||||
await import("@fontsource/ubuntu/300.css");
|
||||
await import("@fontsource/ubuntu/400.css");
|
||||
await import("@fontsource/ubuntu/500.css");
|
||||
await import("@fontsource/ubuntu/700.css");
|
||||
await import("@fontsource/ubuntu/400-italic.css");
|
||||
},
|
||||
},
|
||||
"Comic Neue": {
|
||||
name: "Comic Neue",
|
||||
load: async () => {
|
||||
await import("@fontsource/comic-neue/300.css");
|
||||
await import("@fontsource/comic-neue/400.css");
|
||||
await import("@fontsource/comic-neue/700.css");
|
||||
await import("@fontsource/comic-neue/400-italic.css");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MONOSCAPE_FONTS: Record<MonoscapeFonts, { name: string, load: () => void }> = {
|
||||
"Fira Code": {
|
||||
name: "Fira Code",
|
||||
load: () => import("@fontsource/fira-code/400.css")
|
||||
},
|
||||
"Roboto Mono": {
|
||||
name: "Roboto Mono",
|
||||
load: () => import("@fontsource/roboto-mono/400.css")
|
||||
},
|
||||
"Source Code Pro": {
|
||||
name: "Source Code Pro",
|
||||
load: () => import("@fontsource/source-code-pro/400.css")
|
||||
},
|
||||
"Space Mono": {
|
||||
name: "Space Mono",
|
||||
load: () => import("@fontsource/space-mono/400.css")
|
||||
},
|
||||
"Ubuntu Mono": {
|
||||
name: "Ubuntu Mono",
|
||||
load: () => import("@fontsource/ubuntu-mono/400.css")
|
||||
}
|
||||
export const MONOSCAPE_FONTS: Record<
|
||||
MonoscapeFonts,
|
||||
{ name: string; load: () => void }
|
||||
> = {
|
||||
"Fira Code": {
|
||||
name: "Fira Code",
|
||||
load: () => import("@fontsource/fira-code/400.css"),
|
||||
},
|
||||
"Roboto Mono": {
|
||||
name: "Roboto Mono",
|
||||
load: () => import("@fontsource/roboto-mono/400.css"),
|
||||
},
|
||||
"Source Code Pro": {
|
||||
name: "Source Code Pro",
|
||||
load: () => import("@fontsource/source-code-pro/400.css"),
|
||||
},
|
||||
"Space Mono": {
|
||||
name: "Space Mono",
|
||||
load: () => import("@fontsource/space-mono/400.css"),
|
||||
},
|
||||
"Ubuntu Mono": {
|
||||
name: "Ubuntu Mono",
|
||||
load: () => import("@fontsource/ubuntu-mono/400.css"),
|
||||
},
|
||||
};
|
||||
|
||||
export const FONT_KEYS = Object.keys(FONTS).sort();
|
||||
export const MONOSCAPE_FONT_KEYS = Object.keys(MONOSCAPE_FONTS).sort();
|
||||
|
||||
export const DEFAULT_FONT = 'Open Sans';
|
||||
export const DEFAULT_MONO_FONT = 'Fira Code';
|
||||
export const DEFAULT_FONT = "Open Sans";
|
||||
export const DEFAULT_MONO_FONT = "Fira Code";
|
||||
|
||||
// Generated from https://gitlab.insrt.uk/revolt/community/themes
|
||||
export const PRESETS: Record<string, Theme> = {
|
||||
light: {
|
||||
light: true,
|
||||
accent: "#FD6671",
|
||||
background: "#F6F6F6",
|
||||
foreground: "#101010",
|
||||
block: "#414141",
|
||||
"message-box": "#F1F1F1",
|
||||
mention: "rgba(251, 255, 0, 0.40)",
|
||||
success: "#65E572",
|
||||
warning: "#FAA352",
|
||||
error: "#F06464",
|
||||
hover: "rgba(0, 0, 0, 0.2)",
|
||||
"scrollbar-thumb": "#CA525A",
|
||||
"scrollbar-track": "transparent",
|
||||
"primary-background": "#FFFFFF",
|
||||
"primary-header": "#F1F1F1",
|
||||
"secondary-background": "#F1F1F1",
|
||||
"secondary-foreground": "#888888",
|
||||
"secondary-header": "#F1F1F1",
|
||||
"tertiary-background": "#4D4D4D",
|
||||
"tertiary-foreground": "#646464",
|
||||
"status-online": "#3ABF7E",
|
||||
"status-away": "#F39F00",
|
||||
"status-busy": "#F84848",
|
||||
"status-streaming": "#977EFF",
|
||||
"status-invisible": "#A5A5A5"
|
||||
},
|
||||
dark: {
|
||||
light: false,
|
||||
accent: "#FD6671",
|
||||
background: "#191919",
|
||||
foreground: "#F6F6F6",
|
||||
block: "#2D2D2D",
|
||||
"message-box": "#363636",
|
||||
mention: "rgba(251, 255, 0, 0.06)",
|
||||
success: "#65E572",
|
||||
warning: "#FAA352",
|
||||
error: "#F06464",
|
||||
hover: "rgba(0, 0, 0, 0.1)",
|
||||
"scrollbar-thumb": "#CA525A",
|
||||
"scrollbar-track": "transparent",
|
||||
"primary-background": "#242424",
|
||||
"primary-header": "#363636",
|
||||
"secondary-background": "#1E1E1E",
|
||||
"secondary-foreground": "#C8C8C8",
|
||||
"secondary-header": "#2D2D2D",
|
||||
"tertiary-background": "#4D4D4D",
|
||||
"tertiary-foreground": "#848484",
|
||||
"status-online": "#3ABF7E",
|
||||
"status-away": "#F39F00",
|
||||
"status-busy": "#F84848",
|
||||
"status-streaming": "#977EFF",
|
||||
"status-invisible": "#A5A5A5"
|
||||
},
|
||||
light: {
|
||||
light: true,
|
||||
accent: "#FD6671",
|
||||
background: "#F6F6F6",
|
||||
foreground: "#101010",
|
||||
block: "#414141",
|
||||
"message-box": "#F1F1F1",
|
||||
mention: "rgba(251, 255, 0, 0.40)",
|
||||
success: "#65E572",
|
||||
warning: "#FAA352",
|
||||
error: "#F06464",
|
||||
hover: "rgba(0, 0, 0, 0.2)",
|
||||
"scrollbar-thumb": "#CA525A",
|
||||
"scrollbar-track": "transparent",
|
||||
"primary-background": "#FFFFFF",
|
||||
"primary-header": "#F1F1F1",
|
||||
"secondary-background": "#F1F1F1",
|
||||
"secondary-foreground": "#888888",
|
||||
"secondary-header": "#F1F1F1",
|
||||
"tertiary-background": "#4D4D4D",
|
||||
"tertiary-foreground": "#646464",
|
||||
"status-online": "#3ABF7E",
|
||||
"status-away": "#F39F00",
|
||||
"status-busy": "#F84848",
|
||||
"status-streaming": "#977EFF",
|
||||
"status-invisible": "#A5A5A5",
|
||||
},
|
||||
dark: {
|
||||
light: false,
|
||||
accent: "#FD6671",
|
||||
background: "#191919",
|
||||
foreground: "#F6F6F6",
|
||||
block: "#2D2D2D",
|
||||
"message-box": "#363636",
|
||||
mention: "rgba(251, 255, 0, 0.06)",
|
||||
success: "#65E572",
|
||||
warning: "#FAA352",
|
||||
error: "#F06464",
|
||||
hover: "rgba(0, 0, 0, 0.1)",
|
||||
"scrollbar-thumb": "#CA525A",
|
||||
"scrollbar-track": "transparent",
|
||||
"primary-background": "#242424",
|
||||
"primary-header": "#363636",
|
||||
"secondary-background": "#1E1E1E",
|
||||
"secondary-foreground": "#C8C8C8",
|
||||
"secondary-header": "#2D2D2D",
|
||||
"tertiary-background": "#4D4D4D",
|
||||
"tertiary-foreground": "#848484",
|
||||
"status-online": "#3ABF7E",
|
||||
"status-away": "#F39F00",
|
||||
"status-busy": "#F84848",
|
||||
"status-streaming": "#977EFF",
|
||||
"status-invisible": "#A5A5A5",
|
||||
},
|
||||
};
|
||||
|
||||
const keys = Object.keys(PRESETS.dark);
|
||||
const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
|
||||
:root {
|
||||
${(props) =>
|
||||
(Object.keys(props.theme) as Variables[]).map((key) => {
|
||||
if (!keys.includes(key)) return;
|
||||
return `--${key}: ${props.theme[key]};`;
|
||||
})}
|
||||
(Object.keys(props.theme) as Variables[]).map((key) => {
|
||||
if (!keys.includes(key)) return;
|
||||
return `--${key}: ${props.theme[key]};`;
|
||||
})}
|
||||
}
|
||||
`;
|
||||
|
||||
// Load the default default them and apply extras later
|
||||
export const ThemeContext = createContext<Theme>(PRESETS['dark']);
|
||||
export const ThemeContext = createContext<Theme>(PRESETS["dark"]);
|
||||
|
||||
interface Props {
|
||||
children: Children;
|
||||
options?: ThemeOptions;
|
||||
children: Children;
|
||||
options?: ThemeOptions;
|
||||
}
|
||||
|
||||
function Theme({ children, options }: Props) {
|
||||
const theme: Theme = {
|
||||
...PRESETS["dark"],
|
||||
...PRESETS[options?.preset ?? ''],
|
||||
...options?.custom
|
||||
};
|
||||
const theme: Theme = {
|
||||
...PRESETS["dark"],
|
||||
...PRESETS[options?.preset ?? ""],
|
||||
...options?.custom,
|
||||
};
|
||||
|
||||
const root = document.documentElement.style;
|
||||
useEffect(() => {
|
||||
const font = theme.font ?? DEFAULT_FONT;
|
||||
root.setProperty('--font', `"${font}"`);
|
||||
FONTS[font].load();
|
||||
}, [ theme.font ]);
|
||||
const root = document.documentElement.style;
|
||||
useEffect(() => {
|
||||
const font = theme.font ?? DEFAULT_FONT;
|
||||
root.setProperty("--font", `"${font}"`);
|
||||
FONTS[font].load();
|
||||
}, [theme.font]);
|
||||
|
||||
useEffect(() => {
|
||||
const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT;
|
||||
root.setProperty('--monoscape-font', `"${font}"`);
|
||||
MONOSCAPE_FONTS[font].load();
|
||||
}, [ theme.monoscapeFont ]);
|
||||
useEffect(() => {
|
||||
const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT;
|
||||
root.setProperty("--monoscape-font", `"${font}"`);
|
||||
MONOSCAPE_FONTS[font].load();
|
||||
}, [theme.monoscapeFont]);
|
||||
|
||||
useEffect(() => {
|
||||
root.setProperty('--ligatures', options?.ligatures ? 'normal' : 'none');
|
||||
}, [ options?.ligatures ]);
|
||||
useEffect(() => {
|
||||
root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
|
||||
}, [options?.ligatures]);
|
||||
|
||||
useEffect(() => {
|
||||
const resize = () => root.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
resize();
|
||||
useEffect(() => {
|
||||
const resize = () =>
|
||||
root.setProperty("--app-height", `${window.innerHeight}px`);
|
||||
resize();
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
return () => window.removeEventListener('resize', resize);
|
||||
}, []);
|
||||
window.addEventListener("resize", resize);
|
||||
return () => window.removeEventListener("resize", resize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalTheme theme={theme} />
|
||||
{theme.css && (
|
||||
<style dangerouslySetInnerHTML={{ __html: theme.css }} />
|
||||
)}
|
||||
{ children }
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalTheme theme={theme} />
|
||||
{theme.css && (
|
||||
<style dangerouslySetInnerHTML={{ __html: theme.css }} />
|
||||
)}
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState<{ children: Children }>(Theme, state => {
|
||||
return {
|
||||
options: state.settings.theme
|
||||
};
|
||||
export default connectState<{ children: Children }>(Theme, (state) => {
|
||||
return {
|
||||
options: state.settings.theme,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import { createContext } from "preact";
|
||||
import { Children } from "../types/Preact";
|
||||
import { useForceUpdate } from "./revoltjs/hooks";
|
||||
import { AppContext } from "./revoltjs/RevoltClient";
|
||||
import type VoiceClient from "../lib/vortex/VoiceClient";
|
||||
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
||||
import type VoiceClient from "../lib/vortex/VoiceClient";
|
||||
|
||||
import { Children } from "../types/Preact";
|
||||
import { SoundContext } from "./Settings";
|
||||
import { AppContext } from "./revoltjs/RevoltClient";
|
||||
import { useForceUpdate } from "./revoltjs/hooks";
|
||||
|
||||
export enum VoiceStatus {
|
||||
LOADING = 0,
|
||||
UNAVAILABLE,
|
||||
ERRORED,
|
||||
READY = 3,
|
||||
CONNECTING = 4,
|
||||
AUTHENTICATING,
|
||||
RTC_CONNECTING,
|
||||
CONNECTED
|
||||
// RECONNECTING
|
||||
LOADING = 0,
|
||||
UNAVAILABLE,
|
||||
ERRORED,
|
||||
READY = 3,
|
||||
CONNECTING = 4,
|
||||
AUTHENTICATING,
|
||||
RTC_CONNECTING,
|
||||
CONNECTED,
|
||||
// RECONNECTING
|
||||
}
|
||||
|
||||
export interface VoiceOperations {
|
||||
connect: (channelId: string) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
isProducing: (type: ProduceType) => boolean;
|
||||
startProducing: (type: ProduceType) => Promise<void>;
|
||||
stopProducing: (type: ProduceType) => Promise<void> | undefined;
|
||||
connect: (channelId: string) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
isProducing: (type: ProduceType) => boolean;
|
||||
startProducing: (type: ProduceType) => Promise<void>;
|
||||
stopProducing: (type: ProduceType) => Promise<void> | undefined;
|
||||
}
|
||||
|
||||
export interface VoiceState {
|
||||
roomId?: string;
|
||||
status: VoiceStatus;
|
||||
participants?: Readonly<Map<string, VoiceUser>>;
|
||||
roomId?: string;
|
||||
status: VoiceStatus;
|
||||
participants?: Readonly<Map<string, VoiceUser>>;
|
||||
}
|
||||
|
||||
// They should be present from first render. - insert's words
|
||||
@@ -38,166 +40,168 @@ export const VoiceContext = createContext<VoiceState>(null!);
|
||||
export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
|
||||
|
||||
type Props = {
|
||||
children: Children;
|
||||
children: Children;
|
||||
};
|
||||
|
||||
export default function Voice({ children }: Props) {
|
||||
const revoltClient = useContext(AppContext);
|
||||
const [client, setClient] = useState<VoiceClient | undefined>(undefined);
|
||||
const [state, setState] = useState<VoiceState>({
|
||||
status: VoiceStatus.LOADING,
|
||||
participants: new Map()
|
||||
});
|
||||
const revoltClient = useContext(AppContext);
|
||||
const [client, setClient] = useState<VoiceClient | undefined>(undefined);
|
||||
const [state, setState] = useState<VoiceState>({
|
||||
status: VoiceStatus.LOADING,
|
||||
participants: new Map(),
|
||||
});
|
||||
|
||||
function setStatus(status: VoiceStatus, roomId?: string) {
|
||||
setState({
|
||||
status,
|
||||
roomId: roomId ?? client?.roomId,
|
||||
participants: client?.participants ?? new Map(),
|
||||
});
|
||||
}
|
||||
function setStatus(status: VoiceStatus, roomId?: string) {
|
||||
setState({
|
||||
status,
|
||||
roomId: roomId ?? client?.roomId,
|
||||
participants: client?.participants ?? new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
import('../lib/vortex/VoiceClient')
|
||||
.then(({ default: VoiceClient }) => {
|
||||
const client = new VoiceClient();
|
||||
setClient(client);
|
||||
useEffect(() => {
|
||||
import("../lib/vortex/VoiceClient")
|
||||
.then(({ default: VoiceClient }) => {
|
||||
const client = new VoiceClient();
|
||||
setClient(client);
|
||||
|
||||
if (!client?.supported()) {
|
||||
setStatus(VoiceStatus.UNAVAILABLE);
|
||||
} else {
|
||||
setStatus(VoiceStatus.READY);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load voice library!', err);
|
||||
setStatus(VoiceStatus.UNAVAILABLE);
|
||||
})
|
||||
}, []);
|
||||
if (!client?.supported()) {
|
||||
setStatus(VoiceStatus.UNAVAILABLE);
|
||||
} else {
|
||||
setStatus(VoiceStatus.READY);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load voice library!", err);
|
||||
setStatus(VoiceStatus.UNAVAILABLE);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isConnecting = useRef(false);
|
||||
const operations: VoiceOperations = useMemo(() => {
|
||||
return {
|
||||
connect: async channelId => {
|
||||
if (!client?.supported())
|
||||
throw new Error("RTC is unavailable");
|
||||
|
||||
isConnecting.current = true;
|
||||
setStatus(VoiceStatus.CONNECTING, channelId);
|
||||
const isConnecting = useRef(false);
|
||||
const operations: VoiceOperations = useMemo(() => {
|
||||
return {
|
||||
connect: async (channelId) => {
|
||||
if (!client?.supported()) throw new Error("RTC is unavailable");
|
||||
|
||||
try {
|
||||
const call = await revoltClient.channels.joinCall(
|
||||
channelId
|
||||
);
|
||||
isConnecting.current = true;
|
||||
setStatus(VoiceStatus.CONNECTING, channelId);
|
||||
|
||||
if (!isConnecting.current) {
|
||||
setStatus(VoiceStatus.READY);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const call = await revoltClient.channels.joinCall(
|
||||
channelId,
|
||||
);
|
||||
|
||||
// ! FIXME: use configuration to check if voso is enabled
|
||||
// await client.connect("wss://voso.revolt.chat/ws");
|
||||
await client.connect("wss://voso.revolt.chat/ws", channelId);
|
||||
if (!isConnecting.current) {
|
||||
setStatus(VoiceStatus.READY);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(VoiceStatus.AUTHENTICATING);
|
||||
// ! FIXME: use configuration to check if voso is enabled
|
||||
// await client.connect("wss://voso.revolt.chat/ws");
|
||||
await client.connect(
|
||||
"wss://voso.revolt.chat/ws",
|
||||
channelId,
|
||||
);
|
||||
|
||||
await client.authenticate(call.token);
|
||||
setStatus(VoiceStatus.RTC_CONNECTING);
|
||||
setStatus(VoiceStatus.AUTHENTICATING);
|
||||
|
||||
await client.initializeTransports();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(VoiceStatus.READY);
|
||||
return;
|
||||
}
|
||||
await client.authenticate(call.token);
|
||||
setStatus(VoiceStatus.RTC_CONNECTING);
|
||||
|
||||
setStatus(VoiceStatus.CONNECTED);
|
||||
isConnecting.current = false;
|
||||
},
|
||||
disconnect: () => {
|
||||
if (!client?.supported())
|
||||
throw new Error("RTC is unavailable");
|
||||
|
||||
// if (status <= VoiceStatus.READY) return;
|
||||
// this will not update in this context
|
||||
await client.initializeTransports();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(VoiceStatus.READY);
|
||||
return;
|
||||
}
|
||||
|
||||
isConnecting.current = false;
|
||||
client.disconnect();
|
||||
setStatus(VoiceStatus.READY);
|
||||
},
|
||||
isProducing: (type: ProduceType) => {
|
||||
switch (type) {
|
||||
case "audio":
|
||||
return client?.audioProducer !== undefined;
|
||||
}
|
||||
},
|
||||
startProducing: async (type: ProduceType) => {
|
||||
switch (type) {
|
||||
case "audio": {
|
||||
if (client?.audioProducer !== undefined) return console.log('No audio producer.'); // ! FIXME: let the user know
|
||||
if (navigator.mediaDevices === undefined) return console.log('No media devices.'); // ! FIXME: let the user know
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia(
|
||||
{
|
||||
audio: true
|
||||
}
|
||||
);
|
||||
setStatus(VoiceStatus.CONNECTED);
|
||||
isConnecting.current = false;
|
||||
},
|
||||
disconnect: () => {
|
||||
if (!client?.supported()) throw new Error("RTC is unavailable");
|
||||
|
||||
await client?.startProduce(
|
||||
mediaStream.getAudioTracks()[0],
|
||||
"audio"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
stopProducing: (type: ProduceType) => {
|
||||
return client?.stopProduce(type);
|
||||
}
|
||||
}
|
||||
}, [ client ]);
|
||||
// if (status <= VoiceStatus.READY) return;
|
||||
// this will not update in this context
|
||||
|
||||
const { forceUpdate } = useForceUpdate();
|
||||
const playSound = useContext(SoundContext);
|
||||
isConnecting.current = false;
|
||||
client.disconnect();
|
||||
setStatus(VoiceStatus.READY);
|
||||
},
|
||||
isProducing: (type: ProduceType) => {
|
||||
switch (type) {
|
||||
case "audio":
|
||||
return client?.audioProducer !== undefined;
|
||||
}
|
||||
},
|
||||
startProducing: async (type: ProduceType) => {
|
||||
switch (type) {
|
||||
case "audio": {
|
||||
if (client?.audioProducer !== undefined)
|
||||
return console.log("No audio producer."); // ! FIXME: let the user know
|
||||
if (navigator.mediaDevices === undefined)
|
||||
return console.log("No media devices."); // ! FIXME: let the user know
|
||||
const mediaStream =
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!client?.supported()) return;
|
||||
await client?.startProduce(
|
||||
mediaStream.getAudioTracks()[0],
|
||||
"audio",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
stopProducing: (type: ProduceType) => {
|
||||
return client?.stopProduce(type);
|
||||
},
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
// ! FIXME: message for fatal:
|
||||
// ! get rid of these force updates
|
||||
// ! handle it through state or smth
|
||||
const { forceUpdate } = useForceUpdate();
|
||||
const playSound = useContext(SoundContext);
|
||||
|
||||
client.on("startProduce", forceUpdate);
|
||||
client.on("stopProduce", forceUpdate);
|
||||
useEffect(() => {
|
||||
if (!client?.supported()) return;
|
||||
|
||||
client.on("userJoined", () => {
|
||||
playSound('call_join');
|
||||
forceUpdate();
|
||||
});
|
||||
client.on("userLeft", () => {
|
||||
playSound('call_leave');
|
||||
forceUpdate();
|
||||
});
|
||||
client.on("userStartProduce", forceUpdate);
|
||||
client.on("userStopProduce", forceUpdate);
|
||||
client.on("close", forceUpdate);
|
||||
// ! FIXME: message for fatal:
|
||||
// ! get rid of these force updates
|
||||
// ! handle it through state or smth
|
||||
|
||||
return () => {
|
||||
client.removeListener("startProduce", forceUpdate);
|
||||
client.removeListener("stopProduce", forceUpdate);
|
||||
client.on("startProduce", forceUpdate);
|
||||
client.on("stopProduce", forceUpdate);
|
||||
|
||||
client.removeListener("userJoined", forceUpdate);
|
||||
client.removeListener("userLeft", forceUpdate);
|
||||
client.removeListener("userStartProduce", forceUpdate);
|
||||
client.removeListener("userStopProduce", forceUpdate);
|
||||
client.removeListener("close", forceUpdate);
|
||||
};
|
||||
}, [ client, state ]);
|
||||
client.on("userJoined", () => {
|
||||
playSound("call_join");
|
||||
forceUpdate();
|
||||
});
|
||||
client.on("userLeft", () => {
|
||||
playSound("call_leave");
|
||||
forceUpdate();
|
||||
});
|
||||
client.on("userStartProduce", forceUpdate);
|
||||
client.on("userStopProduce", forceUpdate);
|
||||
client.on("close", forceUpdate);
|
||||
|
||||
return (
|
||||
<VoiceContext.Provider value={state}>
|
||||
<VoiceOperationsContext.Provider value={operations}>
|
||||
{ children }
|
||||
</VoiceOperationsContext.Provider>
|
||||
</VoiceContext.Provider>
|
||||
);
|
||||
return () => {
|
||||
client.removeListener("startProduce", forceUpdate);
|
||||
client.removeListener("stopProduce", forceUpdate);
|
||||
|
||||
client.removeListener("userJoined", forceUpdate);
|
||||
client.removeListener("userLeft", forceUpdate);
|
||||
client.removeListener("userStartProduce", forceUpdate);
|
||||
client.removeListener("userStopProduce", forceUpdate);
|
||||
client.removeListener("close", forceUpdate);
|
||||
};
|
||||
}, [client, state]);
|
||||
|
||||
return (
|
||||
<VoiceContext.Provider value={state}>
|
||||
<VoiceOperationsContext.Provider value={operations}>
|
||||
{children}
|
||||
</VoiceOperationsContext.Provider>
|
||||
</VoiceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import State from "../redux/State";
|
||||
import { Children } from "../types/Preact";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
import Intermediate from './intermediate/Intermediate';
|
||||
import Client from './revoltjs/RevoltClient';
|
||||
import Settings from "./Settings";
|
||||
import State from "../redux/State";
|
||||
|
||||
import { Children } from "../types/Preact";
|
||||
import Locale from "./Locale";
|
||||
import Voice from "./Voice";
|
||||
import Settings from "./Settings";
|
||||
import Theme from "./Theme";
|
||||
import Voice from "./Voice";
|
||||
import Intermediate from "./intermediate/Intermediate";
|
||||
import Client from "./revoltjs/RevoltClient";
|
||||
|
||||
export default function Context({ children }: { children: Children }) {
|
||||
return (
|
||||
<Router>
|
||||
<State>
|
||||
<Theme>
|
||||
<Settings>
|
||||
<Locale>
|
||||
<Intermediate>
|
||||
<Client>
|
||||
<Voice>
|
||||
{children}
|
||||
</Voice>
|
||||
</Client>
|
||||
</Intermediate>
|
||||
</Locale>
|
||||
</Settings>
|
||||
</Theme>
|
||||
</State>
|
||||
</Router>
|
||||
);
|
||||
return (
|
||||
<Router>
|
||||
<State>
|
||||
<Theme>
|
||||
<Settings>
|
||||
<Locale>
|
||||
<Intermediate>
|
||||
<Client>
|
||||
<Voice>{children}</Voice>
|
||||
</Client>
|
||||
</Intermediate>
|
||||
</Locale>
|
||||
</Settings>
|
||||
</Theme>
|
||||
</State>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,141 +1,179 @@
|
||||
import { Attachment, Channels, EmbedImage, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||
import { Action } from "../../components/ui/Modal";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { createContext } from "preact";
|
||||
import { Prompt } from "react-router";
|
||||
import Modals from './Modals';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
Attachment,
|
||||
Channels,
|
||||
EmbedImage,
|
||||
Servers,
|
||||
Users,
|
||||
} from "revolt.js/dist/api/objects";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||
|
||||
import { Action } from "../../components/ui/Modal";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import Modals from "./Modals";
|
||||
|
||||
export type Screen =
|
||||
| { id: "none" }
|
||||
| { id: "none" }
|
||||
|
||||
// Modals
|
||||
| { id: "signed_out" }
|
||||
| { id: "error"; error: string }
|
||||
| { id: "clipboard"; text: string }
|
||||
| { id: "_prompt"; question: Children; content?: Children; actions: Action[] }
|
||||
| ({ id: "special_prompt" } & (
|
||||
{ type: "leave_group", target: Channels.GroupChannel } |
|
||||
{ type: "close_dm", target: Channels.DirectMessageChannel } |
|
||||
{ type: "leave_server", target: Servers.Server } |
|
||||
{ type: "delete_server", target: Servers.Server } |
|
||||
{ type: "delete_channel", target: Channels.TextChannel } |
|
||||
{ type: "delete_message", target: Channels.Message } |
|
||||
{ type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
|
||||
{ type: "kick_member", target: Servers.Server, user: string } |
|
||||
{ type: "ban_member", target: Servers.Server, user: string } |
|
||||
{ type: "unfriend_user", target: Users.User } |
|
||||
{ type: "block_user", target: Users.User } |
|
||||
{ type: "create_channel", target: Servers.Server }
|
||||
)) |
|
||||
({ id: "special_input" } & (
|
||||
{ type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
|
||||
{ type: "create_role", server: string, callback: (id: string) => void }
|
||||
))
|
||||
| {
|
||||
id: "_input";
|
||||
question: Children;
|
||||
field: Children;
|
||||
defaultValue?: string;
|
||||
callback: (value: string) => Promise<void>;
|
||||
}
|
||||
| {
|
||||
id: "onboarding";
|
||||
callback: (
|
||||
username: string,
|
||||
loginAfterSuccess?: true
|
||||
) => Promise<void>;
|
||||
}
|
||||
// Modals
|
||||
| { id: "signed_out" }
|
||||
| { id: "error"; error: string }
|
||||
| { id: "clipboard"; text: string }
|
||||
| {
|
||||
id: "_prompt";
|
||||
question: Children;
|
||||
content?: Children;
|
||||
actions: Action[];
|
||||
}
|
||||
| ({ id: "special_prompt" } & (
|
||||
| { type: "leave_group"; target: Channels.GroupChannel }
|
||||
| { type: "close_dm"; target: Channels.DirectMessageChannel }
|
||||
| { type: "leave_server"; target: Servers.Server }
|
||||
| { type: "delete_server"; target: Servers.Server }
|
||||
| { type: "delete_channel"; target: Channels.TextChannel }
|
||||
| { type: "delete_message"; target: Channels.Message }
|
||||
| {
|
||||
type: "create_invite";
|
||||
target: Channels.TextChannel | Channels.GroupChannel;
|
||||
}
|
||||
| { type: "kick_member"; target: Servers.Server; user: string }
|
||||
| { type: "ban_member"; target: Servers.Server; user: string }
|
||||
| { type: "unfriend_user"; target: Users.User }
|
||||
| { type: "block_user"; target: Users.User }
|
||||
| { type: "create_channel"; target: Servers.Server }
|
||||
))
|
||||
| ({ id: "special_input" } & (
|
||||
| {
|
||||
type:
|
||||
| "create_group"
|
||||
| "create_server"
|
||||
| "set_custom_status"
|
||||
| "add_friend";
|
||||
}
|
||||
| {
|
||||
type: "create_role";
|
||||
server: string;
|
||||
callback: (id: string) => void;
|
||||
}
|
||||
))
|
||||
| {
|
||||
id: "_input";
|
||||
question: Children;
|
||||
field: Children;
|
||||
defaultValue?: string;
|
||||
callback: (value: string) => Promise<void>;
|
||||
}
|
||||
| {
|
||||
id: "onboarding";
|
||||
callback: (
|
||||
username: string,
|
||||
loginAfterSuccess?: true,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
// Pop-overs
|
||||
| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage; }
|
||||
| { id: "modify_account"; field: "username" | "email" | "password" }
|
||||
| { id: "profile"; user_id: string }
|
||||
| { id: "channel_info"; channel_id: string }
|
||||
| { id: "pending_requests"; users: string[] }
|
||||
| {
|
||||
id: "user_picker";
|
||||
omit?: string[];
|
||||
callback: (users: string[]) => Promise<void>;
|
||||
};
|
||||
// Pop-overs
|
||||
| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage }
|
||||
| { id: "modify_account"; field: "username" | "email" | "password" }
|
||||
| { id: "profile"; user_id: string }
|
||||
| { id: "channel_info"; channel_id: string }
|
||||
| { id: "pending_requests"; users: string[] }
|
||||
| {
|
||||
id: "user_picker";
|
||||
omit?: string[];
|
||||
callback: (users: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IntermediateContext = createContext({
|
||||
screen: { id: "none" } as Screen,
|
||||
focusTaken: false
|
||||
screen: { id: "none" } as Screen,
|
||||
focusTaken: false,
|
||||
});
|
||||
|
||||
export const IntermediateActionsContext = createContext({
|
||||
openScreen: (screen: Screen) => {},
|
||||
writeClipboard: (text: string) => {}
|
||||
openScreen: (screen: Screen) => {},
|
||||
writeClipboard: (text: string) => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
children: Children;
|
||||
children: Children;
|
||||
}
|
||||
|
||||
export default function Intermediate(props: Props) {
|
||||
const [screen, openScreen] = useState<Screen>({ id: "none" });
|
||||
const history = useHistory();
|
||||
const [screen, openScreen] = useState<Screen>({ id: "none" });
|
||||
const history = useHistory();
|
||||
|
||||
const value = {
|
||||
screen,
|
||||
focusTaken: screen.id !== 'none'
|
||||
};
|
||||
const value = {
|
||||
screen,
|
||||
focusTaken: screen.id !== "none",
|
||||
};
|
||||
|
||||
const actions = useMemo(() => {
|
||||
return {
|
||||
openScreen: (screen: Screen) => openScreen(screen),
|
||||
writeClipboard: (text: string) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
actions.openScreen({ id: "clipboard", text });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
const actions = useMemo(() => {
|
||||
return {
|
||||
openScreen: (screen: Screen) => openScreen(screen),
|
||||
writeClipboard: (text: string) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
actions.openScreen({ id: "clipboard", text });
|
||||
}
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const openProfile = (user_id: string) => openScreen({ id: "profile", user_id });
|
||||
const navigate = (path: string) => history.push(path);
|
||||
useEffect(() => {
|
||||
const openProfile = (user_id: string) =>
|
||||
openScreen({ id: "profile", user_id });
|
||||
const navigate = (path: string) => history.push(path);
|
||||
|
||||
const subs = [
|
||||
internalSubscribe("Intermediate", "openProfile", openProfile),
|
||||
internalSubscribe("Intermediate", "navigate", navigate)
|
||||
]
|
||||
const subs = [
|
||||
internalSubscribe("Intermediate", "openProfile", openProfile),
|
||||
internalSubscribe("Intermediate", "navigate", navigate),
|
||||
];
|
||||
|
||||
return () => subs.map(unsub => unsub());
|
||||
}, []);
|
||||
return () => subs.map((unsub) => unsub());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IntermediateContext.Provider value={value}>
|
||||
<IntermediateActionsContext.Provider value={actions}>
|
||||
{ screen.id !== 'onboarding' && props.children }
|
||||
<Modals
|
||||
{...value}
|
||||
{...actions}
|
||||
key={
|
||||
screen.id
|
||||
} /** By specifying a key, we reset state whenever switching screen. */
|
||||
/>
|
||||
<Prompt
|
||||
when={[ 'modify_account', 'special_prompt', 'special_input', 'image_viewer', 'profile', 'channel_info', 'pending_requests', 'user_picker' ].includes(screen.id)}
|
||||
message={(_, action) => {
|
||||
if (action === 'POP') {
|
||||
openScreen({ id: 'none' });
|
||||
setTimeout(() => history.push(history.location), 0);
|
||||
return (
|
||||
<IntermediateContext.Provider value={value}>
|
||||
<IntermediateActionsContext.Provider value={actions}>
|
||||
{screen.id !== "onboarding" && props.children}
|
||||
<Modals
|
||||
{...value}
|
||||
{...actions}
|
||||
key={
|
||||
screen.id
|
||||
} /** By specifying a key, we reset state whenever switching screen. */
|
||||
/>
|
||||
<Prompt
|
||||
when={[
|
||||
"modify_account",
|
||||
"special_prompt",
|
||||
"special_input",
|
||||
"image_viewer",
|
||||
"profile",
|
||||
"channel_info",
|
||||
"pending_requests",
|
||||
"user_picker",
|
||||
].includes(screen.id)}
|
||||
message={(_, action) => {
|
||||
if (action === "POP") {
|
||||
openScreen({ id: "none" });
|
||||
setTimeout(() => history.push(history.location), 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</IntermediateActionsContext.Provider>
|
||||
</IntermediateContext.Provider>
|
||||
);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</IntermediateActionsContext.Provider>
|
||||
</IntermediateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useIntermediate = () => useContext(IntermediateActionsContext);
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { Screen } from "./Intermediate";
|
||||
|
||||
import { ClipboardModal } from "./modals/Clipboard";
|
||||
import { ErrorModal } from "./modals/Error";
|
||||
import { InputModal } from "./modals/Input";
|
||||
import { OnboardingModal } from "./modals/Onboarding";
|
||||
import { PromptModal } from "./modals/Prompt";
|
||||
import { SignedOutModal } from "./modals/SignedOut";
|
||||
import { ClipboardModal } from "./modals/Clipboard";
|
||||
import { OnboardingModal } from "./modals/Onboarding";
|
||||
|
||||
export interface Props {
|
||||
screen: Screen;
|
||||
openScreen: (id: any) => void;
|
||||
screen: Screen;
|
||||
openScreen: (id: any) => void;
|
||||
}
|
||||
|
||||
export default function Modals({ screen, openScreen }: Props) {
|
||||
const onClose = () => openScreen({ id: "none" });
|
||||
const onClose = () => openScreen({ id: "none" });
|
||||
|
||||
switch (screen.id) {
|
||||
case "_prompt":
|
||||
return <PromptModal onClose={onClose} {...screen} />;
|
||||
case "_input":
|
||||
return <InputModal onClose={onClose} {...screen} />;
|
||||
case "error":
|
||||
return <ErrorModal onClose={onClose} {...screen} />;
|
||||
case "signed_out":
|
||||
return <SignedOutModal onClose={onClose} {...screen} />;
|
||||
case "clipboard":
|
||||
return <ClipboardModal onClose={onClose} {...screen} />;
|
||||
case "onboarding":
|
||||
return <OnboardingModal onClose={onClose} {...screen} />;
|
||||
}
|
||||
switch (screen.id) {
|
||||
case "_prompt":
|
||||
return <PromptModal onClose={onClose} {...screen} />;
|
||||
case "_input":
|
||||
return <InputModal onClose={onClose} {...screen} />;
|
||||
case "error":
|
||||
return <ErrorModal onClose={onClose} {...screen} />;
|
||||
case "signed_out":
|
||||
return <SignedOutModal onClose={onClose} {...screen} />;
|
||||
case "clipboard":
|
||||
return <ClipboardModal onClose={onClose} {...screen} />;
|
||||
case "onboarding":
|
||||
return <OnboardingModal onClose={onClose} {...screen} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { UserPicker } from "./popovers/UserPicker";
|
||||
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||
import { SpecialInputModal } from "./modals/Input";
|
||||
import { SpecialPromptModal } from "./modals/Prompt";
|
||||
import { UserProfile } from "./popovers/UserProfile";
|
||||
import { ImageViewer } from "./popovers/ImageViewer";
|
||||
import { ChannelInfo } from "./popovers/ChannelInfo";
|
||||
import { PendingRequests } from "./popovers/PendingRequests";
|
||||
import { ImageViewer } from "./popovers/ImageViewer";
|
||||
import { ModifyAccountModal } from "./popovers/ModifyAccount";
|
||||
import { PendingRequests } from "./popovers/PendingRequests";
|
||||
import { UserPicker } from "./popovers/UserPicker";
|
||||
import { UserProfile } from "./popovers/UserProfile";
|
||||
|
||||
export default function Popovers() {
|
||||
const { screen } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const { screen } = useContext(IntermediateContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const onClose = () => openScreen({ id: "none" });
|
||||
const onClose = () => openScreen({ id: "none" });
|
||||
|
||||
switch (screen.id) {
|
||||
case "profile":
|
||||
return <UserProfile {...screen} onClose={onClose} />;
|
||||
case "user_picker":
|
||||
return <UserPicker {...screen} onClose={onClose} />;
|
||||
case "image_viewer":
|
||||
return <ImageViewer {...screen} onClose={onClose} />;
|
||||
case "channel_info":
|
||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
||||
case "pending_requests":
|
||||
return <PendingRequests {...screen} onClose={onClose} />;
|
||||
case "modify_account":
|
||||
return <ModifyAccountModal onClose={onClose} {...screen} />;
|
||||
case "special_prompt":
|
||||
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
||||
case "special_input":
|
||||
return <SpecialInputModal onClose={onClose} {...screen} />;
|
||||
}
|
||||
switch (screen.id) {
|
||||
case "profile":
|
||||
return <UserProfile {...screen} onClose={onClose} />;
|
||||
case "user_picker":
|
||||
return <UserPicker {...screen} onClose={onClose} />;
|
||||
case "image_viewer":
|
||||
return <ImageViewer {...screen} onClose={onClose} />;
|
||||
case "channel_info":
|
||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
||||
case "pending_requests":
|
||||
return <PendingRequests {...screen} onClose={onClose} />;
|
||||
case "modify_account":
|
||||
return <ModifyAccountModal onClose={onClose} {...screen} />;
|
||||
case "special_prompt":
|
||||
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
||||
case "special_input":
|
||||
return <SpecialInputModal onClose={onClose} {...screen} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
text: string;
|
||||
onClose: () => void;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function ClipboardModal({ onClose, text }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id="app.special.modals.clipboard.unavailable" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.close" />
|
||||
}
|
||||
]}
|
||||
>
|
||||
{location.protocol !== "https:" && (
|
||||
<p>
|
||||
<Text id="app.special.modals.clipboard.https" />
|
||||
</p>
|
||||
)}
|
||||
<Text id="app.special.modals.clipboard.copy" />{" "}
|
||||
<code style={{ userSelect: "all" }}>{text}</code>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id="app.special.modals.clipboard.unavailable" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.close" />,
|
||||
},
|
||||
]}>
|
||||
{location.protocol !== "https:" && (
|
||||
<p>
|
||||
<Text id="app.special.modals.clipboard.https" />
|
||||
</p>
|
||||
)}
|
||||
<Text id="app.special.modals.clipboard.copy" />{" "}
|
||||
<code style={{ userSelect: "all" }}>{text}</code>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
error: string;
|
||||
onClose: () => void;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export function ErrorModal({ onClose, error }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={() => false}
|
||||
title={<Text id="app.special.modals.error" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />
|
||||
},
|
||||
{
|
||||
onClick: () => location.reload(),
|
||||
text: <Text id="app.special.modals.actions.reload" />
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Text id={`error.${error}`}>{error}</Text>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={() => false}
|
||||
title={<Text id="app.special.modals.error" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
},
|
||||
{
|
||||
onClick: () => location.reload(),
|
||||
text: <Text id="app.special.modals.actions.reload" />,
|
||||
},
|
||||
]}>
|
||||
<Text id={`error.${error}`}>{error}</Text>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,154 +1,176 @@
|
||||
import { ulid } from "ulid";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useHistory } from "react-router";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
import Overline from '../../../components/ui/Overline';
|
||||
import InputBox from '../../../components/ui/InputBox';
|
||||
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
question: Children;
|
||||
field?: Children;
|
||||
defaultValue?: string;
|
||||
callback: (value: string) => Promise<void>;
|
||||
onClose: () => void;
|
||||
question: Children;
|
||||
field?: Children;
|
||||
defaultValue?: string;
|
||||
callback: (value: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function InputModal({
|
||||
onClose,
|
||||
question,
|
||||
field,
|
||||
defaultValue,
|
||||
callback
|
||||
onClose,
|
||||
question,
|
||||
field,
|
||||
defaultValue,
|
||||
callback,
|
||||
}: Props) {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [value, setValue] = useState(defaultValue ?? "");
|
||||
const [error, setError] = useState<undefined | string>(undefined);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [value, setValue] = useState(defaultValue ?? "");
|
||||
const [error, setError] = useState<undefined | string>(undefined);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={question}
|
||||
disabled={processing}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
onClick: () => {
|
||||
setProcessing(true);
|
||||
callback(value)
|
||||
.then(onClose)
|
||||
.catch(err => {
|
||||
setError(takeError(err));
|
||||
setProcessing(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.cancel" />,
|
||||
onClick: onClose
|
||||
}
|
||||
]}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
{ field ? <Overline error={error} block>
|
||||
{field}
|
||||
</Overline> : (error && <Overline error={error} type="error" block />) }
|
||||
<InputBox
|
||||
value={value}
|
||||
onChange={e => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={question}
|
||||
disabled={processing}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
onClick: () => {
|
||||
setProcessing(true);
|
||||
callback(value)
|
||||
.then(onClose)
|
||||
.catch((err) => {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.cancel" />,
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
onClose={onClose}>
|
||||
<form>
|
||||
{field ? (
|
||||
<Overline error={error} block>
|
||||
{field}
|
||||
</Overline>
|
||||
) : (
|
||||
error && <Overline error={error} type="error" block />
|
||||
)}
|
||||
<InputBox
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type SpecialProps = { onClose: () => void } & (
|
||||
{ type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
|
||||
{ type: "create_role", server: string, callback: (id: string) => void }
|
||||
)
|
||||
| {
|
||||
type:
|
||||
| "create_group"
|
||||
| "create_server"
|
||||
| "set_custom_status"
|
||||
| "add_friend";
|
||||
}
|
||||
| { type: "create_role"; server: string; callback: (id: string) => void }
|
||||
);
|
||||
|
||||
export function SpecialInputModal(props: SpecialProps) {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const { onClose } = props;
|
||||
switch (props.type) {
|
||||
case "create_group": {
|
||||
return <InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.main.groups.create" />}
|
||||
field={<Text id="app.main.groups.name" />}
|
||||
callback={async name => {
|
||||
const group = await client.channels.createGroup(
|
||||
{
|
||||
name,
|
||||
nonce: ulid(),
|
||||
users: []
|
||||
}
|
||||
);
|
||||
const { onClose } = props;
|
||||
switch (props.type) {
|
||||
case "create_group": {
|
||||
return (
|
||||
<InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.main.groups.create" />}
|
||||
field={<Text id="app.main.groups.name" />}
|
||||
callback={async (name) => {
|
||||
const group = await client.channels.createGroup({
|
||||
name,
|
||||
nonce: ulid(),
|
||||
users: [],
|
||||
});
|
||||
|
||||
history.push(`/channel/${group._id}`);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
case "create_server": {
|
||||
return <InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.main.servers.create" />}
|
||||
field={<Text id="app.main.servers.name" />}
|
||||
callback={async name => {
|
||||
const server = await client.servers.createServer(
|
||||
{
|
||||
name,
|
||||
nonce: ulid()
|
||||
}
|
||||
);
|
||||
history.push(`/channel/${group._id}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "create_server": {
|
||||
return (
|
||||
<InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.main.servers.create" />}
|
||||
field={<Text id="app.main.servers.name" />}
|
||||
callback={async (name) => {
|
||||
const server = await client.servers.createServer({
|
||||
name,
|
||||
nonce: ulid(),
|
||||
});
|
||||
|
||||
history.push(`/server/${server._id}`);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
case "create_role": {
|
||||
return <InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.settings.permissions.create_role" />}
|
||||
field={<Text id="app.settings.permissions.role_name" />}
|
||||
callback={async name => {
|
||||
const role = await client.servers.createRole(props.server, name);
|
||||
props.callback(role.id);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
case "set_custom_status": {
|
||||
return <InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.context_menu.set_custom_status" />}
|
||||
field={<Text id="app.context_menu.custom_status" />}
|
||||
defaultValue={client.user?.status?.text}
|
||||
callback={text =>
|
||||
client.users.editUser({
|
||||
status: {
|
||||
...client.user?.status,
|
||||
text: text.trim().length > 0 ? text : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
/>;
|
||||
}
|
||||
case "add_friend": {
|
||||
return <InputModal
|
||||
onClose={onClose}
|
||||
question={"Add Friend"}
|
||||
callback={username =>
|
||||
client.users.addFriend(username)
|
||||
}
|
||||
/>;
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
history.push(`/server/${server._id}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "create_role": {
|
||||
return (
|
||||
<InputModal
|
||||
onClose={onClose}
|
||||
question={
|
||||
<Text id="app.settings.permissions.create_role" />
|
||||
}
|
||||
field={<Text id="app.settings.permissions.role_name" />}
|
||||
callback={async (name) => {
|
||||
const role = await client.servers.createRole(
|
||||
props.server,
|
||||
name,
|
||||
);
|
||||
props.callback(role.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "set_custom_status": {
|
||||
return (
|
||||
<InputModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.context_menu.set_custom_status" />}
|
||||
field={<Text id="app.context_menu.custom_status" />}
|
||||
defaultValue={client.user?.status?.text}
|
||||
callback={(text) =>
|
||||
client.users.editUser({
|
||||
status: {
|
||||
...client.user?.status,
|
||||
text: text.trim().length > 0 ? text : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "add_friend": {
|
||||
return (
|
||||
<InputModal
|
||||
onClose={onClose}
|
||||
question={"Add Friend"}
|
||||
callback={(username) => client.users.addFriend(username)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,78 @@
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
import styles from "./Onboarding.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import styles from "./Onboarding.module.scss";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
|
||||
import wideSVG from "../../../assets/wide.svg";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import FormField from "../../../pages/login/FormField";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
import wideSVG from '../../../assets/wide.svg';
|
||||
import FormField from "../../../pages/login/FormField";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
||||
onClose: () => void;
|
||||
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
||||
}
|
||||
|
||||
interface FormInputs {
|
||||
username: string
|
||||
username: string;
|
||||
}
|
||||
|
||||
export function OnboardingModal({ onClose, callback }: Props) {
|
||||
const { handleSubmit, register } = useForm<FormInputs>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const { handleSubmit, register } = useForm<FormInputs>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
||||
setLoading(true);
|
||||
callback(username, true)
|
||||
.then(onClose)
|
||||
.catch((err: any) => {
|
||||
setError(takeError(err));
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
||||
setLoading(true);
|
||||
callback(username, true)
|
||||
.then(onClose)
|
||||
.catch((err: any) => {
|
||||
setError(takeError(err));
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.onboarding}>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
<Text id="app.special.modals.onboarding.welcome" />
|
||||
<img src={wideSVG} />
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
{loading ? (
|
||||
<Preloader type="spinner" />
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<Text id="app.special.modals.onboarding.pick" />
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
|
||||
<div>
|
||||
<FormField
|
||||
type="username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">
|
||||
<Text id="app.special.modals.actions.continue" />
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.onboarding}>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
<Text id="app.special.modals.onboarding.welcome" />
|
||||
<img src={wideSVG} />
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
{loading ? (
|
||||
<Preloader type="spinner" />
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<Text id="app.special.modals.onboarding.pick" />
|
||||
</p>
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
) as JSX.GenericEventHandler<HTMLFormElement>
|
||||
}>
|
||||
<div>
|
||||
<FormField
|
||||
type="username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">
|
||||
<Text id="app.special.modals.actions.continue" />
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,346 +1,473 @@
|
||||
import { ulid } from "ulid";
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from './Prompt.module.scss';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Radio from "../../../components/ui/Radio";
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { useIntermediate } from "../Intermediate";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { mapMessage, takeError } from "../../revoltjs/util";
|
||||
import Modal, { Action } from "../../../components/ui/Modal";
|
||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import styles from "./Prompt.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import Message from "../../../components/common/messaging/Message";
|
||||
|
||||
import { TextReact } from "../../../lib/i18n";
|
||||
|
||||
import Message from "../../../components/common/messaging/Message";
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import Modal, { Action } from "../../../components/ui/Modal";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
import Radio from "../../../components/ui/Radio";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { mapMessage, takeError } from "../../revoltjs/util";
|
||||
import { useIntermediate } from "../Intermediate";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
question: Children;
|
||||
content?: Children;
|
||||
disabled?: boolean;
|
||||
actions: Action[];
|
||||
error?: string;
|
||||
onClose: () => void;
|
||||
question: Children;
|
||||
content?: Children;
|
||||
disabled?: boolean;
|
||||
actions: Action[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function PromptModal({ onClose, question, content, actions, disabled, error }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={question}
|
||||
actions={actions}
|
||||
onClose={onClose}
|
||||
disabled={disabled}>
|
||||
{ error && <Overline error={error} type="error" /> }
|
||||
{ content }
|
||||
</Modal>
|
||||
);
|
||||
export function PromptModal({
|
||||
onClose,
|
||||
question,
|
||||
content,
|
||||
actions,
|
||||
disabled,
|
||||
error,
|
||||
}: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={question}
|
||||
actions={actions}
|
||||
onClose={onClose}
|
||||
disabled={disabled}>
|
||||
{error && <Overline error={error} type="error" />}
|
||||
{content}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type SpecialProps = { onClose: () => void } & (
|
||||
{ type: "leave_group", target: Channels.GroupChannel } |
|
||||
{ type: "close_dm", target: Channels.DirectMessageChannel } |
|
||||
{ type: "leave_server", target: Servers.Server } |
|
||||
{ type: "delete_server", target: Servers.Server } |
|
||||
{ type: "delete_channel", target: Channels.TextChannel } |
|
||||
{ type: "delete_message", target: Channels.Message } |
|
||||
{ type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
|
||||
{ type: "kick_member", target: Servers.Server, user: string } |
|
||||
{ type: "ban_member", target: Servers.Server, user: string } |
|
||||
{ type: "unfriend_user", target: Users.User } |
|
||||
{ type: "block_user", target: Users.User } |
|
||||
{ type: "create_channel", target: Servers.Server }
|
||||
)
|
||||
| { type: "leave_group"; target: Channels.GroupChannel }
|
||||
| { type: "close_dm"; target: Channels.DirectMessageChannel }
|
||||
| { type: "leave_server"; target: Servers.Server }
|
||||
| { type: "delete_server"; target: Servers.Server }
|
||||
| { type: "delete_channel"; target: Channels.TextChannel }
|
||||
| { type: "delete_message"; target: Channels.Message }
|
||||
| {
|
||||
type: "create_invite";
|
||||
target: Channels.TextChannel | Channels.GroupChannel;
|
||||
}
|
||||
| { type: "kick_member"; target: Servers.Server; user: string }
|
||||
| { type: "ban_member"; target: Servers.Server; user: string }
|
||||
| { type: "unfriend_user"; target: Users.User }
|
||||
| { type: "block_user"; target: Users.User }
|
||||
| { type: "create_channel"; target: Servers.Server }
|
||||
);
|
||||
|
||||
export function SpecialPromptModal(props: SpecialProps) {
|
||||
const client = useContext(AppContext);
|
||||
const [ processing, setProcessing ] = useState(false);
|
||||
const [ error, setError ] = useState<undefined | string>(undefined);
|
||||
const client = useContext(AppContext);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<undefined | string>(undefined);
|
||||
|
||||
const { onClose } = props;
|
||||
switch (props.type) {
|
||||
case 'leave_group':
|
||||
case 'close_dm':
|
||||
case 'leave_server':
|
||||
case 'delete_server':
|
||||
case 'delete_channel':
|
||||
case 'unfriend_user':
|
||||
case 'block_user': {
|
||||
const EVENTS = {
|
||||
'close_dm': ['confirm_close_dm', 'close'],
|
||||
'delete_server': ['confirm_delete', 'delete'],
|
||||
'delete_channel': ['confirm_delete', 'delete'],
|
||||
'leave_group': ['confirm_leave', 'leave'],
|
||||
'leave_server': ['confirm_leave', 'leave'],
|
||||
'unfriend_user': ['unfriend_user', 'remove'],
|
||||
'block_user': ['block_user', 'block']
|
||||
};
|
||||
const { onClose } = props;
|
||||
switch (props.type) {
|
||||
case "leave_group":
|
||||
case "close_dm":
|
||||
case "leave_server":
|
||||
case "delete_server":
|
||||
case "delete_channel":
|
||||
case "unfriend_user":
|
||||
case "block_user": {
|
||||
const EVENTS = {
|
||||
close_dm: ["confirm_close_dm", "close"],
|
||||
delete_server: ["confirm_delete", "delete"],
|
||||
delete_channel: ["confirm_delete", "delete"],
|
||||
leave_group: ["confirm_leave", "leave"],
|
||||
leave_server: ["confirm_leave", "leave"],
|
||||
unfriend_user: ["unfriend_user", "remove"],
|
||||
block_user: ["block_user", "block"],
|
||||
};
|
||||
|
||||
let event = EVENTS[props.type];
|
||||
let name;
|
||||
switch (props.type) {
|
||||
case 'unfriend_user':
|
||||
case 'block_user': name = props.target.username; break;
|
||||
case 'close_dm': name = client.users.get(client.channels.getRecipient(props.target._id))?.username; break;
|
||||
default: name = props.target.name;
|
||||
}
|
||||
let event = EVENTS[props.type];
|
||||
let name;
|
||||
switch (props.type) {
|
||||
case "unfriend_user":
|
||||
case "block_user":
|
||||
name = props.target.username;
|
||||
break;
|
||||
case "close_dm":
|
||||
name = client.users.get(
|
||||
client.channels.getRecipient(props.target._id),
|
||||
)?.username;
|
||||
break;
|
||||
default:
|
||||
name = props.target.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text
|
||||
id={`app.special.modals.prompt.${event[0]}`}
|
||||
fields={{ name }}
|
||||
/>}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
error: true,
|
||||
text: <Text id={`app.special.modals.actions.${event[1]}`} />,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={
|
||||
<Text
|
||||
id={`app.special.modals.prompt.${event[0]}`}
|
||||
fields={{ name }}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
error: true,
|
||||
text: (
|
||||
<Text
|
||||
id={`app.special.modals.actions.${event[1]}`}
|
||||
/>
|
||||
),
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
switch (props.type) {
|
||||
case 'unfriend_user':
|
||||
await client.users.removeFriend(props.target._id); break;
|
||||
case 'block_user':
|
||||
await client.users.blockUser(props.target._id); break;
|
||||
case 'leave_group':
|
||||
case 'close_dm':
|
||||
case 'delete_channel':
|
||||
await client.channels.delete(props.target._id); break;
|
||||
case 'leave_server':
|
||||
case 'delete_server':
|
||||
await client.servers.delete(props.target._id); break;
|
||||
}
|
||||
try {
|
||||
switch (props.type) {
|
||||
case "unfriend_user":
|
||||
await client.users.removeFriend(
|
||||
props.target._id,
|
||||
);
|
||||
break;
|
||||
case "block_user":
|
||||
await client.users.blockUser(
|
||||
props.target._id,
|
||||
);
|
||||
break;
|
||||
case "leave_group":
|
||||
case "close_dm":
|
||||
case "delete_channel":
|
||||
await client.channels.delete(
|
||||
props.target._id,
|
||||
);
|
||||
break;
|
||||
case "leave_server":
|
||||
case "delete_server":
|
||||
await client.servers.delete(
|
||||
props.target._id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
|
||||
]}
|
||||
content={<TextReact id={`app.special.modals.prompt.${event[0]}_long`} fields={{ name: <b>{ name }</b> }} />}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'delete_message': {
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={'app.context_menu.delete_message'} />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
error: true,
|
||||
text: <Text id="app.special.modals.actions.delete" />,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
<TextReact
|
||||
id={`app.special.modals.prompt.${event[0]}_long`}
|
||||
fields={{ name: <b>{name}</b> }}
|
||||
/>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "delete_message": {
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={"app.context_menu.delete_message"} />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
error: true,
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.delete" />
|
||||
),
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
await client.channels.deleteMessage(props.target.channel, props.target._id);
|
||||
try {
|
||||
await client.channels.deleteMessage(
|
||||
props.target.channel,
|
||||
props.target._id,
|
||||
);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
|
||||
]}
|
||||
content={<>
|
||||
<Text id={`app.special.modals.prompt.confirm_delete_message_long`} />
|
||||
<Message message={mapMessage(props.target)} head={true} contrast />
|
||||
</>}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "create_invite": {
|
||||
const [ code, setCode ] = useState('abcdef');
|
||||
const { writeClipboard } = useIntermediate();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
<>
|
||||
<Text
|
||||
id={`app.special.modals.prompt.confirm_delete_message_long`}
|
||||
/>
|
||||
<Message
|
||||
message={mapMessage(props.target)}
|
||||
head={true}
|
||||
contrast
|
||||
/>
|
||||
</>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "create_invite": {
|
||||
const [code, setCode] = useState("abcdef");
|
||||
const { writeClipboard } = useIntermediate();
|
||||
|
||||
useEffect(() => {
|
||||
setProcessing(true);
|
||||
useEffect(() => {
|
||||
setProcessing(true);
|
||||
|
||||
client.channels.createInvite(props.target._id)
|
||||
.then(code => setCode(code))
|
||||
.catch(err => setError(takeError(err)))
|
||||
.finally(() => setProcessing(false));
|
||||
}, []);
|
||||
client.channels
|
||||
.createInvite(props.target._id)
|
||||
.then((code) => setCode(code))
|
||||
.catch((err) => setError(takeError(err)))
|
||||
.finally(() => setProcessing(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.create_invite`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
confirmation: true,
|
||||
onClick: onClose
|
||||
},
|
||||
{
|
||||
text: <Text id="app.context_menu.copy_link" />,
|
||||
onClick: () => writeClipboard(`${window.location.protocol}//${window.location.host}/invite/${code}`)
|
||||
}
|
||||
]}
|
||||
content={
|
||||
processing ?
|
||||
<Text id="app.special.modals.prompt.create_invite_generate" />
|
||||
: <div className={styles.invite}>
|
||||
<Text id="app.special.modals.prompt.create_invite_created" />
|
||||
<code>{code}</code>
|
||||
</div>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "kick_member": {
|
||||
const user = client.users.get(props.user);
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.create_invite`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
confirmation: true,
|
||||
onClick: onClose,
|
||||
},
|
||||
{
|
||||
text: <Text id="app.context_menu.copy_link" />,
|
||||
onClick: () =>
|
||||
writeClipboard(
|
||||
`${window.location.protocol}//${window.location.host}/invite/${code}`,
|
||||
),
|
||||
},
|
||||
]}
|
||||
content={
|
||||
processing ? (
|
||||
<Text id="app.special.modals.prompt.create_invite_generate" />
|
||||
) : (
|
||||
<div className={styles.invite}>
|
||||
<Text id="app.special.modals.prompt.create_invite_created" />
|
||||
<code>{code}</code>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "kick_member": {
|
||||
const user = client.users.get(props.user);
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.kick_member`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.kick" />,
|
||||
contrast: true,
|
||||
error: true,
|
||||
confirmation: true,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
await client.servers.members.kickMember(props.target._id, props.user);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
|
||||
]}
|
||||
content={<div className={styles.column}>
|
||||
<UserIcon target={user} size={64} />
|
||||
<Text
|
||||
id="app.special.modals.prompt.confirm_kick"
|
||||
fields={{ name: user?.username }} />
|
||||
</div>}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "ban_member": {
|
||||
const [ reason, setReason ] = useState<string | undefined>(undefined);
|
||||
const user = client.users.get(props.user);
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.kick_member`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.kick" />,
|
||||
contrast: true,
|
||||
error: true,
|
||||
confirmation: true,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.ban_member`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ban" />,
|
||||
contrast: true,
|
||||
error: true,
|
||||
confirmation: true,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
await client.servers.banUser(props.target._id, props.user, { reason });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
|
||||
]}
|
||||
content={<div className={styles.column}>
|
||||
<UserIcon target={user} size={64} />
|
||||
<Text
|
||||
id="app.special.modals.prompt.confirm_ban"
|
||||
fields={{ name: user?.username }} />
|
||||
<Overline><Text id="app.special.modals.prompt.confirm_ban_reason" /></Overline>
|
||||
<InputBox value={reason ?? ''} onChange={e => setReason(e.currentTarget.value)} />
|
||||
</div>}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'create_channel': {
|
||||
const [ name, setName ] = useState('');
|
||||
const [ type, setType ] = useState<'Text' | 'Voice'>('Text');
|
||||
const history = useHistory();
|
||||
try {
|
||||
await client.servers.members.kickMember(
|
||||
props.target._id,
|
||||
props.user,
|
||||
);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
<div className={styles.column}>
|
||||
<UserIcon target={user} size={64} />
|
||||
<Text
|
||||
id="app.special.modals.prompt.confirm_kick"
|
||||
fields={{ name: user?.username }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "ban_member": {
|
||||
const [reason, setReason] = useState<string | undefined>(undefined);
|
||||
const user = client.users.get(props.user);
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.context_menu.create_channel" />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
text: <Text id="app.special.modals.actions.create" />,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id={`app.context_menu.ban_member`} />}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ban" />,
|
||||
contrast: true,
|
||||
error: true,
|
||||
confirmation: true,
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
const channel = await client.servers.createChannel(
|
||||
props.target._id,
|
||||
{
|
||||
type,
|
||||
name,
|
||||
nonce: ulid()
|
||||
}
|
||||
);
|
||||
|
||||
history.push(`/server/${props.target._id}/channel/${channel._id}`);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
|
||||
]}
|
||||
content={<>
|
||||
<Overline block type="subtle"><Text id="app.main.servers.channel_type" /></Overline>
|
||||
<Radio checked={type === 'Text'} onSelect={() => setType('Text')}>
|
||||
<Text id="app.main.servers.text_channel" /></Radio>
|
||||
<Radio checked={type === 'Voice'} onSelect={() => setType('Voice')}>
|
||||
<Text id="app.main.servers.voice_channel" /></Radio>
|
||||
<Overline block type="subtle"><Text id="app.main.servers.channel_name" /></Overline>
|
||||
<InputBox
|
||||
value={name}
|
||||
onChange={e => setName(e.currentTarget.value)} />
|
||||
</>}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
try {
|
||||
await client.servers.banUser(
|
||||
props.target._id,
|
||||
props.user,
|
||||
{ reason },
|
||||
);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
<div className={styles.column}>
|
||||
<UserIcon target={user} size={64} />
|
||||
<Text
|
||||
id="app.special.modals.prompt.confirm_ban"
|
||||
fields={{ name: user?.username }}
|
||||
/>
|
||||
<Overline>
|
||||
<Text id="app.special.modals.prompt.confirm_ban_reason" />
|
||||
</Overline>
|
||||
<InputBox
|
||||
value={reason ?? ""}
|
||||
onChange={(e) =>
|
||||
setReason(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "create_channel": {
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState<"Text" | "Voice">("Text");
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<PromptModal
|
||||
onClose={onClose}
|
||||
question={<Text id="app.context_menu.create_channel" />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
contrast: true,
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.create" />
|
||||
),
|
||||
onClick: async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
const channel =
|
||||
await client.servers.createChannel(
|
||||
props.target._id,
|
||||
{
|
||||
type,
|
||||
name,
|
||||
nonce: ulid(),
|
||||
},
|
||||
);
|
||||
|
||||
history.push(
|
||||
`/server/${props.target._id}/channel/${channel._id}`,
|
||||
);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: onClose,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
<>
|
||||
<Overline block type="subtle">
|
||||
<Text id="app.main.servers.channel_type" />
|
||||
</Overline>
|
||||
<Radio
|
||||
checked={type === "Text"}
|
||||
onSelect={() => setType("Text")}>
|
||||
<Text id="app.main.servers.text_channel" />
|
||||
</Radio>
|
||||
<Radio
|
||||
checked={type === "Voice"}
|
||||
onSelect={() => setType("Voice")}>
|
||||
<Text id="app.main.servers.voice_channel" />
|
||||
</Radio>
|
||||
<Overline block type="subtle">
|
||||
<Text id="app.main.servers.channel_name" />
|
||||
</Overline>
|
||||
<InputBox
|
||||
value={name}
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
disabled={processing}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SignedOutModal({ onClose }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id="app.special.modals.signed_out" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id="app.special.modals.signed_out" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: onClose,
|
||||
confirmation: true,
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import { X } from "@styled-icons/boxicons-regular";
|
||||
|
||||
import styles from "./ChannelInfo.module.scss";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { getChannelName } from "../../revoltjs/util";
|
||||
|
||||
import Markdown from "../../../components/markdown/Markdown";
|
||||
import { useChannel, useForceUpdate } from "../../revoltjs/hooks";
|
||||
import { getChannelName } from "../../revoltjs/util";
|
||||
|
||||
interface Props {
|
||||
channel_id: string;
|
||||
onClose: () => void;
|
||||
channel_id: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ChannelInfo({ channel_id, onClose }: Props) {
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(channel_id, ctx);
|
||||
if (!channel) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(channel_id, ctx);
|
||||
if (!channel) return null;
|
||||
|
||||
if (channel.channel_type === "DirectMessage" || channel.channel_type === 'SavedMessages') {
|
||||
onClose();
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
channel.channel_type === "DirectMessage" ||
|
||||
channel.channel_type === "SavedMessages"
|
||||
) {
|
||||
onClose();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={true} onClose={onClose}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.header}>
|
||||
<h1>{ getChannelName(ctx.client, channel, true) }</h1>
|
||||
<div onClick={onClose}>
|
||||
<X size={36} />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<Markdown content={channel.description} />
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal visible={true} onClose={onClose}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.header}>
|
||||
<h1>{getChannelName(ctx.client, channel, true)}</h1>
|
||||
<div onClick={onClose}>
|
||||
<X size={36} />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<Markdown content={channel.description} />
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,46 @@
|
||||
import styles from "./ImageViewer.module.scss";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { Attachment, EmbedImage } from "revolt.js/dist/api/objects";
|
||||
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
||||
|
||||
import styles from "./ImageViewer.module.scss";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
||||
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
embed?: EmbedImage;
|
||||
attachment?: Attachment;
|
||||
onClose: () => void;
|
||||
embed?: EmbedImage;
|
||||
attachment?: Attachment;
|
||||
}
|
||||
|
||||
export function ImageViewer({ attachment, embed, onClose }: Props) {
|
||||
// ! FIXME: temp code
|
||||
// ! add proxy function to client
|
||||
function proxyImage(url: string) {
|
||||
return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
if (attachment && attachment.metadata.type !== "Image") return null;
|
||||
const client = useContext(AppContext);
|
||||
// ! FIXME: temp code
|
||||
// ! add proxy function to client
|
||||
function proxyImage(url: string) {
|
||||
return "https://jan.revolt.chat/proxy?url=" + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={true} onClose={onClose} noBackground>
|
||||
<div className={styles.viewer}>
|
||||
{ attachment &&
|
||||
<>
|
||||
<img src={client.generateFileURL(attachment)} />
|
||||
<AttachmentActions attachment={attachment} />
|
||||
</>
|
||||
}
|
||||
{ embed &&
|
||||
<>
|
||||
<img src={proxyImage(embed.url)} />
|
||||
<EmbedMediaActions embed={embed} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
if (attachment && attachment.metadata.type !== "Image") return null;
|
||||
const client = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Modal visible={true} onClose={onClose} noBackground>
|
||||
<div className={styles.viewer}>
|
||||
{attachment && (
|
||||
<>
|
||||
<img src={client.generateFileURL(attachment)} />
|
||||
<AttachmentActions attachment={attachment} />
|
||||
</>
|
||||
)}
|
||||
{embed && (
|
||||
<>
|
||||
<img src={proxyImage(embed.url)} />
|
||||
<EmbedMediaActions embed={embed} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,127 +1,134 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
import FormField from '../../../pages/login/FormField';
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
|
||||
import FormField from "../../../pages/login/FormField";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
field: "username" | "email" | "password";
|
||||
onClose: () => void;
|
||||
field: "username" | "email" | "password";
|
||||
}
|
||||
|
||||
interface FormInputs {
|
||||
password: string,
|
||||
new_email: string,
|
||||
new_username: string,
|
||||
new_password: string,
|
||||
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
|
||||
// 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) {
|
||||
const client = useContext(AppContext);
|
||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const client = useContext(AppContext);
|
||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSubmit: SubmitHandler<FormInputs> = async ({
|
||||
password,
|
||||
new_username,
|
||||
new_email,
|
||||
new_password
|
||||
}) => {
|
||||
try {
|
||||
if (field === "email") {
|
||||
await client.req("POST", "/auth/change/email", {
|
||||
password,
|
||||
new_email
|
||||
});
|
||||
onClose();
|
||||
} else if (field === "password") {
|
||||
await client.req("POST", "/auth/change/password", {
|
||||
password,
|
||||
new_password
|
||||
});
|
||||
onClose();
|
||||
} else if (field === "username") {
|
||||
await client.req("PATCH", "/users/id/username", {
|
||||
username: new_username,
|
||||
password
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
}
|
||||
}
|
||||
const onSubmit: SubmitHandler<FormInputs> = async ({
|
||||
password,
|
||||
new_username,
|
||||
new_email,
|
||||
new_password,
|
||||
}) => {
|
||||
try {
|
||||
if (field === "email") {
|
||||
await client.req("POST", "/auth/change/email", {
|
||||
password,
|
||||
new_email,
|
||||
});
|
||||
onClose();
|
||||
} else if (field === "password") {
|
||||
await client.req("POST", "/auth/change/password", {
|
||||
password,
|
||||
new_password,
|
||||
});
|
||||
onClose();
|
||||
} else if (field === "username") {
|
||||
await client.req("PATCH", "/users/id/username", {
|
||||
username: new_username,
|
||||
password,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id={`app.special.modals.account.change.${field}`} />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
onClick: handleSubmit(onSubmit),
|
||||
text:
|
||||
field === "email" ? (
|
||||
<Text id="app.special.modals.actions.send_email" />
|
||||
) : (
|
||||
<Text id="app.special.modals.actions.update" />
|
||||
)
|
||||
},
|
||||
{
|
||||
onClick: onClose,
|
||||
text: <Text id="app.special.modals.actions.close" />
|
||||
}
|
||||
]}
|
||||
>
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
|
||||
{field === "email" && (
|
||||
<FormField
|
||||
type="email"
|
||||
name="new_email"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_email?.message}
|
||||
/>
|
||||
)}
|
||||
{field === "password" && (
|
||||
<FormField
|
||||
type="password"
|
||||
name="new_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_password?.message}
|
||||
/>
|
||||
)}
|
||||
{field === "username" && (
|
||||
<FormField
|
||||
type="username"
|
||||
name="new_username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_username?.message}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
type="current_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.current_password?.message}
|
||||
/>
|
||||
{error && (
|
||||
<Overline type="error" error={error}>
|
||||
<Text id="app.special.modals.account.failed" />
|
||||
</Overline>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onClose={onClose}
|
||||
title={<Text id={`app.special.modals.account.change.${field}`} />}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
onClick: handleSubmit(onSubmit),
|
||||
text:
|
||||
field === "email" ? (
|
||||
<Text id="app.special.modals.actions.send_email" />
|
||||
) : (
|
||||
<Text id="app.special.modals.actions.update" />
|
||||
),
|
||||
},
|
||||
{
|
||||
onClick: onClose,
|
||||
text: <Text id="app.special.modals.actions.close" />,
|
||||
},
|
||||
]}>
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form
|
||||
onSubmit={
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
) as JSX.GenericEventHandler<HTMLFormElement>
|
||||
}>
|
||||
{field === "email" && (
|
||||
<FormField
|
||||
type="email"
|
||||
name="new_email"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_email?.message}
|
||||
/>
|
||||
)}
|
||||
{field === "password" && (
|
||||
<FormField
|
||||
type="password"
|
||||
name="new_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_password?.message}
|
||||
/>
|
||||
)}
|
||||
{field === "username" && (
|
||||
<FormField
|
||||
type="username"
|
||||
name="new_username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_username?.message}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
type="current_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.current_password?.message}
|
||||
/>
|
||||
{error && (
|
||||
<Overline type="error" error={error}>
|
||||
<Text id="app.special.modals.account.failed" />
|
||||
</Overline>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./UserPicker.module.scss";
|
||||
import { useUsers } from "../../revoltjs/hooks";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
import { Friend } from "../../../pages/friends/Friend";
|
||||
import { useUsers } from "../../revoltjs/hooks";
|
||||
|
||||
interface Props {
|
||||
users: string[];
|
||||
onClose: () => void;
|
||||
users: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function PendingRequests({ users: ids, onClose }: Props) {
|
||||
const users = useUsers(ids);
|
||||
const users = useUsers(ids);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={<Text id="app.special.friends.pending" />}
|
||||
onClose={onClose}>
|
||||
<div className={styles.list}>
|
||||
{ users
|
||||
.filter(x => typeof x !== 'undefined')
|
||||
.map(x => <Friend user={x!} key={x!._id} />) }
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={<Text id="app.special.friends.pending" />}
|
||||
onClose={onClose}>
|
||||
<div className={styles.list}>
|
||||
{users
|
||||
.filter((x) => typeof x !== "undefined")
|
||||
.map((x) => (
|
||||
<Friend user={x!} key={x!._id} />
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
import { User, Users } from "revolt.js/dist/api/objects";
|
||||
|
||||
import styles from "./UserPicker.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
import styles from "./UserPicker.module.scss";
|
||||
import { useUsers } from "../../revoltjs/hooks";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { User, Users } from "revolt.js/dist/api/objects";
|
||||
|
||||
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
import { useUsers } from "../../revoltjs/hooks";
|
||||
|
||||
interface Props {
|
||||
omit?: string[];
|
||||
onClose: () => void;
|
||||
callback: (users: string[]) => Promise<void>;
|
||||
omit?: string[];
|
||||
onClose: () => void;
|
||||
callback: (users: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function UserPicker(props: Props) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const omit = [...(props.omit || []), "00000000000000000000000000"];
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const omit = [...(props.omit || []), "00000000000000000000000000"];
|
||||
|
||||
const users = useUsers();
|
||||
const users = useUsers();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={<Text id="app.special.popovers.user_picker.select" />}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
onClick: () => props.callback(selected).then(props.onClose)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{(users.filter(
|
||||
x =>
|
||||
x &&
|
||||
x.relationship === Users.Relationship.Friend &&
|
||||
!omit.includes(x._id)
|
||||
) as User[])
|
||||
.map(x => {
|
||||
return {
|
||||
...x,
|
||||
selected: selected.includes(x._id)
|
||||
};
|
||||
})
|
||||
.map(x => (
|
||||
<UserCheckbox
|
||||
user={x}
|
||||
checked={x.selected}
|
||||
onChange={v => {
|
||||
if (v) {
|
||||
setSelected([...selected, x._id]);
|
||||
} else {
|
||||
setSelected(
|
||||
selected.filter(y => y !== x._id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={<Text id="app.special.popovers.user_picker.select" />}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
{
|
||||
text: <Text id="app.special.modals.actions.ok" />,
|
||||
onClick: () => props.callback(selected).then(props.onClose),
|
||||
},
|
||||
]}>
|
||||
<div className={styles.list}>
|
||||
{(
|
||||
users.filter(
|
||||
(x) =>
|
||||
x &&
|
||||
x.relationship === Users.Relationship.Friend &&
|
||||
!omit.includes(x._id),
|
||||
) as User[]
|
||||
)
|
||||
.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
selected: selected.includes(x._id),
|
||||
};
|
||||
})
|
||||
.map((x) => (
|
||||
<UserCheckbox
|
||||
user={x}
|
||||
checked={x.selected}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
setSelected([...selected, x._id]);
|
||||
} else {
|
||||
setSelected(
|
||||
selected.filter((y) => y !== x._id),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,331 +1,362 @@
|
||||
import { decodeTime } from "ulid";
|
||||
import {
|
||||
Envelope,
|
||||
Edit,
|
||||
UserPlus,
|
||||
Shield,
|
||||
Money,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import styles from "./UserProfile.module.scss";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
import { Users } from "revolt.js/dist/api/objects";
|
||||
import { useIntermediate } from "../Intermediate";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
import Tooltip from '../../../components/common/Tooltip';
|
||||
import IconButton from "../../../components/ui/IconButton";
|
||||
import Markdown from '../../../components/markdown/Markdown';
|
||||
import { UserPermission } from "revolt.js/dist/api/permissions";
|
||||
import UserIcon from '../../../components/common/user/UserIcon';
|
||||
import ChannelIcon from '../../../components/common/ChannelIcon';
|
||||
import UserStatus from '../../../components/common/user/UserStatus';
|
||||
import { Envelope, Edit, UserPlus, Shield, Money } from "@styled-icons/boxicons-regular";
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import styles from "./UserProfile.module.scss";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||
import { AppContext, ClientStatus, StatusContext } from "../../revoltjs/RevoltClient";
|
||||
import { useChannels, useForceUpdate, useUserPermission, useUsers } from "../../revoltjs/hooks";
|
||||
|
||||
import ChannelIcon from "../../../components/common/ChannelIcon";
|
||||
import Tooltip from "../../../components/common/Tooltip";
|
||||
import UserIcon from "../../../components/common/user/UserIcon";
|
||||
import UserStatus from "../../../components/common/user/UserStatus";
|
||||
import IconButton from "../../../components/ui/IconButton";
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
import Markdown from "../../../components/markdown/Markdown";
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../revoltjs/RevoltClient";
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUserPermission,
|
||||
useUsers,
|
||||
} from "../../revoltjs/hooks";
|
||||
import { useIntermediate } from "../Intermediate";
|
||||
|
||||
interface Props {
|
||||
user_id: string;
|
||||
dummy?: boolean;
|
||||
onClose: () => void;
|
||||
dummyProfile?: Users.Profile;
|
||||
user_id: string;
|
||||
dummy?: boolean;
|
||||
onClose: () => void;
|
||||
dummyProfile?: Users.Profile;
|
||||
}
|
||||
|
||||
enum Badges {
|
||||
Developer = 1,
|
||||
Translator = 2,
|
||||
Supporter = 4,
|
||||
ResponsibleDisclosure = 8,
|
||||
EarlyAdopter = 256
|
||||
Developer = 1,
|
||||
Translator = 2,
|
||||
Supporter = 4,
|
||||
ResponsibleDisclosure = 8,
|
||||
EarlyAdopter = 256,
|
||||
}
|
||||
|
||||
export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) {
|
||||
const { openScreen, writeClipboard } = useIntermediate();
|
||||
const { openScreen, writeClipboard } = useIntermediate();
|
||||
|
||||
const [profile, setProfile] = useState<undefined | null | Users.Profile>(
|
||||
undefined
|
||||
);
|
||||
const [mutual, setMutual] = useState<
|
||||
undefined | null | Route<"GET", "/users/id/mutual">["response"]
|
||||
>(undefined);
|
||||
const [profile, setProfile] = useState<undefined | null | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const [mutual, setMutual] = useState<
|
||||
undefined | null | Route<"GET", "/users/id/mutual">["response"]
|
||||
>(undefined);
|
||||
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const [tab, setTab] = useState("profile");
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const [tab, setTab] = useState("profile");
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const all_users = useUsers(undefined, ctx);
|
||||
const channels = useChannels(undefined, ctx);
|
||||
|
||||
const user = all_users.find(x => x!._id === user_id);
|
||||
const users = mutual?.users ? all_users.filter(x => mutual.users.includes(x!._id)) : undefined;
|
||||
|
||||
if (!user) {
|
||||
useEffect(onClose, []);
|
||||
return null;
|
||||
}
|
||||
const ctx = useForceUpdate();
|
||||
const all_users = useUsers(undefined, ctx);
|
||||
const channels = useChannels(undefined, ctx);
|
||||
|
||||
const permissions = useUserPermission(user!._id, ctx);
|
||||
const user = all_users.find((x) => x!._id === user_id);
|
||||
const users = mutual?.users
|
||||
? all_users.filter((x) => mutual.users.includes(x!._id))
|
||||
: undefined;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!user_id) return;
|
||||
if (typeof profile !== 'undefined') setProfile(undefined);
|
||||
if (typeof mutual !== 'undefined') setMutual(undefined);
|
||||
}, [user_id]);
|
||||
if (!user) {
|
||||
useEffect(onClose, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dummy) {
|
||||
useLayoutEffect(() => {
|
||||
setProfile(dummyProfile);
|
||||
}, [dummyProfile]);
|
||||
}
|
||||
const permissions = useUserPermission(user!._id, ctx);
|
||||
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (
|
||||
status === ClientStatus.ONLINE &&
|
||||
typeof mutual === "undefined"
|
||||
) {
|
||||
setMutual(null);
|
||||
client.users
|
||||
.fetchMutual(user_id)
|
||||
.then(data => setMutual(data));
|
||||
}
|
||||
}, [mutual, status]);
|
||||
useLayoutEffect(() => {
|
||||
if (!user_id) return;
|
||||
if (typeof profile !== "undefined") setProfile(undefined);
|
||||
if (typeof mutual !== "undefined") setMutual(undefined);
|
||||
}, [user_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (
|
||||
status === ClientStatus.ONLINE &&
|
||||
typeof profile === "undefined"
|
||||
) {
|
||||
setProfile(null);
|
||||
if (dummy) {
|
||||
useLayoutEffect(() => {
|
||||
setProfile(dummyProfile);
|
||||
}, [dummyProfile]);
|
||||
}
|
||||
|
||||
if (permissions & UserPermission.ViewProfile) {
|
||||
client.users
|
||||
.fetchProfile(user_id)
|
||||
.then(data => setProfile(data))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}, [profile, status]);
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (status === ClientStatus.ONLINE && typeof mutual === "undefined") {
|
||||
setMutual(null);
|
||||
client.users.fetchMutual(user_id).then((data) => setMutual(data));
|
||||
}
|
||||
}, [mutual, status]);
|
||||
|
||||
const mutualGroups = channels.filter(
|
||||
channel =>
|
||||
channel?.channel_type === "Group" &&
|
||||
channel.recipients.includes(user_id)
|
||||
);
|
||||
useEffect(() => {
|
||||
if (dummy) return;
|
||||
if (status === ClientStatus.ONLINE && typeof profile === "undefined") {
|
||||
setProfile(null);
|
||||
|
||||
const backgroundURL = profile && client.users.getBackgroundURL(profile, { width: 1000 }, true);
|
||||
const badges = (user.badges ?? 0) | (decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
|
||||
if (permissions & UserPermission.ViewProfile) {
|
||||
client.users
|
||||
.fetchProfile(user_id)
|
||||
.then((data) => setProfile(data))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}, [profile, status]);
|
||||
|
||||
return (
|
||||
<Modal visible
|
||||
border={dummy}
|
||||
padding={false}
|
||||
onClose={onClose}
|
||||
dontModal={dummy}>
|
||||
<div
|
||||
className={styles.header}
|
||||
data-force={
|
||||
profile?.background
|
||||
? "light"
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
backgroundImage: backgroundURL && `linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`
|
||||
}}
|
||||
>
|
||||
<div className={styles.profile}>
|
||||
<UserIcon size={80} target={user} status />
|
||||
<div className={styles.details}>
|
||||
<Localizer>
|
||||
<span
|
||||
className={styles.username}
|
||||
onClick={() => writeClipboard(user.username)}>
|
||||
@{user.username}
|
||||
</span>
|
||||
</Localizer>
|
||||
{user.status?.text && (
|
||||
<span className={styles.status}>
|
||||
<UserStatus user={user} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{user.relationship === Users.Relationship.Friend && (
|
||||
<Localizer>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.context_menu.message_user" />
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
history.push(`/open/${user_id}`);
|
||||
}}>
|
||||
<Envelope size={30} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Localizer>
|
||||
)}
|
||||
{user.relationship === Users.Relationship.User && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
if (dummy) return;
|
||||
history.push(`/settings/profile`);
|
||||
}}>
|
||||
<Edit size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
{(user.relationship === Users.Relationship.Incoming ||
|
||||
user.relationship === Users.Relationship.None) && (
|
||||
<IconButton onClick={() => client.users.addFriend(user.username)}>
|
||||
<UserPlus size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.tabs}>
|
||||
<div
|
||||
data-active={tab === "profile"}
|
||||
onClick={() => setTab("profile")}
|
||||
>
|
||||
<Text id="app.special.popovers.user_profile.profile" />
|
||||
</div>
|
||||
{ user.relationship !== Users.Relationship.User &&
|
||||
<>
|
||||
<div
|
||||
data-active={tab === "friends"}
|
||||
onClick={() => setTab("friends")}
|
||||
>
|
||||
<Text id="app.special.popovers.user_profile.mutual_friends" />
|
||||
</div>
|
||||
<div
|
||||
data-active={tab === "groups"}
|
||||
onClick={() => setTab("groups")}
|
||||
>
|
||||
<Text id="app.special.popovers.user_profile.mutual_groups" />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{tab === "profile" &&
|
||||
<div>
|
||||
{ !(profile?.content || (badges > 0)) &&
|
||||
<div className={styles.empty}><Text id="app.special.popovers.user_profile.empty" /></div> }
|
||||
{ (badges > 0) && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.badges" /></div> }
|
||||
{ (badges > 0) && (
|
||||
<div className={styles.badges}>
|
||||
<Localizer>
|
||||
{badges & Badges.Developer ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.navigation.tabs.dev" />
|
||||
}
|
||||
>
|
||||
<img src="/assets/badges/developer.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.Translator ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.translator" />
|
||||
}
|
||||
>
|
||||
<img src="/assets/badges/translator.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.EarlyAdopter ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.early_adopter" />
|
||||
}
|
||||
>
|
||||
<img src="/assets/badges/early_adopter.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.Supporter ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.supporter" />
|
||||
}
|
||||
>
|
||||
<Money size={32} color="#efab44" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.ResponsibleDisclosure ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
|
||||
}
|
||||
>
|
||||
<Shield size={32} color="gray" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Localizer>
|
||||
</div>
|
||||
)}
|
||||
{ profile?.content && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.information" /></div> }
|
||||
<Markdown content={profile?.content} />
|
||||
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
|
||||
</div>}
|
||||
{tab === "friends" &&
|
||||
(users ? (
|
||||
<div className={styles.entries}>
|
||||
{users.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.special.popovers.user_profile.no_users" />
|
||||
</div>
|
||||
) : (
|
||||
users.map(
|
||||
x =>
|
||||
x && (
|
||||
<div onClick={() => openScreen({ id: 'profile', user_id: x._id })}
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<UserIcon size={32} target={x} />
|
||||
<span>{x.username}</span>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Preloader type="ring" />
|
||||
))}
|
||||
{tab === "groups" && (
|
||||
<div className={styles.entries}>
|
||||
{mutualGroups.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.special.popovers.user_profile.no_groups" />
|
||||
</div>
|
||||
) : (
|
||||
mutualGroups.map(
|
||||
x =>
|
||||
x?.channel_type === "Group" && (
|
||||
<Link to={`/channel/${x._id}`}>
|
||||
<div
|
||||
className={styles.entry}
|
||||
key={x._id}
|
||||
>
|
||||
<ChannelIcon target={x} size={32} />
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
const mutualGroups = channels.filter(
|
||||
(channel) =>
|
||||
channel?.channel_type === "Group" &&
|
||||
channel.recipients.includes(user_id),
|
||||
);
|
||||
|
||||
const backgroundURL =
|
||||
profile &&
|
||||
client.users.getBackgroundURL(profile, { width: 1000 }, true);
|
||||
const badges =
|
||||
(user.badges ?? 0) |
|
||||
(decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
border={dummy}
|
||||
padding={false}
|
||||
onClose={onClose}
|
||||
dontModal={dummy}>
|
||||
<div
|
||||
className={styles.header}
|
||||
data-force={profile?.background ? "light" : undefined}
|
||||
style={{
|
||||
backgroundImage:
|
||||
backgroundURL &&
|
||||
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
|
||||
}}>
|
||||
<div className={styles.profile}>
|
||||
<UserIcon size={80} target={user} status />
|
||||
<div className={styles.details}>
|
||||
<Localizer>
|
||||
<span
|
||||
className={styles.username}
|
||||
onClick={() => writeClipboard(user.username)}>
|
||||
@{user.username}
|
||||
</span>
|
||||
</Localizer>
|
||||
{user.status?.text && (
|
||||
<span className={styles.status}>
|
||||
<UserStatus user={user} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{user.relationship === Users.Relationship.Friend && (
|
||||
<Localizer>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.context_menu.message_user" />
|
||||
}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
history.push(`/open/${user_id}`);
|
||||
}}>
|
||||
<Envelope size={30} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Localizer>
|
||||
)}
|
||||
{user.relationship === Users.Relationship.User && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
if (dummy) return;
|
||||
history.push(`/settings/profile`);
|
||||
}}>
|
||||
<Edit size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
{(user.relationship === Users.Relationship.Incoming ||
|
||||
user.relationship === Users.Relationship.None) && (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
client.users.addFriend(user.username)
|
||||
}>
|
||||
<UserPlus size={28} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.tabs}>
|
||||
<div
|
||||
data-active={tab === "profile"}
|
||||
onClick={() => setTab("profile")}>
|
||||
<Text id="app.special.popovers.user_profile.profile" />
|
||||
</div>
|
||||
{user.relationship !== Users.Relationship.User && (
|
||||
<>
|
||||
<div
|
||||
data-active={tab === "friends"}
|
||||
onClick={() => setTab("friends")}>
|
||||
<Text id="app.special.popovers.user_profile.mutual_friends" />
|
||||
</div>
|
||||
<div
|
||||
data-active={tab === "groups"}
|
||||
onClick={() => setTab("groups")}>
|
||||
<Text id="app.special.popovers.user_profile.mutual_groups" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{tab === "profile" && (
|
||||
<div>
|
||||
{!(profile?.content || badges > 0) && (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.special.popovers.user_profile.empty" />
|
||||
</div>
|
||||
)}
|
||||
{badges > 0 && (
|
||||
<div className={styles.category}>
|
||||
<Text id="app.special.popovers.user_profile.sub.badges" />
|
||||
</div>
|
||||
)}
|
||||
{badges > 0 && (
|
||||
<div className={styles.badges}>
|
||||
<Localizer>
|
||||
{badges & Badges.Developer ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.navigation.tabs.dev" />
|
||||
}>
|
||||
<img src="/assets/badges/developer.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.Translator ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.translator" />
|
||||
}>
|
||||
<img src="/assets/badges/translator.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.EarlyAdopter ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.early_adopter" />
|
||||
}>
|
||||
<img src="/assets/badges/early_adopter.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.Supporter ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.supporter" />
|
||||
}>
|
||||
<Money size={32} color="#efab44" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.ResponsibleDisclosure ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
|
||||
}>
|
||||
<Shield size={32} color="gray" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Localizer>
|
||||
</div>
|
||||
)}
|
||||
{profile?.content && (
|
||||
<div className={styles.category}>
|
||||
<Text id="app.special.popovers.user_profile.sub.information" />
|
||||
</div>
|
||||
)}
|
||||
<Markdown content={profile?.content} />
|
||||
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
|
||||
</div>
|
||||
)}
|
||||
{tab === "friends" &&
|
||||
(users ? (
|
||||
<div className={styles.entries}>
|
||||
{users.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.special.popovers.user_profile.no_users" />
|
||||
</div>
|
||||
) : (
|
||||
users.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "profile",
|
||||
user_id: x._id,
|
||||
})
|
||||
}
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<UserIcon
|
||||
size={32}
|
||||
target={x}
|
||||
/>
|
||||
<span>{x.username}</span>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Preloader type="ring" />
|
||||
))}
|
||||
{tab === "groups" && (
|
||||
<div className={styles.entries}>
|
||||
{mutualGroups.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.special.popovers.user_profile.no_groups" />
|
||||
</div>
|
||||
) : (
|
||||
mutualGroups.map(
|
||||
(x) =>
|
||||
x?.channel_type === "Group" && (
|
||||
<Link to={`/channel/${x._id}`}>
|
||||
<div
|
||||
className={styles.entry}
|
||||
key={x._id}>
|
||||
<ChannelIcon
|
||||
target={x}
|
||||
size={32}
|
||||
/>
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import { OperationsContext } from "./RevoltClient";
|
||||
|
||||
interface Props {
|
||||
auth?: boolean;
|
||||
children: Children;
|
||||
auth?: boolean;
|
||||
children: Children;
|
||||
}
|
||||
|
||||
export const CheckAuth = (props: Props) => {
|
||||
const operations = useContext(OperationsContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
|
||||
if (props.auth && !operations.ready()) {
|
||||
return <Redirect to="/login" />;
|
||||
} else if (!props.auth && operations.ready()) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
if (props.auth && !operations.ready()) {
|
||||
return <Redirect to="/login" />;
|
||||
} else if (!props.auth && operations.ready()) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
|
||||
return <>{props.children}</>;
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
@@ -1,221 +1,292 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import { takeError } from "./util";
|
||||
import classNames from "classnames";
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import styles from './FileUploads.module.scss';
|
||||
import Axios, { AxiosRequestConfig } from "axios";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
import { determineFileSize } from "../../lib/fileSize";
|
||||
import IconButton from '../../components/ui/IconButton';
|
||||
import { Plus, X, XCircle } from "@styled-icons/boxicons-regular";
|
||||
import { Pencil } from "@styled-icons/boxicons-solid";
|
||||
import Axios, { AxiosRequestConfig } from "axios";
|
||||
|
||||
import styles from "./FileUploads.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { determineFileSize } from "../../lib/fileSize";
|
||||
|
||||
import IconButton from "../../components/ui/IconButton";
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
|
||||
import { useIntermediate } from "../intermediate/Intermediate";
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import { takeError } from "./util";
|
||||
|
||||
type Props = {
|
||||
maxFileSize: number
|
||||
remove: () => Promise<void>
|
||||
fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners'
|
||||
maxFileSize: number;
|
||||
remove: () => Promise<void>;
|
||||
fileType: "backgrounds" | "icons" | "avatars" | "attachments" | "banners";
|
||||
} & (
|
||||
{ behaviour: 'ask', onChange: (file: File) => void } |
|
||||
{ behaviour: 'upload', onUpload: (id: string) => Promise<void> } |
|
||||
{ behaviour: 'multi', onChange: (files: File[]) => void, append?: (files: File[]) => void }
|
||||
) & (
|
||||
{ style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } |
|
||||
{ style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number }
|
||||
)
|
||||
| { behaviour: "ask"; onChange: (file: File) => void }
|
||||
| { behaviour: "upload"; onUpload: (id: string) => Promise<void> }
|
||||
| {
|
||||
behaviour: "multi";
|
||||
onChange: (files: File[]) => void;
|
||||
append?: (files: File[]) => void;
|
||||
}
|
||||
) &
|
||||
(
|
||||
| {
|
||||
style: "icon" | "banner";
|
||||
defaultPreview?: string;
|
||||
previewURL?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
| {
|
||||
style: "attachment";
|
||||
attached: boolean;
|
||||
uploading: boolean;
|
||||
cancel: () => void;
|
||||
size?: number;
|
||||
}
|
||||
);
|
||||
|
||||
export async function uploadFile(autumnURL: string, tag: string, file: File, config?: AxiosRequestConfig) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const res = await Axios.post(autumnURL + "/" + tag, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
...config
|
||||
});
|
||||
export async function uploadFile(
|
||||
autumnURL: string,
|
||||
tag: string,
|
||||
file: File,
|
||||
config?: AxiosRequestConfig,
|
||||
) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
return res.data.id;
|
||||
const res = await Axios.post(autumnURL + "/" + tag, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
return res.data.id;
|
||||
}
|
||||
|
||||
export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooLarge: () => void, multiple?: boolean) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = multiple ?? false;
|
||||
export function grabFiles(
|
||||
maxFileSize: number,
|
||||
cb: (files: File[]) => void,
|
||||
tooLarge: () => void,
|
||||
multiple?: boolean,
|
||||
) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = multiple ?? false;
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const files = (e.currentTarget as HTMLInputElement)?.files;
|
||||
if (!files) return;
|
||||
for (let file of files) {
|
||||
if (file.size > maxFileSize) {
|
||||
return tooLarge();
|
||||
}
|
||||
}
|
||||
input.onchange = async (e) => {
|
||||
const files = (e.currentTarget as HTMLInputElement)?.files;
|
||||
if (!files) return;
|
||||
for (let file of files) {
|
||||
if (file.size > maxFileSize) {
|
||||
return tooLarge();
|
||||
}
|
||||
}
|
||||
|
||||
cb(Array.from(files));
|
||||
};
|
||||
cb(Array.from(files));
|
||||
};
|
||||
|
||||
input.click();
|
||||
input.click();
|
||||
}
|
||||
|
||||
export function FileUploader(props: Props) {
|
||||
const { fileType, maxFileSize, remove } = props;
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const { fileType, maxFileSize, remove } = props;
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [ uploading, setUploading ] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
function onClick() {
|
||||
if (uploading) return;
|
||||
function onClick() {
|
||||
if (uploading) return;
|
||||
|
||||
grabFiles(maxFileSize, async files => {
|
||||
setUploading(true);
|
||||
grabFiles(
|
||||
maxFileSize,
|
||||
async (files) => {
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
if (props.behaviour === 'multi') {
|
||||
props.onChange(files);
|
||||
} else if (props.behaviour === 'ask') {
|
||||
props.onChange(files[0]);
|
||||
} else {
|
||||
await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, files[0]));
|
||||
}
|
||||
} catch (err) {
|
||||
return openScreen({ id: "error", error: takeError(err) });
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, () =>
|
||||
openScreen({ id: "error", error: "FileTooLarge" }),
|
||||
props.behaviour === 'multi');
|
||||
}
|
||||
try {
|
||||
if (props.behaviour === "multi") {
|
||||
props.onChange(files);
|
||||
} else if (props.behaviour === "ask") {
|
||||
props.onChange(files[0]);
|
||||
} else {
|
||||
await props.onUpload(
|
||||
await uploadFile(
|
||||
client.configuration!.features.autumn.url,
|
||||
fileType,
|
||||
files[0],
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
return openScreen({ id: "error", error: takeError(err) });
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
},
|
||||
() => openScreen({ id: "error", error: "FileTooLarge" }),
|
||||
props.behaviour === "multi",
|
||||
);
|
||||
}
|
||||
|
||||
function removeOrUpload() {
|
||||
if (uploading) return;
|
||||
function removeOrUpload() {
|
||||
if (uploading) return;
|
||||
|
||||
if (props.style === 'attachment') {
|
||||
if (props.attached) {
|
||||
props.remove();
|
||||
} else {
|
||||
onClick();
|
||||
}
|
||||
} else {
|
||||
if (props.previewURL) {
|
||||
props.remove();
|
||||
} else {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.style === "attachment") {
|
||||
if (props.attached) {
|
||||
props.remove();
|
||||
} else {
|
||||
onClick();
|
||||
}
|
||||
} else {
|
||||
if (props.previewURL) {
|
||||
props.remove();
|
||||
} else {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.behaviour === 'multi' && props.append) {
|
||||
useEffect(() => {
|
||||
// File pasting.
|
||||
function paste(e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (typeof items === "undefined") return;
|
||||
if (props.behaviour !== 'multi' || !props.append) return;
|
||||
if (props.behaviour === "multi" && props.append) {
|
||||
useEffect(() => {
|
||||
// File pasting.
|
||||
function paste(e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (typeof items === "undefined") return;
|
||||
if (props.behaviour !== "multi" || !props.append) return;
|
||||
|
||||
let files = [];
|
||||
for (const item of items) {
|
||||
if (!item.type.startsWith("text/")) {
|
||||
const blob = item.getAsFile();
|
||||
if (blob) {
|
||||
if (blob.size > props.maxFileSize) {
|
||||
openScreen({ id: 'error', error: 'FileTooLarge' });
|
||||
}
|
||||
let files = [];
|
||||
for (const item of items) {
|
||||
if (!item.type.startsWith("text/")) {
|
||||
const blob = item.getAsFile();
|
||||
if (blob) {
|
||||
if (blob.size > props.maxFileSize) {
|
||||
openScreen({
|
||||
id: "error",
|
||||
error: "FileTooLarge",
|
||||
});
|
||||
}
|
||||
|
||||
files.push(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.push(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
props.append(files);
|
||||
}
|
||||
props.append(files);
|
||||
}
|
||||
|
||||
// Let the browser know we can drop files.
|
||||
function dragover(e: DragEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
// Let the browser know we can drop files.
|
||||
function dragover(e: DragEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
// File dropping.
|
||||
function drop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
if (props.behaviour !== 'multi' || !props.append) return;
|
||||
// File dropping.
|
||||
function drop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
if (props.behaviour !== "multi" || !props.append) return;
|
||||
|
||||
const dropped = e.dataTransfer?.files;
|
||||
if (dropped) {
|
||||
let files = [];
|
||||
for (const item of dropped) {
|
||||
if (item.size > props.maxFileSize) {
|
||||
openScreen({ id: 'error', error: 'FileTooLarge' });
|
||||
}
|
||||
const dropped = e.dataTransfer?.files;
|
||||
if (dropped) {
|
||||
let files = [];
|
||||
for (const item of dropped) {
|
||||
if (item.size > props.maxFileSize) {
|
||||
openScreen({ id: "error", error: "FileTooLarge" });
|
||||
}
|
||||
|
||||
files.push(item);
|
||||
}
|
||||
files.push(item);
|
||||
}
|
||||
|
||||
props.append(files);
|
||||
}
|
||||
}
|
||||
props.append(files);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("paste", paste);
|
||||
document.addEventListener("dragover", dragover);
|
||||
document.addEventListener("drop", drop);
|
||||
document.addEventListener("paste", paste);
|
||||
document.addEventListener("dragover", dragover);
|
||||
document.addEventListener("drop", drop);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("paste", paste);
|
||||
document.removeEventListener("dragover", dragover);
|
||||
document.removeEventListener("drop", drop);
|
||||
};
|
||||
}, [ props.append ]);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("paste", paste);
|
||||
document.removeEventListener("dragover", dragover);
|
||||
document.removeEventListener("drop", drop);
|
||||
};
|
||||
}, [props.append]);
|
||||
}
|
||||
|
||||
if (props.style === 'icon' || props.style === 'banner') {
|
||||
const { style, previewURL, defaultPreview, width, height } = props;
|
||||
return (
|
||||
<div className={classNames(styles.uploader,
|
||||
{ [styles.icon]: style === 'icon',
|
||||
[styles.banner]: style === 'banner' })}
|
||||
data-uploading={uploading}>
|
||||
<div className={styles.image}
|
||||
style={{ backgroundImage:
|
||||
style === 'icon' ? `url('${previewURL ?? defaultPreview}')` :
|
||||
(previewURL ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` : 'black'),
|
||||
width, height
|
||||
}}
|
||||
onClick={onClick}>
|
||||
{ uploading ?
|
||||
<div className={styles.uploading}>
|
||||
<Preloader type="ring" />
|
||||
</div> :
|
||||
<div className={styles.edit}>
|
||||
<Pencil size={30} />
|
||||
</div> }
|
||||
</div>
|
||||
<div className={styles.modify}>
|
||||
<span onClick={removeOrUpload}>{
|
||||
uploading ? <Text id="app.main.channel.uploading_file" /> :
|
||||
props.previewURL ? <Text id="app.settings.actions.remove" /> :
|
||||
<Text id="app.settings.actions.upload" /> }</span>
|
||||
<span className={styles.small}><Text id="app.settings.actions.max_filesize" fields={{ filesize: determineFileSize(maxFileSize) }} /></span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else if (props.style === 'attachment') {
|
||||
const { attached, uploading, cancel, size } = props;
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (uploading) return cancel();
|
||||
if (attached) return remove();
|
||||
onClick();
|
||||
}}>
|
||||
{ uploading ? <XCircle size={size} /> : attached ? <X size={size} /> : <Plus size={size} />}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
if (props.style === "icon" || props.style === "banner") {
|
||||
const { style, previewURL, defaultPreview, width, height } = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.uploader, {
|
||||
[styles.icon]: style === "icon",
|
||||
[styles.banner]: style === "banner",
|
||||
})}
|
||||
data-uploading={uploading}>
|
||||
<div
|
||||
className={styles.image}
|
||||
style={{
|
||||
backgroundImage:
|
||||
style === "icon"
|
||||
? `url('${previewURL ?? defaultPreview}')`
|
||||
: previewURL
|
||||
? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
|
||||
: "black",
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
onClick={onClick}>
|
||||
{uploading ? (
|
||||
<div className={styles.uploading}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.edit}>
|
||||
<Pencil size={30} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.modify}>
|
||||
<span onClick={removeOrUpload}>
|
||||
{uploading ? (
|
||||
<Text id="app.main.channel.uploading_file" />
|
||||
) : props.previewURL ? (
|
||||
<Text id="app.settings.actions.remove" />
|
||||
) : (
|
||||
<Text id="app.settings.actions.upload" />
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.small}>
|
||||
<Text
|
||||
id="app.settings.actions.max_filesize"
|
||||
fields={{
|
||||
filesize: determineFileSize(maxFileSize),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (props.style === "attachment") {
|
||||
const { attached, uploading, cancel, size } = props;
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (uploading) return cancel();
|
||||
if (attached) return remove();
|
||||
onClick();
|
||||
}}>
|
||||
{uploading ? (
|
||||
<XCircle size={size} />
|
||||
) : attached ? (
|
||||
<X size={size} />
|
||||
) : (
|
||||
<Plus size={size} />
|
||||
)}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,247 +1,286 @@
|
||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
|
||||
import { Users } from "revolt.js/dist/api/objects";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { useTranslation } from "../../lib/i18n";
|
||||
|
||||
import { connectState } from "../../redux/connector";
|
||||
import {
|
||||
getNotificationState,
|
||||
Notifications,
|
||||
shouldNotify,
|
||||
} from "../../redux/reducers/notifications";
|
||||
import { NotificationOptions } from "../../redux/reducers/settings";
|
||||
|
||||
import { SoundContext } from "../Settings";
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import { useTranslation } from "../../lib/i18n";
|
||||
import { Users } from "revolt.js/dist/api/objects";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
|
||||
import { NotificationOptions } from "../../redux/reducers/settings";
|
||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||
import { getNotificationState, Notifications, shouldNotify } from "../../redux/reducers/notifications";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
notifs: Notifications;
|
||||
options?: NotificationOptions;
|
||||
notifs: Notifications;
|
||||
}
|
||||
|
||||
const notifications: { [key: string]: Notification } = {};
|
||||
|
||||
async function createNotification(title: string, options: globalThis.NotificationOptions) {
|
||||
try {
|
||||
return new Notification(title, options);
|
||||
} catch (err) {
|
||||
let sw = await navigator.serviceWorker.getRegistration();
|
||||
sw?.showNotification(title, options);
|
||||
}
|
||||
async function createNotification(
|
||||
title: string,
|
||||
options: globalThis.NotificationOptions,
|
||||
) {
|
||||
try {
|
||||
return new Notification(title, options);
|
||||
} catch (err) {
|
||||
let sw = await navigator.serviceWorker.getRegistration();
|
||||
sw?.showNotification(title, options);
|
||||
}
|
||||
}
|
||||
|
||||
function Notifier({ options, notifs }: Props) {
|
||||
const translate = useTranslation();
|
||||
const showNotification = options?.desktopEnabled ?? false;
|
||||
const translate = useTranslation();
|
||||
const showNotification = options?.desktopEnabled ?? false;
|
||||
|
||||
const client = useContext(AppContext);
|
||||
const { guild: guild_id, channel: channel_id } = useParams<{
|
||||
guild: string;
|
||||
channel: string;
|
||||
}>();
|
||||
const history = useHistory();
|
||||
const playSound = useContext(SoundContext);
|
||||
const client = useContext(AppContext);
|
||||
const { guild: guild_id, channel: channel_id } = useParams<{
|
||||
guild: string;
|
||||
channel: string;
|
||||
}>();
|
||||
const history = useHistory();
|
||||
const playSound = useContext(SoundContext);
|
||||
|
||||
async function message(msg: Message) {
|
||||
if (msg.author === client.user!._id) return;
|
||||
if (msg.channel === channel_id && document.hasFocus()) return;
|
||||
if (client.user!.status?.presence === Users.Presence.Busy) return;
|
||||
async function message(msg: Message) {
|
||||
if (msg.author === client.user!._id) return;
|
||||
if (msg.channel === channel_id && document.hasFocus()) return;
|
||||
if (client.user!.status?.presence === Users.Presence.Busy) return;
|
||||
|
||||
const channel = client.channels.get(msg.channel);
|
||||
const author = client.users.get(msg.author);
|
||||
if (!channel) return;
|
||||
if (author?.relationship === Users.Relationship.Blocked) return;
|
||||
const channel = client.channels.get(msg.channel);
|
||||
const author = client.users.get(msg.author);
|
||||
if (!channel) return;
|
||||
if (author?.relationship === Users.Relationship.Blocked) return;
|
||||
|
||||
const notifState = getNotificationState(notifs, channel);
|
||||
if (!shouldNotify(notifState, msg, client.user!._id)) return;
|
||||
const notifState = getNotificationState(notifs, channel);
|
||||
if (!shouldNotify(notifState, msg, client.user!._id)) return;
|
||||
|
||||
playSound('message');
|
||||
if (!showNotification) return;
|
||||
playSound("message");
|
||||
if (!showNotification) return;
|
||||
|
||||
let title;
|
||||
switch (channel.channel_type) {
|
||||
case "SavedMessages":
|
||||
return;
|
||||
case "DirectMessage":
|
||||
title = `@${author?.username}`;
|
||||
break;
|
||||
case "Group":
|
||||
if (author?._id === SYSTEM_USER_ID) {
|
||||
title = channel.name;
|
||||
} else {
|
||||
title = `@${author?.username} - ${channel.name}`;
|
||||
}
|
||||
break;
|
||||
case "TextChannel":
|
||||
const server = client.servers.get(channel.server);
|
||||
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
|
||||
break;
|
||||
default:
|
||||
title = msg.channel;
|
||||
break;
|
||||
}
|
||||
let title;
|
||||
switch (channel.channel_type) {
|
||||
case "SavedMessages":
|
||||
return;
|
||||
case "DirectMessage":
|
||||
title = `@${author?.username}`;
|
||||
break;
|
||||
case "Group":
|
||||
if (author?._id === SYSTEM_USER_ID) {
|
||||
title = channel.name;
|
||||
} else {
|
||||
title = `@${author?.username} - ${channel.name}`;
|
||||
}
|
||||
break;
|
||||
case "TextChannel":
|
||||
const server = client.servers.get(channel.server);
|
||||
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
|
||||
break;
|
||||
default:
|
||||
title = msg.channel;
|
||||
break;
|
||||
}
|
||||
|
||||
let image;
|
||||
if (msg.attachments) {
|
||||
let imageAttachment = msg.attachments.find(x => x.metadata.type === 'Image');
|
||||
if (imageAttachment) {
|
||||
image = client.generateFileURL(imageAttachment, { max_side: 720 });
|
||||
}
|
||||
}
|
||||
let image;
|
||||
if (msg.attachments) {
|
||||
let imageAttachment = msg.attachments.find(
|
||||
(x) => x.metadata.type === "Image",
|
||||
);
|
||||
if (imageAttachment) {
|
||||
image = client.generateFileURL(imageAttachment, {
|
||||
max_side: 720,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let body, icon;
|
||||
if (typeof msg.content === "string") {
|
||||
body = client.markdownToText(msg.content);
|
||||
icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
|
||||
} else {
|
||||
let users = client.users;
|
||||
switch (msg.content.type) {
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
body = translate(
|
||||
`app.main.channel.system.${msg.content.type === 'user_added' ? 'added_by' : 'removed_by'}`,
|
||||
{ user: users.get(msg.content.id)?.username, other_user: users.get(msg.content.by)?.username }
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
|
||||
break;
|
||||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
body = translate(
|
||||
`app.main.channel.system.${msg.content.type}`,
|
||||
{ user: users.get(msg.content.id)?.username }
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
|
||||
break;
|
||||
case "channel_renamed":
|
||||
body = translate(
|
||||
`app.main.channel.system.channel_renamed`,
|
||||
{ user: users.get(msg.content.by)?.username, name: msg.content.name }
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
|
||||
break;
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
body = translate(
|
||||
`app.main.channel.system.${msg.content.type}`,
|
||||
{ user: users.get(msg.content.by)?.username }
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
|
||||
break;
|
||||
}
|
||||
}
|
||||
let body, icon;
|
||||
if (typeof msg.content === "string") {
|
||||
body = client.markdownToText(msg.content);
|
||||
icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
|
||||
} else {
|
||||
let users = client.users;
|
||||
switch (msg.content.type) {
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
body = translate(
|
||||
`app.main.channel.system.${
|
||||
msg.content.type === "user_added"
|
||||
? "added_by"
|
||||
: "removed_by"
|
||||
}`,
|
||||
{
|
||||
user: users.get(msg.content.id)?.username,
|
||||
other_user: users.get(msg.content.by)?.username,
|
||||
},
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.id, {
|
||||
max_side: 256,
|
||||
});
|
||||
break;
|
||||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
body = translate(
|
||||
`app.main.channel.system.${msg.content.type}`,
|
||||
{ user: users.get(msg.content.id)?.username },
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.id, {
|
||||
max_side: 256,
|
||||
});
|
||||
break;
|
||||
case "channel_renamed":
|
||||
body = translate(
|
||||
`app.main.channel.system.channel_renamed`,
|
||||
{
|
||||
user: users.get(msg.content.by)?.username,
|
||||
name: msg.content.name,
|
||||
},
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.by, {
|
||||
max_side: 256,
|
||||
});
|
||||
break;
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
body = translate(
|
||||
`app.main.channel.system.${msg.content.type}`,
|
||||
{ user: users.get(msg.content.by)?.username },
|
||||
);
|
||||
icon = client.users.getAvatarURL(msg.content.by, {
|
||||
max_side: 256,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let notif = await createNotification(title, {
|
||||
icon,
|
||||
image,
|
||||
body,
|
||||
timestamp: decodeTime(msg._id),
|
||||
tag: msg.channel,
|
||||
badge: '/assets/icons/android-chrome-512x512.png',
|
||||
silent: true
|
||||
});
|
||||
let notif = await createNotification(title, {
|
||||
icon,
|
||||
image,
|
||||
body,
|
||||
timestamp: decodeTime(msg._id),
|
||||
tag: msg.channel,
|
||||
badge: "/assets/icons/android-chrome-512x512.png",
|
||||
silent: true,
|
||||
});
|
||||
|
||||
if (notif) {
|
||||
notif.addEventListener("click", () => {
|
||||
window.focus();
|
||||
const id = msg.channel;
|
||||
if (id !== channel_id) {
|
||||
let channel = client.channels.get(id);
|
||||
if (channel) {
|
||||
if (channel.channel_type === 'TextChannel') {
|
||||
history.push(`/server/${channel.server}/channel/${id}`);
|
||||
} else {
|
||||
history.push(`/channel/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (notif) {
|
||||
notif.addEventListener("click", () => {
|
||||
window.focus();
|
||||
const id = msg.channel;
|
||||
if (id !== channel_id) {
|
||||
let channel = client.channels.get(id);
|
||||
if (channel) {
|
||||
if (channel.channel_type === "TextChannel") {
|
||||
history.push(
|
||||
`/server/${channel.server}/channel/${id}`,
|
||||
);
|
||||
} else {
|
||||
history.push(`/channel/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notifications[msg.channel] = notif;
|
||||
notif.addEventListener(
|
||||
"close",
|
||||
() => delete notifications[msg.channel]
|
||||
);
|
||||
}
|
||||
}
|
||||
notifications[msg.channel] = notif;
|
||||
notif.addEventListener(
|
||||
"close",
|
||||
() => delete notifications[msg.channel],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function relationship(user: User, property: string) {
|
||||
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
||||
if (property !== "relationship") return;
|
||||
if (!showNotification) return;
|
||||
async function relationship(user: User, property: string) {
|
||||
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
||||
if (property !== "relationship") return;
|
||||
if (!showNotification) return;
|
||||
|
||||
let event;
|
||||
switch (user.relationship) {
|
||||
case Users.Relationship.Incoming:
|
||||
event = translate("notifications.sent_request", { person: user.username });
|
||||
break;
|
||||
case Users.Relationship.Friend:
|
||||
event = translate("notifications.now_friends", { person: user.username });
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
let event;
|
||||
switch (user.relationship) {
|
||||
case Users.Relationship.Incoming:
|
||||
event = translate("notifications.sent_request", {
|
||||
person: user.username,
|
||||
});
|
||||
break;
|
||||
case Users.Relationship.Friend:
|
||||
event = translate("notifications.now_friends", {
|
||||
person: user.username,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
let notif = await createNotification(event, {
|
||||
icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
|
||||
badge: '/assets/icons/android-chrome-512x512.png',
|
||||
timestamp: +new Date()
|
||||
});
|
||||
let notif = await createNotification(event, {
|
||||
icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
|
||||
badge: "/assets/icons/android-chrome-512x512.png",
|
||||
timestamp: +new Date(),
|
||||
});
|
||||
|
||||
notif?.addEventListener("click", () => {
|
||||
history.push(`/friends`);
|
||||
});
|
||||
}
|
||||
notif?.addEventListener("click", () => {
|
||||
history.push(`/friends`);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
client.addListener("message", message);
|
||||
client.users.addListener("mutation", relationship);
|
||||
useEffect(() => {
|
||||
client.addListener("message", message);
|
||||
client.users.addListener("mutation", relationship);
|
||||
|
||||
return () => {
|
||||
client.removeListener("message", message);
|
||||
client.users.removeListener("mutation", relationship);
|
||||
};
|
||||
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
|
||||
return () => {
|
||||
client.removeListener("message", message);
|
||||
client.users.removeListener("mutation", relationship);
|
||||
};
|
||||
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
|
||||
|
||||
useEffect(() => {
|
||||
function visChange() {
|
||||
if (document.visibilityState === "visible") {
|
||||
if (notifications[channel_id]) {
|
||||
notifications[channel_id].close();
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
function visChange() {
|
||||
if (document.visibilityState === "visible") {
|
||||
if (notifications[channel_id]) {
|
||||
notifications[channel_id].close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visChange();
|
||||
visChange();
|
||||
|
||||
document.addEventListener("visibilitychange", visChange);
|
||||
return () =>
|
||||
document.removeEventListener("visibilitychange", visChange);
|
||||
}, [guild_id, channel_id]);
|
||||
document.addEventListener("visibilitychange", visChange);
|
||||
return () =>
|
||||
document.removeEventListener("visibilitychange", visChange);
|
||||
}, [guild_id, channel_id]);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const NotifierComponent = connectState(
|
||||
Notifier,
|
||||
state => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
notifs: state.notifications
|
||||
};
|
||||
},
|
||||
true
|
||||
Notifier,
|
||||
(state) => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
notifs: state.notifications,
|
||||
};
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
export default function Notifications() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/server/:server/channel/:channel">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
<Route path="/channel/:channel">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
export default function NotificationsComponent() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/server/:server/channel/:channel">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
<Route path="/channel/:channel">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<NotifierComponent />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styled from "styled-components";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { WifiOff } from "@styled-icons/boxicons-regular";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import { ClientStatus, StatusContext } from "./RevoltClient";
|
||||
|
||||
interface Props {
|
||||
children: Children;
|
||||
children: Children;
|
||||
}
|
||||
|
||||
const Base = styled.div`
|
||||
gap: 16px;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
color: var(--tertiary-foreground);
|
||||
background: var(--secondary-header);
|
||||
gap: 16px;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
color: var(--tertiary-foreground);
|
||||
background: var(--secondary-header);
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
}
|
||||
> div {
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function RequiresOnline(props: Props) {
|
||||
const status = useContext(StatusContext);
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
|
||||
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
|
||||
return (
|
||||
<Base>
|
||||
<WifiOff size={16} />
|
||||
<div>
|
||||
<Text id="app.special.requires_online" />
|
||||
</div>
|
||||
</Base>
|
||||
);
|
||||
if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
|
||||
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
|
||||
return (
|
||||
<Base>
|
||||
<WifiOff size={16} />
|
||||
<div>
|
||||
<Text id="app.special.requires_online" />
|
||||
</div>
|
||||
</Base>
|
||||
);
|
||||
|
||||
return <>{ props.children }</>;
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import { openDB } from 'idb';
|
||||
import { openDB } from "idb";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Client } from "revolt.js";
|
||||
import { takeError } from "./util";
|
||||
import { createContext } from "preact";
|
||||
import { dispatch } from '../../redux';
|
||||
import { Children } from "../../types/Preact";
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
import { AuthState } from "../../redux/reducers/auth";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { useIntermediate } from '../intermediate/Intermediate';
|
||||
|
||||
import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { AuthState } from "../../redux/reducers/auth";
|
||||
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import { useIntermediate } from "../intermediate/Intermediate";
|
||||
import { registerEvents, setReconnectDisallowed } from "./events";
|
||||
import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
|
||||
import { takeError } from "./util";
|
||||
|
||||
export enum ClientStatus {
|
||||
INIT,
|
||||
LOADING,
|
||||
READY,
|
||||
OFFLINE,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
RECONNECTING,
|
||||
ONLINE,
|
||||
INIT,
|
||||
LOADING,
|
||||
READY,
|
||||
OFFLINE,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
RECONNECTING,
|
||||
ONLINE,
|
||||
}
|
||||
|
||||
export interface ClientOperations {
|
||||
login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
|
||||
logout: (shouldRequest?: boolean) => Promise<void>;
|
||||
loggedIn: () => boolean;
|
||||
ready: () => boolean;
|
||||
login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
|
||||
logout: (shouldRequest?: boolean) => Promise<void>;
|
||||
loggedIn: () => boolean;
|
||||
ready: () => boolean;
|
||||
|
||||
openDM: (user_id: string) => Promise<string>;
|
||||
openDM: (user_id: string) => Promise<string>;
|
||||
}
|
||||
|
||||
// By the time they are used, they should all be initialized.
|
||||
@@ -42,189 +47,195 @@ export const StatusContext = createContext<ClientStatus>(null!);
|
||||
export const OperationsContext = createContext<ClientOperations>(null!);
|
||||
|
||||
type Props = {
|
||||
auth: AuthState;
|
||||
children: Children;
|
||||
auth: AuthState;
|
||||
children: Children;
|
||||
};
|
||||
|
||||
function Context({ auth, children }: Props) {
|
||||
const history = useHistory();
|
||||
const { openScreen } = useIntermediate();
|
||||
const [status, setStatus] = useState(ClientStatus.INIT);
|
||||
const [client, setClient] = useState<Client>(undefined as unknown as Client);
|
||||
const history = useHistory();
|
||||
const { openScreen } = useIntermediate();
|
||||
const [status, setStatus] = useState(ClientStatus.INIT);
|
||||
const [client, setClient] = useState<Client>(
|
||||
undefined as unknown as Client,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let db;
|
||||
try {
|
||||
// Match sw.ts#L23
|
||||
db = await openDB('state', 3, {
|
||||
upgrade(db) {
|
||||
for (let store of [ "channels", "servers", "users", "members" ]) {
|
||||
db.createObjectStore(store, {
|
||||
keyPath: '_id'
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to open IndexedDB store, continuing without.');
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let db;
|
||||
try {
|
||||
// Match sw.ts#L23
|
||||
db = await openDB("state", 3, {
|
||||
upgrade(db) {
|
||||
for (let store of [
|
||||
"channels",
|
||||
"servers",
|
||||
"users",
|
||||
"members",
|
||||
]) {
|
||||
db.createObjectStore(store, {
|
||||
keyPath: "_id",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Failed to open IndexedDB store, continuing without.",
|
||||
);
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
autoReconnect: false,
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
debug: import.meta.env.DEV,
|
||||
db
|
||||
});
|
||||
const client = new Client({
|
||||
autoReconnect: false,
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
debug: import.meta.env.DEV,
|
||||
db,
|
||||
});
|
||||
|
||||
setClient(client);
|
||||
SingletonMessageRenderer.subscribe(client);
|
||||
setStatus(ClientStatus.LOADING);
|
||||
})();
|
||||
}, [ ]);
|
||||
setClient(client);
|
||||
SingletonMessageRenderer.subscribe(client);
|
||||
setStatus(ClientStatus.LOADING);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (status === ClientStatus.INIT) return null;
|
||||
if (status === ClientStatus.INIT) return null;
|
||||
|
||||
const operations: ClientOperations = useMemo(() => {
|
||||
return {
|
||||
login: async data => {
|
||||
setReconnectDisallowed(true);
|
||||
const operations: ClientOperations = useMemo(() => {
|
||||
return {
|
||||
login: async (data) => {
|
||||
setReconnectDisallowed(true);
|
||||
|
||||
try {
|
||||
const onboarding = await client.login(data);
|
||||
setReconnectDisallowed(false);
|
||||
const login = () =>
|
||||
dispatch({
|
||||
type: "LOGIN",
|
||||
session: client.session! // This [null assertion] is ok, we should have a session by now. - insert's words
|
||||
});
|
||||
try {
|
||||
const onboarding = await client.login(data);
|
||||
setReconnectDisallowed(false);
|
||||
const login = () =>
|
||||
dispatch({
|
||||
type: "LOGIN",
|
||||
session: client.session!, // This [null assertion] is ok, we should have a session by now. - insert's words
|
||||
});
|
||||
|
||||
if (onboarding) {
|
||||
openScreen({
|
||||
id: "onboarding",
|
||||
callback: (username: string) =>
|
||||
onboarding(username, true).then(login)
|
||||
});
|
||||
} else {
|
||||
login();
|
||||
}
|
||||
} catch (err) {
|
||||
setReconnectDisallowed(false);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
logout: async shouldRequest => {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
if (onboarding) {
|
||||
openScreen({
|
||||
id: "onboarding",
|
||||
callback: (username: string) =>
|
||||
onboarding(username, true).then(login),
|
||||
});
|
||||
} else {
|
||||
login();
|
||||
}
|
||||
} catch (err) {
|
||||
setReconnectDisallowed(false);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
logout: async (shouldRequest) => {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
|
||||
client.reset();
|
||||
dispatch({ type: "RESET" });
|
||||
client.reset();
|
||||
dispatch({ type: "RESET" });
|
||||
|
||||
openScreen({ id: "none" });
|
||||
setStatus(ClientStatus.READY);
|
||||
openScreen({ id: "none" });
|
||||
setStatus(ClientStatus.READY);
|
||||
|
||||
client.websocket.disconnect();
|
||||
client.websocket.disconnect();
|
||||
|
||||
if (shouldRequest) {
|
||||
try {
|
||||
await client.logout();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
loggedIn: () => typeof auth.active !== "undefined",
|
||||
ready: () => (
|
||||
operations.loggedIn() &&
|
||||
typeof client.user !== "undefined"
|
||||
),
|
||||
openDM: async (user_id: string) => {
|
||||
let channel = await client.users.openDM(user_id);
|
||||
history.push(`/channel/${channel!._id}`);
|
||||
return channel!._id;
|
||||
}
|
||||
}
|
||||
}, [ client, auth.active ]);
|
||||
if (shouldRequest) {
|
||||
try {
|
||||
await client.logout();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
loggedIn: () => typeof auth.active !== "undefined",
|
||||
ready: () =>
|
||||
operations.loggedIn() && typeof client.user !== "undefined",
|
||||
openDM: async (user_id: string) => {
|
||||
let channel = await client.users.openDM(user_id);
|
||||
history.push(`/channel/${channel!._id}`);
|
||||
return channel!._id;
|
||||
},
|
||||
};
|
||||
}, [client, auth.active]);
|
||||
|
||||
useEffect(() => registerEvents({ operations }, setStatus, client), [ client ]);
|
||||
useEffect(
|
||||
() => registerEvents({ operations }, setStatus, client),
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (client.db) {
|
||||
await client.restore();
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (client.db) {
|
||||
await client.restore();
|
||||
}
|
||||
|
||||
if (auth.active) {
|
||||
dispatch({ type: "QUEUE_FAIL_ALL" });
|
||||
if (auth.active) {
|
||||
dispatch({ type: "QUEUE_FAIL_ALL" });
|
||||
|
||||
const active = auth.accounts[auth.active];
|
||||
client.user = client.users.get(active.session.user_id);
|
||||
if (!navigator.onLine) {
|
||||
return setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
const active = auth.accounts[auth.active];
|
||||
client.user = client.users.get(active.session.user_id);
|
||||
if (!navigator.onLine) {
|
||||
return setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
|
||||
if (operations.ready())
|
||||
setStatus(ClientStatus.CONNECTING);
|
||||
|
||||
if (navigator.onLine) {
|
||||
await client
|
||||
.fetchConfiguration()
|
||||
.catch(() =>
|
||||
console.error("Failed to connect to API server.")
|
||||
);
|
||||
}
|
||||
if (operations.ready()) setStatus(ClientStatus.CONNECTING);
|
||||
|
||||
try {
|
||||
await client.fetchConfiguration();
|
||||
const callback = await client.useExistingSession(
|
||||
active.session
|
||||
);
|
||||
if (navigator.onLine) {
|
||||
await client
|
||||
.fetchConfiguration()
|
||||
.catch(() =>
|
||||
console.error("Failed to connect to API server."),
|
||||
);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
openScreen({ id: "onboarding", callback });
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
const error = takeError(err);
|
||||
if (error === "Forbidden" || error === "Unauthorized") {
|
||||
operations.logout(true);
|
||||
openScreen({ id: "signed_out" });
|
||||
} else {
|
||||
openScreen({ id: "error", error });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await client.fetchConfiguration()
|
||||
} catch (err) {
|
||||
console.error("Failed to connect to API server.");
|
||||
}
|
||||
try {
|
||||
await client.fetchConfiguration();
|
||||
const callback = await client.useExistingSession(
|
||||
active.session,
|
||||
);
|
||||
|
||||
setStatus(ClientStatus.READY);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
if (callback) {
|
||||
openScreen({ id: "onboarding", callback });
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
const error = takeError(err);
|
||||
if (error === "Forbidden" || error === "Unauthorized") {
|
||||
operations.logout(true);
|
||||
openScreen({ id: "signed_out" });
|
||||
} else {
|
||||
openScreen({ id: "error", error });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await client.fetchConfiguration();
|
||||
} catch (err) {
|
||||
console.error("Failed to connect to API server.");
|
||||
}
|
||||
|
||||
if (status === ClientStatus.LOADING) {
|
||||
return <Preloader type="spinner" />;
|
||||
}
|
||||
setStatus(ClientStatus.READY);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={client}>
|
||||
<StatusContext.Provider value={status}>
|
||||
<OperationsContext.Provider value={operations}>
|
||||
{ children }
|
||||
</OperationsContext.Provider>
|
||||
</StatusContext.Provider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
if (status === ClientStatus.LOADING) {
|
||||
return <Preloader type="spinner" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={client}>
|
||||
<StatusContext.Provider value={status}>
|
||||
<OperationsContext.Provider value={operations}>
|
||||
{children}
|
||||
</OperationsContext.Provider>
|
||||
</StatusContext.Provider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState<{ children: Children }>(
|
||||
Context,
|
||||
state => {
|
||||
return {
|
||||
auth: state.auth,
|
||||
sync: state.sync
|
||||
};
|
||||
}
|
||||
);
|
||||
export default connectState<{ children: Children }>(Context, (state) => {
|
||||
return {
|
||||
auth: state.auth,
|
||||
sync: state.sync,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,77 +1,76 @@
|
||||
/**
|
||||
* This file monitors the message cache to delete any queued messages that have already sent.
|
||||
*/
|
||||
|
||||
import { Message } from "revolt.js";
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import { Typing } from "../../redux/reducers/typing";
|
||||
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { QueuedMessage } from "../../redux/reducers/queue";
|
||||
import { dispatch } from "../../redux";
|
||||
import { Typing } from "../../redux/reducers/typing";
|
||||
|
||||
import { AppContext } from "./RevoltClient";
|
||||
|
||||
type Props = {
|
||||
messages: QueuedMessage[];
|
||||
typing: Typing
|
||||
messages: QueuedMessage[];
|
||||
typing: Typing;
|
||||
};
|
||||
|
||||
function StateMonitor(props: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'QUEUE_DROP_ALL'
|
||||
});
|
||||
}, [ ]);
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: "QUEUE_DROP_ALL",
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function add(msg: Message) {
|
||||
if (!msg.nonce) return;
|
||||
if (!props.messages.find(x => x.id === msg.nonce)) return;
|
||||
useEffect(() => {
|
||||
function add(msg: Message) {
|
||||
if (!msg.nonce) return;
|
||||
if (!props.messages.find((x) => x.id === msg.nonce)) return;
|
||||
|
||||
dispatch({
|
||||
type: 'QUEUE_REMOVE',
|
||||
nonce: msg.nonce
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: "QUEUE_REMOVE",
|
||||
nonce: msg.nonce,
|
||||
});
|
||||
}
|
||||
|
||||
client.addListener('message', add);
|
||||
return () => client.removeListener('message', add);
|
||||
}, [ props.messages ]);
|
||||
client.addListener("message", add);
|
||||
return () => client.removeListener("message", add);
|
||||
}, [props.messages]);
|
||||
|
||||
useEffect(() => {
|
||||
function removeOld() {
|
||||
if (!props.typing) return;
|
||||
for (let channel of Object.keys(props.typing)) {
|
||||
let users = props.typing[channel];
|
||||
useEffect(() => {
|
||||
function removeOld() {
|
||||
if (!props.typing) return;
|
||||
for (let channel of Object.keys(props.typing)) {
|
||||
let users = props.typing[channel];
|
||||
|
||||
for (let user of users) {
|
||||
if (+ new Date() > user.started + 5000) {
|
||||
dispatch({
|
||||
type: 'TYPING_STOP',
|
||||
channel,
|
||||
user: user.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let user of users) {
|
||||
if (+new Date() > user.started + 5000) {
|
||||
dispatch({
|
||||
type: "TYPING_STOP",
|
||||
channel,
|
||||
user: user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeOld();
|
||||
removeOld();
|
||||
|
||||
let interval = setInterval(removeOld, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [ props.typing ]);
|
||||
let interval = setInterval(removeOld, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [props.typing]);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default connectState(
|
||||
StateMonitor,
|
||||
state => {
|
||||
return {
|
||||
messages: [...state.queue],
|
||||
typing: state.typing
|
||||
};
|
||||
}
|
||||
);
|
||||
export default connectState(StateMonitor, (state) => {
|
||||
return {
|
||||
messages: [...state.queue],
|
||||
typing: state.typing,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,126 +1,146 @@
|
||||
/**
|
||||
* This file monitors changes to settings and syncs them to the server.
|
||||
*/
|
||||
|
||||
import isEqual from "lodash.isequal";
|
||||
import { Language } from "../Locale";
|
||||
import { Sync } from "revolt.js/dist/api/objects";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { Settings } from "../../redux/reducers/settings";
|
||||
import { Notifications } from "../../redux/reducers/notifications";
|
||||
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../redux/reducers/sync";
|
||||
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { Notifications } from "../../redux/reducers/notifications";
|
||||
import { Settings } from "../../redux/reducers/settings";
|
||||
import {
|
||||
DEFAULT_ENABLED_SYNC,
|
||||
SyncData,
|
||||
SyncKeys,
|
||||
SyncOptions,
|
||||
} from "../../redux/reducers/sync";
|
||||
|
||||
import { Language } from "../Locale";
|
||||
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
|
||||
|
||||
type Props = {
|
||||
settings: Settings,
|
||||
locale: Language,
|
||||
sync: SyncOptions,
|
||||
notifications: Notifications
|
||||
settings: Settings;
|
||||
locale: Language;
|
||||
sync: SyncOptions;
|
||||
notifications: Notifications;
|
||||
};
|
||||
|
||||
var lastValues: { [key in SyncKeys]?: any } = { };
|
||||
var lastValues: { [key in SyncKeys]?: any } = {};
|
||||
|
||||
export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
|
||||
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
|
||||
for (let key of Object.keys(packet)) {
|
||||
let [ timestamp, obj ] = packet[key];
|
||||
if (timestamp < (revision ?? {})[key] ?? 0) {
|
||||
continue;
|
||||
}
|
||||
export function mapSync(
|
||||
packet: Sync.UserSettings,
|
||||
revision?: Record<string, number>,
|
||||
) {
|
||||
let update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
|
||||
for (let key of Object.keys(packet)) {
|
||||
let [timestamp, obj] = packet[key];
|
||||
if (timestamp < (revision ?? {})[key] ?? 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let object;
|
||||
if (obj[0] === '{') {
|
||||
object = JSON.parse(obj)
|
||||
} else {
|
||||
object = obj;
|
||||
}
|
||||
let object;
|
||||
if (obj[0] === "{") {
|
||||
object = JSON.parse(obj);
|
||||
} else {
|
||||
object = obj;
|
||||
}
|
||||
|
||||
lastValues[key as SyncKeys] = object;
|
||||
update[key as SyncKeys] = [ timestamp, object ];
|
||||
}
|
||||
lastValues[key as SyncKeys] = object;
|
||||
update[key as SyncKeys] = [timestamp, object];
|
||||
}
|
||||
|
||||
return update;
|
||||
return update;
|
||||
}
|
||||
|
||||
function SyncManager(props: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === ClientStatus.ONLINE) {
|
||||
client
|
||||
.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
|
||||
.then(data => {
|
||||
dispatch({
|
||||
type: 'SYNC_UPDATE',
|
||||
update: mapSync(data)
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
if (status === ClientStatus.ONLINE) {
|
||||
client
|
||||
.syncFetchSettings(
|
||||
DEFAULT_ENABLED_SYNC.filter(
|
||||
(x) => !props.sync?.disabled?.includes(x),
|
||||
),
|
||||
)
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: "SYNC_UPDATE",
|
||||
update: mapSync(data),
|
||||
});
|
||||
});
|
||||
|
||||
client
|
||||
.syncFetchUnreads()
|
||||
.then(unreads => dispatch({ type: 'UNREADS_SET', unreads }));
|
||||
}
|
||||
}, [ status ]);
|
||||
client
|
||||
.syncFetchUnreads()
|
||||
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
function syncChange(key: SyncKeys, data: any) {
|
||||
let timestamp = + new Date();
|
||||
dispatch({
|
||||
type: 'SYNC_SET_REVISION',
|
||||
key,
|
||||
timestamp
|
||||
});
|
||||
function syncChange(key: SyncKeys, data: any) {
|
||||
let timestamp = +new Date();
|
||||
dispatch({
|
||||
type: "SYNC_SET_REVISION",
|
||||
key,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
client.syncSetSettings({
|
||||
[key]: data
|
||||
}, timestamp);
|
||||
}
|
||||
client.syncSetSettings(
|
||||
{
|
||||
[key]: data,
|
||||
},
|
||||
timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
let disabled = props.sync.disabled ?? [];
|
||||
for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale], ['notifications', props.notifications] ] as [SyncKeys, any][]) {
|
||||
useEffect(() => {
|
||||
if (disabled.indexOf(key) === -1) {
|
||||
if (typeof lastValues[key] !== 'undefined') {
|
||||
if (!isEqual(lastValues[key], object)) {
|
||||
syncChange(key, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
let disabled = props.sync.disabled ?? [];
|
||||
for (let [key, object] of [
|
||||
["appearance", props.settings.appearance],
|
||||
["theme", props.settings.theme],
|
||||
["locale", props.locale],
|
||||
["notifications", props.notifications],
|
||||
] as [SyncKeys, any][]) {
|
||||
useEffect(() => {
|
||||
if (disabled.indexOf(key) === -1) {
|
||||
if (typeof lastValues[key] !== "undefined") {
|
||||
if (!isEqual(lastValues[key], object)) {
|
||||
syncChange(key, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastValues[key] = object;
|
||||
}, [ disabled, object ]);
|
||||
}
|
||||
lastValues[key] = object;
|
||||
}, [disabled, object]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onPacket(packet: ClientboundNotification) {
|
||||
if (packet.type === 'UserSettingsUpdate') {
|
||||
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = mapSync(packet.update, props.sync.revision);
|
||||
useEffect(() => {
|
||||
function onPacket(packet: ClientboundNotification) {
|
||||
if (packet.type === "UserSettingsUpdate") {
|
||||
let update: { [key in SyncKeys]?: [number, SyncData[key]] } =
|
||||
mapSync(packet.update, props.sync.revision);
|
||||
|
||||
dispatch({
|
||||
type: 'SYNC_UPDATE',
|
||||
update
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
type: "SYNC_UPDATE",
|
||||
update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
client.addListener('packet', onPacket);
|
||||
return () => client.removeListener('packet', onPacket);
|
||||
}, [ disabled, props.sync ]);
|
||||
client.addListener("packet", onPacket);
|
||||
return () => client.removeListener("packet", onPacket);
|
||||
}, [disabled, props.sync]);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default connectState(
|
||||
SyncManager,
|
||||
state => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
locale: state.locale,
|
||||
sync: state.sync,
|
||||
notifications: state.notifications
|
||||
};
|
||||
}
|
||||
);
|
||||
export default connectState(SyncManager, (state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
locale: state.locale,
|
||||
sync: state.sync,
|
||||
notifications: state.notifications,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,148 +1,155 @@
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
import { Client, Message } from "revolt.js/dist";
|
||||
import {
|
||||
ClientOperations,
|
||||
ClientStatus
|
||||
} from "./RevoltClient";
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
|
||||
import { StateUpdater } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
|
||||
import { ClientOperations, ClientStatus } from "./RevoltClient";
|
||||
|
||||
export var preventReconnect = false;
|
||||
let preventUntil = 0;
|
||||
|
||||
export function setReconnectDisallowed(allowed: boolean) {
|
||||
preventReconnect = allowed;
|
||||
preventReconnect = allowed;
|
||||
}
|
||||
|
||||
export function registerEvents({
|
||||
operations
|
||||
}: { operations: ClientOperations }, setStatus: StateUpdater<ClientStatus>, client: Client) {
|
||||
function attemptReconnect() {
|
||||
if (preventReconnect) return;
|
||||
function reconnect() {
|
||||
preventUntil = +new Date() + 2000;
|
||||
client.websocket.connect().catch(err => console.error(err));
|
||||
}
|
||||
export function registerEvents(
|
||||
{ operations }: { operations: ClientOperations },
|
||||
setStatus: StateUpdater<ClientStatus>,
|
||||
client: Client,
|
||||
) {
|
||||
function attemptReconnect() {
|
||||
if (preventReconnect) return;
|
||||
function reconnect() {
|
||||
preventUntil = +new Date() + 2000;
|
||||
client.websocket.connect().catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
if (+new Date() > preventUntil) {
|
||||
setTimeout(reconnect, 2000);
|
||||
} else {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
if (+new Date() > preventUntil) {
|
||||
setTimeout(reconnect, 2000);
|
||||
} else {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
let listeners: Record<string, (...args: any[]) => void> = {
|
||||
connecting: () =>
|
||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||
let listeners: Record<string, (...args: any[]) => void> = {
|
||||
connecting: () =>
|
||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||
|
||||
dropped: () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
attemptReconnect();
|
||||
}
|
||||
},
|
||||
dropped: () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
attemptReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
packet: (packet: ClientboundNotification) => {
|
||||
switch (packet.type) {
|
||||
case "ChannelStartTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatch({
|
||||
type: "TYPING_START",
|
||||
channel: packet.id,
|
||||
user: packet.user
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelStopTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatch({
|
||||
type: "TYPING_STOP",
|
||||
channel: packet.id,
|
||||
user: packet.user
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelAck": {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: packet.id,
|
||||
message: packet.message_id
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
packet: (packet: ClientboundNotification) => {
|
||||
switch (packet.type) {
|
||||
case "ChannelStartTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatch({
|
||||
type: "TYPING_START",
|
||||
channel: packet.id,
|
||||
user: packet.user,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelStopTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatch({
|
||||
type: "TYPING_STOP",
|
||||
channel: packet.id,
|
||||
user: packet.user,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelAck": {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: packet.id,
|
||||
message: packet.message_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
message: (message: Message) => {
|
||||
if (message.mentions?.includes(client.user!._id)) {
|
||||
dispatch({
|
||||
type: "UNREADS_MENTION",
|
||||
channel: message.channel,
|
||||
message: message._id
|
||||
});
|
||||
}
|
||||
},
|
||||
message: (message: Message) => {
|
||||
if (message.mentions?.includes(client.user!._id)) {
|
||||
dispatch({
|
||||
type: "UNREADS_MENTION",
|
||||
channel: message.channel,
|
||||
message: message._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ready: () => setStatus(ClientStatus.ONLINE)
|
||||
};
|
||||
ready: () => setStatus(ClientStatus.ONLINE),
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
listeners = new Proxy(listeners, {
|
||||
get: (target, listener, receiver) => (...args: unknown[]) => {
|
||||
console.debug(`Calling ${listener.toString()} with`, args);
|
||||
Reflect.get(target, listener)(...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
listeners = new Proxy(listeners, {
|
||||
get:
|
||||
(target, listener, receiver) =>
|
||||
(...args: unknown[]) => {
|
||||
console.debug(`Calling ${listener.toString()} with`, args);
|
||||
Reflect.get(target, listener)(...args);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: clean this a bit and properly handle types
|
||||
for (const listener in listeners) {
|
||||
client.addListener(listener, listeners[listener]);
|
||||
}
|
||||
// TODO: clean this a bit and properly handle types
|
||||
for (const listener in listeners) {
|
||||
client.addListener(listener, listeners[listener]);
|
||||
}
|
||||
|
||||
function logMutation(target: string, key: string) {
|
||||
console.log('(o) Object mutated', target, '\nChanged:', key);
|
||||
}
|
||||
function logMutation(target: string, key: string) {
|
||||
console.log("(o) Object mutated", target, "\nChanged:", key);
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
client.users.addListener('mutation', logMutation);
|
||||
client.servers.addListener('mutation', logMutation);
|
||||
client.channels.addListener('mutation', logMutation);
|
||||
client.servers.members.addListener('mutation', logMutation);
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
client.users.addListener("mutation", logMutation);
|
||||
client.servers.addListener("mutation", logMutation);
|
||||
client.channels.addListener("mutation", logMutation);
|
||||
client.servers.members.addListener("mutation", logMutation);
|
||||
}
|
||||
|
||||
const online = () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.RECONNECTING);
|
||||
setReconnectDisallowed(false);
|
||||
attemptReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
const offline = () => {
|
||||
if (operations.ready()) {
|
||||
setReconnectDisallowed(true);
|
||||
client.websocket.disconnect();
|
||||
setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
};
|
||||
const online = () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.RECONNECTING);
|
||||
setReconnectDisallowed(false);
|
||||
attemptReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("online", online);
|
||||
window.addEventListener("offline", offline);
|
||||
const offline = () => {
|
||||
if (operations.ready()) {
|
||||
setReconnectDisallowed(true);
|
||||
client.websocket.disconnect();
|
||||
setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
for (const listener in listeners) {
|
||||
client.removeListener(listener, listeners[listener as keyof typeof listeners]);
|
||||
}
|
||||
window.addEventListener("online", online);
|
||||
window.addEventListener("offline", offline);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
client.users.removeListener('mutation', logMutation);
|
||||
client.servers.removeListener('mutation', logMutation);
|
||||
client.channels.removeListener('mutation', logMutation);
|
||||
client.servers.members.removeListener('mutation', logMutation);
|
||||
}
|
||||
return () => {
|
||||
for (const listener in listeners) {
|
||||
client.removeListener(
|
||||
listener,
|
||||
listeners[listener as keyof typeof listeners],
|
||||
);
|
||||
}
|
||||
|
||||
window.removeEventListener("online", online);
|
||||
window.removeEventListener("offline", offline);
|
||||
};
|
||||
if (import.meta.env.DEV) {
|
||||
client.users.removeListener("mutation", logMutation);
|
||||
client.servers.removeListener("mutation", logMutation);
|
||||
client.channels.removeListener("mutation", logMutation);
|
||||
client.servers.members.removeListener("mutation", logMutation);
|
||||
}
|
||||
|
||||
window.removeEventListener("online", online);
|
||||
window.removeEventListener("offline", offline);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,177 +1,232 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
import { Client, PermissionCalculator } from "revolt.js";
|
||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { Client, PermissionCalculator } from 'revolt.js';
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import Collection from "revolt.js/dist/maps/Collection";
|
||||
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "./RevoltClient";
|
||||
|
||||
export interface HookContext {
|
||||
client: Client,
|
||||
forceUpdate: () => void
|
||||
client: Client;
|
||||
forceUpdate: () => void;
|
||||
}
|
||||
|
||||
export function useForceUpdate(context?: HookContext): HookContext {
|
||||
const client = useContext(AppContext);
|
||||
if (context) return context;
|
||||
const client = useContext(AppContext);
|
||||
if (context) return context;
|
||||
|
||||
const H = useState(0);
|
||||
var updateState: (_: number) => void;
|
||||
if (Array.isArray(H)) {
|
||||
let [, u] = H;
|
||||
updateState = u;
|
||||
} else {
|
||||
console.warn('Failed to construct using useState.');
|
||||
updateState = ()=>{};
|
||||
}
|
||||
const H = useState(0);
|
||||
var updateState: (_: number) => void;
|
||||
if (Array.isArray(H)) {
|
||||
let [, u] = H;
|
||||
updateState = u;
|
||||
} else {
|
||||
console.warn("Failed to construct using useState.");
|
||||
updateState = () => {};
|
||||
}
|
||||
|
||||
return { client, forceUpdate: () => updateState(Math.random()) };
|
||||
return { client, forceUpdate: () => updateState(Math.random()) };
|
||||
}
|
||||
|
||||
// TODO: utils.d.ts maybe?
|
||||
type PickProperties<T, U> = Pick<T, {
|
||||
[K in keyof T]: T[K] extends U ? K : never
|
||||
}[keyof T]>
|
||||
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>;
|
||||
type ClientCollectionKey = Exclude<
|
||||
keyof PickProperties<Client, Collection<any>>,
|
||||
undefined
|
||||
>;
|
||||
|
||||
function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
function useObject(
|
||||
type: ClientCollectionKey,
|
||||
id?: string | string[],
|
||||
context?: HookContext,
|
||||
) {
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
function update(target: any) {
|
||||
if (typeof id === 'string' ? target === id :
|
||||
Array.isArray(id) ? id.includes(target) : true) {
|
||||
ctx.forceUpdate();
|
||||
}
|
||||
}
|
||||
function update(target: any) {
|
||||
if (
|
||||
typeof id === "string"
|
||||
? target === id
|
||||
: Array.isArray(id)
|
||||
? id.includes(target)
|
||||
: true
|
||||
) {
|
||||
ctx.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
const map = ctx.client[type];
|
||||
useEffect(() => {
|
||||
map.addListener("update", update);
|
||||
return () => map.removeListener("update", update);
|
||||
}, [id]);
|
||||
const map = ctx.client[type];
|
||||
useEffect(() => {
|
||||
map.addListener("update", update);
|
||||
return () => map.removeListener("update", update);
|
||||
}, [id]);
|
||||
|
||||
return typeof id === 'string' ? map.get(id)
|
||||
: Array.isArray(id) ? id.map(x => map.get(x))
|
||||
: map.toArray();
|
||||
return typeof id === "string"
|
||||
? map.get(id)
|
||||
: Array.isArray(id)
|
||||
? id.map((x) => map.get(x))
|
||||
: map.toArray();
|
||||
}
|
||||
|
||||
export function useUser(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('users', id, context) as Readonly<Users.User> | undefined;
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject("users", id, context) as Readonly<Users.User> | undefined;
|
||||
}
|
||||
|
||||
export function useSelf(context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
return useUser(ctx.client.user!._id, ctx);
|
||||
const ctx = useForceUpdate(context);
|
||||
return useUser(ctx.client.user!._id, ctx);
|
||||
}
|
||||
|
||||
export function useUsers(ids?: string[], context?: HookContext) {
|
||||
return useObject('users', ids, context) as (Readonly<Users.User> | undefined)[];
|
||||
return useObject("users", ids, context) as (
|
||||
| Readonly<Users.User>
|
||||
| undefined
|
||||
)[];
|
||||
}
|
||||
|
||||
export function useChannel(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('channels', id, context) as Readonly<Channels.Channel> | undefined;
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject("channels", id, context) as
|
||||
| Readonly<Channels.Channel>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export function useChannels(ids?: string[], context?: HookContext) {
|
||||
return useObject('channels', ids, context) as (Readonly<Channels.Channel> | undefined)[];
|
||||
return useObject("channels", ids, context) as (
|
||||
| Readonly<Channels.Channel>
|
||||
| undefined
|
||||
)[];
|
||||
}
|
||||
|
||||
export function useServer(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('servers', id, context) as Readonly<Servers.Server> | undefined;
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject("servers", id, context) as
|
||||
| Readonly<Servers.Server>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export function useServers(ids?: string[], context?: HookContext) {
|
||||
return useObject('servers', ids, context) as (Readonly<Servers.Server> | undefined)[];
|
||||
return useObject("servers", ids, context) as (
|
||||
| Readonly<Servers.Server>
|
||||
| undefined
|
||||
)[];
|
||||
}
|
||||
|
||||
export function useDMs(context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
function mutation(target: string) {
|
||||
let channel = ctx.client.channels.get(target);
|
||||
if (channel) {
|
||||
if (channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group') {
|
||||
ctx.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
function mutation(target: string) {
|
||||
let channel = ctx.client.channels.get(target);
|
||||
if (channel) {
|
||||
if (
|
||||
channel.channel_type === "DirectMessage" ||
|
||||
channel.channel_type === "Group"
|
||||
) {
|
||||
ctx.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const map = ctx.client.channels;
|
||||
useEffect(() => {
|
||||
map.addListener("update", mutation);
|
||||
return () => map.removeListener("update", mutation);
|
||||
}, []);
|
||||
const map = ctx.client.channels;
|
||||
useEffect(() => {
|
||||
map.addListener("update", mutation);
|
||||
return () => map.removeListener("update", mutation);
|
||||
}, []);
|
||||
|
||||
return map
|
||||
.toArray()
|
||||
.filter(x => x.channel_type === 'DirectMessage' || x.channel_type === 'Group' || x.channel_type === 'SavedMessages') as (Channels.GroupChannel | Channels.DirectMessageChannel | Channels.SavedMessagesChannel)[];
|
||||
return map
|
||||
.toArray()
|
||||
.filter(
|
||||
(x) =>
|
||||
x.channel_type === "DirectMessage" ||
|
||||
x.channel_type === "Group" ||
|
||||
x.channel_type === "SavedMessages",
|
||||
) as (
|
||||
| Channels.GroupChannel
|
||||
| Channels.DirectMessageChannel
|
||||
| Channels.SavedMessagesChannel
|
||||
)[];
|
||||
}
|
||||
|
||||
export function useUserPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
useEffect(() => {
|
||||
ctx.client.users.addListener("update", mutation);
|
||||
return () => ctx.client.users.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forUser(id);
|
||||
const mutation = (target: string) => target === id && ctx.forceUpdate();
|
||||
useEffect(() => {
|
||||
ctx.client.users.addListener("update", mutation);
|
||||
return () => ctx.client.users.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forUser(id);
|
||||
}
|
||||
|
||||
export function useChannelPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const channel = ctx.client.channels.get(id);
|
||||
const server = (channel && (channel.channel_type === 'TextChannel' || channel.channel_type === 'VoiceChannel')) ? channel.server : undefined;
|
||||
const channel = ctx.client.channels.get(id);
|
||||
const server =
|
||||
channel &&
|
||||
(channel.channel_type === "TextChannel" ||
|
||||
channel.channel_type === "VoiceChannel")
|
||||
? channel.server
|
||||
: undefined;
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
const mutationServer = (target: string) => (target === server) && ctx.forceUpdate();
|
||||
const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
|
||||
const mutation = (target: string) => target === id && ctx.forceUpdate();
|
||||
const mutationServer = (target: string) =>
|
||||
target === server && ctx.forceUpdate();
|
||||
const mutationMember = (target: string) =>
|
||||
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.channels.addListener("update", mutation);
|
||||
useEffect(() => {
|
||||
ctx.client.channels.addListener("update", mutation);
|
||||
|
||||
if (server) {
|
||||
ctx.client.servers.addListener("update", mutationServer);
|
||||
ctx.client.servers.members.addListener("update", mutationMember);
|
||||
}
|
||||
if (server) {
|
||||
ctx.client.servers.addListener("update", mutationServer);
|
||||
ctx.client.servers.members.addListener("update", mutationMember);
|
||||
}
|
||||
|
||||
return () => {
|
||||
ctx.client.channels.removeListener("update", mutation);
|
||||
return () => {
|
||||
ctx.client.channels.removeListener("update", mutation);
|
||||
|
||||
if (server) {
|
||||
ctx.client.servers.removeListener("update", mutationServer);
|
||||
ctx.client.servers.members.removeListener("update", mutationMember);
|
||||
}
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forChannel(id);
|
||||
if (server) {
|
||||
ctx.client.servers.removeListener("update", mutationServer);
|
||||
ctx.client.servers.members.removeListener(
|
||||
"update",
|
||||
mutationMember,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forChannel(id);
|
||||
}
|
||||
|
||||
export function useServerPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
|
||||
const mutation = (target: string) => target === id && ctx.forceUpdate();
|
||||
const mutationMember = (target: string) =>
|
||||
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.servers.addListener("update", mutation);
|
||||
ctx.client.servers.members.addListener("update", mutationMember);
|
||||
useEffect(() => {
|
||||
ctx.client.servers.addListener("update", mutation);
|
||||
ctx.client.servers.members.addListener("update", mutationMember);
|
||||
|
||||
return () => {
|
||||
ctx.client.servers.removeListener("update", mutation);
|
||||
ctx.client.servers.members.removeListener("update", mutationMember);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forServer(id);
|
||||
return () => {
|
||||
ctx.client.servers.removeListener("update", mutation);
|
||||
ctx.client.servers.members.removeListener("update", mutationMember);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forServer(id);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,57 @@
|
||||
import { Channel, Message, User } from "revolt.js/dist/api/objects";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { Text } from "preact-i18n";
|
||||
import { Client } from "revolt.js";
|
||||
import { Channel, Message, User } from "revolt.js/dist/api/objects";
|
||||
|
||||
export function takeError(
|
||||
error: any
|
||||
): string {
|
||||
const type = error?.response?.data?.type;
|
||||
let id = type;
|
||||
if (!type) {
|
||||
if (error?.response?.status === 403) {
|
||||
return "Unauthorized";
|
||||
} else if (error && (!!error.isAxiosError && !error.response)) {
|
||||
return "NetworkError";
|
||||
}
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
console.error(error);
|
||||
return "UnknownError";
|
||||
}
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
return id;
|
||||
export function takeError(error: any): string {
|
||||
const type = error?.response?.data?.type;
|
||||
let id = type;
|
||||
if (!type) {
|
||||
if (error?.response?.status === 403) {
|
||||
return "Unauthorized";
|
||||
} else if (error && !!error.isAxiosError && !error.response) {
|
||||
return "NetworkError";
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return "UnknownError";
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export function getChannelName(client: Client, channel: Channel, prefixType?: boolean): Children {
|
||||
if (channel.channel_type === "SavedMessages")
|
||||
return <Text id="app.navigation.tabs.saved" />;
|
||||
export function getChannelName(
|
||||
client: Client,
|
||||
channel: Channel,
|
||||
prefixType?: boolean,
|
||||
): Children {
|
||||
if (channel.channel_type === "SavedMessages")
|
||||
return <Text id="app.navigation.tabs.saved" />;
|
||||
|
||||
if (channel.channel_type === "DirectMessage") {
|
||||
let uid = client.channels.getRecipient(channel._id);
|
||||
return <>{prefixType && "@"}{client.users.get(uid)?.username}</>;
|
||||
}
|
||||
if (channel.channel_type === "DirectMessage") {
|
||||
let uid = client.channels.getRecipient(channel._id);
|
||||
return (
|
||||
<>
|
||||
{prefixType && "@"}
|
||||
{client.users.get(uid)?.username}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (channel.channel_type === "TextChannel" && prefixType) {
|
||||
return <>#{channel.name}</>;
|
||||
}
|
||||
if (channel.channel_type === "TextChannel" && prefixType) {
|
||||
return <>#{channel.name}</>;
|
||||
}
|
||||
|
||||
return <>{channel.name}</>;
|
||||
return <>{channel.name}</>;
|
||||
}
|
||||
|
||||
export type MessageObject = Omit<Message, "edited"> & { edited?: string };
|
||||
export function mapMessage(message: Partial<Message>) {
|
||||
const { edited, ...msg } = message;
|
||||
return {
|
||||
...msg,
|
||||
edited: edited?.$date,
|
||||
} as MessageObject;
|
||||
const { edited, ...msg } = message;
|
||||
return {
|
||||
...msg,
|
||||
edited: edited?.$date,
|
||||
} as MessageObject;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user