feat(categories): include uncategorised channels; add category / channel; delete category

This commit is contained in:
Paul 2021-10-30 19:38:18 +01:00
parent bb5509f660
commit c208064d2c
3 changed files with 297 additions and 255 deletions

View file

@ -2,6 +2,7 @@ import { Prompt } from "react-router";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import type { Attachment } from "revolt-api/types/Autumn"; import type { Attachment } from "revolt-api/types/Autumn";
import { Bot } from "revolt-api/types/Bots"; import { Bot } from "revolt-api/types/Bots";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import type { EmbedImage } from "revolt-api/types/January"; import type { EmbedImage } from "revolt-api/types/January";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js/dist/maps/Messages";
@ -42,7 +43,12 @@ export type Screen =
| { type: "leave_server"; target: Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_bot"; target: string; name: string; cb: () => void } | {
type: "delete_bot";
target: string;
name: string;
cb?: () => void;
}
| { type: "delete_message"; target: Message } | { type: "delete_message"; target: Message }
| { | {
type: "create_invite"; type: "create_invite";
@ -52,7 +58,11 @@ export type Screen =
| { type: "ban_member"; target: Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Server } | {
type: "create_channel";
target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void;
}
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
)) ))
| ({ id: "special_input" } & ( | ({ id: "special_input" } & (

View file

@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
@ -60,7 +61,7 @@ type SpecialProps = { onClose: () => void } & (
| { type: "leave_server"; target: Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channel } | { type: "delete_channel"; target: Channel }
| { type: "delete_bot"; target: string; name: string; cb: () => void } | { type: "delete_bot"; target: string; name: string; cb?: () => void }
| { type: "delete_message"; target: MessageI } | { type: "delete_message"; target: MessageI }
| { | {
type: "create_invite"; type: "create_invite";
@ -70,7 +71,11 @@ type SpecialProps = { onClose: () => void } & (
| { type: "ban_member"; target: Server; user: User } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Server } | {
type: "create_channel";
target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void;
}
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
); );
@ -158,7 +163,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
break; break;
case "delete_bot": case "delete_bot":
client.bots.delete(props.target); client.bots.delete(props.target);
props.cb(); props.cb?.();
break; break;
} }
@ -424,9 +429,14 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
nonce: ulid(), nonce: ulid(),
}); });
history.push( if (props.cb) {
`/server/${props.target._id}/channel/${channel._id}`, props.cb(channel);
); } else {
history.push(
`/server/${props.target._id}/channel/${channel._id}`,
);
}
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
@ -472,7 +482,6 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
} }
case "create_category": { case "create_category": {
const [name, setName] = useState(""); const [name, setName] = useState("");
const history = useHistory();
return ( return (
<PromptModal <PromptModal

View file

@ -1,16 +1,20 @@
import { Check } from "@styled-icons/boxicons-regular"; import { Filter, Plus, X } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import { Category } from "revolt-api/types/Servers"; import { Category } from "revolt-api/types/Servers";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useAutosave, useAutosaveCallback } from "../../../lib/debounce"; import { useAutosave } from "../../../lib/debounce";
import { noop } from "../../../lib/js";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import ChannelIcon from "../../../components/common/ChannelIcon"; import ChannelIcon from "../../../components/common/ChannelIcon";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
@ -19,16 +23,6 @@ import InputBox from "../../../components/ui/InputBox";
import SaveStatus, { EditStatus } from "../../../components/ui/SaveStatus"; import SaveStatus, { EditStatus } from "../../../components/ui/SaveStatus";
import Tip from "../../../components/ui/Tip"; import Tip from "../../../components/ui/Tip";
/* interface CreateCategoryProps {
callback: (name: string) => void;
}
function CreateCategory({ callback }: CreateCategoryProps) {
const [name, setName] = useState("");
return <></>;
} */
const KanbanEntry = styled.div` const KanbanEntry = styled.div`
padding: 2px 4px; padding: 2px 4px;
@ -73,16 +67,43 @@ const KanbanList = styled.div<{ last: boolean }>`
flex-direction: column; flex-direction: column;
background: var(--secondary-background); background: var(--secondary-background);
input {
width: 100%;
height: 100%;
border: none;
font-size: 1em;
text-align: center;
background: transparent;
color: var(--foreground);
}
> [data-rbd-droppable-id] { > [data-rbd-droppable-id] {
min-height: 24px; min-height: 24px;
} }
} }
`; `;
const KanbanListTitle = styled.div` const Row = styled.div`
height: 42px; gap: 2px;
margin: 4px;
display: flex;
> :first-child {
flex-grow: 1;
}
`;
const KanbanListHeader = styled.div`
height: 34px;
display: grid; display: grid;
min-width: 34px;
place-items: center; place-items: center;
cursor: pointer !important;
transition: 0.2s ease background-color;
&:hover {
background: var(--background);
}
`; `;
const KanbanBoard = styled.div` const KanbanBoard = styled.div`
@ -129,6 +150,19 @@ export const Categories = observer(({ server }: Props) => {
() => setStatus("editing"), () => setStatus("editing"),
); );
const defaultCategory = useMemo(() => {
return {
title: "Uncategorized",
channels: [...server.channels]
.filter((x) => x)
.map((x) => x!._id)
.filter(
(x) => !categories.find((cat) => cat.channels.includes(x)),
),
id: "none",
};
}, [categories, server.channels]);
return ( return (
<> <>
<Header> <Header>
@ -150,6 +184,8 @@ export const Categories = observer(({ server }: Props) => {
} }
if (type === "column") { if (type === "column") {
if (destination.index === 0) return;
// Remove from array. // Remove from array.
const cat = categories.find( const cat = categories.find(
(x) => x.id === draggableId, (x) => x.id === draggableId,
@ -159,7 +195,7 @@ export const Categories = observer(({ server }: Props) => {
); );
// Insert at new position. // Insert at new position.
arr.splice(destination.index, 0, cat!); arr.splice(destination.index - 1, 0, cat!);
setCategories(arr); setCategories(arr);
} else { } else {
setCategories( setCategories(
@ -204,125 +240,75 @@ export const Categories = observer(({ server }: Props) => {
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps}> {...provided.droppableProps}>
<KanbanBoard> <KanbanBoard>
<ListElement
category={defaultCategory}
server={server}
index={0}
addChannel={noop}
/>
{categories.map((category, index) => ( {categories.map((category, index) => (
<Draggable <ListElement
draggable
category={category}
server={server}
index={index + 1}
key={category.id} key={category.id}
draggableId={category.id} setTitle={(title) => {
index={index}> setCategories(
{(provided) => categories.map((x) =>
( x.id === category.id
<div ? {
{...(provided.draggableProps as any)} ...x,
ref={ title,
provided.innerRef }
}> : x,
<KanbanList ),
last={ );
index === }}
categories.length - deleteSelf={() =>
1 setCategories(
} categories.filter(
key={ (x) =>
category.id x.id !==
}> category.id,
<div class="inner"> ),
<KanbanListTitle )
{...(provided.dragHandleProps as any)}>
<span>
{
category.title
}
</span>
</KanbanListTitle>
<Droppable
droppableId={
category.id
}
key={
category.id
}>
{(
provided,
) =>
(
<div
ref={
provided.innerRef
}
{...provided.droppableProps}>
{category.channels.map(
(
x,
index,
) => {
const channel =
server.client.channels.get(
x,
);
if (
!channel
)
return null;
return (
<Draggable
key={
x
}
draggableId={
x
}
index={
index
}>
{(
provided,
) =>
(
<div
{...(provided.draggableProps as any)}
{...provided.dragHandleProps}
ref={
provided.innerRef
}>
<KanbanEntry>
<div class="inner">
<ChannelIcon
target={
channel
}
size={
24
}
/>
<span>
{
channel.name
}
</span>
</div>
</KanbanEntry>
</div>
) as any
}
</Draggable>
);
},
)}
{
provided.placeholder
}
</div>
) as any
}
</Droppable>
</div>
</KanbanList>
</div>
) as any
} }
</Draggable> addChannel={(channel) => {
setCategories(
categories.map((x) =>
x.id === category.id
? {
...x,
channels:
[
...x.channels,
channel._id,
],
}
: x,
),
);
}}
/>
))} ))}
<KanbanList last>
<div class="inner">
<KanbanListHeader
onClick={() =>
setCategories([
...categories,
{
id: ulid(),
title: "New Category",
channels: [],
},
])
}>
<Plus size={24} />
</KanbanListHeader>
</div>
</KanbanList>
{provided.placeholder} {provided.placeholder}
</KanbanBoard> </KanbanBoard>
</div> </div>
@ -335,124 +321,161 @@ export const Categories = observer(({ server }: Props) => {
); );
}); });
// ! FIXME: really bad code function ListElement({
export const Categories0 = observer(({ server }: Props) => { category,
const channels = server.channels.filter((x) => typeof x !== "undefined"); server,
index,
setTitle,
deleteSelf,
addChannel,
draggable,
}: {
category: Category;
server: Server;
index: number;
setTitle?: (title: string) => void;
deleteSelf?: () => void;
addChannel: (channel: TextChannel | VoiceChannel) => void;
draggable?: boolean;
}) {
const { openScreen } = useIntermediate();
const [editing, setEditing] = useState<string>();
const startEditing = () => setTitle && setEditing(category.title);
const [cats, setCats] = useState<Category[]>(server.categories ?? []); const save = useCallback(() => {
const [name, setName] = useState(""); setEditing(undefined);
setTitle!(editing!);
}, [editing, setTitle]);
useEffect(() => {
if (!editing) return;
function onClick(ev: MouseEvent) {
if ((ev.target as HTMLElement)?.id !== category.id) {
save();
}
}
document.addEventListener("mousedown", onClick);
return () => document.removeEventListener("mousedown", onClick);
}, [editing, category.id, save]);
return ( return (
<div> <Draggable
<Tip warning>This section is under construction.</Tip> isDragDisabled={!draggable}
<p> key={category.id}
<Button draggableId={category.id}
contrast index={index}>
disabled={isEqual(server.categories ?? [], cats)} {(provided) =>
onClick={() => server.edit({ categories: cats })}> (
save categories
</Button>
</p>
<h2>categories</h2>
{cats.map((category) => (
<div style={{ background: "var(--hover)" }} key={category.id}>
<InputBox
value={category.title}
onChange={(e) =>
setCats(
cats.map((y) =>
y.id === category.id
? {
...y,
title: e.currentTarget.value,
}
: y,
),
)
}
contrast
/>
<Button
contrast
onClick={() =>
setCats(cats.filter((x) => x.id !== category.id))
}>
delete {category.title}
</Button>
</div>
))}
<h2>create new</h2>
<p>
<InputBox
value={name}
onChange={(e) => setName(e.currentTarget.value)}
contrast
/>
<Button
contrast
onClick={() => {
setName("");
setCats([
...cats,
{
id: ulid(),
title: name,
channels: [],
},
]);
}}>
create
</Button>
</p>
<h2>channels</h2>
{channels.map((channel) => {
return (
<div <div
key={channel!._id} {...(provided.draggableProps as any)}
style={{ ref={provided.innerRef}>
display: "flex", <KanbanList last={false} key={category.id}>
gap: "12px", <div class="inner">
alignItems: "center", <Row>
}}> <KanbanListHeader
<div style={{ flexShrink: 0 }}> {...(provided.dragHandleProps as any)}>
<ChannelIcon target={channel} size={24} />{" "} {editing ? (
<span>{channel!.name}</span> <input
</div> value={editing}
<ComboBox onChange={(e) =>
style={{ flexGrow: 1 }} setEditing(
value={ e.currentTarget.value,
cats.find((x) => )
x.channels.includes(channel!._id), }
)?.id ?? "none" onKeyDown={(e) =>
} e.key === "Enter" && save()
onChange={(e) => }
setCats( id={category.id}
cats.map((x) => { />
return { ) : (
...x, <span onClick={startEditing}>
channels: [ {category.title}
...x.channels.filter( </span>
(y) => y !== channel!._id, )}
), </KanbanListHeader>
...(e.currentTarget.value === {deleteSelf && (
x.id <KanbanListHeader onClick={deleteSelf}>
? [channel!._id] <X size={24} />
: []), </KanbanListHeader>
], )}
}; </Row>
}), <Droppable
) droppableId={category.id}
}> key={category.id}>
<option value="none">Uncategorised</option> {(provided) =>
{cats.map((x) => ( (
<option key={x.id} value={x.id}> <div
{x.title} ref={provided.innerRef}
</option> {...provided.droppableProps}>
))} {category.channels.map(
</ComboBox> (x, index) => {
const channel =
server.client.channels.get(
x,
);
if (!channel)
return null;
return (
<Draggable
key={x}
draggableId={x}
index={index}>
{(provided) =>
(
<div
{...(provided.draggableProps as any)}
{...provided.dragHandleProps}
ref={
provided.innerRef
}>
<KanbanEntry>
<div class="inner">
<ChannelIcon
target={
channel
}
size={
24
}
/>
<span>
{
channel.name
}
</span>
</div>
</KanbanEntry>
</div>
) as any
}
</Draggable>
);
},
)}
{provided.placeholder}
</div>
) as any
}
</Droppable>
<KanbanListHeader
onClick={() =>
openScreen({
id: "special_prompt",
type: "create_channel",
target: server,
cb: addChannel,
})
}>
<Plus size={24} />
</KanbanListHeader>
</div>
</KanbanList>
</div> </div>
); ) as any
})} }
</div> </Draggable>
); );
}); }