Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 509 additions and 478 deletions
import { mapMessage } from "../../../context/revoltjs/util"; 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, smooth) => { init: async (renderer, id, nearby, smooth) => {
if (renderer.client!.websocket.connected) { if (renderer.client!.websocket.connected) {
renderer if (nearby)
.client!.channels.fetchMessagesWithUsers(id, {}, true) renderer
.then(({ messages: data }) => { .client!.channels.get(id)!
data.reverse(); .fetchMessagesWithUsers({ nearby, limit: 100 })
let messages = data.map((x) => mapMessage(x)); .then(({ messages }) => {
renderer.setState( messages.sort((a, b) => a._id.localeCompare(b._id));
id, renderer.setState(
{ id,
type: "RENDER", {
messages, type: "RENDER",
atTop: data.length < 50, messages,
atBottom: true, atTop: false,
}, atBottom: false,
{ type: "ScrollToBottom", smooth }, },
); { type: "ScrollToView", id: nearby },
}); );
});
else
renderer
.client!.channels.get(id)!
.fetchMessagesWithUsers({})
.then(({ messages }) => {
messages.reverse();
renderer.setState(
id,
{
type: "RENDER",
messages,
atTop: messages.length < 50,
atBottom: true,
},
{ type: "ScrollToBottom", smooth },
);
});
} else { } else {
renderer.setState(id, { type: "WAITING_FOR_NETWORK" }); renderer.setState(id, { type: "WAITING_FOR_NETWORK" });
} }
}, },
receive: async (renderer, message) => { receive: async (renderer, message) => {
if (message.channel !== renderer.channel) return; if (message.channel_id !== renderer.channel) return;
if (renderer.state.type !== "RENDER") return; if (renderer.state.type !== "RENDER") return;
if (renderer.state.messages.find((x) => x._id === message._id)) return; if (renderer.state.messages.find((x) => x._id === message._id)) return;
if (!renderer.state.atBottom) return; if (!renderer.state.atBottom) return;
let messages = [...renderer.state.messages, mapMessage(message)]; let messages = [...renderer.state.messages, message];
let atTop = renderer.state.atTop; let atTop = renderer.state.atTop;
if (messages.length > 150) { if (messages.length > 150) {
messages = messages.slice(messages.length - 150); messages = messages.slice(messages.length - 150);
...@@ -40,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -40,7 +57,7 @@ export const SimpleRenderer: RendererRoutines = {
} }
renderer.setState( renderer.setState(
message.channel, message.channel_id,
{ {
...renderer.state, ...renderer.state,
messages, messages,
...@@ -49,35 +66,14 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -49,35 +66,14 @@ export const SimpleRenderer: RendererRoutines = {
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE }, { type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
); );
}, },
edit: async (renderer, id, patch) => { edit: noopAsync,
const channel = renderer.channel;
if (!channel) return;
if (renderer.state.type !== "RENDER") return;
let messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id);
if (index > -1) {
let message = { ...messages[index], ...mapMessage(patch) };
messages.splice(index, 1, message);
renderer.setState(
channel,
{
...renderer.state,
messages,
},
{ type: "StayAtBottom" },
);
}
},
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.type !== "RENDER") return;
let messages = [...renderer.state.messages]; const messages = [...renderer.state.messages];
let index = messages.findIndex((x) => x._id === id); const index = messages.findIndex((x) => x._id === id);
if (index > -1) { if (index > -1) {
messages.splice(index, 1); messages.splice(index, 1);
...@@ -100,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -100,14 +96,11 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return; if (state.type !== "RENDER") return;
if (state.atTop) return; if (state.atTop) return;
const { messages: data } = const { messages: data } = await renderer
await renderer.client!.channels.fetchMessagesWithUsers( .client!.channels.get(channel)!
channel, .fetchMessagesWithUsers({
{ before: state.messages[0]._id,
before: state.messages[0]._id, });
},
true,
);
if (data.length === 0) { if (data.length === 0) {
return renderer.setState(channel, { return renderer.setState(channel, {
...@@ -117,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -117,7 +110,7 @@ export const SimpleRenderer: RendererRoutines = {
} }
data.reverse(); data.reverse();
let messages = [...data.map((x) => mapMessage(x)), ...state.messages]; let messages = [...data, ...state.messages];
let atTop = false; let atTop = false;
if (data.length < 50) { if (data.length < 50) {
...@@ -144,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -144,15 +137,12 @@ export const SimpleRenderer: RendererRoutines = {
if (state.type !== "RENDER") return; if (state.type !== "RENDER") return;
if (state.atBottom) return; if (state.atBottom) return;
const { messages: data } = const { messages: data } = await renderer
await renderer.client!.channels.fetchMessagesWithUsers( .client!.channels.get(channel)!
channel, .fetchMessagesWithUsers({
{ after: state.messages[state.messages.length - 1]._id,
after: state.messages[state.messages.length - 1]._id, sort: "Oldest",
sort: "Oldest", });
},
true,
);
if (data.length === 0) { if (data.length === 0) {
return renderer.setState(channel, { return renderer.setState(channel, {
...@@ -161,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = { ...@@ -161,7 +151,7 @@ export const SimpleRenderer: RendererRoutines = {
}); });
} }
let messages = [...state.messages, ...data.map((x) => mapMessage(x))]; let messages = [...state.messages, ...data];
let atBottom = false; let atBottom = false;
if (data.length < 50) { if (data.length < 50) {
......
import { Message } from "revolt.js"; import { Message } from "revolt.js/dist/maps/Messages";
import { MessageObject } from "../../context/revoltjs/util";
import { SingletonRenderer } from "./Singleton"; import { SingletonRenderer } from "./Singleton";
...@@ -8,6 +6,7 @@ export type ScrollState = ...@@ -8,6 +6,7 @@ export type ScrollState =
| { type: "Free" } | { type: "Free" }
| { type: "Bottom"; scrollingUntil?: number } | { type: "Bottom"; scrollingUntil?: number }
| { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean } | { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean }
| { type: "ScrollToView"; id: string }
| { type: "OffsetTop"; previousHeight: number } | { type: "OffsetTop"; previousHeight: number }
| { type: "ScrollTop"; y: number }; | { type: "ScrollTop"; y: number };
...@@ -19,13 +18,14 @@ export type RenderState = ...@@ -19,13 +18,14 @@ export type RenderState =
type: "RENDER"; type: "RENDER";
atTop: boolean; atTop: boolean;
atBottom: boolean; atBottom: boolean;
messages: MessageObject[]; messages: Message[];
}; };
export interface RendererRoutines { export interface RendererRoutines {
init: ( init: (
renderer: SingletonRenderer, renderer: SingletonRenderer,
id: string, id: string,
message?: string,
smooth?: boolean, smooth?: boolean,
) => Promise<void>; ) => Promise<void>;
......
export const stopPropagation = ( export const stopPropagation = (
ev: JSX.TargetedMouseEvent<HTMLDivElement>, ev: JSX.TargetedMouseEvent<HTMLElement>,
_consume?: any, // eslint-disable-next-line
_consume?: unknown,
) => { ) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
......
...@@ -20,6 +20,7 @@ interface SignalingEvents { ...@@ -20,6 +20,7 @@ interface SignalingEvents {
open: (event: Event) => void; open: (event: Event) => void;
close: (event: CloseEvent) => void; close: (event: CloseEvent) => void;
error: (event: Event) => void; error: (event: Event) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: (data: any) => void; data: (data: any) => void;
} }
...@@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
entry(json); entry(json);
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
sendRequest(type: string, data?: any): Promise<any> { sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN) if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected }); return Promise.reject({ error: WSErrorCode.NotConnected });
...@@ -117,13 +119,14 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -117,13 +119,14 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
this.once("close", onClose); this.once("close", onClose);
const json = { const json = {
id: this.index, id: this.index,
type: type, type,
data, data,
}; };
ws.send(JSON.stringify(json) + "\n"); ws.send(`${JSON.stringify(json)}\n`);
this.index++; this.index++;
}); });
} }
/* eslint-enable @typescript-eslint/no-explicit-any */
authenticate(token: string, roomId: string): Promise<AuthenticationResult> { authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId }); return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
...@@ -161,7 +164,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> { ...@@ -161,7 +164,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
type: ProduceType, type: ProduceType,
rtpParameters: RtpParameters, rtpParameters: RtpParameters,
): Promise<string> { ): Promise<string> {
let result = await this.sendRequest(WSCommandType.StartProduce, { const result = await this.sendRequest(WSCommandType.StartProduce, {
type, type,
rtpParameters, rtpParameters,
}); });
......
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import * as mediasoupClient from "mediasoup-client"; import * as mediasoupClient from "mediasoup-client";
import { types } from "mediasoup-client";
import { import { Device, Producer, Transport } from "mediasoup-client/lib/types";
Device,
Producer,
Transport,
UnsupportedError,
} from "mediasoup-client/lib/types";
import Signaling from "./Signaling"; import Signaling from "./Signaling";
import { import {
...@@ -18,6 +14,8 @@ import { ...@@ -18,6 +14,8 @@ import {
WSErrorCode, WSErrorCode,
} from "./Types"; } from "./Types";
const UnsupportedError = types.UnsupportedError;
interface VoiceEvents { interface VoiceEvents {
ready: () => void; ready: () => void;
error: (error: Error) => void; error: (error: Error) => void;
...@@ -116,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -116,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.signaling.on( this.signaling.on(
"error", "error",
(error) => { () => {
this.emit("error", new Error("Signaling error")); this.emit("error", new Error("Signaling error"));
}, },
this, this,
...@@ -174,7 +172,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -174,7 +172,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
if (this.device === undefined || this.roomId === undefined) if (this.device === undefined || this.roomId === undefined)
throw new ReferenceError("Voice Client is in an invalid state"); throw new ReferenceError("Voice Client is in an invalid state");
const result = await this.signaling.authenticate(token, this.roomId); const result = await this.signaling.authenticate(token, this.roomId);
let [room] = await Promise.all([ const [room] = await Promise.all([
this.signaling.roomInfo(), this.signaling.roomInfo(),
this.device.load({ routerRtpCapabilities: result.rtpCapabilities }), this.device.load({ routerRtpCapabilities: result.rtpCapabilities }),
]); ]);
...@@ -231,7 +229,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -231,7 +229,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
}); });
this.emit("ready"); this.emit("ready");
for (let user of this.participants) { for (const user of this.participants) {
if (user[1].audio && user[0] !== this.userId) if (user[1].audio && user[0] !== this.userId)
this.startConsume(user[0], "audio"); this.startConsume(user[0], "audio");
} }
...@@ -325,7 +323,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> { ...@@ -325,7 +323,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
await this.signaling.stopProduce(type); await this.signaling.stopProduce(type);
} catch (error) { } catch (error) {
if (error.error === WSErrorCode.ProducerNotFound) return; if (error.error === WSErrorCode.ProducerNotFound) return;
else throw error; throw error;
} }
} }
} }
/* eslint-disable react-hooks/rules-of-hooks */
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -9,11 +10,6 @@ import { ...@@ -9,11 +10,6 @@ import {
ClientStatus, ClientStatus,
StatusContext, StatusContext,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUser,
} from "../context/revoltjs/hooks";
import Header from "../components/ui/Header"; import Header from "../components/ui/Header";
...@@ -32,39 +28,39 @@ export default function Open() { ...@@ -32,39 +28,39 @@ export default function Open() {
); );
} }
const ctx = useForceUpdate();
const channels = useChannels(undefined, ctx);
const user = useUser(id, ctx);
useEffect(() => { useEffect(() => {
if (id === "saved") { if (id === "saved") {
for (const channel of channels) { for (const channel of [...client.channels.values()]) {
if (channel?.channel_type === "SavedMessages") { if (channel?.channel_type === "SavedMessages") {
history.push(`/channel/${channel._id}`); history.push(`/channel/${channel._id}`);
return; return;
} }
} }
client.users client
.openDM(client.user?._id as string) .user!.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`)) .then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error })); .catch((error) => openScreen({ id: "error", error }));
return; return;
} }
const user = client.users.get(id);
if (user) { if (user) {
const channel: string | undefined = channels.find( const channel: string | undefined = [
...client.channels.values(),
].find(
(channel) => (channel) =>
channel?.channel_type === "DirectMessage" && channel?.channel_type === "DirectMessage" &&
channel.recipients.includes(id), channel.recipient_ids!.includes(id),
)?._id; )?._id;
if (channel) { if (channel) {
history.push(`/channel/${channel}`); history.push(`/channel/${channel}`);
} else { } else {
client.users client.users
.openDM(id) .get(id)
?.openDM()
.then((channel) => history.push(`/channel/${channel?._id}`)) .then((channel) => history.push(`/channel/${channel?._id}`))
.catch((error) => openScreen({ id: "error", error })); .catch((error) => openScreen({ id: "error", error }));
} }
...@@ -73,7 +69,7 @@ export default function Open() { ...@@ -73,7 +69,7 @@ export default function Open() {
} }
history.push("/"); history.push("/");
}, []); });
return ( return (
<Header placement="primary"> <Header placement="primary">
......
...@@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications"; ...@@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications";
import StateMonitor from "../context/revoltjs/StateMonitor"; import StateMonitor from "../context/revoltjs/StateMonitor";
import SyncManager from "../context/revoltjs/SyncManager"; import SyncManager from "../context/revoltjs/SyncManager";
import { Titlebar } from "../components/native/Titlebar";
import BottomNavigation from "../components/navigation/BottomNavigation"; import BottomNavigation from "../components/navigation/BottomNavigation";
import LeftSidebar from "../components/navigation/LeftSidebar"; import LeftSidebar from "../components/navigation/LeftSidebar";
import RightSidebar from "../components/navigation/RightSidebar"; import RightSidebar from "../components/navigation/RightSidebar";
...@@ -35,86 +36,99 @@ export default function App() { ...@@ -35,86 +36,99 @@ export default function App() {
const path = useLocation().pathname; const path = useLocation().pathname;
const fixedBottomNav = const fixedBottomNav =
path === "/" || path === "/settings" || path.startsWith("/friends"); path === "/" || path === "/settings" || path.startsWith("/friends");
const inSettings = path.includes("/settings");
const inChannel = path.includes("/channel"); const inChannel = path.includes("/channel");
const inSpecial = const inSpecial =
(path.startsWith("/friends") && isTouchscreenDevice) || (path.startsWith("/friends") && isTouchscreenDevice) ||
path.startsWith("/invite") || path.startsWith("/invite") ||
path.startsWith("/settings"); path.includes("/settings");
return ( return (
<OverlappingPanels <>
width="100vw" {window.isNative && !window.native.getConfig().frame && (
height="var(--app-height)" <Titlebar />
leftPanel={ )}
inSpecial <OverlappingPanels
? undefined width="100vw"
: { width: 292, component: <LeftSidebar /> } height={
} window.isNative && !window.native.getConfig().frame
rightPanel={ ? "calc(var(--app-height) - var(--titlebar-height))"
!inSettings && inChannel : "var(--app-height)"
? { width: 240, component: <RightSidebar /> } }
: undefined leftPanel={
} inSpecial
bottomNav={{ ? undefined
component: <BottomNavigation />, : { width: 292, component: <LeftSidebar /> }
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left, }
height: 50, rightPanel={
}} !inSpecial && inChannel
docked={isTouchscreenDevice ? Docked.None : Docked.Left}> ? { width: 240, component: <RightSidebar /> }
<Routes> : undefined
<Switch> }
<Route bottomNav={{
path="/server/:server/channel/:channel/settings/:page" component: <BottomNavigation />,
component={ChannelSettings} showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
/> height: 50,
<Route }}
path="/server/:server/channel/:channel/settings" docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
component={ChannelSettings} <Routes>
/> <Switch>
<Route <Route
path="/server/:server/settings/:page" path="/server/:server/channel/:channel/settings/:page"
component={ServerSettings} component={ChannelSettings}
/> />
<Route <Route
path="/server/:server/settings" path="/server/:server/channel/:channel/settings"
component={ServerSettings} component={ChannelSettings}
/> />
<Route <Route
path="/channel/:channel/settings/:page" path="/server/:server/settings/:page"
component={ChannelSettings} component={ServerSettings}
/> />
<Route <Route
path="/channel/:channel/settings" path="/server/:server/settings"
component={ChannelSettings} component={ServerSettings}
/> />
<Route
path="/channel/:channel/settings/:page"
component={ChannelSettings}
/>
<Route
path="/channel/:channel/settings"
component={ChannelSettings}
/>
<Route <Route
path="/channel/:channel/message/:message" path="/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route <Route
path="/server/:server/channel/:channel" path="/server/:server/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} />
<Route path="/settings/:page" component={Settings} /> <Route
<Route path="/settings" component={Settings} /> path="/server/:server/channel/:channel"
component={Channel}
/>
<Route path="/server/:server" />
<Route path="/channel/:channel" component={Channel} />
<Route path="/dev" component={Developer} /> <Route path="/settings/:page" component={Settings} />
<Route path="/friends" component={Friends} /> <Route path="/settings" component={Settings} />
<Route path="/open/:id" component={Open} />
<Route path="/invite/:code" component={Invite} /> <Route path="/dev" component={Developer} />
<Route path="/" component={Home} /> <Route path="/friends" component={Friends} />
</Switch> <Route path="/open/:id" component={Open} />
</Routes> <Route path="/invite/:code" component={Invite} />
<ContextMenus /> <Route path="/" component={Home} />
<Popovers /> </Switch>
<Notifications /> </Routes>
<StateMonitor /> <ContextMenus />
<SyncManager /> <Popovers />
</OverlappingPanels> <Notifications />
<StateMonitor />
<SyncManager />
</OverlappingPanels>
</>
); );
} }
...@@ -16,7 +16,7 @@ export function App() { ...@@ -16,7 +16,7 @@ export function App() {
<Context> <Context>
<Masks /> <Masks />
{/* {/*
// @ts-expect-error */} // @ts-expect-error typings mis-match between preact... and preact? */}
<Suspense fallback={<Preloader type="spinner" />}> <Suspense fallback={<Preloader type="spinner" />}>
<Switch> <Switch>
<Route path="/login"> <Route path="/login">
......
import { useParams, useHistory } from "react-router-dom"; import { observer } from "mobx-react-lite";
import { Channels } from "revolt.js/dist/api/objects"; import { useParams } from "react-router-dom";
import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks"; import { dispatch, getState } from "../../redux";
import { useClient } from "../../context/revoltjs/RevoltClient";
import AgeGate from "../../components/common/AgeGate";
import MessageBox from "../../components/common/messaging/MessageBox"; import MessageBox from "../../components/common/messaging/MessageBox";
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator"; import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
import Button from "../../components/ui/Button";
import Checkbox from "../../components/ui/Checkbox";
import MemberSidebar from "../../components/navigation/right/MemberSidebar"; import MemberSidebar from "../../components/navigation/right/MemberSidebar";
import ChannelHeader from "./ChannelHeader"; import ChannelHeader from "./ChannelHeader";
...@@ -34,99 +36,60 @@ const ChannelContent = styled.div` ...@@ -34,99 +36,60 @@ const ChannelContent = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const AgeGate = styled.div`
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
justify-content: center;
user-select: none;
padding: 12px;
img {
height: 150px;
}
.subtext {
color: var(--secondary-foreground);
margin-bottom: 12px;
font-size: 14px;
}
.actions {
margin-top: 20px;
display: flex;
gap: 12px;
}
`;
export function Channel({ id }: { id: string }) { export function Channel({ id }: { id: string }) {
const ctx = useForceUpdate(); const client = useClient();
const channel = useChannel(id, ctx); const channel = client.channels.get(id);
if (!channel) return null; if (!channel) return null;
if (channel.channel_type === "VoiceChannel") { if (channel.channel_type === "VoiceChannel") {
return <VoiceChannel channel={channel} />; return <VoiceChannel channel={channel} />;
} else {
return <TextChannel channel={channel} />;
} }
return <TextChannel channel={channel} />;
} }
function TextChannel({ channel }: { channel: Channels.Channel }) { const MEMBERS_SIDEBAR_KEY = "sidebar_members";
const [showMembers, setMembers] = useState(true); const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const [showMembers, setMembers] = useState(
if ( getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true,
(channel.channel_type === "TextChannel" || );
channel.channel_type === "Group") &&
channel.name.includes("nsfw")
) {
const goBack = useHistory();
const [consent, setConsent] = useState(false);
const [ageGate, setAgeGate] = useState(false);
if (!ageGate) {
return (
<AgeGate>
<img
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
draggable={false}
/>
<h2>{channel.name}</h2>
<span className="subtext">
This channel is marked as NSFW.{" "}
<a href="#">Learn more</a>
</span>
<Checkbox checked={consent} onChange={(v) => setConsent(v)}>
I confirm that I am at least 18 years old.
</Checkbox>
<div className="actions">
<Button contrast onClick={() => goBack}>
Go back
</Button>
<Button
contrast
onClick={() => consent && setAgeGate(true)}>
Enter Channel
</Button>
</div>
</AgeGate>
);
}
}
let id = channel._id; const id = channel._id;
return ( return (
<> <AgeGate
type="channel"
channel={channel}
gated={
!!(
(channel.channel_type === "TextChannel" ||
channel.channel_type === "Group") &&
channel.name?.includes("nsfw")
)
}>
<ChannelHeader <ChannelHeader
channel={channel} channel={channel}
toggleSidebar={() => setMembers(!showMembers)} toggleSidebar={() => {
setMembers(!showMembers);
if (showMembers) {
dispatch({
type: "SECTION_TOGGLE_SET",
id: MEMBERS_SIDEBAR_KEY,
state: false,
});
} else {
dispatch({
type: "SECTION_TOGGLE_UNSET",
id: MEMBERS_SIDEBAR_KEY,
});
}
}}
/> />
<ChannelMain> <ChannelMain>
<ChannelContent> <ChannelContent>
<VoiceHeader id={id} /> <VoiceHeader id={id} />
<MessageArea id={id} /> <MessageArea id={id} />
<TypingIndicator id={id} /> <TypingIndicator channel={channel} />
<JumpToBottom id={id} /> <JumpToBottom id={id} />
<MessageBox channel={channel} /> <MessageBox channel={channel} />
</ChannelContent> </ChannelContent>
...@@ -134,11 +97,11 @@ function TextChannel({ channel }: { channel: Channels.Channel }) { ...@@ -134,11 +97,11 @@ function TextChannel({ channel }: { channel: Channels.Channel }) {
<MemberSidebar channel={channel} /> <MemberSidebar channel={channel} />
)} )}
</ChannelMain> </ChannelMain>
</> </AgeGate>
); );
} });
function VoiceChannel({ channel }: { channel: Channels.Channel }) { function VoiceChannel({ channel }: { channel: ChannelI }) {
return ( return (
<> <>
<ChannelHeader channel={channel} /> <ChannelHeader channel={channel} />
...@@ -147,7 +110,7 @@ function VoiceChannel({ channel }: { channel: Channels.Channel }) { ...@@ -147,7 +110,7 @@ function VoiceChannel({ channel }: { channel: Channels.Channel }) {
); );
} }
export default function () { export default function ChannelComponent() {
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />; return <Channel id={channel} key={channel} />;
} }
import { At, Hash } from "@styled-icons/boxicons-regular"; import { At, Hash, Menu } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { Notepad, Group } from "@styled-icons/boxicons-solid";
import { Channel, User } from "revolt.js"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components"; import styled from "styled-components";
import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../context/revoltjs/util"; import { getChannelName } from "../../context/revoltjs/util";
import { useStatusColour } from "../../components/common/user/UserIcon"; import { useStatusColour } from "../../components/common/user/UserIcon";
...@@ -58,26 +57,25 @@ const Info = styled.div` ...@@ -58,26 +57,25 @@ const Info = styled.div`
font-size: 0.8em; font-size: 0.8em;
font-weight: 400; font-weight: 400;
color: var(--secondary-foreground); color: var(--secondary-foreground);
> * {
pointer-events: none;
}
} }
`; `;
export default function ChannelHeader({ export default observer(({ channel, toggleSidebar }: ChannelHeaderProps) => {
channel,
toggleSidebar,
}: ChannelHeaderProps) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const name = getChannelName(client, channel); const name = getChannelName(channel);
let icon, recipient; let icon, recipient: User | undefined;
switch (channel.channel_type) { switch (channel.channel_type) {
case "SavedMessages": case "SavedMessages":
icon = <Notepad size={24} />; icon = <Notepad size={24} />;
break; break;
case "DirectMessage": case "DirectMessage":
icon = <At size={24} />; icon = <At size={24} />;
const uid = client.channels.getRecipient(channel._id); recipient = channel.recipient;
recipient = client.users.get(uid);
break; break;
case "Group": case "Group":
icon = <Group size={24} />; icon = <Group size={24} />;
...@@ -89,6 +87,11 @@ export default function ChannelHeader({ ...@@ -89,6 +87,11 @@ export default function ChannelHeader({
return ( return (
<Header placement="primary"> <Header placement="primary">
{isTouchscreenDevice && (
<div className="menu">
<Menu size={27} />
</div>
)}
{icon} {icon}
<Info> <Info>
<span className="name">{name}</span> <span className="name">{name}</span>
...@@ -100,12 +103,11 @@ export default function ChannelHeader({ ...@@ -100,12 +103,11 @@ export default function ChannelHeader({
<div <div
className="status" className="status"
style={{ style={{
backgroundColor: useStatusColour( backgroundColor:
recipient as User, useStatusColour(recipient),
),
}} }}
/> />
<UserStatus user={recipient as User} /> <UserStatus user={recipient} />
</span> </span>
</> </>
)} )}
...@@ -120,7 +122,7 @@ export default function ChannelHeader({ ...@@ -120,7 +122,7 @@ export default function ChannelHeader({
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "channel_info", id: "channel_info",
channel_id: channel._id, channel,
}) })
}> }>
<Markdown <Markdown
...@@ -136,4 +138,4 @@ export default function ChannelHeader({ ...@@ -136,4 +138,4 @@ export default function ChannelHeader({
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} /> <HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
</Header> </Header>
); );
} });
import { Sidebar as SidebarIcon } from "@styled-icons/boxicons-regular"; /* eslint-disable react-hooks/rules-of-hooks */
import { import {
UserPlus, UserPlus,
Cog, Cog,
PhoneCall, PhoneCall,
PhoneOutgoing, PhoneOff,
Group,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
VoiceContext, VoiceContext,
VoiceOperationsContext, VoiceOperationsContext,
VoiceStatus, VoiceStatus,
} from "../../../context/Voice"; } from "../../../context/Voice";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import UpdateIndicator from "../../../components/common/UpdateIndicator"; import UpdateIndicator from "../../../components/common/UpdateIndicator";
import IconButton from "../../../components/ui/IconButton"; import IconButton from "../../../components/ui/IconButton";
...@@ -29,25 +27,21 @@ export default function HeaderActions({ ...@@ -29,25 +27,21 @@ export default function HeaderActions({
toggleSidebar, toggleSidebar,
}: ChannelHeaderProps) { }: ChannelHeaderProps) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const history = useHistory(); const history = useHistory();
return ( return (
<> <>
<UpdateIndicator /> <UpdateIndicator style="channel" />
{channel.channel_type === "Group" && ( {channel.channel_type === "Group" && (
<> <>
<IconButton <IconButton
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "user_picker", id: "user_picker",
omit: channel.recipients, omit: channel.recipient_ids!,
callback: async (users) => { callback: async (users) => {
for (const user of users) { for (const user of users) {
await client.channels.addMember( await channel.addMember(user);
channel._id,
user,
);
} }
}, },
}) })
...@@ -64,12 +58,11 @@ export default function HeaderActions({ ...@@ -64,12 +58,11 @@ export default function HeaderActions({
)} )}
<VoiceActions channel={channel} /> <VoiceActions channel={channel} />
{(channel.channel_type === "Group" || {(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") && channel.channel_type === "TextChannel") && (
!isTouchscreenDevice && ( <IconButton onClick={toggleSidebar}>
<IconButton onClick={toggleSidebar}> <Group size={25} />
<SidebarIcon size={22} /> </IconButton>
</IconButton> )}
)}
</> </>
); );
} }
...@@ -88,25 +81,23 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) { ...@@ -88,25 +81,23 @@ function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
if (voice.roomId === channel._id) { if (voice.roomId === channel._id) {
return ( return (
<IconButton onClick={disconnect}> <IconButton onClick={disconnect}>
<PhoneOutgoing size={22} /> <PhoneOff size={22} />
</IconButton>
);
} else {
return (
<IconButton
onClick={() => {
disconnect();
connect(channel._id);
}}>
<PhoneCall size={24} />
</IconButton> </IconButton>
); );
} }
} else {
return ( return (
<IconButton> <IconButton
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" /> onClick={() => {
disconnect();
connect(channel);
}}>
<PhoneCall size={24} />
</IconButton> </IconButton>
); );
} }
return (
<IconButton>
<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
</IconButton>
);
} }
import { observer } from "mobx-react-lite";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useChannel, useForceUpdate } from "../../../context/revoltjs/hooks"; 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`
...@@ -24,17 +25,17 @@ interface Props { ...@@ -24,17 +25,17 @@ interface Props {
id: string; id: string;
} }
export default function ConversationStart({ id }: Props) { export default observer(({ id }: Props) => {
const ctx = useForceUpdate(); const client = useClient();
const channel = useChannel(id, ctx); const channel = client.channels.get(id);
if (!channel) return null; if (!channel) return null;
return ( return (
<StartBase> <StartBase>
<h1>{getChannelName(ctx.client, channel, true)}</h1> <h1>{getChannelName(channel, true)}</h1>
<h4> <h4>
<Text id="app.main.channel.start.group" /> <Text id="app.main.channel.start.group" />
</h4> </h4>
</StartBase> </StartBase>
); );
} });
import { useHistory, useParams } from "react-router-dom";
import { animateScroll } from "react-scroll"; import { animateScroll } from "react-scroll";
import styled from "styled-components"; import styled from "styled-components";
import useResizeObserver from "use-resize-observer"; import useResizeObserver from "use-resize-observer";
import { createContext } from "preact"; import { createContext } from "preact";
import { import {
useCallback,
useContext, useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
...@@ -12,13 +14,14 @@ import { ...@@ -12,13 +14,14 @@ import {
} from "preact/hooks"; } from "preact/hooks";
import { defer } from "../../../lib/defer"; import { defer } from "../../../lib/defer";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton"; import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
import { RenderState, ScrollState } from "../../../lib/renderer/types"; import { RenderState, 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";
...@@ -53,9 +56,15 @@ export const MessageAreaWidthContext = createContext(0); ...@@ -53,9 +56,15 @@ export const MessageAreaWidthContext = createContext(0);
export const MESSAGE_AREA_PADDING = 82; export const MESSAGE_AREA_PADDING = 82;
export function MessageArea({ id }: Props) { export function MessageArea({ id }: Props) {
const history = useHistory();
const client = useContext(AppContext);
const status = useContext(StatusContext); const status = useContext(StatusContext);
const { focusTaken } = useContext(IntermediateContext); const { focusTaken } = useContext(IntermediateContext);
// ? Required data for message links.
const { message } = useParams<{ message: string }>();
const [highlight, setHighlight] = useState<string | undefined>(undefined);
// ? This is the scroll container. // ? This is the scroll container.
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver<HTMLDivElement>({ ref }); const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
...@@ -66,7 +75,7 @@ export function MessageArea({ id }: Props) { ...@@ -66,7 +75,7 @@ export function MessageArea({ id }: Props) {
// ? useRef to avoid re-renders // ? useRef to avoid re-renders
const scrollState = useRef<ScrollState>({ type: "Free" }); const scrollState = useRef<ScrollState>({ type: "Free" });
const setScrollState = (v: ScrollState) => { const setScrollState = useCallback((v: ScrollState) => {
if (v.type === "StayAtBottom") { if (v.type === "StayAtBottom") {
if (scrollState.current.type === "Bottom" || atBottom()) { if (scrollState.current.type === "Bottom" || atBottom()) {
scrollState.current = { scrollState.current = {
...@@ -91,13 +100,21 @@ export function MessageArea({ id }: Props) { ...@@ -91,13 +100,21 @@ export function MessageArea({ id }: Props) {
container: ref.current, container: ref.current,
duration: scrollState.current.smooth ? 150 : 0, 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") { } else if (scrollState.current.type === "OffsetTop") {
animateScroll.scrollTo( animateScroll.scrollTo(
Math.max( Math.max(
101, 101,
ref.current.scrollTop + ref.current
(ref.current.scrollHeight - ? ref.current.scrollTop +
scrollState.current.previousHeight), (ref.current.scrollHeight -
scrollState.current.previousHeight)
: 101,
), ),
{ {
container: ref.current, container: ref.current,
...@@ -115,19 +132,27 @@ export function MessageArea({ id }: Props) { ...@@ -115,19 +132,27 @@ export function MessageArea({ id }: Props) {
setScrollState({ type: "Free" }); setScrollState({ type: "Free" });
} }
}); });
}; }, []);
// ? 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
// By default, we assume we are at the bottom, i.e. when we first load. // By default, we assume we are at the bottom, i.e. when we first load.
const atBottom = (offset = 0) => const atBottom = (offset = 0) =>
ref.current ref.current
? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) - ? Math.floor(ref.current?.scrollHeight - ref.current?.scrollTop) -
offset <= offset <=
ref.current.clientHeight ref.current?.clientHeight
: true; : true;
const atTop = (offset = 0) => ref.current.scrollTop <= offset; const atTop = (offset = 0) =>
ref.current ? ref.current.scrollTop <= offset : false;
// ? Handle global jump to bottom, e.g. when editing last message in chat.
useEffect(() => {
return internalSubscribe("MessageArea", "jump_to_bottom", () =>
setScrollState({ type: "ScrollToBottom" }),
);
}, [setScrollState]);
// ? Handle events from renderer. // ? Handle events from renderer.
useEffect(() => { useEffect(() => {
...@@ -139,13 +164,31 @@ export function MessageArea({ id }: Props) { ...@@ -139,13 +164,31 @@ export function MessageArea({ id }: Props) {
SingletonMessageRenderer.addListener("scroll", setScrollState); SingletonMessageRenderer.addListener("scroll", setScrollState);
return () => return () =>
SingletonMessageRenderer.removeListener("scroll", setScrollState); SingletonMessageRenderer.removeListener("scroll", setScrollState);
}, [scrollState]); }, [scrollState, setScrollState]);
// ? Load channel initially. // ? Load channel initially.
useEffect(() => { useEffect(() => {
if (message) return;
SingletonMessageRenderer.init(id); SingletonMessageRenderer.init(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); }, [id]);
// ? If message present or changes, load it as well.
useEffect(() => {
if (message) {
setHighlight(message);
SingletonMessageRenderer.init(id, message);
const channel = client.channels.get(id);
if (channel?.channel_type === "TextChannel") {
history.push(`/server/${channel.server_id}/channel/${id}`);
} else {
history.push(`/channel/${id}`);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message]);
// ? If we are waiting for network, try again. // ? If we are waiting for network, try again.
useEffect(() => { useEffect(() => {
switch (status) { switch (status) {
...@@ -163,11 +206,14 @@ export function MessageArea({ id }: Props) { ...@@ -163,11 +206,14 @@ export function MessageArea({ id }: Props) {
SingletonMessageRenderer.markStale(); SingletonMessageRenderer.markStale();
break; break;
} }
}, [status, state]); }, [id, status, state]);
// ? When the container is scrolled. // ? When the container is scrolled.
// ? Also handle StayAtBottom // ? Also handle StayAtBottom
useEffect(() => { useEffect(() => {
const current = ref.current;
if (!current) return;
async function onScroll() { async function onScroll() {
if (scrollState.current.type === "Free" && atBottom()) { if (scrollState.current.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" }); setScrollState({ type: "Bottom" });
...@@ -181,28 +227,31 @@ export function MessageArea({ id }: Props) { ...@@ -181,28 +227,31 @@ export function MessageArea({ id }: Props) {
} }
} }
ref.current.addEventListener("scroll", onScroll); current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll); return () => current.removeEventListener("scroll", onScroll);
}, [ref, scrollState]); }, [ref, scrollState, setScrollState]);
// ? Top and bottom loaders. // ? Top and bottom loaders.
useEffect(() => { useEffect(() => {
const current = ref.current;
if (!current) return;
async function onScroll() { async function onScroll() {
if (atTop(100)) { if (atTop(100)) {
SingletonMessageRenderer.loadTop(ref.current); SingletonMessageRenderer.loadTop(ref.current!);
} }
if (atBottom(100)) { if (atBottom(100)) {
SingletonMessageRenderer.loadBottom(ref.current); SingletonMessageRenderer.loadBottom(ref.current!);
} }
} }
ref.current.addEventListener("scroll", onScroll); current.addEventListener("scroll", onScroll);
return () => ref.current.removeEventListener("scroll", onScroll); return () => current.removeEventListener("scroll", onScroll);
}, [ref]); }, [ref]);
// ? Scroll down whenever the message area resizes. // ? Scroll down whenever the message area resizes.
function stbOnResize() { const stbOnResize = useCallback(() => {
if (!atBottom() && scrollState.current.type === "Bottom") { if (!atBottom() && scrollState.current.type === "Bottom") {
animateScroll.scrollToBottom({ animateScroll.scrollToBottom({
container: ref.current, container: ref.current,
...@@ -211,18 +260,18 @@ export function MessageArea({ id }: Props) { ...@@ -211,18 +260,18 @@ export function MessageArea({ id }: Props) {
setScrollState({ type: "Bottom" }); setScrollState({ type: "Bottom" });
} }
} }, [setScrollState]);
// ? Scroll down when container resized. // ? Scroll down when container resized.
useLayoutEffect(() => { useLayoutEffect(() => {
stbOnResize(); stbOnResize();
}, [height]); }, [stbOnResize, height]);
// ? Scroll down whenever the window resizes. // ? Scroll down whenever the window resizes.
useLayoutEffect(() => { useLayoutEffect(() => {
document.addEventListener("resize", stbOnResize); document.addEventListener("resize", stbOnResize);
return () => document.removeEventListener("resize", stbOnResize); return () => document.removeEventListener("resize", stbOnResize);
}, [ref, scrollState]); }, [ref, scrollState, stbOnResize]);
// ? Scroll to bottom when pressing 'Escape'. // ? Scroll to bottom when pressing 'Escape'.
useEffect(() => { useEffect(() => {
...@@ -235,7 +284,7 @@ export function MessageArea({ id }: Props) { ...@@ -235,7 +284,7 @@ export function MessageArea({ id }: Props) {
document.body.addEventListener("keyup", keyUp); document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp);
}, [ref, focusTaken]); }, [id, ref, focusTaken]);
return ( return (
<MessageAreaWidthContext.Provider <MessageAreaWidthContext.Provider
...@@ -249,7 +298,11 @@ export function MessageArea({ id }: Props) { ...@@ -249,7 +298,11 @@ export function MessageArea({ id }: Props) {
</RequiresOnline> </RequiresOnline>
)} )}
{state.type === "RENDER" && ( {state.type === "RENDER" && (
<MessageRenderer id={id} state={state} /> <MessageRenderer
id={id}
state={state}
highlight={highlight}
/>
)} )}
{state.type === "EMPTY" && <ConversationStart id={id} />} {state.type === "EMPTY" && <ConversationStart id={id} />}
</div> </div>
......
import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components"; import styled from "styled-components";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
...@@ -9,8 +10,6 @@ import { ...@@ -9,8 +10,6 @@ import {
IntermediateContext, IntermediateContext,
useIntermediate, useIntermediate,
} from "../../../context/intermediate/Intermediate"; } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import AutoComplete, { import AutoComplete, {
useAutoComplete, useAutoComplete,
...@@ -23,9 +22,9 @@ const EditorBase = styled.div` ...@@ -23,9 +22,9 @@ const EditorBase = styled.div`
textarea { textarea {
resize: none; resize: none;
padding: 12px; padding: 12px;
border-radius: 3px;
white-space: pre-wrap; white-space: pre-wrap;
font-size: var(--text-size); font-size: var(--text-size);
border-radius: var(--border-radius);
background: var(--secondary-header); background: var(--secondary-header);
} }
...@@ -44,7 +43,7 @@ const EditorBase = styled.div` ...@@ -44,7 +43,7 @@ const EditorBase = styled.div`
`; `;
interface Props { interface Props {
message: MessageObject; message: Message;
finish: () => void; finish: () => void;
} }
...@@ -52,7 +51,6 @@ export default function MessageEditor({ message, finish }: Props) { ...@@ -52,7 +51,6 @@ export default function MessageEditor({ message, finish }: Props) {
const [content, setContent] = useState((message.content as string) ?? ""); const [content, setContent] = useState((message.content as string) ?? "");
const { focusTaken } = useContext(IntermediateContext); const { focusTaken } = useContext(IntermediateContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext);
async function save() { async function save() {
finish(); finish();
...@@ -60,13 +58,11 @@ export default function MessageEditor({ message, finish }: Props) { ...@@ -60,13 +58,11 @@ export default function MessageEditor({ message, finish }: Props) {
if (content.length === 0) { if (content.length === 0) {
openScreen({ openScreen({
id: "special_prompt", id: "special_prompt",
// @ts-expect-error
type: "delete_message", type: "delete_message",
// @ts-expect-error
target: message, target: message,
}); });
} else if (content !== message.content) { } else if (content !== message.content) {
await client.channels.editMessage(message.channel, message._id, { await message.edit({
content, content,
}); });
} }
...@@ -82,7 +78,7 @@ export default function MessageEditor({ message, finish }: Props) { ...@@ -82,7 +78,7 @@ export default function MessageEditor({ message, finish }: Props) {
document.body.addEventListener("keyup", keyUp); document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken]); }, [focusTaken, finish]);
const { const {
onChange, onChange,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { Users } from "revolt.js/dist/api/objects"; import { RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import styled from "styled-components"; import styled from "styled-components";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useContext, 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 { RenderState } from "../../../lib/renderer/types";
...@@ -13,8 +17,7 @@ import { connectState } from "../../../redux/connector"; ...@@ -13,8 +17,7 @@ import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue"; import { QueuedMessage } from "../../../redux/reducers/queue";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import { MessageObject } from "../../../context/revoltjs/util";
import Message from "../../../components/common/messaging/Message"; import Message from "../../../components/common/messaging/Message";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage"; import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
...@@ -28,6 +31,7 @@ import MessageEditor from "./MessageEditor"; ...@@ -28,6 +31,7 @@ import MessageEditor from "./MessageEditor";
interface Props { interface Props {
id: string; id: string;
state: RenderState; state: RenderState;
highlight?: string;
queue: QueuedMessage[]; queue: QueuedMessage[];
} }
...@@ -42,10 +46,10 @@ const BlockedMessage = styled.div` ...@@ -42,10 +46,10 @@ const BlockedMessage = styled.div`
} }
`; `;
function MessageRenderer({ id, state, queue }: Props) { function MessageRenderer({ id, state, queue, highlight }: Props) {
if (state.type !== "RENDER") return null; if (state.type !== "RENDER") return null;
const client = useContext(AppContext); const client = useClient();
const userId = client.user!._id; const userId = client.user!._id;
const [editing, setEditing] = useState<string | undefined>(undefined); const [editing, setEditing] = useState<string | undefined>(undefined);
...@@ -58,8 +62,9 @@ function MessageRenderer({ id, state, queue }: Props) { ...@@ -58,8 +62,9 @@ function MessageRenderer({ id, state, queue }: Props) {
function editLast() { function editLast() {
if (state.type !== "RENDER") return; if (state.type !== "RENDER") return;
for (let i = state.messages.length - 1; i >= 0; i--) { for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].author === userId) { if (state.messages[i].author_id === userId) {
setEditing(state.messages[i]._id); setEditing(state.messages[i]._id);
internalEmit("MessageArea", "jump_to_bottom");
return; return;
} }
} }
...@@ -71,10 +76,10 @@ function MessageRenderer({ id, state, queue }: Props) { ...@@ -71,10 +76,10 @@ function MessageRenderer({ id, state, queue }: Props) {
]; ];
return () => subs.forEach((unsub) => unsub()); return () => subs.forEach((unsub) => unsub());
}, [state.messages]); }, [state.messages, state.type, userId]);
let render: Children[] = [], const render: Children[] = [];
previous: MessageObject | undefined; let previous: MessageI | undefined;
if (state.atTop) { if (state.atTop) {
render.push(<ConversationStart id={id} />); render.push(<ConversationStart id={id} />);
...@@ -114,7 +119,11 @@ function MessageRenderer({ id, state, queue }: Props) { ...@@ -114,7 +119,11 @@ function MessageRenderer({ id, state, queue }: Props) {
function pushBlocked() { function pushBlocked() {
render.push( render.push(
<BlockedMessage> <BlockedMessage>
<X size={16} /> {blocked} blocked messages <X size={16} />{" "}
<Text
id="app.main.channel.misc.blocked_messages"
fields={{ count: blocked }}
/>
</BlockedMessage>, </BlockedMessage>,
); );
blocked = 0; blocked = 0;
...@@ -122,44 +131,47 @@ function MessageRenderer({ id, state, queue }: Props) { ...@@ -122,44 +131,47 @@ function MessageRenderer({ id, state, queue }: Props) {
for (const message of state.messages) { for (const message of state.messages) {
if (previous) { if (previous) {
compare(message._id, message.author, previous._id, previous.author); compare(
message._id,
message.author_id,
previous._id,
previous.author_id,
);
} }
if (message.author === "00000000000000000000000000") { if (message.author_id === SYSTEM_USER_ID) {
render.push( render.push(
<SystemMessage <SystemMessage
key={message._id} key={message._id}
message={message} message={message}
attachContext attachContext
highlight={highlight === message._id}
/>, />,
); );
} else if (
message.author?.relationship === RelationshipStatus.Blocked
) {
blocked++;
} else { } else {
// ! FIXME: temp solution if (blocked > 0) pushBlocked();
if (
client.users.get(message.author)?.relationship === render.push(
Users.Relationship.Blocked <Message
) { message={message}
blocked++; key={message._id}
} else { head={head}
if (blocked > 0) pushBlocked(); content={
editing === message._id ? (
render.push( <MessageEditor
<Message message={message}
message={message} finish={stopEditing}
key={message._id} />
head={head} ) : undefined
content={ }
editing === message._id ? ( attachContext
<MessageEditor highlight={highlight === message._id}
message={message} />,
finish={stopEditing} );
/>
) : undefined
}
attachContext
/>,
);
}
} }
previous = message; previous = message;
...@@ -174,20 +186,22 @@ function MessageRenderer({ id, state, queue }: Props) { ...@@ -174,20 +186,22 @@ function MessageRenderer({ id, state, queue }: Props) {
if (nonces.includes(msg.id)) continue; if (nonces.includes(msg.id)) continue;
if (previous) { if (previous) {
compare(msg.id, userId!, previous._id, previous.author); compare(msg.id, userId!, previous._id, previous.author_id);
previous = { previous = {
_id: msg.id, _id: msg.id,
data: { author: userId! }, author_id: userId!,
} as any; } as MessageI;
} }
render.push( render.push(
<Message <Message
message={{ message={
...msg.data, new MessageI(client, {
replies: msg.data.replies.map((x) => x.id), ...msg.data,
}} replies: msg.data.replies.map((x) => x.id),
})
}
key={msg.id} key={msg.id}
queued={msg} queued={msg}
head={head} head={head}
......
import { BarChart } from "@styled-icons/boxicons-regular"; import { BarChart } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -9,11 +10,7 @@ import { ...@@ -9,11 +10,7 @@ import {
VoiceOperationsContext, VoiceOperationsContext,
VoiceStatus, VoiceStatus,
} from "../../../context/Voice"; } from "../../../context/Voice";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useForceUpdate,
useSelf,
useUsers,
} from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
...@@ -27,18 +24,20 @@ const VoiceBase = styled.div` ...@@ -27,18 +24,20 @@ const VoiceBase = styled.div`
background: var(--secondary-background); background: var(--secondary-background);
.status { .status {
position: absolute; flex: 1 0;
color: var(--success);
background: var(--primary-background);
display: flex; display: flex;
position: absolute;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
border-radius: 7px;
flex: 1 0;
user-select: none; user-select: none;
color: var(--success);
border-radius: var(--border-radius);
background: var(--primary-background);
svg { svg {
margin-inline-end: 4px; margin-inline-end: 4px;
cursor: help; cursor: help;
...@@ -68,17 +67,20 @@ const VoiceBase = styled.div` ...@@ -68,17 +67,20 @@ const VoiceBase = styled.div`
} }
`; `;
export default function VoiceHeader({ id }: Props) { export default observer(({ id }: Props) => {
const { status, participants, roomId } = useContext(VoiceContext); const { status, participants, roomId } = useContext(VoiceContext);
if (roomId !== id) return null; if (roomId !== id) return null;
const { isProducing, startProducing, stopProducing, disconnect } = const { isProducing, startProducing, stopProducing, disconnect } =
useContext(VoiceOperationsContext); useContext(VoiceOperationsContext);
const ctx = useForceUpdate(); const client = useClient();
const self = useSelf(ctx); const self = client.users.get(client.user!._id);
//const ctx = useForceUpdate();
//const self = useSelf(ctx);
const keys = participants ? Array.from(participants.keys()) : undefined; const keys = participants ? Array.from(participants.keys()) : undefined;
const users = keys ? useUsers(keys, ctx) : undefined; const users = keys?.map((key) => client.users.get(key));
return ( return (
<VoiceBase> <VoiceBase>
...@@ -133,7 +135,7 @@ export default function VoiceHeader({ id }: Props) { ...@@ -133,7 +135,7 @@ export default function VoiceHeader({ id }: Props) {
</div> </div>
</VoiceBase> </VoiceBase>
); );
} });
/**{voice.roomId === id && ( /**{voice.roomId === id && (
<div className={styles.rtc}> <div className={styles.rtc}>
......
...@@ -6,14 +6,13 @@ import PaintCounter from "../../lib/PaintCounter"; ...@@ -6,14 +6,13 @@ import PaintCounter from "../../lib/PaintCounter";
import { TextReact } from "../../lib/i18n"; import { TextReact } from "../../lib/i18n";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import { useUserPermission } from "../../context/revoltjs/hooks";
import Header from "../../components/ui/Header"; import Header from "../../components/ui/Header";
export default function Developer() { export default function Developer() {
// const voice = useContext(VoiceContext); // const voice = useContext(VoiceContext);
const client = useContext(AppContext); const client = useContext(AppContext);
const userPermission = useUserPermission(client.user!._id); const userPermission = client.user!.permission;
return ( return (
<div> <div>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
&[data-empty="true"] { &[data-empty="true"] {
img { img {
height: 120px; height: 120px;
border-radius: 8px; border-radius: var(--border-radius);
} }
gap: 16px; gap: 16px;
...@@ -35,12 +35,12 @@ ...@@ -35,12 +35,12 @@
} }
.friend { .friend {
padding: 0 10px;
height: 60px; height: 60px;
display: flex; display: flex;
border-radius: 5px; padding: 0 10px;
align-items: center;
cursor: pointer; cursor: pointer;
align-items: center;
border-radius: var(--border-radius);
&:hover { &:hover {
background: var(--secondary-background); background: var(--secondary-background);
...@@ -110,9 +110,9 @@ ...@@ -110,9 +110,9 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
margin-top: 1em; margin-top: 1em;
border-radius: 7px;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
border-radius: var(--border-radius);
background: var(--secondary-background); background: var(--secondary-background);
svg { svg {
...@@ -191,7 +191,7 @@ ...@@ -191,7 +191,7 @@
padding: 0 8px 8px 8px; padding: 0 8px 8px 8px;
} }
.call { .remove {
display: none; display: none;
} }
} }
import { X, Plus } from "@styled-icons/boxicons-regular"; import { X, Plus } from "@styled-icons/boxicons-regular";
import { PhoneCall, Envelope } from "@styled-icons/boxicons-solid"; import { PhoneCall, Envelope, UserX } from "@styled-icons/boxicons-solid";
import { User, Users } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -12,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation"; ...@@ -12,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation";
import { VoiceOperationsContext } from "../../context/Voice"; import { VoiceOperationsContext } from "../../context/Voice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import {
AppContext,
OperationsContext,
} from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus"; import UserStatus from "../../components/common/user/UserStatus";
...@@ -27,49 +26,57 @@ interface Props { ...@@ -27,49 +26,57 @@ interface Props {
user: User; user: User;
} }
export function Friend({ user }: Props) { export const Friend = observer(({ user }: Props) => {
const client = useContext(AppContext); const history = useHistory();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const { openDM } = useContext(OperationsContext);
const { connect } = useContext(VoiceOperationsContext); const { connect } = useContext(VoiceOperationsContext);
const actions: Children[] = []; const actions: Children[] = [];
let subtext: Children = null; let subtext: Children = null;
if (user.relationship === Users.Relationship.Friend) { if (user.relationship === RelationshipStatus.Friend) {
subtext = <UserStatus user={user} />; subtext = <UserStatus user={user} />;
actions.push( actions.push(
<> <>
<IconButton <IconButton
type="circle" type="circle"
className={classNames( className={classNames(styles.button, styles.success)}
styles.button,
styles.call,
styles.success,
)}
onClick={(ev) => onClick={(ev) =>
stopPropagation(ev, openDM(user._id).then(connect)) stopPropagation(
ev,
user
.openDM()
.then(connect)
.then((x) => history.push(`/channel/${x._id}`)),
)
}> }>
<PhoneCall size={20} /> <PhoneCall size={20} />
</IconButton> </IconButton>
<IconButton <IconButton
type="circle" type="circle"
className={styles.button} className={styles.button}
onClick={(ev) => stopPropagation(ev, openDM(user._id))}> onClick={(ev) =>
stopPropagation(
ev,
user
.openDM()
.then((channel) =>
history.push(`/channel/${channel._id}`),
),
)
}>
<Envelope size={20} /> <Envelope size={20} />
</IconButton> </IconButton>
</>, </>,
); );
} }
if (user.relationship === Users.Relationship.Incoming) { if (user.relationship === RelationshipStatus.Incoming) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={styles.button} className={styles.button}
onClick={(ev) => onClick={(ev) => stopPropagation(ev, user.addFriend())}>
stopPropagation(ev, client.users.addFriend(user.username))
}>
<Plus size={24} /> <Plus size={24} />
</IconButton>, </IconButton>,
); );
...@@ -77,29 +84,33 @@ export function Friend({ user }: Props) { ...@@ -77,29 +84,33 @@ export function Friend({ user }: Props) {
subtext = <Text id="app.special.friends.incoming" />; subtext = <Text id="app.special.friends.incoming" />;
} }
if (user.relationship === Users.Relationship.Outgoing) { if (user.relationship === RelationshipStatus.Outgoing) {
subtext = <Text id="app.special.friends.outgoing" />; subtext = <Text id="app.special.friends.outgoing" />;
} }
if ( if (
user.relationship === Users.Relationship.Friend || user.relationship === RelationshipStatus.Friend ||
user.relationship === Users.Relationship.Outgoing || user.relationship === RelationshipStatus.Outgoing ||
user.relationship === Users.Relationship.Incoming user.relationship === RelationshipStatus.Incoming
) { ) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={classNames(styles.button, styles.error)} className={classNames(
styles.button,
styles.remove,
styles.error,
)}
onClick={(ev) => onClick={(ev) =>
stopPropagation( stopPropagation(
ev, ev,
user.relationship === Users.Relationship.Friend user.relationship === RelationshipStatus.Friend
? openScreen({ ? openScreen({
id: "special_prompt", id: "special_prompt",
type: "unfriend_user", type: "unfriend_user",
target: user, target: user,
}) })
: client.users.removeFriend(user._id), : user.removeFriend(),
) )
}> }>
<X size={24} /> <X size={24} />
...@@ -107,15 +118,13 @@ export function Friend({ user }: Props) { ...@@ -107,15 +118,13 @@ export function Friend({ user }: Props) {
); );
} }
if (user.relationship === Users.Relationship.Blocked) { if (user.relationship === RelationshipStatus.Blocked) {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
className={classNames(styles.button, styles.error)} className={classNames(styles.button, styles.error)}
onClick={(ev) => onClick={(ev) => stopPropagation(ev, user.unblockUser())}>
stopPropagation(ev, client.users.unblockUser(user._id)) <UserX size={24} />
}>
<X size={24} />
</IconButton>, </IconButton>,
); );
} }
...@@ -127,10 +136,10 @@ export function Friend({ user }: Props) { ...@@ -127,10 +136,10 @@ export function Friend({ user }: Props) {
onContextMenu={attachContextMenu("Menu", { user: user._id })}> onContextMenu={attachContextMenu("Menu", { user: user._id })}>
<UserIcon target={user} size={36} status /> <UserIcon target={user} size={36} status />
<div className={styles.name}> <div className={styles.name}>
<span>@{user.username}</span> <span>{user.username}</span>
{subtext && <span className={styles.subtext}>{subtext}</span>} {subtext && <span className={styles.subtext}>{subtext}</span>}
</div> </div>
<div className={styles.actions}>{actions}</div> <div className={styles.actions}>{actions}</div>
</div> </div>
); );
} });
import { import { ChevronRight } from "@styled-icons/boxicons-regular";
ChevronDown,
ChevronRight,
ListPlus,
} from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid"; import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
import { User, Users } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { RelationshipStatus, Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -13,65 +11,62 @@ import { TextReact } from "../../lib/i18n"; ...@@ -13,65 +11,62 @@ import { TextReact } from "../../lib/i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import { useUsers } from "../../context/revoltjs/hooks"; import { useClient } from "../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../components/common/CollapsibleSection"; import CollapsibleSection from "../../components/common/CollapsibleSection";
import Tooltip from "../../components/common/Tooltip"; import Tooltip from "../../components/common/Tooltip";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import Details from "../../components/ui/Details";
import Header from "../../components/ui/Header"; import Header from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton"; import IconButton from "../../components/ui/IconButton";
import Overline from "../../components/ui/Overline";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import { Friend } from "./Friend"; import { Friend } from "./Friend";
export default function Friends() { export default observer(() => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const users = useUsers() as User[]; const client = useClient();
const users = [...client.users.values()];
users.sort((a, b) => a.username.localeCompare(b.username)); users.sort((a, b) => a.username.localeCompare(b.username));
const friends = users.filter( const friends = users.filter(
(x) => x.relationship === Users.Relationship.Friend, (x) => x.relationship === RelationshipStatus.Friend,
); );
const lists = [ const lists = [
[ [
"", "",
users.filter((x) => x.relationship === Users.Relationship.Incoming), users.filter((x) => x.relationship === RelationshipStatus.Incoming),
], ],
[ [
"app.special.friends.sent", "app.special.friends.sent",
users.filter((x) => x.relationship === Users.Relationship.Outgoing), users.filter((x) => x.relationship === RelationshipStatus.Outgoing),
"outgoing", "outgoing",
], ],
[ [
"app.status.online", "app.status.online",
friends.filter( friends.filter(
(x) => (x) => x.online && x.status?.presence !== Presence.Invisible,
x.online && x.status?.presence !== Users.Presence.Invisible,
), ),
"online", "online",
], ],
[ [
"app.status.offline", "app.status.offline",
friends.filter( friends.filter(
(x) => (x) => !x.online || x.status?.presence === Presence.Invisible,
!x.online ||
x.status?.presence === Users.Presence.Invisible,
), ),
"offline", "offline",
], ],
[ [
"app.special.friends.blocked", "app.special.friends.blocked",
users.filter((x) => x.relationship === Users.Relationship.Blocked), users.filter((x) => x.relationship === RelationshipStatus.Blocked),
"blocked", "blocked",
], ],
] as [string, User[], string][]; ] as [string, User[], string][];
const incoming = lists[0][1]; const incoming = lists[0][1];
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>); const userlist: Children[] = incoming.map((x) => (
<b key={x._id}>{x.username}</b>
));
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", "); for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0; const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
...@@ -121,7 +116,10 @@ export default function Friends() { ...@@ -121,7 +116,10 @@ export default function Friends() {
*/} */}
</div> </div>
</Header> </Header>
<div className={styles.list} data-empty={isEmpty} data-mobile={isTouchscreenDevice}> <div
className={styles.list}
data-empty={isEmpty}
data-mobile={isTouchscreenDevice}>
{isEmpty && ( {isEmpty && (
<> <>
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" /> <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
...@@ -135,7 +133,7 @@ export default function Friends() { ...@@ -135,7 +133,7 @@ export default function Friends() {
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "pending_requests", id: "pending_requests",
users: incoming.map((x) => x._id), users: incoming,
}) })
}> }>
<div className={styles.avatars}> <div className={styles.avatars}>
...@@ -195,6 +193,7 @@ export default function Friends() { ...@@ -195,6 +193,7 @@ export default function Friends() {
return ( return (
<CollapsibleSection <CollapsibleSection
key={section_id}
id={`friends_${section_id}`} id={`friends_${section_id}`}
defaultValue={true} defaultValue={true}
sticky sticky
...@@ -213,4 +212,4 @@ export default function Friends() { ...@@ -213,4 +212,4 @@ export default function Friends() {
</div> </div>
</> </>
); );
} });