import { Row } from 'web/components/layout/row' import { Col } from 'web/components/layout/col' import { PrivateUser, User } from 'common/user' import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments' import { CommentInputTextArea, TruncatedComment, } from 'web/components/feed/feed-comments' import { track } from 'web/lib/service/analytics' import { firebaseLogin } from 'web/lib/firebase/users' import { useRouter } from 'next/router' import clsx from 'clsx' import { UserLink } from 'web/components/user-page' import { groupPath } from 'web/lib/firebase/groups' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { Tipper } from 'web/components/tipper' import { sum } from 'lodash' import { formatMoney } from 'common/util/format' import { useWindowSize } from 'web/hooks/use-window-size' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { ChatIcon, ChevronDownIcon } from '@heroicons/react/outline' import { setNotificationsAsSeen } from 'web/pages/notifications' export function GroupChat(props: { messages: Comment[] user: User | null | undefined group: Group tips: CommentTipMap }) { const { messages, user, group, tips } = props const [messageText, setMessageText] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [scrollToBottomRef, setScrollToBottomRef] = useState(null) const [scrollToMessageId, setScrollToMessageId] = useState('') const [scrollToMessageRef, setScrollToMessageRef] = useState(null) const [replyToUsername, setReplyToUsername] = useState('') const [inputRef, setInputRef] = useState(null) const [groupedMessages, setGroupedMessages] = useState([]) const router = useRouter() const isMember = user && group.memberIds.includes(user?.id) const { width, height } = useWindowSize() const [containerRef, setContainerRef] = useState(null) // Subtract bottom bar when it's showing (less than lg screen) const bottomBarHeight = (width ?? 0) < 1024 ? 58 : 0 const remainingHeight = (height ?? 0) - (containerRef?.offsetTop ?? 0) - bottomBarHeight useMemo(() => { // Group messages with createdTime within 2 minutes of each other. const tempMessages = [] for (let i = 0; i < messages.length; i++) { const message = messages[i] if (i === 0) tempMessages.push({ ...message }) else { const prevMessage = messages[i - 1] const diff = message.createdTime - prevMessage.createdTime const creatorsMatch = message.userId === prevMessage.userId if (diff < 2 * 60 * 1000 && creatorsMatch) { tempMessages[tempMessages.length - 1].text += `\n${message.text}` } else { tempMessages.push({ ...message }) } } } setGroupedMessages(tempMessages) }, [messages]) useEffect(() => { scrollToMessageRef?.scrollIntoView() }, [scrollToMessageRef]) useEffect(() => { if (scrollToBottomRef) scrollToBottomRef.scrollTo({ top: scrollToBottomRef.scrollHeight || 0 }) // Must also listen to groupedMessages as they update the height of the messaging window }, [scrollToBottomRef, groupedMessages]) useEffect(() => { const elementInUrl = router.asPath.split('#')[1] if (messages.map((m) => m.id).includes(elementInUrl)) { setScrollToMessageId(elementInUrl) } }, [messages, router.asPath]) useEffect(() => { // is mobile? if (inputRef && width && width > 720) inputRef.focus() }, [inputRef, width]) function onReplyClick(comment: Comment) { setReplyToUsername(comment.userUsername) } async function submitMessage() { if (!user) { track('sign in to comment') return await firebaseLogin() } if (!messageText || isSubmitting) return setIsSubmitting(true) await createCommentOnGroup(group.id, messageText, user) setMessageText('') setIsSubmitting(false) setReplyToUsername('') inputRef?.focus() } return ( {groupedMessages.map((message) => ( ))} {messages.length === 0 && (
No messages yet. Why not{isMember ? ` ` : ' join and '}
)} {user && group.memberIds.includes(user.id) && (
)} ) } export function GroupChatInBubble(props: { messages: Comment[] user: User | null | undefined privateUser: PrivateUser | null | undefined group: Group tips: CommentTipMap }) { const { messages, user, group, tips, privateUser } = props const [shouldShowChat, setShouldShowChat] = useState(false) const router = useRouter() useEffect(() => { const groupsWithChatEmphasis = [ 'welcome', 'bugs', 'manifold-features-25bad7c7792e', 'updates', ] if ( router.asPath.includes('/chat') || groupsWithChatEmphasis.includes( router.asPath.split('/group/')[1].split('/')[0] ) ) { setShouldShowChat(true) } // Leave chat open between groups if user is using chat? else { setShouldShowChat(false) } }, [router.asPath]) return ( {shouldShowChat && ( )} ) } function GroupChatNotificationsIcon(props: { group: Group privateUser: PrivateUser shouldSetAsSeen: boolean }) { const { privateUser, group, shouldSetAsSeen } = props const preferredNotificationsForThisGroup = useUnseenPreferredNotifications( privateUser, { customHref: `/group/${group.slug}`, } ) useEffect(() => { preferredNotificationsForThisGroup.forEach((notification) => { if ( (shouldSetAsSeen && notification.isSeenOnHref?.includes('chat')) || // old style chat notif that simply ended with the group slug notification.isSeenOnHref?.endsWith(group.slug) ) { setNotificationsAsSeen([notification]) } }) }, [group.slug, preferredNotificationsForThisGroup, shouldSetAsSeen]) return (
0 && !shouldSetAsSeen ? 'absolute right-4 top-4 h-3 w-3 rounded-full border-2 border-white bg-red-500' : 'hidden' } >
) } const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined comment: Comment group: Group onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void highlight?: boolean tips: CommentTips }) { const { comment, onReplyClick, group, setRef, highlight, user, tips } = props const { text, userUsername, userName, userAvatarUrl, createdTime } = comment const isCreatorsComment = user && comment.userId === user.id return ( {!isCreatorsComment && ( )} {!isCreatorsComment ? ( ) : ( {'You'} )} {!isCreatorsComment && onReplyClick && ( )} {isCreatorsComment && sum(Object.values(tips)) > 0 && ( {formatMoney(sum(Object.values(tips)))} )} {!isCreatorsComment && } ) })