mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-27 07:52:11 -05:00
Manage state per channel. Closes #2
This commit is contained in:
parent
7d76a657fa
commit
1f903cd56b
14 changed files with 392 additions and 404 deletions
|
@ -16,7 +16,7 @@ import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
|
||||||
import { useTranslation } from "../../../lib/i18n";
|
import { useTranslation } from "../../../lib/i18n";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
import {
|
import {
|
||||||
SingletonMessageRenderer,
|
getRenderer,
|
||||||
SMOOTH_SCROLL_ON_RECEIVE,
|
SMOOTH_SCROLL_ON_RECEIVE,
|
||||||
} from "../../../lib/renderer/Singleton";
|
} from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
|
@ -122,6 +122,8 @@ export default observer(({ channel }: Props) => {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
|
||||||
|
const renderer = getRenderer(channel);
|
||||||
|
|
||||||
if (!(channel.permission & ChannelPermission.SendMessage)) {
|
if (!(channel.permission & ChannelPermission.SendMessage)) {
|
||||||
return (
|
return (
|
||||||
<Base>
|
<Base>
|
||||||
|
@ -213,12 +215,7 @@ export default observer(({ channel }: Props) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defer(() =>
|
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
|
||||||
SingletonMessageRenderer.jumpToBottom(
|
|
||||||
channel._id,
|
|
||||||
SMOOTH_SCROLL_ON_RECEIVE,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await channel.sendMessage({
|
await channel.sendMessage({
|
||||||
|
@ -405,7 +402,7 @@ export default observer(({ channel }: Props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ReplyBar
|
<ReplyBar
|
||||||
channel={channel._id}
|
channel={channel}
|
||||||
replies={replies}
|
replies={replies}
|
||||||
setReplies={setReplies}
|
setReplies={setReplies}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import styled, { css } from "styled-components";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useLayoutEffect, useState } from "preact/hooks";
|
import { useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useRenderState } from "../../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import UserShort from "../../user/UserShort";
|
import UserShort from "../../user/UserShort";
|
||||||
|
@ -134,8 +134,8 @@ export const ReplyBase = styled.div<{
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageReply = observer(({ index, channel, id }: Props) => {
|
export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||||
const view = useRenderState(channel._id);
|
const view = getRenderer(channel);
|
||||||
if (view?.type !== "RENDER") return null;
|
if (view.state !== "RENDER") return null;
|
||||||
|
|
||||||
const [message, setMessage] = useState<Message | undefined>(undefined);
|
const [message, setMessage] = useState<Message | undefined>(undefined);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
|
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import {
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
SingletonMessageRenderer,
|
|
||||||
useRenderState,
|
|
||||||
} from "../../../../lib/renderer/Singleton";
|
|
||||||
|
|
||||||
const Bar = styled.div`
|
const Bar = styled.div`
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
@ -51,14 +50,13 @@ const Bar = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function JumpToBottom({ id }: { id: string }) {
|
export default observer(({ channel }: { channel: Channel }) => {
|
||||||
const view = useRenderState(id);
|
const renderer = getRenderer(channel);
|
||||||
if (!view || view.type !== "RENDER" || view.atBottom) return null;
|
if (renderer.state !== "RENDER" || renderer.atBottom) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bar>
|
<Bar>
|
||||||
<div
|
<div onClick={() => renderer.jumpToBottom(true)}>
|
||||||
onClick={() => SingletonMessageRenderer.jumpToBottom(id, true)}>
|
|
||||||
<div>
|
<div>
|
||||||
<Text id="app.main.channel.misc.viewing_old" />
|
<Text id="app.main.channel.misc.viewing_old" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,4 +67,4 @@ export default function JumpToBottom({ id }: { id: string }) {
|
||||||
</div>
|
</div>
|
||||||
</Bar>
|
</Bar>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular";
|
||||||
import { File, XCircle } from "@styled-icons/boxicons-solid";
|
import { File, XCircle } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { SYSTEM_USER_ID } from "revolt.js";
|
import { SYSTEM_USER_ID } from "revolt.js";
|
||||||
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { StateUpdater, useEffect } from "preact/hooks";
|
import { StateUpdater, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||||
import { useRenderState } from "../../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import { Reply } from "../../../../redux/reducers/queue";
|
import { Reply } from "../../../../redux/reducers/queue";
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ import { SystemMessage } from "../SystemMessage";
|
||||||
import { ReplyBase } from "../attachments/MessageReply";
|
import { ReplyBase } from "../attachments/MessageReply";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
channel: string;
|
channel: Channel;
|
||||||
replies: Reply[];
|
replies: Reply[];
|
||||||
setReplies: StateUpdater<Reply[]>;
|
setReplies: StateUpdater<Reply[]>;
|
||||||
}
|
}
|
||||||
|
@ -87,11 +88,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||||
);
|
);
|
||||||
}, [replies, setReplies]);
|
}, [replies, setReplies]);
|
||||||
|
|
||||||
const view = useRenderState(channel);
|
const renderer = getRenderer(channel);
|
||||||
if (view?.type !== "RENDER") return null;
|
if (renderer.state !== "RENDER") return null;
|
||||||
|
|
||||||
const ids = replies.map((x) => x.id);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* 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 {
|
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;
|
if (process.env.NODE_ENV !== "development") return null;
|
||||||
const view = useRenderState(id);
|
const renderer = getRenderer(channel);
|
||||||
if (!view) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
|
<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
|
||||||
|
@ -22,20 +24,26 @@ export function ChannelDebugInfo({ id }: Props) {
|
||||||
Channel Info
|
Channel Info
|
||||||
</span>
|
</span>
|
||||||
<p style={{ fontSize: "10px", userSelect: "text" }}>
|
<p style={{ fontSize: "10px", userSelect: "text" }}>
|
||||||
State: <b>{view.type}</b> <br />
|
State: <b>{renderer.state}</b> <br />
|
||||||
{view.type === "RENDER" && view.messages.length > 0 && (
|
Stale: <b>{renderer.stale ? "Yes" : "No"}</b> <br />
|
||||||
|
Fetching: <b>{renderer.fetching ? "Yes" : "No"}</b> <br />
|
||||||
|
<br />
|
||||||
|
{renderer.state === "RENDER" && renderer.messages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
Start: <b>{view.messages[0]._id}</b> <br />
|
Start: <b>{renderer.messages[0]._id}</b> <br />
|
||||||
End:{" "}
|
End:{" "}
|
||||||
<b>
|
<b>
|
||||||
{view.messages[view.messages.length - 1]._id}
|
{
|
||||||
|
renderer.messages[renderer.messages.length - 1]
|
||||||
|
._id
|
||||||
|
}
|
||||||
</b>{" "}
|
</b>{" "}
|
||||||
<br />
|
<br />
|
||||||
At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
|
At Top: <b>{renderer.atTop ? "Yes" : "No"}</b> <br />
|
||||||
At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
|
At Bottom: <b>{renderer.atBottom ? "Yes" : "No"}</b>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const GroupMemberSidebar = observer(
|
||||||
return (
|
return (
|
||||||
<GenericSidebarBase>
|
<GenericSidebarBase>
|
||||||
<GenericSidebarList>
|
<GenericSidebarList>
|
||||||
<ChannelDebugInfo id={channel._id} />
|
<ChannelDebugInfo channel={channel} />
|
||||||
<Search channel={channel} />
|
<Search channel={channel} />
|
||||||
|
|
||||||
{/*voiceActive && voiceParticipants.length !== 0 && (
|
{/*voiceActive && voiceParticipants.length !== 0 && (
|
||||||
|
@ -202,7 +202,7 @@ export const ServerMemberSidebar = observer(
|
||||||
return (
|
return (
|
||||||
<GenericSidebarBase>
|
<GenericSidebarBase>
|
||||||
<GenericSidebarList>
|
<GenericSidebarList>
|
||||||
<ChannelDebugInfo id={channel._id} />
|
<ChannelDebugInfo channel={channel} />
|
||||||
<Search channel={channel} />
|
<Search channel={channel} />
|
||||||
<div>{users.length === 0 && <Preloader type="ring" />}</div>
|
<div>{users.length === 0 && <Preloader type="ring" />}</div>
|
||||||
{users.length > 0 && (
|
{users.length > 0 && (
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { Route } from "revolt.js/dist/api/routes";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
|
|
||||||
|
|
||||||
import { dispatch } from "../../redux";
|
import { dispatch } from "../../redux";
|
||||||
import { connectState } from "../../redux/connector";
|
import { connectState } from "../../redux/connector";
|
||||||
import { AuthState } from "../../redux/reducers/auth";
|
import { AuthState } from "../../redux/reducers/auth";
|
||||||
|
@ -64,7 +62,6 @@ function Context({ auth, children }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
setClient(client);
|
setClient(client);
|
||||||
SingletonMessageRenderer.subscribe(client);
|
|
||||||
setStatus(ClientStatus.LOADING);
|
setStatus(ClientStatus.LOADING);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,34 +1,52 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import EventEmitter3 from "eventemitter3";
|
import { action, makeAutoObservable } from "mobx";
|
||||||
import { Client } from "revolt.js";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
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 { SimpleRenderer } from "./simple/SimpleRenderer";
|
||||||
import { RendererRoutines, RenderState, ScrollState } from "./types";
|
import { RendererRoutines, ScrollState } from "./types";
|
||||||
|
|
||||||
export const SMOOTH_SCROLL_ON_RECEIVE = false;
|
export const SMOOTH_SCROLL_ON_RECEIVE = false;
|
||||||
|
|
||||||
export class SingletonRenderer extends EventEmitter3 {
|
export class ChannelRenderer {
|
||||||
client?: Client;
|
channel: Channel;
|
||||||
channel?: string;
|
|
||||||
state: RenderState;
|
state: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY" | "RENDER" = "LOADING";
|
||||||
currentRenderer: RendererRoutines;
|
scrollState: ScrollState = { type: "ScrollToBottom" };
|
||||||
|
atTop: Nullable<boolean> = null;
|
||||||
|
atBottom: Nullable<boolean> = null;
|
||||||
|
messages: Message[] = [];
|
||||||
|
|
||||||
|
currentRenderer: RendererRoutines = SimpleRenderer;
|
||||||
|
|
||||||
stale = false;
|
stale = false;
|
||||||
fetchingTop = false;
|
fetching = false;
|
||||||
fetchingBottom = false;
|
|
||||||
|
|
||||||
constructor() {
|
constructor(channel: Channel) {
|
||||||
super();
|
this.channel = channel;
|
||||||
|
|
||||||
|
makeAutoObservable(this, {
|
||||||
|
channel: false,
|
||||||
|
currentRenderer: false,
|
||||||
|
});
|
||||||
|
|
||||||
this.receive = this.receive.bind(this);
|
this.receive = this.receive.bind(this);
|
||||||
this.edit = this.edit.bind(this);
|
this.edit = this.edit.bind(this);
|
||||||
this.delete = this.delete.bind(this);
|
this.delete = this.delete.bind(this);
|
||||||
|
|
||||||
this.state = { type: "LOADING" };
|
const client = this.channel.client;
|
||||||
this.currentRenderer = SimpleRenderer;
|
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) {
|
private receive(message: Message) {
|
||||||
|
@ -43,90 +61,73 @@ export class SingletonRenderer extends EventEmitter3 {
|
||||||
this.currentRenderer.delete(this, id);
|
this.currentRenderer.delete(this, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(client: Client) {
|
@action async init(message_id?: string) {
|
||||||
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) {
|
|
||||||
if (message_id) {
|
if (message_id) {
|
||||||
if (this.state.type === "RENDER") {
|
if (this.state === "RENDER") {
|
||||||
const message = this.state.messages.find(
|
const message = this.messages.find((x) => x._id === message_id);
|
||||||
(x) => x._id === message_id,
|
|
||||||
);
|
|
||||||
if (message) {
|
if (message) {
|
||||||
this.emit("scroll", {
|
this.emitScroll({
|
||||||
type: "ScrollToView",
|
type: "ScrollToView",
|
||||||
id: message_id,
|
id: message_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.channel = id;
|
|
||||||
this.stale = false;
|
this.stale = false;
|
||||||
this.setStateUnguarded({ type: "LOADING" });
|
this.state = "LOADING";
|
||||||
await this.currentRenderer.init(this, id, message_id);
|
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) {
|
if (this.stale) {
|
||||||
this.stale = false;
|
this.stale = false;
|
||||||
await this.init(id);
|
await this.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTop(ref?: HTMLDivElement) {
|
async loadTop(ref?: HTMLDivElement) {
|
||||||
if (this.fetchingTop) return;
|
if (this.fetching) return;
|
||||||
this.fetchingTop = true;
|
this.fetching = true;
|
||||||
|
|
||||||
function generateScroll(end: string): ScrollState {
|
function generateScroll(end: string): ScrollState {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
let heightRemoved = 0;
|
let heightRemoved = 0,
|
||||||
|
removing = false;
|
||||||
const messageContainer = ref.children[0];
|
const messageContainer = ref.children[0];
|
||||||
if (messageContainer) {
|
if (messageContainer) {
|
||||||
for (const child of Array.from(messageContainer.children)) {
|
for (const child of Array.from(messageContainer.children)) {
|
||||||
// If this child has a ulid.
|
// If this child has a ulid, check whether it was removed.
|
||||||
if (child.id?.length === 26) {
|
if (
|
||||||
// Check whether it was removed.
|
removing ||
|
||||||
if (child.id.localeCompare(end) === 1) {
|
(child.id?.length === 26 &&
|
||||||
heightRemoved +=
|
child.id.localeCompare(end) === 1)
|
||||||
child.clientHeight +
|
) {
|
||||||
// We also need to take into account the top margin of the container.
|
removing = true;
|
||||||
parseInt(
|
heightRemoved +=
|
||||||
window
|
child.clientHeight +
|
||||||
.getComputedStyle(child)
|
// We also need to take into account the top margin of the container.
|
||||||
.marginTop.slice(0, -2),
|
parseInt(
|
||||||
10,
|
window
|
||||||
);
|
.getComputedStyle(child)
|
||||||
}
|
.marginTop.slice(0, -2),
|
||||||
|
10,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,37 +143,44 @@ export class SingletonRenderer extends EventEmitter3 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.currentRenderer.loadTop(this, generateScroll);
|
if (await this.currentRenderer.loadTop(this, generateScroll)) {
|
||||||
|
this.fetching = false;
|
||||||
// Allow state updates to propagate.
|
}
|
||||||
setTimeout(() => (this.fetchingTop = false), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBottom(ref?: HTMLDivElement) {
|
async loadBottom(ref?: HTMLDivElement) {
|
||||||
if (this.fetchingBottom) return;
|
if (this.fetching) return;
|
||||||
this.fetchingBottom = true;
|
this.fetching = true;
|
||||||
|
|
||||||
function generateScroll(start: string): ScrollState {
|
function generateScroll(start: string): ScrollState {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
let heightRemoved = 0;
|
let heightRemoved = 0,
|
||||||
|
removing = true;
|
||||||
const messageContainer = ref.children[0];
|
const messageContainer = ref.children[0];
|
||||||
if (messageContainer) {
|
if (messageContainer) {
|
||||||
for (const child of Array.from(messageContainer.children)) {
|
for (const child of Array.from(messageContainer.children)) {
|
||||||
// If this child has a ulid.
|
// If this child has a ulid check whether it was removed.
|
||||||
if (child.id?.length === 26) {
|
if (
|
||||||
// Check whether it was removed.
|
removing /* ||
|
||||||
if (child.id.localeCompare(start) === -1) {
|
(child.id?.length === 26 &&
|
||||||
heightRemoved +=
|
child.id.localeCompare(start) === -1)*/
|
||||||
child.clientHeight +
|
) {
|
||||||
// We also need to take into account the top margin of the container.
|
heightRemoved +=
|
||||||
parseInt(
|
child.clientHeight +
|
||||||
window
|
// We also need to take into account the top margin of the container.
|
||||||
.getComputedStyle(child)
|
parseInt(
|
||||||
.marginTop.slice(0, -2),
|
window
|
||||||
10,
|
.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);
|
if (await this.currentRenderer.loadBottom(this, generateScroll)) {
|
||||||
|
this.fetching = false;
|
||||||
// Allow state updates to propagate.
|
}
|
||||||
setTimeout(() => (this.fetchingBottom = false), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async jumpToBottom(id: string, smooth: boolean) {
|
async jumpToBottom(smooth: boolean) {
|
||||||
if (id !== this.channel) return;
|
if (this.state === "RENDER" && this.atBottom) {
|
||||||
if (this.state.type === "RENDER" && this.state.atBottom) {
|
this.emitScroll({ type: "ScrollToBottom", smooth });
|
||||||
this.emit("scroll", { type: "ScrollToBottom", smooth });
|
|
||||||
} else {
|
} else {
|
||||||
await this.currentRenderer.init(this, id, undefined, true);
|
await this.currentRenderer.init(this, undefined, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingletonMessageRenderer = new SingletonRenderer();
|
const renderers: Record<string, ChannelRenderer> = {};
|
||||||
|
|
||||||
export function useRenderState(id: string) {
|
export function getRenderer(channel: Channel) {
|
||||||
const [state, setState] = useState<Readonly<RenderState>>(
|
let renderer = renderers[channel._id];
|
||||||
SingletonMessageRenderer.state,
|
if (!renderer) {
|
||||||
);
|
renderer = new ChannelRenderer(channel);
|
||||||
if (typeof id === "undefined") return;
|
renderers[channel._id] = renderer;
|
||||||
|
|
||||||
function render(state: RenderState) {
|
|
||||||
setState(state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
return renderer;
|
||||||
SingletonMessageRenderer.addListener("state", render);
|
|
||||||
return () => SingletonMessageRenderer.removeListener("state", render);
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,173 +1,160 @@
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
import { noopAsync } from "../../js";
|
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";
|
||||||
|
|
||||||
export const SimpleRenderer: RendererRoutines = {
|
export const SimpleRenderer: RendererRoutines = {
|
||||||
init: async (renderer, id, nearby, smooth) => {
|
init: async (renderer, nearby, smooth) => {
|
||||||
if (renderer.client!.websocket.connected) {
|
if (renderer.channel.client.websocket.connected) {
|
||||||
if (nearby)
|
if (nearby)
|
||||||
renderer
|
renderer.channel
|
||||||
.client!.channels.get(id)!
|
|
||||||
.fetchMessagesWithUsers({ nearby, limit: 100 })
|
.fetchMessagesWithUsers({ nearby, limit: 100 })
|
||||||
.then(({ messages }) => {
|
.then(({ messages }) => {
|
||||||
messages.sort((a, b) => a._id.localeCompare(b._id));
|
messages.sort((a, b) => a._id.localeCompare(b._id));
|
||||||
renderer.setState(
|
|
||||||
id,
|
runInAction(() => {
|
||||||
{
|
renderer.state = "RENDER";
|
||||||
type: "RENDER",
|
renderer.messages = messages;
|
||||||
messages,
|
renderer.atTop = false;
|
||||||
atTop: false,
|
renderer.atBottom = false;
|
||||||
atBottom: false,
|
|
||||||
},
|
renderer.emitScroll({
|
||||||
{ type: "ScrollToView", id: nearby },
|
type: "ScrollToView",
|
||||||
);
|
id: nearby,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
renderer
|
renderer.channel
|
||||||
.client!.channels.get(id)!
|
|
||||||
.fetchMessagesWithUsers({})
|
.fetchMessagesWithUsers({})
|
||||||
.then(({ messages }) => {
|
.then(({ messages }) => {
|
||||||
messages.reverse();
|
messages.reverse();
|
||||||
renderer.setState(
|
|
||||||
id,
|
runInAction(() => {
|
||||||
{
|
renderer.state = "RENDER";
|
||||||
type: "RENDER",
|
renderer.messages = messages;
|
||||||
messages,
|
renderer.atTop = messages.length < 50;
|
||||||
atTop: messages.length < 50,
|
renderer.atBottom = true;
|
||||||
atBottom: true,
|
|
||||||
},
|
renderer.emitScroll({
|
||||||
{ type: "ScrollToBottom", smooth },
|
type: "ScrollToBottom",
|
||||||
);
|
smooth,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
renderer.setState(id, { type: "WAITING_FOR_NETWORK" });
|
runInAction(() => {
|
||||||
|
renderer.state = "WAITING_FOR_NETWORK";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
receive: async (renderer, message) => {
|
receive: async (renderer, message) => {
|
||||||
if (message.channel_id !== renderer.channel) return;
|
if (message.channel_id !== renderer.channel._id) return;
|
||||||
if (renderer.state.type !== "RENDER") return;
|
if (renderer.state !== "RENDER") return;
|
||||||
if (renderer.state.messages.find((x) => x._id === message._id)) return;
|
if (renderer.messages.find((x) => x._id === message._id)) return;
|
||||||
if (!renderer.state.atBottom) return;
|
if (!renderer.atBottom) return;
|
||||||
|
|
||||||
let messages = [...renderer.state.messages, message];
|
let messages = [...renderer.messages, message];
|
||||||
let atTop = renderer.state.atTop;
|
let atTop = renderer.atTop;
|
||||||
if (messages.length > 150) {
|
if (messages.length > 150) {
|
||||||
messages = messages.slice(messages.length - 150);
|
messages = messages.slice(messages.length - 150);
|
||||||
atTop = false;
|
atTop = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.setState(
|
runInAction(() => {
|
||||||
message.channel_id,
|
renderer.messages = messages;
|
||||||
{
|
renderer.atTop = atTop;
|
||||||
...renderer.state,
|
|
||||||
messages,
|
renderer.emitScroll({
|
||||||
atTop,
|
type: "StayAtBottom",
|
||||||
},
|
smooth: SMOOTH_SCROLL_ON_RECEIVE,
|
||||||
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
|
});
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
edit: noopAsync,
|
edit: noopAsync,
|
||||||
delete: async (renderer, id) => {
|
delete: async (renderer, id) => {
|
||||||
const channel = renderer.channel;
|
const channel = renderer.channel;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
if (renderer.state.type !== "RENDER") return;
|
if (renderer.state !== "RENDER") return;
|
||||||
|
|
||||||
const messages = [...renderer.state.messages];
|
const index = renderer.messages.findIndex((x) => x._id === id);
|
||||||
const index = messages.findIndex((x) => x._id === id);
|
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
messages.splice(index, 1);
|
runInAction(() => {
|
||||||
|
renderer.messages.splice(index, 1);
|
||||||
renderer.setState(
|
renderer.emitScroll({ type: "StayAtBottom" });
|
||||||
channel,
|
});
|
||||||
{
|
|
||||||
...renderer.state,
|
|
||||||
messages,
|
|
||||||
},
|
|
||||||
{ type: "StayAtBottom" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadTop: async (renderer, generateScroll) => {
|
loadTop: async (renderer, generateScroll) => {
|
||||||
const channel = renderer.channel;
|
const channel = renderer.channel;
|
||||||
if (!channel) return;
|
if (!channel) return true;
|
||||||
|
|
||||||
const state = renderer.state;
|
if (renderer.state !== "RENDER") return true;
|
||||||
if (state.type !== "RENDER") return;
|
if (renderer.atTop) return true;
|
||||||
if (state.atTop) return;
|
|
||||||
|
|
||||||
const { messages: data } = await renderer
|
const { messages: data } =
|
||||||
.client!.channels.get(channel)!
|
await renderer.channel.fetchMessagesWithUsers({
|
||||||
.fetchMessagesWithUsers({
|
before: renderer.messages[0]._id,
|
||||||
before: state.messages[0]._id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length === 0) {
|
runInAction(() => {
|
||||||
return renderer.setState(channel, {
|
if (data.length === 0) {
|
||||||
...state,
|
renderer.atTop = true;
|
||||||
atTop: true,
|
return;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data.reverse();
|
data.reverse();
|
||||||
let messages = [...data, ...state.messages];
|
renderer.messages = [...data, ...renderer.messages];
|
||||||
|
|
||||||
let atTop = false;
|
if (data.length < 50) {
|
||||||
if (data.length < 50) {
|
renderer.atTop = true;
|
||||||
atTop = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let atBottom = state.atBottom;
|
if (renderer.messages.length > 150) {
|
||||||
if (messages.length > 150) {
|
renderer.messages = renderer.messages.slice(0, 150);
|
||||||
messages = messages.slice(0, 150);
|
renderer.atBottom = false;
|
||||||
atBottom = false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
renderer.setState(
|
renderer.emitScroll(
|
||||||
channel,
|
generateScroll(
|
||||||
{ ...state, atTop, atBottom, messages },
|
renderer.messages[renderer.messages.length - 1]._id,
|
||||||
generateScroll(messages[messages.length - 1]._id),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
loadBottom: async (renderer, generateScroll) => {
|
loadBottom: async (renderer, generateScroll) => {
|
||||||
const channel = renderer.channel;
|
const channel = renderer.channel;
|
||||||
if (!channel) return;
|
if (!channel) return true;
|
||||||
|
|
||||||
const state = renderer.state;
|
if (renderer.state !== "RENDER") return true;
|
||||||
if (state.type !== "RENDER") return;
|
if (renderer.atBottom) return true;
|
||||||
if (state.atBottom) return;
|
|
||||||
|
|
||||||
const { messages: data } = await renderer
|
const { messages: data } =
|
||||||
.client!.channels.get(channel)!
|
await renderer.channel.fetchMessagesWithUsers({
|
||||||
.fetchMessagesWithUsers({
|
after: renderer.messages[renderer.messages.length - 1]._id,
|
||||||
after: state.messages[state.messages.length - 1]._id,
|
|
||||||
sort: "Oldest",
|
sort: "Oldest",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length === 0) {
|
runInAction(() => {
|
||||||
return renderer.setState(channel, {
|
if (data.length === 0) {
|
||||||
...state,
|
renderer.atBottom = true;
|
||||||
atBottom: true,
|
return;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let messages = [...state.messages, ...data];
|
renderer.messages.splice(renderer.messages.length, 0, ...data);
|
||||||
|
|
||||||
let atBottom = false;
|
if (data.length < 50) {
|
||||||
if (data.length < 50) {
|
renderer.atBottom = true;
|
||||||
atBottom = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let atTop = state.atTop;
|
if (renderer.messages.length > 150) {
|
||||||
if (messages.length > 150) {
|
renderer.messages.splice(0, renderer.messages.length - 150);
|
||||||
messages = messages.slice(messages.length - 150);
|
renderer.atTop = false;
|
||||||
atTop = false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
renderer.setState(
|
renderer.emitScroll(generateScroll(renderer.messages[0]._id));
|
||||||
channel,
|
});
|
||||||
{ ...state, atTop, atBottom, messages },
|
|
||||||
generateScroll(messages[0]._id),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import { Message } from "revolt.js/dist/maps/Messages";
|
||||||
|
|
||||||
import { SingletonRenderer } from "./Singleton";
|
import { ChannelRenderer } from "./Singleton";
|
||||||
|
|
||||||
export type ScrollState =
|
export type ScrollState =
|
||||||
| { type: "Free" }
|
| { type: "Free" }
|
||||||
|
@ -23,26 +23,25 @@ export type RenderState =
|
||||||
|
|
||||||
export interface RendererRoutines {
|
export interface RendererRoutines {
|
||||||
init: (
|
init: (
|
||||||
renderer: SingletonRenderer,
|
renderer: ChannelRenderer,
|
||||||
id: string,
|
|
||||||
message?: string,
|
message?: string,
|
||||||
smooth?: boolean,
|
smooth?: boolean,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
|
receive: (renderer: ChannelRenderer, message: Message) => Promise<void>;
|
||||||
edit: (
|
edit: (
|
||||||
renderer: SingletonRenderer,
|
renderer: ChannelRenderer,
|
||||||
id: string,
|
id: string,
|
||||||
partial: Partial<Message>,
|
partial: Partial<Message>,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
|
delete: (renderer: ChannelRenderer, id: string) => Promise<void>;
|
||||||
|
|
||||||
loadTop: (
|
loadTop: (
|
||||||
renderer: SingletonRenderer,
|
renderer: ChannelRenderer,
|
||||||
generateScroll: (end: string) => ScrollState,
|
generateScroll: (end: string) => ScrollState,
|
||||||
) => Promise<void>;
|
) => Promise<void | true>;
|
||||||
loadBottom: (
|
loadBottom: (
|
||||||
renderer: SingletonRenderer,
|
renderer: ChannelRenderer,
|
||||||
generateScroll: (start: string) => ScrollState,
|
generateScroll: (start: string) => ScrollState,
|
||||||
) => Promise<void>;
|
) => Promise<void | true>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,9 +88,9 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
||||||
<ChannelMain>
|
<ChannelMain>
|
||||||
<ChannelContent>
|
<ChannelContent>
|
||||||
<VoiceHeader id={id} />
|
<VoiceHeader id={id} />
|
||||||
<MessageArea id={id} />
|
<MessageArea channel={channel} />
|
||||||
<TypingIndicator channel={channel} />
|
<TypingIndicator channel={channel} />
|
||||||
<JumpToBottom id={id} />
|
<JumpToBottom channel={channel} />
|
||||||
<MessageBox channel={channel} />
|
<MessageBox channel={channel} />
|
||||||
</ChannelContent>
|
</ChannelContent>
|
||||||
{!isTouchscreenDevice && showMembers && (
|
{!isTouchscreenDevice && showMembers && (
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
import { getChannelName } from "../../../context/revoltjs/util";
|
import { getChannelName } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
const StartBase = styled.div`
|
const StartBase = styled.div`
|
||||||
|
@ -22,14 +22,10 @@ const StartBase = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
channel: Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(({ id }: Props) => {
|
export default observer(({ channel }: Props) => {
|
||||||
const client = useClient();
|
|
||||||
const channel = client.channels.get(id);
|
|
||||||
if (!channel) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StartBase>
|
<StartBase>
|
||||||
<h1>{getChannelName(channel, true)}</h1>
|
<h1>{getChannelName(channel, true)}</h1>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { animateScroll } from "react-scroll";
|
import { animateScroll } from "react-scroll";
|
||||||
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import useResizeObserver from "use-resize-observer";
|
import useResizeObserver from "use-resize-observer";
|
||||||
|
|
||||||
|
@ -15,13 +18,12 @@ import {
|
||||||
|
|
||||||
import { defer } from "../../../lib/defer";
|
import { defer } from "../../../lib/defer";
|
||||||
import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
|
import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
|
||||||
import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../lib/renderer/Singleton";
|
||||||
import { RenderState, ScrollState } from "../../../lib/renderer/types";
|
import { ScrollState } from "../../../lib/renderer/types";
|
||||||
|
|
||||||
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
||||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../context/revoltjs/RevoltClient";
|
||||||
|
@ -49,15 +51,14 @@ const Area = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
channel: Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageAreaWidthContext = createContext(0);
|
export const MessageAreaWidthContext = createContext(0);
|
||||||
export const MESSAGE_AREA_PADDING = 82;
|
export const MESSAGE_AREA_PADDING = 82;
|
||||||
|
|
||||||
export function MessageArea({ id }: Props) {
|
export const MessageArea = observer(({ channel }: Props) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
|
||||||
const status = useContext(StatusContext);
|
const status = useContext(StatusContext);
|
||||||
const { focusTaken } = useContext(IntermediateContext);
|
const { focusTaken } = useContext(IntermediateContext);
|
||||||
|
|
||||||
|
@ -70,69 +71,75 @@ export function MessageArea({ id }: Props) {
|
||||||
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
|
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
|
||||||
|
|
||||||
// ? Current channel state.
|
// ? Current channel state.
|
||||||
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
const renderer = getRenderer(channel);
|
||||||
|
|
||||||
// ? useRef to avoid re-renders
|
// ? useRef to avoid re-renders
|
||||||
const scrollState = useRef<ScrollState>({ type: "Free" });
|
const scrollState = useRef<ScrollState>({ type: "Free" });
|
||||||
|
|
||||||
const setScrollState = useCallback((v: ScrollState) => {
|
const setScrollState = useCallback(
|
||||||
if (v.type === "StayAtBottom") {
|
(v: ScrollState) => {
|
||||||
if (scrollState.current.type === "Bottom" || atBottom()) {
|
if (v.type === "StayAtBottom") {
|
||||||
scrollState.current = {
|
if (scrollState.current.type === "Bottom" || atBottom()) {
|
||||||
type: "ScrollToBottom",
|
scrollState.current = {
|
||||||
smooth: v.smooth,
|
type: "ScrollToBottom",
|
||||||
};
|
smooth: v.smooth,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
scrollState.current = { type: "Free" };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
scrollState.current = { type: "Free" };
|
scrollState.current = v;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
scrollState.current = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
defer(() => {
|
defer(() => {
|
||||||
if (scrollState.current.type === "ScrollToBottom") {
|
if (scrollState.current.type === "ScrollToBottom") {
|
||||||
setScrollState({
|
setScrollState({
|
||||||
type: "Bottom",
|
type: "Bottom",
|
||||||
scrollingUntil: +new Date() + 150,
|
scrollingUntil: +new Date() + 150,
|
||||||
});
|
});
|
||||||
|
|
||||||
animateScroll.scrollToBottom({
|
animateScroll.scrollToBottom({
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
duration: scrollState.current.smooth ? 150 : 0,
|
duration: scrollState.current.smooth ? 150 : 0,
|
||||||
});
|
});
|
||||||
} else if (scrollState.current.type === "ScrollToView") {
|
} else if (scrollState.current.type === "ScrollToView") {
|
||||||
document
|
document
|
||||||
.getElementById(scrollState.current.id)
|
.getElementById(scrollState.current.id)
|
||||||
?.scrollIntoView({ block: "center" });
|
?.scrollIntoView({ block: "center" });
|
||||||
|
|
||||||
setScrollState({ type: "Free" });
|
setScrollState({ type: "Free" });
|
||||||
} else if (scrollState.current.type === "OffsetTop") {
|
} else if (scrollState.current.type === "OffsetTop") {
|
||||||
animateScroll.scrollTo(
|
animateScroll.scrollTo(
|
||||||
Math.max(
|
Math.max(
|
||||||
101,
|
101,
|
||||||
ref.current
|
ref.current
|
||||||
? ref.current.scrollTop +
|
? ref.current.scrollTop +
|
||||||
(ref.current.scrollHeight -
|
(ref.current.scrollHeight -
|
||||||
scrollState.current.previousHeight)
|
scrollState.current.previousHeight)
|
||||||
: 101,
|
: 101,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
container: ref.current,
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setScrollState({ type: "Free" });
|
||||||
|
} else if (scrollState.current.type === "ScrollTop") {
|
||||||
|
animateScroll.scrollTo(scrollState.current.y, {
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
setScrollState({ type: "Free" });
|
setScrollState({ type: "Free" });
|
||||||
} else if (scrollState.current.type === "ScrollTop") {
|
}
|
||||||
animateScroll.scrollTo(scrollState.current.y, {
|
|
||||||
container: ref.current,
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
setScrollState({ type: "Free" });
|
defer(() => renderer.complete());
|
||||||
}
|
});
|
||||||
});
|
},
|
||||||
}, []);
|
// eslint-disable-next-line
|
||||||
|
[scrollState],
|
||||||
|
);
|
||||||
|
|
||||||
// ? Determine if we are at the bottom of the scroll container.
|
// ? Determine if we are at the bottom of the scroll container.
|
||||||
// -> https://stackoverflow.com/a/44893438
|
// -> https://stackoverflow.com/a/44893438
|
||||||
|
@ -155,35 +162,36 @@ export function MessageArea({ id }: Props) {
|
||||||
}, [setScrollState]);
|
}, [setScrollState]);
|
||||||
|
|
||||||
// ? Handle events from renderer.
|
// ? Handle events from renderer.
|
||||||
useEffect(() => {
|
useLayoutEffect(
|
||||||
SingletonMessageRenderer.addListener("state", setState);
|
() => setScrollState(renderer.scrollState),
|
||||||
return () => SingletonMessageRenderer.removeListener("state", setState);
|
// eslint-disable-next-line
|
||||||
}, []);
|
[renderer.scrollState],
|
||||||
|
);
|
||||||
useEffect(() => {
|
|
||||||
SingletonMessageRenderer.addListener("scroll", setScrollState);
|
|
||||||
return () =>
|
|
||||||
SingletonMessageRenderer.removeListener("scroll", setScrollState);
|
|
||||||
}, [scrollState, setScrollState]);
|
|
||||||
|
|
||||||
// ? Load channel initially.
|
// ? Load channel initially.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message) return;
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id]);
|
}, []);
|
||||||
|
|
||||||
// ? If message present or changes, load it as well.
|
// ? If message present or changes, load it as well.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message) {
|
if (message) {
|
||||||
setHighlight(message);
|
setHighlight(message);
|
||||||
SingletonMessageRenderer.init(id, message);
|
renderer.init(message);
|
||||||
|
|
||||||
const channel = client.channels.get(id);
|
if (channel.channel_type === "TextChannel") {
|
||||||
if (channel?.channel_type === "TextChannel") {
|
history.push(
|
||||||
history.push(`/server/${channel.server_id}/channel/${id}`);
|
`/server/${channel.server_id}/channel/${channel._id}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
history.push(`/channel/${id}`);
|
history.push(`/channel/${channel._id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -193,20 +201,20 @@ export function MessageArea({ id }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ClientStatus.ONLINE:
|
case ClientStatus.ONLINE:
|
||||||
if (state.type === "WAITING_FOR_NETWORK") {
|
if (renderer.state === "WAITING_FOR_NETWORK") {
|
||||||
SingletonMessageRenderer.init(id);
|
renderer.init();
|
||||||
} else {
|
} else {
|
||||||
SingletonMessageRenderer.reloadStale(id);
|
renderer.reloadStale();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ClientStatus.OFFLINE:
|
case ClientStatus.OFFLINE:
|
||||||
case ClientStatus.DISCONNECTED:
|
case ClientStatus.DISCONNECTED:
|
||||||
case ClientStatus.CONNECTING:
|
case ClientStatus.CONNECTING:
|
||||||
SingletonMessageRenderer.markStale();
|
renderer.markStale();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [id, status, state]);
|
}, [renderer, status]);
|
||||||
|
|
||||||
// ? When the container is scrolled.
|
// ? When the container is scrolled.
|
||||||
// ? Also handle StayAtBottom
|
// ? Also handle StayAtBottom
|
||||||
|
@ -238,17 +246,17 @@ export function MessageArea({ id }: Props) {
|
||||||
|
|
||||||
async function onScroll() {
|
async function onScroll() {
|
||||||
if (atTop(100)) {
|
if (atTop(100)) {
|
||||||
SingletonMessageRenderer.loadTop(ref.current!);
|
renderer.loadTop(ref.current!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atBottom(100)) {
|
if (atBottom(100)) {
|
||||||
SingletonMessageRenderer.loadBottom(ref.current!);
|
renderer.loadBottom(ref.current!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current.addEventListener("scroll", onScroll);
|
current.addEventListener("scroll", onScroll);
|
||||||
return () => current.removeEventListener("scroll", onScroll);
|
return () => current.removeEventListener("scroll", onScroll);
|
||||||
}, [ref]);
|
}, [ref, renderer]);
|
||||||
|
|
||||||
// ? Scroll down whenever the message area resizes.
|
// ? Scroll down whenever the message area resizes.
|
||||||
const stbOnResize = useCallback(() => {
|
const stbOnResize = useCallback(() => {
|
||||||
|
@ -277,36 +285,37 @@ export function MessageArea({ id }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function keyUp(e: KeyboardEvent) {
|
function keyUp(e: KeyboardEvent) {
|
||||||
if (e.key === "Escape" && !focusTaken) {
|
if (e.key === "Escape" && !focusTaken) {
|
||||||
SingletonMessageRenderer.jumpToBottom(id, true);
|
renderer.jumpToBottom(true);
|
||||||
internalEmit("TextArea", "focus", "message");
|
internalEmit("TextArea", "focus", "message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.addEventListener("keyup", keyUp);
|
document.body.addEventListener("keyup", keyUp);
|
||||||
return () => document.body.removeEventListener("keyup", keyUp);
|
return () => document.body.removeEventListener("keyup", keyUp);
|
||||||
}, [id, ref, focusTaken]);
|
}, [renderer, ref, focusTaken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageAreaWidthContext.Provider
|
<MessageAreaWidthContext.Provider
|
||||||
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
||||||
<Area ref={ref}>
|
<Area ref={ref}>
|
||||||
<div>
|
<div>
|
||||||
{state.type === "LOADING" && <Preloader type="ring" />}
|
{renderer.state === "LOADING" && <Preloader type="ring" />}
|
||||||
{state.type === "WAITING_FOR_NETWORK" && (
|
{renderer.state === "WAITING_FOR_NETWORK" && (
|
||||||
<RequiresOnline>
|
<RequiresOnline>
|
||||||
<Preloader type="ring" />
|
<Preloader type="ring" />
|
||||||
</RequiresOnline>
|
</RequiresOnline>
|
||||||
)}
|
)}
|
||||||
{state.type === "RENDER" && (
|
{renderer.state === "RENDER" && (
|
||||||
<MessageRenderer
|
<MessageRenderer
|
||||||
id={id}
|
renderer={renderer}
|
||||||
state={state}
|
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{state.type === "EMPTY" && <ConversationStart id={id} />}
|
{renderer.state === "EMPTY" && (
|
||||||
|
<ConversationStart channel={channel} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Area>
|
</Area>
|
||||||
</MessageAreaWidthContext.Provider>
|
</MessageAreaWidthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { X } from "@styled-icons/boxicons-regular";
|
import { X } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
import { RelationshipStatus } from "revolt-api/types/Users";
|
||||||
import { SYSTEM_USER_ID } from "revolt.js";
|
import { SYSTEM_USER_ID } from "revolt.js";
|
||||||
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
|
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 { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
||||||
import { RenderState } from "../../../lib/renderer/types";
|
import { ChannelRenderer } from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||||
|
@ -29,10 +30,9 @@ import ConversationStart from "./ConversationStart";
|
||||||
import MessageEditor from "./MessageEditor";
|
import MessageEditor from "./MessageEditor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
|
||||||
state: RenderState;
|
|
||||||
highlight?: string;
|
highlight?: string;
|
||||||
queue: QueuedMessage[];
|
queue: QueuedMessage[];
|
||||||
|
renderer: ChannelRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockedMessage = styled.div`
|
const BlockedMessage = styled.div`
|
||||||
|
@ -46,9 +46,7 @@ const BlockedMessage = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function MessageRenderer({ id, state, queue, highlight }: Props) {
|
const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => {
|
||||||
if (state.type !== "RENDER") return null;
|
|
||||||
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const userId = client.user!._id;
|
const userId = client.user!._id;
|
||||||
|
|
||||||
|
@ -60,10 +58,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function editLast() {
|
function editLast() {
|
||||||
if (state.type !== "RENDER") return;
|
if (renderer.state !== "RENDER") return;
|
||||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
for (let i = renderer.messages.length - 1; i >= 0; i--) {
|
||||||
if (state.messages[i].author_id === userId) {
|
if (renderer.messages[i].author_id === userId) {
|
||||||
setEditing(state.messages[i]._id);
|
setEditing(renderer.messages[i]._id);
|
||||||
internalEmit("MessageArea", "jump_to_bottom");
|
internalEmit("MessageArea", "jump_to_bottom");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -80,13 +78,13 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => subs.forEach((unsub) => unsub());
|
return () => subs.forEach((unsub) => unsub());
|
||||||
}, [state.messages, state.type, userId]);
|
}, [renderer.messages, renderer.state, userId]);
|
||||||
|
|
||||||
const render: Children[] = [];
|
const render: Children[] = [];
|
||||||
let previous: MessageI | undefined;
|
let previous: MessageI | undefined;
|
||||||
|
|
||||||
if (state.atTop) {
|
if (renderer.atTop) {
|
||||||
render.push(<ConversationStart id={id} />);
|
render.push(<ConversationStart channel={renderer.channel} />);
|
||||||
} else {
|
} else {
|
||||||
render.push(
|
render.push(
|
||||||
<RequiresOnline>
|
<RequiresOnline>
|
||||||
|
@ -133,7 +131,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
blocked = 0;
|
blocked = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const message of state.messages) {
|
for (const message of renderer.messages) {
|
||||||
if (previous) {
|
if (previous) {
|
||||||
compare(
|
compare(
|
||||||
message._id,
|
message._id,
|
||||||
|
@ -183,10 +181,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
|
|
||||||
if (blocked > 0) pushBlocked();
|
if (blocked > 0) pushBlocked();
|
||||||
|
|
||||||
const nonces = state.messages.map((x) => x.nonce);
|
const nonces = renderer.messages.map((x) => x.nonce);
|
||||||
if (state.atBottom) {
|
if (renderer.atBottom) {
|
||||||
for (const msg of queue) {
|
for (const msg of queue) {
|
||||||
if (msg.channel !== id) continue;
|
if (msg.channel !== renderer.channel._id) continue;
|
||||||
if (nonces.includes(msg.id)) continue;
|
if (nonces.includes(msg.id)) continue;
|
||||||
|
|
||||||
if (previous) {
|
if (previous) {
|
||||||
|
@ -222,7 +220,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{render}</>;
|
return <>{render}</>;
|
||||||
}
|
});
|
||||||
|
|
||||||
export default memo(
|
export default memo(
|
||||||
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
|
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
|
||||||
|
|
Loading…
Reference in a new issue