mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-09 16:53:36 -05:00
feat(mobx): add layout (paths + sections)
This commit is contained in:
parent
f87ecfcbd7
commit
a8491267a4
11 changed files with 208 additions and 132 deletions
|
@ -1,7 +1,6 @@
|
|||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||
|
||||
import { State, store } from "../../redux";
|
||||
import { Action } from "../../redux/reducers";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import Details from "../ui/Details";
|
||||
|
||||
|
@ -25,27 +24,14 @@ export default function CollapsibleSection({
|
|||
children,
|
||||
...detailsProps
|
||||
}: Props) {
|
||||
const state: State = store.getState();
|
||||
|
||||
function setState(state: boolean) {
|
||||
if (state === defaultValue) {
|
||||
store.dispatch({
|
||||
type: "SECTION_TOGGLE_UNSET",
|
||||
id,
|
||||
} as Action);
|
||||
} else {
|
||||
store.dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id,
|
||||
state,
|
||||
} as Action);
|
||||
}
|
||||
}
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
return (
|
||||
<Details
|
||||
open={state.sectionToggle[id] ?? defaultValue}
|
||||
onToggle={(e) => setState(e.currentTarget.open)}
|
||||
open={layout.getSectionState(id, defaultValue)}
|
||||
onToggle={(e) =>
|
||||
layout.setSectionState(id, e.currentTarget.open, defaultValue)
|
||||
}
|
||||
{...detailsProps}>
|
||||
<summary>
|
||||
<div class="padding">
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import ConditionalLink from "../../../lib/ConditionalLink";
|
|||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
@ -37,6 +38,7 @@ type Props = {
|
|||
const HomeSidebar = observer((props: Props) => {
|
||||
const { pathname } = useLocation();
|
||||
const client = useContext(AppContext);
|
||||
const layout = useApplicationState().layout;
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
|
@ -52,15 +54,8 @@ const HomeSidebar = observer((props: Props) => {
|
|||
if (channel && !obj) return <Redirect to="/" />;
|
||||
if (obj) useUnreads({ ...props, channel: obj });
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
dispatch({
|
||||
type: "LAST_OPENED_SET",
|
||||
parent: "home",
|
||||
child: channel,
|
||||
});
|
||||
}, [channel]);
|
||||
// Track what page the user was last on (in home page).
|
||||
useEffect(() => layout.setLastHomePath(pathname), [pathname]);
|
||||
|
||||
channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ import ConditionalLink from "../../../lib/ConditionalLink";
|
|||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { LastOpened } from "../../../redux/reducers/last_opened";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
@ -195,11 +195,11 @@ function Swoosh() {
|
|||
|
||||
interface Props {
|
||||
unreads: Unreads;
|
||||
lastOpened: LastOpened;
|
||||
}
|
||||
|
||||
export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
|
||||
export const ServerListSidebar = observer(({ unreads }: Props) => {
|
||||
const client = useClient();
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
const { server: server_id } = useParams<{ server?: string }>();
|
||||
const server = server_id ? client.servers.get(server_id) : undefined;
|
||||
|
@ -268,7 +268,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
|
|||
<ServerList>
|
||||
<ConditionalLink
|
||||
active={homeActive}
|
||||
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
|
||||
to={layout.getLastHomePath()}>
|
||||
<ServerEntry home active={homeActive}>
|
||||
<Swoosh />
|
||||
<div
|
||||
|
@ -295,15 +295,12 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
|
|||
<LineDivider />
|
||||
{servers.map((entry) => {
|
||||
const active = entry.server._id === server?._id;
|
||||
const id = lastOpened[entry.server._id];
|
||||
|
||||
return (
|
||||
<ConditionalLink
|
||||
key={entry.server._id}
|
||||
active={active}
|
||||
to={`/server/${entry.server._id}${
|
||||
id ? `/channel/${id}` : ""
|
||||
}`}>
|
||||
to={layout.getServerPath(entry.server._id)}>
|
||||
<ServerEntry
|
||||
active={active}
|
||||
onContextMenu={attachContextMenu("Menu", {
|
||||
|
@ -359,6 +356,5 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
|
|||
export default connectState(ServerListSidebar, (state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
lastOpened: state.lastOpened,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import PaintCounter from "../../../lib/PaintCounter";
|
|||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { Notifications } from "../../../redux/reducers/notifications";
|
||||
|
@ -58,6 +59,7 @@ const ServerList = styled.div`
|
|||
|
||||
const ServerSidebar = observer((props: Props) => {
|
||||
const client = useClient();
|
||||
const layout = useApplicationState().layout;
|
||||
const { server: server_id, channel: channel_id } =
|
||||
useParams<{ server: string; channel?: string }>();
|
||||
|
||||
|
@ -75,16 +77,15 @@ const ServerSidebar = observer((props: Props) => {
|
|||
);
|
||||
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
|
||||
|
||||
// Handle unreads; FIXME: should definitely not be here
|
||||
if (channel) useUnreads({ ...props, channel });
|
||||
|
||||
// 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!,
|
||||
});
|
||||
layout.setLastOpened(server_id, channel_id);
|
||||
}, [channel_id, server_id]);
|
||||
|
||||
const uncategorised = new Set(server.channel_ids);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useContext } from "preact/hooks";
|
|||
import Auth from "./stores/Auth";
|
||||
import Draft from "./stores/Draft";
|
||||
import Experiments from "./stores/Experiments";
|
||||
import Layout from "./stores/Layout";
|
||||
import LocaleOptions from "./stores/LocaleOptions";
|
||||
|
||||
interface StoreDefinition {
|
||||
|
@ -24,6 +25,7 @@ export default class State {
|
|||
draft: Draft;
|
||||
locale: LocaleOptions;
|
||||
experiments: Experiments;
|
||||
layout: Layout;
|
||||
|
||||
/**
|
||||
* Construct new State.
|
||||
|
@ -33,6 +35,7 @@ export default class State {
|
|||
this.draft = new Draft();
|
||||
this.locale = new LocaleOptions();
|
||||
this.experiments = new Experiments();
|
||||
this.layout = new Layout();
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import Persistent from "../Persistent";
|
||||
|
||||
interface Data {
|
||||
server?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of the last open channels, tabs, etc.
|
||||
* Handles providing good UX experience on navigating
|
||||
* back and forth between different parts of the app.
|
||||
*/
|
||||
export default class Experiments implements Persistent<Data> {
|
||||
private server: ObservableMap<string, string>;
|
||||
|
||||
/**
|
||||
* Construct new Experiments store.
|
||||
*/
|
||||
constructor() {
|
||||
this.server = new ObservableMap();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
server: this.server,
|
||||
};
|
||||
}
|
||||
|
||||
@action hydrate(data: Data) {
|
||||
if (data.server) {
|
||||
Object.keys(data.server).forEach((key) =>
|
||||
this.server.set(key, data.server![key]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last opened channel in a server.
|
||||
* @param server Server ID
|
||||
*/
|
||||
@computed get(server: string) {
|
||||
return this.server.get(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last opened channel in a server.
|
||||
* @param server Server ID
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
@action enable(server: string, channel: string) {
|
||||
this.server.set(server, channel);
|
||||
}
|
||||
}
|
161
src/mobx/stores/Layout.ts
Normal file
161
src/mobx/stores/Layout.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import Persistent from "../Persistent";
|
||||
|
||||
interface Data {
|
||||
lastSection?: "home" | "server";
|
||||
lastHomePath?: string;
|
||||
lastOpened?: Record<string, string>;
|
||||
openSections?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of the last open channels, tabs, etc.
|
||||
* Handles providing good UX experience on navigating
|
||||
* back and forth between different parts of the app.
|
||||
*/
|
||||
export default class Layout implements Persistent<Data> {
|
||||
/**
|
||||
* The last 'major section' that the user had open.
|
||||
* This is either the home tab or a channel ID (for a server channel).
|
||||
*/
|
||||
private lastSection: "home" | string;
|
||||
|
||||
/**
|
||||
* The last path the user had open in the home tab.
|
||||
*/
|
||||
private lastHomePath: string;
|
||||
|
||||
/**
|
||||
* Map of last channels viewed in servers.
|
||||
*/
|
||||
private lastOpened: ObservableMap<string, string>;
|
||||
|
||||
/**
|
||||
* Map of section IDs to their current state.
|
||||
*/
|
||||
private openSections: ObservableMap<string, boolean>;
|
||||
|
||||
/**
|
||||
* Construct new Layout store.
|
||||
*/
|
||||
constructor() {
|
||||
this.lastSection = "home";
|
||||
this.lastHomePath = "/";
|
||||
this.lastOpened = new ObservableMap();
|
||||
this.openSections = new ObservableMap();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
lastSection: this.lastSection,
|
||||
lastHomePath: this.lastHomePath,
|
||||
lastOpened: this.lastOpened,
|
||||
openSections: this.openSections,
|
||||
};
|
||||
}
|
||||
|
||||
@action hydrate(data: Data) {
|
||||
if (data.lastSection) {
|
||||
this.lastSection = data.lastSection;
|
||||
}
|
||||
|
||||
if (data.lastHomePath) {
|
||||
this.lastHomePath = data.lastHomePath;
|
||||
}
|
||||
|
||||
if (data.lastOpened) {
|
||||
Object.keys(data.lastOpened).forEach((key) =>
|
||||
this.lastOpened.set(key, data.lastOpened![key]),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.openSections) {
|
||||
Object.keys(data.openSections).forEach((key) =>
|
||||
this.openSections.set(key, data.openSections![key]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last 'major section' the user had open.
|
||||
* @returns Last open section
|
||||
*/
|
||||
@computed getLastSection() {
|
||||
return this.lastSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last opened channel in a server.
|
||||
* @param server Server ID
|
||||
*/
|
||||
@computed getLastOpened(server: string) {
|
||||
return this.lastOpened.get(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to a server (as seen on sidebar).
|
||||
* @param server Server ID
|
||||
* @returns Pathname
|
||||
*/
|
||||
@computed getServerPath(server: string) {
|
||||
let path = `/server/${server}`;
|
||||
if (this.lastOpened.has(server)) {
|
||||
path += `/channel/${this.getLastOpened(server)}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last opened channel in a server.
|
||||
* @param server Server ID
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
@action setLastOpened(server: string, channel: string) {
|
||||
this.lastOpened.set(server, channel);
|
||||
this.lastSection = "server";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last path the user had open in the home tab.
|
||||
* @returns Last home path
|
||||
*/
|
||||
@computed getLastHomePath() {
|
||||
return this.lastHomePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current path open in the home tab.
|
||||
* @param path Pathname
|
||||
*/
|
||||
@action setLastHomePath(path: string) {
|
||||
this.lastHomePath = path;
|
||||
this.lastSection = "home";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id Section ID
|
||||
* @returns Whether the section is open
|
||||
* @param def Default state value
|
||||
*/
|
||||
@computed getSectionState(id: string, def?: boolean) {
|
||||
return this.openSections.get(id) ?? def ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of a section.
|
||||
* @param id Section ID
|
||||
* @param value New state value
|
||||
* @param def Default state value
|
||||
*/
|
||||
@action setSectionState(id: string, value: boolean, def?: boolean) {
|
||||
if (value === def) {
|
||||
this.openSections.delete(id);
|
||||
} else {
|
||||
this.openSections.set(id, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,20 @@
|
|||
import { Hash } from "@styled-icons/boxicons-regular";
|
||||
import { Ghost } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { useState } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { dispatch, getState } from "../../redux";
|
||||
|
||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { Hash } from "@styled-icons/boxicons-regular";
|
||||
import { Ghost } from "@styled-icons/boxicons-solid";
|
||||
|
||||
import AgeGate from "../../components/common/AgeGate";
|
||||
import MessageBox from "../../components/common/messaging/MessageBox";
|
||||
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
|
||||
|
@ -52,19 +51,19 @@ const PlaceholderBase = styled.div`
|
|||
justify-content: center;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
|
||||
|
||||
.primary {
|
||||
color: var(--secondary-foreground);
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
|
||||
.secondary {
|
||||
color: var(--tertiary-foreground);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
margin: 2em auto;
|
||||
fill-opacity: 0.8;
|
||||
|
@ -94,7 +93,6 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true,
|
||||
);
|
||||
|
||||
const id = channel._id;
|
||||
return (
|
||||
<AgeGate
|
||||
type="channel"
|
||||
|
@ -126,7 +124,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
}}
|
||||
toggleChannelSidebar={() => {
|
||||
if (isTouchscreenDevice) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setChannels(!showChannels);
|
||||
|
@ -147,7 +145,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
/>
|
||||
<ChannelMain>
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={id} />
|
||||
<VoiceHeader id={channel._id} />
|
||||
<MessageArea channel={channel} />
|
||||
<TypingIndicator channel={channel} />
|
||||
<JumpToBottom channel={channel} />
|
||||
|
@ -173,13 +171,19 @@ function ChannelPlaceholder() {
|
|||
<PlaceholderBase>
|
||||
<Header placement="primary">
|
||||
<Hash size={24} />
|
||||
<span className="name"><Text id="app.main.channel.errors.nochannel" /></span>
|
||||
<span className="name">
|
||||
<Text id="app.main.channel.errors.nochannel" />
|
||||
</span>
|
||||
</Header>
|
||||
|
||||
<div className="placeholder">
|
||||
<Ghost width={80} />
|
||||
<div className="primary"><Text id="app.main.channel.errors.title" /></div>
|
||||
<div className="secondary"><Text id="app.main.channel.errors.nochannels" /></div>
|
||||
<div className="primary">
|
||||
<Text id="app.main.channel.errors.title" />
|
||||
</div>
|
||||
<div className="secondary">
|
||||
<Text id="app.main.channel.errors.nochannels" />
|
||||
</div>
|
||||
</div>
|
||||
</PlaceholderBase>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue