From 5c45e29f9258de7592b1e79ba601af0f2f11d41d Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 8 Aug 2021 18:26:16 +0100 Subject: [PATCH] Improve category design for members list. Implement new list for groups. --- .../navigation/right/MemberList.tsx | 39 +- .../navigation/right/MemberSidebar.tsx | 388 ++++-------------- src/components/navigation/right/Search.note | 80 ++++ 3 files changed, 179 insertions(+), 328 deletions(-) create mode 100644 src/components/navigation/right/Search.note diff --git a/src/components/navigation/right/MemberList.tsx b/src/components/navigation/right/MemberList.tsx index 22794d1e..9bf2f83e 100644 --- a/src/components/navigation/right/MemberList.tsx +++ b/src/components/navigation/right/MemberList.tsx @@ -1,23 +1,32 @@ import AutoSizer from "react-virtualized-auto-sizer"; -import { FixedSizeList as List } from "react-window"; +import { VariableSizeList as List } from "react-window"; +import { Channel } from "revolt.js/dist/maps/Channels"; import { User } from "revolt.js/dist/maps/Users"; import styled from "styled-components"; import { Text } from "preact-i18n"; import { forwardRef } from "preact/compat"; +import { + Screen, + useIntermediate, +} from "../../../context/intermediate/Intermediate"; + import { UserButton } from "../items/ButtonItem"; export type MemberListEntry = string | User; interface ItemData { entries: MemberListEntry[]; + context: Channel; + openScreen: (screen: Screen) => void; } const PADDING_SIZE = 6; const ListCategory = styled.div` + height: 100%; display: flex; - padding: 14px; + padding: 0 14px; font-size: 0.8em; font-weight: 600; user-select: none; @@ -28,7 +37,7 @@ const ListCategory = styled.div` const Row = ({ data, - style, + style: styleIn, index, }: { data: ItemData; @@ -36,7 +45,10 @@ const Row = ({ style: JSX.CSSProperties; }) => { const item = data.entries[index]; - style.top = `${parseFloat(style.top as string) + PADDING_SIZE}px`; + const style = { + ...styleIn, + top: `${parseFloat(styleIn.top as string) + PADDING_SIZE}px`, + }; if (typeof item === "string") { const [cat, count] = item.split(":"); @@ -61,13 +73,13 @@ const Row = ({ key={item._id} user={item} margin - /* context={channel} + context={data.context} onClick={() => - openScreen({ + data.openScreen({ id: "profile", - user_id: user._id, + user_id: item._id, }) - } */ + } /> ); @@ -89,22 +101,29 @@ const innerElementType = forwardRef(({ style, ...rest }, ref) => ( export default function MemberList({ entries, + context, }: { entries: MemberListEntry[]; + context: Channel; }) { + const { openScreen } = useIntermediate(); return ( {({ width, height }) => ( + itemSize={(index) => + typeof entries[index] === "string" ? 24 : 42 + } + estimatedItemSize={42}> { // eslint-disable-next-line Row as any diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx index 4529f31d..da082dfc 100644 --- a/src/components/navigation/right/MemberSidebar.tsx +++ b/src/components/navigation/right/MemberSidebar.tsx @@ -1,34 +1,20 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { observer } from "mobx-react-lite"; -import { Link, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { Presence } from "revolt-api/types/Users"; import { Channel } from "revolt.js/dist/maps/Channels"; -import { Message } from "revolt.js/dist/maps/Messages"; import { User } from "revolt.js/dist/maps/Users"; -import { Text } from "preact-i18n"; -import { useContext, useEffect, useMemo, useState } from "preact/hooks"; +import { useContext, useEffect, useMemo } from "preact/hooks"; -import { getState } from "../../../redux"; - -import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { ClientStatus, StatusContext, useClient, } from "../../../context/revoltjs/RevoltClient"; -import CollapsibleSection from "../../common/CollapsibleSection"; -import Button from "../../ui/Button"; -import Category from "../../ui/Category"; -import InputBox from "../../ui/InputBox"; -import Preloader from "../../ui/Preloader"; -import placeholderSVG from "../items/placeholder.svg"; - -import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; -import { UserButton } from "../items/ButtonItem"; -import { ChannelDebugInfo } from "./ChannelDebugInfo"; -import MemberList, { MemberListEntry } from "./MemberList"; +import { GenericSidebarBase } from "../SidebarBase"; +import MemberList from "./MemberList"; export default function MemberSidebar({ channel: obj }: { channel?: Channel }) { const { channel: channel_id } = useParams<{ channel: string }>(); @@ -45,116 +31,77 @@ export default function MemberSidebar({ channel: obj }: { channel?: Channel }) { } } -export const GroupMemberSidebar = observer( - ({ channel }: { channel: Channel }) => { - const { openScreen } = useIntermediate(); +function useEntries(channel: Channel, keys: string[], isServer?: boolean) { + const client = channel.client; + return useMemo(() => { + const categories: { [key: string]: [User, string][] } = { + online: [], + offline: [], + }; - const members = channel.recipients?.filter( - (x) => typeof x !== "undefined", - ); - - /*const voice = useContext(VoiceContext); - const voiceActive = voice.roomId === channel._id; - - let voiceParticipants: User[] = []; - if (voiceActive) { - const idArray = Array.from(voice.participants.keys()); - voiceParticipants = idArray - .map(x => users.find(y => y?._id === x)) - .filter(x => typeof x !== "undefined") as User[]; - - members = members.filter(member => idArray.indexOf(member._id) === -1); - - 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 !== Presence.Invisible) ?? - false - ) | 0; - const r = - +( - (b!.online && b!.status?.presence !== Presence.Invisible) ?? - false - ) | 0; - - const n = r - l; - if (n !== 0) { - return n; + keys.forEach((key) => { + let u; + if (isServer) { + const { server, user } = JSON.parse(key); + if (server !== channel.server_id) return; + u = client.users.get(user); + } else { + u = client.users.get(key); } - return a!.username.localeCompare(b!.username); + if (!u) return; + + const member = client.members.get(key); + const sort = member?.nickname ?? u.username; + const entry = [u, sort] as [User, string]; + + if (isServer) { + // Sort users into hoisted roles here. + } else { + // Sort users into "participants" list here. + // For voice calls. + } + + if (!u.online || u.status?.presence === Presence.Invisible) { + categories.offline.push(entry); + } else { + categories.online.push(entry); + } }); + Object.keys(categories).forEach((key) => + categories[key].sort((a, b) => a[1].localeCompare(b[1])), + ); + + const entries = []; + + if (categories.online.length > 0) { + entries.push( + `online:${categories.online.length}`, + ...categories.online.map((x) => x[0]), + ); + } + + if (categories.offline.length > 0) { + entries.push( + `offline:${categories.offline.length}`, + ...categories.offline.map((x) => x[0]), + ); + } + + return entries; + // eslint-disable-next-line + }, [keys]); +} + +export const GroupMemberSidebar = observer( + ({ channel }: { channel: Channel }) => { + const keys = [...channel.recipient_ids!]; + const entries = useEntries(channel, keys); + return ( - - - - {/*voiceActive && voiceParticipants.length !== 0 && ( - - - {" "} - — {voiceParticipants.length} - - } - /> - {voiceParticipants.map( - user => - user && ( - - - - ) - )} - - )*/} - - {" "} - — {channel.recipients?.length ?? 0} - - } - /> - }> - {members?.length === 0 && ( - - )} - {members?.map( - (user) => - user && ( - - openScreen({ - id: "profile", - user_id: user._id, - }) - } - /> - ), - )} - - + ); }, @@ -172,207 +119,12 @@ export const ServerMemberSidebar = observer( }, [status, channel.server]); const keys = [...client.members.keys()]; - const entries = useMemo(() => { - const categories: { [key: string]: [User, string][] } = { - online: [], - offline: [], - }; - - keys.forEach((key) => { - const { server, user } = JSON.parse(key); - if (server !== channel.server_id) return; - - const u = client.users.get(user); - if (!u) return; - - const member = client.members.get(key); - const sort = member?.nickname ?? u.username; - const entry = [u, sort] as [User, string]; - - if (!u.online || u.status?.presence === Presence.Invisible) { - categories.offline.push(entry); - } else { - categories.online.push(entry); - } - }); - - Object.keys(categories).forEach((key) => - categories[key].sort((a, b) => a[1].localeCompare(b[1])), - ); - - const entries = []; - - entries.push( - `online:${categories.online.length}`, - ...categories.online.map((x) => x[0]), - `offline:${categories.offline.length}`, - ...categories.offline.map((x) => x[0]), - ); - - return entries; - // eslint-disable-next-line - }, [keys]); + const entries = useEntries(channel, keys, true); return ( - + ); - - /* - const client = useClient(); - const { openScreen } = useIntermediate(); - const status = useContext(StatusContext); - - useEffect(() => { - if (status === ClientStatus.ONLINE) { - channel.server!.fetchMembers(); - } - }, [status, channel.server]); - - const users = [...client.members.keys()] - .map((x) => JSON.parse(x)) - .filter((x) => x.server === channel.server_id) - .map((y) => client.users.get(y.user)!) - .filter((z) => typeof z !== "undefined"); - - // copy paste from above - users.sort((a, b) => { - // ! FIXME: should probably rewrite all this code - const l = - +( - (a.online && a.status?.presence !== Presence.Invisible) ?? - false - ) | 0; - const r = - +( - (b.online && b.status?.presence !== Presence.Invisible) ?? - false - ) | 0; - - const n = r - l; - if (n !== 0) { - return n; - } - - return a.username.localeCompare(b.username); - }); - - return ( - - - -
{users.length === 0 && }
- {users.length > 0 && ( - - —{" "} - {users?.length ?? 0} - - }> - {users.map( - (user) => - user && ( - - openScreen({ - id: "profile", - user_id: user._id, - }) - } - /> - ), - )} - - )} -
-
- );*/ }, ); - -function Search({ channel }: { channel: Channel }) { - if (!getState().experiments.enabled?.includes("search")) return null; - - type Sort = "Relevance" | "Latest" | "Oldest"; - const [sort, setSort] = useState("Relevance"); - - const [query, setV] = useState(""); - const [results, setResults] = useState([]); - - async function search() { - const data = await channel.searchWithUsers({ query, sort }); - setResults(data.messages); - } - - return ( - - (BETA) - - }> -
- {["Relevance", "Latest", "Oldest"].map((key) => ( - - ))} -
- e.key === "Enter" && search()} - value={query} - onChange={(e) => setV(e.currentTarget.value)} - /> -
- {results.map((message) => { - let href = ""; - if (channel?.channel_type === "TextChannel") { - href += `/server/${channel.server_id}`; - } - - href += `/channel/${message.channel_id}/${message._id}`; - - return ( - -
- @{message.author?.username} -
- {message.content} -
- - ); - })} -
-
- ); -} diff --git a/src/components/navigation/right/Search.note b/src/components/navigation/right/Search.note new file mode 100644 index 00000000..0e467399 --- /dev/null +++ b/src/components/navigation/right/Search.note @@ -0,0 +1,80 @@ +// this is the search code + +function Search({ channel }: { channel: Channel }) { + if (!getState().experiments.enabled?.includes("search")) return null; + + type Sort = "Relevance" | "Latest" | "Oldest"; + const [sort, setSort] = useState("Relevance"); + + const [query, setV] = useState(""); + const [results, setResults] = useState([]); + + async function search() { + const data = await channel.searchWithUsers({ query, sort }); + setResults(data.messages); + } + + return ( + + (BETA) + + }> +
+ {["Relevance", "Latest", "Oldest"].map((key) => ( + + ))} +
+ e.key === "Enter" && search()} + value={query} + onChange={(e) => setV(e.currentTarget.value)} + /> +
+ {results.map((message) => { + let href = ""; + if (channel?.channel_type === "TextChannel") { + href += `/server/${channel.server_id}`; + } + + href += `/channel/${message.channel_id}/${message._id}`; + + return ( + +
+ @{message.author?.username} +
+ {message.content} +
+ + ); + })} +
+
+ ); +}