mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-25 23:22:06 -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 { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import {
|
||||
SingletonMessageRenderer,
|
||||
getRenderer,
|
||||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
|
@ -122,6 +122,8 @@ export default observer(({ channel }: Props) => {
|
|||
const client = useContext(AppContext);
|
||||
const translate = useTranslation();
|
||||
|
||||
const renderer = getRenderer(channel);
|
||||
|
||||
if (!(channel.permission & ChannelPermission.SendMessage)) {
|
||||
return (
|
||||
<Base>
|
||||
|
@ -213,12 +215,7 @@ export default observer(({ channel }: Props) => {
|
|||
},
|
||||
});
|
||||
|
||||
defer(() =>
|
||||
SingletonMessageRenderer.jumpToBottom(
|
||||
channel._id,
|
||||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
),
|
||||
);
|
||||
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
|
||||
|
||||
try {
|
||||
await channel.sendMessage({
|
||||
|
@ -405,7 +402,7 @@ export default observer(({ channel }: Props) => {
|
|||
}}
|
||||
/>
|
||||
<ReplyBar
|
||||
channel={channel._id}
|
||||
channel={channel}
|
||||
replies={replies}
|
||||
setReplies={setReplies}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,7 @@ import styled, { css } from "styled-components";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useRenderState } from "../../../../lib/renderer/Singleton";
|
||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||
|
||||
import Markdown from "../../../markdown/Markdown";
|
||||
import UserShort from "../../user/UserShort";
|
||||
|
@ -134,8 +134,8 @@ export const ReplyBase = styled.div<{
|
|||
`;
|
||||
|
||||
export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||
const view = useRenderState(channel._id);
|
||||
if (view?.type !== "RENDER") return null;
|
||||
const view = getRenderer(channel);
|
||||
if (view.state !== "RENDER") return null;
|
||||
|
||||
const [message, setMessage] = useState<Message | undefined>(undefined);
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
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 { Text } from "preact-i18n";
|
||||
|
||||
import {
|
||||
SingletonMessageRenderer,
|
||||
useRenderState,
|
||||
} from "../../../../lib/renderer/Singleton";
|
||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||
|
||||
const Bar = styled.div`
|
||||
z-index: 10;
|
||||
|
@ -51,14 +50,13 @@ const Bar = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
export default function JumpToBottom({ id }: { id: string }) {
|
||||
const view = useRenderState(id);
|
||||
if (!view || view.type !== "RENDER" || view.atBottom) return null;
|
||||
export default observer(({ channel }: { channel: Channel }) => {
|
||||
const renderer = getRenderer(channel);
|
||||
if (renderer.state !== "RENDER" || renderer.atBottom) return null;
|
||||
|
||||
return (
|
||||
<Bar>
|
||||
<div
|
||||
onClick={() => SingletonMessageRenderer.jumpToBottom(id, true)}>
|
||||
<div onClick={() => renderer.jumpToBottom(true)}>
|
||||
<div>
|
||||
<Text id="app.main.channel.misc.viewing_old" />
|
||||
</div>
|
||||
|
@ -69,4 +67,4 @@ export default function JumpToBottom({ id }: { id: string }) {
|
|||
</div>
|
||||
</Bar>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<Reply[]>;
|
||||
}
|
||||
|
@ -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 (
|
||||
<div>
|
||||
|
|
|
@ -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 (
|
||||
<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
|
||||
|
@ -22,20 +24,26 @@ export function ChannelDebugInfo({ id }: Props) {
|
|||
Channel Info
|
||||
</span>
|
||||
<p style={{ fontSize: "10px", userSelect: "text" }}>
|
||||
State: <b>{view.type}</b> <br />
|
||||
{view.type === "RENDER" && view.messages.length > 0 && (
|
||||
State: <b>{renderer.state}</b> <br />
|
||||
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:{" "}
|
||||
<b>
|
||||
{view.messages[view.messages.length - 1]._id}
|
||||
{
|
||||
renderer.messages[renderer.messages.length - 1]
|
||||
._id
|
||||
}
|
||||
</b>{" "}
|
||||
<br />
|
||||
At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
|
||||
At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
|
||||
At Top: <b>{renderer.atTop ? "Yes" : "No"}</b> <br />
|
||||
At Bottom: <b>{renderer.atBottom ? "Yes" : "No"}</b>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -90,7 +90,7 @@ export const GroupMemberSidebar = observer(
|
|||
return (
|
||||
<GenericSidebarBase>
|
||||
<GenericSidebarList>
|
||||
<ChannelDebugInfo id={channel._id} />
|
||||
<ChannelDebugInfo channel={channel} />
|
||||
<Search channel={channel} />
|
||||
|
||||
{/*voiceActive && voiceParticipants.length !== 0 && (
|
||||
|
@ -202,7 +202,7 @@ export const ServerMemberSidebar = observer(
|
|||
return (
|
||||
<GenericSidebarBase>
|
||||
<GenericSidebarList>
|
||||
<ChannelDebugInfo id={channel._id} />
|
||||
<ChannelDebugInfo channel={channel} />
|
||||
<Search channel={channel} />
|
||||
<div>{users.length === 0 && <Preloader type="ring" />}</div>
|
||||
{users.length > 0 && (
|
||||
|
|
|
@ -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);
|
||||
})();
|
||||
}, []);
|
||||
|
|
|
@ -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<boolean> = null;
|
||||
atBottom: Nullable<boolean> = 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<string, ChannelRenderer> = {};
|
||||
|
||||
export function useRenderState(id: string) {
|
||||
const [state, setState] = useState<Readonly<RenderState>>(
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<void>;
|
||||
|
||||
receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
|
||||
receive: (renderer: ChannelRenderer, message: Message) => Promise<void>;
|
||||
edit: (
|
||||
renderer: SingletonRenderer,
|
||||
renderer: ChannelRenderer,
|
||||
id: string,
|
||||
partial: Partial<Message>,
|
||||
) => Promise<void>;
|
||||
delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
|
||||
delete: (renderer: ChannelRenderer, id: string) => Promise<void>;
|
||||
|
||||
loadTop: (
|
||||
renderer: SingletonRenderer,
|
||||
renderer: ChannelRenderer,
|
||||
generateScroll: (end: string) => ScrollState,
|
||||
) => Promise<void>;
|
||||
) => Promise<void | true>;
|
||||
loadBottom: (
|
||||
renderer: SingletonRenderer,
|
||||
renderer: ChannelRenderer,
|
||||
generateScroll: (start: string) => ScrollState,
|
||||
) => Promise<void>;
|
||||
) => Promise<void | true>;
|
||||
}
|
||||
|
|
|
@ -88,9 +88,9 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
<ChannelMain>
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={id} />
|
||||
<MessageArea id={id} />
|
||||
<MessageArea channel={channel} />
|
||||
<TypingIndicator channel={channel} />
|
||||
<JumpToBottom id={id} />
|
||||
<JumpToBottom channel={channel} />
|
||||
<MessageBox channel={channel} />
|
||||
</ChannelContent>
|
||||
{!isTouchscreenDevice && showMembers && (
|
||||
|
|
|
@ -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 (
|
||||
<StartBase>
|
||||
<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 { 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<HTMLDivElement>({ ref });
|
||||
|
||||
// ? Current channel state.
|
||||
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
||||
const renderer = getRenderer(channel);
|
||||
|
||||
// ? useRef to avoid re-renders
|
||||
const scrollState = useRef<ScrollState>({ 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 (
|
||||
<MessageAreaWidthContext.Provider
|
||||
value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
||||
<Area ref={ref}>
|
||||
<div>
|
||||
{state.type === "LOADING" && <Preloader type="ring" />}
|
||||
{state.type === "WAITING_FOR_NETWORK" && (
|
||||
{renderer.state === "LOADING" && <Preloader type="ring" />}
|
||||
{renderer.state === "WAITING_FOR_NETWORK" && (
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>
|
||||
)}
|
||||
{state.type === "RENDER" && (
|
||||
{renderer.state === "RENDER" && (
|
||||
<MessageRenderer
|
||||
id={id}
|
||||
state={state}
|
||||
renderer={renderer}
|
||||
highlight={highlight}
|
||||
/>
|
||||
)}
|
||||
{state.type === "EMPTY" && <ConversationStart id={id} />}
|
||||
{renderer.state === "EMPTY" && (
|
||||
<ConversationStart channel={channel} />
|
||||
)}
|
||||
</div>
|
||||
</Area>
|
||||
</MessageAreaWidthContext.Provider>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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(<ConversationStart id={id} />);
|
||||
if (renderer.atTop) {
|
||||
render.push(<ConversationStart channel={renderer.channel} />);
|
||||
} else {
|
||||
render.push(
|
||||
<RequiresOnline>
|
||||
|
@ -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<Omit<Props, "queue">>(MessageRenderer, (state) => {
|
||||
|
|
Loading…
Reference in a new issue