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 711 additions and 1042 deletions
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects"; 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 { ulid } from "ulid";
import styles from "./Prompt.module.scss"; import styles from "./Prompt.module.scss";
...@@ -9,9 +12,6 @@ import { useContext, useEffect, useState } from "preact/hooks"; ...@@ -9,9 +12,6 @@ import { useContext, useEffect, useState } from "preact/hooks";
import { TextReact } from "../../../lib/i18n"; import { TextReact } from "../../../lib/i18n";
import { Channel, User } from "../../../mobx";
import { useData } from "../../../mobx/State";
import Message from "../../../components/common/messaging/Message"; import Message from "../../../components/common/messaging/Message";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
...@@ -21,7 +21,7 @@ import Radio from "../../../components/ui/Radio"; ...@@ -21,7 +21,7 @@ import Radio from "../../../components/ui/Radio";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import { AppContext } from "../../revoltjs/RevoltClient"; import { AppContext } from "../../revoltjs/RevoltClient";
import { mapMessage, takeError } from "../../revoltjs/util"; import { takeError } from "../../revoltjs/util";
import { useIntermediate } from "../Intermediate"; import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
...@@ -57,19 +57,19 @@ export function PromptModal({ ...@@ -57,19 +57,19 @@ export function PromptModal({
type SpecialProps = { onClose: () => void } & ( type SpecialProps = { onClose: () => void } & (
| { type: "leave_group"; target: Channel } | { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channel } | { 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: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: Channels.Message } | { type: "delete_message"; target: MessageI }
| { | {
type: "create_invite"; type: "create_invite";
target: Channel; target: Channel;
} }
| { type: "kick_member"; target: Servers.Server; user: User } | { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Servers.Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Servers.Server } | { type: "create_channel"; target: Server }
); );
export const SpecialPromptModal = observer((props: SpecialProps) => { export const SpecialPromptModal = observer((props: SpecialProps) => {
...@@ -104,9 +104,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -104,9 +104,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
name = props.target.username; name = props.target.username;
break; break;
case "close_dm": case "close_dm":
name = client.users.get( name = props.target.recipient?.username;
client.channels.getRecipient(props.target._id),
)?.username;
break; break;
default: default:
name = props.target.name; name = props.target.name;
...@@ -137,27 +135,19 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -137,27 +135,19 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
try { try {
switch (props.type) { switch (props.type) {
case "unfriend_user": case "unfriend_user":
await client.users.removeFriend( await props.target.removeFriend();
props.target._id,
);
break; break;
case "block_user": case "block_user":
await client.users.blockUser( await props.target.blockUser();
props.target._id,
);
break; break;
case "leave_group": case "leave_group":
case "close_dm": case "close_dm":
case "delete_channel": case "delete_channel":
await client.channels.delete( props.target.delete();
props.target._id,
);
break; break;
case "leave_server": case "leave_server":
case "delete_server": case "delete_server":
await client.servers.delete( props.target.delete();
props.target._id,
);
break; break;
} }
...@@ -203,11 +193,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -203,11 +193,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
setProcessing(true); setProcessing(true);
try { try {
await client.channels.deleteMessage( props.target.delete();
props.target.channel,
props.target._id,
);
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -229,7 +215,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -229,7 +215,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
id={`app.special.modals.prompt.confirm_delete_message_long`} id={`app.special.modals.prompt.confirm_delete_message_long`}
/> />
<Message <Message
message={mapMessage(props.target)} message={props.target}
head={true} head={true}
contrast contrast
/> />
...@@ -247,12 +233,12 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -247,12 +233,12 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
useEffect(() => { useEffect(() => {
setProcessing(true); setProcessing(true);
client.channels props.target
.createInvite(props.target._id) .createInvite()
.then((code) => setCode(code)) .then((code) => setCode(code))
.catch((err) => setError(takeError(err))) .catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false)); .finally(() => setProcessing(false));
}, []); }, [props.target]);
return ( return (
<PromptModal <PromptModal
...@@ -306,10 +292,13 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -306,10 +292,13 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
setProcessing(true); setProcessing(true);
try { try {
await client.members.kickMember( client.members
props.target._id, .getKey({
props.user._id, server: props.target._id,
); user: props.user._id,
})
?.kick();
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -357,11 +346,9 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -357,11 +346,9 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
setProcessing(true); setProcessing(true);
try { try {
await client.servers.banUser( await props.target.banUser(props.user._id, {
props.target._id, reason,
props.user._id, });
{ reason },
);
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -420,14 +407,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { ...@@ -420,14 +407,11 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
try { try {
const channel = const channel =
await client.servers.createChannel( await props.target.createChannel({
props.target._id, type,
{ name,
type, nonce: ulid(),
name, });
nonce: ulid(),
},
);
history.push( history.push(
`/server/${props.target._id}/channel/${channel._id}`, `/server/${props.target._id}/channel/${channel._id}`,
......
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; 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 { Channel } from "../../../mobx";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { useClient } from "../../revoltjs/RevoltClient";
import { useForceUpdate } from "../../revoltjs/hooks";
import { getChannelName } from "../../revoltjs/util"; import { getChannelName } from "../../revoltjs/util";
interface Props { interface Props {
...@@ -26,12 +23,11 @@ export const ChannelInfo = observer(({ channel, onClose }: Props) => { ...@@ -26,12 +23,11 @@ export const ChannelInfo = observer(({ channel, onClose }: Props) => {
return null; return null;
} }
const client = useClient();
return ( return (
<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(client, channel, true)}</h1> <h1>{getChannelName(channel, true)}</h1>
<div onClick={onClose}> <div onClick={onClose}>
<X size={36} /> <X size={36} />
</div> </div>
......
import { /* eslint-disable react-hooks/rules-of-hooks */
Attachment, import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
AttachmentMetadata, import { EmbedImage } from "revolt-api/types/January";
EmbedImage,
} from "revolt.js/dist/api/objects";
import styles from "./ImageViewer.module.scss"; import styles from "./ImageViewer.module.scss";
import { useContext, useEffect } from "preact/hooks";
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions"; import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions"; import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
......
...@@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) { ...@@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) {
]}> ]}>
{/* Preact / React typing incompatabilities */} {/* Preact / React typing incompatabilities */}
<form <form
onSubmit={ onSubmit={(e) => {
e.preventDefault();
handleSubmit( handleSubmit(
onSubmit, onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement> // 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 { observer } from "mobx-react-lite";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./UserPicker.module.scss"; import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { User } from "../../../mobx";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { Friend } from "../../../pages/friends/Friend"; import { Friend } from "../../../pages/friends/Friend";
......
...@@ -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 { Users } from "revolt.js/dist/api/objects"; import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss"; 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 { useData } from "../../../mobx/State";
import UserCheckbox from "../../../components/common/user/UserCheckbox"; import UserCheckbox from "../../../components/common/user/UserCheckbox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { useClient } from "../../revoltjs/RevoltClient";
interface Props { interface Props {
omit?: string[]; omit?: string[];
onClose: () => void; onClose: () => void;
...@@ -19,7 +19,7 @@ export function UserPicker(props: Props) { ...@@ -19,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 store = useData(); const client = useClient();
return ( return (
<Modal <Modal
...@@ -33,15 +33,16 @@ export function UserPicker(props: Props) { ...@@ -33,15 +33,16 @@ export function UserPicker(props: Props) {
}, },
]}> ]}>
<div className={styles.list}> <div className={styles.list}>
{[...store.users.values()] {[...client.users.values()]
.filter( .filter(
(x) => (x) =>
x && x &&
x.relationship === Users.Relationship.Friend && x.relationship === RelationshipStatus.Friend &&
!omit.includes(x._id), !omit.includes(x._id),
) )
.map((x) => ( .map((x) => (
<UserCheckbox <UserCheckbox
key={x._id}
user={x} user={x}
checked={selected.includes(x._id)} checked={selected.includes(x._id)}
onChange={(v) => { onChange={(v) => {
......
...@@ -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);
...@@ -141,7 +144,7 @@ ...@@ -141,7 +144,7 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
transition: background-color .1s; transition: background-color 0.1s;
color: var(--secondary-foreground); color: var(--secondary-foreground);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--secondary-background); background-color: var(--secondary-background);
......
import { Money } from "@styled-icons/boxicons-regular"; import { Money } from "@styled-icons/boxicons-regular";
import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid"; import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { Users } from "revolt.js/dist/api/objects"; import { Profile, RelationshipStatus } from "revolt-api/types/Users";
import { UserPermission } from "revolt.js/dist/api/permissions"; import { UserPermission } from "revolt.js/dist/api/permissions";
import { Route } from "revolt.js/dist/api/routes"; import { Route } from "revolt.js/dist/api/routes";
import { decodeTime } from "ulid";
import styles from "./UserProfile.module.scss"; import styles from "./UserProfile.module.scss";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks"; import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
import { useData } from "../../../mobx/State";
import ChannelIcon from "../../../components/common/ChannelIcon"; import ChannelIcon from "../../../components/common/ChannelIcon";
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
...@@ -22,19 +20,17 @@ import Preloader from "../../../components/ui/Preloader"; ...@@ -22,19 +20,17 @@ import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { import {
AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient, useClient,
} from "../../revoltjs/RevoltClient"; } from "../../revoltjs/RevoltClient";
import { useForceUpdate, useUserPermission } from "../../revoltjs/hooks";
import { useIntermediate } from "../Intermediate"; 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 {
...@@ -45,307 +41,313 @@ enum Badges { ...@@ -45,307 +41,313 @@ enum Badges {
EarlyAdopter = 256, EarlyAdopter = 256,
} }
export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) { export const UserProfile = observer(
const { openScreen, writeClipboard } = useIntermediate(); ({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate();
const [profile, setProfile] = useState<undefined | null | Users.Profile>(
undefined,
);
const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const history = useHistory(); const [profile, setProfile] = useState<undefined | null | Profile>(
const client = useClient(); undefined,
const status = useContext(StatusContext); );
const [tab, setTab] = useState("profile"); const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const ctx = useForceUpdate(); const history = useHistory();
const permissions = useUserPermission(client.user!._id, ctx); const client = useClient();
const status = useContext(StatusContext);
const [tab, setTab] = useState("profile");
const store = useData(); const user = client.users.get(user_id);
if (!store.users.has(user_id)) { if (!user) {
useEffect(onClose, []); if (onClose) useEffect(onClose, []);
return null; return null;
} }
const user = store.users.get(user_id)!;
const users = mutual?.users.map((id) => store.users.get(id));
const mutualGroups = [...store.channels.values()].filter( const users = mutual?.users.map((id) => client.users.get(id));
(channel) =>
channel?.channel_type === "Group" &&
channel.recipients!.includes(user_id),
);
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 (status === ClientStatus.ONLINE && typeof mutual === "undefined") { setProfile(dummyProfile);
setMutual(null); }
client.users.fetchMutual(user_id).then((data) => setMutual(data)); }, [dummy, dummyProfile]);
}
}, [mutual, status]); useEffect(() => {
if (dummy) return;
if (
status === ClientStatus.ONLINE &&
typeof mutual === "undefined"
) {
setMutual(null);
user.fetchMutual().then(setMutual);
}
}, [mutual, status, dummy, user]);
useEffect(() => { useEffect(() => {
if (dummy) return; if (dummy) return;
if (status === ClientStatus.ONLINE && typeof profile === "undefined") { if (
setProfile(null); status === ClientStatus.ONLINE &&
typeof profile === "undefined"
) {
setProfile(null);
if (permissions & UserPermission.ViewProfile) { if (user.permission & UserPermission.ViewProfile) {
client.users user.fetchProfile().then(setProfile);
.fetchProfile(user_id) }
.then((data) => setProfile(data))
.catch(() => {});
} }
} }, [profile, status, dummy, user]);
}, [profile, status]);
const backgroundURL = const backgroundURL =
profile && profile &&
client.users.getBackgroundURL(profile, { width: 1000 }, true); client.generateFileURL(profile.background, { width: 1000 }, true);
const badges = const badges = user.badges ?? 0;
(user.badges ?? 0) |
(decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
return ( return (
<Modal <Modal
visible visible
border={dummy} border={dummy}
padding={false} padding={false}
onClose={onClose} onClose={onClose}
dontModal={dummy}> dontModal={dummy}>
<div <div
className={styles.header} className={styles.header}
data-force={profile?.background ? "light" : undefined} data-force={profile?.background ? "light" : undefined}
style={{ style={{
backgroundImage: backgroundImage:
backgroundURL && backgroundURL &&
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`, `linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
}}> }}>
<div className={styles.profile}> <div className={styles.profile}>
<UserIcon size={80} target={user} status animate /> <UserIcon size={80} target={user} status animate />
<div className={styles.details}> <div className={styles.details}>
<Localizer> <Localizer>
<span <span
className={styles.username} className={styles.username}
onClick={() => writeClipboard(user.username)}> onClick={() =>
@{user.username} writeClipboard(user.username)
</span> }>
</Localizer> @{user.username}
{user.status?.text && ( </span>
<span className={styles.status}> </Localizer>
<UserStatus user={user} tooltip /> {user.status?.text && (
</span> <span className={styles.status}>
)} <UserStatus user={user} tooltip />
</div> </span>
{user.relationship === Users.Relationship.Friend && ( )}
<Localizer> </div>
<Tooltip {user.relationship === RelationshipStatus.Friend && (
content={ <Localizer>
<Text id="app.context_menu.message_user" /> <Tooltip
}> content={
<IconButton <Text id="app.context_menu.message_user" />
onClick={() => { }>
onClose(); <IconButton
history.push(`/open/${user_id}`); onClick={() => {
}}> onClose?.();
<Envelope size={30} /> history.push(`/open/${user_id}`);
</IconButton> }}>
</Tooltip> <Envelope size={30} />
</Localizer> </IconButton>
)} </Tooltip>
{user.relationship === Users.Relationship.User && ( </Localizer>
<IconButton
onClick={() => {
onClose();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)}
{(user.relationship === Users.Relationship.Incoming ||
user.relationship === Users.Relationship.None) && (
<IconButton
onClick={() =>
client.users.addFriend(user.username)
}>
<UserPlus size={28} />
</IconButton>
)}
</div>
<div className={styles.tabs}>
<div
data-active={tab === "profile"}
onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" />
</div>
{user.relationship !== Users.Relationship.User && (
<>
<div
data-active={tab === "friends"}
onClick={() => setTab("friends")}>
<Text id="app.special.popovers.user_profile.mutual_friends" />
</div>
<div
data-active={tab === "groups"}
onClick={() => setTab("groups")}>
<Text id="app.special.popovers.user_profile.mutual_groups" />
</div>
</>
)}
</div>
</div>
<div className={styles.content}>
{tab === "profile" && (
<div>
{!(profile?.content || badges > 0) && (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" />
</div>
)} )}
{badges > 0 && ( {user.relationship === RelationshipStatus.User && (
<div className={styles.category}> <IconButton
<Text id="app.special.popovers.user_profile.sub.badges" /> onClick={() => {
</div> onClose?.();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)} )}
{badges > 0 && ( {(user.relationship === RelationshipStatus.Incoming ||
<div className={styles.badges}> user.relationship === RelationshipStatus.None) && (
<Localizer> <IconButton onClick={() => user.addFriend()}>
{badges & Badges.Developer ? ( <UserPlus size={28} />
<Tooltip </IconButton>
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>
<div className={styles.category}> <div className={styles.tabs}>
<Text id="app.special.popovers.user_profile.sub.information" /> <div
</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>
</>
)} )}
<Markdown content={profile?.content} />
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div> </div>
)} </div>
{tab === "friends" && <div className={styles.content}>
(users ? ( {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}> <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" && (
<div <Link to={`/channel/${x._id}`}>
onClick={() => <div
openScreen({ className={styles.entry}
id: "profile", key={x._id}>
user_id: x._id, <ChannelIcon
}) target={x}
} size={32}
className={styles.entry} />
key={x._id}> <span>{x.name}</span>
<UserIcon </div>
size={32} </Link>
target={x}
/>
<span>{x.username}</span>
</div>
), ),
) )
)} )}
</div> </div>
) : ( )}
<Preloader type="ring" /> </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 { Plus, X, XCircle } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid"; import { Pencil } from "@styled-icons/boxicons-solid";
import Axios, { AxiosRequestConfig } from "axios"; import Axios, { AxiosRequestConfig } from "axios";
...@@ -147,6 +147,7 @@ export function FileUploader(props: Props) { ...@@ -147,6 +147,7 @@ export function FileUploader(props: Props) {
} }
if (props.behaviour === "multi" && props.append) { if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => { useEffect(() => {
// File pasting. // File pasting.
function paste(e: ClipboardEvent) { function paste(e: ClipboardEvent) {
...@@ -210,7 +211,7 @@ export function FileUploader(props: Props) { ...@@ -210,7 +211,7 @@ export function FileUploader(props: Props) {
document.removeEventListener("dragover", dragover); document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop); document.removeEventListener("drop", drop);
}; };
}, [props.append]); }, [openScreen, props, props.append]);
} }
if (props.style === "icon" || props.style === "banner") { if (props.style === "icon" || props.style === "banner") {
......
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, SYSTEM_USER_ID, User } from "revolt.js"; import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import { Users } from "revolt.js/dist/api/objects"; 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 { decodeTime } from "ulid";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n"; import { useTranslation } from "../../lib/i18n";
...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) { ...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) {
const history = useHistory(); const history = useHistory();
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
async function message(msg: Message) { const message = useCallback(
if (msg.author === client.user!._id) return; async (msg: Message) => {
if (msg.channel === channel_id && document.hasFocus()) return; if (msg.author_id === client.user!._id) return;
if (client.user!.status?.presence === Users.Presence.Busy) 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 channel = client.channels.get(msg.channel); const notifState = getNotificationState(notifs, msg.channel!);
const author = client.users.get(msg.author); if (!shouldNotify(notifState, msg, client.user!._id)) return;
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const notifState = getNotificationState(notifs, channel); playSound("message");
if (!shouldNotify(notifState, msg, client.user!._id)) return; if (!showNotification) return;
playSound("message"); let title;
if (!showNotification) return; switch (msg.channel?.channel_type) {
case "SavedMessages":
let title; return;
switch (channel.channel_type) { case "DirectMessage":
case "SavedMessages": title = `@${msg.author?.username}`;
return;
case "DirectMessage":
title = `@${author?.username}`;
break;
case "Group":
if (author?._id === SYSTEM_USER_ID) {
title = channel.name;
} else {
title = `@${author?.username} - ${channel.name}`;
}
break;
case "TextChannel":
const server = client.servers.get(channel.server);
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
break;
default:
title = msg.channel;
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 = client.users.getAvatarURL(msg.author, { max_side: 256 });
} else {
const users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: users.get(msg.content.id)?.username,
other_user: users.get(msg.content.by)?.username,
},
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break; break;
case "user_joined": case "Group":
case "user_left": if (msg.author?._id === SYSTEM_USER_ID) {
case "user_kicked": title = msg.channel.name;
case "user_banned": } else {
body = translate( title = `@${msg.author?.username} - ${msg.channel.name}`;
`app.main.channel.system.${msg.content.type}`, }
{ user: users.get(msg.content.id)?.username },
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break; break;
case "channel_renamed": case "TextChannel":
body = translate( title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break; break;
case "channel_description_changed": default:
case "channel_icon_changed": title = msg.channel?._id;
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break; break;
} }
}
const notif = await createNotification(title, { let image;
icon, if (msg.attachments) {
image, const imageAttachment = msg.attachments.find(
body, (x) => x.metadata.type === "Image",
timestamp: decodeTime(msg._id), );
tag: msg.channel, if (imageAttachment) {
badge: "/assets/icons/android-chrome-512x512.png", image = client.generateFileURL(imageAttachment, {
silent: true, max_side: 720,
}); });
}
}
if (notif) { let body, icon;
notif.addEventListener("click", () => { if (typeof msg.content === "string") {
window.focus(); body = client.markdownToText(msg.content);
const id = msg.channel; icon = msg.author?.generateAvatarURL({ max_side: 256 });
if (id !== channel_id) { } else {
const channel = client.channels.get(id); const users = client.users;
if (channel) { switch (msg.content.type) {
if (channel.channel_type === "TextChannel") { case "user_added":
history.push( case "user_remove":
`/server/${channel.server}/channel/${id}`, {
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,
},
); );
} else { icon = user?.generateAvatarURL({
history.push(`/channel/${id}`); 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,
}); });
notifications[msg.channel] = notif; if (notif) {
notif.addEventListener( notif.addEventListener("click", () => {
"close", window.focus();
() => delete notifications[msg.channel], 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,
],
);
async function relationship(user: User, property: string) { const relationship = useCallback(
if (client.user?.status?.presence === Users.Presence.Busy) return; async (user: User) => {
if (property !== "relationship") return; if (client.user?.status?.presence === Presence.Busy) return;
if (!showNotification) return; if (!showNotification) return;
let event; let event;
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.Incoming: case RelationshipStatus.Incoming:
event = translate("notifications.sent_request", { event = translate("notifications.sent_request", {
person: user.username, person: user.username,
}); });
break; break;
case Users.Relationship.Friend: case RelationshipStatus.Friend:
event = translate("notifications.now_friends", { event = translate("notifications.now_friends", {
person: user.username, person: user.username,
}); });
break; break;
default: default:
return; return;
} }
const notif = await createNotification(event, { const notif = await createNotification(event, {
icon: client.users.getAvatarURL(user._id, { max_side: 256 }), icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png", badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(), timestamp: +new Date(),
}); });
notif?.addEventListener("click", () => { notif?.addEventListener("click", () => {
history.push(`/friends`); history.push(`/friends`);
}); });
} },
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => { useEffect(() => {
client.addListener("message", message); client.addListener("message", message);
client.users.addListener("mutation", relationship); client.addListener("user/relationship", relationship);
return () => { return () => {
client.removeListener("message", message); client.removeListener("message", message);
client.users.removeListener("mutation", relationship); client.removeListener("user/relationship", relationship);
}; };
}, [client, playSound, guild_id, channel_id, showNotification, notifs]); }, [
client,
playSound,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => { useEffect(() => {
function visChange() { function visChange() {
......
import { openDB } from "idb"; /* eslint-disable react-hooks/rules-of-hooks */
import { useHistory } from "react-router-dom";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { Route } from "revolt.js/dist/api/routes"; import { Route } from "revolt.js/dist/api/routes";
...@@ -14,7 +13,6 @@ import { AuthState } from "../../redux/reducers/auth"; ...@@ -14,7 +13,6 @@ import { AuthState } from "../../redux/reducers/auth";
import Preloader from "../../components/ui/Preloader"; import Preloader from "../../components/ui/Preloader";
import { useData } from "../../mobx/State";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import { useIntermediate } from "../intermediate/Intermediate"; import { useIntermediate } from "../intermediate/Intermediate";
import { registerEvents, setReconnectDisallowed } from "./events"; import { registerEvents, setReconnectDisallowed } from "./events";
...@@ -36,8 +34,6 @@ export interface ClientOperations { ...@@ -36,8 +34,6 @@ export interface ClientOperations {
logout: (shouldRequest?: boolean) => Promise<void>; logout: (shouldRequest?: boolean) => Promise<void>;
loggedIn: () => boolean; loggedIn: () => boolean;
ready: () => boolean; ready: () => boolean;
openDM: (user_id: string) => Promise<string>;
} }
// By the time they are used, they should all be initialized. // By the time they are used, they should all be initialized.
...@@ -53,7 +49,6 @@ type Props = { ...@@ -53,7 +49,6 @@ type Props = {
}; };
function Context({ auth, children }: Props) { function Context({ auth, children }: Props) {
const history = useHistory();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT); const [status, setStatus] = useState(ClientStatus.INIT);
const [client, setClient] = useState<Client>( const [client, setClient] = useState<Client>(
...@@ -62,34 +57,10 @@ function Context({ auth, children }: Props) { ...@@ -62,34 +57,10 @@ function Context({ auth, children }: Props) {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
let db;
try {
// Match sw.ts#L23
db = await openDB("state", 3, {
upgrade(db) {
for (const store of [
"channels",
"servers",
"users",
"members",
]) {
db.createObjectStore(store, {
keyPath: "_id",
});
}
},
});
} catch (err) {
console.error(
"Failed to open IndexedDB store, continuing without.",
);
}
const client = new Client({ const client = new Client({
autoReconnect: false, autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL, apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV, debug: import.meta.env.DEV,
db,
}); });
setClient(client); setClient(client);
...@@ -150,26 +121,16 @@ function Context({ auth, children }: Props) { ...@@ -150,26 +121,16 @@ function Context({ auth, children }: Props) {
loggedIn: () => typeof auth.active !== "undefined", loggedIn: () => typeof auth.active !== "undefined",
ready: () => ready: () =>
operations.loggedIn() && typeof client.user !== "undefined", operations.loggedIn() && typeof client.user !== "undefined",
openDM: async (user_id: string) => {
const channel = await client.users.openDM(user_id);
history.push(`/channel/${channel!._id}`);
return channel!._id;
},
}; };
}, [client, auth.active]); }, [client, auth.active, openScreen]);
const store = useData();
useEffect( useEffect(
() => registerEvents({ operations }, setStatus, client, store), () => registerEvents({ operations }, setStatus, client),
[client, store], [client, operations],
); );
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (client.db) {
await client.restore();
}
if (auth.active) { if (auth.active) {
dispatch({ type: "QUEUE_FAIL_ALL" }); dispatch({ type: "QUEUE_FAIL_ALL" });
...@@ -218,6 +179,7 @@ function Context({ auth, children }: Props) { ...@@ -218,6 +179,7 @@ function Context({ auth, children }: Props) {
setStatus(ClientStatus.READY); setStatus(ClientStatus.READY);
} }
})(); })();
// eslint-disable-next-line
}, []); }, []);
if (status === ClientStatus.LOADING) { if (status === ClientStatus.LOADING) {
......
/** /**
* This file monitors the message cache to delete any queued messages that have already sent. * This file monitors the message cache to delete any queued messages that have already sent.
*/ */
import { Message } from "revolt.js"; import { Message } from "revolt.js/dist/maps/Messages";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue"; import { QueuedMessage } from "../../redux/reducers/queue";
import { Typing } from "../../redux/reducers/typing";
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
type Props = { type Props = {
messages: QueuedMessage[]; messages: QueuedMessage[];
typing: Typing;
}; };
function StateMonitor(props: Props) { function StateMonitor(props: Props) {
...@@ -39,31 +37,7 @@ function StateMonitor(props: Props) { ...@@ -39,31 +37,7 @@ function StateMonitor(props: Props) {
client.addListener("message", add); client.addListener("message", add);
return () => client.removeListener("message", add); return () => client.removeListener("message", add);
}, [props.messages]); }, [client, props.messages]);
useEffect(() => {
function removeOld() {
if (!props.typing) return;
for (const channel of Object.keys(props.typing)) {
const users = props.typing[channel];
for (const user of users) {
if (+new Date() > user.started + 5000) {
dispatch({
type: "TYPING_STOP",
channel,
user: user.id,
});
}
}
}
}
removeOld();
const interval = setInterval(removeOld, 1000);
return () => clearInterval(interval);
}, [props.typing]);
return null; return null;
} }
...@@ -71,6 +45,5 @@ function StateMonitor(props: Props) { ...@@ -71,6 +45,5 @@ function StateMonitor(props: Props) {
export default connectState(StateMonitor, (state) => { export default connectState(StateMonitor, (state) => {
return { return {
messages: [...state.queue], messages: [...state.queue],
typing: state.typing,
}; };
}); });
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
* This file monitors changes to settings and syncs them to the server. * This file monitors changes to settings and syncs them to the server.
*/ */
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { Sync } from "revolt.js/dist/api/objects"; import { UserSettings } from "revolt-api/types/Sync";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect, useMemo } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
...@@ -28,10 +28,10 @@ type Props = { ...@@ -28,10 +28,10 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
const lastValues: { [key in SyncKeys]?: any } = {}; const lastValues: { [key in SyncKeys]?: unknown } = {};
export function mapSync( export function mapSync(
packet: Sync.UserSettings, packet: UserSettings,
revision?: Record<string, number>, revision?: Record<string, number>,
) { ) {
const update: { [key in SyncKeys]?: [number, SyncData[key]] } = {}; const update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
...@@ -78,31 +78,38 @@ function SyncManager(props: Props) { ...@@ -78,31 +78,38 @@ function SyncManager(props: Props) {
.syncFetchUnreads() .syncFetchUnreads()
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads })); .then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
} }
}, [status]); }, [client, props.sync?.disabled, status]);
function syncChange(key: SyncKeys, data: any) { const syncChange = useCallback(
const timestamp = +new Date(); (key: SyncKeys, data: unknown) => {
dispatch({ const timestamp = +new Date();
type: "SYNC_SET_REVISION", dispatch({
key, type: "SYNC_SET_REVISION",
timestamp, key,
}); timestamp,
});
client.syncSetSettings(
{ client.syncSetSettings(
[key]: data, {
}, [key]: data as string,
timestamp, },
); timestamp,
} );
},
const disabled = props.sync.disabled ?? []; [client],
);
const disabled = useMemo(
() => props.sync.disabled ?? [],
[props.sync.disabled],
);
for (const [key, object] of [ for (const [key, object] of [
["appearance", props.settings.appearance], ["appearance", props.settings.appearance],
["theme", props.settings.theme], ["theme", props.settings.theme],
["locale", props.locale], ["locale", props.locale],
["notifications", props.notifications], ["notifications", props.notifications],
] as [SyncKeys, any][]) { ] as [SyncKeys, unknown][]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { useEffect(() => {
if (disabled.indexOf(key) === -1) { if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== "undefined") { if (typeof lastValues[key] !== "undefined") {
...@@ -113,7 +120,7 @@ function SyncManager(props: Props) { ...@@ -113,7 +120,7 @@ function SyncManager(props: Props) {
} }
lastValues[key] = object; lastValues[key] = object;
}, [disabled, object]); }, [key, syncChange, disabled, object]);
} }
useEffect(() => { useEffect(() => {
...@@ -131,7 +138,7 @@ function SyncManager(props: Props) { ...@@ -131,7 +138,7 @@ function SyncManager(props: Props) {
client.addListener("packet", onPacket); client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket); return () => client.removeListener("packet", onPacket);
}, [disabled, props.sync]); }, [client, disabled, props.sync]);
return null; return null;
} }
......
import { Client, Message } from "revolt.js/dist"; import { Client } from "revolt.js/dist";
import { Message } from "revolt.js/dist/maps/Messages";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { StateUpdater } from "preact/hooks"; import { StateUpdater } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { DataStore } from "../../mobx";
import { useData } from "../../mobx/State";
import { ClientOperations, ClientStatus } from "./RevoltClient"; import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false; export let preventReconnect = false;
let preventUntil = 0; let preventUntil = 0;
export function setReconnectDisallowed(allowed: boolean) { export function setReconnectDisallowed(allowed: boolean) {
...@@ -20,7 +19,6 @@ export function registerEvents( ...@@ -20,7 +19,6 @@ export function registerEvents(
{ operations }: { operations: ClientOperations }, { operations }: { operations: ClientOperations },
setStatus: StateUpdater<ClientStatus>, setStatus: StateUpdater<ClientStatus>,
client: Client, client: Client,
store: DataStore,
) { ) {
function attemptReconnect() { function attemptReconnect() {
if (preventReconnect) return; if (preventReconnect) return;
...@@ -36,6 +34,7 @@ export function registerEvents( ...@@ -36,6 +34,7 @@ export function registerEvents(
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let listeners: Record<string, (...args: any[]) => void> = { let listeners: Record<string, (...args: any[]) => void> = {
connecting: () => connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING), operations.ready() && setStatus(ClientStatus.CONNECTING),
...@@ -48,26 +47,7 @@ export function registerEvents( ...@@ -48,26 +47,7 @@ export function registerEvents(
}, },
packet: (packet: ClientboundNotification) => { packet: (packet: ClientboundNotification) => {
store.packet(packet);
switch (packet.type) { switch (packet.type) {
case "ChannelStartTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_START",
channel: packet.id,
user: packet.user,
});
break;
}
case "ChannelStopTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_STOP",
channel: packet.id,
user: packet.user,
});
break;
}
case "ChannelAck": { case "ChannelAck": {
dispatch({ dispatch({
type: "UNREADS_MARK_READ", type: "UNREADS_MARK_READ",
...@@ -80,10 +60,10 @@ export function registerEvents( ...@@ -80,10 +60,10 @@ export function registerEvents(
}, },
message: (message: Message) => { message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) { if (message.mention_ids?.includes(client.user!._id)) {
dispatch({ dispatch({
type: "UNREADS_MENTION", type: "UNREADS_MENTION",
channel: message.channel, channel: message.channel_id,
message: message._id, message: message._id,
}); });
} }
...@@ -95,7 +75,7 @@ export function registerEvents( ...@@ -95,7 +75,7 @@ export function registerEvents(
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
listeners = new Proxy(listeners, { listeners = new Proxy(listeners, {
get: get:
(target, listener, receiver) => (target, listener) =>
(...args: unknown[]) => { (...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args); console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args); Reflect.get(target, listener)(...args);
...@@ -108,17 +88,6 @@ export function registerEvents( ...@@ -108,17 +88,6 @@ export function registerEvents(
client.addListener(listener, listeners[listener]); client.addListener(listener, listeners[listener]);
} }
function logMutation(target: string, key: string) {
console.log("(o) Object mutated", target, "\nChanged:", key);
}
if (import.meta.env.DEV) {
client.users.addListener("mutation", logMutation);
client.servers.addListener("mutation", logMutation);
client.channels.addListener("mutation", logMutation);
client.members.addListener("mutation", logMutation);
}
const online = () => { const online = () => {
if (operations.ready()) { if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING); setStatus(ClientStatus.RECONNECTING);
...@@ -146,13 +115,6 @@ export function registerEvents( ...@@ -146,13 +115,6 @@ export function registerEvents(
); );
} }
if (import.meta.env.DEV) {
client.users.removeListener("mutation", logMutation);
client.servers.removeListener("mutation", logMutation);
client.channels.removeListener("mutation", logMutation);
client.members.removeListener("mutation", logMutation);
}
window.removeEventListener("online", online); window.removeEventListener("online", online);
window.removeEventListener("offline", offline); window.removeEventListener("offline", offline);
}; };
......
import isEqual from "lodash.isequal";
import { Client, PermissionCalculator } from "revolt.js";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
import Collection from "revolt.js/dist/maps/Collection";
import { useContext, useEffect, useState } from "preact/hooks";
//#region Hooks v1 (deprecated)
import { AppContext } from "./RevoltClient";
export interface HookContext {
client: Client;
forceUpdate: () => void;
}
export function useForceUpdate(context?: HookContext): HookContext {
const client = useContext(AppContext);
if (context) return context;
const H = useState(0);
let updateState: (_: number) => void;
if (Array.isArray(H)) {
const [, u] = H;
updateState = u;
} else {
console.warn("Failed to construct using useState.");
updateState = () => {};
}
return { client, forceUpdate: () => updateState(Math.random()) };
}
// TODO: utils.d.ts maybe?
type PickProperties<T, U> = Pick<
T,
{
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T]
>;
// The keys in Client that are an object
// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
type ClientCollectionKey = Exclude<
keyof PickProperties<Client, Collection<any>>,
undefined
>;
function useObject(
type: ClientCollectionKey,
id?: string | string[],
context?: HookContext,
) {
const ctx = useForceUpdate(context);
function update(target: any) {
if (
typeof id === "string"
? target === id
: Array.isArray(id)
? id.includes(target)
: true
) {
ctx.forceUpdate();
}
}
const map = ctx.client[type];
useEffect(() => {
map.addListener("update", update);
return () => map.removeListener("update", update);
}, [id]);
return typeof id === "string"
? map.get(id)
: Array.isArray(id)
? id.map((x) => map.get(x))
: map.toArray();
}
export function useServer(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject("servers", id, context) as
| Readonly<Servers.Server>
| undefined;
}
export function useServers(ids?: string[], context?: HookContext) {
return useObject("servers", ids, context) as (
| Readonly<Servers.Server>
| undefined
)[];
}
export function useMember(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject("members", id, context) as
| Readonly<Servers.Member>
| undefined;
}
export function useDMs(context?: HookContext) {
const ctx = useForceUpdate(context);
function mutation(target: string) {
const channel = ctx.client.channels.get(target);
if (channel) {
if (
channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group"
) {
ctx.forceUpdate();
}
}
}
const map = ctx.client.channels;
useEffect(() => {
map.addListener("update", mutation);
return () => map.removeListener("update", mutation);
}, []);
return map
.toArray()
.filter(
(x) =>
x.channel_type === "DirectMessage" ||
x.channel_type === "Group" ||
x.channel_type === "SavedMessages",
) as (
| Channels.GroupChannel
| Channels.DirectMessageChannel
| Channels.SavedMessagesChannel
)[];
}
export function useUserPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const mutation = (target: string) => target === id && ctx.forceUpdate();
useEffect(() => {
ctx.client.users.addListener("update", mutation);
return () => ctx.client.users.removeListener("update", mutation);
}, [id]);
const calculator = new PermissionCalculator(ctx.client);
return calculator.forUser(id);
}
export function useChannelPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const channel = ctx.client.channels.get(id);
const server =
channel &&
(channel.channel_type === "TextChannel" ||
channel.channel_type === "VoiceChannel")
? channel.server
: undefined;
const mutation = (target: string) => target === id && ctx.forceUpdate();
const mutationServer = (target: string) =>
target === server && ctx.forceUpdate();
const mutationMember = (target: string) =>
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
useEffect(() => {
ctx.client.channels.addListener("update", mutation);
if (server) {
ctx.client.servers.addListener("update", mutationServer);
ctx.client.members.addListener("update", mutationMember);
}
return () => {
ctx.client.channels.removeListener("update", mutation);
if (server) {
ctx.client.servers.removeListener("update", mutationServer);
ctx.client.members.removeListener("update", mutationMember);
}
};
}, [id]);
const calculator = new PermissionCalculator(ctx.client);
return calculator.forChannel(id);
}
export function useServerPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const mutation = (target: string) => target === id && ctx.forceUpdate();
const mutationMember = (target: string) =>
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
useEffect(() => {
ctx.client.servers.addListener("update", mutation);
ctx.client.members.addListener("update", mutationMember);
return () => {
ctx.client.servers.removeListener("update", mutation);
ctx.client.members.removeListener("update", mutationMember);
};
}, [id]);
const calculator = new PermissionCalculator(ctx.client);
return calculator.forServer(id);
}
//#endregion
//#region Hooks v2 (deprecated)
type CollectionKeys = Exclude<
keyof PickProperties<Client, Collection<any>>,
undefined
>;
interface Depedency {
key: CollectionKeys;
id?: string;
}
export function useDataDeprecated<T>(
cb: (client: Client) => T,
dependencies: Depedency[],
): T {
// ! FIXME: not sure if this may cost a lot
const client = useContext(AppContext);
const [data, setData] = useState(cb(client));
useEffect(() => {
let fns = dependencies.map((dependency) => {
function update() {
let generated = cb(client);
if (!isEqual(data, generated)) {
setData(generated);
}
}
client[dependency.key].addListener("update", update);
return () =>
client[dependency.key].removeListener("update", update);
});
return () => fns.forEach((x) => x());
}, [data]);
return data;
}
//#endregion
import { Client } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/api/objects";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { Channel } from "../../mobx";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function takeError(error: any): string { export function takeError(error: any): string {
const type = error?.response?.data?.type; const type = error?.response?.data?.type;
const id = type; const id = type;
...@@ -25,7 +23,6 @@ export function takeError(error: any): string { ...@@ -25,7 +23,6 @@ export function takeError(error: any): string {
} }
export function getChannelName( export function getChannelName(
client: Client,
channel: Channel, channel: Channel,
prefixType?: boolean, prefixType?: boolean,
): Children { ): Children {
...@@ -33,11 +30,10 @@ export function getChannelName( ...@@ -33,11 +30,10 @@ export function getChannelName(
return <Text id="app.navigation.tabs.saved" />; return <Text id="app.navigation.tabs.saved" />;
if (channel.channel_type === "DirectMessage") { if (channel.channel_type === "DirectMessage") {
const uid = client.channels.getRecipient(channel._id);
return ( return (
<> <>
{prefixType && "@"} {prefixType && "@"}
{client.users.get(uid)?.username} {channel.recipient!.username}
</> </>
); );
} }
...@@ -48,12 +44,3 @@ export function getChannelName( ...@@ -48,12 +44,3 @@ export function getChannelName(
return <>{channel.name}</>; return <>{channel.name}</>;
} }
export type MessageObject = Omit<Message, "edited"> & { edited?: string };
export function mapMessage(message: Partial<Message>) {
const { edited, ...msg } = message;
return {
...msg,
edited: edited?.$date,
} as MessageObject;
}
type Build = "stable" | "nightly" | "dev";
type NativeConfig = {
frame: boolean;
build: Build;
discordRPC: boolean;
hardwareAcceleration: boolean;
};
declare interface Window {
isNative?: boolean;
nativeVersion: string;
native: {
min();
max();
close();
reload();
relaunch();
getConfig(): NativeConfig;
set(key: keyof NativeConfig, value: unknown);
getAutoStart(): Promise<boolean>;
enableAutoStart(): Promise<void>;
disableAutoStart(): Promise<void>;
};
}
declare const Fragment = preact.Fragment;
...@@ -12,21 +12,19 @@ import { ...@@ -12,21 +12,19 @@ import {
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import { Attachment } from "revolt-api/types/Autumn";
Attachment, import { Presence, RelationshipStatus } from "revolt-api/types/Users";
Channels,
Message,
Servers,
Users,
} from "revolt.js/dist/api/objects";
import { import {
ChannelPermission, ChannelPermission,
ServerPermission, ServerPermission,
UserPermission, UserPermission,
} from "revolt.js/dist/api/permissions"; } from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { import {
ContextMenu,
ContextMenuWithData, ContextMenuWithData,
MenuItem, MenuItem,
openContextMenu, openContextMenu,
...@@ -34,8 +32,6 @@ import { ...@@ -34,8 +32,6 @@ import {
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { Channel, User } from "../mobx";
import { useData } from "../mobx/State";
import { dispatch } from "../redux"; import { dispatch } from "../redux";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
import { import {
...@@ -45,20 +41,12 @@ import { ...@@ -45,20 +41,12 @@ import {
} from "../redux/reducers/notifications"; } from "../redux/reducers/notifications";
import { QueuedMessage } from "../redux/reducers/queue"; import { QueuedMessage } from "../redux/reducers/queue";
import { useIntermediate } from "../context/intermediate/Intermediate"; import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
import { import {
AppContext, AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import {
useChannelPermission,
useForceUpdate,
useServer,
useServerPermission,
useUserPermission,
} from "../context/revoltjs/hooks";
import { takeError } from "../context/revoltjs/util"; import { takeError } from "../context/revoltjs/util";
import Tooltip from "../components/common/Tooltip"; import Tooltip from "../components/common/Tooltip";
...@@ -92,15 +80,15 @@ type Action = ...@@ -92,15 +80,15 @@ type Action =
| { action: "reply_message"; id: string } | { action: "reply_message"; id: string }
| { action: "quote_message"; content: string } | { action: "quote_message"; content: string }
| { action: "edit_message"; id: string } | { action: "edit_message"; id: string }
| { action: "delete_message"; target: Channels.Message } | { action: "delete_message"; target: Message }
| { action: "open_file"; attachment: Attachment } | { action: "open_file"; attachment: Attachment }
| { action: "save_file"; attachment: Attachment } | { action: "save_file"; attachment: Attachment }
| { action: "copy_file_link"; attachment: Attachment } | { action: "copy_file_link"; attachment: Attachment }
| { action: "open_link"; link: string } | { action: "open_link"; link: string }
| { action: "copy_link"; link: string } | { action: "copy_link"; link: string }
| { action: "remove_member"; channel: string; user: User } | { action: "remove_member"; channel: Channel; user: User }
| { action: "kick_member"; target: Servers.Server; user: User } | { action: "kick_member"; target: Server; user: User }
| { action: "ban_member"; target: Servers.Server; user: User } | { action: "ban_member"; target: Server; user: User }
| { action: "view_profile"; user: User } | { action: "view_profile"; user: User }
| { action: "message_user"; user: User } | { action: "message_user"; user: User }
| { action: "block_user"; user: User } | { action: "block_user"; user: User }
...@@ -108,10 +96,10 @@ type Action = ...@@ -108,10 +96,10 @@ type Action =
| { action: "add_friend"; user: User } | { action: "add_friend"; user: User }
| { action: "remove_friend"; user: User } | { action: "remove_friend"; user: User }
| { action: "cancel_friend"; user: User } | { action: "cancel_friend"; user: User }
| { action: "set_presence"; presence: Users.Presence } | { action: "set_presence"; presence: Presence }
| { action: "set_status" } | { action: "set_status" }
| { action: "clear_status" } | { action: "clear_status" }
| { action: "create_channel"; target: Servers.Server } | { action: "create_channel"; target: Server }
| { | {
action: "create_invite"; action: "create_invite";
target: Channel; target: Channel;
...@@ -122,8 +110,8 @@ type Action = ...@@ -122,8 +110,8 @@ type Action =
target: Channel; target: Channel;
} }
| { action: "close_dm"; target: Channel } | { action: "close_dm"; target: Channel }
| { action: "leave_server"; target: Servers.Server } | { action: "leave_server"; target: Server }
| { action: "delete_server"; target: Servers.Server } | { action: "delete_server"; target: Server }
| { action: "open_notification_options"; channel: Channel } | { action: "open_notification_options"; channel: Channel }
| { action: "open_settings" } | { action: "open_settings" }
| { action: "open_channel_settings"; id: string } | { action: "open_channel_settings"; id: string }
...@@ -139,7 +127,8 @@ type Props = { ...@@ -139,7 +127,8 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
// ! FIXME: no observers here! // ! FIXME: I dare someone to re-write this
// Tip: This should just be split into separate context menus per logical area.
function ContextMenus(props: Props) { function ContextMenus(props: Props) {
const { openScreen, writeClipboard } = useIntermediate(); const { openScreen, writeClipboard } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
...@@ -188,7 +177,7 @@ function ContextMenus(props: Props) { ...@@ -188,7 +177,7 @@ function ContextMenus(props: Props) {
case "retry_message": case "retry_message":
{ {
const nonce = data.message.id; const nonce = data.message.id;
const fail = (error: any) => const fail = (error: string) =>
dispatch({ dispatch({
type: "QUEUE_FAIL", type: "QUEUE_FAIL",
nonce, nonce,
...@@ -196,7 +185,8 @@ function ContextMenus(props: Props) { ...@@ -196,7 +185,8 @@ function ContextMenus(props: Props) {
}); });
client.channels client.channels
.sendMessage(data.message.channel, { .get(data.message.channel)!
.sendMessage({
nonce: data.message.id, nonce: data.message.id,
content: data.message.data.content as string, content: data.message.data.content as string,
replies: data.message.data.replies, replies: data.message.data.replies,
...@@ -313,10 +303,7 @@ function ContextMenus(props: Props) { ...@@ -313,10 +303,7 @@ function ContextMenus(props: Props) {
case "remove_member": case "remove_member":
{ {
client.channels.removeMember( data.channel.removeMember(data.user._id);
data.channel,
data.user._id,
);
} }
break; break;
...@@ -326,9 +313,7 @@ function ContextMenus(props: Props) { ...@@ -326,9 +313,7 @@ function ContextMenus(props: Props) {
case "message_user": case "message_user":
{ {
const channel = await client.users.openDM( const channel = await data.user.openDM();
data.user._id,
);
if (channel) { if (channel) {
history.push(`/channel/${channel._id}`); history.push(`/channel/${channel._id}`);
} }
...@@ -337,7 +322,7 @@ function ContextMenus(props: Props) { ...@@ -337,7 +322,7 @@ function ContextMenus(props: Props) {
case "add_friend": case "add_friend":
{ {
await client.users.addFriend(data.user.username); await data.user.addFriend();
} }
break; break;
...@@ -349,7 +334,7 @@ function ContextMenus(props: Props) { ...@@ -349,7 +334,7 @@ function ContextMenus(props: Props) {
}); });
break; break;
case "unblock_user": case "unblock_user":
await client.users.unblockUser(data.user._id); await data.user.unblockUser();
break; break;
case "remove_friend": case "remove_friend":
openScreen({ openScreen({
...@@ -359,12 +344,12 @@ function ContextMenus(props: Props) { ...@@ -359,12 +344,12 @@ function ContextMenus(props: Props) {
}); });
break; break;
case "cancel_friend": case "cancel_friend":
await client.users.removeFriend(data.user._id); await data.user.removeFriend();
break; break;
case "set_presence": case "set_presence":
{ {
await client.users.editUser({ await client.users.edit({
status: { status: {
...client.user?.status, ...client.user?.status,
presence: data.presence, presence: data.presence,
...@@ -382,8 +367,9 @@ function ContextMenus(props: Props) { ...@@ -382,8 +367,9 @@ function ContextMenus(props: Props) {
case "clear_status": case "clear_status":
{ {
const { text, ...status } = client.user?.status ?? {}; const { text: _text, ...status } =
await client.users.editUser({ status }); client.user?.status ?? {};
await client.users.edit({ status });
} }
break; break;
...@@ -395,12 +381,12 @@ function ContextMenus(props: Props) { ...@@ -395,12 +381,12 @@ function ContextMenus(props: Props) {
case "delete_message": case "delete_message":
case "create_channel": case "create_channel":
case "create_invite": case "create_invite":
// The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever // Typescript flattens the case types into a single type and type structure and specifity is lost
openScreen({ openScreen({
id: "special_prompt", id: "special_prompt",
type: data.action, type: data.action,
target: data.target as any, target: data.target,
}); } as unknown as Screen);
break; break;
case "ban_member": case "ban_member":
...@@ -463,9 +449,6 @@ function ContextMenus(props: Props) { ...@@ -463,9 +449,6 @@ function ContextMenus(props: Props) {
unread, unread,
contextualChannel: cxid, contextualChannel: cxid,
}: ContextMenuData) => { }: ContextMenuData) => {
const store = useData();
const forceUpdate = useForceUpdate();
const elements: Children[] = []; const elements: Children[] = [];
let lastDivider = false; let lastDivider = false;
...@@ -495,11 +478,8 @@ function ContextMenus(props: Props) { ...@@ -495,11 +478,8 @@ function ContextMenus(props: Props) {
} }
if (server_list) { if (server_list) {
const server = useServer(server_list, forceUpdate); const server = client.servers.get(server_list)!;
const permissions = useServerPermission( const permissions = server.permission;
server_list,
forceUpdate,
);
if (server) { if (server) {
if (permissions & ServerPermission.ManageChannels) if (permissions & ServerPermission.ManageChannels)
generateAction({ generateAction({
...@@ -526,38 +506,31 @@ function ContextMenus(props: Props) { ...@@ -526,38 +506,31 @@ function ContextMenus(props: Props) {
pushDivider(); pushDivider();
} }
const channel = cid ? store.channels.get(cid) : undefined; const channel = cid ? client.channels.get(cid) : undefined;
const contextualChannel = cxid const contextualChannel = cxid
? store.channels.get(cxid) ? client.channels.get(cxid)
: undefined; : undefined;
const targetChannel = channel ?? contextualChannel; const targetChannel = channel ?? contextualChannel;
const user = uid ? store.users.get(uid) : undefined; const user = uid ? client.users.get(uid) : undefined;
const serverChannel = const serverChannel =
targetChannel && targetChannel &&
(targetChannel.channel_type === "TextChannel" || (targetChannel.channel_type === "TextChannel" ||
targetChannel.channel_type === "VoiceChannel") targetChannel.channel_type === "VoiceChannel")
? targetChannel ? targetChannel
: undefined; : undefined;
const server = useServer(
serverChannel ? serverChannel.server! : sid,
forceUpdate,
);
const channelPermissions = targetChannel const s = serverChannel ? serverChannel.server_id! : sid;
? useChannelPermission(targetChannel._id, forceUpdate) const server = s ? client.servers.get(s) : undefined;
: 0;
const serverPermissions = server const channelPermissions = targetChannel?.permission || 0;
? useServerPermission(server._id, forceUpdate) const serverPermissions =
: serverChannel (server
? useServerPermission( ? server.permission
serverChannel.server!, : serverChannel
forceUpdate, ? serverChannel.server?.permission
) : 0) || 0;
: 0; const userPermissions = (user ? user.permission : 0) || 0;
const userPermissions = user
? useUserPermission(user._id, forceUpdate)
: 0;
if (channel && unread) { if (channel && unread) {
generateAction({ action: "mark_as_read", channel }); generateAction({ action: "mark_as_read", channel });
...@@ -577,29 +550,29 @@ function ContextMenus(props: Props) { ...@@ -577,29 +550,29 @@ function ContextMenus(props: Props) {
if (user) { if (user) {
let actions: Action["action"][]; let actions: Action["action"][];
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.User: case RelationshipStatus.User:
actions = []; actions = [];
break; break;
case Users.Relationship.Friend: case RelationshipStatus.Friend:
actions = ["remove_friend", "block_user"]; actions = ["remove_friend", "block_user"];
break; break;
case Users.Relationship.Incoming: case RelationshipStatus.Incoming:
actions = [ actions = [
"add_friend", "add_friend",
"cancel_friend", "cancel_friend",
"block_user", "block_user",
]; ];
break; break;
case Users.Relationship.Outgoing: case RelationshipStatus.Outgoing:
actions = ["cancel_friend", "block_user"]; actions = ["cancel_friend", "block_user"];
break; break;
case Users.Relationship.Blocked: case RelationshipStatus.Blocked:
actions = ["unblock_user"]; actions = ["unblock_user"];
break; break;
case Users.Relationship.BlockedOther: case RelationshipStatus.BlockedOther:
actions = ["block_user"]; actions = ["block_user"];
break; break;
case Users.Relationship.None: case RelationshipStatus.None:
default: default:
actions = ["add_friend", "block_user"]; actions = ["add_friend", "block_user"];
} }
...@@ -622,20 +595,23 @@ function ContextMenus(props: Props) { ...@@ -622,20 +595,23 @@ function ContextMenus(props: Props) {
} }
for (let i = 0; i < actions.length; i++) { for (let i = 0; i < actions.length; i++) {
// The any here is because typescript can't determine that user the actions are linked together correctly // Typescript can't determine that user the actions are linked together correctly
generateAction({ action: actions[i] as any, user }); generateAction({
action: actions[i],
user,
} as unknown as Action);
} }
} }
if (contextualChannel) { if (contextualChannel) {
if (contextualChannel.channel_type === "Group" && uid) { if (contextualChannel.channel_type === "Group" && uid) {
if ( if (
contextualChannel.owner === userId && contextualChannel.owner_id === userId &&
userId !== uid userId !== uid
) { ) {
generateAction({ generateAction({
action: "remove_member", action: "remove_member",
channel: contextualChannel._id, channel: contextualChannel,
user: user!, user: user!,
}); });
} }
...@@ -698,7 +674,7 @@ function ContextMenus(props: Props) { ...@@ -698,7 +674,7 @@ function ContextMenus(props: Props) {
}); });
} }
if (message.author === userId) { if (message.author_id === userId) {
generateAction({ generateAction({
action: "edit_message", action: "edit_message",
id: message._id, id: message._id,
...@@ -706,7 +682,7 @@ function ContextMenus(props: Props) { ...@@ -706,7 +682,7 @@ function ContextMenus(props: Props) {
} }
if ( if (
message.author === userId || message.author_id === userId ||
channelPermissions & channelPermissions &
ChannelPermission.ManageMessages ChannelPermission.ManageMessages
) { ) {
...@@ -808,11 +784,15 @@ function ContextMenus(props: Props) { ...@@ -808,11 +784,15 @@ function ContextMenus(props: Props) {
break; break;
case "TextChannel": case "TextChannel":
case "VoiceChannel": case "VoiceChannel":
// ! FIXME: add permission for invites if (
generateAction({ channelPermissions &
action: "create_invite", ChannelPermission.InviteOthers
target: channel, ) {
}); generateAction({
action: "create_invite",
target: channel,
});
}
if ( if (
serverPermissions & serverPermissions &
...@@ -821,7 +801,7 @@ function ContextMenus(props: Props) { ...@@ -821,7 +801,7 @@ function ContextMenus(props: Props) {
generateAction( generateAction(
{ {
action: "open_server_channel_settings", action: "open_server_channel_settings",
server: channel.server!, server: channel.server_id!,
id: channel._id, id: channel._id,
}, },
"open_channel_settings", "open_channel_settings",
...@@ -886,9 +866,7 @@ function ContextMenus(props: Props) { ...@@ -886,9 +866,7 @@ function ContextMenus(props: Props) {
onClose={contextClick} onClose={contextClick}
className="Status"> className="Status">
{() => { {() => {
const store = useData(); const user = client.user!;
const user = store.users.get(client.user!._id)!;
return ( return (
<> <>
<div className="header"> <div className="header">
...@@ -928,7 +906,7 @@ function ContextMenus(props: Props) { ...@@ -928,7 +906,7 @@ function ContextMenus(props: Props) {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Users.Presence.Online, presence: Presence.Online,
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator online" /> <div className="indicator online" />
...@@ -937,7 +915,7 @@ function ContextMenus(props: Props) { ...@@ -937,7 +915,7 @@ function ContextMenus(props: Props) {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Users.Presence.Idle, presence: Presence.Idle,
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator idle" /> <div className="indicator idle" />
...@@ -946,7 +924,7 @@ function ContextMenus(props: Props) { ...@@ -946,7 +924,7 @@ function ContextMenus(props: Props) {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Users.Presence.Busy, presence: Presence.Busy,
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator busy" /> <div className="indicator busy" />
...@@ -955,7 +933,7 @@ function ContextMenus(props: Props) { ...@@ -955,7 +933,7 @@ function ContextMenus(props: Props) {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Users.Presence.Invisible, presence: Presence.Invisible,
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator invisible" /> <div className="indicator invisible" />
...@@ -983,7 +961,7 @@ function ContextMenus(props: Props) { ...@@ -983,7 +961,7 @@ function ContextMenus(props: Props) {
<ContextMenuWithData <ContextMenuWithData
id="NotificationOptions" id="NotificationOptions"
onClose={contextClick}> onClose={contextClick}>
{({ channel }: { channel: Channels.Channel }) => { {({ channel }: { channel: Channel }) => {
const state = props.notifications[channel._id]; const state = props.notifications[channel._id];
const actual = getNotificationState( const actual = getNotificationState(
props.notifications, props.notifications,
...@@ -992,6 +970,7 @@ function ContextMenus(props: Props) { ...@@ -992,6 +970,7 @@ function ContextMenus(props: Props) {
const elements: Children[] = [ const elements: Children[] = [
<MenuItem <MenuItem
key="notif"
data={{ data={{
action: "set_notification_state", action: "set_notification_state",
key: channel._id, key: channel._id,
...@@ -1011,6 +990,7 @@ function ContextMenus(props: Props) { ...@@ -1011,6 +990,7 @@ function ContextMenus(props: Props) {
function generate(key: string, icon: Children) { function generate(key: string, icon: Children) {
elements.push( elements.push(
<MenuItem <MenuItem
key={key}
data={{ data={{
action: "set_notification_state", action: "set_notification_state",
key: channel._id, key: channel._id,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {}; const counts: { [key: string]: number } = {};
......