signal-desktop/ts/state/smart/Timeline.tsx

204 lines
6.5 KiB
TypeScript
Raw Normal View History

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
import { isEmpty, pick } from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import type { ReadonlyDeep } from 'type-fest';
import { mapDispatchToProps } from '../actions';
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer';
import type { ConversationType } from '../ducks/conversations';
2021-11-20 15:41:21 +00:00
import { getIntl, getTheme } from '../selectors/user';
import {
2022-12-23 00:32:03 +00:00
getMessages,
2023-08-16 20:54:39 +00:00
getConversationByServiceIdSelector,
getConversationMessagesSelector,
getConversationSelector,
2021-03-03 20:09:58 +00:00
getInvitedContactsForNewlyCreatedGroup,
getSafeConversationWithSameTitle,
2023-03-20 22:23:53 +00:00
getTargetedMessage,
} from '../selectors/conversations';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
2024-02-27 16:01:25 +00:00
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartCollidingAvatars } from './CollidingAvatars';
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
import { SmartTypingBubble } from './TypingBubble';
2020-05-27 21:37:06 +00:00
import { SmartHeroRow } from './HeroRow';
2021-06-01 23:30:25 +00:00
import { missingCaseError } from '../../util/missingCaseError';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import {
dehydrateCollisionsWithConversations,
getCollisionsFromMemberships,
} from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
2021-11-20 15:41:21 +00:00
import { getPreferredBadgeSelector } from '../selectors/badges';
import { SmartMiniPlayer } from './MiniPlayer';
2021-04-21 16:31:12 +00:00
type ExternalProps = {
id: string;
};
function renderItem({
containerElementRef,
containerWidthBreakpoint,
conversationId,
2024-03-12 16:29:31 +00:00
isBlocked,
2022-01-26 23:05:26 +00:00
isOldestTimelineItem,
messageId,
nextMessageId,
previousMessageId,
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
containerElementRef={containerElementRef}
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}
messageId={messageId}
previousMessageId={previousMessageId}
nextMessageId={nextMessageId}
unreadIndicatorPlacement={unreadIndicatorPlacement}
/>
);
}
function renderCollidingAvatars(
props: SmartCollidingAvatarsPropsType
): JSX.Element {
return <SmartCollidingAvatars {...props} />;
}
function renderContactSpoofingReviewDialog(
props: SmartContactSpoofingReviewDialogPropsType
): JSX.Element {
return <SmartContactSpoofingReviewDialog {...props} />;
}
function renderHeroRow(id: string): JSX.Element {
return <SmartHeroRow id={id} />;
2020-05-27 21:37:06 +00:00
}
function renderMiniPlayer(options: { shouldFlow: boolean }): JSX.Element {
return <SmartMiniPlayer {...options} />;
}
function renderTypingBubble(conversationId: string): JSX.Element {
return <SmartTypingBubble conversationId={conversationId} />;
}
2021-04-21 16:31:12 +00:00
const getWarning = (
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) {
const safeConversation = getSafeConversationWithSameTitle(state, {
possiblyUnsafeConversation: conversation,
});
2021-06-01 23:30:25 +00:00
if (safeConversation) {
return {
type: ContactSpoofingType.DirectConversationWithSameTitle,
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:
conversation.acknowledgedGroupNameCollisions,
2021-11-11 22:43:05 +00:00
groupNameCollisions:
dehydrateCollisionsWithConversations(groupNameCollisions),
2021-06-01 23:30:25 +00:00
};
}
return undefined;
}
default:
throw missingCaseError(conversation);
2021-06-01 23:30:25 +00:00
}
2021-04-21 16:31:12 +00:00
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const conversation = getConversationSelector(state)(id);
const conversationMessages = getConversationMessagesSelector(state)(id);
2023-03-20 22:23:53 +00:00
const targetedMessage = getTargetedMessage(state);
const getTimestampForMessage = (messageId: string): undefined | number =>
2022-12-23 00:32:03 +00:00
getMessages(state)[messageId]?.timestamp;
2022-01-26 23:05:26 +00:00
const shouldShowMiniPlayer = Boolean(selectAudioPlayerActive(state));
return {
id,
...pick(conversation, [
'unreadCount',
'unreadMentionsCount',
'isGroupV1AndDisabled',
2023-09-27 21:23:52 +00:00
'typingContactIdTimestamps',
]),
2024-03-12 16:29:31 +00:00
isBlocked: conversation.isBlocked ?? false,
isConversationSelected: state.conversations.selectedConversationId === id,
isIncomingMessageRequest: Boolean(
!conversation.acceptedMessageRequest &&
conversation.removalStage !== 'justNotification'
),
2023-09-27 21:23:52 +00:00
isSomeoneTyping: Boolean(
Object.keys(conversation.typingContactIdTimestamps ?? {}).length > 0
),
...conversationMessages,
2021-11-11 22:43:05 +00:00
invitedContactsForNewlyCreatedGroup:
getInvitedContactsForNewlyCreatedGroup(state),
2023-03-20 22:23:53 +00:00
targetedMessageId: targetedMessage ? targetedMessage.id : undefined,
shouldShowMiniPlayer,
2021-04-21 16:31:12 +00:00
warning: getWarning(conversation, state),
hasContactSpoofingReview: state.conversations.hasContactSpoofingReview,
2021-04-21 16:31:12 +00:00
2022-01-26 23:05:26 +00:00
getTimestampForMessage,
2021-11-20 15:41:21 +00:00
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
2021-11-20 15:41:21 +00:00
theme: getTheme(state),
renderCollidingAvatars,
renderContactSpoofingReviewDialog,
2020-05-27 21:37:06 +00:00
renderHeroRow,
renderItem,
renderMiniPlayer,
renderTypingBubble,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartTimeline = smart(Timeline);