From f8611ddea56a0e8ae19c1a8cfd084e49fe576c0b Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Thu, 29 Jul 2021 15:51:19 +0100 Subject: [PATCH] Finish migrating user state over to MobX. --- src/components/common/AutoComplete.tsx | 15 +- src/components/common/messaging/Message.tsx | 203 ++++++++-------- .../common/messaging/SystemMessage.tsx | 226 +++++++++--------- .../messaging/attachments/MessageReply.tsx | 10 +- .../common/messaging/bars/ReplyBar.tsx | 14 +- .../common/messaging/bars/TypingIndicator.tsx | 20 +- src/components/common/user/UserCheckbox.tsx | 5 +- src/components/common/user/UserHeader.tsx | 8 +- src/components/common/user/UserShort.tsx | 4 +- .../navigation/BottomNavigation.tsx | 13 +- .../intermediate/popovers/UserPicker.tsx | 23 +- 11 files changed, 286 insertions(+), 255 deletions(-) diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx index 966894f..faca2ff 100644 --- a/src/components/common/AutoComplete.tsx +++ b/src/components/common/AutoComplete.tsx @@ -1,9 +1,13 @@ -import { SYSTEM_USER_ID, User } from "revolt.js"; +import { useStore } from "react-redux"; +import { SYSTEM_USER_ID } from "revolt.js"; import { Channels } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components"; import { StateUpdater, useState } from "preact/hooks"; +import { User } from "../../mobx"; +import { useData } from "../../mobx/State"; + import { useClient } from "../../context/revoltjs/RevoltClient"; import { emojiDictionary } from "../../assets/emojis"; @@ -53,6 +57,7 @@ export function useAutoComplete( const [state, setState] = useState<AutoCompleteState>({ type: "none" }); const [focused, setFocused] = useState(false); const client = useClient(); + const store = useData(); function findSearchString( el: HTMLTextAreaElement, @@ -127,7 +132,7 @@ export function useAutoComplete( let users: User[] = []; switch (searchClues.users.type) { case "all": - users = client.users.toArray(); + users = [...store.users.values()]; break; case "channel": { const channel = client.channels.get( @@ -136,8 +141,8 @@ export function useAutoComplete( switch (channel?.channel_type) { case "Group": case "DirectMessage": - users = client.users - .mapKeys(channel.recipients) + users = channel.recipients + .map((x) => store.users.get(x)) .filter( (x) => typeof x !== "undefined", ) as User[]; @@ -150,7 +155,7 @@ export function useAutoComplete( (x) => x._id.substr(0, 26) === server, ) .map((x) => - client.users.get(x._id.substr(26)), + store.users.get(x._id.substr(26)), ) .filter( (x) => typeof x !== "undefined", diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index c55d251..a7a92d3 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -1,7 +1,10 @@ +import { observer } from "mobx-react-lite"; + import { attachContextMenu } from "preact-context-menu"; import { memo } from "preact/compat"; import { useContext, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; import { QueuedMessage } from "../../../redux/reducers/queue"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; @@ -34,109 +37,117 @@ interface Props { head?: boolean; } -function Message({ - highlight, - attachContext, - message, - contrast, - content: replacement, - head: preferHead, - queued, -}: Props) { - // TODO: Can improve re-renders here by providing a list - // TODO: of dependencies. We only need to update on u/avatar. - const user = useUser(message.author); - const client = useContext(AppContext); - const { openScreen } = useIntermediate(); +const Message = observer( + ({ + highlight, + attachContext, + message, + contrast, + content: replacement, + head: preferHead, + queued, + }: Props) => { + const store = useData(); + const user = store.users.get(message.author); + + const client = useContext(AppContext); + const { openScreen } = useIntermediate(); - const content = message.content as string; - const head = preferHead || (message.replies && message.replies.length > 0); + const content = message.content as string; + const head = + preferHead || (message.replies && message.replies.length > 0); - // ! FIXME: tell fatal to make this type generic - // bree: Fatal please... - const userContext = attachContext - ? (attachContextMenu("Menu", { - user: message.author, - contextualChannel: message.channel, - }) as any) - : undefined; + // ! FIXME: tell fatal to make this type generic + // bree: Fatal please... + const userContext = attachContext + ? (attachContextMenu("Menu", { + user: message.author, + contextualChannel: message.channel, + }) as any) + : undefined; - const openProfile = () => - openScreen({ id: "profile", user_id: message.author }); + const openProfile = () => + openScreen({ id: "profile", user_id: message.author }); - // ! FIXME: animate on hover - const [animate, setAnimate] = useState(false); + // ! FIXME: animate on hover + const [animate, setAnimate] = useState(false); - return ( - <div id={message._id}> - {message.replies?.map((message_id, index) => ( - <MessageReply - index={index} - id={message_id} - channel={message.channel} - /> - ))} - <MessageBase - highlight={highlight} - head={head && !(message.replies && message.replies.length > 0)} - contrast={contrast} - sending={typeof queued !== "undefined"} - mention={message.mentions?.includes(client.user!._id)} - failed={typeof queued?.error !== "undefined"} - onContextMenu={ - attachContext - ? attachContextMenu("Menu", { - message, - contextualChannel: message.channel, - queued, - }) - : undefined - } - onMouseEnter={() => setAnimate(true)} - onMouseLeave={() => setAnimate(false)}> - <MessageInfo> - {head ? ( - <UserIcon - target={user} - size={36} - onContextMenu={userContext} - onClick={openProfile} - animate={animate} - /> - ) : ( - <MessageDetail message={message} position="left" /> - )} - </MessageInfo> - <MessageContent> - {head && ( - <span className="detail"> - <Username - className="author" - user={user} + return ( + <div id={message._id}> + {message.replies?.map((message_id, index) => ( + <MessageReply + index={index} + id={message_id} + channel={message.channel} + /> + ))} + <MessageBase + highlight={highlight} + head={ + head && !(message.replies && message.replies.length > 0) + } + contrast={contrast} + sending={typeof queued !== "undefined"} + mention={message.mentions?.includes(client.user!._id)} + failed={typeof queued?.error !== "undefined"} + onContextMenu={ + attachContext + ? attachContextMenu("Menu", { + message, + contextualChannel: message.channel, + queued, + }) + : undefined + } + onMouseEnter={() => setAnimate(true)} + onMouseLeave={() => setAnimate(false)}> + <MessageInfo> + {head ? ( + <UserIcon + target={user} + size={36} onContextMenu={userContext} onClick={openProfile} + animate={animate} /> - <MessageDetail message={message} position="top" /> - </span> - )} - {replacement ?? <Markdown content={content} />} - {queued?.error && ( - <Overline type="error" error={queued.error} /> - )} - {message.attachments?.map((attachment, index) => ( - <Attachment - key={index} - attachment={attachment} - hasContent={index > 0 || content.length > 0} - /> - ))} - {message.embeds?.map((embed, index) => ( - <Embed key={index} embed={embed} /> - ))} - </MessageContent> - </MessageBase> - </div> - ); -} + ) : ( + <MessageDetail message={message} position="left" /> + )} + </MessageInfo> + <MessageContent> + {head && ( + <span className="detail"> + <Username + className="author" + user={user} + onContextMenu={userContext} + onClick={openProfile} + /> + <MessageDetail + message={message} + position="top" + /> + </span> + )} + {replacement ?? <Markdown content={content} />} + {queued?.error && ( + <Overline type="error" error={queued.error} /> + )} + {message.attachments?.map((attachment, index) => ( + <Attachment + key={index} + attachment={attachment} + hasContent={index > 0 || content.length > 0} + /> + ))} + {message.embeds?.map((embed, index) => ( + <Embed key={index} embed={embed} /> + ))} + </MessageContent> + </MessageBase> + </div> + ); + }, +); export default memo(Message); diff --git a/src/components/common/messaging/SystemMessage.tsx b/src/components/common/messaging/SystemMessage.tsx index a19c0fe..59051ba 100644 --- a/src/components/common/messaging/SystemMessage.tsx +++ b/src/components/common/messaging/SystemMessage.tsx @@ -1,10 +1,13 @@ -import { User } from "revolt.js"; +import { observer } from "mobx-react-lite"; import styled from "styled-components"; import { attachContextMenu } from "preact-context-menu"; import { TextReact } from "../../../lib/i18n"; +import { User } from "../../../mobx"; +import { useData } from "../../../mobx/State"; + import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks"; import { MessageObject } from "../../../context/revoltjs/util"; @@ -39,132 +42,131 @@ interface Props { hideInfo?: boolean; } -export function SystemMessage({ - attachContext, - message, - highlight, - hideInfo, -}: Props) { - const ctx = useForceUpdate(); +export const SystemMessage = observer( + ({ attachContext, message, highlight, hideInfo }: Props) => { + const store = useData(); + + let data: SystemMessageParsed; + const content = message.content; + if (typeof content === "object") { + switch (content.type) { + case "text": + data = content; + break; + case "user_added": + case "user_remove": + data = { + type: content.type, + user: store.users.get(content.id)!, + by: store.users.get(content.by)!, + }; + break; + case "user_joined": + case "user_left": + case "user_kicked": + case "user_banned": + data = { + type: content.type, + user: store.users.get(content.id)!, + }; + break; + case "channel_renamed": + data = { + type: "channel_renamed", + name: content.name, + by: store.users.get(content.by)!, + }; + break; + case "channel_description_changed": + case "channel_icon_changed": + data = { + type: content.type, + by: store.users.get(content.by)!, + }; + break; + default: + data = { type: "text", content: JSON.stringify(content) }; + } + } else { + data = { type: "text", content }; + } - let data: SystemMessageParsed; - const content = message.content; - if (typeof content === "object") { - switch (content.type) { + let children; + switch (data.type) { case "text": - data = content; + children = <span>{data.content}</span>; break; case "user_added": case "user_remove": - data = { - type: content.type, - user: useUser(content.id, ctx) as User, - by: useUser(content.by, ctx) as User, - }; + children = ( + <TextReact + id={`app.main.channel.system.${ + data.type === "user_added" + ? "added_by" + : "removed_by" + }`} + fields={{ + user: <UserShort user={data.user} />, + other_user: <UserShort user={data.by} />, + }} + /> + ); break; case "user_joined": case "user_left": case "user_kicked": case "user_banned": - data = { - type: content.type, - user: useUser(content.id, ctx) as User, - }; + children = ( + <TextReact + id={`app.main.channel.system.${data.type}`} + fields={{ + user: <UserShort user={data.user} />, + }} + /> + ); break; case "channel_renamed": - data = { - type: "channel_renamed", - name: content.name, - by: useUser(content.by, ctx) as User, - }; + children = ( + <TextReact + id={`app.main.channel.system.channel_renamed`} + fields={{ + user: <UserShort user={data.by} />, + name: <b>{data.name}</b>, + }} + /> + ); break; case "channel_description_changed": case "channel_icon_changed": - data = { - type: content.type, - by: useUser(content.by, ctx) as User, - }; + children = ( + <TextReact + id={`app.main.channel.system.${data.type}`} + fields={{ + user: <UserShort user={data.by} />, + }} + /> + ); break; - default: - data = { type: "text", content: JSON.stringify(content) }; } - } else { - data = { type: "text", content }; - } - let children; - switch (data.type) { - case "text": - children = <span>{data.content}</span>; - break; - case "user_added": - case "user_remove": - children = ( - <TextReact - id={`app.main.channel.system.${ - data.type === "user_added" ? "added_by" : "removed_by" - }`} - fields={{ - user: <UserShort user={data.user} />, - other_user: <UserShort user={data.by} />, - }} - /> - ); - break; - case "user_joined": - case "user_left": - case "user_kicked": - case "user_banned": - children = ( - <TextReact - id={`app.main.channel.system.${data.type}`} - fields={{ - user: <UserShort user={data.user} />, - }} - /> - ); - break; - case "channel_renamed": - children = ( - <TextReact - id={`app.main.channel.system.channel_renamed`} - fields={{ - user: <UserShort user={data.by} />, - name: <b>{data.name}</b>, - }} - /> - ); - break; - case "channel_description_changed": - case "channel_icon_changed": - children = ( - <TextReact - id={`app.main.channel.system.${data.type}`} - fields={{ - user: <UserShort user={data.by} />, - }} - /> - ); - break; - } - - return ( - <MessageBase - highlight={highlight} - onContextMenu={ - attachContext - ? attachContextMenu("Menu", { - message, - contextualChannel: message.channel, - }) - : undefined - }> - {!hideInfo && ( - <MessageInfo> - <MessageDetail message={message} position="left" /> - </MessageInfo> - )} - <SystemContent>{children}</SystemContent> - </MessageBase> - ); -} + return ( + <MessageBase + highlight={highlight} + onContextMenu={ + attachContext + ? attachContextMenu("Menu", { + message, + contextualChannel: message.channel, + }) + : undefined + }> + {!hideInfo && ( + <MessageInfo> + <MessageDetail message={message} position="left" /> + </MessageInfo> + )} + <SystemContent>{children}</SystemContent> + </MessageBase> + ); + }, +); diff --git a/src/components/common/messaging/attachments/MessageReply.tsx b/src/components/common/messaging/attachments/MessageReply.tsx index 7cb3457..b849e3d 100644 --- a/src/components/common/messaging/attachments/MessageReply.tsx +++ b/src/components/common/messaging/attachments/MessageReply.tsx @@ -1,5 +1,6 @@ import { Reply } from "@styled-icons/boxicons-regular"; import { File } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; import { SYSTEM_USER_ID } from "revolt.js"; import { Users } from "revolt.js/dist/api/objects"; @@ -10,6 +11,8 @@ import { useLayoutEffect, useState } from "preact/hooks"; import { useRenderState } from "../../../../lib/renderer/Singleton"; +import { useData } from "../../../../mobx/State"; + import { useForceUpdate, useUser } from "../../../../context/revoltjs/hooks"; import { mapMessage, MessageObject } from "../../../../context/revoltjs/util"; @@ -120,7 +123,7 @@ export const ReplyBase = styled.div<{ `} `; -export function MessageReply({ index, channel, id }: Props) { +export const MessageReply = observer(({ index, channel, id }: Props) => { const ctx = useForceUpdate(); const view = useRenderState(channel); if (view?.type !== "RENDER") return null; @@ -152,7 +155,8 @@ export function MessageReply({ index, channel, id }: Props) { ); } - const user = useUser(message.author, ctx); + const store = useData(); + const user = store.users.get(message.author); const history = useHistory(); return ( @@ -203,4 +207,4 @@ export function MessageReply({ index, channel, id }: Props) { )} </ReplyBase> ); -} +}); diff --git a/src/components/common/messaging/bars/ReplyBar.tsx b/src/components/common/messaging/bars/ReplyBar.tsx index 6d7ed91..5de6906 100644 --- a/src/components/common/messaging/bars/ReplyBar.tsx +++ b/src/components/common/messaging/bars/ReplyBar.tsx @@ -4,6 +4,7 @@ import { File, XCircle, } from "@styled-icons/boxicons-regular"; +import { observer } from "mobx-react-lite"; import { SYSTEM_USER_ID } from "revolt.js"; import styled from "styled-components"; @@ -13,6 +14,7 @@ import { StateUpdater, useEffect } from "preact/hooks"; import { internalSubscribe } from "../../../../lib/eventEmitter"; import { useRenderState } from "../../../../lib/renderer/Singleton"; +import { useData } from "../../../../mobx/State"; import { Reply } from "../../../../redux/reducers/queue"; import { useUsers } from "../../../../context/revoltjs/hooks"; @@ -56,7 +58,7 @@ const Base = styled.div` // ! FIXME: Move to global config const MAX_REPLIES = 5; -export default function ReplyBar({ channel, replies, setReplies }: Props) { +export default observer(({ channel, replies, setReplies }: Props) => { useEffect(() => { return internalSubscribe( "ReplyBar", @@ -73,7 +75,9 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { const ids = replies.map((x) => x.id); const messages = view.messages.filter((x) => ids.includes(x._id)); - const users = useUsers(messages.map((x) => x.author)); + + const store = useData(); + const users = messages.map((x) => store.users.get(x.author)); return ( <div> @@ -90,9 +94,7 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { </span> ); - const user = users.find((x) => message!.author === x?._id); - if (!user) return; - + const user = users[index]; return ( <Base key={reply.id}> <ReplyBase preview> @@ -143,4 +145,4 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { })} </div> ); -} +}); diff --git a/src/components/common/messaging/bars/TypingIndicator.tsx b/src/components/common/messaging/bars/TypingIndicator.tsx index 6cbee0e..8c2fe0b 100644 --- a/src/components/common/messaging/bars/TypingIndicator.tsx +++ b/src/components/common/messaging/bars/TypingIndicator.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react-lite"; import { User } from "revolt.js"; import styled from "styled-components"; @@ -6,10 +7,14 @@ import { useContext } from "preact/hooks"; import { TextReact } from "../../../../lib/i18n"; +import { useData } from "../../../../mobx/State"; import { connectState } from "../../../../redux/connector"; import { TypingUser } from "../../../../redux/reducers/typing"; -import { AppContext } from "../../../../context/revoltjs/RevoltClient"; +import { + AppContext, + useClient, +} from "../../../../context/revoltjs/RevoltClient"; import { useUsers } from "../../../../context/revoltjs/hooks"; import { Username } from "../../user/UserShort"; @@ -61,12 +66,13 @@ const Base = styled.div` } `; -export function TypingIndicator({ typing }: Props) { +export const TypingIndicator = observer(({ typing }: Props) => { if (typing && typing.length > 0) { - const client = useContext(AppContext); - const users = useUsers(typing.map((x) => x.id)).filter( - (x) => typeof x !== "undefined", - ) as User[]; + const client = useClient(); + const store = useData(); + const users = typing + .map((x) => store.users.get(x.id)!) + .filter((x) => typeof x !== "undefined"); users.sort((a, b) => a._id.toUpperCase().localeCompare(b._id.toUpperCase()), @@ -123,7 +129,7 @@ export function TypingIndicator({ typing }: Props) { } return null; -} +}); export default connectState<{ id: string }>(TypingIndicator, (state, props) => { return { diff --git a/src/components/common/user/UserCheckbox.tsx b/src/components/common/user/UserCheckbox.tsx index c125afd..6f88230 100644 --- a/src/components/common/user/UserCheckbox.tsx +++ b/src/components/common/user/UserCheckbox.tsx @@ -1,8 +1,9 @@ -import { User } from "revolt.js"; +import { User } from "../../../mobx"; import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; import UserIcon from "./UserIcon"; +import { Username } from "./UserShort"; type UserProps = Omit<CheckboxProps, "children"> & { user: User }; @@ -10,7 +11,7 @@ export default function UserCheckbox({ user, ...props }: UserProps) { return ( <Checkbox {...props}> <UserIcon target={user} size={32} /> - {user.username} + <Username user={user} /> </Checkbox> ); } diff --git a/src/components/common/user/UserHeader.tsx b/src/components/common/user/UserHeader.tsx index cd63f6c..d45bfbd 100644 --- a/src/components/common/user/UserHeader.tsx +++ b/src/components/common/user/UserHeader.tsx @@ -1,6 +1,6 @@ import { Cog } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; import { Link } from "react-router-dom"; -import { User } from "revolt.js"; import styled from "styled-components"; import { openContextMenu } from "preact-context-menu"; @@ -9,6 +9,8 @@ import { Localizer } from "preact-i18n"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; +import { User } from "../../../mobx"; + import { useIntermediate } from "../../../context/intermediate/Intermediate"; import Header from "../../ui/Header"; @@ -49,7 +51,7 @@ interface Props { user: User; } -export default function UserHeader({ user }: Props) { +export default observer(({ user }: Props) => { const { writeClipboard } = useIntermediate(); return ( @@ -81,4 +83,4 @@ export default function UserHeader({ user }: Props) { )} </Header> ); -} +}); diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index 2ca54e9..3eab5a1 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -21,7 +21,7 @@ export const Username = observer( let username = user?.username; let color; - // ! FIXME: this must be really bad for perf. + /* // ! FIXME: this must be really bad for perf. if (user) { let { server } = useParams<{ server?: string }>(); if (server) { @@ -44,7 +44,7 @@ export const Username = observer( } } } - } + } */ return ( <span {...otherProps} style={{ color }}> diff --git a/src/components/navigation/BottomNavigation.tsx b/src/components/navigation/BottomNavigation.tsx index 755c09a..71a1a51 100644 --- a/src/components/navigation/BottomNavigation.tsx +++ b/src/components/navigation/BottomNavigation.tsx @@ -1,14 +1,16 @@ import { Search } from "@styled-icons/boxicons-regular"; import { Message, Group, Inbox } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; import { useHistory, useLocation } from "react-router"; import styled, { css } from "styled-components"; import ConditionalLink from "../../lib/ConditionalLink"; +import { useData } from "../../mobx/State"; import { connectState } from "../../redux/connector"; import { LastOpened } from "../../redux/reducers/last_opened"; -import { useSelf } from "../../context/revoltjs/hooks"; +import { useClient } from "../../context/revoltjs/RevoltClient"; import UserIcon from "../common/user/UserIcon"; import IconButton from "../ui/IconButton"; @@ -51,8 +53,11 @@ interface Props { lastOpened: LastOpened; } -export function BottomNavigation({ lastOpened }: Props) { - const user = useSelf(); +export const BottomNavigation = observer(({ lastOpened }: Props) => { + const client = useClient(); + const store = useData(); + const user = store.users.get(client.user!._id); + const history = useHistory(); const path = useLocation().pathname; @@ -114,7 +119,7 @@ export function BottomNavigation({ lastOpened }: Props) { </Navbar> </Base> ); -} +}); export default connectState(BottomNavigation, (state) => { return { diff --git a/src/context/intermediate/popovers/UserPicker.tsx b/src/context/intermediate/popovers/UserPicker.tsx index 748bdc0..3eaeb1d 100644 --- a/src/context/intermediate/popovers/UserPicker.tsx +++ b/src/context/intermediate/popovers/UserPicker.tsx @@ -1,14 +1,14 @@ -import { User, Users } from "revolt.js/dist/api/objects"; +import { Users } from "revolt.js/dist/api/objects"; import styles from "./UserPicker.module.scss"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + import UserCheckbox from "../../../components/common/user/UserCheckbox"; import Modal from "../../../components/ui/Modal"; -import { useUsers } from "../../revoltjs/hooks"; - interface Props { omit?: string[]; onClose: () => void; @@ -19,7 +19,7 @@ export function UserPicker(props: Props) { const [selected, setSelected] = useState<string[]>([]); const omit = [...(props.omit || []), "00000000000000000000000000"]; - const users = useUsers(); + const store = useData(); return ( <Modal @@ -33,24 +33,17 @@ export function UserPicker(props: Props) { }, ]}> <div className={styles.list}> - {( - users.filter( + {[...store.users.values()] + .filter( (x) => x && x.relationship === Users.Relationship.Friend && !omit.includes(x._id), - ) as User[] - ) - .map((x) => { - return { - ...x, - selected: selected.includes(x._id), - }; - }) + ) .map((x) => ( <UserCheckbox user={x} - checked={x.selected} + checked={selected.includes(x._id)} onChange={(v) => { if (v) { setSelected([...selected, x._id]); -- GitLab