Signal-Desktop/ts/state/smart/Timeline.tsx

183 lines
5.4 KiB
TypeScript
Raw Normal View History

// Copyright 2019-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import { pick } from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
2021-04-21 16:31:12 +00:00
import {
Timeline,
WarningType as TimelineWarningType,
} from '../../components/conversation/Timeline';
import { StateType } from '../reducer';
2021-04-21 16:31:12 +00:00
import { ConversationType } from '../ducks/conversations';
import { getIntl } from '../selectors/user';
import {
getConversationMessagesSelector,
getConversationSelector,
2021-04-21 16:31:12 +00:00
getConversationsByTitleSelector,
2021-03-03 20:09:58 +00:00
getInvitedContactsForNewlyCreatedGroup,
2019-11-07 21:36:16 +00:00
getSelectedMessage,
} from '../selectors/conversations';
import { SmartTimelineItem } from './TimelineItem';
import { SmartTypingBubble } from './TypingBubble';
import { SmartLastSeenIndicator } from './LastSeenIndicator';
2020-05-27 21:37:06 +00:00
import { SmartHeroRow } from './HeroRow';
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
import { renderAudioAttachment } from './renderAudioAttachment';
import { renderEmojiPicker } from './renderEmojiPicker';
2021-04-21 16:31:12 +00:00
import { assert } from '../../util/assert';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSmartTimelineItem = SmartTimelineItem as any;
const FilteredSmartTypingBubble = SmartTypingBubble as any;
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
2020-05-27 21:37:06 +00:00
const FilteredSmartHeroRow = SmartHeroRow as any;
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
type ExternalProps = {
id: string;
// Note: most action creators are not wired into redux; for now they
// are provided by ConversationView in setupTimeline().
};
2019-11-07 21:36:16 +00:00
function renderItem(
messageId: string,
conversationId: string,
actionProps: Record<string, unknown>
2019-11-07 21:36:16 +00:00
): JSX.Element {
return (
<FilteredSmartTimelineItem
{...actionProps}
conversationId={conversationId}
id={messageId}
renderEmojiPicker={renderEmojiPicker}
renderAudioAttachment={renderAudioAttachment}
/>
);
}
function renderLastSeenIndicator(id: string): JSX.Element {
return <FilteredSmartLastSeenIndicator id={id} />;
}
2020-08-07 00:50:54 +00:00
function renderHeroRow(
id: string,
onHeightChange: () => unknown,
unblurAvatar: () => void,
2020-08-07 00:50:54 +00:00
updateSharedGroups: () => unknown
): JSX.Element {
return (
<FilteredSmartHeroRow
id={id}
onHeightChange={onHeightChange}
unblurAvatar={unblurAvatar}
2020-08-07 00:50:54 +00:00
updateSharedGroups={updateSharedGroups}
/>
);
2020-05-27 21:37:06 +00:00
}
function renderLoadingRow(id: string): JSX.Element {
return <FilteredSmartTimelineLoadingRow id={id} />;
}
function renderTypingBubble(id: string): JSX.Element {
return <FilteredSmartTypingBubble id={id} />;
}
2021-04-21 16:31:12 +00:00
const getWarning = (
conversation: Readonly<ConversationType>,
state: Readonly<StateType>
): undefined | TimelineWarningType => {
if (
conversation.type === 'direct' &&
!conversation.acceptedMessageRequest &&
!conversation.isBlocked
) {
const getConversationsWithTitle = getConversationsByTitleSelector(state);
const conversationsWithSameTitle = getConversationsWithTitle(
conversation.title
);
assert(
conversationsWithSameTitle.length,
'Expected at least 1 conversation with the same title (this one)'
);
const safeConversation = conversationsWithSameTitle.find(
otherConversation =>
otherConversation.acceptedMessageRequest &&
otherConversation.type === 'direct' &&
otherConversation.id !== conversation.id
);
return safeConversation ? { safeConversation } : undefined;
}
return undefined;
};
const getContactSpoofingReview = (
selectedConversationId: string,
state: Readonly<StateType>
):
| undefined
| {
possiblyUnsafeConversation: ConversationType;
safeConversation: ConversationType;
} => {
const { contactSpoofingReview } = state.conversations;
if (!contactSpoofingReview) {
return undefined;
}
const conversationSelector = getConversationSelector(state);
return {
possiblyUnsafeConversation: conversationSelector(selectedConversationId),
safeConversation: conversationSelector(
contactSpoofingReview.safeConversationId
),
};
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id, ...actions } = props;
const conversation = getConversationSelector(state)(id);
const conversationMessages = getConversationMessagesSelector(state)(id);
2019-11-07 21:36:16 +00:00
const selectedMessage = getSelectedMessage(state);
return {
id,
...pick(conversation, [
'unreadCount',
'typingContact',
'isGroupV1AndDisabled',
]),
...conversationMessages,
2021-03-03 20:09:58 +00:00
invitedContactsForNewlyCreatedGroup: getInvitedContactsForNewlyCreatedGroup(
state
),
2019-11-07 21:36:16 +00:00
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
2021-04-21 16:31:12 +00:00
warning: getWarning(conversation, state),
contactSpoofingReview: getContactSpoofingReview(id, state),
i18n: getIntl(state),
renderItem,
renderLastSeenIndicator,
2020-05-27 21:37:06 +00:00
renderHeroRow,
renderLoadingRow,
renderTypingBubble,
...actions,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const SmartTimeline = smart(Timeline as any);