Improve category design for members list.

Implement new list for groups.
This commit is contained in:
Paul 2021-08-08 18:26:16 +01:00
parent f2c59ae451
commit 5c45e29f92
3 changed files with 179 additions and 328 deletions

View file

@ -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,
})
} */
}
/>
</div>
);
@ -89,22 +101,29 @@ const innerElementType = forwardRef(({ style, ...rest }, ref) => (
export default function MemberList({
entries,
context,
}: {
entries: MemberListEntry[];
context: Channel;
}) {
const { openScreen } = useIntermediate();
return (
<AutoSizer>
{({ width, height }) => (
<List
className="virtualList"
width={width}
height={height}
itemData={{
entries,
context,
openScreen,
}}
itemCount={entries.length}
innerElementType={innerElementType}
itemSize={42}>
itemSize={(index) =>
typeof entries[index] === "string" ? 24 : 42
}
estimatedItemSize={42}>
{
// eslint-disable-next-line
Row as any

View file

@ -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 (
<GenericSidebarBase>
<GenericSidebarList>
<Search channel={channel} />
{/*voiceActive && voiceParticipants.length !== 0 && (
<Fragment>
<Category
type="members"
text={
<span>
<Text id="app.main.categories.participants" />{" "}
{voiceParticipants.length}
</span>
}
/>
{voiceParticipants.map(
user =>
user && (
<LinkProfile user_id={user._id}>
<UserButton
key={user._id}
user={user}
context={channel}
/>
</LinkProfile>
)
)}
</Fragment>
)*/}
<CollapsibleSection
sticky
id="members"
defaultValue
summary={
<Category
variant="uniform"
text={
<span>
<Text id="app.main.categories.members" />{" "}
{channel.recipients?.length ?? 0}
</span>
}
/>
}>
{members?.length === 0 && (
<img src={placeholderSVG} loading="eager" />
)}
{members?.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel!}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</CollapsibleSection>
</GenericSidebarList>
<MemberList entries={entries} context={channel} />
</GenericSidebarBase>
);
},
@ -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 (
<GenericSidebarBase>
<MemberList entries={entries} />
<MemberList entries={entries} context={channel} />
</GenericSidebarBase>
);
/*
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 (
<GenericSidebarBase>
<GenericSidebarList>
<Search channel={channel} />
<div>{users.length === 0 && <Preloader type="ring" />}</div>
{users.length > 0 && (
<CollapsibleSection
//sticky //will re-add later, need to fix css
id="members"
defaultValue
summary={
<span>
<Text id="app.main.categories.members" /> {" "}
{users?.length ?? 0}
</span>
}>
{users.map(
(user) =>
user && (
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() =>
openScreen({
id: "profile",
user_id: user._id,
})
}
/>
),
)}
</CollapsibleSection>
)}
</GenericSidebarList>
</GenericSidebarBase>
);*/
},
);
function Search({ channel }: { channel: Channel }) {
if (!getState().experiments.enabled?.includes("search")) return null;
type Sort = "Relevance" | "Latest" | "Oldest";
const [sort, setSort] = useState<Sort>("Relevance");
const [query, setV] = useState("");
const [results, setResults] = useState<Message[]>([]);
async function search() {
const data = await channel.searchWithUsers({ query, sort });
setResults(data.messages);
}
return (
<CollapsibleSection
sticky
id="search"
defaultValue={false}
summary={
<>
<Text id="app.main.channel.search.title" /> (BETA)
</>
}>
<div style={{ display: "flex" }}>
{["Relevance", "Latest", "Oldest"].map((key) => (
<Button
key={key}
style={{ flex: 1, minWidth: 0 }}
compact
error={sort === key}
onClick={() => setSort(key as Sort)}>
<Text
id={`app.main.channel.search.sort.${key.toLowerCase()}`}
/>
</Button>
))}
</div>
<InputBox
style={{ width: "100%" }}
onKeyDown={(e) => e.key === "Enter" && search()}
value={query}
onChange={(e) => setV(e.currentTarget.value)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
marginTop: "8px",
}}>
{results.map((message) => {
let href = "";
if (channel?.channel_type === "TextChannel") {
href += `/server/${channel.server_id}`;
}
href += `/channel/${message.channel_id}/${message._id}`;
return (
<Link to={href} key={message._id}>
<div
style={{
margin: "2px",
padding: "6px",
background: "var(--primary-background)",
}}>
<b>@{message.author?.username}</b>
<br />
{message.content}
</div>
</Link>
);
})}
</div>
</CollapsibleSection>
);
}

View file

@ -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<Sort>("Relevance");
const [query, setV] = useState("");
const [results, setResults] = useState<Message[]>([]);
async function search() {
const data = await channel.searchWithUsers({ query, sort });
setResults(data.messages);
}
return (
<CollapsibleSection
sticky
id="search"
defaultValue={false}
summary={
<>
<Text id="app.main.channel.search.title" /> (BETA)
</>
}>
<div style={{ display: "flex" }}>
{["Relevance", "Latest", "Oldest"].map((key) => (
<Button
key={key}
style={{ flex: 1, minWidth: 0 }}
compact
error={sort === key}
onClick={() => setSort(key as Sort)}>
<Text
id={`app.main.channel.search.sort.${key.toLowerCase()}`}
/>
</Button>
))}
</div>
<InputBox
style={{ width: "100%" }}
onKeyDown={(e) => e.key === "Enter" && search()}
value={query}
onChange={(e) => setV(e.currentTarget.value)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
marginTop: "8px",
}}>
{results.map((message) => {
let href = "";
if (channel?.channel_type === "TextChannel") {
href += `/server/${channel.server_id}`;
}
href += `/channel/${message.channel_id}/${message._id}`;
return (
<Link to={href} key={message._id}>
<div
style={{
margin: "2px",
padding: "6px",
background: "var(--primary-background)",
}}>
<b>@{message.author?.username}</b>
<br />
{message.content}
</div>
</Link>
);
})}
</div>
</CollapsibleSection>
);
}