mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-25 03:32:54 +01:00
replace channel list with virtualized list
This commit is contained in:
parent
53eaf7d41f
commit
94e9880197
@ -53,6 +53,7 @@
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use-error-boundary": "^3.0.0",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"reoverlay": "^1.0.3",
|
||||
"styled-components": "^5.3.11",
|
||||
@ -67,6 +68,7 @@
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"@types/react-virtualized": "^9.21.22",
|
||||
"@types/styled-components": "^5.1.27",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
|
@ -152,6 +152,9 @@ dependencies:
|
||||
react-use-error-boundary:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(react@18.2.0)
|
||||
react-virtualized:
|
||||
specifier: ^9.22.5
|
||||
version: 9.22.5(react-dom@18.2.0)(react@18.2.0)
|
||||
remark-gfm:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@ -190,6 +193,9 @@ devDependencies:
|
||||
'@types/react-syntax-highlighter':
|
||||
specifier: ^15.5.7
|
||||
version: 15.5.7
|
||||
'@types/react-virtualized':
|
||||
specifier: ^9.21.22
|
||||
version: 9.21.22
|
||||
'@types/styled-components':
|
||||
specifier: ^5.1.27
|
||||
version: 5.1.27
|
||||
@ -3973,6 +3979,13 @@ packages:
|
||||
'@types/react': 18.2.22
|
||||
dev: false
|
||||
|
||||
/@types/react-virtualized@9.21.22:
|
||||
resolution: {integrity: sha512-YRifyCKnBG84+J/Hny0f3bo8BRrcNT74CvsAVpQpZcS83fdC7lP7RfzwL2ND8/ihhpnDFL1IbxJ9MpQNaKUDuQ==}
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.5
|
||||
'@types/react': 18.2.22
|
||||
dev: true
|
||||
|
||||
/@types/react@18.2.22:
|
||||
resolution: {integrity: sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==}
|
||||
dependencies:
|
||||
@ -5356,6 +5369,11 @@ packages:
|
||||
shallow-clone: 3.0.1
|
||||
dev: true
|
||||
|
||||
/clsx@1.2.1:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/clsx@2.0.0:
|
||||
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
|
||||
engines: {node: '>=6'}
|
||||
@ -11242,6 +11260,10 @@ packages:
|
||||
/react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
|
||||
/react-lifecycles-compat@3.0.4:
|
||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||
dev: false
|
||||
|
||||
/react-loading-skeleton@3.3.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-NilqqwMh2v9omN7LteiDloEVpFyMIa0VGqF+ukqp0ncVlYu1sKYbYGX9JEl+GtOT9TKsh04zCHAbavnQ2USldA==}
|
||||
peerDependencies:
|
||||
@ -11476,6 +11498,22 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-virtualized@9.22.5(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==}
|
||||
peerDependencies:
|
||||
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.15
|
||||
clsx: 1.2.1
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-lifecycles-compat: 3.0.4
|
||||
dev: false
|
||||
|
||||
/react@18.2.0:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -1,53 +1,58 @@
|
||||
import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { AutoSizer, List, ListRowProps } from "react-virtualized";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import { Permissions } from "../utils/Permissions";
|
||||
import ChannelListItem from "./ChannelListItem";
|
||||
|
||||
const List = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export function EmptyChannelList() {
|
||||
return <List></List>;
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
guild: Guild;
|
||||
}
|
||||
|
||||
function ChannelList({ guild }: Props) {
|
||||
function ChannelList() {
|
||||
const app = useAppStore();
|
||||
|
||||
const renderChannelListItem = React.useCallback(
|
||||
(channel: Channel) => {
|
||||
const permission = Permissions.getPermission(app.account!.id, guild, channel);
|
||||
if (!permission.has("VIEW_CHANNEL")) return null;
|
||||
if (!app.activeGuild || !app.activeChannel) return null;
|
||||
const { channelsSorted } = app.activeGuild;
|
||||
|
||||
const active = app.activeChannelId === channel.id;
|
||||
const isCategory = channel.type === ChannelType.GuildCategory;
|
||||
const rowRenderer = ({ index, key, style }: ListRowProps) => {
|
||||
const item = channelsSorted[index];
|
||||
|
||||
const active = app.activeChannelId === item.id;
|
||||
const isCategory = item.type === ChannelType.GuildCategory;
|
||||
return (
|
||||
<ChannelListItem
|
||||
key={channel.id}
|
||||
guild={guild}
|
||||
channel={channel}
|
||||
isCategory={isCategory}
|
||||
active={active}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[app.account, app.activeChannelId, guild],
|
||||
<div style={style}>
|
||||
<ChannelListItem key={key} isCategory={isCategory} active={active} channel={item} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <List>{guild.channelsMapped.map((channel) => renderChannelListItem(channel))}</List>;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: "1 0 auto",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<List
|
||||
height={height}
|
||||
overscanRowCount={2}
|
||||
rowCount={channelsSorted.length}
|
||||
rowHeight={({ index }) => {
|
||||
const item = channelsSorted[index];
|
||||
if (item.type === ChannelType.GuildCategory) {
|
||||
return 44;
|
||||
}
|
||||
return 33;
|
||||
}}
|
||||
rowRenderer={rowRenderer}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ChannelList);
|
||||
|
@ -4,12 +4,11 @@ import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import { IContextMenuItem } from "./ContextMenuItem";
|
||||
import Icon from "./Icon";
|
||||
import CreateInviteModal from "./modals/CreateInviteModal";
|
||||
|
||||
const ListItem = styled.li<{ isCategory?: boolean }>`
|
||||
const ListItem = styled.div<{ isCategory?: boolean }>`
|
||||
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
||||
cursor: pointer;
|
||||
`;
|
||||
@ -36,13 +35,12 @@ const Text = styled.span<{ isCategory?: boolean }>`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
guild: Guild;
|
||||
channel: Channel;
|
||||
isCategory: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
function ChannelListItem({ guild, channel, isCategory, active }: Props) {
|
||||
function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { openModal } = useModals();
|
||||
@ -63,7 +61,7 @@ function ChannelListItem({ guild, channel, isCategory, active }: Props) {
|
||||
index: 0,
|
||||
label: "Create Channel Invite",
|
||||
onClick: () => {
|
||||
openModal(CreateInviteModal, { guild_id: guild.id, channel_id: channel.id });
|
||||
openModal(CreateInviteModal, { guild_id: channel.guildId!, channel_id: channel.id });
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiAccountPlus",
|
||||
@ -79,7 +77,7 @@ function ChannelListItem({ guild, channel, isCategory, active }: Props) {
|
||||
// prevent navigating to non-text channels
|
||||
if (!channel.isTextChannel) return;
|
||||
|
||||
navigate(`/channels/${guild.id}/${channel.id}`);
|
||||
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -24,7 +24,7 @@ function ChannelSidebar() {
|
||||
<Wrapper>
|
||||
{/* TODO: replace with dm search if no guild */}
|
||||
<ChannelHeader />
|
||||
{app.activeGuild ? <ChannelList guild={app.activeGuild} /> : <EmptyChannelList />}
|
||||
{app.activeGuild ? <ChannelList /> : <EmptyChannelList />}
|
||||
<UserPanel />
|
||||
</Wrapper>
|
||||
);
|
||||
|
@ -82,7 +82,7 @@ function GuildItem({ guild, active }: Props) {
|
||||
]);
|
||||
|
||||
const doNavigate = () => {
|
||||
const channel = guild.channelsMapped.find((x) => {
|
||||
const channel = guild.channels.find((x) => {
|
||||
const permission = Permissions.getPermission(app.account!.id, guild, x);
|
||||
return permission.has("VIEW_CHANNEL") && x.type !== ChannelType.GuildCategory;
|
||||
});
|
||||
|
@ -70,7 +70,7 @@ function SystemMessage({ message, highlight }: Props) {
|
||||
|
||||
return (
|
||||
<MessageBase header>
|
||||
<MessageInfo click={false}>
|
||||
<MessageInfo>
|
||||
<Icon icon={icon.icon} size="16px" color={icon.color ?? "var(--text-secondary)"} />
|
||||
</MessageInfo>
|
||||
<SystemContent>{children}</SystemContent>
|
||||
|
@ -131,7 +131,7 @@ function CreateInviteModal(props: InviteModalProps) {
|
||||
const [inviteExpiresAt, setInviteExpiresAt] = React.useState<Date | null>(null);
|
||||
|
||||
const guild = app.guilds.get(props.guild_id);
|
||||
const channel = props.channel_id ? guild?.channels.get(props.channel_id) : guild?.channels.getAll()[0];
|
||||
const channel = props.channel_id ? guild?.channels.find((x) => x.id === props.channel_id) : guild?.channels[0];
|
||||
|
||||
if (!guild || !channel) {
|
||||
closeModal();
|
||||
|
@ -144,7 +144,46 @@ export default class Guild {
|
||||
|
||||
@computed
|
||||
get channels() {
|
||||
return this.app.channels.getAll().filter((channel) => this.channels_.has(channel.id));
|
||||
return this.app.channels
|
||||
.getAll()
|
||||
.filter((channel) => this.channels_.has(channel.id))
|
||||
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
||||
}
|
||||
|
||||
@computed
|
||||
get channelsSorted() {
|
||||
const channels = this.channels;
|
||||
const categoryChannels = channels.filter((channel) => channel.type === ChannelType.GuildCategory);
|
||||
const nonCatChannels = channels.filter((channel) => channel.type !== ChannelType.GuildCategory);
|
||||
|
||||
const categories: { id: Snowflake; parent: Channel; children: Channel[] }[] = [];
|
||||
const uncategorized: Channel[] = [];
|
||||
|
||||
for (const channel of categoryChannels) {
|
||||
categories.push({
|
||||
id: channel.id,
|
||||
parent: channel,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const channel of nonCatChannels) {
|
||||
if (channel.parentId) {
|
||||
const category = categories.find((category) => category.id === channel.parentId);
|
||||
if (category) {
|
||||
category.children.push(channel);
|
||||
}
|
||||
} else {
|
||||
uncategorized.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
const a = categories.map((x) => {
|
||||
// return an array of parent, and children flattened
|
||||
return [x.parent, ...x.children.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))];
|
||||
});
|
||||
|
||||
return [...a.flat(), ...uncategorized.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))];
|
||||
}
|
||||
|
||||
@computed
|
||||
@ -154,52 +193,13 @@ export default class Guild {
|
||||
|
||||
@action
|
||||
addChannel(data: APIChannel) {
|
||||
this.channels_.add(data.id);
|
||||
this.app.channels.add(data);
|
||||
this.channels_.add(data.id);
|
||||
}
|
||||
|
||||
@action
|
||||
removeChannel(id: Snowflake) {
|
||||
this.channels_.delete(id);
|
||||
this.app.channels.remove(id);
|
||||
}
|
||||
|
||||
@computed
|
||||
get channelsMapped(): Channel[] {
|
||||
const channels = this.channels;
|
||||
|
||||
const result: {
|
||||
id: string;
|
||||
children: Channel[];
|
||||
category: Channel | null;
|
||||
}[] = [];
|
||||
|
||||
const categories = this.app.channels.sortPosition(channels.filter((x) => x.type === ChannelType.GuildCategory));
|
||||
const categorizedChannels = channels.filter((x) => x.type !== ChannelType.GuildCategory && x.parentId !== null);
|
||||
const uncategorizedChannels = this.app.channels.sortPosition(
|
||||
channels.filter((x) => x.type !== ChannelType.GuildCategory && x.parentId === null),
|
||||
);
|
||||
|
||||
// for each category, add an object containing the category and its children
|
||||
categories.forEach((category) => {
|
||||
result.push({
|
||||
id: category.id,
|
||||
children: this.app.channels.sortPosition(categorizedChannels.filter((x) => x.parentId === category.id)),
|
||||
category: category,
|
||||
});
|
||||
});
|
||||
|
||||
// add an object containing the remaining uncategorized channels
|
||||
result.push({
|
||||
id: "root",
|
||||
children: uncategorizedChannels,
|
||||
category: null,
|
||||
});
|
||||
|
||||
// flatten down to a single array where the category is the first element followed by its children
|
||||
return result
|
||||
.map((x) => [x.category, ...x.children])
|
||||
.flat()
|
||||
.filter((x) => x !== null) as Channel[];
|
||||
this.channels_.delete(id);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user