diff --git a/.prettierrc.js b/.prettierrc.js index 2d694ad6..cc690915 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,8 +1,14 @@ module.exports = { - "tabWidth": 4, - "trailingComma": "all", - "jsxBracketSameLine": true, - "importOrder": ["preact|classnames|.scss$", "/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"], - "importOrderSeparation": true, -} - \ No newline at end of file + tabWidth: 4, + trailingComma: "all", + jsxBracketSameLine: true, + importOrder: [ + "preact|classnames|.scss$", + "/(lib)", + "/(redux|mobx)", + "/(context)", + "/(ui|common)|.svg$", + "^[./]", + ], + importOrderSeparation: true, +}; diff --git a/src/components/common/user/UserHover.tsx b/src/components/common/user/UserHover.tsx index 07b8a37c..fbff942d 100644 --- a/src/components/common/user/UserHover.tsx +++ b/src/components/common/user/UserHover.tsx @@ -1,7 +1,7 @@ -import { InfoCircle } from "@styled-icons/boxicons-regular"; -import { User } from "revolt.js"; import styled from "styled-components"; +import { User } from "../../../mobx"; + import { Children } from "../../../types/Preact"; import Tooltip from "../Tooltip"; import { Username } from "./UserShort"; diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx index 15e1a002..1aea5f2b 100644 --- a/src/components/common/user/UserIcon.tsx +++ b/src/components/common/user/UserIcon.tsx @@ -1,5 +1,5 @@ import { MicrophoneOff } from "@styled-icons/boxicons-regular"; -import { User } from "revolt.js"; +import { observer } from "mobx-react-lite"; import { Users } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components"; @@ -8,6 +8,7 @@ import { useContext } from "preact/hooks"; import { ThemeContext } from "../../../context/Theme"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; +import { User } from "../../../mobx"; import IconBase, { IconBaseProps } from "../IconBase"; import fallback from "../assets/user.png"; @@ -50,55 +51,63 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>` `} `; -export default function UserIcon( - props: Props & Omit, keyof Props>, -) { - const client = useContext(AppContext); +export default observer( + (props: Props & Omit, keyof Props>) => { + const client = useContext(AppContext); - const { - target, - attachment, - size, - voice, - status, - animate, - mask, - children, - as, - ...svgProps - } = props; - const iconURL = - client.generateFileURL( - target?.avatar ?? attachment, - { max_side: 256 }, + const { + target, + attachment, + size, + voice, + status, animate, - ) ?? (target ? client.users.getDefaultAvatarURL(target._id) : fallback); + mask, + children, + as, + ...svgProps + } = props; + const iconURL = + client.generateFileURL( + target?.avatar ?? attachment, + { max_side: 256 }, + animate, + ) ?? + (target ? client.users.getDefaultAvatarURL(target._id) : fallback); - return ( - + ); + }, +); diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index ad14e513..2ca54e93 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -1,8 +1,10 @@ +import { observer } from "mobx-react-lite"; import { useParams } from "react-router-dom"; -import { User } from "revolt.js"; import { Text } from "preact-i18n"; +import { User } from "../../../mobx"; + import { useForceUpdate, useMember, @@ -11,44 +13,46 @@ import { import UserIcon from "./UserIcon"; -export function Username({ - user, - ...otherProps -}: { user?: User } & JSX.HTMLAttributes) { - let username = user?.username; - let color; +export const Username = observer( + ({ + user, + ...otherProps + }: { user?: User } & JSX.HTMLAttributes) => { + let username = user?.username; + let color; - // ! FIXME: this must be really bad for perf. - if (user) { - let { server } = useParams<{ server?: string }>(); - if (server) { - let ctx = useForceUpdate(); - let member = useMember(`${server}${user._id}`, ctx); - if (member) { - if (member.nickname) { - username = member.nickname; - } + // ! FIXME: this must be really bad for perf. + if (user) { + let { server } = useParams<{ server?: string }>(); + if (server) { + let ctx = useForceUpdate(); + let member = useMember(`${server}${user._id}`, ctx); + if (member) { + if (member.nickname) { + username = member.nickname; + } - if (member.roles && member.roles.length > 0) { - let s = useServer(server, ctx); - for (let role of member.roles) { - let c = s?.roles?.[role].colour; - if (c) { - color = c; - continue; + if (member.roles && member.roles.length > 0) { + let s = useServer(server, ctx); + for (let role of member.roles) { + let c = s?.roles?.[role].colour; + if (c) { + color = c; + continue; + } } } } } } - } - return ( - - {username ?? } - - ); -} + return ( + + {username ?? } + + ); + }, +); export default function UserShort({ user, diff --git a/src/components/common/user/UserStatus.tsx b/src/components/common/user/UserStatus.tsx index 2ff2ce24..8c95d043 100644 --- a/src/components/common/user/UserStatus.tsx +++ b/src/components/common/user/UserStatus.tsx @@ -1,8 +1,9 @@ -import { User } from "revolt.js"; +import { observer } from "mobx-react-lite"; import { Users } from "revolt.js/dist/api/objects"; import { Text } from "preact-i18n"; +import { User } from "../../../mobx"; import Tooltip from "../Tooltip"; interface Props { @@ -10,7 +11,7 @@ interface Props { tooltip?: boolean; } -export default function UserStatus({ user, tooltip }: Props) { +export default observer(({ user, tooltip }: Props) => { if (user?.online) { if (user.status?.text) { if (tooltip) { @@ -40,4 +41,4 @@ export default function UserStatus({ user, tooltip }: Props) { } return ; -} +}); diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx index 5fdd63ff..d19290d3 100644 --- a/src/components/navigation/items/ButtonItem.tsx +++ b/src/components/navigation/items/ButtonItem.tsx @@ -1,4 +1,5 @@ import { X, Crown } from "@styled-icons/boxicons-regular"; +import { observer } from "mobx-react-lite"; import { Channels, Users } from "revolt.js/dist/api/objects"; import styles from "./Item.module.scss"; @@ -9,6 +10,8 @@ import { Localizer, Text } from "preact-i18n"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { stopPropagation } from "../../../lib/stopPropagation"; +import { User } from "../../../mobx"; + import { useIntermediate } from "../../../context/intermediate/Intermediate"; import ChannelIcon from "../../common/ChannelIcon"; @@ -30,12 +33,12 @@ type CommonProps = Omit< }; type UserProps = CommonProps & { - user: Users.User; + user: User; context?: Channels.Channel; channel?: Channels.DirectMessageChannel; }; -export function UserButton(props: UserProps) { +export const UserButton = observer((props: UserProps) => { const { active, alert, alertCount, user, context, channel, ...divProps } = props; const { openScreen } = useIntermediate(); @@ -109,11 +112,11 @@ export function UserButton(props: UserProps) { ); -} +}); type ChannelProps = CommonProps & { channel: Channels.Channel & { unread?: string }; - user?: Users.User; + user?: User; compact?: boolean; }; diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx index 12b51daf..b1203138 100644 --- a/src/components/navigation/left/HomeSidebar.tsx +++ b/src/components/navigation/left/HomeSidebar.tsx @@ -4,6 +4,7 @@ import { Wrench, Notepad, } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; import { Link, Redirect, useLocation, useParams } from "react-router-dom"; import { Channels } from "revolt.js/dist/api/objects"; import { Users as UsersNS } from "revolt.js/dist/api/objects"; @@ -15,6 +16,7 @@ import ConditionalLink from "../../../lib/ConditionalLink"; import PaintCounter from "../../../lib/PaintCounter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; +import { useData } from "../../../mobx/State"; import { dispatch } from "../../../redux"; import { connectState } from "../../../redux/connector"; import { Unreads } from "../../../redux/reducers/unreads"; @@ -39,7 +41,7 @@ type Props = { unreads: Unreads; }; -function HomeSidebar(props: Props) { +const HomeSidebar = observer((props: Props) => { const { pathname } = useLocation(); const client = useContext(AppContext); const { channel } = useParams<{ channel: string }>(); @@ -66,7 +68,7 @@ function HomeSidebar(props: Props) { .filter((x) => x.channel_type !== "SavedMessages") .map((x) => mapChannelWithUnread(x, props.unreads)); - const users = useUsers(undefined, ctx); + const store = useData(); channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp)); return ( @@ -89,7 +91,7 @@ function HomeSidebar(props: Props) { user?.relationship === UsersNS.Relationship.Incoming, @@ -143,7 +145,7 @@ function HomeSidebar(props: Props) { if (!x.active) return null; const recipient = client.channels.getRecipient(x._id); - user = users.find((x) => x?._id === recipient); + user = store.users.get(recipient); if (!user) { console.warn( @@ -171,7 +173,7 @@ function HomeSidebar(props: Props) { ); -} +}); export default connectState( HomeSidebar, diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx index 881d6114..14f24322 100644 --- a/src/components/navigation/left/ServerListSidebar.tsx +++ b/src/components/navigation/left/ServerListSidebar.tsx @@ -1,4 +1,5 @@ import { Plus } from "@styled-icons/boxicons-regular"; +import { observer } from "mobx-react-lite"; import { useLocation, useParams } from "react-router-dom"; import { Channel, Servers, Users } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components"; @@ -9,17 +10,17 @@ import ConditionalLink from "../../../lib/ConditionalLink"; import PaintCounter from "../../../lib/PaintCounter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; +import { useData } 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"; +import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useChannels, useForceUpdate, - useSelf, useServers, - useUsers, } from "../../../context/revoltjs/hooks"; import logoSVG from "../../../assets/logo.svg"; @@ -178,14 +179,16 @@ interface Props { lastOpened: LastOpened; } -export function ServerListSidebar({ unreads, lastOpened }: Props) { +export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { + const store = useData(); + const client = useClient(); + const self = store.users.get(client.user!._id); + const ctx = useForceUpdate(); - const self = useSelf(ctx); const activeServers = useServers(undefined, ctx) as Servers.Server[]; const channels = (useChannels(undefined, ctx) as Channel[]).map((x) => mapChannelWithUnread(x, unreads), ); - const users = useUsers(undefined, ctx); const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id); @@ -230,7 +233,11 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { } } - if (users.find((x) => x?.relationship === Users.Relationship.Incoming)) { + if ( + [...store.users.values()].find( + (x) => x.relationship === Users.Relationship.Incoming, + ) + ) { alertCount++; } @@ -298,7 +305,7 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ); -} +}); export default connectState(ServerListSidebar, (state) => { return { diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx index 3ae16afd..8beac5b8 100644 --- a/src/components/navigation/right/MemberSidebar.tsx +++ b/src/components/navigation/right/MemberSidebar.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react-lite"; import { useParams } from "react-router"; import { Link } from "react-router-dom"; import { User } from "revolt.js"; @@ -7,6 +8,7 @@ import { ClientboundNotification } from "revolt.js/dist/websocket/notifications" import { Text } from "preact-i18n"; import { useContext, useEffect, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; import { getState } from "../../../redux"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; @@ -52,17 +54,16 @@ export default function MemberSidebar(props: { channel?: Channels.Channel }) { } } -export function GroupMemberSidebar({ - channel, - ctx, -}: Props & { channel: Channels.GroupChannel }) { - const { openScreen } = useIntermediate(); - const users = useUsers(undefined, ctx); - const members = channel.recipients - .map((x) => users.find((y) => y?._id === x)) - .filter((x) => typeof x !== "undefined") as User[]; +export const GroupMemberSidebar = observer( + ({ channel }: Props & { channel: Channels.GroupChannel }) => { + const { openScreen } = useIntermediate(); - /*const voice = useContext(VoiceContext); + const store = useData(); + const members = channel.recipients + ?.map((member) => store.users.get(member)!) + .filter((x) => typeof x !== "undefined"); + + /*const voice = useContext(VoiceContext); const voiceActive = voice.roomId === channel._id; let voiceParticipants: User[] = []; @@ -77,34 +78,36 @@ export function GroupMemberSidebar({ voiceParticipants.sort((a, b) => a.username.localeCompare(b.username)); }*/ - members.sort((a, b) => { - // ! FIXME: should probably rewrite all this code - const l = - +( - (a.online && a.status?.presence !== Users.Presence.Invisible) ?? - false - ) | 0; - const r = - +( - (b.online && b.status?.presence !== Users.Presence.Invisible) ?? - false - ) | 0; + members.sort((a, b) => { + // ! FIXME: should probably rewrite all this code + const l = + +( + (a.online && + a.status?.presence !== Users.Presence.Invisible) ?? + false + ) | 0; + const r = + +( + (b.online && + b.status?.presence !== Users.Presence.Invisible) ?? + false + ) | 0; - const n = r - l; - if (n !== 0) { - return n; - } + const n = r - l; + if (n !== 0) { + return n; + } - return a.username.localeCompare(b.username); - }); + return a.username.localeCompare(b.username); + }); - return ( - - - - + return ( + + + + - {/*voiceActive && voiceParticipants.length !== 0 && ( + {/*voiceActive && voiceParticipants.length !== 0 && ( )*/} - - —{" "} - {channel.recipients.length} - - } - /> - }> - {members.length === 0 && ( - - )} - {members.map( - (user) => - user && ( - - openScreen({ - id: "profile", - user_id: user._id, - }) - } - /> - ), - )} - - - - ); -} - -export function ServerMemberSidebar({ - channel, - ctx, -}: Props & { channel: Channels.TextChannel }) { - const [members, setMembers] = useState( - undefined, - ); - const users = useUsers(members?.map((x) => x._id.user) ?? []).filter( - (x) => typeof x !== "undefined", - ctx, - ) as Users.User[]; - const { openScreen } = useIntermediate(); - const status = useContext(StatusContext); - const client = useContext(AppContext); - - useEffect(() => { - if (status === ClientStatus.ONLINE && typeof members === "undefined") { - client.members - .fetchMembers(channel.server) - .then((members) => setMembers(members)); - } - }, [status]); - - // ! FIXME: temporary code - useEffect(() => { - function onPacket(packet: ClientboundNotification) { - if (!members) return; - if (packet.type === "ServerMemberJoin") { - if (packet.id !== channel.server) return; - setMembers([ - ...members, - { _id: { server: packet.id, user: packet.user } }, - ]); - } else if (packet.type === "ServerMemberLeave") { - if (packet.id !== channel.server) return; - setMembers( - members.filter( - (x) => - !( - x._id.user === packet.user && - x._id.server === packet.id - ), - ), - ); - } - } - - client.addListener("packet", onPacket); - return () => client.removeListener("packet", onPacket); - }, [members]); - - // copy paste from above - users.sort((a, b) => { - // ! FIXME: should probably rewrite all this code - const l = - +( - (a.online && a.status?.presence !== Users.Presence.Invisible) ?? - false - ) | 0; - const r = - +( - (b.online && b.status?.presence !== Users.Presence.Invisible) ?? - false - ) | 0; - - const n = r - l; - if (n !== 0) { - return n; - } - - return a.username.localeCompare(b.username); - }); - - return ( - - - - -
{!members && }
- {members && ( - —{" "} - {users.length} - + + {" "} + — {channel.recipients.length} + + } + /> }> - {users.length === 0 && ( + {members.length === 0 && ( )} - {users.map( + {members.map( (user) => user && ( - )} -
-
- ); -} +
+
+ ); + }, +); + +export const ServerMemberSidebar = observer( + ({ channel }: Props & { channel: Channels.TextChannel }) => { + const [members, setMembers] = useState( + undefined, + ); + + const store = useData(); + const users = members + ?.map((member) => store.users.get(member._id.user)!) + .filter((x) => typeof x !== "undefined"); + + const { openScreen } = useIntermediate(); + const status = useContext(StatusContext); + const client = useContext(AppContext); + + useEffect(() => { + if ( + status === ClientStatus.ONLINE && + typeof members === "undefined" + ) { + client.members + .fetchMembers(channel.server) + .then((members) => setMembers(members)); + } + }, [status]); + + // ! FIXME: temporary code + useEffect(() => { + function onPacket(packet: ClientboundNotification) { + if (!members) return; + if (packet.type === "ServerMemberJoin") { + if (packet.id !== channel.server) return; + setMembers([ + ...members, + { _id: { server: packet.id, user: packet.user } }, + ]); + } else if (packet.type === "ServerMemberLeave") { + if (packet.id !== channel.server) return; + setMembers( + members.filter( + (x) => + !( + x._id.user === packet.user && + x._id.server === packet.id + ), + ), + ); + } + } + + client.addListener("packet", onPacket); + return () => client.removeListener("packet", onPacket); + }, [members]); + + // copy paste from above + users?.sort((a, b) => { + // ! FIXME: should probably rewrite all this code + const l = + +( + (a.online && + a.status?.presence !== Users.Presence.Invisible) ?? + false + ) | 0; + const r = + +( + (b.online && + b.status?.presence !== Users.Presence.Invisible) ?? + false + ) | 0; + + const n = r - l; + if (n !== 0) { + return n; + } + + return a.username.localeCompare(b.username); + }); + + return ( + + + + +
{!members && }
+ {members && ( + + —{" "} + {users?.length ?? 0} + + }> + {(users?.length ?? 0) === 0 && ( + + )} + {users?.map( + (user) => + user && ( + + openScreen({ + id: "profile", + user_id: user._id, + }) + } + /> + ), + )} + + )} +
+
+ ); + }, +); function Search({ channel }: { channel: string }) { if (!getState().experiments.enabled?.includes("search")) return null; diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 22b07115..03b41251 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -13,6 +13,8 @@ import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { internalSubscribe } from "../../lib/eventEmitter"; +import { User } from "../../mobx"; + import { Action } from "../../components/ui/Modal"; import { Children } from "../../types/Preact"; @@ -42,10 +44,10 @@ export type Screen = type: "create_invite"; target: Channels.TextChannel | Channels.GroupChannel; } - | { type: "kick_member"; target: Servers.Server; user: string } - | { type: "ban_member"; target: Servers.Server; user: string } - | { type: "unfriend_user"; target: Users.User } - | { type: "block_user"; target: Users.User } + | { type: "kick_member"; target: Servers.Server; user: User } + | { type: "ban_member"; target: Servers.Server; user: User } + | { type: "unfriend_user"; target: User } + | { type: "block_user"; target: User } | { type: "create_channel"; target: Servers.Server } )) | ({ id: "special_input" } & ( @@ -82,7 +84,7 @@ export type Screen = | { id: "modify_account"; field: "username" | "email" | "password" } | { id: "profile"; user_id: string } | { id: "channel_info"; channel_id: string } - | { id: "pending_requests"; users: string[] } + | { id: "pending_requests"; users: User[] } | { id: "user_picker"; omit?: string[]; diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx index 4b919a9e..38096fc2 100644 --- a/src/context/intermediate/modals/Prompt.tsx +++ b/src/context/intermediate/modals/Prompt.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; import { Channels, Servers, Users } from "revolt.js/dist/api/objects"; import { ulid } from "ulid"; @@ -8,6 +9,9 @@ import { useContext, useEffect, useState } from "preact/hooks"; import { TextReact } from "../../../lib/i18n"; +import { User } from "../../../mobx"; +import { useData } from "../../../mobx/State"; + import Message from "../../../components/common/messaging/Message"; import UserIcon from "../../../components/common/user/UserIcon"; import InputBox from "../../../components/ui/InputBox"; @@ -61,14 +65,14 @@ type SpecialProps = { onClose: () => void } & ( type: "create_invite"; target: Channels.TextChannel | Channels.GroupChannel; } - | { type: "kick_member"; target: Servers.Server; user: string } - | { type: "ban_member"; target: Servers.Server; user: string } - | { type: "unfriend_user"; target: Users.User } - | { type: "block_user"; target: Users.User } + | { type: "kick_member"; target: Servers.Server; user: User } + | { type: "ban_member"; target: Servers.Server; user: User } + | { type: "unfriend_user"; target: User } + | { type: "block_user"; target: User } | { type: "create_channel"; target: Servers.Server } ); -export function SpecialPromptModal(props: SpecialProps) { +export const SpecialPromptModal = observer((props: SpecialProps) => { const client = useContext(AppContext); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); @@ -286,8 +290,6 @@ export function SpecialPromptModal(props: SpecialProps) { ); } case "kick_member": { - const user = client.users.get(props.user); - return ( - + } @@ -338,7 +340,6 @@ export function SpecialPromptModal(props: SpecialProps) { } case "ban_member": { const [reason, setReason] = useState(undefined); - const user = client.users.get(props.user); return ( - + @@ -477,4 +478,4 @@ export function SpecialPromptModal(props: SpecialProps) { default: return null; } -} +}); diff --git a/src/context/intermediate/popovers/PendingRequests.tsx b/src/context/intermediate/popovers/PendingRequests.tsx index 896958a1..03449e8e 100644 --- a/src/context/intermediate/popovers/PendingRequests.tsx +++ b/src/context/intermediate/popovers/PendingRequests.tsx @@ -1,31 +1,30 @@ +import { observer } from "mobx-react-lite"; + import styles from "./UserPicker.module.scss"; import { Text } from "preact-i18n"; +import { User } from "../../../mobx"; + import Modal from "../../../components/ui/Modal"; import { Friend } from "../../../pages/friends/Friend"; -import { useUsers } from "../../revoltjs/hooks"; interface Props { - users: string[]; + users: User[]; onClose: () => void; } -export function PendingRequests({ users: ids, onClose }: Props) { - const users = useUsers(ids); - +export const PendingRequests = observer(({ users, onClose }: Props) => { return ( } onClose={onClose}>
- {users - .filter((x) => typeof x !== "undefined") - .map((x) => ( - - ))} + {users.map((x) => ( + + ))}
); -} +}); diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/context/intermediate/popovers/UserProfile.tsx index ff77647c..c321b9f4 100644 --- a/src/context/intermediate/popovers/UserProfile.tsx +++ b/src/context/intermediate/popovers/UserProfile.tsx @@ -10,6 +10,8 @@ import styles from "./UserProfile.module.scss"; import { Localizer, Text } from "preact-i18n"; import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + import ChannelIcon from "../../../components/common/ChannelIcon"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; @@ -23,6 +25,7 @@ import { AppContext, ClientStatus, StatusContext, + useClient, } from "../../revoltjs/RevoltClient"; import { useChannels, @@ -58,25 +61,22 @@ export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) { >(undefined); const history = useHistory(); - const client = useContext(AppContext); + const client = useClient(); const status = useContext(StatusContext); const [tab, setTab] = useState("profile"); const ctx = useForceUpdate(); - const all_users = useUsers(undefined, ctx); const channels = useChannels(undefined, ctx); + const permissions = useUserPermission(client.user!._id, ctx); - const user = all_users.find((x) => x!._id === user_id); - const users = mutual?.users - ? all_users.filter((x) => mutual.users.includes(x!._id)) - : undefined; - - if (!user) { + const store = useData(); + if (!store.users.has(user_id)) { useEffect(onClose, []); return null; } - const permissions = useUserPermission(user!._id, ctx); + const user = store.users.get(user_id)!; + const users = mutual?.users.map((id) => store.users.get(id)); useLayoutEffect(() => { if (!user_id) return; diff --git a/src/context/revoltjs/hooks.ts b/src/context/revoltjs/hooks.ts index 22a297c9..e693ccf7 100644 --- a/src/context/revoltjs/hooks.ts +++ b/src/context/revoltjs/hooks.ts @@ -82,11 +82,6 @@ export function useUser(id?: string, context?: HookContext) { return useObject("users", id, context) as Readonly | undefined; } -export function useSelf(context?: HookContext) { - const ctx = useForceUpdate(context); - return useUser(ctx.client.user!._id, ctx); -} - export function useUsers(ids?: string[], context?: HookContext) { return useObject("users", ids, context) as ( | Readonly diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index f9be7fdb..9cca0a20 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -34,6 +34,8 @@ import { import { Text } from "preact-i18n"; import { useContext } from "preact/hooks"; +import { User } from "../mobx"; +import { useData } from "../mobx/State"; import { dispatch } from "../redux"; import { connectState } from "../redux/connector"; import { @@ -48,6 +50,7 @@ import { AppContext, ClientStatus, StatusContext, + useClient, } from "../context/revoltjs/RevoltClient"; import { useChannel, @@ -55,7 +58,6 @@ import { useForceUpdate, useServer, useServerPermission, - useUser, useUserPermission, } from "../context/revoltjs/hooks"; import { takeError } from "../context/revoltjs/util"; @@ -97,16 +99,16 @@ type Action = | { action: "copy_file_link"; attachment: Attachment } | { action: "open_link"; link: string } | { action: "copy_link"; link: string } - | { action: "remove_member"; channel: string; user: string } - | { action: "kick_member"; target: Servers.Server; user: string } - | { action: "ban_member"; target: Servers.Server; user: string } - | { action: "view_profile"; user: string } - | { action: "message_user"; user: string } - | { action: "block_user"; user: Users.User } - | { action: "unblock_user"; user: Users.User } - | { action: "add_friend"; user: Users.User } - | { action: "remove_friend"; user: Users.User } - | { action: "cancel_friend"; user: Users.User } + | { action: "remove_member"; channel: string; user: User } + | { action: "kick_member"; target: Servers.Server; user: User } + | { action: "ban_member"; target: Servers.Server; user: User } + | { action: "view_profile"; user: User } + | { action: "message_user"; user: User } + | { action: "block_user"; user: User } + | { action: "unblock_user"; user: User } + | { action: "add_friend"; user: User } + | { action: "remove_friend"; user: User } + | { action: "cancel_friend"; user: User } | { action: "set_presence"; presence: Users.Presence } | { action: "set_status" } | { action: "clear_status" } @@ -141,6 +143,7 @@ type Props = { notifications: Notifications; }; +// ! FIXME: no observers here! function ContextMenus(props: Props) { const { openScreen, writeClipboard } = useIntermediate(); const client = useContext(AppContext); @@ -313,17 +316,22 @@ function ContextMenus(props: Props) { case "remove_member": { - client.channels.removeMember(data.channel, data.user); + client.channels.removeMember( + data.channel, + data.user._id, + ); } break; case "view_profile": - openScreen({ id: "profile", user_id: data.user }); + openScreen({ id: "profile", user_id: data.user._id }); break; case "message_user": { - const channel = await client.users.openDM(data.user); + const channel = await client.users.openDM( + data.user._id, + ); if (channel) { history.push(`/channel/${channel._id}`); } @@ -458,6 +466,8 @@ function ContextMenus(props: Props) { unread, contextualChannel: cxid, }: ContextMenuData) => { + const store = useData(); + const forceUpdate = useForceUpdate(); const elements: Children[] = []; let lastDivider = false; @@ -523,7 +533,7 @@ function ContextMenus(props: Props) { const contextualChannel = useChannel(cxid, forceUpdate); const targetChannel = channel ?? contextualChannel; - const user = useUser(uid, forceUpdate); + const user = uid ? store.users.get(uid) : undefined; const serverChannel = targetChannel && (targetChannel.channel_type === "TextChannel" || @@ -595,7 +605,7 @@ function ContextMenus(props: Props) { if (userPermissions & UserPermission.ViewProfile) { generateAction({ action: "view_profile", - user: user._id, + user, }); } @@ -605,7 +615,7 @@ function ContextMenus(props: Props) { ) { generateAction({ action: "message_user", - user: user._id, + user, }); } @@ -624,7 +634,7 @@ function ContextMenus(props: Props) { generateAction({ action: "remove_member", channel: contextualChannel._id, - user: uid, + user: user!, }); } } @@ -641,14 +651,14 @@ function ContextMenus(props: Props) { generateAction({ action: "kick_member", target: server, - user: uid, + user: user!, }); if (serverPermissions & ServerPermission.BanMembers) generateAction({ action: "ban_member", target: server, - user: uid, + user: user!, }); } } @@ -873,89 +883,100 @@ function ContextMenus(props: Props) { id="Status" onClose={contextClick} className="Status"> - {() => ( - <> -
-
-
- writeClipboard(client.user!.username) - }> - + {() => { + const store = useData(); + const user = store.users.get(client.user!._id)!; + + return ( + <> +
+
+
+ writeClipboard( + client.user!.username, + ) }> - @{client.user!.username} - + + }> + @{user.username} + +
+
+ contextClick({ + action: "set_status", + }) + }> + +
-
- contextClick({ action: "set_status" }) - }> - -
-
- - - - - -
- - -
- - - -
- - - -
- - - -
- - - - - - - {client.user!.status?.text && ( - - + + - )} - - - )} +
+ + +
+ + + +
+ + + +
+ + + +
+ + + + + + + {client.user!.status?.text && ( + + + + + + )} + + + ); + }} (data?: T) { return typeof data === "undefined" ? null : data; } -class User { +export class User { _id: string; username: string; diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx index 3324d520..52795ea9 100644 --- a/src/pages/channels/ChannelHeader.tsx +++ b/src/pages/channels/ChannelHeader.tsx @@ -1,14 +1,18 @@ import { At, Hash, Menu } from "@styled-icons/boxicons-regular"; import { Notepad, Group } from "@styled-icons/boxicons-solid"; -import { Channel, User } from "revolt.js"; +import { observable } from "mobx"; +import { Channel } from "revolt.js"; import styled from "styled-components"; import { useContext } from "preact/hooks"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; +import { User } from "../../mobx"; +import { useData } from "../../mobx/State"; + import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; +import { AppContext, useClient } from "../../context/revoltjs/RevoltClient"; import { getChannelName } from "../../context/revoltjs/util"; import { useStatusColour } from "../../components/common/user/UserIcon"; @@ -65,15 +69,13 @@ const Info = styled.div` } `; -export default function ChannelHeader({ - channel, - toggleSidebar, -}: ChannelHeaderProps) { +export default observable(({ channel, toggleSidebar }: ChannelHeaderProps) => { const { openScreen } = useIntermediate(); - const client = useContext(AppContext); + const client = useClient(); + const state = useData(); const name = getChannelName(client, channel); - let icon, recipient; + let icon, recipient: User | undefined; switch (channel.channel_type) { case "SavedMessages": icon = ; @@ -81,7 +83,7 @@ export default function ChannelHeader({ case "DirectMessage": icon = ; const uid = client.channels.getRecipient(channel._id); - recipient = client.users.get(uid); + recipient = state.users.get(uid); break; case "Group": icon = ; @@ -109,12 +111,11 @@ export default function ChannelHeader({
- + )} @@ -145,4 +146,4 @@ export default function ChannelHeader({ ); -} +}); diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx index eef24d84..45f65de4 100644 --- a/src/pages/channels/voice/VoiceHeader.tsx +++ b/src/pages/channels/voice/VoiceHeader.tsx @@ -1,19 +1,18 @@ import { BarChart } from "@styled-icons/boxicons-regular"; +import { observable } from "mobx"; import styled from "styled-components"; import { Text } from "preact-i18n"; import { useContext } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + import { VoiceContext, VoiceOperationsContext, VoiceStatus, } from "../../../context/Voice"; -import { - useForceUpdate, - useSelf, - useUsers, -} from "../../../context/revoltjs/hooks"; +import { useClient } from "../../../context/revoltjs/RevoltClient"; import UserIcon from "../../../components/common/user/UserIcon"; import Button from "../../../components/ui/Button"; @@ -70,17 +69,21 @@ const VoiceBase = styled.div` } `; -export default function VoiceHeader({ id }: Props) { +export default observable(({ id }: Props) => { const { status, participants, roomId } = useContext(VoiceContext); if (roomId !== id) return null; const { isProducing, startProducing, stopProducing, disconnect } = useContext(VoiceOperationsContext); - const ctx = useForceUpdate(); - const self = useSelf(ctx); + const store = useData(); + const client = useClient(); + const self = store.users.get(client.user!._id); + + //const ctx = useForceUpdate(); + //const self = useSelf(ctx); const keys = participants ? Array.from(participants.keys()) : undefined; - const users = keys ? useUsers(keys, ctx) : undefined; + const users = keys?.map((key) => store.users.get(key)); return ( @@ -135,7 +138,7 @@ export default function VoiceHeader({ id }: Props) {
); -} +}); /**{voice.roomId === id && (
diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx index 0a643f1e..1b3f458c 100644 --- a/src/pages/friends/Friend.tsx +++ b/src/pages/friends/Friend.tsx @@ -1,6 +1,7 @@ import { X, Plus } from "@styled-icons/boxicons-regular"; 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 { Users } from "revolt.js/dist/api/objects"; import styles from "./Friend.module.scss"; import classNames from "classnames"; @@ -10,6 +11,8 @@ import { useContext } from "preact/hooks"; import { stopPropagation } from "../../lib/stopPropagation"; +import { User } from "../../mobx"; + import { VoiceOperationsContext } from "../../context/Voice"; import { useIntermediate } from "../../context/intermediate/Intermediate"; import { @@ -27,7 +30,7 @@ interface Props { user: User; } -export function Friend({ user }: Props) { +export const Friend = observer(({ user }: Props) => { const client = useContext(AppContext); const { openScreen } = useIntermediate(); const { openDM } = useContext(OperationsContext); @@ -133,4 +136,4 @@ export function Friend({ user }: Props) {
{actions}
); -} +}); diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index 7fb1506b..4b63d8f3 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -4,7 +4,8 @@ import { ListPlus, } from "@styled-icons/boxicons-regular"; 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 { Users } from "revolt.js/dist/api/objects"; import styles from "./Friend.module.scss"; import { Text } from "preact-i18n"; @@ -12,30 +13,30 @@ import { Text } from "preact-i18n"; import { TextReact } from "../../lib/i18n"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; +import { User } from "../../mobx"; +import { useData } from "../../mobx/State"; + import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { useUsers } from "../../context/revoltjs/hooks"; import CollapsibleSection from "../../components/common/CollapsibleSection"; import Tooltip from "../../components/common/Tooltip"; import UserIcon from "../../components/common/user/UserIcon"; -import Details from "../../components/ui/Details"; import Header from "../../components/ui/Header"; import IconButton from "../../components/ui/IconButton"; -import Overline from "../../components/ui/Overline"; import { Children } from "../../types/Preact"; import { Friend } from "./Friend"; -export default function Friends() { +export default observer(() => { const { openScreen } = useIntermediate(); - const users = useUsers() as User[]; + const store = useData(); + const users = [...store.users.values()]; users.sort((a, b) => a.username.localeCompare(b.username)); const friends = users.filter( (x) => x.relationship === Users.Relationship.Friend, ); - const lists = [ [ "", @@ -138,7 +139,7 @@ export default function Friends() { onClick={() => openScreen({ id: "pending_requests", - users: incoming.map((x) => x._id), + users: incoming, }) }>
@@ -216,4 +217,4 @@ export default function Friends() {
); -} +}); diff --git a/src/pages/settings/panes/Account.tsx b/src/pages/settings/panes/Account.tsx index 2c858d55..d6ee7bca 100644 --- a/src/pages/settings/panes/Account.tsx +++ b/src/pages/settings/panes/Account.tsx @@ -1,5 +1,6 @@ import { At } from "@styled-icons/boxicons-regular"; import { Envelope, Key, HelpCircle } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; import { Link, useHistory } from "react-router-dom"; import { Users } from "revolt.js/dist/api/objects"; @@ -7,26 +8,28 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useContext, useEffect, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { ClientStatus, StatusContext, + useClient, } from "../../../context/revoltjs/RevoltClient"; -import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks"; +import { useForceUpdate } from "../../../context/revoltjs/hooks"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; import Button from "../../../components/ui/Button"; -import Overline from "../../../components/ui/Overline"; import Tip from "../../../components/ui/Tip"; -export function Account() { +export const Account = observer(() => { const { openScreen, writeClipboard } = useIntermediate(); const status = useContext(StatusContext); - const ctx = useForceUpdate(); - const user = useSelf(ctx); - if (!user) return null; + const client = useClient(); + const store = useData(); + const user = store.users.get(client.user!._id)!; const [email, setEmail] = useState("..."); const [revealEmail, setRevealEmail] = useState(false); @@ -41,13 +44,13 @@ export function Account() { useEffect(() => { if (email === "..." && status === ClientStatus.ONLINE) { - ctx.client + client .req("GET", "/auth/user") .then((account) => setEmail(account.email)); } if (profile === undefined && status === ClientStatus.ONLINE) { - ctx.client.users + client.users .fetchProfile(user._id) .then((profile) => setProfile(profile ?? {})); } @@ -180,4 +183,4 @@ export function Account() {
); -} +}); diff --git a/src/pages/settings/panes/Feedback.tsx b/src/pages/settings/panes/Feedback.tsx index 508b5f33..14af3db4 100644 --- a/src/pages/settings/panes/Feedback.tsx +++ b/src/pages/settings/panes/Feedback.tsx @@ -2,7 +2,7 @@ import styles from "./Panes.module.scss"; import { Localizer, Text } from "preact-i18n"; import { useState } from "preact/hooks"; -import { useSelf } from "../../../context/revoltjs/hooks"; +import { useClient } from "../../../context/revoltjs/RevoltClient"; import Button from "../../../components/ui/Button"; import InputBox from "../../../components/ui/InputBox"; @@ -10,7 +10,7 @@ import Radio from "../../../components/ui/Radio"; import TextArea from "../../../components/ui/TextArea"; export function Feedback() { - const user = useSelf(); + const client = useClient(); const [other, setOther] = useState(""); const [description, setDescription] = useState(""); const [state, setState] = useState<"ready" | "sending" | "sent">("ready"); @@ -28,7 +28,7 @@ export function Feedback() { checked, other, description, - name: user?.username ?? "Unknown User", + name: client.user!.username, }), mode: "no-cors", }); diff --git a/src/pages/settings/panes/Profile.tsx b/src/pages/settings/panes/Profile.tsx index b9fe3a27..265e7998 100644 --- a/src/pages/settings/panes/Profile.tsx +++ b/src/pages/settings/panes/Profile.tsx @@ -11,8 +11,8 @@ import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { ClientStatus, StatusContext, + useClient, } from "../../../context/revoltjs/RevoltClient"; -import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks"; import AutoComplete, { useAutoComplete, @@ -23,9 +23,7 @@ export function Profile() { const { intl } = useContext(IntlContext); const status = useContext(StatusContext); - const ctx = useForceUpdate(); - const user = useSelf(); - if (!user) return null; + const client = useClient(); const [profile, setProfile] = useState( undefined, @@ -34,8 +32,8 @@ export function Profile() { // ! FIXME: temporary solution // ! we should just announce profile changes through WS function refreshProfile() { - ctx.client.users - .fetchProfile(user!._id) + client.users + .fetchProfile(client.user!._id) .then((profile) => setProfile(profile ?? {})); } @@ -69,7 +67,7 @@ export function Profile() {
{}} @@ -87,19 +85,17 @@ export function Profile() { fileType="avatars" behaviour="upload" maxFileSize={4_000_000} - onUpload={(avatar) => - ctx.client.users.editUser({ avatar }) - } + onUpload={(avatar) => client.users.editUser({ avatar })} remove={() => - ctx.client.users.editUser({ remove: "Avatar" }) + client.users.editUser({ remove: "Avatar" }) } - defaultPreview={ctx.client.users.getAvatarURL( - user._id, + defaultPreview={client.users.getAvatarURL( + client.user!._id, { max_side: 256 }, true, )} - previewURL={ctx.client.users.getAvatarURL( - user._id, + previewURL={client.users.getAvatarURL( + client.user!._id, { max_side: 256 }, true, true, @@ -117,20 +113,20 @@ export function Profile() { fileType="backgrounds" maxFileSize={6_000_000} onUpload={async (background) => { - await ctx.client.users.editUser({ + await client.users.editUser({ profile: { background }, }); refreshProfile(); }} remove={async () => { - await ctx.client.users.editUser({ + await client.users.editUser({ remove: "ProfileBackground", }); setProfile({ ...profile, background: undefined }); }} previewURL={ profile?.background - ? ctx.client.users.getBackgroundURL( + ? client.users.getBackgroundURL( profile, { width: 1000 }, true, @@ -173,7 +169,7 @@ export function Profile() { contrast onClick={() => { setChanged(false); - ctx.client.users.editUser({ + client.users.editUser({ profile: { content: profile?.content }, }); }} diff --git a/src/pages/settings/server/Invites.tsx b/src/pages/settings/server/Invites.tsx index ce411705..e77bf3b5 100644 --- a/src/pages/settings/server/Invites.tsx +++ b/src/pages/settings/server/Invites.tsx @@ -1,10 +1,14 @@ import { XCircle } from "@styled-icons/boxicons-regular"; +import { observer } from "mobx-react-lite"; import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + +import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useChannels, useForceUpdate, @@ -20,16 +24,19 @@ interface Props { server: Servers.Server; } -export function Invites({ server }: Props) { +export const Invites = observer(({ server }: Props) => { + const [deleting, setDelete] = useState([]); const [invites, setInvites] = useState< InvitesNS.ServerInvite[] | undefined >(undefined); const ctx = useForceUpdate(); - const [deleting, setDelete] = useState([]); - const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx); const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx); + const store = useData(); + const client = useClient(); + const users = invites?.map((invite) => store.users.get(invite.creator)); + useEffect(() => { ctx.client.servers .fetchInvites(server._id) @@ -53,8 +60,8 @@ export function Invites({ server }: Props) {
{typeof invites === "undefined" && } - {invites?.map((invite) => { - const creator = users.find((x) => x?._id === invite.creator); + {invites?.map((invite, index) => { + const creator = users![index]; const channel = channels.find((x) => x?._id === invite.channel); return ( @@ -93,4 +100,4 @@ export function Invites({ server }: Props) { })}
); -} +}); diff --git a/src/pages/settings/server/Members.tsx b/src/pages/settings/server/Members.tsx index edd618f9..24528cba 100644 --- a/src/pages/settings/server/Members.tsx +++ b/src/pages/settings/server/Members.tsx @@ -1,11 +1,15 @@ import { ChevronDown } from "@styled-icons/boxicons-regular"; import { isEqual } from "lodash"; +import { observer } from "mobx-react-lite"; import { Servers } from "revolt.js/dist/api/objects"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; +import { useData } from "../../../mobx/State"; + +import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks"; import UserIcon from "../../../components/common/user/UserIcon"; @@ -18,17 +22,18 @@ interface Props { server: Servers.Server; } -export function Members({ server }: Props) { +export const Members = observer(({ server }: Props) => { + const [selected, setSelected] = useState(); const [members, setMembers] = useState( undefined, ); - const ctx = useForceUpdate(); - const [selected, setSelected] = useState(); - const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx); + const store = useData(); + const client = useClient(); + const users = members?.map((member) => store.users.get(member._id.user)); useEffect(() => { - ctx.client.members + client.members .fetchMembers(server._id) .then((members) => setMembers(members)); }, []); @@ -50,10 +55,10 @@ export function Members({ server }: Props) { {members && members.length > 0 && members - .map((x) => { + .map((member, index) => { return { - member: x, - user: users.find((y) => y?._id === x._id.user), + member, + user: users![index], }; }) .map(({ member, user }) => ( @@ -126,7 +131,7 @@ export function Members({ server }: Props) { roles, )} onClick={async () => { - await ctx.client.members.editMember( + await client.members.editMember( server._id, member._id.user, { @@ -154,4 +159,4 @@ export function Members({ server }: Props) { ))}
); -} +});