mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-25 11:42:30 +01:00
rip out context menus and popouts system
This commit is contained in:
parent
a97932112f
commit
ece2345197
@ -2,13 +2,11 @@ import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { PopoutContext } from "../contexts/PopoutContext";
|
||||
import AccountStore from "../stores/AccountStore";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Presence from "../stores/objects/Presence";
|
||||
import User from "../stores/objects/User";
|
||||
import Container from "./Container";
|
||||
import UserProfilePopout from "./UserProfilePopout";
|
||||
|
||||
const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>`
|
||||
width: ${(props) => props.size}px;
|
||||
@ -49,7 +47,6 @@ interface Props {
|
||||
function Avatar(props: Props) {
|
||||
const app = useAppStore();
|
||||
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const user = props.user ?? app.account;
|
||||
@ -59,16 +56,7 @@ function Avatar(props: Props) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
popoutContext.open({
|
||||
element: <UserProfilePopout user={user} presence={props.presence} />,
|
||||
position: rect,
|
||||
placement: props.popoutPlacement,
|
||||
});
|
||||
// TODO:
|
||||
};
|
||||
|
||||
const clickProp = props.onClick === null ? {} : { onClick: props.onClick ?? openPopout };
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { StackedModalProps, useModals } from "@mattjennings/react-modal-stack";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React, { ComponentType } from "react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { IContextMenuItem } from "./ContextMenuItem";
|
||||
import Icon, { IconProps } from "./Icon";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import LeaveServerModal from "./modals/LeaveServerModal";
|
||||
|
||||
const Wrapper = styled(SectionHeader)`
|
||||
background-color: var(--background-secondary);
|
||||
@ -29,62 +25,12 @@ const HeaderText = styled.header`
|
||||
|
||||
function ChannelHeader() {
|
||||
const app = useAppStore();
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const { openModal } = useModals();
|
||||
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([]);
|
||||
const [icon, setIcon] = React.useState<IconProps["icon"]>("mdiChevronDown");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app.activeGuild && app.activeGuild.ownerId !== app.account?.id) {
|
||||
setContextMenuItems([
|
||||
{
|
||||
label: "Leave Server",
|
||||
color: "var(--danger)",
|
||||
onClick: async () => {
|
||||
openModal(LeaveServerModal as ComponentType<StackedModalProps>, {
|
||||
guild: app.activeGuild,
|
||||
});
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiLocationExit",
|
||||
color: "var(--danger)",
|
||||
},
|
||||
hover: {
|
||||
color: "var(--text)",
|
||||
backgroundColor: "var(--danger)",
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setContextMenuItems([]);
|
||||
}
|
||||
}, [app.activeGuild]);
|
||||
|
||||
function openMenu(e: React.MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (contextMenu.visible) {
|
||||
// "toggles" the menu
|
||||
contextMenu.close();
|
||||
setIcon("mdiChevronDown");
|
||||
return;
|
||||
}
|
||||
|
||||
const horizontalPadding = 5;
|
||||
const verticalPadding = 10;
|
||||
contextMenu.open({
|
||||
position: {
|
||||
x: e.currentTarget.offsetLeft + horizontalPadding, // centers the menu under the header
|
||||
y: e.currentTarget.offsetHeight + horizontalPadding, // add a slight gap between the header and the menu
|
||||
},
|
||||
items: contextMenuItems,
|
||||
style: {
|
||||
width: e.currentTarget.clientWidth - verticalPadding, // adds "margin" to the left and right of the menu
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
});
|
||||
|
||||
setIcon("mdiClose");
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { modalController } from "../../controllers/modals/ModalController";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import { IContextMenuItem } from "../ContextMenuItem";
|
||||
import Icon from "../Icon";
|
||||
import Tooltip from "../Tooltip";
|
||||
|
||||
@ -46,34 +42,6 @@ interface Props {
|
||||
function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { openModal } = useModals();
|
||||
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
||||
{
|
||||
index: 1,
|
||||
label: "Copy Channel ID",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(channel.id);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiIdentifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
label: "Create Channel Invite",
|
||||
onClick: () => {
|
||||
modalController.push({
|
||||
type: "create_invite",
|
||||
target: channel,
|
||||
});
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiAccountPlus",
|
||||
},
|
||||
},
|
||||
]);
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
@ -86,7 +54,6 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||
|
||||
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
||||
}}
|
||||
onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}
|
||||
>
|
||||
<Wrapper
|
||||
isCategory={isCategory}
|
||||
|
@ -1,56 +0,0 @@
|
||||
import React from "react";
|
||||
import { ContextMenuOpenProps } from "../contexts/ContextMenuContext";
|
||||
import Container from "./Container";
|
||||
import ContextMenuItem, { IContextMenuItem } from "./ContextMenuItem";
|
||||
|
||||
interface Props {
|
||||
open: (props: ContextMenuOpenProps) => void;
|
||||
close: () => void;
|
||||
visible: boolean;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
items: IContextMenuItem[];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
function ContextMenu({ position, close, items, style }: Props) {
|
||||
// Close the context menu when the user clicks outside of it
|
||||
React.useEffect(() => {
|
||||
const listener = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
document.addEventListener("click", listener);
|
||||
return () => {
|
||||
document.removeEventListener("click", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container
|
||||
onBlur={close}
|
||||
style={{
|
||||
...style,
|
||||
position: "absolute",
|
||||
minWidth: "10vw",
|
||||
// maxWidth: "20vw",
|
||||
borderRadius: 4,
|
||||
zIndex: 4,
|
||||
padding: "6px 8px",
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
}}
|
||||
>
|
||||
{items
|
||||
.filter((a) => a.visible !== false)
|
||||
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
||||
.map((item, index) => {
|
||||
return <ContextMenuItem key={index} item={item} close={close} index={index} />;
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextMenu;
|
@ -1,84 +0,0 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Container from "./Container";
|
||||
import Icon, { IconProps } from "./Icon";
|
||||
|
||||
export interface IContextMenuItem {
|
||||
index?: number;
|
||||
label: string;
|
||||
color?: string;
|
||||
onClick: React.MouseEventHandler<HTMLDivElement>;
|
||||
iconProps?: IconProps;
|
||||
hover?: {
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
};
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const ContextMenuContainer = styled(Container)`
|
||||
border-radius: 4px;
|
||||
min-height: 32px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
// we handle the hover state ourselves to prevent "lag" with the icon color
|
||||
const Wrapper = styled(Container)<{ hover?: IContextMenuItem["hover"]; hovered?: boolean }>`
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: ${(props) => (props.hovered ? props.hover?.color ?? "var(--text)" : props.color ?? "var(--text)")};
|
||||
background-color: ${(props) => (props.hovered ? props.hover?.backgroundColor ?? "var(--primary)" : "transparent")};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
item: IContextMenuItem;
|
||||
index: number;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
function ContextMenuItem({ item, index, close }: Props) {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<ContextMenuContainer
|
||||
key={index}
|
||||
onClick={async (e) => {
|
||||
await item.onClick(e);
|
||||
close();
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Wrapper hover={item.hover} hovered={isHovered} color={item.color}>
|
||||
<div
|
||||
style={{
|
||||
// color: item.color ?? "var(--text)",
|
||||
fontWeight: 500,
|
||||
fontSize: "14px",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
{item.iconProps && (
|
||||
<Icon
|
||||
{...item.iconProps}
|
||||
size={item.iconProps.size ?? "20px"}
|
||||
color={isHovered ? item.hover?.color ?? "var(--text)" : item.iconProps.color ?? "var(--text)"}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</ContextMenuContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextMenuItem;
|
@ -3,15 +3,12 @@ import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||
import { modalController } from "../controllers/modals/ModalController";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import { Permissions } from "../utils/Permissions";
|
||||
import REST from "../utils/REST";
|
||||
import Container from "./Container";
|
||||
import { IContextMenuItem } from "./ContextMenuItem";
|
||||
import SidebarPill, { PillType } from "./SidebarPill";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
@ -55,40 +52,6 @@ function GuildItem({ guild, active }: Props) {
|
||||
const [pillType, setPillType] = React.useState<PillType>("none");
|
||||
const [isHovered, setHovered] = React.useState(false);
|
||||
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
||||
{
|
||||
index: 1,
|
||||
label: "Copy Guild ID",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(guild.id);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiIdentifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
label: "Create Invite",
|
||||
onClick: () => {
|
||||
// get first channel with view permissions in guild
|
||||
const channel = guild.channels.find((x) => {
|
||||
const permission = Permissions.getPermission(app.account!.id, guild, x);
|
||||
return permission.has("VIEW_CHANNEL") && x.type !== ChannelType.GuildCategory;
|
||||
});
|
||||
if (!channel) return logger.error("No suitable channel found for invite creation");
|
||||
|
||||
modalController.push({
|
||||
type: "create_invite",
|
||||
target: channel,
|
||||
});
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiAccountPlus",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app.activeChannelId && app.activeGuildId === guild.id) return setPillType("active");
|
||||
else if (isHovered) return setPillType("hover");
|
||||
@ -105,7 +68,7 @@ function GuildItem({ guild, active }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<GuildSidebarListItem onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}>
|
||||
<GuildSidebarListItem>
|
||||
<SidebarPill type={pillType} />
|
||||
<Tooltip title={guild.name} placement="right">
|
||||
<Wrapper
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -39,7 +38,6 @@ const Divider = styled.div`
|
||||
function GuildSidebar() {
|
||||
const app = useAppStore();
|
||||
|
||||
const { openModal } = useModals();
|
||||
const navigate = useNavigate();
|
||||
const { all } = app.guilds;
|
||||
const itemCount = all.length + 3; // add the home button, divider, and add server button
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import GuildMember from "../../stores/objects/GuildMember";
|
||||
import ContextMenus from "../../utils/ContextMenus";
|
||||
import Avatar from "../Avatar";
|
||||
import { IContextMenuItem } from "../ContextMenuItem";
|
||||
import UserProfilePopout from "../UserProfilePopout";
|
||||
|
||||
const ListItem = styled.div<{ isCategory?: boolean }>`
|
||||
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
||||
@ -64,28 +58,16 @@ interface Props {
|
||||
|
||||
function MemberListItem({ item }: Props) {
|
||||
const app = useAppStore();
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
||||
...ContextMenus.User(item.user!),
|
||||
...ContextMenus.Member(app.account!, item, item.guild!),
|
||||
]);
|
||||
|
||||
const presence = app.presences.get(item.guild.id)?.get(item.user!.id);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={item.user?.id}
|
||||
onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
popoutContext.open({
|
||||
element: <UserProfilePopout user={item.user!} presence={presence} member={item} />,
|
||||
position: e.currentTarget.getBoundingClientRect(),
|
||||
placement: "right",
|
||||
});
|
||||
// TODO: user popout
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
|
@ -1,145 +0,0 @@
|
||||
import React from "react";
|
||||
import Measure, { BoundingRect, ContentRect } from "react-measure";
|
||||
import { PopoutOpenProps } from "../contexts/PopoutContext";
|
||||
|
||||
const OFFSET = 10;
|
||||
|
||||
function isRectZero(rect: BoundingRect) {
|
||||
return (
|
||||
rect.bottom === 0 &&
|
||||
rect.left === 0 &&
|
||||
rect.right === 0 &&
|
||||
rect.top === 0 &&
|
||||
rect.width === 0 &&
|
||||
rect.height === 0
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: (props: PopoutOpenProps) => void;
|
||||
close: () => void;
|
||||
position: DOMRect;
|
||||
element: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
placement?: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
|
||||
function PopoutRenderer({ position, element, placement, close }: Props) {
|
||||
const [rect, setRect] = React.useState<ContentRect>({});
|
||||
const [positionStyle, setPositionStyle] = React.useState<React.CSSProperties>({
|
||||
visibility: "hidden",
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
document.addEventListener("click", listener);
|
||||
return () => {
|
||||
document.removeEventListener("click", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (rect.bounds && !isRectZero(rect.bounds)) {
|
||||
switch (placement) {
|
||||
default:
|
||||
case "right": {
|
||||
let x = position.left + position.width + OFFSET;
|
||||
let y = position.top;
|
||||
if (x + rect.bounds.width > window.innerWidth) {
|
||||
x = position.left - rect.bounds.width - OFFSET;
|
||||
}
|
||||
if (y + rect.bounds.height > window.innerHeight) {
|
||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
||||
}
|
||||
setPositionStyle({
|
||||
visibility: "visible",
|
||||
top: y,
|
||||
left: x,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "left": {
|
||||
let x = position.left - rect.bounds.width - OFFSET;
|
||||
let y = position.top;
|
||||
if (x < 0) {
|
||||
x = position.left + position.width + OFFSET;
|
||||
}
|
||||
if (y + rect.bounds.height > window.innerHeight) {
|
||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
||||
}
|
||||
setPositionStyle({
|
||||
visibility: "visible",
|
||||
top: y,
|
||||
left: x,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "top": {
|
||||
// center x
|
||||
let x = position.left - rect.bounds.width / 2 + position.width / 2;
|
||||
let y = position.top - rect.bounds.height - OFFSET;
|
||||
if (x + rect.bounds.width > window.innerWidth) {
|
||||
x = window.innerWidth - rect.bounds.width - OFFSET;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = position.top + position.height + OFFSET;
|
||||
}
|
||||
setPositionStyle({
|
||||
visibility: "visible",
|
||||
top: y,
|
||||
left: x,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "bottom": {
|
||||
let x = position.left - position.width / 1;
|
||||
let y = position.top + position.height + OFFSET;
|
||||
if (x + rect.bounds.width > window.innerWidth) {
|
||||
x = window.innerWidth - rect.bounds.width - OFFSET;
|
||||
}
|
||||
if (y + rect.bounds.height > window.innerHeight) {
|
||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
||||
}
|
||||
setPositionStyle({
|
||||
visibility: "visible",
|
||||
top: y,
|
||||
left: x,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [rect, element]);
|
||||
|
||||
const handleResize = (contentRect: ContentRect) => setRect(contentRect);
|
||||
|
||||
return (
|
||||
<div
|
||||
onBlur={close}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 100,
|
||||
...positionStyle,
|
||||
}}
|
||||
>
|
||||
<Measure bounds onResize={handleResize}>
|
||||
{({ measureRef }) => (
|
||||
<div
|
||||
style={{
|
||||
width: "fit-content",
|
||||
height: "fit-content",
|
||||
}}
|
||||
ref={measureRef}
|
||||
>
|
||||
{element}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PopoutRenderer;
|
@ -1,13 +1,11 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { PopoutContext } from "../contexts/PopoutContext";
|
||||
import { modalController } from "../controllers/modals/ModalController";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Avatar from "./Avatar";
|
||||
import Icon from "./Icon";
|
||||
import IconButton from "./IconButton";
|
||||
import Tooltip from "./Tooltip";
|
||||
import UserProfilePopout from "./UserProfilePopout";
|
||||
|
||||
const Section = styled.section`
|
||||
flex: 0 0 auto;
|
||||
@ -71,7 +69,6 @@ const ActionsWrapper = styled.div`
|
||||
|
||||
function UserPanel() {
|
||||
const app = useAppStore();
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const openSettingsModal = () => {
|
||||
@ -85,16 +82,6 @@ function UserPanel() {
|
||||
const openPopout = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
popoutContext.open({
|
||||
element: <UserProfilePopout user={app.account!} />,
|
||||
position: rect,
|
||||
placement: "top",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,11 @@
|
||||
import React, { memo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Role from "../../stores/objects/Role";
|
||||
import User from "../../stores/objects/User";
|
||||
import { hexToRGB, rgbToHsl } from "../../utils/Utils";
|
||||
import UserProfilePopout from "../UserProfilePopout";
|
||||
|
||||
const Container = styled.span<{ color?: string; withHover?: boolean }>`
|
||||
padding: 0 2px;
|
||||
@ -29,22 +27,12 @@ interface MentionProps {
|
||||
}
|
||||
function UserMention({ id }: MentionProps) {
|
||||
const app = useAppStore();
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const click = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!user || !ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
|
||||
popoutContext.open({
|
||||
element: <UserProfilePopout user={user} />,
|
||||
position: rect,
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React, { memo } from "react";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { memo } from "react";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { MessageLike } from "../../stores/objects/Message";
|
||||
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
||||
import ContextMenus from "../../utils/ContextMenus";
|
||||
import Avatar from "../Avatar";
|
||||
import { IContextMenuItem } from "../ContextMenuItem";
|
||||
import Markdown from "../markdown/MarkdownRenderer";
|
||||
import MessageAttachment from "./MessageAttachment";
|
||||
import MessageAuthor from "./MessageAuthor";
|
||||
@ -21,10 +18,6 @@ interface Props {
|
||||
|
||||
function Message({ message, header }: Props) {
|
||||
const app = useAppStore();
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
||||
...ContextMenus.Message(app, message, app.account),
|
||||
]);
|
||||
|
||||
const guild = message.guild_id ? app.guilds.get(message.guild_id) : undefined;
|
||||
const isEveryoneMentioned = "mention_everyone" in message && message.mention_everyone;
|
||||
@ -35,11 +28,7 @@ function Message({ message, header }: Props) {
|
||||
message.mention_roles.some((role) => guild.members.me?.roles.some((role) => role.id === role.id));
|
||||
|
||||
return (
|
||||
<MessageBase
|
||||
header={header}
|
||||
onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}
|
||||
mention={isEveryoneMentioned || isUserMentioned || isRoleMentioned}
|
||||
>
|
||||
<MessageBase header={header} mention={isEveryoneMentioned || isUserMentioned || isRoleMentioned}>
|
||||
<MessageInfo>
|
||||
{header ? (
|
||||
<Avatar key={message.author.id} user={message.author} size={40} />
|
||||
|
@ -1,17 +1,11 @@
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import ContextMenus from "../../utils/ContextMenus";
|
||||
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
|
||||
import { getFileDetails, zoomFit } from "../../utils/Utils";
|
||||
import { IContextMenuItem } from "../ContextMenuItem";
|
||||
import Audio from "../media/Audio";
|
||||
import File from "../media/File";
|
||||
import Video from "../media/Video";
|
||||
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";
|
||||
|
||||
const Attachment = styled.div<{ withPointer?: boolean }>`
|
||||
cursor: ${(props) => (props.withPointer ? "pointer" : "default")};
|
||||
@ -25,17 +19,13 @@ const Image = styled.img`
|
||||
|
||||
interface AttachmentProps {
|
||||
attachment: APIAttachment;
|
||||
contextMenuItems?: IContextMenuItem[];
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export default function MessageAttachment({ attachment, contextMenuItems, maxWidth, maxHeight }: AttachmentProps) {
|
||||
export default function MessageAttachment({ attachment, maxWidth, maxHeight }: AttachmentProps) {
|
||||
const logger = useLogger("MessageAttachment");
|
||||
|
||||
const { openModal } = useModals();
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
|
||||
const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url;
|
||||
|
||||
const details = getFileDetails(attachment);
|
||||
@ -64,13 +54,10 @@ export default function MessageAttachment({ attachment, contextMenuItems, maxWid
|
||||
<Attachment
|
||||
withPointer={attachment.content_type?.startsWith("image")}
|
||||
key={attachment.id}
|
||||
onContextMenu={(e) =>
|
||||
contextMenu.open2(e, [...(contextMenuItems ?? []), ...ContextMenus.MessageAttachment(attachment)])
|
||||
}
|
||||
onClick={() => {
|
||||
if (!attachment.content_type?.startsWith("image")) return;
|
||||
const { width, height } = zoomFit(attachment.width!, attachment.height!);
|
||||
openModal(AttachmentPreviewModal, { attachment, width, height });
|
||||
// TODO: preview modal
|
||||
}}
|
||||
>
|
||||
{finalElement}
|
||||
|
@ -1,12 +1,8 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { MessageLike } from "../../stores/objects/Message";
|
||||
import ContextMenus from "../../utils/ContextMenus";
|
||||
import UserProfilePopout from "../UserProfilePopout";
|
||||
|
||||
const Container = styled.div`
|
||||
font-size: 16px;
|
||||
@ -25,8 +21,6 @@ interface Props {
|
||||
|
||||
function MessageAuthor({ message }: Props) {
|
||||
const app = useAppStore();
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
const [color, setColor] = React.useState<string | undefined>(undefined);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -44,16 +38,7 @@ function MessageAuthor({ message }: Props) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
popoutContext.open({
|
||||
element: <UserProfilePopout user={message.author} />,
|
||||
position: rect,
|
||||
placement: "right",
|
||||
});
|
||||
// TODO: user popout
|
||||
};
|
||||
|
||||
return (
|
||||
@ -62,14 +47,7 @@ function MessageAuthor({ message }: Props) {
|
||||
style={{
|
||||
color,
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contextMenu.open2(e, [
|
||||
...ContextMenus.User(message.author),
|
||||
...(message.guild_id ? ContextMenus.Member2(app, message.author, message.guild_id) : []),
|
||||
]);
|
||||
}}
|
||||
|
||||
onClick={openPopout}
|
||||
>
|
||||
{message.author.username}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { ChannelType, MessageType, RESTPostAPIChannelMessageJSONBody } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
@ -60,7 +59,6 @@ function MessageInput({ channel }: Props) {
|
||||
const logger = useLogger("MessageInput");
|
||||
const [content, setContent] = React.useState("");
|
||||
const [attachments, setAttachments] = React.useState<File[]>([]);
|
||||
const { openModal } = useModals();
|
||||
|
||||
/**
|
||||
* Debounced stopTyping
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -27,7 +26,6 @@ type FormValues = {
|
||||
|
||||
function JoinServerModal({ ...props }: ModalProps<"join_server">) {
|
||||
const logger = useLogger("JoinServerModal");
|
||||
const { openModal, closeAllModals } = useModals();
|
||||
const app = useAppStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -46,7 +44,8 @@ function JoinServerModal({ ...props }: ModalProps<"join_server">) {
|
||||
.post<never, { guild_id: string; channel_id: string }>(Routes.invite(code))
|
||||
.then((r) => {
|
||||
navigate(`/channels/${r.guild_id}/${r.channel_id}`);
|
||||
closeAllModals();
|
||||
// modalController.closeAll();
|
||||
// TODO:
|
||||
})
|
||||
.catch((r) => {
|
||||
if ("message" in r) {
|
||||
|
@ -1,47 +0,0 @@
|
||||
import React from "react";
|
||||
import { IContextMenuItem } from "../components/ContextMenuItem";
|
||||
|
||||
export interface ContextMenuOpenProps {
|
||||
position: { x: number; y: number };
|
||||
items: { label: string; onClick: React.MouseEventHandler<HTMLDivElement> }[];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const useValue = () => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
||||
const [items, setItems] = React.useState<IContextMenuItem[]>([]);
|
||||
const [style, setStyle] = React.useState<ContextMenuOpenProps["style"]>({});
|
||||
|
||||
const open = (props: ContextMenuOpenProps) => {
|
||||
setPosition(props.position);
|
||||
setItems(props.items);
|
||||
setStyle(props.style);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const open2 = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, items: IContextMenuItem[]) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPosition({ x: e.pageX, y: e.pageY });
|
||||
setItems(items);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
open2,
|
||||
close: () => setVisible(false),
|
||||
visible,
|
||||
position,
|
||||
items,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export const ContextMenuContext = React.createContext({} as ReturnType<typeof useValue>);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const ContextMenuContextProvider: React.FC<any> = (props) => {
|
||||
return <ContextMenuContext.Provider value={useValue()}>{props.children}</ContextMenuContext.Provider>;
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export interface PopoutOpenProps {
|
||||
position: DOMRect;
|
||||
element: React.ReactNode;
|
||||
placement?: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
|
||||
const useValue = () => {
|
||||
const [position, setPosition] = React.useState<DOMRect>(new DOMRect(0, 0, 0, 0));
|
||||
const [element, setElement] = React.useState<React.ReactNode>();
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [placement, setPlacement] = React.useState<"left" | "right" | "top" | "bottom">();
|
||||
|
||||
const close = () => {
|
||||
setIsOpen(false);
|
||||
setElement(undefined);
|
||||
};
|
||||
|
||||
const open = (props: PopoutOpenProps) => {
|
||||
// clicking again on the same trigger should close it
|
||||
if (isOpen && JSON.stringify(position) === JSON.stringify(props.position)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setPosition(props.position);
|
||||
setElement(props.element);
|
||||
setIsOpen(true);
|
||||
setPlacement(props.placement ?? "right");
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
position,
|
||||
element,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
placement,
|
||||
};
|
||||
};
|
||||
|
||||
export const PopoutContext = React.createContext({} as ReturnType<typeof useValue>);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const PopoutContextProvider: React.FC<any> = (props) => {
|
||||
return <PopoutContext.Provider value={useValue()}>{props.children}</PopoutContext.Provider>;
|
||||
};
|
@ -19,8 +19,6 @@ import { BrowserRouter } from "react-router-dom";
|
||||
import { ErrorBoundaryContext } from "react-use-error-boundary";
|
||||
import App from "./App";
|
||||
import { BannerContextProvider } from "./contexts/BannerContext";
|
||||
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
|
||||
import { PopoutContextProvider } from "./contexts/PopoutContext";
|
||||
import Theme from "./contexts/Theme";
|
||||
import ModalRenderer from "./controllers/modals/ModalRenderer";
|
||||
import "./index.css";
|
||||
@ -32,14 +30,10 @@ dayjs.extend(calendar, calendarStrings);
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<ErrorBoundaryContext>
|
||||
<BrowserRouter>
|
||||
<PopoutContextProvider>
|
||||
<ContextMenuContextProvider>
|
||||
<BannerContextProvider>
|
||||
<App />
|
||||
<ModalRenderer />
|
||||
</BannerContextProvider>
|
||||
</ContextMenuContextProvider>
|
||||
</PopoutContextProvider>
|
||||
<BannerContextProvider>
|
||||
<App />
|
||||
<ModalRenderer />
|
||||
</BannerContextProvider>
|
||||
<Theme />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundaryContext>,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import HCaptchaLib from "@hcaptcha/react-hcaptcha";
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -24,7 +23,6 @@ import {
|
||||
} from "../components/AuthComponents";
|
||||
import { TextDivider } from "../components/Divider";
|
||||
import HCaptcha, { HeaderContainer } from "../components/HCaptcha";
|
||||
import ForgotPasswordModal from "../components/modals/ForgotPasswordModal";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { AUTH_NO_BRANDING, useAppStore } from "../stores/AppStore";
|
||||
import { Globals } from "../utils/Globals";
|
||||
@ -56,7 +54,6 @@ function LoginPage() {
|
||||
const captchaRef = React.useRef<HCaptchaLib>(null);
|
||||
const [debounce, setDebounce] = React.useState<NodeJS.Timeout | null>(null);
|
||||
const [isCheckingInstance, setCheckingInstance] = React.useState(false);
|
||||
const { openModal } = useModals();
|
||||
|
||||
const {
|
||||
register,
|
||||
@ -208,7 +205,7 @@ function LoginPage() {
|
||||
};
|
||||
|
||||
const forgotPassword = () => {
|
||||
openModal(ForgotPasswordModal);
|
||||
// TODO: forgot password modal
|
||||
};
|
||||
|
||||
if (captchaSiteKey) {
|
||||
|
@ -5,14 +5,9 @@ import styled from "styled-components";
|
||||
import Banner from "../../components/Banner";
|
||||
import ChannelSidebar from "../../components/ChannelSidebar";
|
||||
import ContainerComponent from "../../components/Container";
|
||||
import ContextMenu from "../../components/ContextMenu";
|
||||
import ErrorBoundary from "../../components/ErrorBoundary";
|
||||
import GuildSidebar from "../../components/GuildSidebar";
|
||||
import PopoutRenderer from "../../components/PopoutRenderer";
|
||||
import Chat from "../../components/messaging/Chat";
|
||||
import { BannerContext } from "../../contexts/BannerContext";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
|
||||
const Container = styled(ContainerComponent)`
|
||||
@ -29,9 +24,6 @@ const Wrapper = styled.div`
|
||||
|
||||
function ChannelPage() {
|
||||
const app = useAppStore();
|
||||
const contextMenuContext = React.useContext(ContextMenuContext);
|
||||
const popoutContext = React.useContext(PopoutContext);
|
||||
const bannerContext = React.useContext(BannerContext);
|
||||
|
||||
const { guildId, channelId } = useParams<{ guildId: string; channelId: string }>();
|
||||
|
||||
@ -44,8 +36,6 @@ function ChannelPage() {
|
||||
<Container>
|
||||
<Banner />
|
||||
<Wrapper>
|
||||
{contextMenuContext.visible && <ContextMenu {...contextMenuContext} />}
|
||||
{popoutContext.element && <PopoutRenderer {...popoutContext} />}
|
||||
<GuildSidebar />
|
||||
<ChannelSidebar />
|
||||
<ErrorBoundary section="component">
|
||||
|
@ -1,167 +0,0 @@
|
||||
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { IContextMenuItem } from "../components/ContextMenuItem";
|
||||
import AccountStore from "../stores/AccountStore";
|
||||
import AppStore from "../stores/AppStore";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import GuildMember from "../stores/objects/GuildMember";
|
||||
import { MessageLike } from "../stores/objects/Message";
|
||||
import User from "../stores/objects/User";
|
||||
import { Permissions } from "./Permissions";
|
||||
|
||||
export default {
|
||||
User: (user: User | AccountStore): IContextMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
label: "Copy User ID",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(user.id);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiIdentifier",
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
Message: (app: AppStore, message: MessageLike, account: AccountStore | null): IContextMenuItem[] => {
|
||||
const channel = app.channels.get(message.channel_id);
|
||||
const permissions = Permissions.getPermission(account?.id, channel?.guild, channel);
|
||||
const canDeleteMessage = permissions.has("MANAGE_MESSAGES") || message.author.id === account?.id;
|
||||
|
||||
const items: IContextMenuItem[] = [
|
||||
{
|
||||
label: "Copy Message ID",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(message.id);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiIdentifier",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Copy Raw Text",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(message.content);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiRaw",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (canDeleteMessage) {
|
||||
items.push({
|
||||
label: "Delete Message",
|
||||
onClick: () => {
|
||||
message.delete();
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiTrashCanOutline",
|
||||
color: "red",
|
||||
},
|
||||
color: "red",
|
||||
hover: {
|
||||
backgroundColor: "red",
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
MessageAttachment: (attachment: APIAttachment): IContextMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
label: "Copy Attachment URL",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(attachment.url);
|
||||
},
|
||||
iconProps: {
|
||||
icon: "mdiLink",
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
// TODO: check if target has higher role
|
||||
Member: (me: AccountStore, them: GuildMember, guild?: Guild): IContextMenuItem[] => {
|
||||
const permissions = Permissions.getPermission(me.id, guild);
|
||||
|
||||
const items: IContextMenuItem[] = [];
|
||||
|
||||
// if (permissions.has("KICK_MEMBERS")) {
|
||||
// items.push({
|
||||
// label: `Kick ${them.user!.username}`,
|
||||
// onClick: () => {
|
||||
// // openModal(KickModal, {
|
||||
// // member: them,
|
||||
// // });
|
||||
// },
|
||||
// color: "red",
|
||||
// hover: {
|
||||
// backgroundColor: "red",
|
||||
// color: "white",
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (permissions.has("BAN_MEMBERS")) {
|
||||
// items.push({
|
||||
// label: `Ban ${them.user!.username}`,
|
||||
// onClick: () => {
|
||||
// // member.kick()
|
||||
// console.log("ban member");
|
||||
// },
|
||||
// color: "red",
|
||||
// hover: {
|
||||
// backgroundColor: "red",
|
||||
// color: "white",
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
return items;
|
||||
},
|
||||
// TODO: check if target has higher role
|
||||
Member2: (app: AppStore, them: User, guildId: string): IContextMenuItem[] => {
|
||||
const me = app.account!;
|
||||
const guild = app.guilds.get(guildId);
|
||||
if (!guild) return [];
|
||||
const member = guild.members.get(them.id);
|
||||
if (!member) return [];
|
||||
const permissions = Permissions.getPermission(me.id, guild);
|
||||
|
||||
const items: IContextMenuItem[] = [];
|
||||
|
||||
if (permissions.has("KICK_MEMBERS")) {
|
||||
items.push({
|
||||
label: `Kick ${them.username}`,
|
||||
onClick: () => {
|
||||
// openModal(KickModal, {
|
||||
// member,
|
||||
// });
|
||||
},
|
||||
color: "red",
|
||||
hover: {
|
||||
backgroundColor: "red",
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (permissions.has("BAN_MEMBERS")) {
|
||||
items.push({
|
||||
label: `Ban ${them.username}`,
|
||||
onClick: () => {
|
||||
// member.kick()
|
||||
console.log("ban member");
|
||||
},
|
||||
color: "red",
|
||||
hover: {
|
||||
backgroundColor: "red",
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user