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 641 additions and 1008 deletions
import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid";
import { Text } from "preact-i18n";
......@@ -81,7 +82,7 @@ type SpecialProps = { onClose: () => void } & (
| "set_custom_status"
| "add_friend";
}
| { type: "create_role"; server: string; callback: (id: string) => void }
| { type: "create_role"; server: Server; callback: (id: string) => void }
);
export function SpecialInputModal(props: SpecialProps) {
......@@ -134,10 +135,7 @@ export function SpecialInputModal(props: SpecialProps) {
}
field={<Text id="app.settings.permissions.role_name" />}
callback={async (name) => {
const role = await client.servers.createRole(
props.server,
name,
);
const role = await props.server.createRole(name);
props.callback(role.id);
}}
/>
......@@ -151,7 +149,7 @@ export function SpecialInputModal(props: SpecialProps) {
field={<Text id="app.context_menu.custom_status" />}
defaultValue={client.user?.status?.text}
callback={(text) =>
client.users.editUser({
client.users.edit({
status: {
...client.user?.status,
text: text.trim().length > 0 ? text : undefined,
......@@ -166,7 +164,14 @@ export function SpecialInputModal(props: SpecialProps) {
<InputModal
onClose={onClose}
question={"Add Friend"}
callback={(username) => client.users.addFriend(username)}
callback={(username) =>
client
.req(
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined)
}
/>
);
}
......
......@@ -29,7 +29,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
setLoading(true);
callback(username, true)
.then(() => onClose())
.catch((err: any) => {
.catch((err: unknown) => {
setError(takeError(err));
setLoading(false);
});
......
......@@ -7,7 +7,7 @@
user-select: all;
font-size: 1.4em;
text-align: center;
font-family: var(--monoscape-font);
font-family: var(--monospace-font);
}
}
......
import { observer } from "mobx-react-lite";
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 styles from "./Prompt.module.scss";
......@@ -17,7 +21,7 @@ import Radio from "../../../components/ui/Radio";
import { Children } from "../../../types/Preact";
import { AppContext } from "../../revoltjs/RevoltClient";
import { mapMessage, takeError } from "../../revoltjs/util";
import { takeError } from "../../revoltjs/util";
import { useIntermediate } from "../Intermediate";
interface Props {
......@@ -51,24 +55,24 @@ export function PromptModal({
}
type SpecialProps = { onClose: () => void } & (
| { type: "leave_group"; target: Channels.GroupChannel }
| { type: "close_dm"; target: Channels.DirectMessageChannel }
| { type: "leave_server"; target: Servers.Server }
| { type: "delete_server"; target: Servers.Server }
| { type: "delete_channel"; target: Channels.TextChannel }
| { type: "delete_message"; target: Channels.Message }
| { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channel }
| { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: MessageI }
| {
type: "create_invite";
target: Channels.TextChannel | Channels.GroupChannel;
target: Channel;
}
| { type: "kick_member"; target: Servers.Server; user: string }
| { type: "ban_member"; target: Servers.Server; user: string }
| { type: "unfriend_user"; target: Users.User }
| { type: "block_user"; target: Users.User }
| { type: "create_channel"; target: Servers.Server }
| { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User }
| { type: "create_channel"; target: Server }
);
export function SpecialPromptModal(props: SpecialProps) {
export const SpecialPromptModal = observer((props: SpecialProps) => {
const client = useContext(AppContext);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<undefined | string>(undefined);
......@@ -100,9 +104,7 @@ export function SpecialPromptModal(props: SpecialProps) {
name = props.target.username;
break;
case "close_dm":
name = client.users.get(
client.channels.getRecipient(props.target._id),
)?.username;
name = props.target.recipient?.username;
break;
default:
name = props.target.name;
......@@ -133,27 +135,19 @@ export function SpecialPromptModal(props: SpecialProps) {
try {
switch (props.type) {
case "unfriend_user":
await client.users.removeFriend(
props.target._id,
);
await props.target.removeFriend();
break;
case "block_user":
await client.users.blockUser(
props.target._id,
);
await props.target.blockUser();
break;
case "leave_group":
case "close_dm":
case "delete_channel":
await client.channels.delete(
props.target._id,
);
props.target.delete();
break;
case "leave_server":
case "delete_server":
await client.servers.delete(
props.target._id,
);
props.target.delete();
break;
}
......@@ -199,11 +193,7 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true);
try {
await client.channels.deleteMessage(
props.target.channel,
props.target._id,
);
props.target.delete();
onClose();
} catch (err) {
setError(takeError(err));
......@@ -225,7 +215,7 @@ export function SpecialPromptModal(props: SpecialProps) {
id={`app.special.modals.prompt.confirm_delete_message_long`}
/>
<Message
message={mapMessage(props.target)}
message={props.target}
head={true}
contrast
/>
......@@ -243,12 +233,12 @@ export function SpecialPromptModal(props: SpecialProps) {
useEffect(() => {
setProcessing(true);
client.channels
.createInvite(props.target._id)
props.target
.createInvite()
.then((code) => setCode(code))
.catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false));
}, []);
}, [props.target]);
return (
<PromptModal
......@@ -286,8 +276,6 @@ export function SpecialPromptModal(props: SpecialProps) {
);
}
case "kick_member": {
const user = client.users.get(props.user);
return (
<PromptModal
onClose={onClose}
......@@ -304,10 +292,13 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true);
try {
await client.servers.members.kickMember(
props.target._id,
props.user,
);
client.members
.getKey({
server: props.target._id,
user: props.user._id,
})
?.kick();
onClose();
} catch (err) {
setError(takeError(err));
......@@ -324,10 +315,10 @@ export function SpecialPromptModal(props: SpecialProps) {
]}
content={
<div className={styles.column}>
<UserIcon target={user} size={64} />
<UserIcon target={props.user} size={64} />
<Text
id="app.special.modals.prompt.confirm_kick"
fields={{ name: user?.username }}
fields={{ name: props.user?.username }}
/>
</div>
}
......@@ -338,7 +329,6 @@ export function SpecialPromptModal(props: SpecialProps) {
}
case "ban_member": {
const [reason, setReason] = useState<string | undefined>(undefined);
const user = client.users.get(props.user);
return (
<PromptModal
......@@ -356,11 +346,9 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true);
try {
await client.servers.banUser(
props.target._id,
props.user,
{ reason },
);
await props.target.banUser(props.user._id, {
reason,
});
onClose();
} catch (err) {
setError(takeError(err));
......@@ -377,10 +365,10 @@ export function SpecialPromptModal(props: SpecialProps) {
]}
content={
<div className={styles.column}>
<UserIcon target={user} size={64} />
<UserIcon target={props.user} size={64} />
<Text
id="app.special.modals.prompt.confirm_ban"
fields={{ name: user?.username }}
fields={{ name: props.user?.username }}
/>
<Overline>
<Text id="app.special.modals.prompt.confirm_ban_reason" />
......@@ -419,14 +407,11 @@ export function SpecialPromptModal(props: SpecialProps) {
try {
const channel =
await client.servers.createChannel(
props.target._id,
{
type,
name,
nonce: ulid(),
},
);
await props.target.createChannel({
type,
name,
nonce: ulid(),
});
history.push(
`/server/${props.target._id}/channel/${channel._id}`,
......@@ -477,4 +462,4 @@ export function SpecialPromptModal(props: SpecialProps) {
default:
return null;
}
}
});
import { X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styles from "./ChannelInfo.module.scss";
import Modal from "../../../components/ui/Modal";
import Markdown from "../../../components/markdown/Markdown";
import { useChannel, useForceUpdate } from "../../revoltjs/hooks";
import { getChannelName } from "../../revoltjs/util";
interface Props {
channel_id: string;
channel: Channel;
onClose: () => void;
}
export function ChannelInfo({ channel_id, onClose }: Props) {
const ctx = useForceUpdate();
const channel = useChannel(channel_id, ctx);
if (!channel) return null;
export const ChannelInfo = observer(({ channel, onClose }: Props) => {
if (
channel.channel_type === "DirectMessage" ||
channel.channel_type === "SavedMessages"
......@@ -30,15 +27,15 @@ export function ChannelInfo({ channel_id, onClose }: Props) {
<Modal visible={true} onClose={onClose}>
<div className={styles.info}>
<div className={styles.header}>
<h1>{getChannelName(ctx.client, channel, true)}</h1>
<h1>{getChannelName(channel, true)}</h1>
<div onClick={onClose}>
<X size={36} />
</div>
</div>
<p>
<Markdown content={channel.description} />
<Markdown content={channel.description!} />
</p>
</div>
</Modal>
);
}
});
import {
Attachment,
AttachmentMetadata,
EmbedImage,
} from "revolt.js/dist/api/objects";
/* 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 { useContext, useEffect } from "preact/hooks";
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
......
......@@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) {
]}>
{/* Preact / React typing incompatabilities */}
<form
onSubmit={
onSubmit={(e) => {
e.preventDefault();
handleSubmit(
onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement>
}>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
)(e as any);
}}>
{field === "email" && (
<FormField
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";
import { useUsers } from "../../revoltjs/hooks";
interface Props {
users: string[];
users: User[];
onClose: () => void;
}
export function PendingRequests({ users: ids, onClose }: Props) {
const users = useUsers(ids);
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
.filter((x) => typeof x !== "undefined")
.map((x) => (
<Friend user={x!} key={x!._id} />
))}
{users.map((x) => (
<Friend user={x!} key={x!._id} />
))}
</div>
</Modal>
);
}
});
......@@ -4,7 +4,6 @@
max-height: 360px;
overflow-y: scroll;
// ! FIXME: very temporary code
> label {
> span {
align-items: flex-start !important;
......@@ -18,4 +17,4 @@
}
}
}
}
\ No newline at end of file
}
import { User, Users } from "revolt.js/dist/api/objects";
import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n";
......@@ -7,7 +7,7 @@ import { useState } from "preact/hooks";
import UserCheckbox from "../../../components/common/user/UserCheckbox";
import Modal from "../../../components/ui/Modal";
import { useUsers } from "../../revoltjs/hooks";
import { useClient } from "../../revoltjs/RevoltClient";
interface Props {
omit?: string[];
......@@ -19,7 +19,7 @@ export function UserPicker(props: Props) {
const [selected, setSelected] = useState<string[]>([]);
const omit = [...(props.omit || []), "00000000000000000000000000"];
const users = useUsers();
const client = useClient();
return (
<Modal
......@@ -33,24 +33,18 @@ export function UserPicker(props: Props) {
},
]}>
<div className={styles.list}>
{(
users.filter(
{[...client.users.values()]
.filter(
(x) =>
x &&
x.relationship === Users.Relationship.Friend &&
x.relationship === RelationshipStatus.Friend &&
!omit.includes(x._id),
) as User[]
)
.map((x) => {
return {
...x,
selected: selected.includes(x._id),
};
})
)
.map((x) => (
<UserCheckbox
key={x._id}
user={x}
checked={x.selected}
checked={selected.includes(x._id)}
onChange={(v) => {
if (v) {
setSelected([...selected, x._id]);
......
......@@ -57,13 +57,13 @@
gap: 8px;
display: flex;
padding: 0 1.5em;
font-size: .875rem;
font-size: 0.875rem;
> div {
padding: 8px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: border-bottom .3s;
transition: border-bottom 0.3s;
&[data-active="true"] {
border-bottom: 2px solid var(--foreground);
......@@ -81,7 +81,10 @@
height: 100%;
display: flex;
padding: 1em 1.5em;
max-width: 560px;
max-height: 240px;
overflow-y: auto;
flex-direction: column;
background: var(--primary-background);
......@@ -141,7 +144,7 @@
display: flex;
cursor: pointer;
align-items: center;
transition: background-color .1s;
transition: background-color 0.1s;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background);
......
import { Money } from "@styled-icons/boxicons-regular";
import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom";
import { Users } from "revolt.js/dist/api/objects";
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 { decodeTime } from "ulid";
import styles from "./UserProfile.module.scss";
import { Localizer, Text } from "preact-i18n";
......@@ -20,23 +20,17 @@ import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown";
import {
AppContext,
ClientStatus,
StatusContext,
useClient,
} from "../../revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUserPermission,
useUsers,
} from "../../revoltjs/hooks";
import { useIntermediate } from "../Intermediate";
interface Props {
user_id: string;
dummy?: boolean;
onClose: () => void;
dummyProfile?: Users.Profile;
onClose?: () => void;
dummyProfile?: Profile;
}
enum Badges {
......@@ -47,311 +41,313 @@ enum Badges {
EarlyAdopter = 256,
}
export function UserProfile({ 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);
export const UserProfile = observer(
({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate();
const history = useHistory();
const client = useContext(AppContext);
const status = useContext(StatusContext);
const [tab, setTab] = useState("profile");
const [profile, setProfile] = useState<undefined | null | Profile>(
undefined,
);
const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const ctx = useForceUpdate();
const all_users = useUsers(undefined, ctx);
const channels = useChannels(undefined, ctx);
const history = useHistory();
const client = useClient();
const status = useContext(StatusContext);
const [tab, setTab] = useState("profile");
const user = all_users.find((x) => x!._id === user_id);
const users = mutual?.users
? all_users.filter((x) => mutual.users.includes(x!._id))
: undefined;
if (!user) {
useEffect(onClose, []);
return null;
}
const user = client.users.get(user_id);
if (!user) {
if (onClose) useEffect(onClose, []);
return null;
}
const permissions = useUserPermission(user!._id, ctx);
const users = mutual?.users.map((id) => client.users.get(id));
useLayoutEffect(() => {
if (!user_id) return;
if (typeof profile !== "undefined") setProfile(undefined);
if (typeof mutual !== "undefined") setMutual(undefined);
}, [user_id]);
const mutualGroups = [...client.channels.values()].filter(
(channel) =>
channel?.channel_type === "Group" &&
channel.recipient_ids!.includes(user_id),
);
if (dummy) {
useLayoutEffect(() => {
setProfile(dummyProfile);
}, [dummyProfile]);
}
if (!user_id) return;
if (typeof profile !== "undefined") setProfile(undefined);
if (typeof mutual !== "undefined") setMutual(undefined);
// eslint-disable-next-line
}, [user_id]);
useEffect(() => {
if (dummy) return;
if (status === ClientStatus.ONLINE && typeof mutual === "undefined") {
setMutual(null);
client.users.fetchMutual(user_id).then((data) => setMutual(data));
}
}, [mutual, status]);
useEffect(() => {
if (dummy) return;
if (status === ClientStatus.ONLINE && typeof profile === "undefined") {
setProfile(null);
useEffect(() => {
if (dummy) {
setProfile(dummyProfile);
}
}, [dummy, dummyProfile]);
if (permissions & UserPermission.ViewProfile) {
client.users
.fetchProfile(user_id)
.then((data) => setProfile(data))
.catch(() => {});
useEffect(() => {
if (dummy) return;
if (
status === ClientStatus.ONLINE &&
typeof mutual === "undefined"
) {
setMutual(null);
user.fetchMutual().then(setMutual);
}
}
}, [profile, status]);
}, [mutual, status, dummy, user]);
const mutualGroups = channels.filter(
(channel) =>
channel?.channel_type === "Group" &&
channel.recipients.includes(user_id),
);
useEffect(() => {
if (dummy) return;
if (
status === ClientStatus.ONLINE &&
typeof profile === "undefined"
) {
setProfile(null);
const backgroundURL =
profile &&
client.users.getBackgroundURL(profile, { width: 1000 }, true);
const badges =
(user.badges ?? 0) |
(decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
if (user.permission & UserPermission.ViewProfile) {
user.fetchProfile().then(setProfile);
}
}
}, [profile, status, dummy, user]);
return (
<Modal
visible
border={dummy}
padding={false}
onClose={onClose}
dontModal={dummy}>
<div
className={styles.header}
data-force={profile?.background ? "light" : undefined}
style={{
backgroundImage:
backgroundURL &&
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
}}>
<div className={styles.profile}>
<UserIcon size={80} target={user} status animate />
<div className={styles.details}>
<Localizer>
<span
className={styles.username}
onClick={() => writeClipboard(user.username)}>
@{user.username}
</span>
</Localizer>
{user.status?.text && (
<span className={styles.status}>
<UserStatus user={user} tooltip />
</span>
)}
</div>
{user.relationship === Users.Relationship.Friend && (
<Localizer>
<Tooltip
content={
<Text id="app.context_menu.message_user" />
}>
<IconButton
onClick={() => {
onClose();
history.push(`/open/${user_id}`);
}}>
<Envelope size={30} />
</IconButton>
</Tooltip>
</Localizer>
)}
{user.relationship === Users.Relationship.User && (
<IconButton
onClick={() => {
onClose();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)}
{(user.relationship === Users.Relationship.Incoming ||
user.relationship === Users.Relationship.None) && (
<IconButton
onClick={() =>
client.users.addFriend(user.username)
}>
<UserPlus size={28} />
</IconButton>
)}
</div>
<div className={styles.tabs}>
<div
data-active={tab === "profile"}
onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" />
</div>
{user.relationship !== Users.Relationship.User && (
<>
<div
data-active={tab === "friends"}
onClick={() => setTab("friends")}>
<Text id="app.special.popovers.user_profile.mutual_friends" />
</div>
<div
data-active={tab === "groups"}
onClick={() => setTab("groups")}>
<Text id="app.special.popovers.user_profile.mutual_groups" />
</div>
</>
)}
</div>
</div>
<div className={styles.content}>
{tab === "profile" && (
<div>
{!(profile?.content || badges > 0) && (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" />
</div>
const backgroundURL =
profile &&
client.generateFileURL(profile.background, { width: 1000 }, true);
const badges = user.badges ?? 0;
return (
<Modal
visible
border={dummy}
padding={false}
onClose={onClose}
dontModal={dummy}>
<div
className={styles.header}
data-force={profile?.background ? "light" : undefined}
style={{
backgroundImage:
backgroundURL &&
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
}}>
<div className={styles.profile}>
<UserIcon size={80} target={user} status animate />
<div className={styles.details}>
<Localizer>
<span
className={styles.username}
onClick={() =>
writeClipboard(user.username)
}>
@{user.username}
</span>
</Localizer>
{user.status?.text && (
<span className={styles.status}>
<UserStatus user={user} tooltip />
</span>
)}
</div>
{user.relationship === RelationshipStatus.Friend && (
<Localizer>
<Tooltip
content={
<Text id="app.context_menu.message_user" />
}>
<IconButton
onClick={() => {
onClose?.();
history.push(`/open/${user_id}`);
}}>
<Envelope size={30} />
</IconButton>
</Tooltip>
</Localizer>
)}
{badges > 0 && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.badges" />
</div>
{user.relationship === RelationshipStatus.User && (
<IconButton
onClick={() => {
onClose?.();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)}
{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>
{(user.relationship === RelationshipStatus.Incoming ||
user.relationship === RelationshipStatus.None) && (
<IconButton onClick={() => user.addFriend()}>
<UserPlus size={28} />
</IconButton>
)}
{profile?.content && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.information" />
</div>
</div>
<div className={styles.tabs}>
<div
data-active={tab === "profile"}
onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" />
</div>
{user.relationship !== RelationshipStatus.User && (
<>
<div
data-active={tab === "friends"}
onClick={() => setTab("friends")}>
<Text id="app.special.popovers.user_profile.mutual_friends" />
</div>
<div
data-active={tab === "groups"}
onClick={() => setTab("groups")}>
<Text id="app.special.popovers.user_profile.mutual_groups" />
</div>
</>
)}
<Markdown content={profile?.content} />
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div>
)}
{tab === "friends" &&
(users ? (
</div>
<div className={styles.content}>
{tab === "profile" && (
<div>
{!(profile?.content || badges > 0) && (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" />
</div>
)}
{badges > 0 && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.badges" />
</div>
)}
{badges > 0 && (
<div className={styles.badges}>
<Localizer>
{badges & Badges.Developer ? (
<Tooltip
content={
<Text id="app.navigation.tabs.dev" />
}>
<img src="/assets/badges/developer.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Translator ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.translator" />
}>
<img src="/assets/badges/translator.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.EarlyAdopter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.early_adopter" />
}>
<img src="/assets/badges/early_adopter.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Supporter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.supporter" />
}>
<Money
size={32}
color="#efab44"
/>
</Tooltip>
) : (
<></>
)}
{badges &
Badges.ResponsibleDisclosure ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
}>
<Shield
size={32}
color="gray"
/>
</Tooltip>
) : (
<></>
)}
</Localizer>
</div>
)}
{profile?.content && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.information" />
</div>
)}
<Markdown content={profile?.content} />
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div>
)}
{tab === "friends" &&
(users ? (
<div className={styles.entries}>
{users.length === 0 ? (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_users" />
</div>
) : (
users.map(
(x) =>
x && (
<div
onClick={() =>
openScreen({
id: "profile",
user_id: x._id,
})
}
className={styles.entry}
key={x._id}>
<UserIcon
size={32}
target={x}
/>
<span>{x.username}</span>
</div>
),
)
)}
</div>
) : (
<Preloader type="ring" />
))}
{tab === "groups" && (
<div className={styles.entries}>
{users.length === 0 ? (
{mutualGroups.length === 0 ? (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_users" />
<Text id="app.special.popovers.user_profile.no_groups" />
</div>
) : (
users.map(
mutualGroups.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>
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>
) : (
<Preloader type="ring" />
))}
{tab === "groups" && (
<div className={styles.entries}>
{mutualGroups.length === 0 ? (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_groups" />
</div>
) : (
mutualGroups.map(
(x) =>
x?.channel_type === "Group" && (
<Link to={`/channel/${x._id}`}>
<div
className={styles.entry}
key={x._id}>
<ChannelIcon
target={x}
size={32}
/>
<span>{x.name}</span>
</div>
</Link>
),
)
)}
</div>
)}
</div>
</Modal>
);
}
)}
</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 Axios, { AxiosRequestConfig } from "axios";
......@@ -147,6 +147,7 @@ export function FileUploader(props: Props) {
}
if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => {
// File pasting.
function paste(e: ClipboardEvent) {
......@@ -210,7 +211,7 @@ export function FileUploader(props: Props) {
document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop);
};
}, [props.append]);
}, [openScreen, props, props.append]);
}
if (props.style === "icon" || props.style === "banner") {
......
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
import { Users } from "revolt.js/dist/api/objects";
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 { useContext, useEffect } from "preact/hooks";
import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n";
......@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) {
const history = useHistory();
const playSound = useContext(SoundContext);
async function message(msg: Message) {
if (msg.author === client.user!._id) return;
if (msg.channel === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Users.Presence.Busy) return;
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 channel = client.channels.get(msg.channel);
const author = client.users.get(msg.author);
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const notifState = getNotificationState(notifs, msg.channel!);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
const notifState = getNotificationState(notifs, channel);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
playSound("message");
if (!showNotification) return;
playSound("message");
if (!showNotification) return;
let title;
switch (channel.channel_type) {
case "SavedMessages":
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,
});
let title;
switch (msg.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${msg.author?.username}`;
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.id)?.username },
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
case "Group":
if (msg.author?._id === SYSTEM_USER_ID) {
title = msg.channel.name;
} else {
title = `@${msg.author?.username} - ${msg.channel.name}`;
}
break;
case "channel_renamed":
body = translate(
`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,
});
case "TextChannel":
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
break;
case "channel_description_changed":
case "channel_icon_changed":
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,
});
default:
title = msg.channel?._id;
break;
}
}
const notif = await createNotification(title, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
});
let image;
if (msg.attachments) {
const imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
}
}
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = msg.channel;
if (id !== channel_id) {
const channel = client.channels.get(id);
if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server}/channel/${id}`,
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,
},
);
} else {
history.push(`/channel/${id}`);
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,
});
notifications[msg.channel] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel],
);
}
}
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,
],
);
async function relationship(user: User, property: string) {
if (client.user?.status?.presence === Users.Presence.Busy) return;
if (property !== "relationship") return;
if (!showNotification) return;
const relationship = useCallback(
async (user: User) => {
if (client.user?.status?.presence === Presence.Busy) return;
if (!showNotification) return;
let event;
switch (user.relationship) {
case Users.Relationship.Incoming:
event = translate("notifications.sent_request", {
person: user.username,
});
break;
case Users.Relationship.Friend:
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
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: client.users.getAvatarURL(user._id, { max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
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`);
});
}
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
},
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => {
client.addListener("message", message);
client.users.addListener("mutation", relationship);
client.addListener("user/relationship", relationship);
return () => {
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(() => {
function visChange() {
......
import { openDB } from "idb";
import { useHistory } from "react-router-dom";
/* eslint-disable react-hooks/rules-of-hooks */
import { Client } from "revolt.js";
import { Route } from "revolt.js/dist/api/routes";
......@@ -35,8 +34,6 @@ export interface ClientOperations {
logout: (shouldRequest?: boolean) => Promise<void>;
loggedIn: () => boolean;
ready: () => boolean;
openDM: (user_id: string) => Promise<string>;
}
// By the time they are used, they should all be initialized.
......@@ -52,7 +49,6 @@ type Props = {
};
function Context({ auth, children }: Props) {
const history = useHistory();
const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT);
const [client, setClient] = useState<Client>(
......@@ -61,34 +57,10 @@ function Context({ auth, children }: Props) {
useEffect(() => {
(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({
autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV,
db,
});
setClient(client);
......@@ -149,25 +121,16 @@ function Context({ auth, children }: Props) {
loggedIn: () => typeof auth.active !== "undefined",
ready: () =>
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]);
useEffect(
() => registerEvents({ operations }, setStatus, client),
[client],
[client, operations],
);
useEffect(() => {
(async () => {
if (client.db) {
await client.restore();
}
if (auth.active) {
dispatch({ type: "QUEUE_FAIL_ALL" });
......@@ -216,6 +179,7 @@ function Context({ auth, children }: Props) {
setStatus(ClientStatus.READY);
}
})();
// eslint-disable-next-line
}, []);
if (status === ClientStatus.LOADING) {
......
/**
* 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 { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue";
import { Typing } from "../../redux/reducers/typing";
import { AppContext } from "./RevoltClient";
type Props = {
messages: QueuedMessage[];
typing: Typing;
};
function StateMonitor(props: Props) {
......@@ -39,31 +37,7 @@ function StateMonitor(props: Props) {
client.addListener("message", add);
return () => client.removeListener("message", add);
}, [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]);
}, [client, props.messages]);
return null;
}
......@@ -71,6 +45,5 @@ function StateMonitor(props: Props) {
export default connectState(StateMonitor, (state) => {
return {
messages: [...state.queue],
typing: state.typing,
};
});
......@@ -2,10 +2,10 @@
* This file monitors changes to settings and syncs them to the server.
*/
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 { useContext, useEffect } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo } from "preact/hooks";
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
......@@ -28,10 +28,10 @@ type Props = {
notifications: Notifications;
};
const lastValues: { [key in SyncKeys]?: any } = {};
const lastValues: { [key in SyncKeys]?: unknown } = {};
export function mapSync(
packet: Sync.UserSettings,
packet: UserSettings,
revision?: Record<string, number>,
) {
const update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
......@@ -78,31 +78,38 @@ function SyncManager(props: Props) {
.syncFetchUnreads()
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
}
}, [status]);
function syncChange(key: SyncKeys, data: any) {
const timestamp = +new Date();
dispatch({
type: "SYNC_SET_REVISION",
key,
timestamp,
});
client.syncSetSettings(
{
[key]: data,
},
timestamp,
);
}
const disabled = props.sync.disabled ?? [];
}, [client, props.sync?.disabled, status]);
const syncChange = useCallback(
(key: SyncKeys, data: unknown) => {
const timestamp = +new Date();
dispatch({
type: "SYNC_SET_REVISION",
key,
timestamp,
});
client.syncSetSettings(
{
[key]: data as string,
},
timestamp,
);
},
[client],
);
const disabled = useMemo(
() => props.sync.disabled ?? [],
[props.sync.disabled],
);
for (const [key, object] of [
["appearance", props.settings.appearance],
["theme", props.settings.theme],
["locale", props.locale],
["notifications", props.notifications],
] as [SyncKeys, any][]) {
] as [SyncKeys, unknown][]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== "undefined") {
......@@ -113,7 +120,7 @@ function SyncManager(props: Props) {
}
lastValues[key] = object;
}, [disabled, object]);
}, [key, syncChange, disabled, object]);
}
useEffect(() => {
......@@ -131,7 +138,7 @@ function SyncManager(props: Props) {
client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket);
}, [disabled, props.sync]);
}, [client, disabled, props.sync]);
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 { StateUpdater } from "preact/hooks";
......@@ -7,7 +8,7 @@ import { dispatch } from "../../redux";
import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false;
export let preventReconnect = false;
let preventUntil = 0;
export function setReconnectDisallowed(allowed: boolean) {
......@@ -33,6 +34,7 @@ export function registerEvents(
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let listeners: Record<string, (...args: any[]) => void> = {
connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING),
......@@ -46,24 +48,6 @@ export function registerEvents(
packet: (packet: ClientboundNotification) => {
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": {
dispatch({
type: "UNREADS_MARK_READ",
......@@ -76,10 +60,10 @@ export function registerEvents(
},
message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) {
if (message.mention_ids?.includes(client.user!._id)) {
dispatch({
type: "UNREADS_MENTION",
channel: message.channel,
channel: message.channel_id,
message: message._id,
});
}
......@@ -91,7 +75,7 @@ export function registerEvents(
if (import.meta.env.DEV) {
listeners = new Proxy(listeners, {
get:
(target, listener, receiver) =>
(target, listener) =>
(...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args);
......@@ -104,17 +88,6 @@ export function registerEvents(
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.servers.members.addListener("mutation", logMutation);
}
const online = () => {
if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING);
......@@ -142,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.servers.members.removeListener("mutation", logMutation);
}
window.removeEventListener("online", online);
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
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 useUser(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject("users", id, context) as Readonly<Users.User> | undefined;
}
export function useSelf(context?: HookContext) {
const ctx = useForceUpdate(context);
return useUser(ctx.client.user!._id, ctx);
}
export function useUsers(ids?: string[], context?: HookContext) {
return useObject("users", ids, context) as (
| Readonly<Users.User>
| undefined
)[];
}
export function useChannel(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject("channels", id, context) as
| Readonly<Channels.Channel>
| undefined;
}
export function useChannels(ids?: string[], context?: HookContext) {
return useObject("channels", ids, context) as (
| Readonly<Channels.Channel>
| undefined
)[];
}
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 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.servers.members.addListener("update", mutationMember);
}
return () => {
ctx.client.channels.removeListener("update", mutation);
if (server) {
ctx.client.servers.removeListener("update", mutationServer);
ctx.client.servers.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.servers.members.addListener("update", mutationMember);
return () => {
ctx.client.servers.removeListener("update", mutation);
ctx.client.servers.members.removeListener("update", mutationMember);
};
}, [id]);
const calculator = new PermissionCalculator(ctx.client);
return calculator.forServer(id);
}
//#endregion
//#region Hooks v2
type CollectionKeys = Exclude<
keyof PickProperties<Client, Collection<any>>,
undefined
>;
interface Depedency {
key: CollectionKeys;
id?: string;
}
export function useData<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, Message, User } from "revolt.js/dist/api/objects";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Text } from "preact-i18n";
import { Children } from "../../types/Preact";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function takeError(error: any): string {
const type = error?.response?.data?.type;
const id = type;
......@@ -23,7 +23,6 @@ export function takeError(error: any): string {
}
export function getChannelName(
client: Client,
channel: Channel,
prefixType?: boolean,
): Children {
......@@ -31,11 +30,10 @@ export function getChannelName(
return <Text id="app.navigation.tabs.saved" />;
if (channel.channel_type === "DirectMessage") {
const uid = client.channels.getRecipient(channel._id);
return (
<>
{prefixType && "@"}
{client.users.get(uid)?.username}
{channel.recipient!.username}
</>
);
}
......@@ -46,12 +44,3 @@ export function getChannelName(
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;
}