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 412 additions and 403 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;
......@@ -12,21 +12,19 @@ import {
} from "@styled-icons/boxicons-regular";
import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom";
import {
Attachment,
Channels,
Message,
Servers,
Users,
} from "revolt.js/dist/api/objects";
import { Attachment } from "revolt-api/types/Autumn";
import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import {
ChannelPermission,
ServerPermission,
UserPermission,
} 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 {
ContextMenu,
ContextMenuWithData,
MenuItem,
openContextMenu,
......@@ -43,21 +41,12 @@ import {
} from "../redux/reducers/notifications";
import { QueuedMessage } from "../redux/reducers/queue";
import { useIntermediate } from "../context/intermediate/Intermediate";
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
import {
AppContext,
ClientStatus,
StatusContext,
} from "../context/revoltjs/RevoltClient";
import {
useChannel,
useChannelPermission,
useForceUpdate,
useServer,
useServerPermission,
useUser,
useUserPermission,
} from "../context/revoltjs/hooks";
import { takeError } from "../context/revoltjs/util";
import Tooltip from "../components/common/Tooltip";
......@@ -84,49 +73,46 @@ type Action =
| { action: "copy_id"; id: string }
| { action: "copy_selection" }
| { action: "copy_text"; content: string }
| { action: "mark_as_read"; channel: Channels.Channel }
| { action: "mark_as_read"; channel: Channel }
| { action: "retry_message"; message: QueuedMessage }
| { action: "cancel_message"; message: QueuedMessage }
| { action: "mention"; user: string }
| { action: "reply_message"; id: string }
| { action: "quote_message"; content: string }
| { action: "edit_message"; id: string }
| { action: "delete_message"; target: Channels.Message }
| { action: "delete_message"; target: Message }
| { action: "open_file"; attachment: Attachment }
| { action: "save_file"; attachment: Attachment }
| { action: "copy_file_link"; attachment: Attachment }
| { action: "open_link"; link: string }
| { action: "copy_link"; link: string }
| { action: "remove_member"; channel: string; user: string }
| { action: "kick_member"; target: Servers.Server; user: string }
| { action: "ban_member"; target: Servers.Server; user: string }
| { action: "view_profile"; user: string }
| { action: "message_user"; user: string }
| { action: "block_user"; user: Users.User }
| { action: "unblock_user"; user: Users.User }
| { action: "add_friend"; user: Users.User }
| { action: "remove_friend"; user: Users.User }
| { action: "cancel_friend"; user: Users.User }
| { action: "set_presence"; presence: Users.Presence }
| { action: "remove_member"; channel: Channel; user: User }
| { action: "kick_member"; target: Server; user: User }
| { action: "ban_member"; target: Server; user: User }
| { action: "view_profile"; user: User }
| { action: "message_user"; user: User }
| { action: "block_user"; user: User }
| { action: "unblock_user"; user: User }
| { action: "add_friend"; user: User }
| { action: "remove_friend"; user: User }
| { action: "cancel_friend"; user: User }
| { action: "set_presence"; presence: Presence }
| { action: "set_status" }
| { action: "clear_status" }
| { action: "create_channel"; target: Servers.Server }
| { action: "create_channel"; target: Server }
| {
action: "create_invite";
target:
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
target: Channel;
}
| { action: "leave_group"; target: Channels.GroupChannel }
| { action: "leave_group"; target: Channel }
| {
action: "delete_channel";
target: Channels.TextChannel | Channels.VoiceChannel;
target: Channel;
}
| { action: "close_dm"; target: Channels.DirectMessageChannel }
| { action: "leave_server"; target: Servers.Server }
| { action: "delete_server"; target: Servers.Server }
| { action: "open_notification_options"; channel: Channels.Channel }
| { action: "close_dm"; target: Channel }
| { action: "leave_server"; target: Server }
| { action: "delete_server"; target: Server }
| { action: "open_notification_options"; channel: Channel }
| { action: "open_settings" }
| { action: "open_channel_settings"; id: string }
| { action: "open_server_settings"; id: string }
......@@ -141,6 +127,8 @@ type Props = {
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) {
const { openScreen, writeClipboard } = useIntermediate();
const client = useContext(AppContext);
......@@ -169,9 +157,10 @@ function ContextMenus(props: Props) {
return;
const message =
data.channel.channel_type === "TextChannel"
typeof data.channel.last_message === "string"
? data.channel.last_message
: data.channel.last_message._id;
: data.channel.last_message!._id;
dispatch({
type: "UNREADS_MARK_READ",
channel: data.channel._id,
......@@ -188,7 +177,7 @@ function ContextMenus(props: Props) {
case "retry_message":
{
const nonce = data.message.id;
const fail = (error: any) =>
const fail = (error: string) =>
dispatch({
type: "QUEUE_FAIL",
nonce,
......@@ -196,7 +185,8 @@ function ContextMenus(props: Props) {
});
client.channels
.sendMessage(data.message.channel, {
.get(data.message.channel)!
.sendMessage({
nonce: data.message.id,
content: data.message.data.content as string,
replies: data.message.data.replies,
......@@ -313,17 +303,17 @@ function ContextMenus(props: Props) {
case "remove_member":
{
client.channels.removeMember(data.channel, data.user);
data.channel.removeMember(data.user._id);
}
break;
case "view_profile":
openScreen({ id: "profile", user_id: data.user });
openScreen({ id: "profile", user_id: data.user._id });
break;
case "message_user":
{
const channel = await client.users.openDM(data.user);
const channel = await data.user.openDM();
if (channel) {
history.push(`/channel/${channel._id}`);
}
......@@ -332,7 +322,7 @@ function ContextMenus(props: Props) {
case "add_friend":
{
await client.users.addFriend(data.user.username);
await data.user.addFriend();
}
break;
......@@ -344,7 +334,7 @@ function ContextMenus(props: Props) {
});
break;
case "unblock_user":
await client.users.unblockUser(data.user._id);
await data.user.unblockUser();
break;
case "remove_friend":
openScreen({
......@@ -354,12 +344,12 @@ function ContextMenus(props: Props) {
});
break;
case "cancel_friend":
await client.users.removeFriend(data.user._id);
await data.user.removeFriend();
break;
case "set_presence":
{
await client.users.editUser({
await client.users.edit({
status: {
...client.user?.status,
presence: data.presence,
......@@ -377,8 +367,9 @@ function ContextMenus(props: Props) {
case "clear_status":
{
const { text, ...status } = client.user?.status ?? {};
await client.users.editUser({ status });
const { text: _text, ...status } =
client.user?.status ?? {};
await client.users.edit({ status });
}
break;
......@@ -390,12 +381,12 @@ function ContextMenus(props: Props) {
case "delete_message":
case "create_channel":
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({
id: "special_prompt",
type: data.action,
target: data.target as any,
});
target: data.target,
} as unknown as Screen);
break;
case "ban_member":
......@@ -458,7 +449,6 @@ function ContextMenus(props: Props) {
unread,
contextualChannel: cxid,
}: ContextMenuData) => {
const forceUpdate = useForceUpdate();
const elements: Children[] = [];
let lastDivider = false;
......@@ -488,11 +478,8 @@ function ContextMenus(props: Props) {
}
if (server_list) {
const server = useServer(server_list, forceUpdate);
const permissions = useServerPermission(
server_list,
forceUpdate,
);
const server = client.servers.get(server_list)!;
const permissions = server.permission;
if (server) {
if (permissions & ServerPermission.ManageChannels)
generateAction({
......@@ -519,33 +506,31 @@ function ContextMenus(props: Props) {
pushDivider();
}
const channel = useChannel(cid, forceUpdate);
const contextualChannel = useChannel(cxid, forceUpdate);
const channel = cid ? client.channels.get(cid) : undefined;
const contextualChannel = cxid
? client.channels.get(cxid)
: undefined;
const targetChannel = channel ?? contextualChannel;
const user = useUser(uid, forceUpdate);
const user = uid ? client.users.get(uid) : undefined;
const serverChannel =
targetChannel &&
(targetChannel.channel_type === "TextChannel" ||
targetChannel.channel_type === "VoiceChannel")
? targetChannel
: undefined;
const server = useServer(
serverChannel ? serverChannel.server : sid,
forceUpdate,
);
const channelPermissions = targetChannel
? useChannelPermission(targetChannel._id, forceUpdate)
: 0;
const serverPermissions = server
? useServerPermission(server._id, forceUpdate)
: serverChannel
? useServerPermission(serverChannel.server, forceUpdate)
: 0;
const userPermissions = user
? useUserPermission(user._id, forceUpdate)
: 0;
const s = serverChannel ? serverChannel.server_id! : sid;
const server = s ? client.servers.get(s) : undefined;
const channelPermissions = targetChannel?.permission || 0;
const serverPermissions =
(server
? server.permission
: serverChannel
? serverChannel.server?.permission
: 0) || 0;
const userPermissions = (user ? user.permission : 0) || 0;
if (channel && unread) {
generateAction({ action: "mark_as_read", channel });
......@@ -565,29 +550,29 @@ function ContextMenus(props: Props) {
if (user) {
let actions: Action["action"][];
switch (user.relationship) {
case Users.Relationship.User:
case RelationshipStatus.User:
actions = [];
break;
case Users.Relationship.Friend:
case RelationshipStatus.Friend:
actions = ["remove_friend", "block_user"];
break;
case Users.Relationship.Incoming:
case RelationshipStatus.Incoming:
actions = [
"add_friend",
"cancel_friend",
"block_user",
];
break;
case Users.Relationship.Outgoing:
case RelationshipStatus.Outgoing:
actions = ["cancel_friend", "block_user"];
break;
case Users.Relationship.Blocked:
case RelationshipStatus.Blocked:
actions = ["unblock_user"];
break;
case Users.Relationship.BlockedOther:
case RelationshipStatus.BlockedOther:
actions = ["block_user"];
break;
case Users.Relationship.None:
case RelationshipStatus.None:
default:
actions = ["add_friend", "block_user"];
}
......@@ -595,7 +580,7 @@ function ContextMenus(props: Props) {
if (userPermissions & UserPermission.ViewProfile) {
generateAction({
action: "view_profile",
user: user._id,
user,
});
}
......@@ -605,26 +590,29 @@ function ContextMenus(props: Props) {
) {
generateAction({
action: "message_user",
user: user._id,
user,
});
}
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
generateAction({ action: actions[i] as any, user });
// Typescript can't determine that user the actions are linked together correctly
generateAction({
action: actions[i],
user,
} as unknown as Action);
}
}
if (contextualChannel) {
if (contextualChannel.channel_type === "Group" && uid) {
if (
contextualChannel.owner === userId &&
contextualChannel.owner_id === userId &&
userId !== uid
) {
generateAction({
action: "remove_member",
channel: contextualChannel._id,
user: uid,
channel: contextualChannel,
user: user!,
});
}
}
......@@ -641,14 +629,14 @@ function ContextMenus(props: Props) {
generateAction({
action: "kick_member",
target: server,
user: uid,
user: user!,
});
if (serverPermissions & ServerPermission.BanMembers)
generateAction({
action: "ban_member",
target: server,
user: uid,
user: user!,
});
}
}
......@@ -686,7 +674,7 @@ function ContextMenus(props: Props) {
});
}
if (message.author === userId) {
if (message.author_id === userId) {
generateAction({
action: "edit_message",
id: message._id,
......@@ -694,7 +682,7 @@ function ContextMenus(props: Props) {
}
if (
message.author === userId ||
message.author_id === userId ||
channelPermissions &
ChannelPermission.ManageMessages
) {
......@@ -796,11 +784,15 @@ function ContextMenus(props: Props) {
break;
case "TextChannel":
case "VoiceChannel":
// ! FIXME: add permission for invites
generateAction({
action: "create_invite",
target: channel,
});
if (
channelPermissions &
ChannelPermission.InviteOthers
) {
generateAction({
action: "create_invite",
target: channel,
});
}
if (
serverPermissions &
......@@ -809,7 +801,7 @@ function ContextMenus(props: Props) {
generateAction(
{
action: "open_server_channel_settings",
server: channel.server,
server: channel.server_id!,
id: channel._id,
},
"open_channel_settings",
......@@ -873,94 +865,103 @@ function ContextMenus(props: Props) {
id="Status"
onClose={contextClick}
className="Status">
{() => (
<>
<div className="header">
<div className="main">
<div
className="username"
onClick={() =>
writeClipboard(client.user!.username)
}>
<Tooltip
content={
<Text id="app.special.copy_username" />
{() => {
const user = client.user!;
return (
<>
<div className="header">
<div className="main">
<div
className="username"
onClick={() =>
writeClipboard(
client.user!.username,
)
}>
@{client.user!.username}
</Tooltip>
</div>
<div
className="status"
onClick={() =>
contextClick({ action: "set_status" })
}>
<UserStatus user={client.user!} />
<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>
<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>
<MenuItem data={{ action: "clear_status" }}>
<Trash size={18} />
<MenuItem
data={{ action: "open_settings" }}>
<Cog size={22} />
</MenuItem>
</IconButton>
)}
</MenuItem>
</>
)}
</div>
<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
id="NotificationOptions"
onClose={contextClick}>
{({ channel }: { channel: Channels.Channel }) => {
{({ channel }: { channel: Channel }) => {
const state = props.notifications[channel._id];
const actual = getNotificationState(
props.notifications,
......@@ -969,6 +970,7 @@ function ContextMenus(props: Props) {
const elements: Children[] = [
<MenuItem
key="notif"
data={{
action: "set_notification_state",
key: channel._id,
......@@ -988,6 +990,7 @@ function ContextMenus(props: Props) {
function generate(key: string, icon: Children) {
elements.push(
<MenuItem
key={key}
data={{
action: "set_notification_state",
key: channel._id,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {};
......
......@@ -10,7 +10,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" | "onChange"
"style" | "value" | "onChange" | "children" | "as"
> &
TextAreaProps & {
forceFocus?: boolean;
......@@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
lineHeight,
hideBorder,
forceFocus,
children,
as,
onChange,
...textAreaProps
} = props;
......@@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
useEffect(() => {
if (isTouchscreenDevice) return;
autoFocus && ref.current && ref.current.focus();
}, [value]);
}, [value, autoFocus]);
const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
......@@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
}, [ref, autoFocus, forceFocus, value]);
useEffect(() => {
if (!ref.current) return;
......@@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
}
}
return internalSubscribe("TextArea", "focus", focus);
}, [ref]);
return internalSubscribe(
"TextArea",
"focus",
focus as (...args: unknown[]) => void,
);
}, [props.id, ref]);
return (
<Container>
......
export function urlBase64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/-/g, "+")
.replace(/_/g, "/");
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.
let timer: NodeJS.Timeout;
// This function is given to React.
return (...args: any[]) => {
return (...args: unknown[]) => {
// Get rid of the old timer.
clearTimeout(timer);
// Set a new timer.
......
......@@ -5,13 +5,13 @@ export const InternalEvent = new EventEmitter();
export function internalSubscribe(
ns: string,
event: string,
fn: (...args: any[]) => void,
fn: (...args: unknown[]) => void,
) {
InternalEvent.addListener(`${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);
}
......
import { IntlContext, translate } from "preact-i18n";
import { useContext } from "preact/hooks";
import { Dictionary } from "../context/Locale";
import { Children } from "../types/Preact";
interface Fields {
......@@ -12,18 +14,6 @@ interface Props {
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 {
intl: {
dictionary: Dictionary;
......@@ -57,7 +47,7 @@ export function TextReact({ id, fields }: Props) {
const path = id.split(".");
let entry = intl.dictionary[path.shift()!];
for (const key of path) {
// @ts-expect-error
// @ts-expect-error TODO: lazy
entry = entry[key];
}
......@@ -66,8 +56,12 @@ export function TextReact({ id, fields }: Props) {
export function useTranslation() {
const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) =>
translate(id, "", intl.dictionary, fields, plural, fallback);
return (
id: string,
fields?: Record<string, string | undefined>,
plural?: number,
fallback?: string,
) => translate(id, "", intl.dictionary, fields, plural, fallback);
}
export function useDictionary() {
......
/* 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 { Client, Message } from "revolt.js";
import { Client } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { useEffect, useState } from "preact/hooks";
......@@ -122,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
10,
);
}
}
......@@ -166,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
10,
);
}
}
......
import { mapMessage } from "../../../context/revoltjs/util";
import { noopAsync } from "../../js";
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
import { RendererRoutines } from "../types";
......@@ -8,14 +7,10 @@ export const SimpleRenderer: RendererRoutines = {
if (renderer.client!.websocket.connected) {
if (nearby)
renderer
.client!.channels.fetchMessagesWithUsers(
id,
{ nearby, limit: 100 },
true,
)
.then(({ messages: data }) => {
data.sort((a, b) => a._id.localeCompare(b._id));
const messages = data.map((x) => mapMessage(x));
.client!.channels.get(id)!
.fetchMessagesWithUsers({ nearby, limit: 100 })
.then(({ messages }) => {
messages.sort((a, b) => a._id.localeCompare(b._id));
renderer.setState(
id,
{
......@@ -29,16 +24,16 @@ export const SimpleRenderer: RendererRoutines = {
});
else
renderer
.client!.channels.fetchMessagesWithUsers(id, {}, true)
.then(({ messages: data }) => {
data.reverse();
const messages = data.map((x) => mapMessage(x));
.client!.channels.get(id)!
.fetchMessagesWithUsers({})
.then(({ messages }) => {
messages.reverse();
renderer.setState(
id,
{
type: "RENDER",
messages,
atTop: data.length < 50,
atTop: messages.length < 50,
atBottom: true,
},
{ type: "ScrollToBottom", smooth },
......@@ -49,12 +44,12 @@ export const SimpleRenderer: RendererRoutines = {
}
},
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.messages.find((x) => x._id === message._id)) return;
if (!renderer.state.atBottom) return;
let messages = [...renderer.state.messages, mapMessage(message)];
let messages = [...renderer.state.messages, message];
let atTop = renderer.state.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
......@@ -62,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = {
}
renderer.setState(
message.channel,
message.channel_id,
{
...renderer.state,
messages,
......@@ -71,28 +66,7 @@ export const SimpleRenderer: RendererRoutines = {
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
);
},
edit: async (renderer, id, patch) => {
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== "RENDER") return;
const messages = [...renderer.state.messages];
const index = messages.findIndex((x) => x._id === id);
if (index > -1) {
const message = { ...messages[index], ...mapMessage(patch) };
messages.splice(index, 1, message);
renderer.setState(
channel,
{
...renderer.state,
messages,
},
{ type: "StayAtBottom" },
);
}
},
edit: noopAsync,
delete: async (renderer, id) => {
const channel = renderer.channel;
if (!channel) return;
......@@ -122,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return;
if (state.atTop) return;
const { messages: data } =
await renderer.client!.channels.fetchMessagesWithUsers(
channel,
{
before: state.messages[0]._id,
},
true,
);
const { messages: data } = await renderer
.client!.channels.get(channel)!
.fetchMessagesWithUsers({
before: state.messages[0]._id,
});
if (data.length === 0) {
return renderer.setState(channel, {
......@@ -139,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = {
}
data.reverse();
let messages = [...data.map((x) => mapMessage(x)), ...state.messages];
let messages = [...data, ...state.messages];
let atTop = false;
if (data.length < 50) {
......@@ -166,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return;
if (state.atBottom) return;
const { messages: data } =
await renderer.client!.channels.fetchMessagesWithUsers(
channel,
{
after: state.messages[state.messages.length - 1]._id,
sort: "Oldest",
},
true,
);
const { messages: data } = await renderer
.client!.channels.get(channel)!
.fetchMessagesWithUsers({
after: state.messages[state.messages.length - 1]._id,
sort: "Oldest",
});
if (data.length === 0) {
return renderer.setState(channel, {
......@@ -183,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = {
});
}
let messages = [...state.messages, ...data.map((x) => mapMessage(x))];
let messages = [...state.messages, ...data];
let atBottom = false;
if (data.length < 50) {
......
import { Message } from "revolt.js";
import { MessageObject } from "../../context/revoltjs/util";
import { Message } from "revolt.js/dist/maps/Messages";
import { SingletonRenderer } from "./Singleton";
......@@ -20,7 +18,7 @@ export type RenderState =
type: "RENDER";
atTop: boolean;
atBottom: boolean;
messages: MessageObject[];
messages: Message[];
};
export interface RendererRoutines {
......
export const stopPropagation = (
ev: JSX.TargetedMouseEvent<HTMLDivElement>,
_consume?: any,
ev: JSX.TargetedMouseEvent<HTMLElement>,
// eslint-disable-next-line
_consume?: unknown,
) => {
ev.preventDefault();
ev.stopPropagation();
......
......@@ -20,6 +20,7 @@ interface SignalingEvents {
open: (event: Event) => void;
close: (event: CloseEvent) => void;
error: (event: Event) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: (data: any) => void;
}
......@@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
entry(json);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected });
......@@ -124,6 +126,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
this.index++;
});
}
/* eslint-enable @typescript-eslint/no-explicit-any */
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
......
......@@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.signaling.on(
"error",
(error) => {
() => {
this.emit("error", new Error("Signaling error"));
},
this,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n";
......@@ -9,11 +10,6 @@ import {
ClientStatus,
StatusContext,
} from "../context/revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUser,
} from "../context/revoltjs/hooks";
import Header from "../components/ui/Header";
......@@ -32,39 +28,39 @@ export default function Open() {
);
}
const ctx = useForceUpdate();
const channels = useChannels(undefined, ctx);
const user = useUser(id, ctx);
useEffect(() => {
if (id === "saved") {
for (const channel of channels) {
for (const channel of [...client.channels.values()]) {
if (channel?.channel_type === "SavedMessages") {
history.push(`/channel/${channel._id}`);
return;
}
}
client.users
.openDM(client.user?._id as string)
client
.user!.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error }));
return;
}
const user = client.users.get(id);
if (user) {
const channel: string | undefined = channels.find(
const channel: string | undefined = [
...client.channels.values(),
].find(
(channel) =>
channel?.channel_type === "DirectMessage" &&
channel.recipients.includes(id),
channel.recipient_ids!.includes(id),
)?._id;
if (channel) {
history.push(`/channel/${channel}`);
} else {
client.users
.openDM(id)
.get(id)
?.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error }));
}
......@@ -73,7 +69,7 @@ export default function Open() {
}
history.push("/");
}, []);
});
return (
<Header placement="primary">
......
......@@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications";
import StateMonitor from "../context/revoltjs/StateMonitor";
import SyncManager from "../context/revoltjs/SyncManager";
import { Titlebar } from "../components/native/Titlebar";
import BottomNavigation from "../components/navigation/BottomNavigation";
import LeftSidebar from "../components/navigation/LeftSidebar";
import RightSidebar from "../components/navigation/RightSidebar";
......@@ -42,83 +43,92 @@ export default function App() {
path.includes("/settings");
return (
<OverlappingPanels
width="100vw"
height="var(--app-height)"
leftPanel={
inSpecial
? undefined
: { width: 292, component: <LeftSidebar /> }
}
rightPanel={
!inSpecial && inChannel
? { width: 240, component: <RightSidebar /> }
: undefined
}
bottomNav={{
component: <BottomNavigation />,
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
height: 50,
}}
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
<Routes>
<Switch>
<Route
path="/server/:server/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/server/:server/channel/:channel/settings"
component={ChannelSettings}
/>
<Route
path="/server/:server/settings/:page"
component={ServerSettings}
/>
<Route
path="/server/:server/settings"
component={ServerSettings}
/>
<Route
path="/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/channel/:channel/settings"
component={ChannelSettings}
/>
<>
{window.isNative && !window.native.getConfig().frame && (
<Titlebar />
)}
<OverlappingPanels
width="100vw"
height={
window.isNative && !window.native.getConfig().frame
? "calc(var(--app-height) - var(--titlebar-height))"
: "var(--app-height)"
}
leftPanel={
inSpecial
? undefined
: { width: 292, component: <LeftSidebar /> }
}
rightPanel={
!inSpecial && inChannel
? { width: 240, component: <RightSidebar /> }
: undefined
}
bottomNav={{
component: <BottomNavigation />,
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
height: 50,
}}
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
<Routes>
<Switch>
<Route
path="/server/:server/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/server/:server/channel/:channel/settings"
component={ChannelSettings}
/>
<Route
path="/server/:server/settings/:page"
component={ServerSettings}
/>
<Route
path="/server/:server/settings"
component={ServerSettings}
/>
<Route
path="/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/channel/:channel/settings"
component={ChannelSettings}
/>
<Route
path="/channel/:channel/:message"
component={Channel}
/>
<Route
path="/server/:server/channel/:channel/:message"
component={Channel}
/>
<Route
path="/channel/:channel/:message"
component={Channel}
/>
<Route
path="/server/:server/channel/:channel/:message"
component={Channel}
/>
<Route
path="/server/:server/channel/:channel"
component={Channel}
/>
<Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} />
<Route
path="/server/:server/channel/:channel"
component={Channel}
/>
<Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} />
<Route path="/settings/:page" component={Settings} />
<Route path="/settings" component={Settings} />
<Route path="/settings/:page" component={Settings} />
<Route path="/settings" component={Settings} />
<Route path="/dev" component={Developer} />
<Route path="/friends" component={Friends} />
<Route path="/open/:id" component={Open} />
<Route path="/invite/:code" component={Invite} />
<Route path="/" component={Home} />
</Switch>
</Routes>
<ContextMenus />
<Popovers />
<Notifications />
<StateMonitor />
<SyncManager />
</OverlappingPanels>
<Route path="/dev" component={Developer} />
<Route path="/friends" component={Friends} />
<Route path="/open/:id" component={Open} />
<Route path="/invite/:code" component={Invite} />
<Route path="/" component={Home} />
</Switch>
</Routes>
<ContextMenus />
<Popovers />
<Notifications />
<StateMonitor />
<SyncManager />
</OverlappingPanels>
</>
);
}
......@@ -16,7 +16,7 @@ export function App() {
<Context>
<Masks />
{/*
// @ts-expect-error */}
// @ts-expect-error typings mis-match between preact... and preact? */}
<Suspense fallback={<Preloader type="spinner" />}>
<Switch>
<Route path="/login">
......
import { useParams, useHistory } from "react-router-dom";
import { Channels } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom";
import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { useState } from "preact/hooks";
......@@ -8,7 +9,7 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { dispatch, getState } from "../../redux";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import AgeGate from "../../components/common/AgeGate";
import MessageBox from "../../components/common/messaging/MessageBox";
......@@ -36,19 +37,19 @@ const ChannelContent = styled.div`
`;
export function Channel({ id }: { id: string }) {
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
const client = useClient();
const channel = client.channels.get(id);
if (!channel) return null;
if (channel.channel_type === "VoiceChannel") {
return <VoiceChannel channel={channel} />;
}
return <TextChannel channel={channel} />;
}
const MEMBERS_SIDEBAR_KEY = "sidebar_members";
function TextChannel({ channel }: { channel: Channels.Channel }) {
const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const [showMembers, setMembers] = useState(
getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true,
);
......@@ -59,9 +60,11 @@ function TextChannel({ channel }: { channel: Channels.Channel }) {
type="channel"
channel={channel}
gated={
(channel.channel_type === "TextChannel" ||
channel.channel_type === "Group") &&
channel.name.includes("nsfw")
!!(
(channel.channel_type === "TextChannel" ||
channel.channel_type === "Group") &&
channel.name?.includes("nsfw")
)
}>
<ChannelHeader
channel={channel}
......@@ -86,7 +89,7 @@ function TextChannel({ channel }: { channel: Channels.Channel }) {
<ChannelContent>
<VoiceHeader id={id} />
<MessageArea id={id} />
<TypingIndicator id={id} />
<TypingIndicator channel={channel} />
<JumpToBottom id={id} />
<MessageBox channel={channel} />
</ChannelContent>
......@@ -96,9 +99,9 @@ function TextChannel({ channel }: { channel: Channels.Channel }) {
</ChannelMain>
</AgeGate>
);
}
});
function VoiceChannel({ channel }: { channel: Channels.Channel }) {
function VoiceChannel({ channel }: { channel: ChannelI }) {
return (
<>
<ChannelHeader channel={channel} />
......@@ -107,7 +110,7 @@ function VoiceChannel({ channel }: { channel: Channels.Channel }) {
);
}
export default function () {
export default function ChannelComponent() {
const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />;
}
import { At, Hash, Menu } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid";
import { Channel, User } from "revolt.js";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components";
import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util";
import { useStatusColour } from "../../components/common/user/UserIcon";
......@@ -65,23 +64,18 @@ const Info = styled.div`
}
`;
export default function ChannelHeader({
channel,
toggleSidebar,
}: ChannelHeaderProps) {
export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const name = getChannelName(client, channel);
let icon, recipient;
const name = getChannelName(channel);
let icon, recipient: User | undefined;
switch (channel.channel_type) {
case "SavedMessages":
icon = <Notepad size={24} />;
break;
case "DirectMessage":
icon = <At size={24} />;
const uid = client.channels.getRecipient(channel._id);
recipient = client.users.get(uid);
recipient = channel.recipient;
break;
case "Group":
icon = <Group size={24} />;
......@@ -109,12 +103,11 @@ export default function ChannelHeader({
<div
className="status"
style={{
backgroundColor: useStatusColour(
recipient as User,
),
backgroundColor:
useStatusColour(recipient),
}}
/>
<UserStatus user={recipient as User} />
<UserStatus user={recipient} />
</span>
</>
)}
......@@ -129,7 +122,7 @@ export default function ChannelHeader({
onClick={() =>
openScreen({
id: "channel_info",
channel_id: channel._id,
channel,
})
}>
<Markdown
......@@ -145,4 +138,4 @@ export default function ChannelHeader({
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
</Header>
);
}
});