remove most uses of as any in typescript

- replaced many uses of `as any` with another more specific cast `as T`
- filled in missing typed for items that needed to be typed
  - new runtime code was added where necessary to satisfy the new types with comments
- added missing theme variable "sidebar-active" to the Theme variables
- forms using `react-hook-form` are now typechecked
- changed some instances of `target` into `currentTarget` while removing `as any` assertions
This commit is contained in:
bree 2021-07-04 07:09:39 -04:00
parent 841320aab7
commit a4051330a3
No known key found for this signature in database
GPG key ID: 1B2E56B9EC985B96
31 changed files with 161 additions and 117 deletions

View file

@ -384,7 +384,7 @@ export default function AutoComplete({ detached, state, setState, onClick }: Pic
}) })
} }
onClick={onClick}> onClick={onClick}>
<Emoji emoji={(emojiDictionary as any)[match]} size={20} /> <Emoji emoji={(emojiDictionary as Record<string, string>)[match]} size={20} />
:{match}: :{match}:
</button> </button>
))} ))}

View file

@ -1,7 +1,7 @@
import ComboBox from "../ui/ComboBox"; import ComboBox from "../ui/ComboBox";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { WithDispatcher } from "../../redux/reducers"; import { WithDispatcher } from "../../redux/reducers";
import { LanguageEntry, Languages } from "../../context/Locale"; import { Language, LanguageEntry, Languages } from "../../context/Locale";
type Props = WithDispatcher & { type Props = WithDispatcher & {
locale: string; locale: string;
@ -15,12 +15,12 @@ export function LocaleSelector(props: Props) {
props.dispatcher && props.dispatcher &&
props.dispatcher({ props.dispatcher({
type: "SET_LOCALE", type: "SET_LOCALE",
locale: e.currentTarget.value as any locale: e.currentTarget.value as Language
}) })
} }
> >
{Object.keys(Languages).map(x => { {Object.keys(Languages).map(x => {
const l = (Languages as any)[x] as LanguageEntry; const l = Languages[x as keyof typeof Languages];
return ( return (
<option value={x}> <option value={x}>
{l.emoji} {l.display} {l.emoji} {l.display}

View file

@ -15,7 +15,7 @@ export default function UpdateIndicator() {
return internalSubscribe('PWA', 'update', () => setPending(true)); return internalSubscribe('PWA', 'update', () => setPending(true));
}); });
if (!pending) return; if (!pending) return <></>;
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
return ( return (

View file

@ -35,7 +35,12 @@ function Message({ attachContext, message, contrast, content: replacement, head:
const content = message.content as string; const content = message.content as string;
const head = preferHead || (message.replies && message.replies.length > 0); const head = preferHead || (message.replies && message.replies.length > 0);
const userContext = attachContext ? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) : undefined as any; // ! FIXME: tell fatal to make this type generic // ! FIXME: tell fatal to make this type generic
// bree: Fatal please...
const userContext = attachContext
? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) as any
: undefined;
const openProfile = () => openScreen({ id: 'profile', user_id: message.author }); const openProfile = () => openScreen({ id: 'profile', user_id: message.author });
return ( return (

View file

@ -18,7 +18,8 @@ export default function AttachmentActions({ attachment }: Props) {
const open_url = `${url}/${filename}`; const open_url = `${url}/${filename}`;
const download_url = url.replace('attachments', 'attachments/download') const download_url = url.replace('attachments', 'attachments/download')
const filesize = determineFileSize(size as any); // for some reason revolt.js says the size is a string even though it's a number
const filesize = determineFileSize(size as unknown as number);
switch (metadata.type) { switch (metadata.type) {
case 'Image': case 'Image':

View file

@ -22,13 +22,21 @@ import MarkdownSup from "markdown-it-sup";
// @ts-ignore // @ts-ignore
import MarkdownSub from "markdown-it-sub"; import MarkdownSub from "markdown-it-sub";
// TODO: global.d.ts file for defining globals
declare global {
interface Window {
copycode: (element: HTMLDivElement) => void
}
}
// Handler for code block copy. // Handler for code block copy.
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
(window as any).copycode = function(element: HTMLDivElement) { window.copycode = function(element: HTMLDivElement) {
try { try {
let code = element.parentElement?.parentElement?.children[1]; let code = element.parentElement?.parentElement?.children[1];
if (code) { if (code) {
navigator.clipboard.writeText((code as any).innerText.trim()); navigator.clipboard.writeText(code.textContent?.trim() ?? '');
} }
} catch (e) {} } catch (e) {}
}; };
@ -65,10 +73,17 @@ const defaultRender =
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
// TODO: global.d.ts file for defining globals
declare global {
interface Window {
internalHandleURL: (element: HTMLAnchorElement) => void
}
}
// Handler for internal links, pushes events to React using magic. // Handler for internal links, pushes events to React using magic.
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
(window as any).internalHandleURL = function(element: HTMLAnchorElement) { window.internalHandleURL = function(element: HTMLAnchorElement) {
const url = new URL(element.href, location as any); const url = new URL(element.href, location.href);
const pathname = url.pathname; const pathname = url.pathname;
if (pathname.startsWith("/@")) { if (pathname.startsWith("/@")) {
@ -87,7 +102,7 @@ md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
// For internal links, we should use our own handler to use react-router history. // For internal links, we should use our own handler to use react-router history.
// @ts-ignore // @ts-ignore
const href = tokens[idx].attrs[hIndex][1]; const href = tokens[idx].attrs[hIndex][1];
const url = new URL(href, location as any); const url = new URL(href, location.href);
if (url.hostname === location.hostname) { if (url.hostname === location.hostname) {
internal = true; internal = true;
@ -161,7 +176,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
data-large-emojis={useLargeEmojis} data-large-emojis={useLargeEmojis}
onClick={ev => { onClick={ev => {
if (ev.target) { if (ev.target) {
let element: Element = ev.target as any; let element = ev.currentTarget;
if (element.classList.contains("spoiler")) { if (element.classList.contains("spoiler")) {
element.classList.add("shown"); element.classList.add("shown");
} }

View file

@ -107,11 +107,8 @@ function HomeSidebar(props: Props) {
)} )}
<Localizer> <Localizer>
<Category <Category
text={ text={<Text id="app.main.categories.conversations" />}
( /** @ts-ignore : ignored due to conflicting naming between the Category property name and the existing JSX attribute */
<Text id="app.main.categories.conversations" />
) as any
}
action={() => openScreen({ id: "special_input", type: "create_group" })} action={() => openScreen({ id: "special_input", type: "create_group" })}
/> />
</Localizer> </Localizer>

View file

@ -55,12 +55,8 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
members.sort((a, b) => { members.sort((a, b) => {
// ! FIXME: should probably rewrite all this code // ! FIXME: should probably rewrite all this code
let l = ((a.online && let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
a.status?.presence !== Users.Presence.Invisible) ?? let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
false) as any | 0;
let r = ((b.online &&
b.status?.presence !== Users.Presence.Invisible) ??
false) as any | 0;
let n = r - l; let n = r - l;
if (n !== 0) { if (n !== 0) {
@ -159,12 +155,8 @@ export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channel
// copy paste from above // copy paste from above
users.sort((a, b) => { users.sort((a, b) => {
// ! FIXME: should probably rewrite all this code // ! FIXME: should probably rewrite all this code
let l = ((a.online && let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
a.status?.presence !== Users.Presence.Invisible) ?? let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
false) as any | 0;
let r = ((b.online &&
b.status?.presence !== Users.Presence.Invisible) ??
false) as any | 0;
let n = r - l; let n = r - l;
if (n !== 0) { if (n !== 0) {

View file

@ -33,6 +33,7 @@ const CategoryBase = styled.div<Pick<Props, 'variant'>>`
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & { type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
text: Children; text: Children;
// TODO: rename from action to prevent type conflicts with the dom
action?: () => void; action?: () => void;
variant?: 'default' | 'uniform'; variant?: 'default' | 'uniform';
} }

View file

@ -122,9 +122,11 @@ interface Props {
} }
function Locale({ children, locale }: Props) { function Locale({ children, locale }: Props) {
const [defns, setDefinition] = useState(definition); // TODO: create and use LanguageDefinition type here
const [defns, setDefinition] = useState<Record<string, unknown>>(definition);
const lang = Languages[locale]; const lang = Languages[locale];
// TOOD: clean this up and use the built in Intl API
function transformLanguage(obj: { [key: string]: any }) { function transformLanguage(obj: { [key: string]: any }) {
const dayjs = obj.dayjs; const dayjs = obj.dayjs;
const defaults = dayjs.defaults; const defaults = dayjs.defaults;
@ -158,7 +160,7 @@ function Locale({ children, locale }: Props) {
if (lang.i18n === "hardcore") { if (lang.i18n === "hardcore") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
setDefinition({} as any); setDefinition({});
return; return;
} }

View file

@ -15,8 +15,8 @@ import { Children } from "../types/Preact";
import { createContext } from "preact"; import { createContext } from "preact";
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
export const SettingsContext = createContext<Settings>({} as any); export const SettingsContext = createContext<Settings>({});
export const SoundContext = createContext<(sound: Sounds) => void>({} as any); export const SoundContext = createContext<((sound: Sounds) => void)>(null!);
interface Props { interface Props {
children?: Children, children?: Children,

View file

@ -30,7 +30,8 @@ export type Variables =
| "status-away" | "status-away"
| "status-busy" | "status-busy"
| "status-streaming" | "status-streaming"
| "status-invisible"; | "status-invisible"
| "sidebar-active";
export type Theme = { export type Theme = {
[variable in Variables]: string; [variable in Variables]: string;
@ -45,7 +46,7 @@ export interface ThemeOptions {
} }
// Generated from https://gitlab.insrt.uk/revolt/community/themes // Generated from https://gitlab.insrt.uk/revolt/community/themes
export const PRESETS: { [key: string]: Theme } = { export const PRESETS: Record<string, Theme> = {
light: { light: {
light: true, light: true,
accent: "#FD6671", accent: "#FD6671",
@ -72,6 +73,7 @@ export const PRESETS: { [key: string]: Theme } = {
"status-busy": "#F84848", "status-busy": "#F84848",
"status-streaming": "#977EFF", "status-streaming": "#977EFF",
"status-invisible": "#A5A5A5", "status-invisible": "#A5A5A5",
"sidebar-active": "var(--secondary-background)"
}, },
dark: { dark: {
light: false, light: false,
@ -99,6 +101,7 @@ export const PRESETS: { [key: string]: Theme } = {
"status-busy": "#F84848", "status-busy": "#F84848",
"status-streaming": "#977EFF", "status-streaming": "#977EFF",
"status-invisible": "#A5A5A5", "status-invisible": "#A5A5A5",
"sidebar-active": "var(--secondary-background)"
}, },
}; };
@ -113,7 +116,8 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
} }
`; `;
export const ThemeContext = createContext<Theme>({} as any); // Load the default default them and apply extras later
export const ThemeContext = createContext<Theme>(PRESETS['dark']);
interface Props { interface Props {
children: Children; children: Children;
@ -123,7 +127,7 @@ interface Props {
function Theme(props: Props) { function Theme(props: Props) {
const theme: Theme = { const theme: Theme = {
...PRESETS["dark"], ...PRESETS["dark"],
...(PRESETS as any)[props.options?.preset as any], ...PRESETS[props.options?.preset ?? ''],
...props.options?.custom ...props.options?.custom
}; };

View file

@ -33,8 +33,10 @@ export interface VoiceState {
participants?: Readonly<Map<string, VoiceUser>>; participants?: Readonly<Map<string, VoiceUser>>;
} }
export const VoiceContext = createContext<VoiceState>(undefined as any); // [bree] TODO: I feel like these should be typechecked anyways but whatever,
export const VoiceOperationsContext = createContext<VoiceOperations>(undefined as any); // I'm asserting that they aren't null because they get used near immedietly from what I can tell
export const VoiceContext = createContext<VoiceState>(null!);
export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
type Props = { type Props = {
children: Children; children: Children;

View file

@ -119,8 +119,9 @@ export function SpecialInputModal(props: SpecialProps) {
question={<Text id="app.settings.permissions.create_role" />} question={<Text id="app.settings.permissions.create_role" />}
field={<Text id="app.settings.permissions.role_name" />} field={<Text id="app.settings.permissions.role_name" />}
callback={async name => { callback={async name => {
// bree: this returns void, dunno why props.callback was being called
const role = await client.servers.createRole(props.server, name); const role = await client.servers.createRole(props.server, name);
props.callback(role.id); // props.callback(role.id);
}} }}
/>; />;
} }

View file

@ -1,6 +1,6 @@
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import styles from "./Onboarding.module.scss"; import styles from "./Onboarding.module.scss";
import { takeError } from "../../revoltjs/util"; import { takeError } from "../../revoltjs/util";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
@ -14,12 +14,16 @@ interface Props {
callback: (username: string, loginAfterSuccess?: true) => Promise<void>; callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
} }
interface FormInputs {
username: string
}
export function OnboardingModal({ onClose, callback }: Props) { export function OnboardingModal({ onClose, callback }: Props) {
const { handleSubmit, register } = useForm(); const { handleSubmit, register } = useForm<FormInputs>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
async function onSubmit({ username }: { username: string }) { const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
setLoading(true); setLoading(true);
callback(username, true) callback(username, true)
.then(onClose) .then(onClose)
@ -45,7 +49,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
<p> <p>
<Text id="app.special.modals.onboarding.pick" /> <Text id="app.special.modals.onboarding.pick" />
</p> </p>
<form onSubmit={handleSubmit(onSubmit) as any}> <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
<div> <div>
<FormField <FormField
type="username" type="username"

View file

@ -1,5 +1,5 @@
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { takeError } from "../../revoltjs/util"; import { takeError } from "../../revoltjs/util";
import { useContext, useState } from "preact/hooks"; import { useContext, useState } from "preact/hooks";
@ -12,22 +12,28 @@ interface Props {
field: "username" | "email" | "password"; field: "username" | "email" | "password";
} }
interface FormInputs {
password: string,
new_email: string,
new_username: string,
new_password: string,
// TODO: figure out if this is correct or not
// it wasn't in the types before this was typed but the element itself was there
current_password?: string
}
export function ModifyAccountModal({ onClose, field }: Props) { export function ModifyAccountModal({ onClose, field }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { handleSubmit, register, errors } = useForm(); const { handleSubmit, register, errors } = useForm<FormInputs>();
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
async function onSubmit({ const onSubmit: SubmitHandler<FormInputs> = async ({
password, password,
new_username, new_username,
new_email, new_email,
new_password new_password
}: { }) => {
password: string;
new_username: string;
new_email: string;
new_password: string;
}) {
try { try {
if (field === "email") { if (field === "email") {
await client.req("POST", "/auth/change/email", { await client.req("POST", "/auth/change/email", {
@ -75,7 +81,8 @@ export function ModifyAccountModal({ onClose, field }: Props) {
} }
]} ]}
> >
<form onSubmit={handleSubmit(onSubmit) as any}> {/* Preact / React typing incompatabilities */}
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
{field === "email" && ( {field === "email" && (
<FormField <FormField
type="email" type="email"

View file

@ -43,8 +43,8 @@ export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooL
input.type = "file"; input.type = "file";
input.multiple = multiple ?? false; input.multiple = multiple ?? false;
input.onchange = async e => { input.onchange = async (e) => {
const files = (e.target as any)?.files; const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return; if (!files) return;
for (let file of files) { for (let file of files) {
if (file.size > maxFileSize) { if (file.size > maxFileSize) {

View file

@ -34,9 +34,10 @@ export interface ClientOperations {
openDM: (user_id: string) => Promise<string>; openDM: (user_id: string) => Promise<string>;
} }
export const AppContext = createContext<Client>(undefined as any); // TODO: remove temporary non-null assertions and properly typecheck these as they aren't always immedietely initialized
export const StatusContext = createContext<ClientStatus>(undefined as any); export const AppContext = createContext<Client>(null!);
export const OperationsContext = createContext<ClientOperations>(undefined as any); export const StatusContext = createContext<ClientStatus>(null!);
export const OperationsContext = createContext<ClientOperations>(null!);
type Props = WithDispatcher & { type Props = WithDispatcher & {
auth: AuthState; auth: AuthState;
@ -93,16 +94,14 @@ function Context({ auth, children, dispatcher }: Props) {
const login = () => const login = () =>
dispatcher({ dispatcher({
type: "LOGIN", type: "LOGIN",
session: client.session as any session: client.session! // TODO: verify that this null assertion is correct
}); });
if (onboarding) { if (onboarding) {
openScreen({ openScreen({
id: "onboarding", id: "onboarding",
callback: async (username: string) => { callback: (username: string) =>
await (onboarding as any)(username, true); onboarding(username, true).then(login)
login();
}
}); });
} else { } else {
login(); login();

View file

@ -23,11 +23,11 @@ type Props = WithDispatcher & {
var lastValues: { [key in SyncKeys]?: any } = { }; var lastValues: { [key in SyncKeys]?: any } = { };
export function mapSync(packet: Sync.UserSettings, revision?: { [key: string]: number }) { export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {}; let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
for (let key of Object.keys(packet)) { for (let key of Object.keys(packet)) {
let [ timestamp, obj ] = packet[key]; let [ timestamp, obj ] = packet[key];
if (timestamp < (revision ?? {} as any)[key] ?? 0) { if (timestamp < (revision ?? {})[key] ?? 0) {
continue; continue;
} }

View file

@ -32,7 +32,7 @@ export function registerEvents({
} }
} }
const listeners = { let listeners: Record<string, (...args: any[]) => void> = {
connecting: () => connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING), operations.ready() && setStatus(ClientStatus.CONNECTING),
@ -87,21 +87,18 @@ export function registerEvents({
ready: () => setStatus(ClientStatus.ONLINE) ready: () => setStatus(ClientStatus.ONLINE)
}; };
let listenerFunc: { [key: string]: Function };
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
listenerFunc = {}; listeners = new Proxy(listeners, {
for (const listener of Object.keys(listeners)) { get: (target, listener, receiver) => (...args: unknown[]) => {
listenerFunc[listener] = (...args: any[]) => { console.debug(`Calling ${listener.toString()} with`, args);
console.debug(`Calling ${listener} with`, args); Reflect.get(target, listener)(...args)
(listeners as any)[listener](...args); }
}; })
}
} else {
listenerFunc = listeners;
} }
for (const listener of Object.keys(listenerFunc)) { // TODO: clean this a bit and properly handle types
client.addListener(listener, (listenerFunc as any)[listener]); for (const listener in listeners) {
client.addListener(listener, listeners[listener]);
} }
function logMutation(target: string, key: string) { function logMutation(target: string, key: string) {
@ -135,8 +132,8 @@ export function registerEvents({
window.addEventListener("offline", offline); window.addEventListener("offline", offline);
return () => { return () => {
for (const listener of Object.keys(listenerFunc)) { for (const listener in listeners) {
client.removeListener(listener, (listenerFunc as any)[listener]); client.removeListener(listener, listeners[listener as keyof typeof listeners]);
} }
if (import.meta.env.DEV) { if (import.meta.env.DEV) {

View file

@ -2,6 +2,7 @@ import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects"; import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
import { Client, PermissionCalculator } from 'revolt.js'; import { Client, PermissionCalculator } from 'revolt.js';
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
import Collection from "revolt.js/dist/maps/Collection";
export interface HookContext { export interface HookContext {
client: Client, client: Client,
@ -25,7 +26,16 @@ export function useForceUpdate(context?: HookContext): HookContext {
return { client, forceUpdate: () => updateState(Math.random()) }; return { client, forceUpdate: () => updateState(Math.random()) };
} }
function useObject(type: string, id?: string | string[], context?: HookContext) { // TODO: utils.d.ts maybe?
type PickProperties<T, U> = Pick<T, {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T]>
// The keys in Client that are an object
// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
type ClientCollectionKey = Exclude<keyof PickProperties<Client, Collection<any>>, undefined>;
function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
const ctx = useForceUpdate(context); const ctx = useForceUpdate(context);
function update(target: any) { function update(target: any) {
@ -35,7 +45,7 @@ function useObject(type: string, id?: string | string[], context?: HookContext)
} }
} }
const map = (ctx.client as any)[type]; const map = ctx.client[type];
useEffect(() => { useEffect(() => {
map.addListener("update", update); map.addListener("update", update);
return () => map.removeListener("update", update); return () => map.removeListener("update", update);

View file

@ -307,11 +307,15 @@ function ContextMenus(props: Props) {
case "delete_server": case "delete_server":
case "delete_message": case "delete_message":
case "create_channel": case "create_channel":
// @ts-expect-error case "create_invite":
case "create_invite": openScreen({ id: "special_prompt", type: data.action, target: data.target }); break; // The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
openScreen({ id: "special_prompt", type: data.action, target: data.target as any });
break;
case "ban_member": case "ban_member":
case "kick_member": openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user }); break; case "kick_member":
openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user });
break;
case "open_notification_options": { case "open_notification_options": {
openContextMenu("NotificationOptions", { channel: data.channel }); openContextMenu("NotificationOptions", { channel: data.channel });
@ -427,7 +431,7 @@ function ContextMenus(props: Props) {
} }
if (user) { if (user) {
let actions: string[]; let actions: Action['action'][];
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.User: actions = []; break; case Users.Relationship.User: actions = []; break;
case Users.Relationship.Friend: case Users.Relationship.Friend:
@ -461,11 +465,9 @@ function ContextMenus(props: Props) {
generateAction({ action: 'message_user', user: user._id }); generateAction({ action: 'message_user', user: user._id });
} }
for (const action of actions) { for(let i = 0; i < actions.length; i++) {
generateAction({ // The any here is because typescript can't determine that user the actions are linked together correctly
action: action as any, generateAction({ action: actions[i] as any, user })
user
});
} }
} }

View file

@ -26,6 +26,8 @@ export default function FormField({
)} )}
<Localizer> <Localizer>
<InputBox <InputBox
// Styled uses React typing while we use Preact
// this leads to inconsistances where things need to be typed oddly
placeholder={(<Text id={`login.enter.${type}`} />) as any} placeholder={(<Text id={`login.enter.${type}`} />) as any}
name={ name={
type === "current_password" ? "password" : name ?? type type === "current_password" ? "password" : name ?? type

View file

@ -35,6 +35,12 @@ function getInviteCode() {
return code ?? ''; return code ?? '';
} }
interface FormInputs {
email: string
password: string
invite: string
}
export function Form({ page, callback }: Props) { export function Form({ page, callback }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
@ -43,7 +49,7 @@ export function Form({ page, callback }: Props) {
const [error, setGlobalError] = useState<string | undefined>(undefined); const [error, setGlobalError] = useState<string | undefined>(undefined);
const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined); const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
const { handleSubmit, register, errors, setError } = useForm({ const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
defaultValues: { defaultValues: {
email: '', email: '',
password: '', password: '',
@ -51,11 +57,7 @@ export function Form({ page, callback }: Props) {
} }
}); });
async function onSubmit(data: { async function onSubmit(data: FormInputs) {
email: string;
password: string;
invite: string;
}) {
setGlobalError(undefined); setGlobalError(undefined);
setLoading(true); setLoading(true);
@ -143,7 +145,8 @@ export function Form({ page, callback }: Props) {
return ( return (
<div className={styles.form}> <div className={styles.form}>
<img src={wideSVG} /> <img src={wideSVG} />
<form onSubmit={handleSubmit(onSubmit) as any}> {/* Preact / React typing incompatabilities */}
<form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
{page !== "reset" && ( {page !== "reset" && (
<FormField <FormField
type="email" type="email"

View file

@ -25,9 +25,9 @@ export function FormReset() {
<Form <Form
page="reset" page="reset"
callback={async data => { callback={async data => {
await client.req("POST", "/auth/reset" as any, { await client.req("POST", "/auth/reset", {
token, token,
...(data as any) ...data
}); });
history.push("/login"); history.push("/login");
}} }}

View file

@ -53,11 +53,11 @@ export function Account() {
<div className={styles.username}>@{user.username}</div> <div className={styles.username}>@{user.username}</div>
</div> </div>
<div className={styles.details}> <div className={styles.details}>
{[ {([
["username", user.username, <At size={24} />], ["username", user.username, <At size={24} />],
["email", email, <Envelope size={24} />], ["email", email, <Envelope size={24} />],
["password", "*****", <Key size={24} />] ["password", "*****", <Key size={24} />]
].map(([field, value, icon]) => ( ] as const).map(([field, value, icon]) => (
<div> <div>
{icon} {icon}
<div className={styles.detail}> <div className={styles.detail}>
@ -71,7 +71,7 @@ export function Account() {
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "modify_account", id: "modify_account",
field: field as any field: field
}) })
} }
contrast contrast

View file

@ -208,7 +208,7 @@ export function Component(props: Props & WithDispatcher) {
</Button> </Button>
</div> </div>
<div className={styles.overrides}> <div className={styles.overrides}>
{[ {([
"accent", "accent",
"background", "background",
"foreground", "foreground",
@ -234,15 +234,15 @@ export function Component(props: Props & WithDispatcher) {
"warning", "warning",
"error", "error",
"hover" "hover"
].map(x => ( ] as const).map(x => (
<div className={styles.entry} key={x}> <div className={styles.entry} key={x}>
<span>{x}</span> <span>{x}</span>
<div className={styles.override}> <div className={styles.override}>
<div className={styles.picker} <div className={styles.picker}
style={{ backgroundColor: (theme as any)[x as any] }}> style={{ backgroundColor: theme[x] }}>
<input <input
type="color" type="color"
value={(theme as any)[x as any]} value={theme[x]}
onChange={v => onChange={v =>
setOverride({ setOverride({
[x]: v.currentTarget.value [x]: v.currentTarget.value
@ -252,7 +252,7 @@ export function Component(props: Props & WithDispatcher) {
</div> </div>
<InputBox <InputBox
className={styles.text} className={styles.text}
value={(theme as any)[x as any]} value={theme[x]}
onChange={y => onChange={y =>
setOverride({ setOverride({
[x]: y.currentTarget.value [x]: y.currentTarget.value

View file

@ -78,11 +78,11 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
// tell the server we just subscribed // tell the server we just subscribed
const json = sub.toJSON(); const json = sub.toJSON();
if (json.keys) { if (json.keys) {;
client.req("POST", "/push/subscribe", { client.req("POST", "/push/subscribe", {
endpoint: sub.endpoint, endpoint: sub.endpoint,
...json.keys ...(json.keys as { p256dh: string, auth: string })
} as any); });
setPushEnabled(true); setPushEnabled(true);
} }
} else { } else {

View file

@ -11,7 +11,7 @@ import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltCli
import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete"; import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
export function Profile() { export function Profile() {
const { intl } = useContext(IntlContext) as any; const { intl } = useContext(IntlContext);
const status = useContext(StatusContext); const status = useContext(StatusContext);
const ctx = useForceUpdate(); const ctx = useForceUpdate();
@ -121,7 +121,7 @@ export function Profile() {
: "placeholder" : "placeholder"
}`, }`,
"", "",
intl.dictionary (intl as any).dictionary as Record<string, unknown>
)} )}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}

View file

@ -155,7 +155,7 @@ export function Sessions() {
]); ]);
await client.req( await client.req(
"DELETE", "DELETE",
`/auth/sessions/${session.id}` as any `/auth/sessions/${session.id}` as '/auth/sessions'
); );
setSessions( setSessions(
sessions?.filter( sessions?.filter(

View file

@ -30,7 +30,7 @@ export function Roles({ server }: Props) {
if (role !== 'default' && typeof roles[role] === 'undefined') { if (role !== 'default' && typeof roles[role] === 'undefined') {
useEffect(() => setRole('default')); useEffect(() => setRole('default'));
return; return <></>;
} }
const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions) const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions)