import { Plus } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite"; import { useLocation, useParams } from "react-router-dom"; import { Channel, Servers, Users } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components"; import { attachContextMenu, openContextMenu } from "preact-context-menu"; import ConditionalLink from "../../../lib/ConditionalLink"; import PaintCounter from "../../../lib/PaintCounter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useData } from "../../../mobx/State"; import { connectState } from "../../../redux/connector"; import { LastOpened } from "../../../redux/reducers/last_opened"; import { Unreads } from "../../../redux/reducers/unreads"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useChannels, useForceUpdate, useServers, } from "../../../context/revoltjs/hooks"; import logoSVG from "../../../assets/logo.svg"; import ServerIcon from "../../common/ServerIcon"; import Tooltip from "../../common/Tooltip"; import UserHover from "../../common/user/UserHover"; import UserIcon from "../../common/user/UserIcon"; import IconButton from "../../ui/IconButton"; import LineDivider from "../../ui/LineDivider"; import { mapChannelWithUnread } from "./common"; import { Children } from "../../../types/Preact"; function Icon({ children, unread, size, }: { children: Children; unread?: "mention" | "unread"; size: number; }) { return ( <svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32"> <use href="#serverIndicator" /> <foreignObject x="0" y="0" width="32" height="32" mask={unread ? "url(#server)" : undefined}> {children} </foreignObject> {unread === "unread" && ( <circle cx="27" cy="5" r="5" fill={"white"} /> )} {unread === "mention" && ( <circle cx="27" cy="5" r="5" fill={"var(--error)"} /> )} </svg> ); } const ServersBase = styled.div` width: 56px; height: 100%; display: flex; flex-direction: column; ${isTouchscreenDevice && css` padding-bottom: 50px; `} `; const ServerList = styled.div` flex-grow: 1; display: flex; overflow-y: scroll; padding-bottom: 48px; flex-direction: column; scrollbar-width: none; > :first-child > svg { margin: 6px 0 6px 4px; } &::-webkit-scrollbar { width: 0px; } `; const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` height: 58px; display: flex; align-items: center; > div { height: 42px; padding-left: 10px; display: grid; place-items: center; border-start-start-radius: 50%; border-end-start-radius: 50%; &:active { transform: translateY(1px); } ${(props) => props.active && css` &:active { transform: none; } `} } > span { width: 0; display: relative; ${(props) => !props.active && css` display: none; `} svg { margin-top: 5px; display: relative; pointer-events: none; // outline: 1px solid red; } } ${(props) => (!props.active || props.home) && css` cursor: pointer; `} `; function Swoosh() { return ( <span> <svg width="56" height="103" viewBox="0 0 56 103" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M55.0368 51.5947C55.0368 64.8596 44.2834 75.613 31.0184 75.613C17.7534 75.613 7 64.8596 7 51.5947C7 38.3297 17.7534 27.5763 31.0184 27.5763C44.2834 27.5763 55.0368 38.3297 55.0368 51.5947Z" fill="var(--sidebar-active)" /> <path d="M55.8809 1C55.5597 16.9971 34.4597 25.2244 24.0847 28.6715L55.8846 60.4859L55.8809 1Z" fill="var(--sidebar-active)" /> <path d="M55.8809 102.249C55.5597 86.2516 34.4597 78.0243 24.0847 74.5771L55.8846 42.7627L55.8809 102.249Z" fill="var(--sidebar-active)" /> </svg> </span> ); } interface Props { unreads: Unreads; lastOpened: LastOpened; } export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => { const store = useData(); const client = useClient(); const self = store.users.get(client.user!._id); const ctx = useForceUpdate(); const activeServers = useServers(undefined, ctx) as Servers.Server[]; const channels = [...store.channels.values()].map((x) => mapChannelWithUnread(x, unreads), ); const unreadChannels = channels .filter((x) => x.unread) .map((x) => x.channel?._id); const servers = activeServers.map((server) => { let alertCount = 0; for (const id of server.channels) { const channel = channels.find((x) => x.channel?._id === id); if (channel?.alertCount) { alertCount += channel.alertCount; } } return { ...server, unread: (typeof server.channels.find((x) => unreadChannels.includes(x), ) !== "undefined" ? alertCount > 0 ? "mention" : "unread" : undefined) as "mention" | "unread" | undefined, alertCount, }; }); const path = useLocation().pathname; const { server: server_id } = useParams<{ server?: string }>(); const server = servers.find((x) => x!._id == server_id); const { openScreen } = useIntermediate(); let homeUnread: "mention" | "unread" | undefined; let alertCount = 0; for (const x of channels) { if ( (x.channel?.channel_type === "DirectMessage" ? x.channel?.active : true) && x.unread ) { homeUnread = "unread"; alertCount += x.alertCount ?? 0; } } if ( [...store.users.values()].find( (x) => x.relationship === Users.Relationship.Incoming, ) ) { alertCount++; } if (alertCount > 0) homeUnread = "mention"; const homeActive = typeof server === "undefined" && !path.startsWith("/invite"); return ( <ServersBase> <ServerList> <ConditionalLink active={homeActive} to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}> <ServerEntry home active={homeActive}> <Swoosh /> <div onContextMenu={attachContextMenu("Status")} onClick={() => homeActive && openContextMenu("Status") }> <UserHover user={self}> <Icon size={42} unread={homeUnread}> <UserIcon target={self} size={32} status /> </Icon> </UserHover> </div> </ServerEntry> </ConditionalLink> <LineDivider /> {servers.map((entry) => { const active = entry!._id === server?._id; const id = lastOpened[entry!._id]; return ( <ConditionalLink active={active} to={`/server/${entry!._id}${ id ? `/channel/${id}` : "" }`}> <ServerEntry active={active} onContextMenu={attachContextMenu("Menu", { server: entry!._id, })}> <Swoosh /> <Tooltip content={entry.name} placement="right"> <Icon size={42} unread={entry.unread}> <ServerIcon size={32} target={entry} /> </Icon> </Tooltip> </ServerEntry> </ConditionalLink> ); })} <IconButton onClick={() => openScreen({ id: "special_input", type: "create_server", }) }> <Plus size={36} /> </IconButton> <PaintCounter small /> </ServerList> </ServersBase> ); }); export default connectState(ServerListSidebar, (state) => { return { unreads: state.unreads, lastOpened: state.lastOpened, }; });