SingletonMessageRenderer.jumpToBottom(id, true)}>
+
renderer.jumpToBottom(true)}>
@@ -69,4 +67,4 @@ export default function JumpToBottom({ id }: { id: string }) {
);
-}
+});
diff --git a/src/components/common/messaging/bars/ReplyBar.tsx b/src/components/common/messaging/bars/ReplyBar.tsx
index b274b490..bbcc79bd 100644
--- a/src/components/common/messaging/bars/ReplyBar.tsx
+++ b/src/components/common/messaging/bars/ReplyBar.tsx
@@ -2,13 +2,14 @@ import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular";
import { File, XCircle } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { SYSTEM_USER_ID } from "revolt.js";
+import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { StateUpdater, useEffect } from "preact/hooks";
import { internalSubscribe } from "../../../../lib/eventEmitter";
-import { useRenderState } from "../../../../lib/renderer/Singleton";
+import { getRenderer } from "../../../../lib/renderer/Singleton";
import { Reply } from "../../../../redux/reducers/queue";
@@ -20,7 +21,7 @@ import { SystemMessage } from "../SystemMessage";
import { ReplyBase } from "../attachments/MessageReply";
interface Props {
- channel: string;
+ channel: Channel;
replies: Reply[];
setReplies: StateUpdater
;
}
@@ -87,11 +88,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
);
}, [replies, setReplies]);
- const view = useRenderState(channel);
- if (view?.type !== "RENDER") return null;
+ const renderer = getRenderer(channel);
+ if (renderer.state !== "RENDER") return null;
const ids = replies.map((x) => x.id);
- const messages = view.messages.filter((x) => ids.includes(x._id));
+ const messages = renderer.messages.filter((x) => ids.includes(x._id));
return (
diff --git a/src/components/navigation/right/ChannelDebugInfo.tsx b/src/components/navigation/right/ChannelDebugInfo.tsx
index 9b23c7d3..fc78a79e 100644
--- a/src/components/navigation/right/ChannelDebugInfo.tsx
+++ b/src/components/navigation/right/ChannelDebugInfo.tsx
@@ -1,14 +1,16 @@
/* eslint-disable react-hooks/rules-of-hooks */
-import { useRenderState } from "../../../lib/renderer/Singleton";
+import { observer } from "mobx-react-lite";
+import { Channel } from "revolt.js/dist/maps/Channels";
+
+import { getRenderer } from "../../../lib/renderer/Singleton";
interface Props {
- id: string;
+ channel: Channel;
}
-export function ChannelDebugInfo({ id }: Props) {
+export const ChannelDebugInfo = observer(({ channel }: Props) => {
if (process.env.NODE_ENV !== "development") return null;
- const view = useRenderState(id);
- if (!view) return null;
+ const renderer = getRenderer(channel);
return (
@@ -22,20 +24,26 @@ export function ChannelDebugInfo({ id }: Props) {
Channel Info
- State: {view.type}
- {view.type === "RENDER" && view.messages.length > 0 && (
+ State: {renderer.state}
+ Stale: {renderer.stale ? "Yes" : "No"}
+ Fetching: {renderer.fetching ? "Yes" : "No"}
+
+ {renderer.state === "RENDER" && renderer.messages.length > 0 && (
<>
- Start: {view.messages[0]._id}
+ Start: {renderer.messages[0]._id}
End:{" "}
- {view.messages[view.messages.length - 1]._id}
+ {
+ renderer.messages[renderer.messages.length - 1]
+ ._id
+ }
{" "}
- At Top: {view.atTop ? "Yes" : "No"}
- At Bottom: {view.atBottom ? "Yes" : "No"}
+ At Top: {renderer.atTop ? "Yes" : "No"}
+ At Bottom: {renderer.atBottom ? "Yes" : "No"}
>
)}
);
-}
+});
diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx
index 1eed07d3..b710b22a 100644
--- a/src/components/navigation/right/MemberSidebar.tsx
+++ b/src/components/navigation/right/MemberSidebar.tsx
@@ -90,7 +90,7 @@ export const GroupMemberSidebar = observer(
return (
-
+
{/*voiceActive && voiceParticipants.length !== 0 && (
@@ -202,7 +202,7 @@ export const ServerMemberSidebar = observer(
return (
-
+
{users.length > 0 && (
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index 30df4745..5e6efcef 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -5,8 +5,6 @@ import { Route } from "revolt.js/dist/api/routes";
import { createContext } from "preact";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
-import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
-
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { AuthState } from "../../redux/reducers/auth";
@@ -64,7 +62,6 @@ function Context({ auth, children }: Props) {
});
setClient(client);
- SingletonMessageRenderer.subscribe(client);
setStatus(ClientStatus.LOADING);
})();
}, []);
diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts
index 15c206d6..1f52a6c1 100644
--- a/src/lib/renderer/Singleton.ts
+++ b/src/lib/renderer/Singleton.ts
@@ -1,34 +1,52 @@
/* eslint-disable react-hooks/rules-of-hooks */
-import EventEmitter3 from "eventemitter3";
-import { Client } from "revolt.js";
+import { action, makeAutoObservable } from "mobx";
+import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
+import { Nullable } from "revolt.js/dist/util/null";
-import { useEffect, useState } from "preact/hooks";
-
+import { defer } from "../defer";
import { SimpleRenderer } from "./simple/SimpleRenderer";
-import { RendererRoutines, RenderState, ScrollState } from "./types";
+import { RendererRoutines, ScrollState } from "./types";
export const SMOOTH_SCROLL_ON_RECEIVE = false;
-export class SingletonRenderer extends EventEmitter3 {
- client?: Client;
- channel?: string;
- state: RenderState;
- currentRenderer: RendererRoutines;
+export class ChannelRenderer {
+ channel: Channel;
+
+ state: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY" | "RENDER" = "LOADING";
+ scrollState: ScrollState = { type: "ScrollToBottom" };
+ atTop: Nullable = null;
+ atBottom: Nullable = null;
+ messages: Message[] = [];
+
+ currentRenderer: RendererRoutines = SimpleRenderer;
stale = false;
- fetchingTop = false;
- fetchingBottom = false;
+ fetching = false;
- constructor() {
- super();
+ constructor(channel: Channel) {
+ this.channel = channel;
+
+ makeAutoObservable(this, {
+ channel: false,
+ currentRenderer: false,
+ });
this.receive = this.receive.bind(this);
this.edit = this.edit.bind(this);
this.delete = this.delete.bind(this);
- this.state = { type: "LOADING" };
- this.currentRenderer = SimpleRenderer;
+ const client = this.channel.client;
+ client.addListener("message", this.receive);
+ client.addListener("message/update", this.edit);
+ client.addListener("message/delete", this.delete);
+ }
+
+ destroy() {
+ const client = this.channel.client;
+ client.removeListener("message", this.receive);
+ client.removeListener("message/update", this.edit);
+ client.removeListener("message/delete", this.delete);
}
private receive(message: Message) {
@@ -43,90 +61,73 @@ export class SingletonRenderer extends EventEmitter3 {
this.currentRenderer.delete(this, id);
}
- subscribe(client: Client) {
- if (this.client) {
- this.client.removeListener("message", this.receive);
- this.client.removeListener("message/update", this.edit);
- this.client.removeListener("message/delete", this.delete);
- }
-
- this.client = client;
- client.addListener("message", this.receive);
- client.addListener("message/update", this.edit);
- client.addListener("message/delete", this.delete);
- }
-
- private setStateUnguarded(state: RenderState, scroll?: ScrollState) {
- this.state = state;
- this.emit("state", state);
-
- if (scroll) {
- this.emit("scroll", scroll);
- }
- }
-
- setState(id: string, state: RenderState, scroll?: ScrollState) {
- if (id !== this.channel) return;
- this.setStateUnguarded(state, scroll);
- }
-
- markStale() {
- this.stale = true;
- }
-
- async init(id: string, message_id?: string) {
+ @action async init(message_id?: string) {
if (message_id) {
- if (this.state.type === "RENDER") {
- const message = this.state.messages.find(
- (x) => x._id === message_id,
- );
+ if (this.state === "RENDER") {
+ const message = this.messages.find((x) => x._id === message_id);
+
if (message) {
- this.emit("scroll", {
+ this.emitScroll({
type: "ScrollToView",
id: message_id,
});
+
return;
}
}
}
- this.channel = id;
this.stale = false;
- this.setStateUnguarded({ type: "LOADING" });
- await this.currentRenderer.init(this, id, message_id);
+ this.state = "LOADING";
+ this.currentRenderer.init(this, message_id);
}
- async reloadStale(id: string) {
+ @action emitScroll(state: ScrollState) {
+ this.scrollState = state;
+ }
+
+ @action markStale() {
+ this.stale = true;
+ }
+
+ @action complete() {
+ this.fetching = false;
+ }
+
+ async reloadStale() {
if (this.stale) {
this.stale = false;
- await this.init(id);
+ await this.init();
}
}
async loadTop(ref?: HTMLDivElement) {
- if (this.fetchingTop) return;
- this.fetchingTop = true;
+ if (this.fetching) return;
+ this.fetching = true;
function generateScroll(end: string): ScrollState {
if (ref) {
- let heightRemoved = 0;
+ let heightRemoved = 0,
+ removing = false;
const messageContainer = ref.children[0];
if (messageContainer) {
for (const child of Array.from(messageContainer.children)) {
- // If this child has a ulid.
- if (child.id?.length === 26) {
- // Check whether it was removed.
- if (child.id.localeCompare(end) === 1) {
- heightRemoved +=
- child.clientHeight +
- // We also need to take into account the top margin of the container.
- parseInt(
- window
- .getComputedStyle(child)
- .marginTop.slice(0, -2),
- 10,
- );
- }
+ // If this child has a ulid, check whether it was removed.
+ if (
+ removing ||
+ (child.id?.length === 26 &&
+ child.id.localeCompare(end) === 1)
+ ) {
+ removing = true;
+ heightRemoved +=
+ child.clientHeight +
+ // We also need to take into account the top margin of the container.
+ parseInt(
+ window
+ .getComputedStyle(child)
+ .marginTop.slice(0, -2),
+ 10,
+ );
}
}
}
@@ -142,37 +143,44 @@ export class SingletonRenderer extends EventEmitter3 {
};
}
- await this.currentRenderer.loadTop(this, generateScroll);
-
- // Allow state updates to propagate.
- setTimeout(() => (this.fetchingTop = false), 0);
+ if (await this.currentRenderer.loadTop(this, generateScroll)) {
+ this.fetching = false;
+ }
}
async loadBottom(ref?: HTMLDivElement) {
- if (this.fetchingBottom) return;
- this.fetchingBottom = true;
+ if (this.fetching) return;
+ this.fetching = true;
function generateScroll(start: string): ScrollState {
if (ref) {
- let heightRemoved = 0;
+ let heightRemoved = 0,
+ removing = true;
const messageContainer = ref.children[0];
if (messageContainer) {
for (const child of Array.from(messageContainer.children)) {
- // If this child has a ulid.
- if (child.id?.length === 26) {
- // Check whether it was removed.
- if (child.id.localeCompare(start) === -1) {
- heightRemoved +=
- child.clientHeight +
- // We also need to take into account the top margin of the container.
- parseInt(
- window
- .getComputedStyle(child)
- .marginTop.slice(0, -2),
- 10,
- );
- }
+ // If this child has a ulid check whether it was removed.
+ if (
+ removing /* ||
+ (child.id?.length === 26 &&
+ child.id.localeCompare(start) === -1)*/
+ ) {
+ heightRemoved +=
+ child.clientHeight +
+ // We also need to take into account the top margin of the container.
+ parseInt(
+ window
+ .getComputedStyle(child)
+ .marginTop.slice(0, -2),
+ 10,
+ );
}
+
+ if (
+ child.id?.length === 26 &&
+ child.id.localeCompare(start) !== -1
+ )
+ removing = false;
}
}
@@ -186,38 +194,28 @@ export class SingletonRenderer extends EventEmitter3 {
};
}
- await this.currentRenderer.loadBottom(this, generateScroll);
-
- // Allow state updates to propagate.
- setTimeout(() => (this.fetchingBottom = false), 0);
+ if (await this.currentRenderer.loadBottom(this, generateScroll)) {
+ this.fetching = false;
+ }
}
- async jumpToBottom(id: string, smooth: boolean) {
- if (id !== this.channel) return;
- if (this.state.type === "RENDER" && this.state.atBottom) {
- this.emit("scroll", { type: "ScrollToBottom", smooth });
+ async jumpToBottom(smooth: boolean) {
+ if (this.state === "RENDER" && this.atBottom) {
+ this.emitScroll({ type: "ScrollToBottom", smooth });
} else {
- await this.currentRenderer.init(this, id, undefined, true);
+ await this.currentRenderer.init(this, undefined, true);
}
}
}
-export const SingletonMessageRenderer = new SingletonRenderer();
+const renderers: Record = {};
-export function useRenderState(id: string) {
- const [state, setState] = useState>(
- SingletonMessageRenderer.state,
- );
- if (typeof id === "undefined") return;
-
- function render(state: RenderState) {
- setState(state);
+export function getRenderer(channel: Channel) {
+ let renderer = renderers[channel._id];
+ if (!renderer) {
+ renderer = new ChannelRenderer(channel);
+ renderers[channel._id] = renderer;
}
- useEffect(() => {
- SingletonMessageRenderer.addListener("state", render);
- return () => SingletonMessageRenderer.removeListener("state", render);
- }, [id]);
-
- return state;
+ return renderer;
}
diff --git a/src/lib/renderer/simple/SimpleRenderer.ts b/src/lib/renderer/simple/SimpleRenderer.ts
index 5d05832e..aa7b1af8 100644
--- a/src/lib/renderer/simple/SimpleRenderer.ts
+++ b/src/lib/renderer/simple/SimpleRenderer.ts
@@ -1,173 +1,160 @@
+import { runInAction } from "mobx";
+
import { noopAsync } from "../../js";
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
import { RendererRoutines } from "../types";
export const SimpleRenderer: RendererRoutines = {
- init: async (renderer, id, nearby, smooth) => {
- if (renderer.client!.websocket.connected) {
+ init: async (renderer, nearby, smooth) => {
+ if (renderer.channel.client.websocket.connected) {
if (nearby)
- renderer
- .client!.channels.get(id)!
+ renderer.channel
.fetchMessagesWithUsers({ nearby, limit: 100 })
.then(({ messages }) => {
messages.sort((a, b) => a._id.localeCompare(b._id));
- renderer.setState(
- id,
- {
- type: "RENDER",
- messages,
- atTop: false,
- atBottom: false,
- },
- { type: "ScrollToView", id: nearby },
- );
+
+ runInAction(() => {
+ renderer.state = "RENDER";
+ renderer.messages = messages;
+ renderer.atTop = false;
+ renderer.atBottom = false;
+
+ renderer.emitScroll({
+ type: "ScrollToView",
+ id: nearby,
+ });
+ });
});
else
- renderer
- .client!.channels.get(id)!
+ renderer.channel
.fetchMessagesWithUsers({})
.then(({ messages }) => {
messages.reverse();
- renderer.setState(
- id,
- {
- type: "RENDER",
- messages,
- atTop: messages.length < 50,
- atBottom: true,
- },
- { type: "ScrollToBottom", smooth },
- );
+
+ runInAction(() => {
+ renderer.state = "RENDER";
+ renderer.messages = messages;
+ renderer.atTop = messages.length < 50;
+ renderer.atBottom = true;
+
+ renderer.emitScroll({
+ type: "ScrollToBottom",
+ smooth,
+ });
+ });
});
} else {
- renderer.setState(id, { type: "WAITING_FOR_NETWORK" });
+ runInAction(() => {
+ renderer.state = "WAITING_FOR_NETWORK";
+ });
}
},
receive: async (renderer, message) => {
- 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;
+ if (message.channel_id !== renderer.channel._id) return;
+ if (renderer.state !== "RENDER") return;
+ if (renderer.messages.find((x) => x._id === message._id)) return;
+ if (!renderer.atBottom) return;
- let messages = [...renderer.state.messages, message];
- let atTop = renderer.state.atTop;
+ let messages = [...renderer.messages, message];
+ let atTop = renderer.atTop;
if (messages.length > 150) {
messages = messages.slice(messages.length - 150);
atTop = false;
}
- renderer.setState(
- message.channel_id,
- {
- ...renderer.state,
- messages,
- atTop,
- },
- { type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
- );
+ runInAction(() => {
+ renderer.messages = messages;
+ renderer.atTop = atTop;
+
+ renderer.emitScroll({
+ type: "StayAtBottom",
+ smooth: SMOOTH_SCROLL_ON_RECEIVE,
+ });
+ });
},
edit: noopAsync,
delete: async (renderer, id) => {
const channel = renderer.channel;
if (!channel) return;
- if (renderer.state.type !== "RENDER") return;
+ if (renderer.state !== "RENDER") return;
- const messages = [...renderer.state.messages];
- const index = messages.findIndex((x) => x._id === id);
+ const index = renderer.messages.findIndex((x) => x._id === id);
if (index > -1) {
- messages.splice(index, 1);
-
- renderer.setState(
- channel,
- {
- ...renderer.state,
- messages,
- },
- { type: "StayAtBottom" },
- );
+ runInAction(() => {
+ renderer.messages.splice(index, 1);
+ renderer.emitScroll({ type: "StayAtBottom" });
+ });
}
},
loadTop: async (renderer, generateScroll) => {
const channel = renderer.channel;
- if (!channel) return;
+ if (!channel) return true;
- const state = renderer.state;
- if (state.type !== "RENDER") return;
- if (state.atTop) return;
+ if (renderer.state !== "RENDER") return true;
+ if (renderer.atTop) return true;
- const { messages: data } = await renderer
- .client!.channels.get(channel)!
- .fetchMessagesWithUsers({
- before: state.messages[0]._id,
+ const { messages: data } =
+ await renderer.channel.fetchMessagesWithUsers({
+ before: renderer.messages[0]._id,
});
- if (data.length === 0) {
- return renderer.setState(channel, {
- ...state,
- atTop: true,
- });
- }
+ runInAction(() => {
+ if (data.length === 0) {
+ renderer.atTop = true;
+ return;
+ }
- data.reverse();
- let messages = [...data, ...state.messages];
+ data.reverse();
+ renderer.messages = [...data, ...renderer.messages];
- let atTop = false;
- if (data.length < 50) {
- atTop = true;
- }
+ if (data.length < 50) {
+ renderer.atTop = true;
+ }
- let atBottom = state.atBottom;
- if (messages.length > 150) {
- messages = messages.slice(0, 150);
- atBottom = false;
- }
+ if (renderer.messages.length > 150) {
+ renderer.messages = renderer.messages.slice(0, 150);
+ renderer.atBottom = false;
+ }
- renderer.setState(
- channel,
- { ...state, atTop, atBottom, messages },
- generateScroll(messages[messages.length - 1]._id),
- );
+ renderer.emitScroll(
+ generateScroll(
+ renderer.messages[renderer.messages.length - 1]._id,
+ ),
+ );
+ });
},
loadBottom: async (renderer, generateScroll) => {
const channel = renderer.channel;
- if (!channel) return;
+ if (!channel) return true;
- const state = renderer.state;
- if (state.type !== "RENDER") return;
- if (state.atBottom) return;
+ if (renderer.state !== "RENDER") return true;
+ if (renderer.atBottom) return true;
- const { messages: data } = await renderer
- .client!.channels.get(channel)!
- .fetchMessagesWithUsers({
- after: state.messages[state.messages.length - 1]._id,
+ const { messages: data } =
+ await renderer.channel.fetchMessagesWithUsers({
+ after: renderer.messages[renderer.messages.length - 1]._id,
sort: "Oldest",
});
- if (data.length === 0) {
- return renderer.setState(channel, {
- ...state,
- atBottom: true,
- });
- }
+ runInAction(() => {
+ if (data.length === 0) {
+ renderer.atBottom = true;
+ return;
+ }
- let messages = [...state.messages, ...data];
+ renderer.messages.splice(renderer.messages.length, 0, ...data);
- let atBottom = false;
- if (data.length < 50) {
- atBottom = true;
- }
+ if (data.length < 50) {
+ renderer.atBottom = true;
+ }
- let atTop = state.atTop;
- if (messages.length > 150) {
- messages = messages.slice(messages.length - 150);
- atTop = false;
- }
+ if (renderer.messages.length > 150) {
+ renderer.messages.splice(0, renderer.messages.length - 150);
+ renderer.atTop = false;
+ }
- renderer.setState(
- channel,
- { ...state, atTop, atBottom, messages },
- generateScroll(messages[0]._id),
- );
+ renderer.emitScroll(generateScroll(renderer.messages[0]._id));
+ });
},
};
diff --git a/src/lib/renderer/types.ts b/src/lib/renderer/types.ts
index 61d830f1..b315c92e 100644
--- a/src/lib/renderer/types.ts
+++ b/src/lib/renderer/types.ts
@@ -1,6 +1,6 @@
import { Message } from "revolt.js/dist/maps/Messages";
-import { SingletonRenderer } from "./Singleton";
+import { ChannelRenderer } from "./Singleton";
export type ScrollState =
| { type: "Free" }
@@ -23,26 +23,25 @@ export type RenderState =
export interface RendererRoutines {
init: (
- renderer: SingletonRenderer,
- id: string,
+ renderer: ChannelRenderer,
message?: string,
smooth?: boolean,
) => Promise;
- receive: (renderer: SingletonRenderer, message: Message) => Promise;
+ receive: (renderer: ChannelRenderer, message: Message) => Promise;
edit: (
- renderer: SingletonRenderer,
+ renderer: ChannelRenderer,
id: string,
partial: Partial,
) => Promise;
- delete: (renderer: SingletonRenderer, id: string) => Promise;
+ delete: (renderer: ChannelRenderer, id: string) => Promise;
loadTop: (
- renderer: SingletonRenderer,
+ renderer: ChannelRenderer,
generateScroll: (end: string) => ScrollState,
- ) => Promise;
+ ) => Promise;
loadBottom: (
- renderer: SingletonRenderer,
+ renderer: ChannelRenderer,
generateScroll: (start: string) => ScrollState,
- ) => Promise;
+ ) => Promise;
}
diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx
index 64e22287..00653181 100644
--- a/src/pages/channels/Channel.tsx
+++ b/src/pages/channels/Channel.tsx
@@ -88,9 +88,9 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
-
+
-
+
{!isTouchscreenDevice && showMembers && (
diff --git a/src/pages/channels/messaging/ConversationStart.tsx b/src/pages/channels/messaging/ConversationStart.tsx
index 1d26bed6..1c829dd9 100644
--- a/src/pages/channels/messaging/ConversationStart.tsx
+++ b/src/pages/channels/messaging/ConversationStart.tsx
@@ -1,9 +1,9 @@
import { observer } from "mobx-react-lite";
+import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
-import { useClient } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util";
const StartBase = styled.div`
@@ -22,14 +22,10 @@ const StartBase = styled.div`
`;
interface Props {
- id: string;
+ channel: Channel;
}
-export default observer(({ id }: Props) => {
- const client = useClient();
- const channel = client.channels.get(id);
- if (!channel) return null;
-
+export default observer(({ channel }: Props) => {
return (
{getChannelName(channel, true)}
diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx
index 71f07ae5..dc9bd6dd 100644
--- a/src/pages/channels/messaging/MessageArea.tsx
+++ b/src/pages/channels/messaging/MessageArea.tsx
@@ -1,5 +1,8 @@
+import { runInAction } from "mobx";
+import { observer } from "mobx-react-lite";
import { useHistory, useParams } from "react-router-dom";
import { animateScroll } from "react-scroll";
+import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import useResizeObserver from "use-resize-observer";
@@ -15,13 +18,12 @@ import {
import { defer } from "../../../lib/defer";
import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
-import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
-import { RenderState, ScrollState } from "../../../lib/renderer/types";
+import { getRenderer } from "../../../lib/renderer/Singleton";
+import { ScrollState } from "../../../lib/renderer/types";
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import {
- AppContext,
ClientStatus,
StatusContext,
} from "../../../context/revoltjs/RevoltClient";
@@ -49,15 +51,14 @@ const Area = styled.div`
`;
interface Props {
- id: string;
+ channel: Channel;
}
export const MessageAreaWidthContext = createContext(0);
export const MESSAGE_AREA_PADDING = 82;
-export function MessageArea({ id }: Props) {
+export const MessageArea = observer(({ channel }: Props) => {
const history = useHistory();
- const client = useContext(AppContext);
const status = useContext(StatusContext);
const { focusTaken } = useContext(IntermediateContext);
@@ -70,69 +71,75 @@ export function MessageArea({ id }: Props) {
const { width, height } = useResizeObserver({ ref });
// ? Current channel state.
- const [state, setState] = useState({ type: "LOADING" });
+ const renderer = getRenderer(channel);
// ? useRef to avoid re-renders
const scrollState = useRef({ type: "Free" });
- const setScrollState = useCallback((v: ScrollState) => {
- if (v.type === "StayAtBottom") {
- if (scrollState.current.type === "Bottom" || atBottom()) {
- scrollState.current = {
- type: "ScrollToBottom",
- smooth: v.smooth,
- };
+ const setScrollState = useCallback(
+ (v: ScrollState) => {
+ if (v.type === "StayAtBottom") {
+ if (scrollState.current.type === "Bottom" || atBottom()) {
+ scrollState.current = {
+ type: "ScrollToBottom",
+ smooth: v.smooth,
+ };
+ } else {
+ scrollState.current = { type: "Free" };
+ }
} else {
- scrollState.current = { type: "Free" };
+ scrollState.current = v;
}
- } else {
- scrollState.current = v;
- }
- defer(() => {
- if (scrollState.current.type === "ScrollToBottom") {
- setScrollState({
- type: "Bottom",
- scrollingUntil: +new Date() + 150,
- });
+ defer(() => {
+ if (scrollState.current.type === "ScrollToBottom") {
+ setScrollState({
+ type: "Bottom",
+ scrollingUntil: +new Date() + 150,
+ });
- animateScroll.scrollToBottom({
- container: ref.current,
- duration: scrollState.current.smooth ? 150 : 0,
- });
- } else if (scrollState.current.type === "ScrollToView") {
- document
- .getElementById(scrollState.current.id)
- ?.scrollIntoView({ block: "center" });
+ animateScroll.scrollToBottom({
+ container: ref.current,
+ duration: scrollState.current.smooth ? 150 : 0,
+ });
+ } else if (scrollState.current.type === "ScrollToView") {
+ document
+ .getElementById(scrollState.current.id)
+ ?.scrollIntoView({ block: "center" });
- setScrollState({ type: "Free" });
- } else if (scrollState.current.type === "OffsetTop") {
- animateScroll.scrollTo(
- Math.max(
- 101,
- ref.current
- ? ref.current.scrollTop +
- (ref.current.scrollHeight -
- scrollState.current.previousHeight)
- : 101,
- ),
- {
+ setScrollState({ type: "Free" });
+ } else if (scrollState.current.type === "OffsetTop") {
+ animateScroll.scrollTo(
+ Math.max(
+ 101,
+ ref.current
+ ? ref.current.scrollTop +
+ (ref.current.scrollHeight -
+ scrollState.current.previousHeight)
+ : 101,
+ ),
+ {
+ container: ref.current,
+ duration: 0,
+ },
+ );
+
+ setScrollState({ type: "Free" });
+ } else if (scrollState.current.type === "ScrollTop") {
+ animateScroll.scrollTo(scrollState.current.y, {
container: ref.current,
duration: 0,
- },
- );
+ });
- setScrollState({ type: "Free" });
- } else if (scrollState.current.type === "ScrollTop") {
- animateScroll.scrollTo(scrollState.current.y, {
- container: ref.current,
- duration: 0,
- });
+ setScrollState({ type: "Free" });
+ }
- setScrollState({ type: "Free" });
- }
- });
- }, []);
+ defer(() => renderer.complete());
+ });
+ },
+ // eslint-disable-next-line
+ [scrollState],
+ );
// ? Determine if we are at the bottom of the scroll container.
// -> https://stackoverflow.com/a/44893438
@@ -155,35 +162,36 @@ export function MessageArea({ id }: Props) {
}, [setScrollState]);
// ? Handle events from renderer.
- useEffect(() => {
- SingletonMessageRenderer.addListener("state", setState);
- return () => SingletonMessageRenderer.removeListener("state", setState);
- }, []);
-
- useEffect(() => {
- SingletonMessageRenderer.addListener("scroll", setScrollState);
- return () =>
- SingletonMessageRenderer.removeListener("scroll", setScrollState);
- }, [scrollState, setScrollState]);
+ useLayoutEffect(
+ () => setScrollState(renderer.scrollState),
+ // eslint-disable-next-line
+ [renderer.scrollState],
+ );
// ? Load channel initially.
useEffect(() => {
if (message) return;
- SingletonMessageRenderer.init(id);
+ if (renderer.state === "RENDER") {
+ runInAction(() => (renderer.fetching = true));
+ setScrollState({ type: "ScrollTop", y: 151 });
+ } else {
+ renderer.init();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [id]);
+ }, []);
// ? If message present or changes, load it as well.
useEffect(() => {
if (message) {
setHighlight(message);
- SingletonMessageRenderer.init(id, message);
+ renderer.init(message);
- const channel = client.channels.get(id);
- if (channel?.channel_type === "TextChannel") {
- history.push(`/server/${channel.server_id}/channel/${id}`);
+ if (channel.channel_type === "TextChannel") {
+ history.push(
+ `/server/${channel.server_id}/channel/${channel._id}`,
+ );
} else {
- history.push(`/channel/${id}`);
+ history.push(`/channel/${channel._id}`);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -193,20 +201,20 @@ export function MessageArea({ id }: Props) {
useEffect(() => {
switch (status) {
case ClientStatus.ONLINE:
- if (state.type === "WAITING_FOR_NETWORK") {
- SingletonMessageRenderer.init(id);
+ if (renderer.state === "WAITING_FOR_NETWORK") {
+ renderer.init();
} else {
- SingletonMessageRenderer.reloadStale(id);
+ renderer.reloadStale();
}
break;
case ClientStatus.OFFLINE:
case ClientStatus.DISCONNECTED:
case ClientStatus.CONNECTING:
- SingletonMessageRenderer.markStale();
+ renderer.markStale();
break;
}
- }, [id, status, state]);
+ }, [renderer, status]);
// ? When the container is scrolled.
// ? Also handle StayAtBottom
@@ -238,17 +246,17 @@ export function MessageArea({ id }: Props) {
async function onScroll() {
if (atTop(100)) {
- SingletonMessageRenderer.loadTop(ref.current!);
+ renderer.loadTop(ref.current!);
}
if (atBottom(100)) {
- SingletonMessageRenderer.loadBottom(ref.current!);
+ renderer.loadBottom(ref.current!);
}
}
current.addEventListener("scroll", onScroll);
return () => current.removeEventListener("scroll", onScroll);
- }, [ref]);
+ }, [ref, renderer]);
// ? Scroll down whenever the message area resizes.
const stbOnResize = useCallback(() => {
@@ -277,36 +285,37 @@ export function MessageArea({ id }: Props) {
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
- SingletonMessageRenderer.jumpToBottom(id, true);
+ renderer.jumpToBottom(true);
internalEmit("TextArea", "focus", "message");
}
}
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
- }, [id, ref, focusTaken]);
+ }, [renderer, ref, focusTaken]);
return (
- {state.type === "LOADING" &&
}
- {state.type === "WAITING_FOR_NETWORK" && (
+ {renderer.state === "LOADING" &&
}
+ {renderer.state === "WAITING_FOR_NETWORK" && (
)}
- {state.type === "RENDER" && (
+ {renderer.state === "RENDER" && (
)}
- {state.type === "EMPTY" &&
}
+ {renderer.state === "EMPTY" && (
+
+ )}
);
-}
+});
diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx
index fc470c96..d1a442fa 100644
--- a/src/pages/channels/messaging/MessageRenderer.tsx
+++ b/src/pages/channels/messaging/MessageRenderer.tsx
@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { X } from "@styled-icons/boxicons-regular";
+import { observer } from "mobx-react-lite";
import { RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
@@ -11,7 +12,7 @@ import { memo } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
-import { RenderState } from "../../../lib/renderer/types";
+import { ChannelRenderer } from "../../../lib/renderer/Singleton";
import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue";
@@ -29,10 +30,9 @@ import ConversationStart from "./ConversationStart";
import MessageEditor from "./MessageEditor";
interface Props {
- id: string;
- state: RenderState;
highlight?: string;
queue: QueuedMessage[];
+ renderer: ChannelRenderer;
}
const BlockedMessage = styled.div`
@@ -46,9 +46,7 @@ const BlockedMessage = styled.div`
}
`;
-function MessageRenderer({ id, state, queue, highlight }: Props) {
- if (state.type !== "RENDER") return null;
-
+const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => {
const client = useClient();
const userId = client.user!._id;
@@ -60,10 +58,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
useEffect(() => {
function editLast() {
- if (state.type !== "RENDER") return;
- for (let i = state.messages.length - 1; i >= 0; i--) {
- if (state.messages[i].author_id === userId) {
- setEditing(state.messages[i]._id);
+ if (renderer.state !== "RENDER") return;
+ for (let i = renderer.messages.length - 1; i >= 0; i--) {
+ if (renderer.messages[i].author_id === userId) {
+ setEditing(renderer.messages[i]._id);
internalEmit("MessageArea", "jump_to_bottom");
return;
}
@@ -80,13 +78,13 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
];
return () => subs.forEach((unsub) => unsub());
- }, [state.messages, state.type, userId]);
+ }, [renderer.messages, renderer.state, userId]);
const render: Children[] = [];
let previous: MessageI | undefined;
- if (state.atTop) {
- render.push();
+ if (renderer.atTop) {
+ render.push();
} else {
render.push(
@@ -133,7 +131,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
blocked = 0;
}
- for (const message of state.messages) {
+ for (const message of renderer.messages) {
if (previous) {
compare(
message._id,
@@ -183,10 +181,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
if (blocked > 0) pushBlocked();
- const nonces = state.messages.map((x) => x.nonce);
- if (state.atBottom) {
+ const nonces = renderer.messages.map((x) => x.nonce);
+ if (renderer.atBottom) {
for (const msg of queue) {
- if (msg.channel !== id) continue;
+ if (msg.channel !== renderer.channel._id) continue;
if (nonces.includes(msg.id)) continue;
if (previous) {
@@ -222,7 +220,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
}
return <>{render}>;
-}
+});
export default memo(
connectState>(MessageRenderer, (state) => {