feat(mobx): migrate unreads to revolt.js

This commit is contained in:
Paul 2021-12-23 19:37:19 +00:00
parent 136238f62e
commit 6e1bcab92b
9 changed files with 117 additions and 147 deletions

View file

@ -143,7 +143,7 @@
"react-virtuoso": "^1.10.4", "react-virtuoso": "^1.10.4",
"redux": "^4.1.0", "redux": "^4.1.0",
"revolt-api": "0.5.3-alpha.10", "revolt-api": "0.5.3-alpha.10",
"revolt.js": "5.1.0-alpha.15", "revolt.js": "5.2.0-patch.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0", "shade-blend-color": "^1.0.0",

View file

@ -5,7 +5,7 @@ import {
Notepad, Notepad,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, Redirect, useLocation, useParams } from "react-router-dom"; import { Link, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users"; import { RelationshipStatus } from "revolt-api/types/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -16,48 +16,37 @@ import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import placeholderSVG from "../items/placeholder.svg"; import placeholderSVG from "../items/placeholder.svg";
import { mapChannelWithUnread, useUnreads } from "./common";
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import ButtonItem, { ChannelButton } from "../items/ButtonItem"; import ButtonItem, { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus"; import ConnectionStatus from "../items/ConnectionStatus";
type Props = { export default observer(() => {
unreads: Unreads;
};
const HomeSidebar = observer((props: Props) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
const layout = useApplicationState().layout; const state = useApplicationState();
const { channel } = useParams<{ channel: string }>(); const { channel: currentChannel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const channels = [...client.channels.values()] const channels = [...client.channels.values()].filter(
.filter( (x) => x.channel_type === "DirectMessage" || x.channel_type === "Group",
(x) => );
x.channel_type === "DirectMessage" ||
x.channel_type === "Group",
)
.map((x) => mapChannelWithUnread(x, props.unreads));
const obj = client.channels.get(channel); const obj = client.channels.get(currentChannel);
if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj });
// ! FIXME: move this globally
// Track what page the user was last on (in home page). // Track what page the user was last on (in home page).
useEffect(() => layout.setLastHomePath(pathname), [pathname]); useEffect(() => state.layout.setLastHomePath(pathname), [pathname]);
channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp)); channels.sort((b, a) =>
a.last_message_id_or_past.localeCompare(b.last_message_id_or_past),
);
return ( return (
<GenericSidebarBase mobilePadding> <GenericSidebarBase mobilePadding>
@ -127,31 +116,37 @@ const HomeSidebar = observer((props: Props) => {
{channels.length === 0 && ( {channels.length === 0 && (
<img src={placeholderSVG} loading="eager" /> <img src={placeholderSVG} loading="eager" />
)} )}
{channels.map((x) => { {channels.map((channel) => {
let user; let user;
if (x.channel.channel_type === "DirectMessage") { if (channel.channel_type === "DirectMessage") {
if (!x.channel.active) return null; if (!channel.active) return null;
user = x.channel.recipient; user = channel.recipient;
if (!user) { if (!user) return null;
console.warn(
`Skipped DM ${x.channel._id} because user was missing.`,
);
return null;
}
} }
const isUnread = channel.isUnread(state.notifications);
const mentionCount = channel.getMentions(
state.notifications,
).length;
return ( return (
<ConditionalLink <ConditionalLink
key={x.channel._id} key={channel._id}
active={x.channel._id === channel} active={channel._id === currentChannel}
to={`/channel/${x.channel._id}`}> to={`/channel/${channel._id}`}>
<ChannelButton <ChannelButton
user={user} user={user}
channel={x.channel} channel={channel}
alert={x.unread} alert={
alertCount={x.alertCount} mentionCount > 0
active={x.channel._id === channel} ? "mention"
: isUnread
? "unread"
: undefined
}
alertCount={mentionCount}
active={channel._id === currentChannel}
/> />
</ConditionalLink> </ConditionalLink>
); );
@ -161,13 +156,3 @@ const HomeSidebar = observer((props: Props) => {
</GenericSidebarBase> </GenericSidebarBase>
); );
}); });
export default connectState(
HomeSidebar,
(state) => {
return {
unreads: state.unreads,
};
},
true,
);

View file

@ -13,8 +13,6 @@ import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
@ -25,7 +23,6 @@ import UserHover from "../../common/user/UserHover";
import UserIcon from "../../common/user/UserIcon"; import UserIcon from "../../common/user/UserIcon";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
import LineDivider from "../../ui/LineDivider"; import LineDivider from "../../ui/LineDivider";
import { mapChannelWithUnread } from "./common";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
@ -193,47 +190,14 @@ function Swoosh() {
); );
} }
interface Props { export default observer(() => {
unreads: Unreads;
}
export const ServerListSidebar = observer(({ unreads }: Props) => {
const client = useClient(); const client = useClient();
const state = useApplicationState(); const state = useApplicationState();
const { server: server_id } = useParams<{ server?: string }>(); const { server: server_id } = useParams<{ server?: string }>();
const server = server_id ? client.servers.get(server_id) : undefined; const server = server_id ? client.servers.get(server_id) : undefined;
const activeServers = [...client.servers.values()]; const servers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) => const channels = [...client.channels.values()];
mapChannelWithUnread(x, unreads),
);
const unreadChannels = channels
.filter((x) => x.unread)
.filter((x) => !state.notifications.isMuted(x.channel))
.map((x) => x.channel?._id);
const servers = activeServers.map((server) => {
let alertCount = 0;
for (const id of server.channel_ids) {
const channel = channels.find((x) => x.channel?._id === id);
if (channel?.alertCount) {
alertCount += channel.alertCount;
}
}
return {
server,
unread: (typeof server.channel_ids.find((x) =>
unreadChannels.includes(x),
) !== "undefined"
? alertCount > 0
? "mention"
: "unread"
: undefined) as "mention" | "unread" | undefined,
alertCount,
};
});
const history = useHistory(); const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
@ -241,16 +205,16 @@ export const ServerListSidebar = observer(({ unreads }: Props) => {
let homeUnread: "mention" | "unread" | undefined; let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0; let alertCount = 0;
for (const x of channels) { for (const channel of channels) {
if (x.channel?.channel_type === "Group" && x.unread) { if (channel?.channel_type === "Group" && channel.unread) {
homeUnread = "unread"; homeUnread = "unread";
alertCount += x.alertCount ?? 0; alertCount += channel.mentions.length;
} }
if ( if (
x.channel?.channel_type === "DirectMessage" && channel.channel_type === "DirectMessage" &&
x.channel.active && channel.active &&
x.unread channel.unread
) { ) {
alertCount++; alertCount++;
} }
@ -294,32 +258,40 @@ export const ServerListSidebar = observer(({ unreads }: Props) => {
</ServerEntry> </ServerEntry>
</ConditionalLink> </ConditionalLink>
<LineDivider /> <LineDivider />
{servers.map((entry) => { {servers.map((server) => {
const active = entry.server._id === server?._id; const active = server._id === server_id;
const isUnread = server.isUnread(state.notifications);
const mentionCount = server.getMentions(
state.notifications,
).length;
return ( return (
<ConditionalLink <ConditionalLink
key={entry.server._id} key={server._id}
active={active} active={active}
to={state.layout.getServerPath(entry.server._id)}> to={state.layout.getServerPath(server._id)}>
<ServerEntry <ServerEntry
active={active} active={active}
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
server: entry.server._id, server: server._id,
unread: entry.unread, unread: isUnread,
})}> })}>
<Swoosh /> <Swoosh />
<Tooltip <Tooltip
content={entry.server.name} content={server.name}
placement="right"> placement="right">
<Icon <Icon
size={42} size={42}
unread={entry.unread} unread={
count={entry.alertCount}> mentionCount > 0
<ServerIcon ? "mention"
size={32} : isUnread
target={entry.server} ? "unread"
/> : undefined
}
count={mentionCount}>
<ServerIcon size={32} target={server} />
</Icon> </Icon>
</Tooltip> </Tooltip>
</ServerEntry> </ServerEntry>
@ -353,9 +325,3 @@ export const ServerListSidebar = observer(({ unreads }: Props) => {
</ServersBase> </ServersBase>
); );
}); });
export default connectState(ServerListSidebar, (state) => {
return {
unreads: state.unreads,
};
});

View file

@ -11,25 +11,17 @@ import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { Notifications } from "../../../redux/reducers/notifications";
import { Unreads } from "../../../redux/reducers/unreads";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader"; import ServerHeader from "../../common/ServerHeader";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import { mapChannelWithUnread, useUnreads } from "./common";
import { ChannelButton } from "../items/ButtonItem"; import { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus"; import ConnectionStatus from "../items/ConnectionStatus";
interface Props {
unreads: Unreads;
}
const ServerBase = styled.div` const ServerBase = styled.div`
height: 100%; height: 100%;
width: 240px; width: 240px;
@ -56,7 +48,7 @@ const ServerList = styled.div`
} }
`; `;
const ServerSidebar = observer((props: Props) => { export default observer(() => {
const client = useClient(); const client = useClient();
const state = useApplicationState(); const state = useApplicationState();
const { server: server_id, channel: channel_id } = const { server: server_id, channel: channel_id } =
@ -76,9 +68,7 @@ const ServerSidebar = observer((props: Props) => {
); );
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />; if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
// Handle unreads; FIXME: should definitely not be here // ! FIXME: move this globally
if (channel) useUnreads({ ...props, channel });
// Track which channel the user was last on. // Track which channel the user was last on.
useEffect(() => { useEffect(() => {
if (!channel_id) return; if (!channel_id) return;
@ -95,6 +85,8 @@ const ServerSidebar = observer((props: Props) => {
if (!entry) return; if (!entry) return;
const active = channel?._id === entry._id; const active = channel?._id === entry._id;
const isUnread = entry.isUnread(state.notifications);
const mentionCount = entry.getMentions(state.notifications);
return ( return (
<ConditionalLink <ConditionalLink
@ -115,8 +107,13 @@ const ServerSidebar = observer((props: Props) => {
<ChannelButton <ChannelButton
channel={entry} channel={entry}
active={active} active={active}
// ! FIXME: pull it out directly alert={
alert={mapChannelWithUnread(entry, props.unreads).unread} mentionCount.length > 0
? "mention"
: isUnread
? "unread"
: undefined
}
compact compact
muted={state.notifications.isMuted(entry)} muted={state.notifications.isMuted(entry)}
/> />
@ -161,10 +158,3 @@ const ServerSidebar = observer((props: Props) => {
</ServerBase> </ServerBase>
); );
}); });
export default connectState(ServerSidebar, (state) => {
return {
unreads: state.unreads,
notifications: state.notifications,
};
});

View file

@ -1,7 +1,7 @@
import { reaction } from "mobx"; import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect, useRef } from "preact/hooks"; import { useLayoutEffect } from "preact/hooks";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";

View file

@ -195,12 +195,17 @@ export default class NotificationOptions implements Store, Persistent<Data> {
* @returns Whether this object is muted * @returns Whether this object is muted
*/ */
isMuted(target?: Channel | Server) { isMuted(target?: Channel | Server) {
var value: NotificationState | undefined;
if (target instanceof Channel) { if (target instanceof Channel) {
return this.computeForChannel(target) === "muted"; value = this.computeForChannel(target);
} else if (target instanceof Server) { } else if (target instanceof Server) {
return this.computeForServer(target._id) === "muted"; value = this.computeForServer(target._id);
} else {
return false;
} }
if (value === "muted") {
return true;
}
return false;
} }
} }

View file

@ -41,6 +41,7 @@ export default class ServerConfig
*/ */
createClient() { createClient() {
const client = new Client({ const client = new Client({
unreads: true,
autoReconnect: false, autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL, apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV, debug: import.meta.env.DEV,

View file

@ -1,5 +1,6 @@
import { Hash } from "@styled-icons/boxicons-regular"; import { Hash } from "@styled-icons/boxicons-regular";
import { Ghost } from "@styled-icons/boxicons-solid"; import { Ghost } from "@styled-icons/boxicons-solid";
import { reaction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Channel as ChannelI } from "revolt.js/dist/maps/Channels"; import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
@ -10,7 +11,6 @@ import { useEffect, useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../mobx/State";
import { dispatch, getState } from "../../redux"; import { dispatch, getState } from "../../redux";
import { useClient } from "../../context/revoltjs/RevoltClient"; import { useClient } from "../../context/revoltjs/RevoltClient";
@ -93,6 +93,23 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true, getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true,
); );
// Mark channel as read.
useEffect(() => {
const checkUnread = () =>
channel.unread &&
channel.client.unreads!.markRead(
channel._id,
channel.last_message_id!,
true,
);
checkUnread();
return reaction(
() => channel.last_message_id,
() => checkUnread(),
);
}, [channel]);
return ( return (
<AgeGate <AgeGate
type="channel" type="channel"

View file

@ -3088,6 +3088,11 @@ lodash.defaultsdeep@^4.6.1:
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6" resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA== integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash.isequal@^4.5.0: lodash.isequal@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@ -3765,16 +3770,17 @@ revolt-api@^0.5.3-alpha.9:
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.9.tgz#46e75b7d8f9c6702df39039b829dddbb7897f237" resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.9.tgz#46e75b7d8f9c6702df39039b829dddbb7897f237"
integrity sha512-L8K9uPV3ME8bLdtWm8L9iPQvFM0GghA+5LzmWFjd6Gbn56u22ZYub2lABi4iHrWgeA2X41dGSsuSBgHSlts9Og== integrity sha512-L8K9uPV3ME8bLdtWm8L9iPQvFM0GghA+5LzmWFjd6Gbn56u22ZYub2lABi4iHrWgeA2X41dGSsuSBgHSlts9Og==
revolt.js@5.1.0-alpha.15: revolt.js@5.2.0-patch.0:
version "5.1.0-alpha.15" version "5.2.0-patch.0"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.1.0-alpha.15.tgz#a2be1f29de93f1ec18f0e502ecb65ade55c0070d" resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.0-patch.0.tgz#af6afc402399e5394b50b2e7d1573ff490fd3906"
integrity sha512-1gGcGDv1+J5NlmnX099XafKugCebACg9ke0NA754I4hLTNMMwkZyphyvYWWWkI394qn2mA3NG7WgEmrIoZUtgw== integrity sha512-PnHKqRpEvrBFm1xtLA/lGG5FIsp5kW4eB8sYiejjQCA1DWi7Xg6MNvyOjjha6jKftPXF8roivfZWEnM7sY1bnA==
dependencies: dependencies:
axios "^0.21.4" axios "^0.21.4"
eventemitter3 "^4.0.7" eventemitter3 "^4.0.7"
exponential-backoff "^3.1.0" exponential-backoff "^3.1.0"
isomorphic-ws "^4.0.1" isomorphic-ws "^4.0.1"
lodash.defaultsdeep "^4.6.1" lodash.defaultsdeep "^4.6.1"
lodash.flatten "^4.4.0"
lodash.isequal "^4.5.0" lodash.isequal "^4.5.0"
mobx "^6.3.2" mobx "^6.3.2"
revolt-api "^0.5.3-alpha.9" revolt-api "^0.5.3-alpha.9"