mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 08:38:37 +00:00
Manage state per channel. Closes #2
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user