import { useMemo, useState, useEffect, Ref } from 'react'; import { Flex, Text, BoxProps, Box, convertDarkText, Icon } from '../..'; import { BubbleStyle, BubbleAuthor, BubbleFooter } from './Bubble.styles'; import { FragmentBlock, LineBreak, renderFragment } from './fragment-lib'; import { Reactions, OnReactionPayload } from './Reaction'; import { FragmentReactionType, FragmentStatusType, FragmentType, } from './Bubble.types'; import { chatDate } from '../../util/date'; import { InlineStatus } from './InlineStatus'; import { BUBBLE_HEIGHT, STATUS_HEIGHT } from './Bubble.constants'; export type BubbleProps = { id: string; author: string; authorColor?: string; themeMode?: 'dark' | 'light'; authorNickname?: string; isEdited?: boolean; isEditing?: boolean; expiresAt?: number | null; updatedAt?: number | null; sentAt: string; isOur?: boolean; ourShip?: string; ourColor?: string; message?: FragmentType[]; reactions?: FragmentReactionType[]; containerWidth?: number; isPrevGrouped?: boolean; // should we show the author if multiple messages by same author? isNextGrouped?: boolean; // should we show the author if multiple messages by same author? innerRef?: Ref; onReaction?: (payload: OnReactionPayload) => void; onReplyClick?: (msgId: string) => void; } & BoxProps; export const Bubble = ({ innerRef, id, author, authorNickname, themeMode, isOur, ourColor, ourShip, sentAt, authorColor, message, isEdited, isEditing, containerWidth, reactions = [], isPrevGrouped, isNextGrouped, updatedAt, expiresAt, onReaction, onReplyClick, }: BubbleProps) => { const [dateDisplay, setDateDisplay] = useState(chatDate(new Date(sentAt))); useEffect(() => { let timer: NodeJS.Timeout; function initClock() { clearTimeout(timer); const sentDate = new Date(sentAt); const interval: number = (60 - sentDate.getSeconds()) * 1000 + 5; setDateDisplay(chatDate(sentDate)); timer = setTimeout(initClock, interval); } initClock(); return () => { clearTimeout(timer); }; }, [sentAt]); const authorColorDisplay = useMemo( () => (authorColor && convertDarkText(authorColor, themeMode)) || 'rgba(var(--rlm-text-rgba))', [authorColor] ); const innerWidth = useMemo( () => (containerWidth ? containerWidth - 16 : undefined), [containerWidth] ); const footerHeight = useMemo(() => { if (reactions.length > 0) { return BUBBLE_HEIGHT.rem.footerReactions; } return BUBBLE_HEIGHT.rem.footer; }, [reactions.length]); const fragments = useMemo(() => { if (!message) return []; return message?.map((fragment, index) => { let prevLineBreak, nextLineBreak; if (index > 0) { if (message[index - 1]) { const previousType = Object.keys(message[index - 1])[0]; if (previousType === 'image') { prevLineBreak = ; } } else { console.warn( 'expected a non-null message at ', index - 1, message[index - 1] ); } } if (index < message.length - 1) { if (message[index + 1]) { const nextType = Object.keys(message[index + 1])[0]; if (nextType === 'image') { nextLineBreak = ; } } else { console.warn( 'expected a non-null message at ', index + 1, message[index + 1] ); } } return ( {prevLineBreak} {renderFragment( id, fragment, index, author, innerWidth, onReplyClick )} {nextLineBreak} ); }); }, [message, updatedAt]); const minBubbleWidth = useMemo(() => (isEdited ? 164 : 114), [isEdited]); const reactionsDisplay = useMemo(() => { return ( ); }, [reactions.length, isOur, ourShip, ourColor, onReaction]); return useMemo(() => { if (message?.length === 1) { const contentType = Object.keys(message[0])[0]; if (contentType === 'status') { return ( ); } } return ( {!isOur && !isPrevGrouped && ( {authorNickname || author} )} {fragments} {reactionsDisplay} {expiresAt && ( // TODO tooltip with time remaining )} {isEditing && 'Editing... · '} {isEdited && !isEditing && 'Edited · '} {dateDisplay} ); }, [ id, isPrevGrouped, isNextGrouped, isOur, ourColor, isEditing, isEdited, authorColorDisplay, authorNickname, author, fragments, reactionsDisplay, dateDisplay, minBubbleWidth, footerHeight, ]); };