Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 1860 additions and 0 deletions
import { internalEmit } from "../../lib/eventEmitter";
import { isModalClosing } from "../../components/ui/Modal";
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";
export interface Props {
screen: Screen;
openScreen: (screen: Screen) => void;
}
export default function Modals({ screen, openScreen }: Props) {
const onClose = () =>
isModalClosing || screen.id === "onboarding"
? openScreen({ id: "none" })
: internalEmit("Modal", "close");
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;
}
import { useContext } from "preact/hooks";
import { internalEmit } from "../../lib/eventEmitter";
import { isModalClosing } from "../../components/ui/Modal";
import { IntermediateContext, useIntermediate } from "./Intermediate";
import { SpecialInputModal } from "./modals/Input";
import { SpecialPromptModal } from "./modals/Prompt";
import { ChannelInfo } from "./popovers/ChannelInfo";
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 onClose = () =>
isModalClosing
? openScreen({ id: "none" })
: internalEmit("Modal", "close");
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;
}
import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal";
interface Props {
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,
children: <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>
);
}
import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal";
interface Props {
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,
children: <Text id="app.special.modals.actions.ok" />,
},
{
onClick: () => location.reload(),
children: <Text id="app.special.modals.actions.reload" />,
},
]}>
<Text id={`error.${error}`}>{error}</Text>
</Modal>
);
}
import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid";
import { Text } from "preact-i18n";
import { useContext, useState } from "preact/hooks";
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>;
}
export function InputModal({
onClose,
question,
field,
defaultValue,
callback,
}: Props) {
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,
children: <Text id="app.special.modals.actions.ok" />,
onClick: () => {
setProcessing(true);
callback(value)
.then(onClose)
.catch((err) => {
setError(takeError(err));
setProcessing(false);
});
},
},
{
children: <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: Server; callback: (id: string) => void }
);
export function SpecialInputModal(props: SpecialProps) {
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: [],
});
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 props.server.createRole(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.edit({
status: {
...client.user?.status,
text: text.trim().length > 0 ? text : undefined,
},
})
}
/>
);
}
case "add_friend": {
return (
<InputModal
onClose={onClose}
question={"Add Friend"}
callback={(username) =>
client
.req(
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined)
}
/>
);
}
default:
return null;
}
}
.onboarding {
height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
div {
flex: 1;
&.header {
gap: 8px;
padding: 3em;
display: flex;
text-align: center;
h1 {
margin: 0;
}
}
&.form {
flex-grow: 1;
max-width: 420px;
img {
margin: auto;
display: block;
max-height: 420px;
border-radius: var(--border-radius);
}
input {
width: 100%;
}
button {
display: block;
margin: 24px 0;
margin-left: auto;
}
}
}
}
import { SubmitHandler, useForm } from "react-hook-form";
import styles from "./Onboarding.module.scss";
import { Text } from "preact-i18n";
import { useState } from "preact/hooks";
import wideSVG from "../../../assets/wide.svg";
import Button from "../../../components/ui/Button";
import Preloader from "../../../components/ui/Preloader";
import FormField from "../../../pages/login/FormField";
import { takeError } from "../../revoltjs/util";
interface Props {
onClose: () => void;
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
}
interface FormInputs {
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 onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
setLoading(true);
callback(username, true)
.then(() => onClose())
.catch((err: unknown) => {
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} loading="eager" />
</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>
);
}
.invite {
display: flex;
flex-direction: column;
code {
padding: 1em;
user-select: all;
font-size: 1.4em;
text-align: center;
font-family: var(--monospace-font);
}
}
.column {
display: flex;
align-items: center;
flex-direction: column;
}
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { ulid } from "ulid";
import styles from "./Prompt.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
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 { takeError } from "../../revoltjs/util";
import { useIntermediate } from "../Intermediate";
interface Props {
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>
);
}
type SpecialProps = { onClose: () => void } & (
| { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channel }
| { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: MessageI }
| {
type: "create_invite";
target: Channel;
}
| { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User }
| { type: "create_channel"; target: Server }
);
export const SpecialPromptModal = observer((props: SpecialProps) => {
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 event = EVENTS[props.type];
let name;
switch (props.type) {
case "unfriend_user":
case "block_user":
name = props.target.username;
break;
case "close_dm":
name = props.target.recipient?.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,
children: (
<Text
id={`app.special.modals.actions.${event[1]}`}
/>
),
onClick: async () => {
setProcessing(true);
try {
switch (props.type) {
case "unfriend_user":
await props.target.removeFriend();
break;
case "block_user":
await props.target.blockUser();
break;
case "leave_group":
case "close_dm":
case "delete_channel":
props.target.delete();
break;
case "leave_server":
case "delete_server":
props.target.delete();
break;
}
onClose();
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
},
},
{
children: (
<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,
children: (
<Text id="app.special.modals.actions.delete" />
),
onClick: async () => {
setProcessing(true);
try {
props.target.delete();
onClose();
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
},
},
{
children: (
<Text id="app.special.modals.actions.cancel" />
),
onClick: onClose,
plain: true,
},
]}
content={
<>
<Text
id={`app.special.modals.prompt.confirm_delete_message_long`}
/>
<Message
message={props.target}
head={true}
contrast
/>
</>
}
disabled={processing}
error={error}
/>
);
}
case "create_invite": {
const [code, setCode] = useState("abcdef");
const { writeClipboard } = useIntermediate();
useEffect(() => {
setProcessing(true);
props.target
.createInvite()
.then((code) => setCode(code))
.catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false));
}, [props.target]);
return (
<PromptModal
onClose={onClose}
question={<Text id={`app.context_menu.create_invite`} />}
actions={[
{
children: (
<Text id="app.special.modals.actions.ok" />
),
confirmation: true,
onClick: onClose,
},
{
children: <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": {
return (
<PromptModal
onClose={onClose}
question={<Text id={`app.context_menu.kick_member`} />}
actions={[
{
children: (
<Text id="app.special.modals.actions.kick" />
),
contrast: true,
error: true,
confirmation: true,
onClick: async () => {
setProcessing(true);
try {
client.members
.getKey({
server: props.target._id,
user: props.user._id,
})
?.kick();
onClose();
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
},
},
{
children: (
<Text id="app.special.modals.actions.cancel" />
),
onClick: onClose,
},
]}
content={
<div className={styles.column}>
<UserIcon target={props.user} size={64} />
<Text
id="app.special.modals.prompt.confirm_kick"
fields={{ name: props.user?.username }}
/>
</div>
}
disabled={processing}
error={error}
/>
);
}
case "ban_member": {
const [reason, setReason] = useState<string | undefined>(undefined);
return (
<PromptModal
onClose={onClose}
question={<Text id={`app.context_menu.ban_member`} />}
actions={[
{
children: (
<Text id="app.special.modals.actions.ban" />
),
contrast: true,
error: true,
confirmation: true,
onClick: async () => {
setProcessing(true);
try {
await props.target.banUser(props.user._id, {
reason,
});
onClose();
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
},
},
{
children: (
<Text id="app.special.modals.actions.cancel" />
),
onClick: onClose,
},
]}
content={
<div className={styles.column}>
<UserIcon target={props.user} size={64} />
<Text
id="app.special.modals.prompt.confirm_ban"
fields={{ name: props.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,
children: (
<Text id="app.special.modals.actions.create" />
),
onClick: async () => {
setProcessing(true);
try {
const channel =
await props.target.createChannel({
type,
name,
nonce: ulid(),
});
history.push(
`/server/${props.target._id}/channel/${channel._id}`,
);
onClose();
} catch (err) {
setError(takeError(err));
setProcessing(false);
}
},
},
{
children: (
<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;
}
});
import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal";
interface Props {
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,
children: <Text id="app.special.modals.actions.ok" />,
},
]}
/>
);
}
.info {
.header {
display: flex;
align-items: center;
flex-direction: row;
h1 {
margin: 0;
flex-grow: 1;
}
div {
cursor: pointer;
}
}
}
import { X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styles from "./ChannelInfo.module.scss";
import Modal from "../../../components/ui/Modal";
import Markdown from "../../../components/markdown/Markdown";
import { getChannelName } from "../../revoltjs/util";
interface Props {
channel: Channel;
onClose: () => void;
}
export const ChannelInfo = observer(({ channel, onClose }: Props) => {
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(channel, true)}</h1>
<div onClick={onClose}>
<X size={36} />
</div>
</div>
<p>
<Markdown content={channel.description!} />
</p>
</div>
</Modal>
);
});
.viewer {
display: flex;
flex-direction: column;
border-end-end-radius: 4px;
border-end-start-radius: 4px;
overflow: hidden;
img {
width: auto;
height: auto;
max-width: 90vw;
max-height: 75vh;
border-bottom: thin solid var(--tertiary-foreground);
object-fit: contain;
}
}
/* eslint-disable react-hooks/rules-of-hooks */
import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
import { EmbedImage } from "revolt-api/types/January";
import styles from "./ImageViewer.module.scss";
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
import Modal from "../../../components/ui/Modal";
import { useClient } from "../../revoltjs/RevoltClient";
interface Props {
onClose: () => void;
embed?: EmbedImage;
attachment?: Attachment;
}
type ImageMetadata = AttachmentMetadata & { type: "Image" };
export function ImageViewer({ attachment, embed, onClose }: Props) {
if (attachment && attachment.metadata.type !== "Image") {
console.warn(
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
);
return null;
}
const client = useClient();
return (
<Modal visible={true} onClose={onClose} noBackground>
<div className={styles.viewer}>
{attachment && (
<>
<img
loading="eager"
src={client.generateFileURL(attachment)}
width={(attachment.metadata as ImageMetadata).width}
height={
(attachment.metadata as ImageMetadata).height
}
/>
<AttachmentActions attachment={attachment} />
</>
)}
{embed && (
<>
<img
loading="eager"
src={client.proxyFile(embed.url)}
width={embed.width}
height={embed.height}
/>
<EmbedMediaActions embed={embed} />
</>
)}
</div>
</Modal>
);
}
import { SubmitHandler, useForm } from "react-hook-form";
import { Text } from "preact-i18n";
import { useContext, useState } from "preact/hooks";
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";
}
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) {
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));
}
};
return (
<Modal
visible={true}
onClose={onClose}
title={<Text id={`app.special.modals.account.change.${field}`} />}
actions={[
{
confirmation: true,
onClick: handleSubmit(onSubmit),
children:
field === "email" ? (
<Text id="app.special.modals.actions.send_email" />
) : (
<Text id="app.special.modals.actions.update" />
),
},
{
onClick: onClose,
children: <Text id="app.special.modals.actions.close" />,
},
]}>
{/* Preact / React typing incompatabilities */}
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(
onSubmit,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
)(e as any);
}}>
{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>
);
}
import { observer } from "mobx-react-lite";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal";
import { Friend } from "../../../pages/friends/Friend";
interface Props {
users: User[];
onClose: () => void;
}
export const PendingRequests = observer(({ users, onClose }: Props) => {
return (
<Modal
visible={true}
title={<Text id="app.special.friends.pending" />}
onClose={onClose}>
<div className={styles.list}>
{users.map((x) => (
<Friend user={x!} key={x!._id} />
))}
</div>
</Modal>
);
});
.list {
width: 400px;
max-width: 100%;
max-height: 360px;
overflow-y: scroll;
> label {
> span {
align-items: flex-start !important;
> span {
display: flex;
padding: 4px;
flex-direction: row;
gap: 10px;
justify-content: flex-start;
align-items: center;
}
}
}
}
import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n";
import { useState } from "preact/hooks";
import UserCheckbox from "../../../components/common/user/UserCheckbox";
import Modal from "../../../components/ui/Modal";
import { useClient } from "../../revoltjs/RevoltClient";
interface Props {
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 client = useClient();
return (
<Modal
visible={true}
title={<Text id="app.special.popovers.user_picker.select" />}
onClose={props.onClose}
actions={[
{
children: <Text id="app.special.modals.actions.ok" />,
onClick: () => props.callback(selected).then(props.onClose),
},
]}>
<div className={styles.list}>
{[...client.users.values()]
.filter(
(x) =>
x &&
x.relationship === RelationshipStatus.Friend &&
!omit.includes(x._id),
)
.map((x) => (
<UserCheckbox
key={x._id}
user={x}
checked={selected.includes(x._id)}
onChange={(v) => {
if (v) {
setSelected([...selected, x._id]);
} else {
setSelected(
selected.filter((y) => y !== x._id),
);
}
}}
/>
))}
</div>
</Modal>
);
}
.modal {
height: 460px;
display: flex;
padding: 0 !important;
flex-direction: column;
}
.header {
background-size: cover;
border-radius: var(--border-radius) var(--border-radius) 0 0;
background-position: center;
background-color: var(--secondary-background);
&[data-force="light"] {
color: white;
}
&[data-force="dark"] {
color: black;
}
}
.profile {
gap: 16px;
width: 560px;
display: flex;
padding: 20px;
max-width: 100%;
align-items: center;
flex-direction: row;
.details {
flex-grow: 1;
min-width: 0;
display: flex;
flex-direction: column;
* {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.username {
font-size: 22px;
font-weight: 600;
}
.status {
font-size: 13px;
}
}
}
.tabs {
gap: 8px;
display: flex;
padding: 0 1.5em;
font-size: 0.875rem;
> div {
padding: 8px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: border-bottom 0.3s;
&[data-active="true"] {
border-bottom: 2px solid var(--foreground);
cursor: default;
}
&:hover:not([data-active="true"]) {
border-bottom: 2px solid var(--tertiary-foreground);
}
}
}
.content {
gap: 8px;
height: 100%;
display: flex;
padding: 1em 1.5em;
max-width: 560px;
max-height: 240px;
overflow-y: auto;
flex-direction: column;
background: var(--primary-background);
border-radius: 0 0 8px 8px;
.empty {
display: flex;
justify-content: center;
align-items: center;
}
.category {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
color: var(--tertiary-foreground);
margin-bottom: 8px;
&:not(:first-child) {
margin-top: 8px;
}
}
> div {
> span {
font-size: 15px;
}
}
}
.badges {
gap: 8px;
display: flex;
margin-top: 4px;
flex-direction: row;
img {
width: 32px;
height: 32px;
cursor: pointer;
}
}
.entries {
gap: 8px;
display: flex;
flex-direction: column;
a {
min-width: 0;
}
.entry {
gap: 8px;
min-width: 0;
padding: 12px;
display: flex;
cursor: pointer;
align-items: center;
transition: background-color 0.1s;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background);
&:hover {
background-color: var(--primary-background);
}
img {
width: 32px;
height: 32px;
border-radius: 50%;
}
span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
import { Money } from "@styled-icons/boxicons-regular";
import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom";
import { Profile, RelationshipStatus } from "revolt-api/types/Users";
import { UserPermission } from "revolt.js/dist/api/permissions";
import { Route } from "revolt.js/dist/api/routes";
import styles from "./UserProfile.module.scss";
import { Localizer, Text } from "preact-i18n";
import { useContext, useEffect, useLayoutEffect, useState } from "preact/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 {
ClientStatus,
StatusContext,
useClient,
} from "../../revoltjs/RevoltClient";
import { useIntermediate } from "../Intermediate";
interface Props {
user_id: string;
dummy?: boolean;
onClose?: () => void;
dummyProfile?: Profile;
}
enum Badges {
Developer = 1,
Translator = 2,
Supporter = 4,
ResponsibleDisclosure = 8,
EarlyAdopter = 256,
}
export const UserProfile = observer(
({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate();
const [profile, setProfile] = useState<undefined | null | Profile>(
undefined,
);
const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const history = useHistory();
const client = useClient();
const status = useContext(StatusContext);
const [tab, setTab] = useState("profile");
const user = client.users.get(user_id);
if (!user) {
if (onClose) useEffect(onClose, []);
return null;
}
const users = mutual?.users.map((id) => client.users.get(id));
const mutualGroups = [...client.channels.values()].filter(
(channel) =>
channel?.channel_type === "Group" &&
channel.recipient_ids!.includes(user_id),
);
useLayoutEffect(() => {
if (!user_id) return;
if (typeof profile !== "undefined") setProfile(undefined);
if (typeof mutual !== "undefined") setMutual(undefined);
// eslint-disable-next-line
}, [user_id]);
useEffect(() => {
if (dummy) {
setProfile(dummyProfile);
}
}, [dummy, dummyProfile]);
useEffect(() => {
if (dummy) return;
if (
status === ClientStatus.ONLINE &&
typeof mutual === "undefined"
) {
setMutual(null);
user.fetchMutual().then(setMutual);
}
}, [mutual, status, dummy, user]);
useEffect(() => {
if (dummy) return;
if (
status === ClientStatus.ONLINE &&
typeof profile === "undefined"
) {
setProfile(null);
if (user.permission & UserPermission.ViewProfile) {
user.fetchProfile().then(setProfile);
}
}
}, [profile, status, dummy, user]);
const backgroundURL =
profile &&
client.generateFileURL(profile.background, { width: 1000 }, true);
const badges = user.badges ?? 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 animate />
<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} tooltip />
</span>
)}
</div>
{user.relationship === RelationshipStatus.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 === RelationshipStatus.User && (
<IconButton
onClick={() => {
onClose?.();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)}
{(user.relationship === RelationshipStatus.Incoming ||
user.relationship === RelationshipStatus.None) && (
<IconButton onClick={() => user.addFriend()}>
<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 !== RelationshipStatus.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>
);
},
);