1
0
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:
Puyodead1 2023-12-11 15:28:21 -05:00
parent a97932112f
commit ece2345197
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
22 changed files with 17 additions and 813 deletions

View File

@ -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 };

View File

@ -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");
}

View File

@ -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}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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 (

View File

@ -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(() => {

View File

@ -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} />

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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) {

View File

@ -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>;
};

View File

@ -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>;
};

View File

@ -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>,

View File

@ -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) {

View File

@ -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">

View File

@ -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;
},
};