RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx

Summary

Maintainability
F
1 wk
Test Coverage
import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings';
import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage';
import type { useTranslation } from '@rocket.chat/ui-contexts';
import { useLayout } from '@rocket.chat/ui-contexts';
import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react';
import React, { memo, useMemo } from 'react';

import { RoomIcon } from '../../components/RoomIcon';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { isIOsDevice } from '../../lib/utils/isIOsDevice';
import { useOmnichannelPriorities } from '../../omnichannel/hooks/useOmnichannelPriorities';
import RoomMenu from '../RoomMenu';
import { OmnichannelBadges } from '../badges/OmnichannelBadges';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { normalizeSidebarMessage } from './normalizeSidebarMessage';

const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnType<typeof useTranslation>): string | undefined => {
    if (!lastMessage) {
        return t('No_messages_yet');
    }
    if (isVideoConfMessage(lastMessage)) {
        return t('Call_started');
    }
    if (!lastMessage.u) {
        return normalizeSidebarMessage(lastMessage, t);
    }
    if (lastMessage.u?.username === room.u?.username) {
        return `${t('You')}: ${normalizeSidebarMessage(lastMessage, t)}`;
    }
    if (isDirectMessageRoom(room) && !isMultipleDirectMessageRoom(room)) {
        return normalizeSidebarMessage(lastMessage, t);
    }
    return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`;
};

const getBadgeTitle = (
    userMentions: number,
    threadUnread: number,
    groupMentions: number,
    unread: number,
    t: ReturnType<typeof useTranslation>,
) => {
    const title = [] as string[];
    if (userMentions) {
        title.push(t('mentions_counter', { count: userMentions }));
    }
    if (threadUnread) {
        title.push(t('threads_counter', { count: threadUnread }));
    }
    if (groupMentions) {
        title.push(t('group_mentions_counter', { count: groupMentions }));
    }
    const count = unread - userMentions - groupMentions;
    if (count > 0) {
        title.push(t('unread_messages_counter', { count }));
    }
    return title.join(', ');
};

type RoomListRowProps = {
    extended: boolean;
    t: ReturnType<typeof useTranslation>;
    SideBarItemTemplate: ComponentType<
        {
            icon: ReactNode;
            title: ReactNode;
            avatar: ReactNode;
            actions: unknown;
            href: string;
            time?: Date;
            menu?: ReactNode;
            menuOptions?: unknown;
            subtitle?: ReactNode;
            titleIcon?: string;
            badges?: ReactNode;
            threadUnread?: boolean;
            unread?: boolean;
            selected?: boolean;
            is?: string;
        } & AllHTMLAttributes<HTMLElement>
    >;
    AvatarTemplate: ReturnType<typeof useAvatarTemplate>;
    openedRoom?: string;
    // sidebarViewMode: 'extended';
    isAnonymous?: boolean;

    room: ISubscription & IRoom;
    id?: string;
    /* @deprecated */
    style?: AllHTMLAttributes<HTMLElement>['style'];

    selected?: boolean;

    sidebarViewMode?: unknown;
    videoConfActions?: {
        [action: string]: () => void;
    };
};

function SideBarItemTemplateWithData({
    room,
    id,
    selected,
    style,
    extended,
    SideBarItemTemplate,
    AvatarTemplate,
    t,
    isAnonymous,
    videoConfActions,
}: RoomListRowProps): ReactElement {
    const { sidebar } = useLayout();

    const href = roomCoordinator.getRouteLink(room.t, room) || '';
    const title = roomCoordinator.getRoomName(room.t, room) || '';

    const {
        lastMessage,
        hideUnreadStatus,
        hideMentionStatus,
        unread = 0,
        alert,
        userMentions,
        groupMentions,
        tunread = [],
        tunreadUser = [],
        rid,
        t: type,
        cl,
    } = room;

    const highlighted = Boolean(!hideUnreadStatus && (alert || unread));
    const icon = (
        // TODO: Remove icon='at'
        <Sidebar.Item.Icon highlighted={highlighted} icon='at'>
            <RoomIcon room={room} placement='sidebar' isIncomingCall={Boolean(videoConfActions)} />
        </Sidebar.Item.Icon>
    );

    const actions = useMemo(
        () =>
            videoConfActions && (
                <SidebarItemActions>
                    <SidebarItemAction onClick={videoConfActions.acceptCall} secondary success icon='phone' />
                    <SidebarItemAction onClick={videoConfActions.rejectCall} secondary danger icon='phone-off' />
                </SidebarItemActions>
            ),
        [videoConfActions],
    );

    const isQueued = isOmnichannelRoom(room) && room.status === 'queued';
    const { enabled: isPriorityEnabled } = useOmnichannelPriorities();

    const message = extended && getMessage(room, lastMessage, t);
    const subtitle = message ? <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }} /> : null;

    const threadUnread = tunread.length > 0;
    const variant =
        ((userMentions || tunreadUser.length) && 'danger') || (threadUnread && 'primary') || (groupMentions && 'warning') || 'secondary';

    const isUnread = unread > 0 || threadUnread;
    const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0));

    const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t);

    const badges = (
        <Margins inlineStart={8}>
            {showBadge && isUnread && (
                <Badge role='status' {...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)} variant={variant} title={badgeTitle}>
                    {unread + tunread?.length}
                </Badge>
            )}
            {isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
        </Margins>
    );

    return (
        <SideBarItemTemplate
            is='a'
            id={id}
            data-qa='sidebar-item'
            data-unread={highlighted}
            unread={highlighted}
            selected={selected}
            href={href}
            onClick={(): void => {
                !selected && sidebar.toggle();
            }}
            aria-label={title}
            title={title}
            time={lastMessage?.ts}
            subtitle={subtitle}
            icon={icon}
            style={style}
            badges={badges}
            avatar={AvatarTemplate && <AvatarTemplate {...room} />}
            actions={actions}
            menu={
                !isIOsDevice &&
                !isAnonymous &&
                (!isQueued || (isQueued && isPriorityEnabled)) &&
                ((): ReactElement => (
                    <RoomMenu
                        alert={alert}
                        threadUnread={threadUnread}
                        rid={rid}
                        unread={!!unread}
                        roomOpen={selected}
                        type={type}
                        cl={cl}
                        name={title}
                        hideDefaultOptions={isQueued}
                    />
                ))
            }
        />
    );
}

function safeDateNotEqualCheck(a: Date | string | undefined, b: Date | string | undefined): boolean {
    if (!a || !b) {
        return a !== b;
    }
    return new Date(a).toISOString() !== new Date(b).toISOString();
}

const keys: (keyof RoomListRowProps)[] = [
    'id',
    'style',
    'extended',
    'selected',
    'SideBarItemTemplate',
    'AvatarTemplate',
    't',
    'sidebarViewMode',
    'videoConfActions',
];

// eslint-disable-next-line react/no-multi-comp
export default memo(SideBarItemTemplateWithData, (prevProps, nextProps) => {
    if (keys.some((key) => prevProps[key] !== nextProps[key])) {
        return false;
    }

    if (prevProps.room === nextProps.room) {
        return true;
    }

    if (prevProps.room._id !== nextProps.room._id) {
        return false;
    }
    if (prevProps.room._updatedAt?.toISOString() !== nextProps.room._updatedAt?.toISOString()) {
        return false;
    }
    if (safeDateNotEqualCheck(prevProps.room.lastMessage?._updatedAt, nextProps.room.lastMessage?._updatedAt)) {
        return false;
    }
    if (prevProps.room.alert !== nextProps.room.alert) {
        return false;
    }
    if (isOmnichannelRoom(prevProps.room) && isOmnichannelRoom(nextProps.room) && prevProps.room?.v?.status !== nextProps.room?.v?.status) {
        return false;
    }
    if (prevProps.room.teamMain !== nextProps.room.teamMain) {
        return false;
    }

    if (
        isOmnichannelRoom(prevProps.room) &&
        isOmnichannelRoom(nextProps.room) &&
        prevProps.room.priorityWeight !== nextProps.room.priorityWeight
    ) {
        return false;
    }

    return true;
});