| 
									
										
										
										
											2023-01-03 11:55:46 -08:00
										 |  |  | // Copyright 2018 Signal Messenger, LLC
 | 
					
						
							| 
									
										
										
										
											2020-10-30 15:34:04 -05:00
										 |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  | import type { ReactElement } from 'react'; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | import React, { useCallback, useEffect, useMemo, useState } from 'react'; | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | import classNames from 'classnames'; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | import { animated, useSpring } from '@react-spring/web'; | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import { TypingAnimation } from './TypingAnimation'; | 
					
						
							|  |  |  | import { Avatar } from '../Avatar'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  | import type { LocalizerType, ThemeType } from '../../types/Util'; | 
					
						
							| 
									
										
										
										
											2021-10-26 14:15:33 -05:00
										 |  |  | import type { ConversationType } from '../../state/ducks/conversations'; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  | const MAX_AVATARS_COUNT = 3; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | type TypingContactType = Pick< | 
					
						
							|  |  |  |   ConversationType, | 
					
						
							|  |  |  |   | 'acceptedMessageRequest' | 
					
						
							|  |  |  |   | 'avatarPath' | 
					
						
							|  |  |  |   | 'badges' | 
					
						
							|  |  |  |   | 'color' | 
					
						
							|  |  |  |   | 'id' | 
					
						
							|  |  |  |   | 'isMe' | 
					
						
							|  |  |  |   | 'phoneNumber' | 
					
						
							|  |  |  |   | 'profileName' | 
					
						
							|  |  |  |   | 'sharedGroupNames' | 
					
						
							|  |  |  |   | 'title' | 
					
						
							|  |  |  | >; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export type TypingBubblePropsType = { | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |   conversationId: string; | 
					
						
							| 
									
										
										
										
											2019-05-31 15:42:01 -07:00
										 |  |  |   conversationType: 'group' | 'direct'; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   typingContactIdTimestamps: Record<string, number>; | 
					
						
							|  |  |  |   lastItemAuthorId: string | undefined; | 
					
						
							|  |  |  |   lastItemTimestamp: number | undefined; | 
					
						
							|  |  |  |   getConversation: (id: string) => ConversationType; | 
					
						
							|  |  |  |   getPreferredBadge: PreferredBadgeSelectorType; | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |   showContactModal: (contactId: string, conversationId?: string) => void; | 
					
						
							| 
									
										
										
										
											2019-01-14 13:49:58 -08:00
										 |  |  |   i18n: LocalizerType; | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  |   theme: ThemeType; | 
					
						
							| 
									
										
										
										
											2021-01-14 12:07:05 -06:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | const SPRING_CONFIG = { | 
					
						
							|  |  |  |   mass: 1, | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   tension: 439, | 
					
						
							|  |  |  |   friction: 42, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   precision: 0, | 
					
						
							|  |  |  |   velocity: 0, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const AVATAR_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = { | 
					
						
							|  |  |  |   visible: { | 
					
						
							|  |  |  |     opacity: 1, | 
					
						
							|  |  |  |     width: '28px', | 
					
						
							|  |  |  |     x: '0px', | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |     top: '0px', | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   }, | 
					
						
							|  |  |  |   hidden: { | 
					
						
							|  |  |  |     opacity: 0.5, | 
					
						
							|  |  |  |     width: '4px', // Match value of module-message__typing-avatar margin-inline-start
 | 
					
						
							| 
									
										
										
										
											2023-10-06 14:30:36 -07:00
										 |  |  |     x: '12px', | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |     top: '34px', | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   }, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function TypingBubbleAvatar({ | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |   conversationId, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   contact, | 
					
						
							|  |  |  |   visible, | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   shouldAnimate, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   getPreferredBadge, | 
					
						
							|  |  |  |   onContactExit, | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |   showContactModal, | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  |   i18n, | 
					
						
							|  |  |  |   theme, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | }: { | 
					
						
							|  |  |  |   conversationId: string; | 
					
						
							|  |  |  |   contact: TypingContactType | undefined; | 
					
						
							|  |  |  |   visible: boolean; | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   shouldAnimate: boolean; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   getPreferredBadge: PreferredBadgeSelectorType; | 
					
						
							|  |  |  |   onContactExit: (id: string | undefined) => void; | 
					
						
							|  |  |  |   showContactModal: (contactId: string, conversationId?: string) => void; | 
					
						
							|  |  |  |   i18n: LocalizerType; | 
					
						
							|  |  |  |   theme: ThemeType; | 
					
						
							|  |  |  | }): ReactElement | null { | 
					
						
							|  |  |  |   const [springProps, springApi] = useSpring( | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       config: SPRING_CONFIG, | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |       from: shouldAnimate | 
					
						
							|  |  |  |         ? AVATAR_ANIMATION_PROPS[visible ? 'hidden' : 'visible'] | 
					
						
							|  |  |  |         : {}, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |       to: AVATAR_ANIMATION_PROPS[visible ? 'visible' : 'hidden'], | 
					
						
							|  |  |  |       onRest: () => { | 
					
						
							|  |  |  |         if (!visible) { | 
					
						
							|  |  |  |           onContactExit(contact?.id); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [visible] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     springApi.stop(); | 
					
						
							|  |  |  |     springApi.start(AVATAR_ANIMATION_PROPS[visible ? 'visible' : 'hidden']); | 
					
						
							|  |  |  |   }, [visible, springApi]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!contact) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <animated.div className="module-message__typing-avatar" style={springProps}> | 
					
						
							|  |  |  |       <Avatar | 
					
						
							|  |  |  |         acceptedMessageRequest={contact.acceptedMessageRequest} | 
					
						
							|  |  |  |         avatarPath={contact.avatarPath} | 
					
						
							|  |  |  |         badge={getPreferredBadge(contact.badges)} | 
					
						
							|  |  |  |         color={contact.color} | 
					
						
							|  |  |  |         conversationType="direct" | 
					
						
							|  |  |  |         i18n={i18n} | 
					
						
							|  |  |  |         isMe={contact.isMe} | 
					
						
							|  |  |  |         onClick={event => { | 
					
						
							|  |  |  |           event.stopPropagation(); | 
					
						
							|  |  |  |           event.preventDefault(); | 
					
						
							|  |  |  |           showContactModal(contact.id, conversationId); | 
					
						
							|  |  |  |         }} | 
					
						
							|  |  |  |         phoneNumber={contact.phoneNumber} | 
					
						
							|  |  |  |         profileName={contact.profileName} | 
					
						
							|  |  |  |         theme={theme} | 
					
						
							|  |  |  |         title={contact.title} | 
					
						
							|  |  |  |         sharedGroupNames={contact.sharedGroupNames} | 
					
						
							|  |  |  |         size={28} | 
					
						
							|  |  |  |       /> | 
					
						
							|  |  |  |     </animated.div> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function TypingBubbleGroupAvatars({ | 
					
						
							|  |  |  |   conversationId, | 
					
						
							|  |  |  |   typingContactIds, | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   shouldAnimate, | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   getConversation, | 
					
						
							|  |  |  |   getPreferredBadge, | 
					
						
							|  |  |  |   showContactModal, | 
					
						
							|  |  |  |   i18n, | 
					
						
							|  |  |  |   theme, | 
					
						
							|  |  |  | }: Pick< | 
					
						
							|  |  |  |   TypingBubblePropsType, | 
					
						
							|  |  |  |   | 'conversationId' | 
					
						
							|  |  |  |   | 'getConversation' | 
					
						
							|  |  |  |   | 'getPreferredBadge' | 
					
						
							|  |  |  |   | 'showContactModal' | 
					
						
							|  |  |  |   | 'i18n' | 
					
						
							|  |  |  |   | 'theme' | 
					
						
							|  |  |  | > & { | 
					
						
							|  |  |  |   typingContactIds: ReadonlyArray<string>; | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   shouldAnimate: boolean; | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  | }): ReactElement { | 
					
						
							|  |  |  |   const [allContactsById, setAllContactsById] = useState< | 
					
						
							|  |  |  |     Map<string, TypingContactType> | 
					
						
							|  |  |  |   >(new Map()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const onContactExit = useCallback((id: string | undefined) => { | 
					
						
							|  |  |  |     if (!id) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setAllContactsById(prevMap => { | 
					
						
							|  |  |  |       const map = new Map([...prevMap]); | 
					
						
							|  |  |  |       map.delete(id); | 
					
						
							|  |  |  |       return map; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const visibleContactIds: Set<string> = useMemo(() => { | 
					
						
							|  |  |  |     const set = new Set<string>(); | 
					
						
							|  |  |  |     for (const id of typingContactIds) { | 
					
						
							|  |  |  |       set.add(id); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return set; | 
					
						
							|  |  |  |   }, [typingContactIds]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     setAllContactsById(prevMap => { | 
					
						
							|  |  |  |       const map = new Map([...prevMap]); | 
					
						
							|  |  |  |       for (const id of typingContactIds) { | 
					
						
							|  |  |  |         map.set(id, getConversation(id)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return map; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }, [typingContactIds, getConversation]); | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |   const typingContactsOverflowCount = Math.max( | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |     typingContactIds.length - MAX_AVATARS_COUNT, | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |     0 | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   // Avatars are rendered Right-to-Left so the leftmost avatars can render on top.
 | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  |   return ( | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |     <div className="module-message__author-avatar-container module-message__author-avatar-container--typing"> | 
					
						
							| 
									
										
										
										
											2023-10-06 14:30:36 -07:00
										 |  |  |       <div className="module-message__typing-avatar-spacer" /> | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |       {typingContactsOverflowCount > 0 && ( | 
					
						
							|  |  |  |         <div | 
					
						
							|  |  |  |           className="module-message__typing-avatar module-message__typing-avatar--overflow-count | 
					
						
							|  |  |  |         " | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |           <div | 
					
						
							|  |  |  |             aria-label={i18n('icu:TypingBubble__avatar--overflow-count', { | 
					
						
							|  |  |  |               count: typingContactsOverflowCount, | 
					
						
							|  |  |  |             })} | 
					
						
							|  |  |  |             className="module-Avatar" | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <div className="module-Avatar__contents"> | 
					
						
							|  |  |  |               <div aria-hidden="true" className="module-Avatar__label"> | 
					
						
							|  |  |  |                 +{typingContactsOverflowCount} | 
					
						
							| 
									
										
										
										
											2023-09-18 14:17:26 -07:00
										 |  |  |               </div> | 
					
						
							|  |  |  |             </div> | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |           </div> | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  |         </div> | 
					
						
							|  |  |  |       )} | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |       {[...allContactsById.keys()] | 
					
						
							|  |  |  |         .slice(-1 * MAX_AVATARS_COUNT) | 
					
						
							|  |  |  |         .map(contactId => ( | 
					
						
							|  |  |  |           <TypingBubbleAvatar | 
					
						
							|  |  |  |             key={contactId} | 
					
						
							|  |  |  |             conversationId={conversationId} | 
					
						
							|  |  |  |             contact={allContactsById.get(contactId)} | 
					
						
							|  |  |  |             getPreferredBadge={getPreferredBadge} | 
					
						
							|  |  |  |             showContactModal={showContactModal} | 
					
						
							|  |  |  |             onContactExit={onContactExit} | 
					
						
							|  |  |  |             i18n={i18n} | 
					
						
							|  |  |  |             theme={theme} | 
					
						
							|  |  |  |             visible={visibleContactIds.has(contactId)} | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |             shouldAnimate={shouldAnimate} | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |           /> | 
					
						
							|  |  |  |         ))} | 
					
						
							|  |  |  |     </div> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const OUTER_DIV_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = { | 
					
						
							|  |  |  |   visible: { height: '44px' }, | 
					
						
							|  |  |  |   hidden: { height: '0px' }, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | const BUBBLE_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = { | 
					
						
							|  |  |  |   visible: { | 
					
						
							|  |  |  |     opacity: 1, | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |     top: '0px', | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   }, | 
					
						
							|  |  |  |   hidden: { | 
					
						
							|  |  |  |     opacity: 0.5, | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |     top: '30px', | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   }, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function TypingBubble({ | 
					
						
							|  |  |  |   conversationId, | 
					
						
							|  |  |  |   conversationType, | 
					
						
							|  |  |  |   typingContactIdTimestamps, | 
					
						
							|  |  |  |   lastItemAuthorId, | 
					
						
							|  |  |  |   lastItemTimestamp, | 
					
						
							|  |  |  |   getConversation, | 
					
						
							|  |  |  |   getPreferredBadge, | 
					
						
							|  |  |  |   showContactModal, | 
					
						
							|  |  |  |   i18n, | 
					
						
							|  |  |  |   theme, | 
					
						
							|  |  |  | }: TypingBubblePropsType): ReactElement | null { | 
					
						
							|  |  |  |   const [isVisible, setIsVisible] = useState(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const typingContactIds = useMemo( | 
					
						
							|  |  |  |     () => Object.keys(typingContactIdTimestamps), | 
					
						
							|  |  |  |     [typingContactIdTimestamps] | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   const [shouldAnimate, setShouldAnimate] = useState(false); | 
					
						
							|  |  |  |   const prevTypingContactIds = React.useRef< | 
					
						
							|  |  |  |     ReadonlyArray<string> | undefined | 
					
						
							|  |  |  |   >(); | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   const isSomeoneTyping = useMemo( | 
					
						
							|  |  |  |     () => typingContactIds.length > 0, | 
					
						
							|  |  |  |     [typingContactIds] | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   const [outerDivStyle, outerDivSpringApi] = useSpring( | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       to: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'], | 
					
						
							|  |  |  |       config: SPRING_CONFIG, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [isSomeoneTyping] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  |   const [typingAnimationStyle, typingAnimationSpringApi] = useSpring( | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       to: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'], | 
					
						
							|  |  |  |       config: SPRING_CONFIG, | 
					
						
							|  |  |  |       onRest: () => { | 
					
						
							|  |  |  |         if (!isSomeoneTyping) { | 
					
						
							|  |  |  |           setIsVisible(false); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [isSomeoneTyping] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     // When typing stops, stay visible to allow time to animate out the bubble.
 | 
					
						
							|  |  |  |     if (isSomeoneTyping) { | 
					
						
							|  |  |  |       setIsVisible(true); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     typingAnimationSpringApi.stop(); | 
					
						
							|  |  |  |     typingAnimationSpringApi.start( | 
					
						
							|  |  |  |       BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'] | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     outerDivSpringApi.stop(); | 
					
						
							|  |  |  |     outerDivSpringApi.start( | 
					
						
							|  |  |  |       OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'] | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }, [isSomeoneTyping, typingAnimationSpringApi, outerDivSpringApi]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // When only one person is typing and they just sent a new message, then instantly
 | 
					
						
							|  |  |  |   // hide the bubble without animation to seamlessly transition to their new message.
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       typingContactIds.length !== 1 || | 
					
						
							|  |  |  |       !lastItemAuthorId || | 
					
						
							|  |  |  |       !lastItemTimestamp | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const lastTypingContactId = typingContactIds[0]; | 
					
						
							|  |  |  |     const lastTypingTimestamp = typingContactIdTimestamps[lastTypingContactId]; | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       lastItemAuthorId === lastTypingContactId && | 
					
						
							|  |  |  |       lastItemTimestamp > lastTypingTimestamp | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       setIsVisible(false); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [ | 
					
						
							|  |  |  |     lastItemAuthorId, | 
					
						
							|  |  |  |     lastItemTimestamp, | 
					
						
							|  |  |  |     typingContactIds, | 
					
						
							|  |  |  |     typingContactIdTimestamps, | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |   // Only animate when the user observes a change in typing contacts, not when first
 | 
					
						
							|  |  |  |   // switching to a conversation.
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     if (shouldAnimate) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!prevTypingContactIds.current) { | 
					
						
							|  |  |  |       prevTypingContactIds.current = typingContactIds; | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (prevTypingContactIds.current !== typingContactIds) { | 
					
						
							|  |  |  |       setShouldAnimate(true); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [shouldAnimate, typingContactIds]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |   if (!isVisible) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const isGroup = conversationType === 'group'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <animated.div | 
					
						
							|  |  |  |       className="module-timeline__typing-bubble-container" | 
					
						
							|  |  |  |       style={outerDivStyle} | 
					
						
							|  |  |  |     > | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |       <animated.div | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |         className={classNames( | 
					
						
							|  |  |  |           'module-message', | 
					
						
							|  |  |  |           'module-message--incoming', | 
					
						
							|  |  |  |           'module-message--typing-bubble', | 
					
						
							|  |  |  |           isGroup ? 'module-message--group' : null | 
					
						
							|  |  |  |         )} | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |         style={outerDivStyle} | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |       > | 
					
						
							|  |  |  |         {isGroup && ( | 
					
						
							|  |  |  |           <TypingBubbleGroupAvatars | 
					
						
							|  |  |  |             conversationId={conversationId} | 
					
						
							|  |  |  |             typingContactIds={typingContactIds} | 
					
						
							| 
									
										
										
										
											2023-10-02 16:18:28 -04:00
										 |  |  |             shouldAnimate={shouldAnimate} | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |             getConversation={getConversation} | 
					
						
							|  |  |  |             getPreferredBadge={getPreferredBadge} | 
					
						
							|  |  |  |             showContactModal={showContactModal} | 
					
						
							|  |  |  |             i18n={i18n} | 
					
						
							|  |  |  |             theme={theme} | 
					
						
							|  |  |  |           /> | 
					
						
							|  |  |  |         )} | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |         <div className="module-message__container-outer module-message__container-outer--typing-bubble"> | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |           <animated.div | 
					
						
							|  |  |  |             className={classNames( | 
					
						
							|  |  |  |               'module-message__container', | 
					
						
							|  |  |  |               'module-message__container--incoming' | 
					
						
							|  |  |  |             )} | 
					
						
							|  |  |  |             style={typingAnimationStyle} | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <div className="module-message__typing-animation-container"> | 
					
						
							|  |  |  |               <TypingAnimation color="light" i18n={i18n} /> | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |           </animated.div> | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  |         </div> | 
					
						
							| 
									
										
										
										
											2023-09-28 12:54:56 -04:00
										 |  |  |       </animated.div> | 
					
						
							| 
									
										
										
										
											2023-09-27 17:23:52 -04:00
										 |  |  |     </animated.div> | 
					
						
							| 
									
										
										
										
											2021-11-15 14:01:58 -06:00
										 |  |  |   ); | 
					
						
							| 
									
										
										
										
											2018-11-14 11:10:32 -08:00
										 |  |  | } |