1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 10:22:30 +01:00

channel creation

This commit is contained in:
Puyodead1 2024-03-04 18:15:33 -05:00
parent 5847ed2df0
commit 09a8401712
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
18 changed files with 444 additions and 31 deletions

8
src-tauri/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="theme" value="material" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/src-tauri.iml" filepath="$PROJECT_DIR$/.idea/src-tauri.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
src-tauri/.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -66,7 +66,7 @@ export const LabelWrapper = styled.div<{ error?: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 8px; margin-bottom: 8px;
color: ${(props) => (props.error ? "var(--error)" : "var(--text)")}; color: ${(props) => (props.error ? "var(--error)" : "var(--text-header-secondary)")};
`; `;
export const InputErrorText = styled.label` export const InputErrorText = styled.label`
@ -88,10 +88,9 @@ export const InputWrapper = styled.div`
display: flex; display: flex;
`; `;
// TODO: Fix border hover causing small layout shift export const Input = styled.input<{ error?: boolean; disableFocusRing?: boolean }>`
export const Input = styled.input<{ error?: boolean }>`
outline: none; outline: none;
background: var(--background-secondary-alt); background: var(--background-secondary);
padding: 10px; padding: 10px;
font-size: 16px; font-size: 16px;
flex: 1; flex: 1;
@ -100,17 +99,23 @@ export const Input = styled.input<{ error?: boolean }>`
margin: 0; margin: 0;
border: none; border: none;
aria-invalid: ${(props) => (props.error ? "true" : "false")}; aria-invalid: ${(props) => (props.error ? "true" : "false")};
border: ${(props) => (props.error ? "1px solid var(--error)" : "none")}; border: ${(props) => (props.error ? "1px solid var(--error)" : "1px solid var(--background-secondary)")};
&:focus { ${(props) =>
border: 1px solid var(--primary); !props.disableFocusRing &&
} `
&:focus {
border: 1px solid var(--primary);
}
`}
// disabled styling // disabled styling
&:disabled { &:disabled {
background: var(--background-secondary-alt); // TODO: this might need to be adjusted
background: var(--background-primary-alt);
color: var(--text-disabled); color: var(--text-disabled);
border: 1px solid var(--background-secondary-alt); border: 1px solid var(--background-secondary-alt);
cursor: not-allowed;
} }
-moz-appearance: textfield; -moz-appearance: textfield;

View File

@ -25,6 +25,7 @@ function ChannelList() {
const active = app.activeChannelId === item.id; const active = app.activeChannelId === item.id;
const isCategory = item.type === ChannelType.GuildCategory; const isCategory = item.type === ChannelType.GuildCategory;
return ( return (
<div style={style}> <div style={style}>
<ChannelListItem key={key} isCategory={isCategory} active={active} channel={item} /> <ChannelListItem key={key} isCategory={isCategory} active={active} channel={item} />

View File

@ -2,6 +2,7 @@ import React, { useContext } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext"; import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { modalController } from "../../controllers/modals";
import Channel from "../../stores/objects/Channel"; import Channel from "../../stores/objects/Channel";
import Icon from "../Icon"; import Icon from "../Icon";
import Floating from "../floating/Floating"; import Floating from "../floating/Floating";
@ -9,7 +10,7 @@ import FloatingTrigger from "../floating/FloatingTrigger";
const ListItem = styled.div<{ isCategory?: boolean }>` const ListItem = styled.div<{ isCategory?: boolean }>`
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")}; padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
cursor: ${(props) => (props.isCategory ? "not-allowed" : "pointer")}; cursor: pointer;
`; `;
const Wrapper = styled.div<{ isCategory?: boolean; active?: boolean }>` const Wrapper = styled.div<{ isCategory?: boolean; active?: boolean }>`
@ -45,7 +46,9 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const contextMenu = useContext(ContextMenuContext); const contextMenu = useContext(ContextMenuContext);
const [hovered, setHovered] = React.useState(false); const [wrapperHovered, setWrapperHovered] = React.useState(false);
const [createChannelHovered, setCreateChannelHovered] = React.useState(false);
const [createChannelDown, setChannelCreateDown] = React.useState(false);
return ( return (
<ListItem <ListItem
@ -63,8 +66,8 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
<Wrapper <Wrapper
isCategory={isCategory} isCategory={isCategory}
active={active} active={active}
onMouseOver={() => setHovered(true)} onMouseOver={() => setWrapperHovered(true)}
onMouseOut={() => setHovered(false)} onMouseOut={() => setWrapperHovered(false)}
> >
<div <div
style={{ style={{
@ -87,13 +90,13 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
<Icon <Icon
icon="mdiChevronDown" icon="mdiChevronDown"
size="12px" size="12px"
color={hovered ? "var(--text)" : "var(--text-secondary)"} color={wrapperHovered ? "var(--text)" : "var(--text-secondary)"}
style={{ style={{
marginRight: "8px", marginRight: "8px",
}} }}
/> />
)} )}
<Text isCategory={isCategory} hovered={hovered}> <Text isCategory={isCategory} hovered={wrapperHovered}>
{channel.name} {channel.name}
</Text> </Text>
</div> </div>
@ -107,14 +110,37 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
}} }}
> >
<FloatingTrigger> <FloatingTrigger>
<span> <span
onMouseOver={() => setCreateChannelHovered(true)}
onMouseOut={() => setCreateChannelHovered(false)}
onMouseDown={() => setChannelCreateDown(true)}
onMouseUp={() => setChannelCreateDown(false)}
onClick={() => {
if (!channel.guild) {
console.warn("No guild found for channel", channel);
return;
}
modalController.push({
type: "create_channel",
guild: channel.guild,
category: channel,
});
}}
>
<Icon <Icon
icon="mdiPlus" icon="mdiPlus"
size="18px" size="18px"
style={{ style={{
marginLeft: "auto", marginLeft: "auto",
}} }}
color={hovered ? "var(--text)" : "var(--text-secondary)"} color={
createChannelDown
? "var(--text-header)"
: createChannelHovered
? "var(--text)"
: "var(--text-secondary)"
}
/> />
</span> </span>
</FloatingTrigger> </FloatingTrigger>

View File

@ -7,7 +7,7 @@
position: relative; position: relative;
display: block; display: block;
padding: 10px; padding: 10px;
background: var(--background-secondary-alt); background: var(--background-secondary);
border: none; border: none;
color: var(--text); color: var(--text);
outline: none; outline: none;

View File

@ -3,7 +3,7 @@ import styled from "styled-components";
// TODO: migrate some things from AuthComponents // TODO: migrate some things from AuthComponents
export const InputSelect = styled.select` export const InputSelect = styled.select`
background-color: var(--background-secondary-alt); background-color: var(--background-secondary);
color: var(--text); color: var(--text);
outline: none; outline: none;
border: 1px solid transparent; border: 1px solid transparent;

View File

@ -24,16 +24,23 @@ function GuildMenuPopout() {
}); });
} }
function onChannelCreateClick() {
modalController.push({
type: "create_channel",
guild: activeGuild!,
});
}
return ( return (
<CustomContextMenu> <CustomContextMenu>
<ContextMenuButton icon="mdiCog" disabled> <ContextMenuButton icon="mdiCog" disabled>
Server Settings Server Settings
</ContextMenuButton> </ContextMenuButton>
<ContextMenuButton icon="mdiPlusCircle" disabled> <ContextMenuButton icon="mdiPlusCircle" onClick={onChannelCreateClick}>
Create Channel Create Channel
</ContextMenuButton> </ContextMenuButton>
<ContextMenuButton icon="mdiFolderPlus" disabled> <ContextMenuButton icon="mdiFolderPlus" disabled>
Create Channel Create Category
</ContextMenuButton> </ContextMenuButton>
<ContextMenuDivider /> <ContextMenuDivider />
<ContextMenuButton icon="mdiBell" disabled> <ContextMenuButton icon="mdiBell" disabled>

View File

@ -0,0 +1,322 @@
import { yupResolver } from "@hookform/resolvers/yup";
import {
ChannelType,
RESTPostAPIGuildChannelJSONBody,
RESTPostAPIGuildChannelResult,
Routes,
} from "@spacebarchat/spacebar-api-types/v9";
import React from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText } from "../AuthComponents";
import { TextDivider } from "../Divider";
import Icon, { IconType } from "../Icon";
import { Modal } from "./ModalComponents";
const CHANNEL_OPTIONS: {
label: string;
description: string;
icon: IconType;
type: ChannelType;
note?: string;
canBePrivate?: boolean;
}[] = [
{
label: "Text",
description: "Send messages, images, and GIFs",
icon: "mdiPound",
type: ChannelType.GuildText,
},
{
label: "Voice",
description: "Hang out and talk with friends",
icon: "mdiVolumeHigh",
type: ChannelType.GuildVoice,
},
{
label: "Announcement",
description: "Important updates for people in and out of the server",
icon: "mdiBullhorn",
type: ChannelType.GuildAnnouncement,
note: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
canBePrivate: false,
},
];
const TitleText = styled.h1`
font-size: 20px;
font-weight: var(--font-weight-medium);
`;
const DescriptionText = styled.p`
font-size: 16px;
font-weight: var(--font-weight-regular);
color: var(--text-header-secondary);
`;
const Form = styled.form`
width: 100%;
margin-top: 10px;
`;
const List = styled.ul`
list-style: none;
padding: 0;
margin: 0;
gap: 10px;
display: flex;
flex-direction: column;
`;
const ListItem = styled.li<{ active: boolean }>`
padding: 10px;
border-radius: 10px;
background-color: ${(props) =>
props.active ? "var(--background-secondary-highlight)" : "var(--background-secondary)"};
filter: brightness(${(props) => (props.active ? 1.3 : 1.1)});
display: flex;
flex-direction: column;
gap: 3px;
&:hover {
cursor: pointer;
background-color: var(--background-secondary-highlight);
filter: ${(props) => (props.active ? "brightness(1.3)" : "brightness(1.1)")};
}
& span {
color: var(--text-header-secondary);
font-size: 16px;
font-weight: var(--font-weight-medium);
}
& span:nth-child(2) {
font-size: 14px;
font-weight: var(--font-weight-regular);
}
`;
const Label = styled.label`
font-weight: var(--font-weight-medium);
font-size: 14px;
color: var(--text-header-secondary);
`;
const Section = styled.div<{ row?: boolean }>`
margin: 10px 0;
display: flex;
flex-direction: ${(props) => (props.row ? "row" : "column")};
align-items: ${(props) => (props.row ? "center" : "stretch")};
justify-content: space-between;
gap: 10px;
`;
export const LabelWrapper = styled.div<{ error?: boolean }>`
& label,
span {
color: ${(props) => (props.error ? "var(--error)" : "var(--text-header-secondary)")};
}
`;
const schema = yup
.object({
type: yup
.number()
.oneOf(Object.values(ChannelType).filter((x) => typeof x === "number") as number[])
.required(),
name: yup.string().required(),
private: yup.boolean(),
})
.required();
export function CreateChannelModel({ guild, category, ...props }: ModalProps<"create_channel">) {
const app = useAppStore();
const navigate = useNavigate();
const [isLoading, setLoading] = React.useState(false);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const {
formState: { disabled, isSubmitting, isValid, errors },
register,
handleSubmit,
setValue,
setError,
watch,
} = useForm({
resolver: yupResolver(schema),
defaultValues: {
type: CHANNEL_OPTIONS[0].type,
private: false,
},
});
const isDisabled = disabled || isLoading || isSubmitting || !isValid;
const onSubmit = handleSubmit((data) => {
// set form loading
setLoading(true);
app.rest
.post<RESTPostAPIGuildChannelJSONBody, RESTPostAPIGuildChannelResult>(Routes.guildChannels(guild.id), {
name: data.name,
type: data.type,
parent_id: category?.id,
// permission_overwrites: []
})
.then((channel) => {
// add the new channel to the guild
app.channels.add(channel);
guild.channels_.add(channel.id);
// navigate to the new channel
navigate(`/channels/${guild.id}/${channel.id}`);
modalController.pop("close");
})
.catch((e) => {
console.error(e);
// error
if (e.errors) {
const t = messageFromFieldError(e.errors);
if (t) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setError(t.field as any, {
type: "manual",
message: t.error,
});
} else {
setError("type", {
type: "manual",
message: e.message,
});
}
} else {
setError("type", {
type: "manual",
message: e.message,
});
}
// only do this here because if successful there is no reason to re-enable the button
setLoading(false);
});
});
const nameProps = register("name", {
required: true,
});
return (
<Modal
{...props}
title={<TitleText>Create Channel</TitleText>}
description={<DescriptionText>in {category ? category.name : guild.name}</DescriptionText>}
actions={[
{
onClick: onSubmit,
children: <span>Create Channel</span>,
palette: "primary",
size: "small",
disabled: isDisabled,
},
{
onClick: () => {
modalController.pop("close");
},
children: <span>Cancel</span>,
palette: "link",
size: "small",
confirmation: true,
},
]}
>
<Form>
<List>
<Section>
<LabelWrapper error={!!errors.type}>
<Label>Channel Type</Label>
{errors.type && (
<>
<TextDivider>-</TextDivider>
<InputErrorText>{errors.type.message}</InputErrorText>
</>
)}
</LabelWrapper>
{CHANNEL_OPTIONS.map((option, index) => (
<ListItem
active={index === selectedIndex}
key={index}
onClick={() => {
setValue("type", option.type);
setSelectedIndex(index);
}}
style={{
display: "flex",
alignItems: "center",
flexDirection: "row",
}}
>
{option.icon && <Icon icon={option.icon} color="var(--text-disabled)" size="24px" />}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "3px",
marginLeft: 10,
}}
>
<span>{option.label}</span>
<span>{option.description}</span>
</div>
</ListItem>
))}
</Section>
<Section>
<LabelWrapper error={!!errors.name}>
<Label>Channel Name</Label>
{errors.name && (
<>
<TextDivider>-</TextDivider>
<InputErrorText>{errors.name.message}</InputErrorText>
</>
)}
</LabelWrapper>
<div
style={{
display: "flex",
alignItems: "center",
backgroundColor: "var(--background-secondary)",
borderRadius: 8,
padding: "0 10px",
}}
>
<Icon icon={CHANNEL_OPTIONS[selectedIndex].icon} size="16px" color="var(--text)" />
<Input
{...nameProps}
onChange={(e) => {
e.target.value = e.target.value.replace(" ", "-");
nameProps.onChange(e);
}}
disableFocusRing
style={{
borderRadius: 8,
}}
placeholder="new-channel"
error={!!errors.name}
/>
</div>
</Section>
{/* // TODO: Add private channel support */}
{/* {(CHANNEL_OPTIONS[selectedIndex].canBePrivate ?? true) && (
<Section row>
<Label>Private Channel</Label>
<Input {...register("private", { required: false })} type="checkbox" />
</Section>
)} */}
</List>
</Form>
</Modal>
);
}

View File

@ -246,7 +246,7 @@ export function CreateInviteModal({ target, ...props }: ModalProps<"create_invit
<InputWrapper <InputWrapper
style={{ style={{
background: "var(--background-secondary-alt)", background: "var(--background-secondary)",
borderRadius: "12px", borderRadius: "12px",
}} }}
> >

View File

@ -16,7 +16,7 @@ interface ModalProps {
children?: React.ReactNode; children?: React.ReactNode;
onClose?: (force: boolean) => void; onClose?: (force: boolean) => void;
signal?: "close" | "confirm" | "cancel"; signal?: "close" | "confirm" | "cancel";
title?: string; title?: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
transparent?: boolean; transparent?: boolean;
nonDismissable?: boolean; nonDismissable?: boolean;
@ -62,10 +62,10 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
> div { > div {
animation-name: ${animationZoomOut}; animation-name: ${animationZoomOut};
} }
` `
: css` : css`
animation-name: ${animationFadeIn}; animation-name: ${animationFadeIn};
`} `}
`; `;
/** /**
@ -96,7 +96,7 @@ export const ModalWrapper = styled.div<
!props.transparent && !props.transparent &&
css` css`
overflow: hidden; overflow: hidden;
background: var(--background-secondary); background: var(--background-primary);
border-radius: 8px; border-radius: 8px;
`} `}
`; `;
@ -126,7 +126,7 @@ export const ModalContentContainer = styled.div<Pick<ModalProps, "transparent" |
${(props) => ${(props) =>
!props.transparent && !props.transparent &&
css` css`
background: var(--background-secondary); background: var(--background-primary);
`} `}
`; `;
@ -135,7 +135,7 @@ const Actions = styled.div`
display: flex; display: flex;
padding: 16px; padding: 16px;
flex-direction: row-reverse; flex-direction: row-reverse;
background: var(--background-primary); background: var(--background-secondary);
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
`; `;

View File

@ -1,5 +1,6 @@
export * from "./AddServerModal"; export * from "./AddServerModal";
export * from "./BanMemberModal"; export * from "./BanMemberModal";
export * from "./CreateChannelModel";
export * from "./CreateInviteModal"; export * from "./CreateInviteModal";
export * from "./CreateServerModal"; export * from "./CreateServerModal";
export * from "./DeleteMessageModal"; export * from "./DeleteMessageModal";

View File

@ -4,6 +4,7 @@ import { action, computed, makeObservable, observable } from "mobx";
import { import {
AddServerModal, AddServerModal,
BanMemberModal, BanMemberModal,
CreateChannelModel,
CreateInviteModal, CreateInviteModal,
CreateServerModal, CreateServerModal,
DeleteMessageModal, DeleteMessageModal,
@ -154,7 +155,7 @@ export const modalController = new ModalControllerExtended({
// block_user: Confirmation, // block_user: Confirmation,
// unfriend_user: Confirmation, // unfriend_user: Confirmation,
// create_category: CreateCategory, // create_category: CreateCategory,
// create_channel: CreateChannel, create_channel: CreateChannelModel,
// create_group: CreateGroup, // create_group: CreateGroup,
create_invite: CreateInviteModal, create_invite: CreateInviteModal,
// create_role: CreateRole, // create_role: CreateRole,

View File

@ -50,6 +50,11 @@ export type Modal = {
height?: number; height?: number;
isVideo?: boolean; isVideo?: boolean;
} }
| {
type: "create_channel";
guild: Guild;
category?: Channel;
}
); );
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & { export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {

View File

@ -198,7 +198,7 @@ export default class Guild {
reason reason
? { ? {
"X-Audit-Log-Reason": reason, "X-Audit-Log-Reason": reason,
} }
: {}, : {},
); );
} }
@ -214,7 +214,7 @@ export default class Guild {
reason reason
? { ? {
"X-Audit-Log-Reason": reason, "X-Audit-Log-Reason": reason,
} }
: {}, : {},
); );
} }