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 194 additions and 451 deletions
...@@ -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;
...@@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
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,13 +5,13 @@ export const InternalEvent = new EventEmitter(); ...@@ -5,13 +5,13 @@ 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);
} }
......
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;
...@@ -57,7 +47,7 @@ export function TextReact({ id, fields }: Props) { ...@@ -57,7 +47,7 @@ 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 (const key of path) { for (const key of path) {
// @ts-expect-error // @ts-expect-error TODO: lazy
entry = entry[key]; entry = entry[key];
} }
...@@ -66,8 +56,12 @@ export function TextReact({ id, fields }: Props) { ...@@ -66,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() {
......
/* 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";
...@@ -122,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -122,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
...@@ -166,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -166,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
......
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,14 +7,10 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -8,14 +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( .client!.channels.get(id)!
id, .fetchMessagesWithUsers({ nearby, limit: 100 })
{ nearby, limit: 100 }, .then(({ messages }) => {
true, messages.sort((a, b) => a._id.localeCompare(b._id));
)
.then(({ messages: data }) => {
data.sort((a, b) => a._id.localeCompare(b._id));
const messages = data.map((x) => mapMessage(x));
renderer.setState( renderer.setState(
id, id,
{ {
...@@ -29,16 +24,16 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -29,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 }) => {
const 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 },
...@@ -49,12 +44,12 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -49,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);
...@@ -62,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -62,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = {
} }
renderer.setState( renderer.setState(
message.channel, message.channel_id,
{ {
...renderer.state, ...renderer.state,
messages, messages,
...@@ -71,28 +66,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -71,28 +66,7 @@ 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;
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" },
);
}
},
delete: async (renderer, id) => { delete: async (renderer, id) => {
const channel = renderer.channel; const channel = renderer.channel;
if (!channel) return; if (!channel) return;
...@@ -122,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -122,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, {
...@@ -139,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -139,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) {
...@@ -166,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -166,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, {
...@@ -183,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -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; 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";
...@@ -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 });
...@@ -124,6 +126,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -124,6 +126,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
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 });
......
...@@ -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,
......
import { createContext } from "preact";
import { useContext } from "preact/hooks";
import { DataStore } from ".";
import { Children } from "../types/Preact";
interface Props {
children: Children;
}
export const DataContext = createContext<DataStore>(null!);
// ! later we can do seamless account switching, by hooking this into Redux
// ! and monitoring changes to active account and hence swapping stores.
// although this may need more work since we need a Client per account too.
const store = new DataStore();
export default function StateLoader(props: Props) {
return (
<DataContext.Provider value={store}>
{props.children}
</DataContext.Provider>
);
}
export const useData = () => useContext(DataContext);
import isEqual from "lodash.isequal";
import {
makeAutoObservable,
observable,
autorun,
runInAction,
reaction,
makeObservable,
action,
extendObservable,
} from "mobx";
import { Attachment, Channels, Users } from "revolt.js/dist/api/objects";
import { RemoveChannelField, RemoveUserField } from "revolt.js/dist/api/routes";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
type Nullable<T> = T | null;
function toNullable<T>(data?: T) {
return typeof data === "undefined" ? null : data;
}
export class User {
_id: string;
username: string;
avatar: Nullable<Attachment>;
badges: Nullable<number>;
status: Nullable<Users.Status>;
relationship: Nullable<Users.Relationship>;
online: Nullable<boolean>;
constructor(data: Users.User) {
this._id = data._id;
this.username = data.username;
this.avatar = toNullable(data.avatar);
this.badges = toNullable(data.badges);
this.status = toNullable(data.status);
this.relationship = toNullable(data.relationship);
this.online = toNullable(data.online);
makeAutoObservable(this);
}
@action update(data: Partial<Users.User>, clear?: RemoveUserField) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Avatar":
this.avatar = null;
break;
case "StatusText": {
if (this.status) {
this.status.text = undefined;
}
}
}
apply("username");
apply("avatar");
apply("badges");
apply("status");
apply("relationship");
apply("online");
}
}
export class Channel {
_id: string;
channel_type: Channels.Channel["channel_type"];
// Direct Message
active: Nullable<boolean> = null;
// Group
owner: Nullable<string> = null;
// Server
server: Nullable<string> = null;
// Permissions
permissions: Nullable<number> = null;
default_permissions: Nullable<number> = null;
role_permissions: Nullable<{ [key: string]: number }> = null;
// Common
name: Nullable<string> = null;
icon: Nullable<Attachment> = null;
description: Nullable<string> = null;
recipients: Nullable<string[]> = null;
last_message: Nullable<string | Channels.LastMessage> = null;
constructor(data: Channels.Channel) {
this._id = data._id;
this.channel_type = data.channel_type;
switch (data.channel_type) {
case "DirectMessage": {
this.active = toNullable(data.active);
this.recipients = toNullable(data.recipients);
this.last_message = toNullable(data.last_message);
break;
}
case "Group": {
this.recipients = toNullable(data.recipients);
this.name = toNullable(data.name);
this.owner = toNullable(data.owner);
this.description = toNullable(data.description);
this.last_message = toNullable(data.last_message);
this.icon = toNullable(data.icon);
this.permissions = toNullable(data.permissions);
break;
}
case "TextChannel":
case "VoiceChannel": {
this.server = toNullable(data.server);
this.name = toNullable(data.name);
this.description = toNullable(data.description);
this.icon = toNullable(data.icon);
this.default_permissions = toNullable(data.default_permissions);
this.role_permissions = toNullable(data.role_permissions);
if (data.channel_type === "TextChannel") {
this.last_message = toNullable(data.last_message);
}
break;
}
}
makeAutoObservable(this);
}
@action update(
data: Partial<Channels.Channel>,
clear?: RemoveChannelField,
) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Description":
this.description = null;
break;
case "Icon":
this.icon = null;
break;
}
apply("active");
apply("owner");
apply("permissions");
apply("default_permissions");
apply("role_permissions");
apply("name");
apply("icon");
apply("description");
apply("recipients");
apply("last_message");
}
}
export class DataStore {
@observable users = new Map<string, User>();
@observable channels = new Map<string, Channel>();
constructor() {
makeAutoObservable(this);
}
@action
packet(packet: ClientboundNotification) {
switch (packet.type) {
case "Ready": {
for (let user of packet.users) {
this.users.set(user._id, new User(user));
}
for (let channel of packet.channels) {
this.channels.set(channel._id, new Channel(channel));
}
break;
}
case "UserUpdate": {
this.users.get(packet.id)?.update(packet.data, packet.clear);
break;
}
}
}
}
/* 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";
...@@ -29,36 +30,37 @@ export default function Open() { ...@@ -29,36 +30,37 @@ export default function Open() {
useEffect(() => { useEffect(() => {
if (id === "saved") { if (id === "saved") {
for (const channel of client.channels.toArray()) { 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;
} }
let user = client.users.get(id); const user = client.users.get(id);
if (user) { if (user) {
const channel: string | undefined = client.channels const channel: string | undefined = [
.toArray() ...client.channels.values(),
.find( ].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 }));
} }
...@@ -67,7 +69,7 @@ export default function Open() { ...@@ -67,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";
...@@ -42,83 +43,92 @@ export default function App() { ...@@ -42,83 +43,92 @@ export default function App() {
path.includes("/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))"
!inSpecial && 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">
......
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams, useHistory } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Channels } from "revolt.js/dist/api/objects"; import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { Channel as MobXChannel } from "../../mobx";
import { useData } from "../../mobx/State";
import { dispatch, getState } from "../../redux"; import { dispatch, getState } from "../../redux";
import { useClient } from "../../context/revoltjs/RevoltClient";
import AgeGate from "../../components/common/AgeGate"; import AgeGate from "../../components/common/AgeGate";
import MessageBox from "../../components/common/messaging/MessageBox"; import MessageBox from "../../components/common/messaging/MessageBox";
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
...@@ -37,8 +37,8 @@ const ChannelContent = styled.div` ...@@ -37,8 +37,8 @@ const ChannelContent = styled.div`
`; `;
export function Channel({ id }: { id: string }) { export function Channel({ id }: { id: string }) {
const store = useData(); const client = useClient();
const channel = store.channels.get(id); const channel = client.channels.get(id);
if (!channel) return null; if (!channel) return null;
if (channel.channel_type === "VoiceChannel") { if (channel.channel_type === "VoiceChannel") {
...@@ -49,7 +49,7 @@ export function Channel({ id }: { id: string }) { ...@@ -49,7 +49,7 @@ export function Channel({ id }: { id: string }) {
} }
const MEMBERS_SIDEBAR_KEY = "sidebar_members"; const MEMBERS_SIDEBAR_KEY = "sidebar_members";
const TextChannel = observer(({ channel }: { channel: MobXChannel }) => { const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const [showMembers, setMembers] = useState( const [showMembers, setMembers] = useState(
getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true, getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true,
); );
...@@ -60,11 +60,11 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => { ...@@ -60,11 +60,11 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => {
type="channel" type="channel"
channel={channel} channel={channel}
gated={ gated={
(channel.channel_type === "TextChannel" || !!(
channel.channel_type === "Group") && (channel.channel_type === "TextChannel" ||
channel.name?.includes("nsfw") channel.channel_type === "Group") &&
? true channel.name?.includes("nsfw")
: false )
}> }>
<ChannelHeader <ChannelHeader
channel={channel} channel={channel}
...@@ -89,7 +89,7 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => { ...@@ -89,7 +89,7 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => {
<ChannelContent> <ChannelContent>
<VoiceHeader id={id} /> <VoiceHeader id={id} />
<MessageArea id={id} /> <MessageArea id={id} />
<TypingIndicator id={id} /> <TypingIndicator channel={channel} />
<JumpToBottom id={id} /> <JumpToBottom id={id} />
<MessageBox channel={channel} /> <MessageBox channel={channel} />
</ChannelContent> </ChannelContent>
...@@ -101,7 +101,7 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => { ...@@ -101,7 +101,7 @@ const TextChannel = observer(({ channel }: { channel: MobXChannel }) => {
); );
}); });
function VoiceChannel({ channel }: { channel: MobXChannel }) { function VoiceChannel({ channel }: { channel: ChannelI }) {
return ( return (
<> <>
<ChannelHeader channel={channel} /> <ChannelHeader channel={channel} />
...@@ -110,7 +110,7 @@ function VoiceChannel({ channel }: { channel: MobXChannel }) { ...@@ -110,7 +110,7 @@ function VoiceChannel({ channel }: { channel: MobXChannel }) {
); );
} }
export default function () { export default function ChannelComponent() {
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />; return <Channel id={channel} key={channel} />;
} }
import { At, Hash, Menu } from "@styled-icons/boxicons-regular"; import { At, Hash, Menu } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { Notepad, Group } from "@styled-icons/boxicons-solid";
import { observable } from "mobx";
import { observer } from "mobx-react-lite"; 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 styled from "styled-components";
import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { Channel, User } from "../../mobx";
import { useData } from "../../mobx/State";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext, useClient } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util"; import { getChannelName } from "../../context/revoltjs/util";
import { useStatusColour } from "../../components/common/user/UserIcon"; import { useStatusColour } from "../../components/common/user/UserIcon";
...@@ -71,10 +66,8 @@ const Info = styled.div` ...@@ -71,10 +66,8 @@ const Info = styled.div`
export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => { export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useClient();
const state = useData();
const name = getChannelName(client, channel); const name = getChannelName(channel);
let icon, recipient: User | undefined; let icon, recipient: User | undefined;
switch (channel.channel_type) { switch (channel.channel_type) {
case "SavedMessages": case "SavedMessages":
...@@ -82,8 +75,7 @@ export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => { ...@@ -82,8 +75,7 @@ export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => {
break; break;
case "DirectMessage": case "DirectMessage":
icon = <At size={24} />; icon = <At size={24} />;
const uid = client.channels.getRecipient(channel._id); recipient = channel.recipient;
recipient = state.users.get(uid);
break; break;
case "Group": case "Group":
icon = <Group size={24} />; icon = <Group size={24} />;
......
/* eslint-disable react-hooks/rules-of-hooks */
import { import {
UserPlus, UserPlus,
Cog, Cog,
PhoneCall, PhoneCall,
PhoneOutgoing, PhoneOff,
Group, Group,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
VoiceContext, VoiceContext,
VoiceOperationsContext, VoiceOperationsContext,
VoiceStatus, VoiceStatus,
} from "../../../context/Voice"; } from "../../../context/Voice";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import UpdateIndicator from "../../../components/common/UpdateIndicator"; import UpdateIndicator from "../../../components/common/UpdateIndicator";
import IconButton from "../../../components/ui/IconButton"; import IconButton from "../../../components/ui/IconButton";
...@@ -29,25 +27,21 @@ export default function HeaderActions({ ...@@ -29,25 +27,21 @@ export default function HeaderActions({
toggleSidebar, toggleSidebar,
}: ChannelHeaderProps) { }: ChannelHeaderProps) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const history = useHistory(); const history = useHistory();
return ( return (
<> <>
<UpdateIndicator /> <UpdateIndicator style="channel" />
{channel.channel_type === "Group" && ( {channel.channel_type === "Group" && (
<> <>
<IconButton <IconButton
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "user_picker", id: "user_picker",
omit: channel.recipients!, omit: channel.recipient_ids!,
callback: async (users) => { callback: async (users) => {
for (const user of users) { for (const user of users) {
await client.channels.addMember( await channel.addMember(user);
channel._id,
user,
);
} }
}, },
}) })
...@@ -87,7 +81,7 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) { ...@@ -87,7 +81,7 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
if (voice.roomId === channel._id) { if (voice.roomId === channel._id) {
return ( return (
<IconButton onClick={disconnect}> <IconButton onClick={disconnect}>
<PhoneOutgoing size={22} /> <PhoneOff size={22} />
</IconButton> </IconButton>
); );
} }
...@@ -95,7 +89,7 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) { ...@@ -95,7 +89,7 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
<IconButton <IconButton
onClick={() => { onClick={() => {
disconnect(); disconnect();
connect(channel._id); connect(channel);
}}> }}>
<PhoneCall size={24} /> <PhoneCall size={24} />
</IconButton> </IconButton>
......