Move general components to typescript
This commit is contained in:
parent
ecfab87aa0
commit
f6d695a48a
@ -105,6 +105,7 @@
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/react-modal": "^3.12.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/simple-peer": "^9.6.3",
|
||||
"@types/uuid": "^8.3.1",
|
||||
|
@ -3,7 +3,13 @@ import { Box, Flex, Text, IconButton, Divider } from "theme-ui";
|
||||
|
||||
import ExpandMoreIcon from "../icons/ExpandMoreIcon";
|
||||
|
||||
function Accordion({ heading, children, defaultOpen }) {
|
||||
type AccordianProps = {
|
||||
heading: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen: boolean;
|
||||
};
|
||||
|
||||
function Accordion({ heading, children, defaultOpen }: AccordianProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
@ -1,7 +1,16 @@
|
||||
import React from "react";
|
||||
import { Divider } from "theme-ui";
|
||||
import { Divider, DividerProps } from "theme-ui";
|
||||
|
||||
function StyledDivider({ vertical, color, fill }) {
|
||||
type StyledDividerProps = {
|
||||
vertical: boolean;
|
||||
fill: boolean;
|
||||
} & DividerProps;
|
||||
|
||||
function StyledDivider({
|
||||
vertical,
|
||||
color,
|
||||
fill,
|
||||
...props
|
||||
}: StyledDividerProps) {
|
||||
return (
|
||||
<Divider
|
||||
my={vertical ? 0 : 2}
|
||||
@ -13,6 +22,7 @@ function StyledDivider({ vertical, color, fill }) {
|
||||
borderRadius: "2px",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import Link from "./Link";
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Group, Rect } from "react-konva";
|
||||
import useImage from "use-image";
|
||||
|
||||
@ -16,7 +15,7 @@ import squarePatternLight from "../images/SquarePatternLight.png";
|
||||
import hexPatternDark from "../images/HexPatternDark.png";
|
||||
import hexPatternLight from "../images/HexPatternLight.png";
|
||||
|
||||
function Grid({ stroke }) {
|
||||
function Grid({ stroke }: { stroke: "black" | "white" }) {
|
||||
const grid = useGrid();
|
||||
const gridPixelSize = useGridPixelSize();
|
||||
const gridOffset = useGridOffset();
|
||||
@ -45,7 +44,7 @@ function Grid({ stroke }) {
|
||||
|
||||
const negativeGridOffset = Vector2.multiply(gridOffset, -1);
|
||||
|
||||
let patternProps = {};
|
||||
let patternProps: Record<any, any> = {};
|
||||
if (grid.type === "square") {
|
||||
// Square grid pattern is 150 DPI
|
||||
const scale = gridCellPixelSize.width / 300;
|
@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
import { Link as ThemeLink } from "theme-ui";
|
||||
import { Link as ThemeLink, LinkProps } from "theme-ui";
|
||||
import { HashLink as RouterLink } from "react-router-hash-link";
|
||||
|
||||
function Link({ to, ...rest }) {
|
||||
function Link({ to, ...rest }: { to: string } & LinkProps) {
|
||||
return (
|
||||
<RouterLink to={to}>
|
||||
<ThemeLink as="span" {...rest} />
|
@ -1,9 +1,14 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Progress } from "theme-ui";
|
||||
|
||||
function LoadingBar({ isLoading, loadingProgressRef }) {
|
||||
const requestRef = useRef();
|
||||
const progressBarRef = useRef();
|
||||
type LoadingBarProps = {
|
||||
isLoading: boolean;
|
||||
loadingProgressRef: React.MutableRefObject<number>;
|
||||
};
|
||||
|
||||
function LoadingBar({ isLoading, loadingProgressRef }: LoadingBarProps) {
|
||||
const requestRef = useRef<number>();
|
||||
const progressBarRef = useRef<HTMLProgressElement>(null);
|
||||
|
||||
// Use an animation frame to update the progress bar
|
||||
// This bypasses react allowing the animation to be smooth
|
||||
@ -21,7 +26,9 @@ function LoadingBar({ isLoading, loadingProgressRef }) {
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (requestRef.current !== undefined) {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
@ -1,24 +1,26 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Text,
|
||||
TextProps,
|
||||
Image as UIImage,
|
||||
ImageProps,
|
||||
Link as UILink,
|
||||
Message,
|
||||
Embed,
|
||||
} from "theme-ui";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
function Paragraph(props) {
|
||||
function Paragraph(props: TextProps) {
|
||||
return <Text as="p" my={2} variant="body2" {...props} />;
|
||||
}
|
||||
|
||||
function Heading({ level, ...props }) {
|
||||
function Heading({ level, ...props }: { level: number } & TextProps) {
|
||||
const fontSize = level === 1 ? 5 : level === 2 ? 3 : 1;
|
||||
return (
|
||||
<Text
|
||||
mt={2}
|
||||
mb={1}
|
||||
as={`h${level}`}
|
||||
as={`h${level}` as React.ElementType}
|
||||
sx={{ fontSize }}
|
||||
variant="heading"
|
||||
{...props}
|
||||
@ -26,11 +28,11 @@ function Heading({ level, ...props }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Image(props) {
|
||||
function Image(props: ImageProps) {
|
||||
if (props.alt === "embed:") {
|
||||
return <Embed as="span" sx={{ display: "block" }} src={props.src} my={2} />;
|
||||
}
|
||||
if (props.src.endsWith(".mp4")) {
|
||||
if (props.src?.endsWith(".mp4")) {
|
||||
return (
|
||||
<video
|
||||
style={{ width: "100%", margin: "8px 0" }}
|
||||
@ -39,7 +41,7 @@ function Image(props) {
|
||||
playsInline
|
||||
loop
|
||||
controls
|
||||
{...props}
|
||||
src={props.src}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -47,11 +49,17 @@ function Image(props) {
|
||||
return <UIImage mt={2} sx={{ borderRadius: "4px" }} {...props} />;
|
||||
}
|
||||
|
||||
function ListItem(props) {
|
||||
function ListItem(props: TextProps) {
|
||||
return <Text as="li" variant="body2" my={1} {...props} />;
|
||||
}
|
||||
|
||||
function Code({ children, value }) {
|
||||
function Code({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
let variant = "";
|
||||
if (value.startsWith("Warning:")) {
|
||||
variant = "warning";
|
||||
@ -71,7 +79,7 @@ function Code({ children, value }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Table({ children }) {
|
||||
function Table({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Text
|
||||
as="table"
|
||||
@ -83,7 +91,7 @@ function Table({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead(props) {
|
||||
function TableHead(props: TextProps) {
|
||||
return (
|
||||
<Text
|
||||
as="thead"
|
||||
@ -94,7 +102,7 @@ function TableHead(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody(props) {
|
||||
function TableBody(props: TextProps) {
|
||||
return (
|
||||
<Text
|
||||
as="tbody"
|
||||
@ -105,7 +113,7 @@ function TableBody(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ children }) {
|
||||
function TableRow({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Text
|
||||
as="tr"
|
||||
@ -119,7 +127,7 @@ function TableRow({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ children }) {
|
||||
function TableCell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Text as="td" p={2}>
|
||||
{children}
|
||||
@ -127,11 +135,17 @@ function TableCell({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Link({ href, children }) {
|
||||
function Link({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return <UILink href={href}>{children}</UILink>;
|
||||
}
|
||||
|
||||
function Markdown({ source, assets }) {
|
||||
function Markdown({
|
||||
source,
|
||||
assets,
|
||||
}: {
|
||||
source: string;
|
||||
assets: Record<string, string>;
|
||||
}) {
|
||||
const renderers = {
|
||||
paragraph: Paragraph,
|
||||
heading: Heading,
|
@ -1,7 +1,17 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
import { IconButton, IconButtonProps } from "theme-ui";
|
||||
|
||||
function RadioIconButton({ title, onClick, isSelected, disabled, children }) {
|
||||
type RadioButttonProps = {
|
||||
isSelected: boolean;
|
||||
} & IconButtonProps;
|
||||
|
||||
function RadioIconButton({
|
||||
title,
|
||||
onClick,
|
||||
isSelected,
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
}: RadioButttonProps) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={title}
|
||||
@ -9,6 +19,7 @@ function RadioIconButton({ title, onClick, isSelected, disabled, children }) {
|
||||
onClick={onClick}
|
||||
sx={{ color: isSelected ? "primary" : "text" }}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
@ -1,76 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactSelect from "react-select";
|
||||
import Creatable from "react-select/creatable";
|
||||
import { useThemeUI } from "theme-ui";
|
||||
|
||||
function Select({ creatable, ...props }) {
|
||||
const { theme } = useThemeUI();
|
||||
|
||||
const Component = creatable ? Creatable : ReactSelect;
|
||||
|
||||
return (
|
||||
<Component
|
||||
styles={{
|
||||
menu: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: theme.colors.background,
|
||||
color: theme.colors.text,
|
||||
borderRadius: "4px",
|
||||
borderColor: theme.colors.gray,
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px",
|
||||
fontFamily: theme.fonts.body2,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: "transparent",
|
||||
color: theme.colors.text,
|
||||
borderColor: theme.colors.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
color: theme.colors.text,
|
||||
fontFamily: theme.fonts.body2,
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
color: theme.colors.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
dropdownIndicator: (provided, state) => ({
|
||||
...provided,
|
||||
color: theme.colors.text,
|
||||
":hover": {
|
||||
color: state.isDisabled
|
||||
? theme.colors.disabled
|
||||
: theme.colors.primary,
|
||||
},
|
||||
}),
|
||||
input: (provided, state) => ({
|
||||
...provided,
|
||||
color: theme.colors.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
margin: "4px 0",
|
||||
}),
|
||||
}}
|
||||
theme={(t) => ({
|
||||
...t,
|
||||
colors: {
|
||||
...t.colors,
|
||||
primary: theme.colors.primary,
|
||||
primary50: theme.colors.secondary,
|
||||
primary25: theme.colors.highlight,
|
||||
},
|
||||
})}
|
||||
captureMenuScroll={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
79
src/components/Select.tsx
Normal file
79
src/components/Select.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import ReactSelect, { Props } from "react-select";
|
||||
import Creatable from "react-select/creatable";
|
||||
import { useThemeUI } from "theme-ui";
|
||||
|
||||
type SelectProps = {
|
||||
creatable: boolean;
|
||||
} & Props;
|
||||
|
||||
function Select({ creatable, ...props }: SelectProps) {
|
||||
const { theme } = useThemeUI();
|
||||
|
||||
const Component = creatable ? Creatable : (ReactSelect as any);
|
||||
|
||||
return (
|
||||
<Component
|
||||
styles={{
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
backgroundColor: theme.colors?.background,
|
||||
color: theme.colors?.text,
|
||||
borderRadius: "4px",
|
||||
borderColor: theme.colors?.gray,
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px",
|
||||
fontFamily: (theme.fonts as any)?.body2,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
control: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
backgroundColor: "transparent",
|
||||
color: theme.colors?.text,
|
||||
borderColor: theme.colors?.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
singleValue: (provided: any) => ({
|
||||
...provided,
|
||||
color: theme.colors?.text,
|
||||
fontFamily: (theme.fonts as any).body2,
|
||||
}),
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
color: theme.colors?.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
dropdownIndicator: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
color: theme.colors?.text,
|
||||
":hover": {
|
||||
color: state.isDisabled
|
||||
? theme.colors?.disabled
|
||||
: theme.colors?.primary,
|
||||
},
|
||||
}),
|
||||
input: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
color: theme.colors?.text,
|
||||
opacity: state.isDisabled ? 0.5 : 1,
|
||||
}),
|
||||
container: (provided: any) => ({
|
||||
...provided,
|
||||
margin: "4px 0",
|
||||
}),
|
||||
}}
|
||||
theme={(t: any) => ({
|
||||
...t,
|
||||
colors: {
|
||||
...t.colors,
|
||||
primary: theme.colors?.primary,
|
||||
primary50: theme.colors?.secondary,
|
||||
primary25: theme.colors?.highlight,
|
||||
},
|
||||
})}
|
||||
captureMenuScroll={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import SettingsIcon from "../icons/SettingsIcon";
|
@ -2,15 +2,21 @@ import { useState } from "react";
|
||||
import { Box, Slider as ThemeSlider, SliderProps } from "theme-ui";
|
||||
|
||||
type SliderModalProps = SliderProps & {
|
||||
min: number,
|
||||
max: number,
|
||||
value: number,
|
||||
ml: any,
|
||||
mr: any,
|
||||
labelFunc: any
|
||||
}
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
labelFunc: (value: number) => string;
|
||||
};
|
||||
|
||||
function Slider({ min, max, value, ml, mr, labelFunc, ...rest }: SliderModalProps ) {
|
||||
function Slider({
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
ml,
|
||||
mr,
|
||||
labelFunc,
|
||||
...rest
|
||||
}: SliderModalProps) {
|
||||
const percentValue = ((value - min) * 100) / (max - min);
|
||||
|
||||
const [labelVisible, setLabelVisible] = useState<boolean>(false);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Box } from "theme-ui";
|
||||
|
||||
import "./Spinner.css";
|
@ -1,8 +0,0 @@
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import "./TextareaAutoSize.css";
|
||||
|
||||
function StyledTextareaAutoSize(props) {
|
||||
return <TextareaAutosize className="textarea-auto-size" {...props} />;
|
||||
}
|
||||
|
||||
export default StyledTextareaAutoSize;
|
10
src/components/TextareaAutoSize.tsx
Normal file
10
src/components/TextareaAutoSize.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import TextareaAutosize, {
|
||||
TextareaAutosizeProps,
|
||||
} from "react-textarea-autosize";
|
||||
import "./TextareaAutoSize.css";
|
||||
|
||||
function StyledTextareaAutoSize(props: TextareaAutosizeProps) {
|
||||
return <TextareaAutosize className="textarea-auto-size" {...props} />;
|
||||
}
|
||||
|
||||
export default StyledTextareaAutoSize;
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Box, Text } from "theme-ui";
|
||||
import { ToastProvider as DefaultToastProvider } from "react-toast-notifications";
|
||||
|
||||
function CustomToast({ children }) {
|
||||
function CustomToast({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
m={2}
|
||||
@ -17,7 +17,7 @@ function CustomToast({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ToastProvider({ children }) {
|
||||
export function ToastProvider({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<DefaultToastProvider
|
||||
components={{ Toast: CustomToast }}
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Text } from "theme-ui";
|
||||
|
||||
import LoadingOverlay from "./LoadingOverlay";
|
||||
@ -21,7 +21,7 @@ const facts = [
|
||||
];
|
||||
|
||||
function UpgradingLoadingOverlay() {
|
||||
const [subText, setSubText] = useState();
|
||||
const [subText, setSubText] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
@ -33,7 +33,7 @@ function UpgradingLoadingOverlay() {
|
||||
}
|
||||
|
||||
// Show first fact after 10 seconds then every 20 seconds after that
|
||||
let interval;
|
||||
let interval: NodeJS.Timeout;
|
||||
let timeout = setTimeout(() => {
|
||||
updateFact();
|
||||
interval = setInterval(() => {
|
17
src/global.d.ts
vendored
17
src/global.d.ts
vendored
@ -1,8 +1,9 @@
|
||||
declare module 'pepjs';
|
||||
declare module 'socket.io-msgpack-parser';
|
||||
declare module 'fake-indexeddb';
|
||||
declare module 'fake-indexeddb/lib/FDBKeyRange';
|
||||
declare module '*.glb';
|
||||
declare module '*.png';
|
||||
declare module '*.mp4';
|
||||
declare module '*.bin';
|
||||
declare module "pepjs";
|
||||
declare module "socket.io-msgpack-parser";
|
||||
declare module "fake-indexeddb";
|
||||
declare module "fake-indexeddb/lib/FDBKeyRange";
|
||||
declare module "*.glb";
|
||||
declare module "*.png";
|
||||
declare module "*.mp4";
|
||||
declare module "*.bin";
|
||||
declare module "react-router-hash-link";
|
||||
|
24
yarn.lock
24
yarn.lock
@ -3056,6 +3056,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||
|
||||
"@types/react-dom@*":
|
||||
version "17.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
|
||||
integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@^17.0.5":
|
||||
version "17.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227"
|
||||
@ -3087,6 +3094,23 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-select@^4.0.17":
|
||||
version "4.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-4.0.17.tgz#2e5ab4042c09c988bfc2711550329b0c3c9f8513"
|
||||
integrity sha512-ZK5wcBhJaqC8ntQl0CJvK2KXNNsk1k5flM7jO+vNPPlceRzdJQazA6zTtQUyNr6exp5yrAiwiudtYxgGlgGHLg==
|
||||
dependencies:
|
||||
"@emotion/serialize" "^1.0.0"
|
||||
"@types/react" "*"
|
||||
"@types/react-dom" "*"
|
||||
"@types/react-transition-group" "*"
|
||||
|
||||
"@types/react-transition-group@*":
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.2.tgz#38890fd9db68bf1f2252b99a942998dc7877c5b3"
|
||||
integrity sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^17.0.6":
|
||||
version "17.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.6.tgz#0ec564566302c562bf497d73219797a5e0297013"
|
||||
|
Loading…
Reference in New Issue
Block a user