2023-01-03 19:55:46 +00:00
|
|
|
// Copyright 2019 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2024-03-13 20:44:13 +00:00
|
|
|
import { isEmpty } from 'lodash';
|
|
|
|
import React, { memo, useCallback } from 'react';
|
|
|
|
import { useSelector } from 'react-redux';
|
2023-01-13 20:07:26 +00:00
|
|
|
import type { ReadonlyDeep } from 'type-fest';
|
2024-02-06 02:13:13 +00:00
|
|
|
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline';
|
2021-10-26 19:15:33 +00:00
|
|
|
import { Timeline } from '../../components/conversation/Timeline';
|
2024-03-13 20:44:13 +00:00
|
|
|
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';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { StateType } from '../reducer';
|
2024-03-13 20:44:13 +00:00
|
|
|
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
|
|
|
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
2019-05-31 22:42:01 +00:00
|
|
|
import {
|
2023-08-16 20:54:39 +00:00
|
|
|
getConversationByServiceIdSelector,
|
2019-05-31 22:42:01 +00:00
|
|
|
getConversationMessagesSelector,
|
|
|
|
getConversationSelector,
|
2024-03-13 20:44:13 +00:00
|
|
|
getHasContactSpoofingReview,
|
2021-03-03 20:09:58 +00:00
|
|
|
getInvitedContactsForNewlyCreatedGroup,
|
2024-03-13 20:44:13 +00:00
|
|
|
getMessages,
|
2024-02-06 02:13:13 +00:00
|
|
|
getSafeConversationWithSameTitle,
|
2024-03-13 20:44:13 +00:00
|
|
|
getSelectedConversationId,
|
2023-03-20 22:23:53 +00:00
|
|
|
getTargetedMessage,
|
2019-05-31 22:42:01 +00:00
|
|
|
} from '../selectors/conversations';
|
2024-03-13 20:44:13 +00:00
|
|
|
import { getIntl, getTheme } from '../selectors/user';
|
2024-02-06 02:13:13 +00:00
|
|
|
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
|
2024-03-13 20:44:13 +00:00
|
|
|
import { SmartCollidingAvatars } from './CollidingAvatars';
|
2022-03-24 21:46:17 +00:00
|
|
|
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
|
2024-03-13 20:44:13 +00:00
|
|
|
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
2020-05-27 21:37:06 +00:00
|
|
|
import { SmartHeroRow } from './HeroRow';
|
2023-03-20 18:03:21 +00:00
|
|
|
import { SmartMiniPlayer } from './MiniPlayer';
|
2024-03-13 20:44:13 +00:00
|
|
|
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
|
|
|
|
import { SmartTypingBubble } from './TypingBubble';
|
2021-04-21 16:31:12 +00:00
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
type ExternalProps = {
|
|
|
|
id: string;
|
|
|
|
};
|
|
|
|
|
2021-09-10 23:59:41 +00:00
|
|
|
function renderItem({
|
|
|
|
containerElementRef,
|
2021-10-12 23:59:08 +00:00
|
|
|
containerWidthBreakpoint,
|
2021-09-10 23:59:41 +00:00
|
|
|
conversationId,
|
2024-03-12 16:29:31 +00:00
|
|
|
isBlocked,
|
2022-01-26 23:05:26 +00:00
|
|
|
isOldestTimelineItem,
|
2021-09-10 23:59:41 +00:00
|
|
|
messageId,
|
|
|
|
nextMessageId,
|
|
|
|
previousMessageId,
|
2022-03-08 14:32:42 +00:00
|
|
|
unreadIndicatorPlacement,
|
2024-02-27 16:01:25 +00:00
|
|
|
}: SmartTimelineItemProps): JSX.Element {
|
2019-11-07 21:36:16 +00:00
|
|
|
return (
|
2021-08-11 16:23:21 +00:00
|
|
|
<SmartTimelineItem
|
2021-08-20 19:36:27 +00:00
|
|
|
containerElementRef={containerElementRef}
|
2021-10-12 23:59:08 +00:00
|
|
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
2019-11-07 21:36:16 +00:00
|
|
|
conversationId={conversationId}
|
2024-03-12 16:29:31 +00:00
|
|
|
isBlocked={isBlocked}
|
2022-01-26 23:05:26 +00:00
|
|
|
isOldestTimelineItem={isOldestTimelineItem}
|
2021-09-10 23:59:41 +00:00
|
|
|
messageId={messageId}
|
|
|
|
previousMessageId={previousMessageId}
|
|
|
|
nextMessageId={nextMessageId}
|
2022-03-08 14:32:42 +00:00
|
|
|
unreadIndicatorPlacement={unreadIndicatorPlacement}
|
2020-05-05 19:49:34 +00:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2021-03-10 20:36:58 +00:00
|
|
|
|
2024-02-06 02:13:13 +00:00
|
|
|
function renderCollidingAvatars(
|
|
|
|
props: SmartCollidingAvatarsPropsType
|
|
|
|
): JSX.Element {
|
|
|
|
return <SmartCollidingAvatars {...props} />;
|
|
|
|
}
|
|
|
|
|
2022-03-24 21:46:17 +00:00
|
|
|
function renderContactSpoofingReviewDialog(
|
|
|
|
props: SmartContactSpoofingReviewDialogPropsType
|
|
|
|
): JSX.Element {
|
|
|
|
return <SmartContactSpoofingReviewDialog {...props} />;
|
|
|
|
}
|
|
|
|
|
2022-12-21 03:25:10 +00:00
|
|
|
function renderHeroRow(id: string): JSX.Element {
|
|
|
|
return <SmartHeroRow id={id} />;
|
2020-05-27 21:37:06 +00:00
|
|
|
}
|
2023-03-20 18:03:21 +00:00
|
|
|
function renderMiniPlayer(options: { shouldFlow: boolean }): JSX.Element {
|
|
|
|
return <SmartMiniPlayer {...options} />;
|
|
|
|
}
|
2023-09-18 21:17:26 +00:00
|
|
|
function renderTypingBubble(conversationId: string): JSX.Element {
|
|
|
|
return <SmartTypingBubble conversationId={conversationId} />;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
2021-04-21 16:31:12 +00:00
|
|
|
const getWarning = (
|
2023-01-13 20:07:26 +00:00
|
|
|
conversation: ReadonlyDeep<ConversationType>,
|
2021-04-21 16:31:12 +00:00
|
|
|
state: Readonly<StateType>
|
|
|
|
): undefined | TimelineWarningType => {
|
2021-06-01 23:30:25 +00:00
|
|
|
switch (conversation.type) {
|
|
|
|
case 'direct':
|
|
|
|
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
|
2024-02-06 02:13:13 +00:00
|
|
|
const safeConversation = getSafeConversationWithSameTitle(state, {
|
|
|
|
possiblyUnsafeConversation: conversation,
|
|
|
|
});
|
2021-06-01 23:30:25 +00:00
|
|
|
|
|
|
|
if (safeConversation) {
|
|
|
|
return {
|
|
|
|
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
2024-02-06 02:13:13 +00:00
|
|
|
safeConversationId: safeConversation.id,
|
2021-06-01 23:30:25 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
case 'group': {
|
|
|
|
if (conversation.left || conversation.groupVersion !== 2) {
|
|
|
|
return undefined;
|
|
|
|
}
|
2021-04-21 16:31:12 +00:00
|
|
|
|
2023-08-16 20:54:39 +00:00
|
|
|
const getConversationByServiceId =
|
|
|
|
getConversationByServiceIdSelector(state);
|
2021-06-01 23:30:25 +00:00
|
|
|
|
|
|
|
const { memberships } = getGroupMemberships(
|
|
|
|
conversation,
|
2023-08-16 20:54:39 +00:00
|
|
|
getConversationByServiceId
|
2021-06-01 23:30:25 +00:00
|
|
|
);
|
|
|
|
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
|
|
|
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
|
|
|
|
if (hasGroupMembersWithSameName) {
|
|
|
|
return {
|
|
|
|
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
|
|
|
acknowledgedGroupNameCollisions:
|
2022-11-19 08:31:18 +00:00
|
|
|
conversation.acknowledgedGroupNameCollisions,
|
2021-11-11 22:43:05 +00:00
|
|
|
groupNameCollisions:
|
|
|
|
dehydrateCollisionsWithConversations(groupNameCollisions),
|
2021-06-01 23:30:25 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
default:
|
2022-11-19 08:31:18 +00:00
|
|
|
throw missingCaseError(conversation);
|
2021-06-01 23:30:25 +00:00
|
|
|
}
|
2021-04-21 16:31:12 +00:00
|
|
|
};
|
|
|
|
|
2024-03-13 20:44:13 +00:00
|
|
|
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);
|
2019-03-20 17:42:28 +00:00
|
|
|
|
2024-03-13 20:44:13 +00:00
|
|
|
const conversation = conversationSelector(id);
|
|
|
|
const conversationMessages = conversationMessagesSelector(id);
|
2019-03-20 17:42:28 +00:00
|
|
|
|
2024-03-13 20:44:13 +00:00
|
|
|
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,
|
|
|
|
} = 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 (
|
|
|
|
<Timeline
|
|
|
|
acknowledgeGroupMemberNameCollisions={
|
|
|
|
acknowledgeGroupMemberNameCollisions
|
|
|
|
}
|
|
|
|
clearInvitedServiceIdsForNewlyCreatedGroup={
|
|
|
|
clearInvitedServiceIdsForNewlyCreatedGroup
|
|
|
|
}
|
|
|
|
clearTargetedMessage={clearTargetedMessage}
|
|
|
|
closeContactSpoofingReview={closeContactSpoofingReview}
|
|
|
|
discardMessages={discardMessages}
|
|
|
|
getPreferredBadge={getPreferredBadge}
|
|
|
|
getTimestampForMessage={getTimestampForMessage}
|
|
|
|
hasContactSpoofingReview={hasContactSpoofingReview}
|
|
|
|
haveNewest={haveNewest}
|
|
|
|
haveOldest={haveOldest}
|
|
|
|
i18n={i18n}
|
|
|
|
id={id}
|
|
|
|
invitedContactsForNewlyCreatedGroup={invitedContactsForNewlyCreatedGroup}
|
|
|
|
isBlocked={isBlocked}
|
|
|
|
isConversationSelected={isConversationSelected}
|
|
|
|
isGroupV1AndDisabled={isGroupV1AndDisabled}
|
|
|
|
isIncomingMessageRequest={isIncomingMessageRequest}
|
|
|
|
isNearBottom={isNearBottom}
|
|
|
|
isSomeoneTyping={isSomeoneTyping}
|
|
|
|
items={items}
|
|
|
|
loadNewerMessages={loadNewerMessages}
|
|
|
|
loadNewestMessages={loadNewestMessages}
|
|
|
|
loadOlderMessages={loadOlderMessages}
|
|
|
|
markMessageRead={markMessageRead}
|
|
|
|
messageChangeCounter={messageChangeCounter}
|
|
|
|
messageLoadingState={messageLoadingState}
|
|
|
|
oldestUnseenIndex={oldestUnseenIndex}
|
|
|
|
peekGroupCallForTheFirstTime={peekGroupCallForTheFirstTime}
|
|
|
|
peekGroupCallIfItHasMembers={peekGroupCallIfItHasMembers}
|
|
|
|
renderCollidingAvatars={renderCollidingAvatars}
|
|
|
|
renderContactSpoofingReviewDialog={renderContactSpoofingReviewDialog}
|
|
|
|
renderHeroRow={renderHeroRow}
|
|
|
|
renderItem={renderItem}
|
|
|
|
renderMiniPlayer={renderMiniPlayer}
|
|
|
|
renderTypingBubble={renderTypingBubble}
|
|
|
|
reviewConversationNameCollision={reviewConversationNameCollision}
|
|
|
|
scrollToIndex={scrollToIndex}
|
|
|
|
scrollToIndexCounter={scrollToIndexCounter}
|
|
|
|
scrollToOldestUnreadMention={scrollToOldestUnreadMention}
|
|
|
|
setIsNearBottom={setIsNearBottom}
|
|
|
|
shouldShowMiniPlayer={shouldShowMiniPlayer}
|
|
|
|
targetedMessageId={targetedMessageId}
|
|
|
|
targetMessage={targetMessage}
|
|
|
|
theme={theme}
|
|
|
|
totalUnseen={totalUnseen}
|
|
|
|
unreadCount={unreadCount}
|
|
|
|
unreadMentionsCount={unreadMentionsCount}
|
|
|
|
warning={warning}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
});
|