Merge branch 'mobx'

This commit is contained in:
Paul
2021-12-24 11:45:49 +00:00
115 changed files with 3973 additions and 3311 deletions

View File

@@ -5,8 +5,7 @@ import styled, { css } from "styled-components";
import ConditionalLink from "../../lib/ConditionalLink";
import { connectState } from "../../redux/connector";
import { LastOpened } from "../../redux/reducers/last_opened";
import { useApplicationState } from "../../mobx/State";
import { useClient } from "../../context/revoltjs/RevoltClient";
@@ -47,19 +46,14 @@ const Button = styled.a<{ active: boolean }>`
`}
`;
interface Props {
lastOpened: LastOpened;
}
export const BottomNavigation = observer(({ lastOpened }: Props) => {
export default observer(() => {
const client = useClient();
const layout = useApplicationState().layout;
const user = client.users.get(client.user!._id);
const history = useHistory();
const path = useLocation().pathname;
const channel_id = lastOpened["home"];
const friendsActive = path.startsWith("/friends");
const settingsActive = path.startsWith("/settings");
const homeActive = !(friendsActive || settingsActive);
@@ -73,14 +67,11 @@ export const BottomNavigation = observer(({ lastOpened }: Props) => {
if (settingsActive) {
if (history.length > 0) {
history.goBack();
return;
}
}
if (channel_id) {
history.push(`/channel/${channel_id}`);
} else {
history.push("/");
}
history.push(layout.getLastHomePath());
}}>
<Message size={24} />
</IconButton>
@@ -117,9 +108,3 @@ export const BottomNavigation = observer(({ lastOpened }: Props) => {
</Base>
);
});
export default connectState(BottomNavigation, (state) => {
return {
lastOpened: state.lastOpened,
};
});

View File

@@ -1,14 +1,16 @@
import { Route, Switch } from "react-router";
import { useApplicationState } from "../../mobx/State";
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
import SidebarBase from "./SidebarBase";
import HomeSidebar from "./left/HomeSidebar";
import ServerListSidebar from "./left/ServerListSidebar";
import ServerSidebar from "./left/ServerSidebar";
import { useSelector } from "react-redux";
import { State } from "../../redux";
export default function LeftSidebar() {
const isOpen = useSelector((state: State) => state.sectionToggle['sidebar_channels'] ?? true)
const layout = useApplicationState().layout;
const isOpen = layout.getSectionState(SIDEBAR_CHANNELS, true);
return (
<SidebarBase>

View File

@@ -117,7 +117,7 @@
}
&[data-muted="true"] {
color: var(--tertiary-foreground);
opacity: 0.4;
}
&[data-alert="true"],

View File

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

View File

@@ -12,9 +12,7 @@ import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { connectState } from "../../../redux/connector";
import { LastOpened } from "../../../redux/reducers/last_opened";
import { Unreads } from "../../../redux/reducers/unreads";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient";
@@ -25,7 +23,6 @@ import UserHover from "../../common/user/UserHover";
import UserIcon from "../../common/user/UserIcon";
import IconButton from "../../ui/IconButton";
import LineDivider from "../../ui/LineDivider";
import { mapChannelWithUnread } from "./common";
import { Children } from "../../../types/Preact";
@@ -195,46 +192,14 @@ function Swoosh() {
);
}
interface Props {
unreads: Unreads;
lastOpened: LastOpened;
}
export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
export default observer(() => {
const client = useClient();
const state = useApplicationState();
const { server: server_id } = useParams<{ server?: string }>();
const server = server_id ? client.servers.get(server_id) : undefined;
const activeServers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) =>
mapChannelWithUnread(x, unreads),
);
const unreadChannels = channels
.filter((x) => x.unread)
.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 servers = [...client.servers.values()];
const channels = [...client.channels.values()];
const history = useHistory();
const path = useLocation().pathname;
@@ -242,16 +207,16 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0;
for (const x of channels) {
if (x.channel?.channel_type === "Group" && x.unread) {
for (const channel of channels) {
if (channel?.channel_type === "Group" && channel.unread) {
homeUnread = "unread";
alertCount += x.alertCount ?? 0;
alertCount += channel.mentions.length;
}
if (
x.channel?.channel_type === "DirectMessage" &&
x.channel.active &&
x.unread
channel.channel_type === "DirectMessage" &&
channel.active &&
channel.unread
) {
alertCount++;
}
@@ -270,7 +235,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
<ServerList>
<ConditionalLink
active={homeActive}
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
to={state.layout.getLastHomePath()}>
<ServerEntry home active={homeActive}>
<Swoosh />
<div
@@ -278,13 +243,13 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
onClick={() =>
homeActive && history.push("/settings")
}>
<UserHover user={client.user}>
<UserHover user={client.user ?? undefined}>
<Icon
size={42}
unread={homeUnread}
count={alertCount}>
<UserIcon
target={client.user}
target={client.user ?? undefined}
size={32}
status
hover
@@ -295,35 +260,40 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
</ServerEntry>
</ConditionalLink>
<LineDivider />
{servers.map((entry) => {
const active = entry.server._id === server?._id;
const id = lastOpened[entry.server._id];
{servers.map((server) => {
const active = server._id === server_id;
const isUnread = server.isUnread(state.notifications);
const mentionCount = server.getMentions(
state.notifications,
).length;
return (
<ConditionalLink
key={entry.server._id}
key={server._id}
active={active}
to={`/server/${entry.server._id}${
id ? `/channel/${id}` : ""
}`}>
to={state.layout.getServerPath(server._id)}>
<ServerEntry
active={active}
onContextMenu={attachContextMenu("Menu", {
server: entry.server._id,
unread: entry.unread,
server: server._id,
unread: isUnread,
})}>
<Swoosh />
<Tooltip
content={entry.server.name}
content={server.name}
placement="right">
<Icon
size={42}
unread={entry.unread}
count={entry.alertCount}>
<ServerIcon
size={32}
target={entry.server}
/>
unread={
mentionCount > 0
? "mention"
: isUnread
? "unread"
: undefined
}
count={mentionCount}>
<ServerIcon size={32} target={server} />
</Icon>
</Tooltip>
</ServerEntry>
@@ -357,10 +327,3 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
</ServersBase>
);
});
export default connectState(ServerListSidebar, (state) => {
return {
unreads: state.unreads,
lastOpened: state.lastOpened,
};
});

View File

@@ -10,26 +10,17 @@ import PaintCounter from "../../../lib/PaintCounter";
import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { Notifications } from "../../../redux/reducers/notifications";
import { Unreads } from "../../../redux/reducers/unreads";
import { useApplicationState } from "../../../mobx/State";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader";
import Category from "../../ui/Category";
import { mapChannelWithUnread, useUnreads } from "./common";
import { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus";
interface Props {
unreads: Unreads;
notifications: Notifications;
}
const ServerBase = styled.div`
height: 100%;
width: 232px;
@@ -57,8 +48,9 @@ const ServerList = styled.div`
}
`;
const ServerSidebar = observer((props: Props) => {
export default observer(() => {
const client = useClient();
const state = useApplicationState();
const { server: server_id, channel: channel_id } =
useParams<{ server: string; channel?: string }>();
@@ -76,16 +68,13 @@ const ServerSidebar = observer((props: Props) => {
);
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel });
// ! FIXME: move this globally
// Track which channel the user was last on.
useEffect(() => {
if (!channel_id) return;
if (!server_id) return;
dispatch({
type: "LAST_OPENED_SET",
parent: server_id!,
child: channel_id!,
});
state.layout.setLastOpened(server_id, channel_id);
}, [channel_id, server_id]);
const uncategorised = new Set(server.channel_ids);
@@ -96,7 +85,8 @@ const ServerSidebar = observer((props: Props) => {
if (!entry) return;
const active = channel?._id === entry._id;
const muted = props.notifications[id] === "none";
const isUnread = entry.isUnread(state.notifications);
const mentionCount = entry.getMentions(state.notifications);
return (
<ConditionalLink
@@ -117,10 +107,15 @@ const ServerSidebar = observer((props: Props) => {
<ChannelButton
channel={entry}
active={active}
// ! FIXME: pull it out directly
alert={mapChannelWithUnread(entry, props.unreads).unread}
alert={
mentionCount.length > 0
? "mention"
: isUnread
? "unread"
: undefined
}
compact
muted={muted}
muted={state.notifications.isMuted(entry)}
/>
</ConditionalLink>
);
@@ -163,10 +158,3 @@ const ServerSidebar = observer((props: Props) => {
</ServerBase>
);
});
export default connectState(ServerSidebar, (state) => {
return {
unreads: state.unreads,
notifications: state.notifications,
};
});

View File

@@ -1,79 +0,0 @@
import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect, useRef } from "preact/hooks";
import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads";
type UnreadProps = {
channel: Channel;
unreads: Unreads;
};
export function useUnreads({ channel, unreads }: UnreadProps) {
// const firstLoad = useRef(true);
useLayoutEffect(() => {
function checkUnread(target: Channel) {
if (!target) return;
if (target._id !== channel._id) return;
if (
target.channel_type === "SavedMessages" ||
target.channel_type === "VoiceChannel"
)
return;
const unread = unreads[channel._id]?.last_id;
if (target.last_message_id) {
if (
!unread ||
(unread && target.last_message_id.localeCompare(unread) > 0)
) {
dispatch({
type: "UNREADS_MARK_READ",
channel: channel._id,
message: target.last_message_id,
});
channel.ack(target.last_message_id);
}
}
}
checkUnread(channel);
return reaction(
() => channel.last_message,
() => checkUnread(channel),
);
}, [channel, unreads]);
}
export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
const last_message_id = channel.last_message_id;
let unread: "mention" | "unread" | undefined;
let alertCount: undefined | number;
if (last_message_id && unreads) {
const u = unreads[channel._id];
if (u) {
if (u.mentions && u.mentions.length > 0) {
alertCount = u.mentions.length;
unread = "mention";
} else if (
u.last_id &&
(last_message_id as string).localeCompare(u.last_id) > 0
) {
unread = "unread";
}
} else {
unread = "unread";
}
}
return {
channel,
timestamp: last_message_id ?? channel._id,
unread,
alertCount,
};
}