diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index adf8828..5433fc7 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -8,9 +8,7 @@ import Presence from "../stores/objects/Presence"; import User from "../stores/objects/User"; import Container from "./Container"; import Floating from "./floating/Floating"; -import FloatingContent from "./floating/FloatingContent"; import FloatingTrigger from "./floating/FloatingTrigger"; -import UserProfilePopout from "./floating/UserProfilePopout"; const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>` width: ${(props) => props.size}px; @@ -52,6 +50,7 @@ interface Props { width?: number; height?: number; }; + showPresence?: boolean; } function Avatar(props: Props) { @@ -65,7 +64,13 @@ function Avatar(props: Props) { const Base = props.onClick ? Yes(props.onClick) : FloatingTrigger; return ( - + - {props.presence && props.presence.status !== PresenceUpdateStatus.Offline && ( - + {props.showPresence && ( + )} - - - - ); } diff --git a/src/components/ChannelList/ChannelListItem.tsx b/src/components/ChannelList/ChannelListItem.tsx index 5f90a9f..78fee98 100644 --- a/src/components/ChannelList/ChannelListItem.tsx +++ b/src/components/ChannelList/ChannelListItem.tsx @@ -3,7 +3,8 @@ import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import Channel from "../../stores/objects/Channel"; import Icon from "../Icon"; -import Tooltip from "../Tooltip"; +import Floating from "../floating/Floating"; +import FloatingTrigger from "../floating/FloatingTrigger"; const ListItem = styled.div<{ isCategory?: boolean }>` padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")}; @@ -93,18 +94,27 @@ function ChannelListItem({ channel, isCategory, active }: Props) { {isCategory && ( - - - - - + Create Channel, + }} + > + + + + + + )} diff --git a/src/components/Codeblock.tsx b/src/components/Codeblock.tsx index 29ec9ca..b5cb703 100644 --- a/src/components/Codeblock.tsx +++ b/src/components/Codeblock.tsx @@ -3,7 +3,8 @@ import React from "react"; import styled from "styled-components"; -import Tooltip from "./Tooltip"; +import Floating from "./floating/Floating"; +import FloatingTrigger from "./floating/FloatingTrigger"; const Actions = styled.div` position: absolute; @@ -57,9 +58,17 @@ function CodeBlock(props: Props) { }} > - - {text} - + "Copy to Clipboard, + }} + > + + {text} + + {props.children} diff --git a/src/components/GuildItem.tsx b/src/components/GuildItem.tsx index bf02e5b..2e46712 100644 --- a/src/components/GuildItem.tsx +++ b/src/components/GuildItem.tsx @@ -10,7 +10,8 @@ import { Permissions } from "../utils/Permissions"; import REST from "../utils/REST"; import Container from "./Container"; import SidebarPill, { PillType } from "./SidebarPill"; -import Tooltip from "./Tooltip"; +import Floating from "./floating/Floating"; +import FloatingTrigger from "./floating/FloatingTrigger"; export const GuildSidebarListItem = styled.div` position: relative; @@ -70,34 +71,43 @@ function GuildItem({ guild, active }: Props) { return ( - - setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - {guild.icon ? ( - - ) : ( - - {guild?.acronym} - - )} - - + {guild.name}, + }} + > + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {guild.icon ? ( + + ) : ( + + {guild?.acronym} + + )} + + + ); } diff --git a/src/components/MemberList/MemberListItem.tsx b/src/components/MemberList/MemberListItem.tsx index d37cab6..4589870 100644 --- a/src/components/MemberList/MemberListItem.tsx +++ b/src/components/MemberList/MemberListItem.tsx @@ -2,12 +2,9 @@ import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9"; import styled from "styled-components"; import { useAppStore } from "../../stores/AppStore"; import GuildMember from "../../stores/objects/GuildMember"; -import User from "../../stores/objects/User"; import Avatar from "../Avatar"; import Floating from "../floating/Floating"; -import FloatingContent from "../floating/FloatingContent"; import FloatingTrigger from "../floating/FloatingTrigger"; -import UserProfilePopout from "../floating/UserProfilePopout"; const ListItem = styled(FloatingTrigger)<{ isCategory?: boolean }>` padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")}; @@ -64,15 +61,23 @@ interface Props { function MemberListItem({ item }: Props) { const app = useAppStore(); - const presence = app.presences.get(item.guild.id)?.get(item.user!.id); + const presence = app.presences.get(item.user!.id); return ( - + - + {item.nick ?? item.user?.username} @@ -80,10 +85,6 @@ function MemberListItem({ item }: Props) { - - - - ); } diff --git a/src/components/SidebarAction.tsx b/src/components/SidebarAction.tsx index a1402db..95d084f 100644 --- a/src/components/SidebarAction.tsx +++ b/src/components/SidebarAction.tsx @@ -4,7 +4,8 @@ import Container from "./Container"; import { GuildSidebarListItem } from "./GuildItem"; import Icon, { IconProps } from "./Icon"; import SidebarPill, { PillType } from "./SidebarPill"; -import Tooltip from "./Tooltip"; +import Floating from "./floating/Floating"; +import FloatingTrigger from "./floating/FloatingTrigger"; const Wrapper = styled(Container)<{ margin?: boolean; @@ -60,25 +61,34 @@ function SidebarAction(props: Props) { return ( - - setHovered(true)} - onMouseLeave={() => setHovered(false)} - margin={props.margin} - active={props.active} - useGreenColorScheme={props.useGreenColorScheme} - > - {props.image && } - {props.icon && ( - - )} - {props.label && {props.label}} - - + {props.tooltip}, + }} + > + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + margin={props.margin} + active={props.active} + useGreenColorScheme={props.useGreenColorScheme} + > + {props.image && } + {props.icon && ( + + )} + {props.label && {props.label}} + + + ); } diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index 366393c..400279d 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -1,21 +1,24 @@ -import MuiTooltip, { TooltipProps as MuiTooltipProps, tooltipClasses } from "@mui/material/Tooltip"; import styled from "styled-components"; +import { FloatingProps } from "./floating/Floating"; -export default styled(({ className, ...props }: MuiTooltipProps) => ( - -))(() => ({ - [`& .${tooltipClasses.popper}`]: { - maxWidth: 200, - borderRadius: 5, - }, - [`& .${tooltipClasses.arrow}`]: { - color: "var(--background-tertiary)", - }, - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: "var(--background-tertiary)", - fontSize: "14px", - padding: "8px 12px", - overflow: "hidden", - textOverflow: "ellipsis", - }, -})); +const Container = styled.div` + background-color: var(--background-tertiary); + line-height: 16px; + box-sizing: border-box; + font-size: 14px; + padding: 8px 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 250px; + border-radius: 4px; + color: var(--text); + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); +`; + +function Tooltip(props: FloatingProps<"tooltip">) { + if (!props) return null; + return {props.content}; +} + +export default Tooltip; diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx index 9f392da..ff31ed3 100644 --- a/src/components/UserPanel.tsx +++ b/src/components/UserPanel.tsx @@ -4,11 +4,8 @@ import User from "../stores/objects/User"; import Avatar from "./Avatar"; import Icon from "./Icon"; import IconButton from "./IconButton"; -import Tooltip from "./Tooltip"; import Floating from "./floating/Floating"; -import FloatingContent from "./floating/FloatingContent"; import FloatingTrigger from "./floating/FloatingTrigger"; -import UserProfilePopout from "./floating/UserProfilePopout"; const Section = styled.section` flex: 0 0 auto; @@ -76,7 +73,13 @@ function UserPanel() { const openSettingsModal = () => {}; return ( - +
@@ -88,18 +91,23 @@ function UserPanel() { - - - - - + Settings, + }} + > + + + + + +
- - - -
); } diff --git a/src/components/floating/Floating.tsx b/src/components/floating/Floating.tsx index 466b857..4b171c6 100644 --- a/src/components/floating/Floating.tsx +++ b/src/components/floating/Floating.tsx @@ -1,14 +1,86 @@ +import { FloatingArrow, FloatingPortal, Placement } from "@floating-ui/react"; +import { motion } from "framer-motion"; import { FloatingContext } from "../../contexts/FloatingContext"; -import useFloating, { FloatingOptions } from "../../hooks/useFloating"; +import useFloating from "../../hooks/useFloating"; +import GuildMember from "../../stores/objects/GuildMember"; +import User from "../../stores/objects/User"; +import Tooltip from "../Tooltip"; +import UserProfilePopout from "./UserProfilePopout"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Components = Record>; + +const components: Components = { + userPopout: UserProfilePopout, + tooltip: Tooltip, +}; + +export type FloatingOptions = { + initialOpen?: boolean; + placement?: Placement; + offset?: number; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} & ( + | { + type: "userPopout"; + props: { + user: User; + member?: GuildMember; + }; + } + | { + type: "tooltip"; + props: { + content: JSX.Element; + aria?: string; + }; + } +); + +export type FloatingProps = (FloatingOptions & { + type: T; +})["props"]; function Floating({ + type, children, + props, ...restOptions }: { children: React.ReactNode; } & FloatingOptions) { - const floating = useFloating({ ...restOptions }); - return {children}; + const floating = useFloating({ type, ...restOptions }); + + const Component = components[type]; + + return ( + + {children} + {Component && floating.open && ( + + + + {type === "tooltip" && ( + + )} + + + )} + + ); } export default Floating; diff --git a/src/components/floating/UserProfilePopout.tsx b/src/components/floating/UserProfilePopout.tsx index 474f8d3..bef1126 100644 --- a/src/components/floating/UserProfilePopout.tsx +++ b/src/components/floating/UserProfilePopout.tsx @@ -9,7 +9,10 @@ import { HorizontalDivider } from "../Divider"; import { CDNRoutes, ImageFormat } from "@spacebarchat/spacebar-api-types/v9"; import dayjs from "dayjs"; import { ReactComponent as SpacebarLogoBlue } from "../../assets/images/logo/Spacebar_Icon.svg"; +import { useAppStore } from "../../stores/AppStore"; import REST from "../../utils/REST"; +import Floating from "./Floating"; +import FloatingTrigger from "./FloatingTrigger"; const Container = styled.div` background-color: #252525; @@ -158,10 +161,12 @@ interface Props { } function UserProfilePopout({ user, member }: Props) { + const app = useAppStore(); const logger = useLogger("UserProfilePopout"); const id = user.id; const { timestamp: createdAt } = Snowflake.deconstruct(id); + const presence = app.presences.get(user.id); return ( @@ -176,11 +181,12 @@ function UserProfilePopout({ user, member }: Props) { logger.debug("open profile modal"); }} user={user} - // presence={presence} + presence={presence} statusDotStyle={{ width: 16, height: 16, }} + showPresence /> @@ -204,11 +210,19 @@ function UserProfilePopout({ user, member }: Props) {
Member Since - {/* */} -
- -
- {/*
*/} + Spacebar, + }} + > + +
+ +
+
+
{dayjs(createdAt).format("MMM D, YYYY")} {member && ( <> @@ -221,25 +235,37 @@ function UserProfilePopout({ user, member }: Props) { }} /> - {/* */} - {member.guild.icon ? ( - {member.guild.name}, + }} + > + + {member.guild.icon ? ( + + ) : ( + + {member.guild.acronym} + )} - width={16} - height={16} - loading="lazy" - style={{ - borderRadius: "50%", - }} - /> - ) : ( - - {member.guild.acronym} - - )} - {/* */} + + {dayjs(member.joined_at).format("MMM D, YYYY")} )} @@ -250,8 +276,8 @@ function UserProfilePopout({ user, member }: Props) {
{member.roles.length ? "Roles" : "No Roles"} - {member.roles.map((x) => ( - + {member.roles.map((x, i) => ( + {x.name} diff --git a/src/components/markdown/Timestamp.tsx b/src/components/markdown/Timestamp.tsx index 0686dd1..7295c8b 100644 --- a/src/components/markdown/Timestamp.tsx +++ b/src/components/markdown/Timestamp.tsx @@ -1,7 +1,8 @@ import dayjs from "dayjs"; import { memo } from "react"; import styled from "styled-components"; -import Tooltip from "../Tooltip"; +import Floating from "../floating/Floating"; +import FloatingTrigger from "../floating/FloatingTrigger"; const Container = styled.div` background-color: hsl(var(--background-tertiary-hsl) / 0.3); @@ -43,9 +44,17 @@ function Timestamp({ timestamp, style }: Props) { return ( - - {value} - + {date.format("dddd, MMMM MM, h:mm A")}, + }} + > + + {value} + + ); } diff --git a/src/components/messaging/ChatHeader.tsx b/src/components/messaging/ChatHeader.tsx index 48a95d9..b76c82f 100644 --- a/src/components/messaging/ChatHeader.tsx +++ b/src/components/messaging/ChatHeader.tsx @@ -6,7 +6,8 @@ import { useAppStore } from "../../stores/AppStore"; import Channel from "../../stores/objects/Channel"; import Icon from "../Icon"; import { SectionHeader } from "../SectionHeader"; -import Tooltip from "../Tooltip"; +import Floating from "../floating/Floating"; +import FloatingTrigger from "../floating/FloatingTrigger"; const IconButton = styled.button` margin: 0; @@ -120,13 +121,21 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro const logger = useLogger("ChatHeader.tsx:ActionItem"); return ( - - - - - - - + {tooltip}, + }} + > + + + + + + + + ); } diff --git a/src/components/messaging/MessageAuthor.tsx b/src/components/messaging/MessageAuthor.tsx index df883a6..23f805c 100644 --- a/src/components/messaging/MessageAuthor.tsx +++ b/src/components/messaging/MessageAuthor.tsx @@ -4,9 +4,7 @@ import styled from "styled-components"; import { useAppStore } from "../../stores/AppStore"; import { MessageLike } from "../../stores/objects/Message"; import Floating from "../floating/Floating"; -import FloatingContent from "../floating/FloatingContent"; import FloatingTrigger from "../floating/FloatingTrigger"; -import UserProfilePopout from "../floating/UserProfilePopout"; const Container = styled.div` font-size: 16px; @@ -39,7 +37,13 @@ function MessageAuthor({ message }: Props) { }, [message]); return ( - + - - - ); } diff --git a/src/components/messaging/MessageBase.tsx b/src/components/messaging/MessageBase.tsx index 979575c..7431a40 100644 --- a/src/components/messaging/MessageBase.tsx +++ b/src/components/messaging/MessageBase.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import styled from "styled-components"; import Message, { MessageLike } from "../../stores/objects/Message"; import { calendarStrings } from "../../utils/i18n"; -import Tooltip from "../Tooltip"; +import Floating from "../floating/Floating"; +import FloatingTrigger from "../floating/FloatingTrigger"; interface Props { header?: boolean; @@ -96,18 +97,33 @@ export const MessageDetails = observer(({ message, position }: { message: Messag if (message instanceof Message && message.edited_timestamp) { return (
- - - + {dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")}, + }} + > + + + + - {dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")} + ), + }} > - (edited) - + + (edited) + +
); @@ -121,15 +137,31 @@ export const MessageDetails = observer(({ message, position }: { message: Messag return ( - - - + {dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")}, + }} + > + + + + {message instanceof Message && message.edited_timestamp && ( - - (edited) - + {dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")}, + }} + > + + (edited) + + )} ); diff --git a/src/hooks/useFloating.tsx b/src/hooks/useFloating.tsx index 980c0a1..0340cd0 100644 --- a/src/hooks/useFloating.tsx +++ b/src/hooks/useFloating.tsx @@ -1,5 +1,5 @@ import { - Placement, + arrow, autoUpdate, flip, offset, @@ -7,27 +7,24 @@ import { useClick, useDismiss, useFloating, + useFocus, + useHover, useInteractions, useRole, } from "@floating-ui/react"; -import { useMemo, useState } from "react"; - -export interface FloatingOptions { - initialOpen?: boolean; - placement?: Placement; - open?: boolean; - onOpenChange?: (open: boolean) => void; -} +import { useMemo, useRef, useState } from "react"; +import { FloatingOptions } from "../components/floating/Floating"; export default function ({ + type, initialOpen = false, + offset: offsetMiddlewareOffset, placement, open: controlledOpen, onOpenChange: setControlledOpen, -}: // config, -FloatingOptions) { +}: Omit) { const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen); - // const [key, setKey] = useState(); + const arrowRef = useRef(null); const open = controlledOpen ?? uncontrolledOpen; const setOpen = setControlledOpen ?? setUncontrolledOpen; @@ -37,32 +34,38 @@ FloatingOptions) { open, onOpenChange: setOpen, whileElementsMounted: autoUpdate, - middleware: [offset(5), flip(), shift()], + middleware: [ + offset(type === "tooltip" && !offsetMiddlewareOffset ? 10 : offsetMiddlewareOffset ?? 5), + flip(), + shift({ + padding: 8, + }), + arrow({ + element: arrowRef, + padding: 4, + }), + ], }); const context = data.context; - const click = useClick(context); + const click = useClick(context, { + enabled: type !== "tooltip", + }); const dismiss = useDismiss(context); - const role = useRole(context); - const interactions = useInteractions([click, dismiss, role]); + const role = useRole(context, { + role: type === "tooltip" ? "tooltip" : undefined, + }); - // useEffect(() => { - // if (open) { - // const k = floatingController.add({ - // type, - // data: { - // ...interactions, - // ...data, - // }, - // open, - // props: config, - // }); - // setKey(k); - // } else { - // key && floatingController.remove(key); - // } - // }, [open]); + const hover = useHover(context, { + move: false, + enabled: type == "tooltip", + }); + const focus = useFocus(context, { + enabled: type == "tooltip", + }); + + const interactions = useInteractions([click, dismiss, role, hover, focus]); return useMemo( () => ({ @@ -70,6 +73,7 @@ FloatingOptions) { setOpen, ...interactions, ...data, + arrowRef, }), [open, setOpen, interactions, data], ); diff --git a/src/stores/PresenceStore.ts b/src/stores/PresenceStore.ts index 9dd378f..b563255 100644 --- a/src/stores/PresenceStore.ts +++ b/src/stores/PresenceStore.ts @@ -1,11 +1,11 @@ import type { GatewayPresenceUpdateDispatchData, Snowflake } from "@spacebarchat/spacebar-api-types/v9"; -import { ObservableMap, action, computed, makeObservable, observable } from "mobx"; +import { action, computed, makeObservable, observable } from "mobx"; import AppStore from "./AppStore"; import Presence from "./objects/Presence"; export default class PresenceStore { private readonly app: AppStore; - @observable presences = observable.map>(); + @observable presences = observable.map(); constructor(app: AppStore) { this.app = app; @@ -15,11 +15,11 @@ export default class PresenceStore { @action add(data: GatewayPresenceUpdateDispatchData) { - if (!this.presences.has(data.guild_id)) { - this.presences.set(data.guild_id, observable.map()); + if (!this.presences.has(data.user.id)) { + this.presences.set(data.user.id, new Presence(this.app, data)); + } else { + this.update(data); } - - this.presences.get(data.guild_id)?.set(data.user.id, new Presence(this.app, data)); } @action @@ -39,7 +39,7 @@ export default class PresenceStore { @action update(data: GatewayPresenceUpdateDispatchData) { - this.presences.get(data.guild_id)?.get(data.user.id)?.update(data); + this.presences.get(data.user.id)?.update(data); } get(id: Snowflake) {