Work on channels, render content of messages.

This commit is contained in:
Paul
2021-06-20 17:31:53 +01:00
parent 89f8ab2694
commit d0b9cf9090
30 changed files with 1415 additions and 58 deletions

View File

@@ -11,6 +11,7 @@ import RightSidebar from "../components/navigation/RightSidebar";
import Home from './home/Home';
import Friends from "./friends/Friends";
import Channel from "./channels/Channel";
import Settings from './settings/Settings';
import Developer from "./developer/Developer";
import ServerSettings from "./settings/ServerSettings";
@@ -40,6 +41,11 @@ export default function App() {
<Route path="/server/:server/settings" component={ServerSettings} />
<Route path="/channel/:channel/settings/:page" component={ChannelSettings} />
<Route path="/channel/:channel/settings" component={ChannelSettings} />
<Route path="/channel/:channel/message/:message" component={Channel} />
<Route path="/server/:server/channel/:channel" component={Channel} />
<Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} />
<Route path="/settings/:page" component={Settings} />
<Route path="/settings" component={Settings} />
@@ -57,17 +63,7 @@ export default function App() {
/**
*
* <Route path="/channel/:channel/message/:message">
<ChannelWrapper />
</Route>
<Route path="/server/:server/channel/:channel">
<ChannelWrapper />
</Route>
<Route path="/server/:server" />
<Route path="/channel/:channel">
<ChannelWrapper />
</Route>
*
<Route path="/open/:id">
<Open />

View File

@@ -0,0 +1,44 @@
import styled from "styled-components";
import { useParams } from "react-router-dom";
import Header from "../../components/ui/Header";
import { useRenderState } from "../../lib/renderer/Singleton";
import { useChannel, useForceUpdate, useUsers } from "../../context/revoltjs/hooks";
import { MessageArea } from "./messaging/MessageArea";
const ChannelMain = styled.div`
flex-grow: 1;
display: flex;
min-height: 0;
overflow: hidden;
flex-direction: row;
`;
const ChannelContent = styled.div`
flex-grow: 1;
display: flex;
overflow: hidden;
flex-direction: column;
`;
export default function Channel() {
const { channel: id } = useParams<{ channel: string }>();
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
// const view = useRenderState(id);
return (
<>
<Header placement="primary">
Channel
</Header>
<ChannelMain>
<ChannelContent>
<MessageArea id={id} />
</ChannelContent>
</ChannelMain>
</>
)
}

View File

@@ -0,0 +1,38 @@
import { Text } from "preact-i18n";
import styled from "styled-components";
import { getChannelName } from "../../../context/revoltjs/util";
import { useChannel, useForceUpdate } from "../../../context/revoltjs/hooks";
const StartBase = styled.div`
margin: 18px 16px 10px 16px;
h1 {
font-size: 23px;
margin: 0 0 8px 0;
}
h4 {
font-weight: 400;
margin: 0;
font-size: 14px;
}
`;
interface Props {
id: string;
}
export default function ConversationStart({ id }: Props) {
const ctx = useForceUpdate();
const channel = useChannel(id, ctx);
if (!channel) return null;
return (
<StartBase>
<h1>{ getChannelName(ctx.client, channel, true) }</h1>
<h4>
<Text id="app.main.channel.start.group" />
</h4>
</StartBase>
);
}

View File

@@ -0,0 +1,231 @@
import styled from "styled-components";
import { createContext } from "preact";
import { animateScroll } from "react-scroll";
import MessageRenderer from "./MessageRenderer";
import ConversationStart from './ConversationStart';
import useResizeObserver from "use-resize-observer";
import Preloader from "../../../components/ui/Preloader";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { RenderState, ScrollState } from "../../../lib/renderer/types";
import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
const Area = styled.div`
height: 100%;
flex-grow: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
word-break: break-word;
> div {
display: flex;
min-height: 100%;
flex-direction: column;
justify-content: flex-end;
}
`;
interface Props {
id: string;
}
export const MessageAreaWidthContext = createContext(0);
export const MESSAGE_AREA_PADDING = 82;
export function MessageArea({ id }: Props) {
const status = useContext(StatusContext);
const { focusTaken } = useContext(IntermediateContext);
// ? This is the scroll container.
const ref = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
// ? Current channel state.
const [state, setState] = useState<RenderState>({ type: "LOADING" });
// ? Hook-based scrolling mechanism.
const [scrollState, setSS] = useState<ScrollState>({
type: "Free"
});
const setScrollState = (v: ScrollState) => {
if (v.type === 'StayAtBottom') {
if (scrollState.type === 'Bottom' || atBottom()) {
setSS({ type: 'ScrollToBottom', smooth: v.smooth });
} else {
setSS({ type: 'Free' });
}
} else {
setSS(v);
}
}
// ? Determine if we are at the bottom of the scroll container.
// -> https://stackoverflow.com/a/44893438
// By default, we assume we are at the bottom, i.e. when we first load.
const atBottom = (offset = 0) =>
ref.current
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
offset <=
ref.current.clientHeight
: true;
const atTop = (offset = 0) => ref.current.scrollTop <= offset;
// ? 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 ]);
// ? Load channel initially.
useEffect(() => {
SingletonMessageRenderer.init(id);
}, [ id ]);
// ? If we are waiting for network, try again.
useEffect(() => {
switch (status) {
case ClientStatus.ONLINE:
if (state.type === 'WAITING_FOR_NETWORK') {
SingletonMessageRenderer.init(id);
} else {
SingletonMessageRenderer.reloadStale(id);
}
break;
case ClientStatus.OFFLINE:
case ClientStatus.DISCONNECTED:
case ClientStatus.CONNECTING:
SingletonMessageRenderer.markStale();
break;
}
}, [ status, state ]);
// ? Scroll to the bottom before the browser paints.
useLayoutEffect(() => {
if (scrollState.type === "ScrollToBottom") {
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
animateScroll.scrollToBottom({
container: ref.current,
duration: scrollState.smooth ? 150 : 0
});
} else if (scrollState.type === "OffsetTop") {
animateScroll.scrollTo(
Math.max(
101,
ref.current.scrollTop +
(ref.current.scrollHeight - scrollState.previousHeight)
),
{
container: ref.current,
duration: 0
}
);
setScrollState({ type: "Free" });
} else if (scrollState.type === "ScrollTop") {
animateScroll.scrollTo(scrollState.y, {
container: ref.current,
duration: 0
});
setScrollState({ type: "Free" });
}
}, [scrollState]);
// ? When the container is scrolled.
// ? Also handle StayAtBottom
useEffect(() => {
async function onScroll() {
if (scrollState.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" });
} else if (scrollState.type === "Bottom" && !atBottom()) {
if (scrollState.scrollingUntil && scrollState.scrollingUntil > + new Date()) return;
setScrollState({ type: "Free" });
}
}
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref, scrollState]);
// ? Top and bottom loaders.
useEffect(() => {
async function onScroll() {
if (atTop(100)) {
SingletonMessageRenderer.loadTop(ref.current);
}
if (atBottom(100)) {
SingletonMessageRenderer.loadBottom(ref.current);
}
}
ref.current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll);
}, [ref]);
// ? Scroll down whenever the message area resizes.
function stbOnResize() {
if (!atBottom() && scrollState.type === "Bottom") {
animateScroll.scrollToBottom({
container: ref.current,
duration: 0
});
setScrollState({ type: "Bottom" });
}
}
// ? Scroll down when container resized.
useLayoutEffect(() => {
stbOnResize();
}, [height]);
// ? Scroll down whenever the window resizes.
useLayoutEffect(() => {
document.addEventListener("resize", stbOnResize);
return () => document.removeEventListener("resize", stbOnResize);
}, [ref, scrollState]);
// ? Scroll to bottom when pressing 'Escape'.
useEffect(() => {
function keyUp(e: KeyboardEvent) {
if (e.key === "Escape" && !focusTaken) {
SingletonMessageRenderer.jumpToBottom(id, true);
}
}
document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp);
}, [ref, focusTaken]);
return (
<MessageAreaWidthContext.Provider value={(width ?? 0) - MESSAGE_AREA_PADDING}>
<Area ref={ref}>
<div>
{state.type === "LOADING" && <Preloader />}
{state.type === "WAITING_FOR_NETWORK" && (
<RequiresOnline>
<Preloader />
</RequiresOnline>
)}
{state.type === "RENDER" && (
<MessageRenderer id={id} state={state} />
)}
{state.type === "EMPTY" && <ConversationStart id={id} />}
</div>
</Area>
</MessageAreaWidthContext.Provider>
);
}

View File

@@ -0,0 +1,179 @@
import { decodeTime } from "ulid";
import { useEffect, useState } from "preact/hooks";
import ConversationStart from "./ConversationStart";
import { connectState } from "../../../redux/connector";
import Preloader from "../../../components/ui/Preloader";
import { RenderState } from "../../../lib/renderer/types";
import DateDivider from "../../../components/ui/DateDivider";
import { QueuedMessage } from "../../../redux/reducers/queue";
import { MessageObject } from "../../../context/revoltjs/util";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import { Children } from "../../../types/Preact";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
import Message from "../../../components/common/messaging/Message";
interface Props {
id: string;
state: RenderState;
queue: QueuedMessage[];
}
function MessageRenderer({ id, state, queue }: Props) {
if (state.type !== 'RENDER') return null;
const ctx = useForceUpdate();
const users = useUsers();
const userId = ctx.client.user!._id;
/*
const view = useView(id);*/
const [editing, setEditing] = useState<string | undefined>(undefined);
const stopEditing = () => {
setEditing(undefined);
// InternalEventEmitter.emit("focus_textarea", "message");
};
useEffect(() => {
function editLast() {
if (state.type !== 'RENDER') return;
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author === userId) {
setEditing(state.messages[i]._id);
return;
}
}
}
// InternalEventEmitter.addListener("edit_last", editLast);
// InternalEventEmitter.addListener("edit_message", setEditing);
return () => {
// InternalEventEmitter.removeListener("edit_last", editLast);
// InternalEventEmitter.removeListener("edit_message", setEditing);
};
}, [state.messages]);
let render: Children[] = [],
previous: MessageObject | undefined;
if (state.atTop) {
render.push(<ConversationStart id={id} />);
} else {
render.push(
<RequiresOnline>
<Preloader />
</RequiresOnline>
);
}
let head = true;
function compare(
current: string,
curAuthor: string,
previous: string,
prevAuthor: string
) {
const atime = decodeTime(current),
adate = new Date(atime),
btime = decodeTime(previous),
bdate = new Date(btime);
if (
adate.getFullYear() !== bdate.getFullYear() ||
adate.getMonth() !== bdate.getMonth() ||
adate.getDate() !== bdate.getDate()
) {
render.push(<DateDivider date={adate} />);
}
head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
}
for (const message of state.messages) {
if (previous) {
compare(
message._id,
message.author,
previous._id,
previous.author
);
}
if (message.author === "00000000000000000000000000") {
render.push(<SystemMessage key={message._id} message={message} attachContext />);
} else {
render.push(
<Message message={message}
key={message._id}
head={head}
attachContext />
);
/*render.push(
<Message
editing={editing === message._id ? stopEditing : undefined}
user={users.find(x => x?._id === message.author)}
message={message}
key={message._id}
head={head}
/>
);*/
}
previous = message;
}
const nonces = state.messages.map(x => x.nonce);
if (state.atBottom) {
for (const msg of queue) {
if (msg.channel !== id) continue;
if (nonces.includes(msg.id)) continue;
if (previous) {
compare(
msg.id,
userId as string,
previous._id,
previous.author
);
previous = {
_id: msg.id,
data: { author: userId as string }
} as any;
}
/*render.push(
<Message
user={users.find(x => x?._id === userId)}
message={msg.data}
queued={msg}
key={msg.id}
head={head}
/>
);*/
render.push(
<Message message={msg.data}
key={msg.id}
head={head}
attachContext />
);
}
render.push(<div>end</div>);
} else {
render.push(
<RequiresOnline>
<Preloader />
</RequiresOnline>
);
}
return <>{ render }</>;
}
export default connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
return {
queue: state.queue
};
});

View File

@@ -1,4 +1,5 @@
import { useContext } from "preact/hooks";
import { TextReact } from "../../lib/i18n";
import Header from "../../components/ui/Header";
import PaintCounter from "../../lib/PaintCounter";
import { AppContext } from "../../context/revoltjs/RevoltClient";
@@ -19,6 +20,9 @@ export default function Developer() {
<b>User ID:</b> {client.user!._id} <br/>
<b>Permission against self:</b> {userPermission} <br/>
</div>
<div style={{ padding: "16px" }}>
<TextReact id="login.open_mail_provider" fields={{ provider: <b>GAMING!</b> }} />
</div>
<div style={{ padding: "16px" }}>
{/*<span>
<b>Voice Status:</b> {VoiceStatus[voice.status]}

View File

@@ -28,7 +28,7 @@ export default function ChannelSettings() {
<GenericSettings
pages={[
{
category: <Category variant="uniform" text={getChannelName(ctx.client, channel, [], true)} />,
category: <Category variant="uniform" text={getChannelName(ctx.client, channel, true)} />,
id: 'overview',
icon: <List size={20} strokeWidth={2} />,
title: <Text id="app.settings.channel_pages.overview.title" />

View File

@@ -81,7 +81,7 @@ export function Overview({ channel }: Props) {
if (!changed) setChanged(true)
}}
/>
<Button onClick={save} style="contrast" disabled={!changed}>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
</div>

View File

@@ -76,7 +76,7 @@ export function Overview({ server }: Props) {
if (!changed) setChanged(true)
}}
/>
<Button onClick={save} style="contrast" disabled={!changed}>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>