diff --git a/src/components/Button.tsx b/src/components/Button.tsx index e1cbaa0..3a328c3 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -44,18 +44,6 @@ export default styled.button` return "45px"; } }}; - width: ${(props) => { - if (props.grow) return "auto"; - switch (props.size) { - default: - case "small": - return "96px"; - case "medium": - return "96px"; - case "large": - return "130px"; - } - }}; min-width: ${(props) => { if (props.grow) return "auto"; switch (props.size) { diff --git a/src/components/ChannelHeader.tsx b/src/components/ChannelHeader.tsx index dd171de..9fb5173 100644 --- a/src/components/ChannelHeader.tsx +++ b/src/components/ChannelHeader.tsx @@ -1,9 +1,11 @@ import { observer } from "mobx-react-lite"; -import React from "react"; +import React, { useEffect } from "react"; import styled from "styled-components"; import { useAppStore } from "../stores/AppStore"; import Icon, { IconProps } from "./Icon"; import { SectionHeader } from "./SectionHeader"; +import Floating from "./floating/Floating"; +import FloatingTrigger from "./floating/FloatingTrigger"; const Wrapper = styled(SectionHeader)` background-color: var(--background-secondary); @@ -26,13 +28,17 @@ const HeaderText = styled.header` function ChannelHeader() { const app = useAppStore(); + const [isOpen, setOpen] = React.useState(false); const [icon, setIcon] = React.useState("mdiChevronDown"); - function openMenu(e: React.MouseEvent) { - e.stopPropagation(); + const onOpenChange = (open: boolean) => { + setOpen(open); + }; - setIcon("mdiClose"); - } + useEffect(() => { + if (isOpen) setIcon("mdiChevronDown"); + else setIcon("mdiClose"); + }, [isOpen]); if (app.activeGuildId === "@me") { return ( @@ -52,10 +58,14 @@ function ChannelHeader() { if (!app.activeGuild) return null; return ( - - {app.activeGuild.name} - - + + + + {app.activeGuild.name} + + + + ); } diff --git a/src/components/floating/Floating.tsx b/src/components/floating/Floating.tsx index 4b171c6..dc8d655 100644 --- a/src/components/floating/Floating.tsx +++ b/src/components/floating/Floating.tsx @@ -2,9 +2,11 @@ import { FloatingArrow, FloatingPortal, Placement } from "@floating-ui/react"; import { motion } from "framer-motion"; import { FloatingContext } from "../../contexts/FloatingContext"; import useFloating from "../../hooks/useFloating"; +import Guild from "../../stores/objects/Guild"; import GuildMember from "../../stores/objects/GuildMember"; import User from "../../stores/objects/User"; import Tooltip from "../Tooltip"; +import GuildMenuPopout from "./GuildMenuPopout"; import UserProfilePopout from "./UserProfilePopout"; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -13,6 +15,7 @@ type Components = Record>; const components: Components = { userPopout: UserProfilePopout, tooltip: Tooltip, + guild: GuildMenuPopout, }; export type FloatingOptions = { @@ -36,6 +39,12 @@ export type FloatingOptions = { aria?: string; }; } + | { + type: "guild"; + props: { + guild: Guild; + }; + } ); export type FloatingProps = (FloatingOptions & { diff --git a/src/components/floating/GuildMenuPopout.tsx b/src/components/floating/GuildMenuPopout.tsx new file mode 100644 index 0000000..34e2438 --- /dev/null +++ b/src/components/floating/GuildMenuPopout.tsx @@ -0,0 +1,53 @@ +import styled from "styled-components"; +import useLogger from "../../hooks/useLogger"; + +import { modalController } from "../../controllers/modals"; +import { useAppStore } from "../../stores/AppStore"; +import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "../contextMenus/ContextMenu"; + +const CustomContextMenu = styled(ContextMenu)` + width: 200px; +`; + +function GuildMenuPopout() { + const { activeGuild } = useAppStore(); + const logger = useLogger("GuildMenuPopout"); + if (!activeGuild) { + logger.error("activeGuild is undefined"); + return null; + } + + function leaveGuild() { + modalController.push({ + type: "leave_server", + target: activeGuild!, + }); + } + + return ( + + + Server Settings + + + Create Channel + + + Create Channel + + + + Notification Settings + + + Privacy Settings + + + + Leave Guild + + + ); +} + +export default GuildMenuPopout; diff --git a/src/components/modals/LeaveServerModal.tsx b/src/components/modals/LeaveServerModal.tsx index 0778b64..f2af500 100644 --- a/src/components/modals/LeaveServerModal.tsx +++ b/src/components/modals/LeaveServerModal.tsx @@ -1,3 +1,53 @@ -export function LeaveServerModal() { - return null; +import { Routes } from "@spacebarchat/spacebar-api-types/v9"; +import { useState } from "react"; +import { ModalProps, modalController } from "../../controllers/modals"; +import { useAppStore } from "../../stores/AppStore"; +import { Modal } from "./ModalComponents"; + +export function LeaveServerModal({ target, ...props }: ModalProps<"leave_server">) { + const app = useAppStore(); + const [isDisabled, setDisabled] = useState(false); + + async function leaveGuild() { + setDisabled(true); + await app.rest + .delete(Routes.guildMember(target.id, "@me")) + .then(() => { + modalController.pop("close"); + }) + .catch((e) => { + console.error(e); + }) + .finally(() => setDisabled(false)); + } + + return ( + + Are you sure you want to leave {target.name}? You won't be able to rejoin this guild unless + you are re-invited. + + } + actions={[ + { + onClick: leaveGuild, + children: Leave Server, + palette: "danger", + confirmation: true, + disabled: isDisabled, + size: "small", + }, + { + onClick: () => modalController.pop("close"), + children: Cancel, + palette: "link", + disabled: isDisabled, + size: "small", + }, + ]} + /> + ); } diff --git a/src/components/modals/ModalComponents.tsx b/src/components/modals/ModalComponents.tsx index 79749ea..ade391f 100644 --- a/src/components/modals/ModalComponents.tsx +++ b/src/components/modals/ModalComponents.tsx @@ -13,7 +13,7 @@ export type ModalAction = Omit, "as"> & }; interface ModalProps { - children: React.ReactNode; + children?: React.ReactNode; onClose?: (force: boolean) => void; signal?: "close" | "confirm" | "cancel"; title?: string; diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index 5c6e087..4f34d2b 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -10,6 +10,7 @@ import { ErrorModal, JoinServerModal, KickMemberModal, + LeaveServerModal, } from "../../components/modals"; import { Modal } from "./types"; @@ -144,7 +145,7 @@ export const modalController = new ModalControllerExtended({ // clipboard: Clipboard, // leave_group: ConfirmLeave, // close_dm: Confirmation, - // leave_server: ConfirmLeave, + leave_server: LeaveServerModal, // delete_server: Confirmation, // delete_channel: Confirmation, // delete_bot: Confirmation, diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index c713360..c7289a5 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -1,6 +1,7 @@ // adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/types.ts import Channel from "../../stores/objects/Channel"; +import Guild from "../../stores/objects/Guild"; import GuildMember from "../../stores/objects/GuildMember"; import Message from "../../stores/objects/Message"; @@ -37,6 +38,10 @@ export type Modal = { type: "delete_message"; target: Message; } + | { + type: "leave_server"; + target: Guild; + } ); export type ModalProps = Modal & { type: T } & {