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 432 additions and 402 deletions
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;
...@@ -10,7 +10,6 @@ export default function ConditionalLink(props: Props) { ...@@ -10,7 +10,6 @@ export default function ConditionalLink(props: Props) {
if (active) { if (active) {
return <a>{props.children}</a>; return <a>{props.children}</a>;
} else {
return <Link {...linkProps} />;
} }
return <Link {...linkProps} />;
} }
...@@ -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,
...@@ -43,23 +41,15 @@ import { ...@@ -43,23 +41,15 @@ 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,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import {
useChannel,
useChannelPermission,
useForceUpdate,
useServer,
useServerPermission,
useUser,
useUserPermission,
} from "../context/revoltjs/hooks";
import { takeError } from "../context/revoltjs/util"; import { takeError } from "../context/revoltjs/util";
import Tooltip from "../components/common/Tooltip";
import UserStatus from "../components/common/user/UserStatus"; import UserStatus from "../components/common/user/UserStatus";
import IconButton from "../components/ui/IconButton"; import IconButton from "../components/ui/IconButton";
import LineDivider from "../components/ui/LineDivider"; import LineDivider from "../components/ui/LineDivider";
...@@ -83,49 +73,46 @@ type Action = ...@@ -83,49 +73,46 @@ type Action =
| { action: "copy_id"; id: string } | { action: "copy_id"; id: string }
| { action: "copy_selection" } | { action: "copy_selection" }
| { action: "copy_text"; content: string } | { action: "copy_text"; content: string }
| { action: "mark_as_read"; channel: Channels.Channel } | { action: "mark_as_read"; channel: Channel }
| { action: "retry_message"; message: QueuedMessage } | { action: "retry_message"; message: QueuedMessage }
| { action: "cancel_message"; message: QueuedMessage } | { action: "cancel_message"; message: QueuedMessage }
| { action: "mention"; user: string } | { action: "mention"; user: string }
| { 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: string } | { action: "remove_member"; channel: Channel; user: User }
| { action: "kick_member"; target: Servers.Server; user: string } | { action: "kick_member"; target: Server; user: User }
| { action: "ban_member"; target: Servers.Server; user: string } | { action: "ban_member"; target: Server; user: User }
| { action: "view_profile"; user: string } | { action: "view_profile"; user: User }
| { action: "message_user"; user: string } | { action: "message_user"; user: User }
| { action: "block_user"; user: Users.User } | { action: "block_user"; user: User }
| { action: "unblock_user"; user: Users.User } | { action: "unblock_user"; user: User }
| { action: "add_friend"; user: Users.User } | { action: "add_friend"; user: User }
| { action: "remove_friend"; user: Users.User } | { action: "remove_friend"; user: User }
| { action: "cancel_friend"; user: Users.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: target: Channel;
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
} }
| { action: "leave_group"; target: Channels.GroupChannel } | { action: "leave_group"; target: Channel }
| { | {
action: "delete_channel"; action: "delete_channel";
target: Channels.TextChannel | Channels.VoiceChannel; target: Channel;
} }
| { action: "close_dm"; target: Channels.DirectMessageChannel } | { 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: Channels.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 }
| { action: "open_server_settings"; id: string } | { action: "open_server_settings"; id: string }
...@@ -140,6 +127,8 @@ type Props = { ...@@ -140,6 +127,8 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
// ! 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);
...@@ -167,10 +156,11 @@ function ContextMenus(props: Props) { ...@@ -167,10 +156,11 @@ function ContextMenus(props: Props) {
) )
return; return;
let message = const message =
data.channel.channel_type === "TextChannel" typeof data.channel.last_message === "string"
? data.channel.last_message ? data.channel.last_message
: data.channel.last_message._id; : data.channel.last_message!._id;
dispatch({ dispatch({
type: "UNREADS_MARK_READ", type: "UNREADS_MARK_READ",
channel: data.channel._id, channel: data.channel._id,
...@@ -187,7 +177,7 @@ function ContextMenus(props: Props) { ...@@ -187,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,
...@@ -195,7 +185,8 @@ function ContextMenus(props: Props) { ...@@ -195,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,
...@@ -291,8 +282,9 @@ function ContextMenus(props: Props) { ...@@ -291,8 +282,9 @@ function ContextMenus(props: Props) {
const { filename } = data.attachment; const { filename } = data.attachment;
writeClipboard( writeClipboard(
// ! FIXME: do from r.js // ! FIXME: do from r.js
client.generateFileURL(data.attachment) + `${client.generateFileURL(
`/${encodeURI(filename)}`, data.attachment,
)}/${encodeURI(filename)}`,
); );
} }
break; break;
...@@ -311,17 +303,17 @@ function ContextMenus(props: Props) { ...@@ -311,17 +303,17 @@ function ContextMenus(props: Props) {
case "remove_member": case "remove_member":
{ {
client.channels.removeMember(data.channel, data.user); data.channel.removeMember(data.user._id);
} }
break; break;
case "view_profile": case "view_profile":
openScreen({ id: "profile", user_id: data.user }); openScreen({ id: "profile", user_id: data.user._id });
break; break;
case "message_user": case "message_user":
{ {
const channel = await client.users.openDM(data.user); const channel = await data.user.openDM();
if (channel) { if (channel) {
history.push(`/channel/${channel._id}`); history.push(`/channel/${channel._id}`);
} }
...@@ -330,7 +322,7 @@ function ContextMenus(props: Props) { ...@@ -330,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;
...@@ -342,7 +334,7 @@ function ContextMenus(props: Props) { ...@@ -342,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({
...@@ -352,12 +344,12 @@ function ContextMenus(props: Props) { ...@@ -352,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,
...@@ -375,8 +367,9 @@ function ContextMenus(props: Props) { ...@@ -375,8 +367,9 @@ function ContextMenus(props: Props) {
case "clear_status": case "clear_status":
{ {
let { text, ...status } = client.user?.status ?? {}; const { text: _text, ...status } =
await client.users.editUser({ status }); client.user?.status ?? {};
await client.users.edit({ status });
} }
break; break;
...@@ -388,12 +381,12 @@ function ContextMenus(props: Props) { ...@@ -388,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":
...@@ -456,9 +449,8 @@ function ContextMenus(props: Props) { ...@@ -456,9 +449,8 @@ function ContextMenus(props: Props) {
unread, unread,
contextualChannel: cxid, contextualChannel: cxid,
}: ContextMenuData) => { }: ContextMenuData) => {
const forceUpdate = useForceUpdate();
const elements: Children[] = []; const elements: Children[] = [];
var lastDivider = false; let lastDivider = false;
function generateAction( function generateAction(
action: Action, action: Action,
...@@ -486,11 +478,8 @@ function ContextMenus(props: Props) { ...@@ -486,11 +478,8 @@ function ContextMenus(props: Props) {
} }
if (server_list) { if (server_list) {
let server = useServer(server_list, forceUpdate); const server = client.servers.get(server_list)!;
let permissions = useServerPermission( const permissions = server.permission;
server_list,
forceUpdate,
);
if (server) { if (server) {
if (permissions & ServerPermission.ManageChannels) if (permissions & ServerPermission.ManageChannels)
generateAction({ generateAction({
...@@ -517,33 +506,31 @@ function ContextMenus(props: Props) { ...@@ -517,33 +506,31 @@ function ContextMenus(props: Props) {
pushDivider(); pushDivider();
} }
const channel = useChannel(cid, forceUpdate); const channel = cid ? client.channels.get(cid) : undefined;
const contextualChannel = useChannel(cxid, forceUpdate); const contextualChannel = cxid
? client.channels.get(cxid)
: undefined;
const targetChannel = channel ?? contextualChannel; const targetChannel = channel ?? contextualChannel;
const user = useUser(uid, forceUpdate); 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(serverChannel.server, forceUpdate) ? server.permission
: 0; : serverChannel
const userPermissions = user ? serverChannel.server?.permission
? useUserPermission(user._id, forceUpdate) : 0) || 0;
: 0; const userPermissions = (user ? user.permission : 0) || 0;
if (channel && unread) { if (channel && unread) {
generateAction({ action: "mark_as_read", channel }); generateAction({ action: "mark_as_read", channel });
...@@ -563,29 +550,29 @@ function ContextMenus(props: Props) { ...@@ -563,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"];
} }
...@@ -593,7 +580,7 @@ function ContextMenus(props: Props) { ...@@ -593,7 +580,7 @@ function ContextMenus(props: Props) {
if (userPermissions & UserPermission.ViewProfile) { if (userPermissions & UserPermission.ViewProfile) {
generateAction({ generateAction({
action: "view_profile", action: "view_profile",
user: user._id, user,
}); });
} }
...@@ -603,26 +590,29 @@ function ContextMenus(props: Props) { ...@@ -603,26 +590,29 @@ function ContextMenus(props: Props) {
) { ) {
generateAction({ generateAction({
action: "message_user", action: "message_user",
user: user._id, user,
}); });
} }
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: uid, user: user!,
}); });
} }
} }
...@@ -639,14 +629,14 @@ function ContextMenus(props: Props) { ...@@ -639,14 +629,14 @@ function ContextMenus(props: Props) {
generateAction({ generateAction({
action: "kick_member", action: "kick_member",
target: server, target: server,
user: uid, user: user!,
}); });
if (serverPermissions & ServerPermission.BanMembers) if (serverPermissions & ServerPermission.BanMembers)
generateAction({ generateAction({
action: "ban_member", action: "ban_member",
target: server, target: server,
user: uid, user: user!,
}); });
} }
} }
...@@ -684,7 +674,7 @@ function ContextMenus(props: Props) { ...@@ -684,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,
...@@ -692,7 +682,7 @@ function ContextMenus(props: Props) { ...@@ -692,7 +682,7 @@ function ContextMenus(props: Props) {
} }
if ( if (
message.author === userId || message.author_id === userId ||
channelPermissions & channelPermissions &
ChannelPermission.ManageMessages ChannelPermission.ManageMessages
) { ) {
...@@ -741,7 +731,7 @@ function ContextMenus(props: Props) { ...@@ -741,7 +731,7 @@ function ContextMenus(props: Props) {
} }
if (document.activeElement?.tagName === "A") { if (document.activeElement?.tagName === "A") {
let link = const link =
document.activeElement.getAttribute("href"); document.activeElement.getAttribute("href");
if (link) { if (link) {
pushDivider(); pushDivider();
...@@ -751,7 +741,7 @@ function ContextMenus(props: Props) { ...@@ -751,7 +741,7 @@ function ContextMenus(props: Props) {
} }
} }
let id = sid ?? cid ?? uid ?? message?._id; const id = sid ?? cid ?? uid ?? message?._id;
if (id) { if (id) {
pushDivider(); pushDivider();
...@@ -794,11 +784,15 @@ function ContextMenus(props: Props) { ...@@ -794,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 &
...@@ -807,7 +801,7 @@ function ContextMenus(props: Props) { ...@@ -807,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",
...@@ -871,91 +865,112 @@ function ContextMenus(props: Props) { ...@@ -871,91 +865,112 @@ function ContextMenus(props: Props) {
id="Status" id="Status"
onClose={contextClick} onClose={contextClick}
className="Status"> className="Status">
{() => ( {() => {
<> const user = client.user!;
<div className="header"> return (
<div className="main"> <>
<div>@{client.user!.username}</div> <div className="header">
<div className="status"> <div className="main">
<UserStatus user={client.user!} /> <div
className="username"
onClick={() =>
writeClipboard(
client.user!.username,
)
}>
<Tooltip
content={
<Text id="app.special.copy_username" />
}>
@{user.username}
</Tooltip>
</div>
<div
className="status"
onClick={() =>
contextClick({
action: "set_status",
})
}>
<UserStatus user={user} />
</div>
</div> </div>
</div>
<IconButton>
<MenuItem data={{ action: "open_settings" }}>
<Cog size={22} />
</MenuItem>
</IconButton>
</div>
<LineDivider />
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Online,
}}
disabled={!isOnline}>
<div className="indicator online" />
<Text id={`app.status.online`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Idle,
}}
disabled={!isOnline}>
<div className="indicator idle" />
<Text id={`app.status.idle`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Busy,
}}
disabled={!isOnline}>
<div className="indicator busy" />
<Text id={`app.status.busy`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Invisible,
}}
disabled={!isOnline}>
<div className="indicator invisible" />
<Text id={`app.status.invisible`} />
</MenuItem>
<LineDivider />
<MenuItem
data={{ action: "set_status" }}
disabled={!isOnline}>
<UserVoice size={18} />
<Text
id={`app.context_menu.custom_status`}
/>
{client.user!.status?.text && (
<IconButton> <IconButton>
<MenuItem data={{ action: "clear_status" }}> <MenuItem
<Trash size={18} /> data={{ action: "open_settings" }}>
<Cog size={22} />
</MenuItem> </MenuItem>
</IconButton> </IconButton>
)} </div>
</MenuItem> <LineDivider />
<MenuItem
data={{
</> action: "set_presence",
)} presence: Presence.Online,
}}
disabled={!isOnline}>
<div className="indicator online" />
<Text id={`app.status.online`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Idle,
}}
disabled={!isOnline}>
<div className="indicator idle" />
<Text id={`app.status.idle`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Busy,
}}
disabled={!isOnline}>
<div className="indicator busy" />
<Text id={`app.status.busy`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Invisible,
}}
disabled={!isOnline}>
<div className="indicator invisible" />
<Text id={`app.status.invisible`} />
</MenuItem>
<LineDivider />
<MenuItem
data={{ action: "set_status" }}
disabled={!isOnline}>
<UserVoice size={18} />
<Text id={`app.context_menu.custom_status`} />
{client.user!.status?.text && (
<IconButton>
<MenuItem
data={{ action: "clear_status" }}>
<Trash size={18} />
</MenuItem>
</IconButton>
)}
</MenuItem>
</>
);
}}
</ContextMenuWithData> </ContextMenuWithData>
<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,
channel, channel,
); );
let 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,
...@@ -975,6 +990,7 @@ function ContextMenus(props: Props) { ...@@ -975,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 } = {};
...@@ -11,7 +12,7 @@ export default function PaintCounter({ ...@@ -11,7 +12,7 @@ export default function PaintCounter({
}) { }) {
if (import.meta.env.PROD && !always) return null; if (import.meta.env.PROD && !always) return null;
const [uniqueId] = useState("" + Math.random()); const [uniqueId] = useState(`${Math.random()}`);
const count = counts[uniqueId] ?? 0; const count = counts[uniqueId] ?? 0;
counts[uniqueId] = count + 1; counts[uniqueId] = count + 1;
return ( return (
......
...@@ -10,7 +10,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice"; ...@@ -10,7 +10,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
type TextAreaAutoSizeProps = Omit< type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>, JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" | "onChange" "style" | "value" | "onChange" | "children" | "as"
> & > &
TextAreaProps & { TextAreaProps & {
forceFocus?: boolean; forceFocus?: boolean;
...@@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
lineHeight, lineHeight,
hideBorder, hideBorder,
forceFocus, forceFocus,
children,
as,
onChange, onChange,
...textAreaProps ...textAreaProps
} = props; } = props;
...@@ -74,14 +72,14 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -74,14 +72,14 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
useLayoutEffect(() => { useLayoutEffect(() => {
if (ref.current && ghost.current) { if (ref.current && ghost.current) {
ref.current.style.height = ghost.current.clientHeight + "px"; ref.current.style.height = `${ghost.current.clientHeight}px`;
} }
}, [ghost, props.value]); }, [ghost, props.value]);
useEffect(() => { useEffect(() => {
if (isTouchscreenDevice) return; if (isTouchscreenDevice) return;
autoFocus && ref.current && ref.current.focus(); autoFocus && ref.current && ref.current.focus();
}, [value]); }, [value, autoFocus]);
const inputSelected = () => const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? ""); ["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
...@@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]); }, [ref, autoFocus, forceFocus, value]);
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (!ref.current) return;
...@@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
} }
} }
return internalSubscribe("TextArea", "focus", focus); return internalSubscribe(
}, [ref]); "TextArea",
"focus",
focus as (...args: unknown[]) => void,
);
}, [props.id, ref]);
return ( return (
<Container> <Container>
......
export function urlBase64ToUint8Array(base64String: string) { export function urlBase64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding)
.replace(/\-/g, "+") .replace(/-/g, "+")
.replace(/_/g, "/"); .replace(/_/g, "/");
const rawData = window.atob(base64); const rawData = window.atob(base64);
......
export function debounce(cb: Function, duration: number) { export function debounce(cb: (...args: unknown[]) => void, duration: number) {
// Store the timer variable. // Store the timer variable.
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
// This function is given to React. // This function is given to React.
return (...args: any[]) => { return (...args: unknown[]) => {
// Get rid of the old timer. // Get rid of the old timer.
clearTimeout(timer); clearTimeout(timer);
// Set a new timer. // Set a new timer.
......
...@@ -5,14 +5,14 @@ export const InternalEvent = new EventEmitter(); ...@@ -5,14 +5,14 @@ export const InternalEvent = new EventEmitter();
export function internalSubscribe( export function internalSubscribe(
ns: string, ns: string,
event: string, event: string,
fn: (...args: any[]) => void, fn: (...args: unknown[]) => void,
) { ) {
InternalEvent.addListener(ns + "/" + event, fn); InternalEvent.addListener(`${ns}/${event}`, fn);
return () => InternalEvent.removeListener(ns + "/" + event, fn); return () => InternalEvent.removeListener(`${ns}/${event}`, fn);
} }
export function internalEmit(ns: string, event: string, ...args: any[]) { export function internalEmit(ns: string, event: string, ...args: unknown[]) {
InternalEvent.emit(ns + "/" + event, ...args); InternalEvent.emit(`${ns}/${event}`, ...args);
} }
// Event structure: namespace/event // Event structure: namespace/event
......
import { IntlContext, translate } from "preact-i18n"; import { IntlContext, translate } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { Dictionary } from "../context/Locale";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
interface Fields { interface Fields {
...@@ -12,18 +14,6 @@ interface Props { ...@@ -12,18 +14,6 @@ interface Props {
fields: Fields; fields: Fields;
} }
export interface Dictionary {
dayjs: {
defaults: {
twelvehour: "yes" | "no";
separator: string;
date: "traditional" | "simplified" | "ISO8601";
};
timeFormat: string;
};
[key: string]: Object | string;
}
export interface IntlType { export interface IntlType {
intl: { intl: {
dictionary: Dictionary; dictionary: Dictionary;
...@@ -46,10 +36,9 @@ function recursiveReplaceFields(input: string, fields: Fields) { ...@@ -46,10 +36,9 @@ function recursiveReplaceFields(input: string, fields: Fields) {
} }
return values.flat(); return values.flat();
} else {
// base case
return [input];
} }
// base case
return [input];
} }
export function TextReact({ id, fields }: Props) { export function TextReact({ id, fields }: Props) {
...@@ -57,8 +46,8 @@ export function TextReact({ id, fields }: Props) { ...@@ -57,8 +46,8 @@ export function TextReact({ id, fields }: Props) {
const path = id.split("."); const path = id.split(".");
let entry = intl.dictionary[path.shift()!]; let entry = intl.dictionary[path.shift()!];
for (let key of path) { for (const key of path) {
// @ts-expect-error // @ts-expect-error TODO: lazy
entry = entry[key]; entry = entry[key];
} }
...@@ -67,8 +56,12 @@ export function TextReact({ id, fields }: Props) { ...@@ -67,8 +56,12 @@ export function TextReact({ id, fields }: Props) {
export function useTranslation() { export function useTranslation() {
const { intl } = useContext(IntlContext) as unknown as IntlType; const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) => return (
translate(id, "", intl.dictionary, fields, plural, fallback); id: string,
fields?: Record<string, string | undefined>,
plural?: number,
fallback?: string,
) => translate(id, "", intl.dictionary, fields, plural, fallback);
} }
export function useDictionary() { export function useDictionary() {
......
import { isDesktop, isMobile, isTablet } from "react-device-detect"; import { isDesktop, isMobile, isTablet } from "react-device-detect";
export const isTouchscreenDevice = export const isTouchscreenDevice =
(isDesktop || isTablet) isDesktop || isTablet
? false ? false
: (typeof window !== "undefined" : (typeof window !== "undefined"
? navigator.maxTouchPoints > 0 ? navigator.maxTouchPoints > 0
......
/* eslint-disable @typescript-eslint/no-empty-function */
export const noop = () => {};
export const noopAsync = async () => {};
/* eslint-enable @typescript-eslint/no-empty-function */
/* eslint-disable react-hooks/rules-of-hooks */
import EventEmitter3 from "eventemitter3"; import EventEmitter3 from "eventemitter3";
import { Client, Message } from "revolt.js"; import { Client } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
...@@ -74,10 +76,15 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -74,10 +76,15 @@ export class SingletonRenderer extends EventEmitter3 {
async init(id: string, message_id?: string) { async init(id: string, message_id?: string) {
if (message_id) { if (message_id) {
if (this.state.type === 'RENDER') { if (this.state.type === "RENDER") {
let message = this.state.messages.find(x => x._id === message_id); const message = this.state.messages.find(
(x) => x._id === message_id,
);
if (message) { if (message) {
this.emit("scroll", { type: "ScrollToView", id: message_id }); this.emit("scroll", {
type: "ScrollToView",
id: message_id,
});
return; return;
} }
} }
...@@ -103,9 +110,9 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -103,9 +110,9 @@ export class SingletonRenderer extends EventEmitter3 {
function generateScroll(end: string): ScrollState { function generateScroll(end: string): ScrollState {
if (ref) { if (ref) {
let heightRemoved = 0; let heightRemoved = 0;
let messageContainer = ref.children[0]; const messageContainer = ref.children[0];
if (messageContainer) { if (messageContainer) {
for (let child of Array.from(messageContainer.children)) { for (const child of Array.from(messageContainer.children)) {
// If this child has a ulid. // If this child has a ulid.
if (child.id?.length === 26) { if (child.id?.length === 26) {
// Check whether it was removed. // Check whether it was removed.
...@@ -117,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -117,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
...@@ -127,12 +135,11 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -127,12 +135,11 @@ export class SingletonRenderer extends EventEmitter3 {
type: "OffsetTop", type: "OffsetTop",
previousHeight: ref.scrollHeight - heightRemoved, previousHeight: ref.scrollHeight - heightRemoved,
}; };
} else {
return {
type: "OffsetTop",
previousHeight: 0,
};
} }
return {
type: "OffsetTop",
previousHeight: 0,
};
} }
await this.currentRenderer.loadTop(this, generateScroll); await this.currentRenderer.loadTop(this, generateScroll);
...@@ -148,9 +155,9 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -148,9 +155,9 @@ export class SingletonRenderer extends EventEmitter3 {
function generateScroll(start: string): ScrollState { function generateScroll(start: string): ScrollState {
if (ref) { if (ref) {
let heightRemoved = 0; let heightRemoved = 0;
let messageContainer = ref.children[0]; const messageContainer = ref.children[0];
if (messageContainer) { if (messageContainer) {
for (let child of Array.from(messageContainer.children)) { for (const child of Array.from(messageContainer.children)) {
// If this child has a ulid. // If this child has a ulid.
if (child.id?.length === 26) { if (child.id?.length === 26) {
// Check whether it was removed. // Check whether it was removed.
...@@ -162,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -162,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
...@@ -172,11 +180,10 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -172,11 +180,10 @@ export class SingletonRenderer extends EventEmitter3 {
type: "ScrollTop", type: "ScrollTop",
y: ref.scrollTop - heightRemoved, y: ref.scrollTop - heightRemoved,
}; };
} else {
return {
type: "ScrollToBottom",
};
} }
return {
type: "ScrollToBottom",
};
} }
await this.currentRenderer.loadBottom(this, generateScroll); await this.currentRenderer.loadBottom(this, generateScroll);
......
import { mapMessage } from "../../../context/revoltjs/util"; import { noopAsync } from "../../js";
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton"; import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
import { RendererRoutines } from "../types"; import { RendererRoutines } from "../types";
...@@ -8,10 +7,10 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -8,10 +7,10 @@ export const SimpleRenderer: RendererRoutines = {
if (renderer.client!.websocket.connected) { if (renderer.client!.websocket.connected) {
if (nearby) if (nearby)
renderer renderer
.client!.channels.fetchMessagesWithUsers(id, { nearby, limit: 100 }, true) .client!.channels.get(id)!
.then(({ messages: data }) => { .fetchMessagesWithUsers({ nearby, limit: 100 })
data.sort((a, b) => a._id.localeCompare(b._id)); .then(({ messages }) => {
let messages = data.map((x) => mapMessage(x)); messages.sort((a, b) => a._id.localeCompare(b._id));
renderer.setState( renderer.setState(
id, id,
{ {
...@@ -25,16 +24,16 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -25,16 +24,16 @@ export const SimpleRenderer: RendererRoutines = {
}); });
else else
renderer renderer
.client!.channels.fetchMessagesWithUsers(id, {}, true) .client!.channels.get(id)!
.then(({ messages: data }) => { .fetchMessagesWithUsers({})
data.reverse(); .then(({ messages }) => {
let messages = data.map((x) => mapMessage(x)); messages.reverse();
renderer.setState( renderer.setState(
id, id,
{ {
type: "RENDER", type: "RENDER",
messages, messages,
atTop: data.length < 50, atTop: messages.length < 50,
atBottom: true, atBottom: true,
}, },
{ type: "ScrollToBottom", smooth }, { type: "ScrollToBottom", smooth },
...@@ -45,12 +44,12 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -45,12 +44,12 @@ export const SimpleRenderer: RendererRoutines = {
} }
}, },
receive: async (renderer, message) => { receive: async (renderer, message) => {
if (message.channel !== renderer.channel) return; if (message.channel_id !== renderer.channel) return;
if (renderer.state.type !== "RENDER") return; if (renderer.state.type !== "RENDER") return;
if (renderer.state.messages.find((x) => x._id === message._id)) return; if (renderer.state.messages.find((x) => x._id === message._id)) return;
if (!renderer.state.atBottom) return; if (!renderer.state.atBottom) return;
let messages = [...renderer.state.messages, mapMessage(message)]; let messages = [...renderer.state.messages, message];
let atTop = renderer.state.atTop; let atTop = renderer.state.atTop;
if (messages.length > 150) { if (messages.length > 150) {
messages = messages.slice(messages.length - 150); messages = messages.slice(messages.length - 150);
...@@ -58,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -58,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = {
} }
renderer.setState( renderer.setState(
message.channel, message.channel_id,
{ {
...renderer.state, ...renderer.state,
messages, messages,
...@@ -67,35 +66,14 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -67,35 +66,14 @@ export const SimpleRenderer: RendererRoutines = {
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE }, { type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
); );
}, },
edit: async (renderer, id, patch) => { edit: noopAsync,
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== "RENDER") return;
let messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id);
if (index > -1) {
let message = { ...messages[index], ...mapMessage(patch) };
messages.splice(index, 1, message);
renderer.setState(
channel,
{
...renderer.state,
messages,
},
{ type: "StayAtBottom" },
);
}
},
delete: async (renderer, id) => { delete: async (renderer, id) => {
const channel = renderer.channel; const channel = renderer.channel;
if (!channel) return; if (!channel) return;
if (renderer.state.type !== "RENDER") return; if (renderer.state.type !== "RENDER") return;
let messages = [...renderer.state.messages]; const messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id); const index = messages.findIndex((x) => x._id === id);
if (index > -1) { if (index > -1) {
messages.splice(index, 1); messages.splice(index, 1);
...@@ -118,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -118,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return; if (state.type !== "RENDER") return;
if (state.atTop) return; if (state.atTop) return;
const { messages: data } = const { messages: data } = await renderer
await renderer.client!.channels.fetchMessagesWithUsers( .client!.channels.get(channel)!
channel, .fetchMessagesWithUsers({
{ before: state.messages[0]._id,
before: state.messages[0]._id, });
},
true,
);
if (data.length === 0) { if (data.length === 0) {
return renderer.setState(channel, { return renderer.setState(channel, {
...@@ -135,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -135,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = {
} }
data.reverse(); data.reverse();
let messages = [...data.map((x) => mapMessage(x)), ...state.messages]; let messages = [...data, ...state.messages];
let atTop = false; let atTop = false;
if (data.length < 50) { if (data.length < 50) {
...@@ -162,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -162,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return; if (state.type !== "RENDER") return;
if (state.atBottom) return; if (state.atBottom) return;
const { messages: data } = const { messages: data } = await renderer
await renderer.client!.channels.fetchMessagesWithUsers( .client!.channels.get(channel)!
channel, .fetchMessagesWithUsers({
{ after: state.messages[state.messages.length - 1]._id,
after: state.messages[state.messages.length - 1]._id, sort: "Oldest",
sort: "Oldest", });
},
true,
);
if (data.length === 0) { if (data.length === 0) {
return renderer.setState(channel, { return renderer.setState(channel, {
...@@ -179,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -179,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = {
}); });
} }
let messages = [...state.messages, ...data.map((x) => mapMessage(x))]; let messages = [...state.messages, ...data];
let atBottom = false; let atBottom = false;
if (data.length < 50) { if (data.length < 50) {
......
import { Message } from "revolt.js"; import { Message } from "revolt.js/dist/maps/Messages";
import { MessageObject } from "../../context/revoltjs/util";
import { SingletonRenderer } from "./Singleton"; import { SingletonRenderer } from "./Singleton";
...@@ -8,7 +6,7 @@ export type ScrollState = ...@@ -8,7 +6,7 @@ export type ScrollState =
| { type: "Free" } | { type: "Free" }
| { type: "Bottom"; scrollingUntil?: number } | { type: "Bottom"; scrollingUntil?: number }
| { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean } | { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean }
| { type: "ScrollToView", id: string } | { type: "ScrollToView"; id: string }
| { type: "OffsetTop"; previousHeight: number } | { type: "OffsetTop"; previousHeight: number }
| { type: "ScrollTop"; y: number }; | { type: "ScrollTop"; y: number };
...@@ -20,7 +18,7 @@ export type RenderState = ...@@ -20,7 +18,7 @@ export type RenderState =
type: "RENDER"; type: "RENDER";
atTop: boolean; atTop: boolean;
atBottom: boolean; atBottom: boolean;
messages: MessageObject[]; messages: Message[];
}; };
export interface RendererRoutines { export interface RendererRoutines {
......
export const stopPropagation = ( export const stopPropagation = (
ev: JSX.TargetedMouseEvent<HTMLDivElement>, ev: JSX.TargetedMouseEvent<HTMLElement>,
_consume?: any, // eslint-disable-next-line
_consume?: unknown,
) => { ) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
......
...@@ -20,6 +20,7 @@ interface SignalingEvents { ...@@ -20,6 +20,7 @@ interface SignalingEvents {
open: (event: Event) => void; open: (event: Event) => void;
close: (event: CloseEvent) => void; close: (event: CloseEvent) => void;
error: (event: Event) => void; error: (event: Event) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: (data: any) => void; data: (data: any) => void;
} }
...@@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
entry(json); entry(json);
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
sendRequest(type: string, data?: any): Promise<any> { sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN) if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected }); return Promise.reject({ error: WSErrorCode.NotConnected });
...@@ -117,13 +119,14 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -117,13 +119,14 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
this.once("close", onClose); this.once("close", onClose);
const json = { const json = {
id: this.index, id: this.index,
type: type, type,
data, data,
}; };
ws.send(JSON.stringify(json) + "\n"); ws.send(`${JSON.stringify(json)}\n`);
this.index++; this.index++;
}); });
} }
/* eslint-enable @typescript-eslint/no-explicit-any */
authenticate(token: string, roomId: string): Promise<AuthenticationResult> { authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId }); return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
...@@ -161,7 +164,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -161,7 +164,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
type: ProduceType, type: ProduceType,
rtpParameters: RtpParameters, rtpParameters: RtpParameters,
): Promise<string> { ): Promise<string> {
let result = await this.sendRequest(WSCommandType.StartProduce, { const result = await this.sendRequest(WSCommandType.StartProduce, {
type, type,
rtpParameters, rtpParameters,
}); });
......
...@@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.signaling.on( this.signaling.on(
"error", "error",
(error) => { () => {
this.emit("error", new Error("Signaling error")); this.emit("error", new Error("Signaling error"));
}, },
this, this,
...@@ -172,7 +172,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -172,7 +172,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
if (this.device === undefined || this.roomId === undefined) if (this.device === undefined || this.roomId === undefined)
throw new ReferenceError("Voice Client is in an invalid state"); throw new ReferenceError("Voice Client is in an invalid state");
const result = await this.signaling.authenticate(token, this.roomId); const result = await this.signaling.authenticate(token, this.roomId);
let [room] = await Promise.all([ const [room] = await Promise.all([
this.signaling.roomInfo(), this.signaling.roomInfo(),
this.device.load({ routerRtpCapabilities: result.rtpCapabilities }), this.device.load({ routerRtpCapabilities: result.rtpCapabilities }),
]); ]);
...@@ -229,7 +229,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -229,7 +229,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
}); });
this.emit("ready"); this.emit("ready");
for (let user of this.participants) { for (const user of this.participants) {
if (user[1].audio && user[0] !== this.userId) if (user[1].audio && user[0] !== this.userId)
this.startConsume(user[0], "audio"); this.startConsume(user[0], "audio");
} }
...@@ -323,7 +323,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -323,7 +323,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
await this.signaling.stopProduce(type); await this.signaling.stopProduce(type);
} catch (error) { } catch (error) {
if (error.error === WSErrorCode.ProducerNotFound) return; if (error.error === WSErrorCode.ProducerNotFound) return;
else throw error; throw error;
} }
} }
} }
/* eslint-disable react-hooks/rules-of-hooks */
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -9,11 +10,6 @@ import { ...@@ -9,11 +10,6 @@ import {
ClientStatus, ClientStatus,
StatusContext, StatusContext,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUser,
} from "../context/revoltjs/hooks";
import Header from "../components/ui/Header"; import Header from "../components/ui/Header";
...@@ -32,39 +28,39 @@ export default function Open() { ...@@ -32,39 +28,39 @@ export default function Open() {
); );
} }
const ctx = useForceUpdate();
const channels = useChannels(undefined, ctx);
const user = useUser(id, ctx);
useEffect(() => { useEffect(() => {
if (id === "saved") { if (id === "saved") {
for (const channel of channels) { for (const channel of [...client.channels.values()]) {
if (channel?.channel_type === "SavedMessages") { if (channel?.channel_type === "SavedMessages") {
history.push(`/channel/${channel._id}`); history.push(`/channel/${channel._id}`);
return; return;
} }
} }
client.users client
.openDM(client.user?._id as string) .user!.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`)) .then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error })); .catch((error) => openScreen({ id: "error", error }));
return; return;
} }
const user = client.users.get(id);
if (user) { if (user) {
const channel: string | undefined = channels.find( const channel: string | undefined = [
...client.channels.values(),
].find(
(channel) => (channel) =>
channel?.channel_type === "DirectMessage" && channel?.channel_type === "DirectMessage" &&
channel.recipients.includes(id), channel.recipient_ids!.includes(id),
)?._id; )?._id;
if (channel) { if (channel) {
history.push(`/channel/${channel}`); history.push(`/channel/${channel}`);
} else { } else {
client.users client.users
.openDM(id) .get(id)
?.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`)) .then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error })); .catch((error) => openScreen({ id: "error", error }));
} }
...@@ -73,7 +69,7 @@ export default function Open() { ...@@ -73,7 +69,7 @@ export default function Open() {
} }
history.push("/"); history.push("/");
}, []); });
return ( return (
<Header placement="primary"> <Header placement="primary">
......
...@@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications"; ...@@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications";
import StateMonitor from "../context/revoltjs/StateMonitor"; import StateMonitor from "../context/revoltjs/StateMonitor";
import SyncManager from "../context/revoltjs/SyncManager"; import SyncManager from "../context/revoltjs/SyncManager";
import { Titlebar } from "../components/native/Titlebar";
import BottomNavigation from "../components/navigation/BottomNavigation"; import BottomNavigation from "../components/navigation/BottomNavigation";
import LeftSidebar from "../components/navigation/LeftSidebar"; import LeftSidebar from "../components/navigation/LeftSidebar";
import RightSidebar from "../components/navigation/RightSidebar"; import RightSidebar from "../components/navigation/RightSidebar";
...@@ -35,91 +36,99 @@ export default function App() { ...@@ -35,91 +36,99 @@ export default function App() {
const path = useLocation().pathname; const path = useLocation().pathname;
const fixedBottomNav = const fixedBottomNav =
path === "/" || path === "/settings" || path.startsWith("/friends"); path === "/" || path === "/settings" || path.startsWith("/friends");
const inSettings = path.includes("/settings");
const inChannel = path.includes("/channel"); const inChannel = path.includes("/channel");
const inSpecial = const inSpecial =
(path.startsWith("/friends") && isTouchscreenDevice) || (path.startsWith("/friends") && isTouchscreenDevice) ||
path.startsWith("/invite") || path.startsWith("/invite") ||
path.startsWith("/settings"); path.includes("/settings");
return ( return (
<OverlappingPanels <>
width="100vw" {window.isNative && !window.native.getConfig().frame && (
height="var(--app-height)" <Titlebar />
leftPanel={ )}
inSpecial <OverlappingPanels
? undefined width="100vw"
: { width: 292, component: <LeftSidebar /> } height={
} window.isNative && !window.native.getConfig().frame
rightPanel={ ? "calc(var(--app-height) - var(--titlebar-height))"
!inSettings && inChannel : "var(--app-height)"
? { width: 240, component: <RightSidebar /> } }
: undefined leftPanel={
} inSpecial
bottomNav={{ ? undefined
component: <BottomNavigation />, : { width: 292, component: <LeftSidebar /> }
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left, }
height: 50, rightPanel={
}} !inSpecial && inChannel
docked={isTouchscreenDevice ? Docked.None : Docked.Left}> ? { width: 240, component: <RightSidebar /> }
<Routes> : undefined
<Switch> }
<Route bottomNav={{
path="/server/:server/channel/:channel/settings/:page" component: <BottomNavigation />,
component={ChannelSettings} showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
/> height: 50,
<Route }}
path="/server/:server/channel/:channel/settings" docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
component={ChannelSettings} <Routes>
/> <Switch>
<Route <Route
path="/server/:server/settings/:page" path="/server/:server/channel/:channel/settings/:page"
component={ServerSettings} component={ChannelSettings}
/> />
<Route <Route
path="/server/:server/settings" path="/server/:server/channel/:channel/settings"
component={ServerSettings} component={ChannelSettings}
/> />
<Route <Route
path="/channel/:channel/settings/:page" path="/server/:server/settings/:page"
component={ChannelSettings} component={ServerSettings}
/> />
<Route <Route
path="/channel/:channel/settings" path="/server/:server/settings"
component={ChannelSettings} component={ServerSettings}
/> />
<Route
path="/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/channel/:channel/settings"
component={ChannelSettings}
/>
<Route <Route
path="/channel/:channel/:message" path="/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route <Route
path="/server/:server/channel/:channel/:message" path="/server/:server/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route <Route
path="/server/:server/channel/:channel" path="/server/:server/channel/:channel"
component={Channel} component={Channel}
/> />
<Route path="/server/:server" /> <Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} /> <Route path="/channel/:channel" component={Channel} />
<Route path="/settings/:page" component={Settings} /> <Route path="/settings/:page" component={Settings} />
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
<Route path="/dev" component={Developer} /> <Route path="/dev" component={Developer} />
<Route path="/friends" component={Friends} /> <Route path="/friends" component={Friends} />
<Route path="/open/:id" component={Open} /> <Route path="/open/:id" component={Open} />
<Route path="/invite/:code" component={Invite} /> <Route path="/invite/:code" component={Invite} />
<Route path="/" component={Home} /> <Route path="/" component={Home} />
</Switch> </Switch>
</Routes> </Routes>
<ContextMenus /> <ContextMenus />
<Popovers /> <Popovers />
<Notifications /> <Notifications />
<StateMonitor /> <StateMonitor />
<SyncManager /> <SyncManager />
</OverlappingPanels> </OverlappingPanels>
</>
); );
} }
...@@ -16,7 +16,7 @@ export function App() { ...@@ -16,7 +16,7 @@ export function App() {
<Context> <Context>
<Masks /> <Masks />
{/* {/*
// @ts-expect-error */} // @ts-expect-error typings mis-match between preact... and preact? */}
<Suspense fallback={<Preloader type="spinner" />}> <Suspense fallback={<Preloader type="spinner" />}>
<Switch> <Switch>
<Route path="/login"> <Route path="/login">
......