mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-10 01:03:36 -05:00
Improve category design for members list.
Implement new list for groups.
This commit is contained in:
parent
f2c59ae451
commit
5c45e29f92
3 changed files with 179 additions and 328 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
80
src/components/navigation/right/Search.note
Normal file
80
src/components/navigation/right/Search.note
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue