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 1503 additions and 690 deletions
import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useHistory } from "react-router"; import { useContext, useState } from "preact/hooks";
import InputBox from "../../../components/ui/InputBox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import Overline from "../../../components/ui/Overline";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import { takeError } from "../../revoltjs/util";
import { useContext, useState } from "preact/hooks";
import Overline from '../../../components/ui/Overline';
import InputBox from '../../../components/ui/InputBox';
import { AppContext } from "../../revoltjs/RevoltClient"; import { AppContext } from "../../revoltjs/RevoltClient";
import { takeError } from "../../revoltjs/util";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
...@@ -22,7 +26,7 @@ export function InputModal({ ...@@ -22,7 +26,7 @@ export function InputModal({
question, question,
field, field,
defaultValue, defaultValue,
callback callback,
}: Props) { }: Props) {
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [value, setValue] = useState(defaultValue ?? ""); const [value, setValue] = useState(defaultValue ?? "");
...@@ -35,39 +39,51 @@ export function InputModal({ ...@@ -35,39 +39,51 @@ export function InputModal({
disabled={processing} disabled={processing}
actions={[ actions={[
{ {
text: <Text id="app.special.modals.actions.ok" />, confirmation: true,
children: <Text id="app.special.modals.actions.ok" />,
onClick: () => { onClick: () => {
setProcessing(true); setProcessing(true);
callback(value) callback(value)
.then(onClose) .then(onClose)
.catch(err => { .catch((err) => {
setError(takeError(err)); setError(takeError(err));
setProcessing(false) setProcessing(false);
}) });
} },
}, },
{ {
text: <Text id="app.special.modals.actions.cancel" />, children: <Text id="app.special.modals.actions.cancel" />,
onClick: onClose onClick: onClose,
} },
]} ]}
onClose={onClose} onClose={onClose}>
> <form>
{ field ? <Overline error={error} block> {field ? (
{field} <Overline error={error} block>
</Overline> : (error && <Overline error={error} type="error" block />) } {field}
<InputBox </Overline>
value={value} ) : (
onChange={e => setValue(e.currentTarget.value)} error && <Overline error={error} type="error" block />
/> )}
<InputBox
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</form>
</Modal> </Modal>
); );
} }
type SpecialProps = { onClose: () => void } & ( type SpecialProps = { onClose: () => void } & (
{ type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | | {
{ type: "create_channel", server: string } type:
) | "create_group"
| "create_server"
| "set_custom_status"
| "add_friend";
}
| { type: "create_role"; server: Server; callback: (id: string) => void }
);
export function SpecialInputModal(props: SpecialProps) { export function SpecialInputModal(props: SpecialProps) {
const history = useHistory(); const history = useHistory();
...@@ -76,83 +92,90 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -76,83 +92,90 @@ export function SpecialInputModal(props: SpecialProps) {
const { onClose } = props; const { onClose } = props;
switch (props.type) { switch (props.type) {
case "create_group": { case "create_group": {
return <InputModal return (
onClose={onClose} <InputModal
question={<Text id="app.main.groups.create" />} onClose={onClose}
field={<Text id="app.main.groups.name" />} question={<Text id="app.main.groups.create" />}
callback={async name => { field={<Text id="app.main.groups.name" />}
const group = await client.channels.createGroup( callback={async (name) => {
{ const group = await client.channels.createGroup({
name, name,
nonce: ulid(), nonce: ulid(),
users: [] users: [],
} });
);
history.push(`/channel/${group._id}`); history.push(`/channel/${group._id}`);
}} }}
/>; />
);
} }
case "create_server": { case "create_server": {
return <InputModal return (
onClose={onClose} <InputModal
question={<Text id="app.main.servers.create" />} onClose={onClose}
field={<Text id="app.main.servers.name" />} question={<Text id="app.main.servers.create" />}
callback={async name => { field={<Text id="app.main.servers.name" />}
const server = await client.servers.createServer( callback={async (name) => {
{ const server = await client.servers.createServer({
name, name,
nonce: ulid() nonce: ulid(),
} });
);
history.push(`/server/${server._id}`); history.push(`/server/${server._id}`);
}} }}
/>; />
);
} }
case "create_channel": { case "create_role": {
return <InputModal return (
onClose={onClose} <InputModal
question={<Text id="app.context_menu.create_channel" />} onClose={onClose}
field={<Text id="app.main.servers.channel_name" />} question={
callback={async name => { <Text id="app.settings.permissions.create_role" />
const channel = await client.servers.createChannel( }
props.server, field={<Text id="app.settings.permissions.role_name" />}
{ callback={async (name) => {
name, const role = await props.server.createRole(name);
nonce: ulid() props.callback(role.id);
} }}
); />
);
history.push(`/server/${props.server}/channel/${channel._id}`);
}}
/>;
} }
case "set_custom_status": { case "set_custom_status": {
return <InputModal return (
onClose={onClose} <InputModal
question={<Text id="app.context_menu.set_custom_status" />} onClose={onClose}
field={<Text id="app.context_menu.custom_status" />} question={<Text id="app.context_menu.set_custom_status" />}
defaultValue={client.user?.status?.text} field={<Text id="app.context_menu.custom_status" />}
callback={text => defaultValue={client.user?.status?.text}
client.users.editUser({ callback={(text) =>
status: { client.users.edit({
...client.user?.status, status: {
text ...client.user?.status,
} text: text.trim().length > 0 ? text : undefined,
}) },
} })
/>; }
/>
);
} }
case "add_friend": { case "add_friend": {
return <InputModal return (
onClose={onClose} <InputModal
question={"Add Friend"} onClose={onClose}
callback={username => question={"Add Friend"}
client.users.addFriend(username) callback={(username) =>
} client
/>; .req(
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined)
}
/>
);
} }
default: return null; default:
return null;
} }
} }
.onboarding { .onboarding {
height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
...@@ -7,7 +8,9 @@ ...@@ -7,7 +8,9 @@
flex: 1; flex: 1;
&.header { &.header {
gap: 8px;
padding: 3em; padding: 3em;
display: flex;
text-align: center; text-align: center;
h1 { h1 {
...@@ -23,7 +26,7 @@ ...@@ -23,7 +26,7 @@
margin: auto; margin: auto;
display: block; display: block;
max-height: 420px; max-height: 420px;
border-radius: 8px; border-radius: var(--border-radius);
} }
input { input {
......
import { SubmitHandler, useForm } from "react-hook-form";
import styles from "./Onboarding.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useForm } from "react-hook-form";
import styles from "./Onboarding.module.scss"; import wideSVG from "../../../assets/wide.svg";
import { takeError } from "../../revoltjs/util";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import FormField from "../../../pages/login/FormField";
import Preloader from "../../../components/ui/Preloader"; import Preloader from "../../../components/ui/Preloader";
import FormField from "../../../pages/login/FormField";
import { takeError } from "../../revoltjs/util";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
callback: (username: string, loginAfterSuccess?: true) => Promise<void>; callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
} }
interface FormInputs {
username: string;
}
export function OnboardingModal({ onClose, callback }: Props) { export function OnboardingModal({ onClose, callback }: Props) {
const { handleSubmit, register } = useForm(); const { handleSubmit, register } = useForm<FormInputs>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
async function onSubmit({ username }: { username: string }) { const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
setLoading(true); setLoading(true);
callback(username, true) callback(username, true)
.then(onClose) .then(() => onClose())
.catch((err: any) => { .catch((err: unknown) => {
setError(takeError(err)); setError(takeError(err));
setLoading(false); setLoading(false);
}); });
} };
return ( return (
<div className={styles.onboarding}> <div className={styles.onboarding}>
<div className={styles.header}> <div className={styles.header}>
<h1> <h1>
<Text id="app.special.modals.onboarding.welcome" /> <Text id="app.special.modals.onboarding.welcome" />
<img src="/assets/wide.svg" /> <img src={wideSVG} loading="eager" />
</h1> </h1>
</div> </div>
<div className={styles.form}> <div className={styles.form}>
{loading ? ( {loading ? (
<Preloader /> <Preloader type="spinner" />
) : ( ) : (
<> <>
<p> <p>
<Text id="app.special.modals.onboarding.pick" /> <Text id="app.special.modals.onboarding.pick" />
</p> </p>
<form onSubmit={handleSubmit(onSubmit) as any}> <form
onSubmit={
handleSubmit(
onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement>
}>
<div> <div>
<FormField <FormField
type="username" type="username"
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
user-select: all; user-select: all;
font-size: 1.4em; font-size: 1.4em;
text-align: center; text-align: center;
font-family: "Fira Mono"; font-family: var(--monospace-font);
} }
} }
......
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 { Text } from "preact-i18n";
import styles from './Prompt.module.scss'; import { useContext, useEffect, useState } from "preact/hooks";
import { Children } from "../../../types/Preact";
import { useIntermediate } from "../Intermediate"; 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 InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline";
import UserIcon from "../../../components/common/UserIcon";
import Modal, { Action } from "../../../components/ui/Modal"; import Modal, { Action } from "../../../components/ui/Modal";
import { Channels, Servers } from "revolt.js/dist/api/objects"; import Overline from "../../../components/ui/Overline";
import { useContext, useEffect, useState } from "preact/hooks"; import Radio from "../../../components/ui/Radio";
import { Children } from "../../../types/Preact";
import { AppContext } from "../../revoltjs/RevoltClient"; import { AppContext } from "../../revoltjs/RevoltClient";
import { takeError } from "../../revoltjs/util"; import { takeError } from "../../revoltjs/util";
import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
...@@ -20,7 +33,14 @@ interface Props { ...@@ -20,7 +33,14 @@ interface Props {
error?: string; error?: string;
} }
export function PromptModal({ onClose, question, content, actions, disabled, error }: Props) { export function PromptModal({
onClose,
question,
content,
actions,
disabled,
error,
}: Props) {
return ( return (
<Modal <Modal
visible={true} visible={true}
...@@ -28,73 +48,107 @@ export function PromptModal({ onClose, question, content, actions, disabled, err ...@@ -28,73 +48,107 @@ export function PromptModal({ onClose, question, content, actions, disabled, err
actions={actions} actions={actions}
onClose={onClose} onClose={onClose}
disabled={disabled}> disabled={disabled}>
{ error && <Overline error={error} type="error" /> } {error && <Overline error={error} type="error" />}
{ content } {content}
</Modal> </Modal>
); );
} }
type SpecialProps = { onClose: () => void } & ( type SpecialProps = { onClose: () => void } & (
{ type: "leave_group", target: Channels.GroupChannel } | | { type: "leave_group"; target: Channel }
{ type: "close_dm", target: Channels.DirectMessageChannel } | | { type: "close_dm"; target: Channel }
{ type: "leave_server", target: Servers.Server } | | { type: "leave_server"; target: Server }
{ type: "delete_server", target: Servers.Server } | | { type: "delete_server"; target: Server }
{ type: "delete_channel", target: Channels.TextChannel } | | { type: "delete_channel"; target: Channel }
{ type: "delete_message", target: Channels.Message } | | { type: "delete_message"; target: MessageI }
{ type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } | | {
{ type: "kick_member", target: Servers.Server, user: string } | type: "create_invite";
{ type: "ban_member", target: Servers.Server, user: string } target: Channel;
) }
| { type: "kick_member"; target: Server; user: User }
export function SpecialPromptModal(props: SpecialProps) { | { 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 client = useContext(AppContext);
const [ processing, setProcessing ] = useState(false); const [processing, setProcessing] = useState(false);
const [ error, setError ] = useState<undefined | string>(undefined); const [error, setError] = useState<undefined | string>(undefined);
const { onClose } = props; const { onClose } = props;
switch (props.type) { switch (props.type) {
case 'leave_group': case "leave_group":
case 'close_dm': case "close_dm":
case 'leave_server': case "leave_server":
case 'delete_server': case "delete_server":
case 'delete_message': case "delete_channel":
case 'delete_channel': { case "unfriend_user":
case "block_user": {
const EVENTS = { const EVENTS = {
'close_dm': 'confirm_close_dm', close_dm: ["confirm_close_dm", "close"],
'delete_server': 'confirm_delete', delete_server: ["confirm_delete", "delete"],
'delete_channel': 'confirm_delete', delete_channel: ["confirm_delete", "delete"],
'delete_message': 'confirm_delete_message', leave_group: ["confirm_leave", "leave"],
'leave_group': 'confirm_leave', leave_server: ["confirm_leave", "leave"],
'leave_server': 'confirm_leave' unfriend_user: ["unfriend_user", "remove"],
block_user: ["block_user", "block"],
}; };
let event = EVENTS[props.type]; const event = EVENTS[props.type];
let name = props.type === 'close_dm' ? client.users.get(client.channels.getRecipient(props.target._id))?.username : let name;
props.type === 'delete_message' ? undefined : props.target.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 ( return (
<PromptModal <PromptModal
onClose={onClose} onClose={onClose}
question={<Text question={
id={props.type === 'delete_message' ? 'app.context_menu.delete_message' : `app.special.modals.prompt.${event}`} <Text
fields={{ name }} id={`app.special.modals.prompt.${event[0]}`}
/>} fields={{ name }}
/>
}
actions={[ actions={[
{ {
confirmation: true, confirmation: true,
contrast: true, contrast: true,
error: true, error: true,
text: <Text id="app.special.modals.actions.delete" />, children: (
<Text
id={`app.special.modals.actions.${event[1]}`}
/>
),
onClick: async () => { onClick: async () => {
setProcessing(true); setProcessing(true);
try { try {
if (props.type === 'leave_group' || props.type === 'close_dm' || props.type === 'delete_channel') { switch (props.type) {
await client.channels.delete(props.target._id); case "unfriend_user":
} else if (props.type === 'delete_message') { await props.target.removeFriend();
await client.channels.deleteMessage(props.target.channel, props.target._id); break;
} else { case "block_user":
await client.servers.delete(props.target._id); 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(); onClose();
...@@ -102,28 +156,89 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -102,28 +156,89 @@ export function SpecialPromptModal(props: SpecialProps) {
setError(takeError(err)); setError(takeError(err));
setProcessing(false); 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,
}, },
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
]} ]}
content={<Text id={`app.special.modals.prompt.${event}_long`} />} content={
<>
<Text
id={`app.special.modals.prompt.confirm_delete_message_long`}
/>
<Message
message={props.target}
head={true}
contrast
/>
</>
}
disabled={processing} disabled={processing}
error={error} error={error}
/> />
) );
} }
case "create_invite": { case "create_invite": {
const [ code, setCode ] = useState('abcdef'); const [code, setCode] = useState("abcdef");
const { writeClipboard } = useIntermediate(); const { writeClipboard } = useIntermediate();
useEffect(() => { useEffect(() => {
setProcessing(true); setProcessing(true);
client.channels.createInvite(props.target._id) props.target
.then(code => setCode(code)) .createInvite()
.catch(err => setError(takeError(err))) .then((code) => setCode(code))
.catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false)); .finally(() => setProcessing(false));
}, []); }, [props.target]);
return ( return (
<PromptModal <PromptModal
...@@ -131,69 +246,89 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -131,69 +246,89 @@ export function SpecialPromptModal(props: SpecialProps) {
question={<Text id={`app.context_menu.create_invite`} />} question={<Text id={`app.context_menu.create_invite`} />}
actions={[ actions={[
{ {
text: <Text id="app.special.modals.actions.ok" />, children: (
<Text id="app.special.modals.actions.ok" />
),
confirmation: true, confirmation: true,
onClick: onClose onClick: onClose,
}, },
{ {
text: <Text id="app.context_menu.copy_link" />, children: <Text id="app.context_menu.copy_link" />,
onClick: () => writeClipboard(`${window.location.protocol}//${window.location.host}/invite/${code}`) onClick: () =>
} writeClipboard(
`${window.location.protocol}//${window.location.host}/invite/${code}`,
),
},
]} ]}
content={ content={
processing ? processing ? (
<Text id="app.special.modals.prompt.create_invite_generate" /> <Text id="app.special.modals.prompt.create_invite_generate" />
: <div className={styles.invite}> ) : (
<Text id="app.special.modals.prompt.create_invite_created" /> <div className={styles.invite}>
<code>{code}</code> <Text id="app.special.modals.prompt.create_invite_created" />
<code>{code}</code>
</div> </div>
)
} }
disabled={processing} disabled={processing}
error={error} error={error}
/> />
) );
} }
case "kick_member": { case "kick_member": {
const user = client.users.get(props.user);
return ( return (
<PromptModal <PromptModal
onClose={onClose} onClose={onClose}
question={<Text id={`app.context_menu.kick_member`} />} question={<Text id={`app.context_menu.kick_member`} />}
actions={[ actions={[
{ {
text: <Text id="app.special.modals.actions.kick" />, children: (
<Text id="app.special.modals.actions.kick" />
),
contrast: true, contrast: true,
error: true, error: true,
confirmation: true, confirmation: true,
onClick: async () => { onClick: async () => {
setProcessing(true); setProcessing(true);
try { try {
await client.servers.members.kickMember(props.target._id, props.user); client.members
.getKey({
server: props.target._id,
user: props.user._id,
})
?.kick();
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
setProcessing(false); setProcessing(false);
} }
} },
},
{
children: (
<Text id="app.special.modals.actions.cancel" />
),
onClick: onClose,
}, },
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
]} ]}
content={<div className={styles.column}> content={
<UserIcon target={user} size={64} /> <div className={styles.column}>
<Text <UserIcon target={props.user} size={64} />
id="app.special.modals.prompt.confirm_kick" <Text
fields={{ name: user?.username }} /> id="app.special.modals.prompt.confirm_kick"
</div>} fields={{ name: props.user?.username }}
/>
</div>
}
disabled={processing} disabled={processing}
error={error} error={error}
/> />
) );
} }
case "ban_member": { case "ban_member": {
const [ reason, setReason ] = useState<string | undefined>(undefined); const [reason, setReason] = useState<string | undefined>(undefined);
const user = client.users.get(props.user);
return ( return (
<PromptModal <PromptModal
...@@ -201,37 +336,130 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -201,37 +336,130 @@ export function SpecialPromptModal(props: SpecialProps) {
question={<Text id={`app.context_menu.ban_member`} />} question={<Text id={`app.context_menu.ban_member`} />}
actions={[ actions={[
{ {
text: <Text id="app.special.modals.actions.ban" />, children: (
<Text id="app.special.modals.actions.ban" />
),
contrast: true, contrast: true,
error: true, error: true,
confirmation: true, confirmation: true,
onClick: async () => { onClick: async () => {
setProcessing(true); setProcessing(true);
try { try {
await client.servers.banUser(props.target._id, props.user, { reason }); await props.target.banUser(props.user._id, {
reason,
});
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
setProcessing(false); setProcessing(false);
} }
} },
},
{
children: (
<Text id="app.special.modals.actions.cancel" />
),
onClick: onClose,
}, },
{ text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
]} ]}
content={<div className={styles.column}> content={
<UserIcon target={user} size={64} /> <div className={styles.column}>
<Text <UserIcon target={props.user} size={64} />
id="app.special.modals.prompt.confirm_ban" <Text
fields={{ name: user?.username }} /> id="app.special.modals.prompt.confirm_ban"
<Overline><Text id="app.special.modals.prompt.confirm_ban_reason" /></Overline> fields={{ name: props.user?.username }}
<InputBox value={reason ?? ''} onChange={e => setReason(e.currentTarget.value)} /> />
</div>} <Overline>
<Text id="app.special.modals.prompt.confirm_ban_reason" />
</Overline>
<InputBox
value={reason ?? ""}
onChange={(e) =>
setReason(e.currentTarget.value)
}
/>
</div>
}
disabled={processing} disabled={processing}
error={error} error={error}
/> />
) );
} }
default: return null; 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 { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
interface Props { interface Props {
...@@ -15,8 +16,8 @@ export function SignedOutModal({ onClose }: Props) { ...@@ -15,8 +16,8 @@ export function SignedOutModal({ onClose }: Props) {
{ {
onClick: onClose, onClick: onClose,
confirmation: true, confirmation: true,
text: <Text id="app.special.modals.actions.ok" /> children: <Text id="app.special.modals.actions.ok" />,
} },
]} ]}
/> />
); );
......
import { X } from "@styled-icons/feather"; 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 styles from "./ChannelInfo.module.scss";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { getChannelName } from "../../revoltjs/util";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { useChannel, useForceUpdate } from "../../revoltjs/hooks"; import { getChannelName } from "../../revoltjs/util";
interface Props { interface Props {
channel_id: string; channel: Channel;
onClose: () => void; onClose: () => void;
} }
export function ChannelInfo({ channel_id, onClose }: Props) { export const ChannelInfo = observer(({ channel, onClose }: Props) => {
const ctx = useForceUpdate(); if (
const channel = useChannel(channel_id, ctx); channel.channel_type === "DirectMessage" ||
if (!channel) return null; channel.channel_type === "SavedMessages"
) {
if (channel.channel_type === "DirectMessage" || channel.channel_type === 'SavedMessages') {
onClose(); onClose();
return null; return null;
} }
...@@ -24,15 +27,15 @@ export function ChannelInfo({ channel_id, onClose }: Props) { ...@@ -24,15 +27,15 @@ export function ChannelInfo({ channel_id, onClose }: Props) {
<Modal visible={true} onClose={onClose}> <Modal visible={true} onClose={onClose}>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.header}> <div className={styles.header}>
<h1>{ getChannelName(ctx.client, channel, [ ], true) }</h1> <h1>{getChannelName(channel, true)}</h1>
<div onClick={onClose}> <div onClick={onClose}>
<X size={36} /> <X size={36} />
</div> </div>
</div> </div>
<p> <p>
<Markdown content={channel.description} /> <Markdown content={channel.description!} />
</p> </p>
</div> </div>
</Modal> </Modal>
); );
} });
.viewer { .viewer {
display: flex;
flex-direction: column;
border-end-end-radius: 4px;
border-end-start-radius: 4px;
overflow: hidden;
img { img {
width: auto;
height: auto;
max-width: 90vw; max-width: 90vw;
max-height: 90vh; 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 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 Modal from "../../../components/ui/Modal";
import { useContext, useEffect } from "preact/hooks";
import { AppContext } from "../../revoltjs/RevoltClient"; import { useClient } from "../../revoltjs/RevoltClient";
import { Attachment, EmbedImage } from "revolt.js/dist/api/objects";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
...@@ -10,36 +16,45 @@ interface Props { ...@@ -10,36 +16,45 @@ interface Props {
attachment?: Attachment; attachment?: Attachment;
} }
export function ImageViewer({ attachment, embed, onClose }: Props) { type ImageMetadata = AttachmentMetadata & { type: "Image" };
if (attachment && attachment.metadata.type !== "Image") return null;
const client = useContext(AppContext);
useEffect(() => { export function ImageViewer({ attachment, embed, onClose }: Props) {
function keyDown(e: KeyboardEvent) { if (attachment && attachment.metadata.type !== "Image") {
if (e.key === "Escape") { console.warn(
onClose(); `Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
} );
} return null;
}
document.body.addEventListener("keydown", keyDown); const client = useClient();
return () => document.body.removeEventListener("keydown", keyDown);
}, []);
return ( return (
<Modal visible={true} onClose={onClose} noBackground> <Modal visible={true} onClose={onClose} noBackground>
<div className={styles.viewer}> <div className={styles.viewer}>
{ attachment && {attachment && (
<> <>
<img src={client.generateFileURL(attachment)} /> <img
{/*<AttachmentActions attachment={attachment} />*/} loading="eager"
src={client.generateFileURL(attachment)}
width={(attachment.metadata as ImageMetadata).width}
height={
(attachment.metadata as ImageMetadata).height
}
/>
<AttachmentActions attachment={attachment} />
</> </>
} )}
{ embed && {embed && (
<> <>
{/*<img src={proxyImage(embed.url)} />*/} <img
{/*<EmbedMediaActions embed={embed} />*/} loading="eager"
src={client.proxyFile(embed.url)}
width={embed.width}
height={embed.height}
/>
<EmbedMediaActions embed={embed} />
</> </>
} )}
</div> </div>
</Modal> </Modal>
); );
......
import { SubmitHandler, useForm } from "react-hook-form";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useForm } from "react-hook-form";
import Modal from "../../../components/ui/Modal";
import { takeError } from "../../revoltjs/util";
import { useContext, useState } from "preact/hooks"; 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 Overline from "../../../components/ui/Overline";
import FormField from "../../../pages/login/FormField";
import { AppContext } from "../../revoltjs/RevoltClient"; import { AppContext } from "../../revoltjs/RevoltClient";
import { takeError } from "../../revoltjs/util";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
field: "username" | "email" | "password"; field: "username" | "email" | "password";
} }
interface FormInputs {
password: string;
new_email: string;
new_username: string;
new_password: string;
// TODO: figure out if this is correct or not
// it wasn't in the types before this was typed but the element itself was there
current_password?: string;
}
export function ModifyAccountModal({ onClose, field }: Props) { export function ModifyAccountModal({ onClose, field }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { handleSubmit, register, errors } = useForm(); const { handleSubmit, register, errors } = useForm<FormInputs>();
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
async function onSubmit({ const onSubmit: SubmitHandler<FormInputs> = async ({
password, password,
new_username, new_username,
new_email, new_email,
new_password new_password,
}: { }) => {
password: string;
new_username: string;
new_email: string;
new_password: string;
}) {
try { try {
if (field === "email") { if (field === "email") {
await client.req("POST", "/auth/change/email", { await client.req("POST", "/auth/change/email", {
password, password,
new_email new_email,
}); });
onClose(); onClose();
} else if (field === "password") { } else if (field === "password") {
await client.req("POST", "/auth/change/password", { await client.req("POST", "/auth/change/password", {
password, password,
new_password new_password,
}); });
onClose(); onClose();
} else if (field === "username") { } else if (field === "username") {
await client.req("PATCH", "/users/id/username", { await client.req("PATCH", "/users/id/username", {
username: new_username, username: new_username,
password password,
}); });
onClose(); onClose();
} }
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
} }
} };
return ( return (
<Modal <Modal
...@@ -62,20 +71,27 @@ export function ModifyAccountModal({ onClose, field }: Props) { ...@@ -62,20 +71,27 @@ export function ModifyAccountModal({ onClose, field }: Props) {
{ {
confirmation: true, confirmation: true,
onClick: handleSubmit(onSubmit), onClick: handleSubmit(onSubmit),
text: children:
field === "email" ? ( field === "email" ? (
<Text id="app.special.modals.actions.send_email" /> <Text id="app.special.modals.actions.send_email" />
) : ( ) : (
<Text id="app.special.modals.actions.update" /> <Text id="app.special.modals.actions.update" />
) ),
}, },
{ {
onClick: onClose, onClick: onClose,
text: <Text id="app.special.modals.actions.close" /> children: <Text id="app.special.modals.actions.close" />,
} },
]} ]}>
> {/* Preact / React typing incompatabilities */}
<form onSubmit={handleSubmit(onSubmit) as any}> <form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(
onSubmit,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
)(e as any);
}}>
{field === "email" && ( {field === "email" && (
<FormField <FormField
type="email" type="email"
......
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>
);
});
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
max-height: 360px; max-height: 360px;
overflow-y: scroll; overflow-y: scroll;
// ! FIXME: very temporary code
> label { > label {
> span { > span {
align-items: flex-start !important; align-items: flex-start !important;
...@@ -18,4 +17,4 @@ ...@@ -18,4 +17,4 @@
} }
} }
} }
} }
\ No newline at end of file
import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import styles from "./UserPicker.module.scss";
import { useUsers } from "../../revoltjs/hooks"; import UserCheckbox from "../../../components/common/user/UserCheckbox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { User, Users } from "revolt.js/dist/api/objects";
import UserCheckbox from "../../../components/common/UserCheckbox"; import { useClient } from "../../revoltjs/RevoltClient";
interface Props { interface Props {
omit?: string[]; omit?: string[];
...@@ -16,7 +19,7 @@ export function UserPicker(props: Props) { ...@@ -16,7 +19,7 @@ export function UserPicker(props: Props) {
const [selected, setSelected] = useState<string[]>([]); const [selected, setSelected] = useState<string[]>([]);
const omit = [...(props.omit || []), "00000000000000000000000000"]; const omit = [...(props.omit || []), "00000000000000000000000000"];
const users = useUsers(); const client = useClient();
return ( return (
<Modal <Modal
...@@ -25,34 +28,29 @@ export function UserPicker(props: Props) { ...@@ -25,34 +28,29 @@ export function UserPicker(props: Props) {
onClose={props.onClose} onClose={props.onClose}
actions={[ actions={[
{ {
text: <Text id="app.special.modals.actions.ok" />, children: <Text id="app.special.modals.actions.ok" />,
onClick: () => props.callback(selected).then(props.onClose) onClick: () => props.callback(selected).then(props.onClose),
} },
]} ]}>
>
<div className={styles.list}> <div className={styles.list}>
{(users.filter( {[...client.users.values()]
x => .filter(
x && (x) =>
x.relationship === Users.Relationship.Friend && x &&
!omit.includes(x._id) x.relationship === RelationshipStatus.Friend &&
) as User[]) !omit.includes(x._id),
.map(x => { )
return { .map((x) => (
...x,
selected: selected.includes(x._id)
};
})
.map(x => (
<UserCheckbox <UserCheckbox
key={x._id}
user={x} user={x}
checked={x.selected} checked={selected.includes(x._id)}
onChange={v => { onChange={(v) => {
if (v) { if (v) {
setSelected([...selected, x._id]); setSelected([...selected, x._id]);
} else { } else {
setSelected( setSelected(
selected.filter(y => y !== x._id) selected.filter((y) => y !== x._id),
); );
} }
}} }}
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.header { .header {
background-size: cover; background-size: cover;
border-radius: 8px 8px 0 0; border-radius: var(--border-radius) var(--border-radius) 0 0;
background-position: center; background-position: center;
background-color: var(--secondary-background); background-color: var(--secondary-background);
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> * { * {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
...@@ -57,13 +57,13 @@ ...@@ -57,13 +57,13 @@
gap: 8px; gap: 8px;
display: flex; display: flex;
padding: 0 1.5em; padding: 0 1.5em;
font-size: .875rem; font-size: 0.875rem;
> div { > div {
padding: 8px; padding: 8px;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: border-bottom .3s; transition: border-bottom 0.3s;
&[data-active="true"] { &[data-active="true"] {
border-bottom: 2px solid var(--foreground); border-bottom: 2px solid var(--foreground);
...@@ -81,7 +81,10 @@ ...@@ -81,7 +81,10 @@
height: 100%; height: 100%;
display: flex; display: flex;
padding: 1em 1.5em; padding: 1em 1.5em;
max-width: 560px; max-width: 560px;
max-height: 240px;
overflow-y: auto; overflow-y: auto;
flex-direction: column; flex-direction: column;
background: var(--primary-background); background: var(--primary-background);
...@@ -140,11 +143,11 @@ ...@@ -140,11 +143,11 @@
padding: 12px; padding: 12px;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
border-radius: 4px;
align-items: center; align-items: center;
transition: background-color 0.1s;
color: var(--secondary-foreground); color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background); background-color: var(--secondary-background);
transition: background-color .1s;
&:hover { &:hover {
background-color: var(--primary-background); background-color: var(--primary-background);
......
import Modal from "../../../components/ui/Modal"; import { Money } from "@styled-icons/boxicons-regular";
import { Localizer, Text } from "preact-i18n"; import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid";
import styles from "./UserProfile.module.scss"; import { observer } from "mobx-react-lite";
import Preloader from "../../../components/ui/Preloader";
import { Route } from "revolt.js/dist/api/routes";
import { Users } from "revolt.js/dist/api/objects";
import { IntermediateContext, useIntermediate } from "../Intermediate";
import { Globe, Mail, Edit, UserPlus, Shield } from "@styled-icons/feather";
import { Link, useHistory } from "react-router-dom"; 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 { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
import { decodeTime } from "ulid";
import { CashStack } from "@styled-icons/bootstrap"; import ChannelIcon from "../../../components/common/ChannelIcon";
import { AppContext, ClientStatus, StatusContext } from "../../revoltjs/RevoltClient"; import Tooltip from "../../../components/common/Tooltip";
import { useChannels, useForceUpdate, useUser, useUsers } from "../../revoltjs/hooks"; import UserIcon from "../../../components/common/user/UserIcon";
import UserIcon from '../../../components/common/UserIcon'; import UserStatus from "../../../components/common/user/UserStatus";
import UserStatus from '../../../components/common/UserStatus'; import IconButton from "../../../components/ui/IconButton";
import Tooltip from '../../../components/common/Tooltip'; import Modal from "../../../components/ui/Modal";
import ChannelIcon from '../../../components/common/ChannelIcon'; import Preloader from "../../../components/ui/Preloader";
import Markdown from '../../../components/markdown/Markdown';
import Markdown from "../../../components/markdown/Markdown";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../revoltjs/RevoltClient";
import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
user_id: string; user_id: string;
dummy?: boolean; dummy?: boolean;
onClose: () => void; onClose?: () => void;
dummyProfile?: Users.Profile; dummyProfile?: Profile;
} }
enum Badges { enum Badges {
...@@ -30,312 +38,316 @@ enum Badges { ...@@ -30,312 +38,316 @@ enum Badges {
Translator = 2, Translator = 2,
Supporter = 4, Supporter = 4,
ResponsibleDisclosure = 8, ResponsibleDisclosure = 8,
EarlyAdopter = 256 EarlyAdopter = 256,
} }
export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) { export const UserProfile = observer(
const { writeClipboard } = useIntermediate(); ({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate();
const [profile, setProfile] = useState<undefined | null | Users.Profile>( const [profile, setProfile] = useState<undefined | null | Profile>(
undefined undefined,
); );
const [mutual, setMutual] = useState< const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"] undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined); >(undefined);
const client = useContext(AppContext); const history = useHistory();
const status = useContext(StatusContext); const client = useClient();
const [tab, setTab] = useState("profile"); const status = useContext(StatusContext);
const history = useHistory(); const [tab, setTab] = useState("profile");
const ctx = useForceUpdate(); const user = client.users.get(user_id);
const all_users = useUsers(undefined, ctx); if (!user) {
const channels = useChannels(undefined, ctx); if (onClose) useEffect(onClose, []);
return null;
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) { const users = mutual?.users.map((id) => client.users.get(id));
useEffect(onClose, []);
return null;
}
useLayoutEffect(() => { const mutualGroups = [...client.channels.values()].filter(
if (!user_id) return; (channel) =>
if (typeof profile !== 'undefined') setProfile(undefined); channel?.channel_type === "Group" &&
if (typeof mutual !== 'undefined') setMutual(undefined); channel.recipient_ids!.includes(user_id),
}, [user_id]); );
if (dummy) {
useLayoutEffect(() => { useLayoutEffect(() => {
setProfile(dummyProfile); if (!user_id) return;
}, [dummyProfile]); if (typeof profile !== "undefined") setProfile(undefined);
} if (typeof mutual !== "undefined") setMutual(undefined);
// eslint-disable-next-line
}, [user_id]);
useEffect(() => { useEffect(() => {
if (dummy) return; if (dummy) {
if ( setProfile(dummyProfile);
status === ClientStatus.ONLINE && }
typeof mutual === "undefined" }, [dummy, dummyProfile]);
) {
setMutual(null);
client.users
.fetchMutual(user_id)
.then(data => setMutual(data));
}
}, [mutual, status]);
useEffect(() => { useEffect(() => {
if (dummy) return; if (dummy) return;
if ( if (
status === ClientStatus.ONLINE && status === ClientStatus.ONLINE &&
typeof profile === "undefined" typeof mutual === "undefined"
) { ) {
setProfile(null); setMutual(null);
user.fetchMutual().then(setMutual);
}
}, [mutual, status, dummy, user]);
// ! FIXME: in the future, also check if mutual guilds useEffect(() => {
// ! maybe just allow mutual group to allow profile viewing if (dummy) return;
/*if ( if (
user.relationship === Users.Relationship.Friend || status === ClientStatus.ONLINE &&
user.relationship === Users.Relationship.User typeof profile === "undefined"
) {*/ ) {
client.users setProfile(null);
.fetchProfile(user_id)
.then(data => setProfile(data))
.catch(() => {});
//}
}
}, [profile, status]);
const mutualGroups = channels.filter( if (user.permission & UserPermission.ViewProfile) {
channel => user.fetchProfile().then(setProfile);
channel?.channel_type === "Group" && }
channel.recipients.includes(user_id) }
); }, [profile, status, dummy, user]);
const backgroundURL = profile && client.users.getBackgroundURL(profile, { width: 1000 }, true); const backgroundURL =
const badges = (user.badges ?? 0) | (decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0); profile &&
client.generateFileURL(profile.background, { width: 1000 }, true);
const badges = user.badges ?? 0;
return ( return (
<Modal <Modal
visible visible
border={dummy} border={dummy}
onClose={onClose} padding={false}
dontModal={dummy} onClose={onClose}
> dontModal={dummy}>
<div <div
className={styles.header} className={styles.header}
data-force={ data-force={profile?.background ? "light" : undefined}
profile?.background style={{
? "light" backgroundImage:
: undefined backgroundURL &&
} `linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
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}>
<div className={styles.profile}> <Localizer>
<UserIcon size={80} target={user} status /> <span
<div className={styles.details}> className={styles.username}
<Localizer> onClick={() =>
<span writeClipboard(user.username)
className={styles.username} }>
onClick={() => writeClipboard(user.username)}> @{user.username}
@{user.username} </span>
</span> </Localizer>
</Localizer> {user.status?.text && (
{user.status?.text && ( <span className={styles.status}>
<span className={styles.status}> <UserStatus user={user} tooltip />
<UserStatus user={user} /> </span>
</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>
{user.relationship === Users.Relationship.Friend && ( <div className={styles.tabs}>
<Localizer> <div
<Tooltip data-active={tab === "profile"}
content={ onClick={() => setTab("profile")}>
<Text id="app.context_menu.message_user" /> <Text id="app.special.popovers.user_profile.profile" />
} </div>
> {user.relationship !== RelationshipStatus.User && (
{/*<IconButton <>
onClick={() => { <div
onClose(); data-active={tab === "friends"}
history.push(`/open/${user_id}`); onClick={() => setTab("friends")}>
}} <Text id="app.special.popovers.user_profile.mutual_friends" />
>*/} </div>
<Mail size={30} strokeWidth={1.5} /> <div
{/*</IconButton>*/} data-active={tab === "groups"}
</Tooltip> onClick={() => setTab("groups")}>
</Localizer> <Text id="app.special.popovers.user_profile.mutual_groups" />
)} </div>
{user.relationship === Users.Relationship.User && ( </>
/*<IconButton )}
onClick={() => {
onClose();
if (dummy) return;
history.push(`/settings/profile`);
}}
>*/
<Edit size={28} strokeWidth={1.5} />
/*</IconButton>*/
)}
{(user.relationship === Users.Relationship.Incoming ||
user.relationship === Users.Relationship.None) && (
/*<IconButton
onClick={() => client.users.addFriend(user.username)}
>*/
<UserPlus size={28} strokeWidth={1.5} />
/*</IconButton>*/
)}
</div>
<div className={styles.tabs}>
<div
data-active={tab === "profile"}
onClick={() => setTab("profile")}
>
<Text id="app.special.popovers.user_profile.profile" />
</div> </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> <div className={styles.content}>
<div className={styles.content}> {tab === "profile" && (
{tab === "profile" && <div>
<div> {!(profile?.content || badges > 0) && (
{ !(profile?.content || (badges > 0)) && <div className={styles.empty}>
<div className={styles.empty}><Text id="app.special.popovers.user_profile.empty" /></div> } <Text id="app.special.popovers.user_profile.empty" />
{ (badges > 0) && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.badges" /></div> } </div>
{ (badges > 0) && ( )}
<div className={styles.badges}> {badges > 0 && (
<Localizer> <div className={styles.category}>
{badges & Badges.Developer ? ( <Text id="app.special.popovers.user_profile.sub.badges" />
<Tooltip </div>
content={ )}
<Text id="app.navigation.tabs.dev" /> {badges > 0 && (
} <div className={styles.badges}>
> <Localizer>
<img src="/assets/badges/developer.svg" /> {badges & Badges.Developer ? (
</Tooltip> <Tooltip
) : ( content={
<></> <Text id="app.navigation.tabs.dev" />
)} }>
{badges & Badges.Translator ? ( <img src="/assets/badges/developer.svg" />
<Tooltip </Tooltip>
content={ ) : (
<Text id="app.special.popovers.user_profile.badges.translator" /> <></>
} )}
> {badges & Badges.Translator ? (
<img src="/assets/badges/translator.svg" /> <Tooltip
</Tooltip> content={
) : ( <Text id="app.special.popovers.user_profile.badges.translator" />
<></> }>
)} <img src="/assets/badges/translator.svg" />
{badges & Badges.EarlyAdopter ? ( </Tooltip>
<Tooltip ) : (
content={ <></>
<Text id="app.special.popovers.user_profile.badges.early_adopter" /> )}
} {badges & Badges.EarlyAdopter ? (
> <Tooltip
<img src="/assets/badges/early_adopter.svg" /> content={
</Tooltip> <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" /> {badges & Badges.Supporter ? (
} <Tooltip
> content={
<CashStack size={32} color="#efab44" /> <Text id="app.special.popovers.user_profile.badges.supporter" />
</Tooltip> }>
) : ( <Money
<></> size={32}
)} color="#efab44"
{badges & Badges.ResponsibleDisclosure ? ( />
<Tooltip </Tooltip>
content={ ) : (
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" /> <></>
} )}
> {badges &
<Shield size={32} color="gray" /> Badges.ResponsibleDisclosure ? (
</Tooltip> <Tooltip
) : ( content={
<></> <Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
)} }>
</Localizer> <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> </div>
)} )}
{ profile?.content && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.information" /></div> } {tab === "friends" &&
<Markdown content={profile?.content} /> (users ? (
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/} <div className={styles.entries}>
</div>} {users.length === 0 ? (
{tab === "friends" && <div className={styles.empty}>
(users ? ( <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}> <div className={styles.entries}>
{users.length === 0 ? ( {mutualGroups.length === 0 ? (
<div className={styles.empty}> <div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_users" /> <Text id="app.special.popovers.user_profile.no_groups" />
</div> </div>
) : ( ) : (
users.map( mutualGroups.map(
x => (x) =>
x && ( x?.channel_type === "Group" && (
//<LinkProfile user_id={x._id}> <Link to={`/channel/${x._id}`}>
<div <div
className={styles.entry} className={styles.entry}
key={x._id} key={x._id}>
> <ChannelIcon
<UserIcon size={32} target={x} /> target={x}
<span>{x.username}</span> size={32}
/>
<span>{x.name}</span>
</div> </div>
//</LinkProfile> </Link>
) ),
) )
)} )}
</div> </div>
) : ( )}
<Preloader /> </div>
))} </Modal>
{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>
);
}
import { ReactNode } from "react";
import { useContext } from "preact/hooks";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import { useContext } from "preact/hooks";
import { Children } from "../../types/Preact";
import { OperationsContext } from "./RevoltClient"; import { OperationsContext } from "./RevoltClient";
interface Props { interface Props {
auth?: boolean; auth?: boolean;
children: ReactNode | ReactNode[]; children: Children;
} }
export const CheckAuth = (props: Props) => { export const CheckAuth = (props: Props) => {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
&.banner { &.banner {
.image { .image {
border-radius: 4px; border-radius: var(--border-radius);
} }
.modify { .modify {
......
// ! FIXME: also TEMP CODE import { Plus } from "@styled-icons/boxicons-regular";
// ! RE-WRITE WITH STYLED-COMPONENTS import { Pencil } from "@styled-icons/boxicons-solid";
import Axios, { AxiosRequestConfig } from "axios";
import { Text } from "preact-i18n"; import styles from "./FileUploads.module.scss";
import { takeError } from "./util";
import classNames from "classnames"; import classNames from "classnames";
import styles from './FileUploads.module.scss'; import { Text } from "preact-i18n";
import Axios, { AxiosRequestConfig } from "axios"; import { useContext, useEffect, useState } from "preact/hooks";
import { useContext, useState } from "preact/hooks";
import { Edit, Plus, X } from "@styled-icons/feather";
import Preloader from "../../components/ui/Preloader";
import { determineFileSize } from "../../lib/fileSize"; import { determineFileSize } from "../../lib/fileSize";
import IconButton from '../../components/ui/IconButton';
import IconButton from "../../components/ui/IconButton";
import Preloader from "../../components/ui/Preloader";
import { useIntermediate } from "../intermediate/Intermediate"; import { useIntermediate } from "../intermediate/Intermediate";
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
import { takeError } from "./util";
type Props = { type Props = {
maxFileSize: number maxFileSize: number;
remove: () => Promise<void> remove: () => Promise<void>;
fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners' fileType: "backgrounds" | "icons" | "avatars" | "attachments" | "banners";
} & ( } & (
{ behaviour: 'ask', onChange: (file: File) => void } | | { behaviour: "ask"; onChange: (file: File) => void }
{ behaviour: 'upload', onUpload: (id: string) => Promise<void> } | { behaviour: "upload"; onUpload: (id: string) => Promise<void> }
) & ( | {
{ style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } | behaviour: "multi";
{ style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number } onChange: (files: File[]) => void;
) append?: (files: File[]) => void;
}
export async function uploadFile(autumnURL: string, tag: string, file: File, config?: AxiosRequestConfig) { ) &
(
| {
style: "icon" | "banner";
defaultPreview?: string;
previewURL?: string;
width?: number;
height?: number;
}
| {
style: "attachment";
attached: boolean;
uploading: boolean;
cancel: () => void;
size?: number;
}
);
export async function uploadFile(
autumnURL: string,
tag: string,
file: File,
config?: AxiosRequestConfig,
) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const res = await Axios.post(autumnURL + "/" + tag, formData, { const res = await Axios.post(`${autumnURL}/${tag}`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data" "Content-Type": "multipart/form-data",
}, },
...config ...config,
}); });
return res.data.id; return res.data.id;
} }
export function grabFiles(
maxFileSize: number,
cb: (files: File[]) => void,
tooLarge: () => void,
multiple?: boolean,
) {
const input = document.createElement("input");
input.type = "file";
input.multiple = multiple ?? false;
input.onchange = async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return;
for (const file of files) {
if (file.size > maxFileSize) {
return tooLarge();
}
}
cb(Array.from(files));
};
input.click();
}
export function FileUploader(props: Props) { export function FileUploader(props: Props) {
const { fileType, maxFileSize, remove } = props; const { fileType, maxFileSize, remove } = props;
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
const [ uploading, setUploading ] = useState(false); const [uploading, setUploading] = useState(false);
function onClick() { function onClick() {
if (uploading) return; if (uploading) return;
const input = document.createElement("input"); grabFiles(
input.type = "file"; maxFileSize,
async (files) => {
input.onchange = async e => { setUploading(true);
setUploading(true);
try {
const files = (e.target as any)?.files;
if (files && files[0]) {
let file = files[0];
if (file.size > maxFileSize) { try {
return openScreen({ id: "error", error: "FileTooLarge" }); if (props.behaviour === "multi") {
} props.onChange(files);
} else if (props.behaviour === "ask") {
if (props.behaviour === 'ask') { props.onChange(files[0]);
await props.onChange(file);
} else { } else {
await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, file)); await props.onUpload(
await uploadFile(
client.configuration!.features.autumn.url,
fileType,
files[0],
),
);
} }
} catch (err) {
return openScreen({ id: "error", error: takeError(err) });
} finally {
setUploading(false);
} }
} catch (err) { },
return openScreen({ id: "error", error: takeError(err) }); () => openScreen({ id: "error", error: "FileTooLarge" }),
} finally { props.behaviour === "multi",
setUploading(false); );
}
};
input.click();
} }
function removeOrUpload() { function removeOrUpload() {
if (uploading) return; if (uploading) return;
if (props.style === 'attachment') { if (props.style === "attachment") {
if (props.attached) { if (props.attached) {
props.remove(); props.remove();
} else { } else {
onClick(); onClick();
} }
} else if (props.previewURL) {
props.remove();
} else { } else {
if (props.previewURL) { onClick();
props.remove();
} else {
onClick();
}
} }
} }
if (props.style === 'icon' || props.style === 'banner') { if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => {
// File pasting.
function paste(e: ClipboardEvent) {
const items = e.clipboardData?.items;
if (typeof items === "undefined") return;
if (props.behaviour !== "multi" || !props.append) return;
const files = [];
for (const item of items) {
if (!item.type.startsWith("text/")) {
const blob = item.getAsFile();
if (blob) {
if (blob.size > props.maxFileSize) {
openScreen({
id: "error",
error: "FileTooLarge",
});
}
files.push(blob);
}
}
}
props.append(files);
}
// Let the browser know we can drop files.
function dragover(e: DragEvent) {
e.stopPropagation();
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
}
// File dropping.
function drop(e: DragEvent) {
e.preventDefault();
if (props.behaviour !== "multi" || !props.append) return;
const dropped = e.dataTransfer?.files;
if (dropped) {
const files = [];
for (const item of dropped) {
if (item.size > props.maxFileSize) {
openScreen({ id: "error", error: "FileTooLarge" });
}
files.push(item);
}
props.append(files);
}
}
document.addEventListener("paste", paste);
document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop);
return () => {
document.removeEventListener("paste", paste);
document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop);
};
}, [openScreen, props, props.append]);
}
if (props.style === "icon" || props.style === "banner") {
const { style, previewURL, defaultPreview, width, height } = props; const { style, previewURL, defaultPreview, width, height } = props;
return ( return (
<div className={classNames(styles.uploader, <div
{ [styles.icon]: style === 'icon', className={classNames(styles.uploader, {
[styles.banner]: style === 'banner' })} [styles.icon]: style === "icon",
[styles.banner]: style === "banner",
})}
data-uploading={uploading}> data-uploading={uploading}>
<div className={styles.image} <div
style={{ backgroundImage: className={styles.image}
style === 'icon' ? `url('${previewURL ?? defaultPreview}')` : style={{
(previewURL ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` : 'black'), backgroundImage:
width, height style === "icon"
? `url('${previewURL ?? defaultPreview}')`
: previewURL
? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
: "black",
width,
height,
}} }}
onClick={onClick}> onClick={onClick}>
{ uploading ? {uploading ? (
<div className={styles.uploading}> <div className={styles.uploading}>
<Preloader /> <Preloader type="ring" />
</div> : </div>
) : (
<div className={styles.edit}> <div className={styles.edit}>
<Edit size={30} /> <Pencil size={30} />
</div> } </div>
)}
</div> </div>
<div className={styles.modify}> <div className={styles.modify}>
<span onClick={removeOrUpload}>{ <span onClick={removeOrUpload}>
uploading ? <Text id="app.main.channel.uploading_file" /> : {uploading ? (
props.previewURL ? <Text id="app.settings.actions.remove" /> : <Text id="app.main.channel.uploading_file" />
<Text id="app.settings.actions.upload" /> }</span> ) : props.previewURL ? (
<span className={styles.small}><Text id="app.settings.actions.max_filesize" fields={{ filesize: determineFileSize(maxFileSize) }} /></span> <Text id="app.settings.actions.remove" />
) : (
<Text id="app.settings.actions.upload" />
)}
</span>
<span className={styles.small}>
<Text
id="app.settings.actions.max_filesize"
fields={{
filesize: determineFileSize(maxFileSize),
}}
/>
</span>
</div> </div>
</div> </div>
) );
} else if (props.style === 'attachment') { } else if (props.style === "attachment") {
const { attached, uploading, cancel, size } = props; const { attached, uploading, cancel, size } = props;
return ( return (
<IconButton <IconButton
...@@ -138,10 +275,11 @@ export function FileUploader(props: Props) { ...@@ -138,10 +275,11 @@ export function FileUploader(props: Props) {
if (uploading) return cancel(); if (uploading) return cancel();
if (attached) return remove(); if (attached) return remove();
onClick(); onClick();
}}> }}
{ attached ? <X size={size} /> : <Plus size={size} />} rotate={uploading || attached ? "45deg" : undefined}>
<Plus size={size} />
</IconButton> </IconButton>
) );
} }
return null; return null;
......
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import { decodeTime } from "ulid";
import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n";
import { connectState } from "../../redux/connector";
import {
getNotificationState,
Notifications,
shouldNotify,
} from "../../redux/reducers/notifications";
import { NotificationOptions } from "../../redux/reducers/settings";
import { SoundContext } from "../Settings";
import { AppContext } from "./RevoltClient";
interface Props {
options?: NotificationOptions;
notifs: Notifications;
}
const notifications: { [key: string]: Notification } = {};
async function createNotification(
title: string,
options: globalThis.NotificationOptions,
) {
try {
return new Notification(title, options);
} catch (err) {
const sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options);
}
}
function Notifier({ options, notifs }: Props) {
const translate = useTranslation();
const showNotification = options?.desktopEnabled ?? false;
const client = useContext(AppContext);
const { guild: guild_id, channel: channel_id } = useParams<{
guild: string;
channel: string;
}>();
const history = useHistory();
const playSound = useContext(SoundContext);
const message = useCallback(
async (msg: Message) => {
if (msg.author_id === client.user!._id) return;
if (msg.channel_id === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const notifState = getNotificationState(notifs, msg.channel!);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
playSound("message");
if (!showNotification) return;
let title;
switch (msg.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${msg.author?.username}`;
break;
case "Group":
if (msg.author?._id === SYSTEM_USER_ID) {
title = msg.channel.name;
} else {
title = `@${msg.author?.username} - ${msg.channel.name}`;
}
break;
case "TextChannel":
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
break;
default:
title = msg.channel?._id;
break;
}
let image;
if (msg.attachments) {
const imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
}
}
let body, icon;
if (typeof msg.content === "string") {
body = client.markdownToText(msg.content);
icon = msg.author?.generateAvatarURL({ max_side: 256 });
} else {
const users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
{
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.content.by)
?.username,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: user?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_renamed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_description_changed":
case "channel_icon_changed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
}
}
const notif = await createNotification(title!, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel?._id,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = msg.channel_id;
if (id !== channel_id) {
const channel = client.channels.get(id);
if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
notifications[msg.channel_id] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
playSound,
],
);
const relationship = useCallback(
async (user: User) => {
if (client.user?.status?.presence === Presence.Busy) return;
if (!showNotification) return;
let event;
switch (user.relationship) {
case RelationshipStatus.Incoming:
event = translate("notifications.sent_request", {
person: user.username,
});
break;
case RelationshipStatus.Friend:
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
return;
}
const notif = await createNotification(event, {
icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
},
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => {
client.addListener("message", message);
client.addListener("user/relationship", relationship);
return () => {
client.removeListener("message", message);
client.removeListener("user/relationship", relationship);
};
}, [
client,
playSound,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => {
function visChange() {
if (document.visibilityState === "visible") {
if (notifications[channel_id]) {
notifications[channel_id].close();
}
}
}
visChange();
document.addEventListener("visibilitychange", visChange);
return () =>
document.removeEventListener("visibilitychange", visChange);
}, [guild_id, channel_id]);
return null;
}
const NotifierComponent = connectState(
Notifier,
(state) => {
return {
options: state.settings.notification,
notifs: state.notifications,
};
},
true,
);
export default function NotificationsComponent() {
return (
<Switch>
<Route path="/server/:server/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/">
<NotifierComponent />
</Route>
</Switch>
);
}
import { Text } from "preact-i18n"; import { WifiOff } from "@styled-icons/boxicons-regular";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { Children } from "../../types/Preact";
import { WifiOff } from "@styled-icons/feather";
import Preloader from "../../components/ui/Preloader"; import Preloader from "../../components/ui/Preloader";
import { Children } from "../../types/Preact";
import { ClientStatus, StatusContext } from "./RevoltClient"; import { ClientStatus, StatusContext } from "./RevoltClient";
interface Props { interface Props {
...@@ -29,7 +32,7 @@ const Base = styled.div` ...@@ -29,7 +32,7 @@ const Base = styled.div`
export default function RequiresOnline(props: Props) { export default function RequiresOnline(props: Props) {
const status = useContext(StatusContext); const status = useContext(StatusContext);
if (status === ClientStatus.CONNECTING) return <Preloader />; if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY) if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
return ( return (
<Base> <Base>
...@@ -40,5 +43,5 @@ export default function RequiresOnline(props: Props) { ...@@ -40,5 +43,5 @@ export default function RequiresOnline(props: Props) {
</Base> </Base>
); );
return <>{ props.children }</>; return <>{props.children}</>;
} }