Skip to content
Snippets Groups Projects
ServerListSidebar.tsx 6.39 KiB
Newer Older
insert's avatar
insert committed
import IconButton from "../../ui/IconButton";
import LineDivider from "../../ui/LineDivider";
insert's avatar
insert committed
import { mapChannelWithUnread } from "./common";
import styled, { css } from "styled-components";
import ServerIcon from "../../common/ServerIcon";
import { Children } from "../../../types/Preact";
insert's avatar
insert committed
import { PlusCircle } from "@styled-icons/feather";
insert's avatar
insert committed
import PaintCounter from "../../../lib/PaintCounter";
insert's avatar
insert committed
import { attachContextMenu } from 'preact-context-menu';
import { connectState } from "../../../redux/connector";
insert's avatar
insert committed
import { useLocation, useParams } from "react-router-dom";
import { Unreads } from "../../../redux/reducers/unreads";
insert's avatar
insert committed
import ConditionalLink from "../../../lib/ConditionalLink";
import { Channel, Servers } from "revolt.js/dist/api/objects";
insert's avatar
insert committed
import { LastOpened } from "../../../redux/reducers/last_opened";
insert's avatar
insert committed
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useChannels, useForceUpdate, useServers } from "../../../context/revoltjs/hooks";

import logoSVG from '../../../assets/logo.svg';
insert's avatar
insert committed

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"
        >
            <foreignObject x="0" y="0" width="32" height="32">
                { children }
            </foreignObject>
            {unread === 'unread' && (
                <circle
                    cx="27"
                    cy="27"
                    r="5"
                    fill={"white"}
                />
            )}
            {unread === 'mention' && (
                <circle
                    cx="27"
                    cy="27"
                    r="5"
                    fill={"red"}
                />
            )}
        </svg>
    )
}

const ServersBase = styled.div`
    width: 52px;
    height: 100%;
    display: flex;
    flex-direction: column;
insert's avatar
insert committed

    ${ isTouchscreenDevice && css`
        padding-bottom: 50px;
    ` }
insert's avatar
insert committed
`;

const ServerList = styled.div`
    flex-grow: 1;
    display: flex;
    overflow-y: scroll;
    padding-bottom: 48px;
    flex-direction: column;
    border-inline-end: 2px solid var(--sidebar-active);

    scrollbar-width: none;

    > :first-child > svg {
        margin: 6px 0 6px 4px;
    }

    &::-webkit-scrollbar {
        width: 0px;
    }
`;

const ServerEntry = styled.div<{ active: boolean, invert?: boolean }>`
    height: 44px;
    padding: 4px;
    margin: 2px 0 2px 4px;

    border-top-left-radius: 4px;
    border-bottom-left-radius: 4px;

    img {
        width: 32px;
        height: 32px;
    }

    ${ props => props.active && css`
        background: var(--sidebar-active);
    ` }

    ${ props => props.active && props.invert && css`
        img {
            filter: saturate(0) brightness(10);
        }
    ` }
`;

interface Props {
    unreads: Unreads;
insert's avatar
insert committed
    lastOpened: LastOpened;
insert's avatar
insert committed
}

insert's avatar
insert committed
export function ServerListSidebar({ unreads, lastOpened }: Props) {
insert's avatar
insert committed
    const ctx = useForceUpdate();
    const activeServers = useServers(undefined, ctx) as Servers.Server[];
    const channels = (useChannels(undefined, ctx) as Channel[])
        .map(x => mapChannelWithUnread(x, unreads));
    
    const unreadChannels = channels.filter(x => x.unread)
        .map(x => x._id);

    const servers = activeServers.map(server => {
        let alertCount = 0;
        for (let id of server.channels) {
            let channel = channels.find(x => x._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);

insert's avatar
insert committed
    const { openScreen } = useIntermediate();
insert's avatar
insert committed

    let homeUnread: 'mention' | 'unread' | undefined;
    let alertCount = 0;
    for (let x of channels) {
        if (((x.channel_type === 'DirectMessage' && x.active) || x.channel_type === 'Group') && x.unread) {
            homeUnread = 'unread';
            alertCount += x.alertCount ?? 0;
        }
    }

    if (alertCount > 0) homeUnread = 'mention';
insert's avatar
insert committed
    const homeActive = typeof server === 'undefined' && !path.startsWith('/invite');
insert's avatar
insert committed

    return (
        <ServersBase>
            <ServerList>
insert's avatar
insert committed
                <ConditionalLink active={homeActive} to={lastOpened.home ? `/channel/${lastOpened.home}` : '/'}>
                    <ServerEntry invert active={homeActive}>
insert's avatar
insert committed
                        <Icon size={36} unread={homeUnread}>
                            <img src={logoSVG} />
insert's avatar
insert committed
                        </Icon>
                    </ServerEntry>
insert's avatar
insert committed
                </ConditionalLink>
insert's avatar
insert committed
                <LineDivider />
                {
insert's avatar
insert committed
                    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 })}>
                                    <Icon size={36} unread={entry.unread}>
                                        <ServerIcon size={32} target={entry} />
                                    </Icon>
                                </ServerEntry>
                            </ConditionalLink>
                        )
                    })
insert's avatar
insert committed
                }
insert's avatar
insert committed
                <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_server' })}>
                    <PlusCircle size={36} />
                </IconButton>
insert's avatar
insert committed
                <PaintCounter small />
            </ServerList>
        </ServersBase>
    )
}

export default connectState(
    ServerListSidebar,
    state => {
        return {
insert's avatar
insert committed
            unreads: state.unreads,
            lastOpened: state.lastOpened
insert's avatar
insert committed
        };
    }
);