// Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { isEmpty } from 'lodash'; import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import type { ReadonlyDeep } from 'type-fest'; import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline'; import { Timeline } from '../../components/conversation/Timeline'; import { ContactSpoofingType } from '../../util/contactSpoofing'; import { getGroupMemberships } from '../../util/getGroupMemberships'; import { dehydrateCollisionsWithConversations, getCollisionsFromMemberships, } from '../../util/groupMemberNameCollisions'; import { missingCaseError } from '../../util/missingCaseError'; import { useCallingActions } from '../ducks/calling'; import { useConversationsActions, type ConversationType, } from '../ducks/conversations'; import type { StateType } from '../reducer'; import { selectAudioPlayerActive } from '../selectors/audioPlayer'; import { getPreferredBadgeSelector } from '../selectors/badges'; import { getConversationByServiceIdSelector, getConversationMessagesSelector, getConversationSelector, getHasContactSpoofingReview, getInvitedContactsForNewlyCreatedGroup, getMessages, getSafeConversationWithSameTitle, getSelectedConversationId, getTargetedMessage, } from '../selectors/conversations'; import { getIntl, getTheme } from '../selectors/user'; import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars'; import { SmartCollidingAvatars } from './CollidingAvatars'; import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog'; import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog'; import { SmartHeroRow } from './HeroRow'; import { SmartMiniPlayer } from './MiniPlayer'; import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem'; import { SmartTypingBubble } from './TypingBubble'; import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager'; type ExternalProps = { id: string; }; function renderItem({ containerElementRef, containerWidthBreakpoint, conversationId, isBlocked, isGroup, isOldestTimelineItem, messageId, nextMessageId, previousMessageId, unreadIndicatorPlacement, }: SmartTimelineItemProps): JSX.Element { return ( ); } function renderCollidingAvatars( props: SmartCollidingAvatarsPropsType ): JSX.Element { return ; } function renderContactSpoofingReviewDialog( props: SmartContactSpoofingReviewDialogPropsType ): JSX.Element { return ; } function renderHeroRow(id: string): JSX.Element { return ; } function renderMiniPlayer(options: { shouldFlow: boolean }): JSX.Element { return ; } function renderTypingBubble(conversationId: string): JSX.Element { return ; } const getWarning = ( conversation: ReadonlyDeep, state: Readonly ): undefined | TimelineWarningType => { switch (conversation.type) { case 'direct': if (!conversation.acceptedMessageRequest && !conversation.isBlocked) { const safeConversation = getSafeConversationWithSameTitle(state, { possiblyUnsafeConversation: conversation, }); if (safeConversation) { return { type: ContactSpoofingType.DirectConversationWithSameTitle, safeConversationId: safeConversation.id, }; } } return undefined; case 'group': { if (conversation.left || conversation.groupVersion !== 2) { return undefined; } const getConversationByServiceId = getConversationByServiceIdSelector(state); const { memberships } = getGroupMemberships( conversation, getConversationByServiceId ); const groupNameCollisions = getCollisionsFromMemberships(memberships); const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions); if (hasGroupMembersWithSameName) { return { type: ContactSpoofingType.MultipleGroupMembersWithSameTitle, acknowledgedGroupNameCollisions: conversation.acknowledgedGroupNameCollisions, groupNameCollisions: dehydrateCollisionsWithConversations(groupNameCollisions), }; } return undefined; } default: throw missingCaseError(conversation); } }; export const SmartTimeline = memo(function SmartTimeline({ id, }: ExternalProps) { const activeAudioPlayer = useSelector(selectAudioPlayerActive); const conversationMessagesSelector = useSelector( getConversationMessagesSelector ); const conversationSelector = useSelector(getConversationSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector); const hasContactSpoofingReview = useSelector(getHasContactSpoofingReview); const i18n = useSelector(getIntl); const invitedContactsForNewlyCreatedGroup = useSelector( getInvitedContactsForNewlyCreatedGroup ); const messages = useSelector(getMessages); const selectedConversationId = useSelector(getSelectedConversationId); const targetedMessage = useSelector(getTargetedMessage); const theme = useSelector(getTheme); const conversation = conversationSelector(id); const conversationMessages = conversationMessagesSelector(id); const warning = useSelector( useCallback( (state: StateType) => { return getWarning(conversation, state); }, [conversation] ) ); const { acknowledgeGroupMemberNameCollisions, clearInvitedServiceIdsForNewlyCreatedGroup, clearTargetedMessage, closeContactSpoofingReview, discardMessages, loadNewerMessages, loadNewestMessages, loadOlderMessages, markMessageRead, reviewConversationNameCollision, scrollToOldestUnreadMention, setIsNearBottom, targetMessage, } = useConversationsActions(); const { peekGroupCallForTheFirstTime, peekGroupCallIfItHasMembers } = useCallingActions(); const getTimestampForMessage = useCallback( (messageId: string): undefined | number => { return messages[messageId]?.timestamp; }, [messages] ); const shouldShowMiniPlayer = activeAudioPlayer != null; const { acceptedMessageRequest, isBlocked = false, isGroupV1AndDisabled, removalStage, typingContactIdTimestamps = {}, unreadCount, unreadMentionsCount, type: conversationType, } = conversation ?? {}; const { haveNewest, haveOldest, isNearBottom, items, messageChangeCounter, messageLoadingState, oldestUnseenIndex, scrollToIndex, scrollToIndexCounter, totalUnseen, } = conversationMessages; const isConversationSelected = selectedConversationId === id; const isIncomingMessageRequest = !acceptedMessageRequest && removalStage !== 'justNotification'; const isSomeoneTyping = Object.keys(typingContactIdTimestamps).length > 0; const targetedMessageId = targetedMessage?.id; return ( ); });