Run prettier on all files.

This commit is contained in:
Paul
2021-07-05 11:23:23 +01:00
parent 50cd6fc1ee
commit a9ce64c9fe
181 changed files with 18084 additions and 13521 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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" />,
},
]}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}