Optimize rendering
This commit is contained in:
parent
81f06e2404
commit
12c78c742f
34 changed files with 702 additions and 444 deletions
|
@ -6624,6 +6624,7 @@ button.module-image__border-overlay:focus {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
@include dark-theme {
|
@include dark-theme {
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { isNumber, noop } from 'lodash';
|
import { isNumber, noop } from 'lodash';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { render } from 'react-dom';
|
import { render, unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
import MessageReceiver from './textsecure/MessageReceiver';
|
import MessageReceiver from './textsecure/MessageReceiver';
|
||||||
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
||||||
|
@ -1241,8 +1241,10 @@ export async function startApp(): Promise<void> {
|
||||||
`${batch.length} into ${deduped.size}`
|
`${batch.length} into ${deduped.size}`
|
||||||
);
|
);
|
||||||
|
|
||||||
deduped.forEach(conversation => {
|
batchedUpdates(() => {
|
||||||
conversationChanged(conversation.id, conversation.format());
|
deduped.forEach(conversation => {
|
||||||
|
conversationChanged(conversation.id, conversation.format());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,6 @@ export const AnnouncementsOnlyGroupBanner = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openConversation(admin.id);
|
openConversation(admin.id);
|
||||||
}}
|
}}
|
||||||
// Required by the component but unecessary for us
|
|
||||||
style={{}}
|
|
||||||
// We don't want these values to show
|
|
||||||
draftPreview=""
|
draftPreview=""
|
||||||
lastMessage={undefined}
|
lastMessage={undefined}
|
||||||
lastUpdated={undefined}
|
lastUpdated={undefined}
|
||||||
|
|
|
@ -54,7 +54,7 @@ const createProps = (rows: ReadonlyArray<Row>): PropsType => ({
|
||||||
onSelectConversation: action('onSelectConversation'),
|
onSelectConversation: action('onSelectConversation'),
|
||||||
onClickArchiveButton: action('onClickArchiveButton'),
|
onClickArchiveButton: action('onClickArchiveButton'),
|
||||||
onClickContactCheckbox: action('onClickContactCheckbox'),
|
onClickContactCheckbox: action('onClickContactCheckbox'),
|
||||||
renderMessageSearchResult: (id: string, style: React.CSSProperties) => (
|
renderMessageSearchResult: (id: string) => (
|
||||||
<MessageSearchResult
|
<MessageSearchResult
|
||||||
body="Lorem ipsum wow"
|
body="Lorem ipsum wow"
|
||||||
bodyRanges={[]}
|
bodyRanges={[]}
|
||||||
|
@ -65,7 +65,6 @@ const createProps = (rows: ReadonlyArray<Row>): PropsType => ({
|
||||||
openConversationInternal={action('openConversationInternal')}
|
openConversationInternal={action('openConversationInternal')}
|
||||||
sentAt={1587358800000}
|
sentAt={1587358800000}
|
||||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||||
style={style}
|
|
||||||
to={defaultConversations[1]}
|
to={defaultConversations[1]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -288,6 +287,7 @@ story.add('Contact checkboxes: disabled', () => (
|
||||||
story.add('Conversation: Typing Status', () =>
|
story.add('Conversation: Typing Status', () =>
|
||||||
renderConversation({
|
renderConversation({
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone Here',
|
name: 'Someone Here',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useRef, useEffect, useCallback, CSSProperties } from 'react';
|
import React, { useRef, useEffect, useCallback, ReactNode } from 'react';
|
||||||
import { List, ListRowRenderer } from 'react-virtualized';
|
import { List, ListRowRenderer } from 'react-virtualized';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
|
@ -128,7 +129,7 @@ export type PropsType = {
|
||||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||||
) => void;
|
) => void;
|
||||||
onSelectConversation: (conversationId: string, messageId?: string) => void;
|
onSelectConversation: (conversationId: string, messageId?: string) => void;
|
||||||
renderMessageSearchResult: (id: string, style: CSSProperties) => JSX.Element;
|
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||||
showChooseGroupMembers: () => void;
|
showChooseGroupMembers: () => void;
|
||||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||||
};
|
};
|
||||||
|
@ -187,13 +188,12 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
return <div key={key} style={style} />;
|
return <div key={key} style={style} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result: ReactNode;
|
||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
case RowType.ArchiveButton:
|
case RowType.ArchiveButton:
|
||||||
return (
|
result = (
|
||||||
<button
|
<button
|
||||||
key={key}
|
|
||||||
className="module-conversation-list__item--archive-button"
|
className="module-conversation-list__item--archive-button"
|
||||||
style={style}
|
|
||||||
onClick={onClickArchiveButton}
|
onClick={onClickArchiveButton}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -203,90 +203,108 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case RowType.Blank:
|
case RowType.Blank:
|
||||||
return <div key={key} style={style} />;
|
result = <></>;
|
||||||
|
break;
|
||||||
case RowType.Contact: {
|
case RowType.Contact: {
|
||||||
const { isClickable = true } = row;
|
const { isClickable = true } = row;
|
||||||
return (
|
result = (
|
||||||
<ContactListItem
|
<ContactListItem
|
||||||
{...row.contact}
|
{...row.contact}
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
onClick={isClickable ? onSelectConversation : undefined}
|
onClick={isClickable ? onSelectConversation : undefined}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case RowType.ContactCheckbox:
|
case RowType.ContactCheckbox:
|
||||||
return (
|
result = (
|
||||||
<ContactCheckboxComponent
|
<ContactCheckboxComponent
|
||||||
{...row.contact}
|
{...row.contact}
|
||||||
isChecked={row.isChecked}
|
isChecked={row.isChecked}
|
||||||
disabledReason={row.disabledReason}
|
disabledReason={row.disabledReason}
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
onClick={onClickContactCheckbox}
|
onClick={onClickContactCheckbox}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case RowType.Conversation:
|
break;
|
||||||
return (
|
case RowType.Conversation: {
|
||||||
|
const itemProps = pick(row.conversation, [
|
||||||
|
'acceptedMessageRequest',
|
||||||
|
'avatarPath',
|
||||||
|
'color',
|
||||||
|
'draftPreview',
|
||||||
|
'id',
|
||||||
|
'isMe',
|
||||||
|
'isSelected',
|
||||||
|
'lastMessage',
|
||||||
|
'lastUpdated',
|
||||||
|
'markedUnread',
|
||||||
|
'muteExpiresAt',
|
||||||
|
'name',
|
||||||
|
'phoneNumber',
|
||||||
|
'profileName',
|
||||||
|
'sharedGroupNames',
|
||||||
|
'shouldShowDraft',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'typingContact',
|
||||||
|
'unblurredAvatarPath',
|
||||||
|
'unreadCount',
|
||||||
|
]);
|
||||||
|
result = (
|
||||||
<ConversationListItem
|
<ConversationListItem
|
||||||
{...row.conversation}
|
{...itemProps}
|
||||||
key={key}
|
key={key}
|
||||||
style={style}
|
|
||||||
onClick={onSelectConversation}
|
onClick={onSelectConversation}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case RowType.CreateNewGroup:
|
case RowType.CreateNewGroup:
|
||||||
return (
|
result = (
|
||||||
<CreateNewGroupButton
|
<CreateNewGroupButton
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
key={key}
|
|
||||||
onClick={showChooseGroupMembers}
|
onClick={showChooseGroupMembers}
|
||||||
style={style}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case RowType.Header:
|
case RowType.Header:
|
||||||
return (
|
result = (
|
||||||
<div
|
<div className="module-conversation-list__item--header">
|
||||||
className="module-conversation-list__item--header"
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{i18n(row.i18nKey)}
|
{i18n(row.i18nKey)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case RowType.MessageSearchResult:
|
case RowType.MessageSearchResult:
|
||||||
return (
|
result = <>{renderMessageSearchResult(row.messageId)}</>;
|
||||||
<React.Fragment key={key}>
|
break;
|
||||||
{renderMessageSearchResult(row.messageId, style)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
case RowType.SearchResultsLoadingFakeHeader:
|
case RowType.SearchResultsLoadingFakeHeader:
|
||||||
return (
|
result = <SearchResultsLoadingFakeHeaderComponent />;
|
||||||
<SearchResultsLoadingFakeHeaderComponent key={key} style={style} />
|
break;
|
||||||
);
|
|
||||||
case RowType.SearchResultsLoadingFakeRow:
|
case RowType.SearchResultsLoadingFakeRow:
|
||||||
return (
|
result = <SearchResultsLoadingFakeRowComponent />;
|
||||||
<SearchResultsLoadingFakeRowComponent key={key} style={style} />
|
break;
|
||||||
);
|
|
||||||
case RowType.StartNewConversation:
|
case RowType.StartNewConversation:
|
||||||
return (
|
result = (
|
||||||
<StartNewConversationComponent
|
<StartNewConversationComponent
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
key={key}
|
|
||||||
phoneNumber={row.phoneNumber}
|
phoneNumber={row.phoneNumber}
|
||||||
onClick={() => {
|
onClick={startNewConversationFromPhoneNumber}
|
||||||
startNewConversationFromPhoneNumber(row.phoneNumber);
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(row);
|
throw missingCaseError(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={style} key={key}>
|
||||||
|
{result}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
getRow,
|
getRow,
|
||||||
|
|
|
@ -98,7 +98,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
setChallengeStatus: action('setChallengeStatus'),
|
setChallengeStatus: action('setChallengeStatus'),
|
||||||
renderExpiredBuildDialog: () => <div />,
|
renderExpiredBuildDialog: () => <div />,
|
||||||
renderMainHeader: () => <div />,
|
renderMainHeader: () => <div />,
|
||||||
renderMessageSearchResult: (id: string, style: React.CSSProperties) => (
|
renderMessageSearchResult: (id: string) => (
|
||||||
<MessageSearchResult
|
<MessageSearchResult
|
||||||
body="Lorem ipsum wow"
|
body="Lorem ipsum wow"
|
||||||
bodyRanges={[]}
|
bodyRanges={[]}
|
||||||
|
@ -109,7 +109,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
openConversationInternal={action('openConversationInternal')}
|
openConversationInternal={action('openConversationInternal')}
|
||||||
sentAt={1587358800000}
|
sentAt={1587358800000}
|
||||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||||
style={style}
|
|
||||||
to={defaultConversations[1]}
|
to={defaultConversations[1]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect, useMemo, CSSProperties } from 'react';
|
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||||
import Measure, { MeasuredComponentProps } from 'react-measure';
|
import Measure, { MeasuredComponentProps } from 'react-measure';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ export type PropsType = {
|
||||||
// Render Props
|
// Render Props
|
||||||
renderExpiredBuildDialog: () => JSX.Element;
|
renderExpiredBuildDialog: () => JSX.Element;
|
||||||
renderMainHeader: () => JSX.Element;
|
renderMainHeader: () => JSX.Element;
|
||||||
renderMessageSearchResult: (id: string, style: CSSProperties) => JSX.Element;
|
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||||
renderNetworkStatus: () => JSX.Element;
|
renderNetworkStatus: () => JSX.Element;
|
||||||
renderRelinkDialog: () => JSX.Element;
|
renderRelinkDialog: () => JSX.Element;
|
||||||
renderUpdateDialog: () => JSX.Element;
|
renderUpdateDialog: () => JSX.Element;
|
||||||
|
@ -376,6 +376,17 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
|
|
||||||
const getRow = useMemo(() => helper.getRow.bind(helper), [helper]);
|
const getRow = useMemo(() => helper.getRow.bind(helper), [helper]);
|
||||||
|
|
||||||
|
const onSelectConversation = useCallback(
|
||||||
|
(conversationId: string, messageId?: string) => {
|
||||||
|
openConversationInternal({
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
switchToAssociatedView: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[openConversationInternal]
|
||||||
|
);
|
||||||
|
|
||||||
const previousSelectedConversationId = usePrevious(
|
const previousSelectedConversationId = usePrevious(
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
selectedConversationId
|
selectedConversationId
|
||||||
|
@ -458,16 +469,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
throw missingCaseError(disabledReason);
|
throw missingCaseError(disabledReason);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSelectConversation={(
|
onSelectConversation={onSelectConversation}
|
||||||
conversationId: string,
|
|
||||||
messageId?: string
|
|
||||||
) => {
|
|
||||||
openConversationInternal({
|
|
||||||
conversationId,
|
|
||||||
messageId,
|
|
||||||
switchToAssociatedView: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
renderMessageSearchResult={renderMessageSearchResult}
|
renderMessageSearchResult={renderMessageSearchResult}
|
||||||
rowCount={helper.getRowCount()}
|
rowCount={helper.getRowCount()}
|
||||||
scrollBehavior={scrollBehavior}
|
scrollBehavior={scrollBehavior}
|
||||||
|
|
|
@ -271,7 +271,7 @@ type State = {
|
||||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||||
const EXPIRED_DELAY = 600;
|
const EXPIRED_DELAY = 600;
|
||||||
|
|
||||||
export class Message extends React.Component<Props, State> {
|
export class Message extends React.PureComponent<Props, State> {
|
||||||
public menuTriggerRef: Trigger | undefined;
|
public menuTriggerRef: Trigger | undefined;
|
||||||
|
|
||||||
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
|
@ -375,7 +375,6 @@ const renderItem = (id: string) => (
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode="keyboard"
|
interactionMode="keyboard"
|
||||||
conversationId=""
|
conversationId=""
|
||||||
conversationAccepted
|
|
||||||
renderContact={() => '*ContactName*'}
|
renderContact={() => '*ContactName*'}
|
||||||
renderUniversalTimerNotification={() => (
|
renderUniversalTimerNotification={() => (
|
||||||
<div>*UniversalTimerNotification*</div>
|
<div>*UniversalTimerNotification*</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { debounce, get, isNumber } from 'lodash';
|
import { debounce, get, isNumber, pick } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { CSSProperties, ReactChild, ReactNode } from 'react';
|
import React, { CSSProperties, ReactChild, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
|
@ -15,12 +15,14 @@ import Measure from 'react-measure';
|
||||||
|
|
||||||
import { ScrollDownButton } from './ScrollDownButton';
|
import { ScrollDownButton } from './ScrollDownButton';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { AssertProps, LocalizerType } from '../../types/Util';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
|
||||||
import { PropsActions as MessageActionsType } from './Message';
|
import { PropsActions as MessageActionsType } from './Message';
|
||||||
|
import { PropsActions as UnsupportedMessageActionsType } from './UnsupportedMessage';
|
||||||
|
import { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification';
|
||||||
import { ErrorBoundary } from './ErrorBoundary';
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
|
@ -104,7 +106,7 @@ type PropsHousekeepingType = {
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
onHeightChange: (messageId: string) => unknown,
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actions: Record<string, unknown>
|
actions: PropsActionsType
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
renderHeroRow: (
|
renderHeroRow: (
|
||||||
|
@ -117,7 +119,7 @@ type PropsHousekeepingType = {
|
||||||
renderTypingBubble: (id: string) => JSX.Element;
|
renderTypingBubble: (id: string) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
acknowledgeGroupMemberNameCollisions: (
|
acknowledgeGroupMemberNameCollisions: (
|
||||||
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||||
) => void;
|
) => void;
|
||||||
|
@ -152,7 +154,9 @@ type PropsActionsType = {
|
||||||
unblurAvatar: () => void;
|
unblurAvatar: () => void;
|
||||||
updateSharedGroups: () => unknown;
|
updateSharedGroups: () => unknown;
|
||||||
} & MessageActionsType &
|
} & MessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType &
|
||||||
|
UnsupportedMessageActionsType &
|
||||||
|
ChatSessionRefreshedNotificationActionsType;
|
||||||
|
|
||||||
export type PropsType = PropsDataType &
|
export type PropsType = PropsDataType &
|
||||||
PropsHousekeepingType &
|
PropsHousekeepingType &
|
||||||
|
@ -721,6 +725,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const messageId = items[itemIndex];
|
const messageId = items[itemIndex];
|
||||||
|
|
||||||
rowContents = (
|
rowContents = (
|
||||||
<div
|
<div
|
||||||
id={messageId}
|
id={messageId}
|
||||||
|
@ -730,7 +735,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
role="row"
|
role="row"
|
||||||
>
|
>
|
||||||
<ErrorBoundary i18n={i18n} showDebugLog={() => window.showDebugLog()}>
|
<ErrorBoundary i18n={i18n} showDebugLog={() => window.showDebugLog()}>
|
||||||
{renderItem(messageId, id, this.resizeMessage, this.props)}
|
{renderItem(messageId, id, this.resizeMessage, this.getActions())}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1474,4 +1479,65 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
throw missingCaseError(warning);
|
throw missingCaseError(warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getActions(): PropsActionsType {
|
||||||
|
const unsafe = pick(this.props, [
|
||||||
|
'acknowledgeGroupMemberNameCollisions',
|
||||||
|
'clearChangedMessages',
|
||||||
|
'clearInvitedConversationsForNewlyCreatedGroup',
|
||||||
|
'closeContactSpoofingReview',
|
||||||
|
'setLoadCountdownStart',
|
||||||
|
'setIsNearBottom',
|
||||||
|
'reviewGroupMemberNameCollision',
|
||||||
|
'reviewMessageRequestNameCollision',
|
||||||
|
'learnMoreAboutDeliveryIssue',
|
||||||
|
'loadAndScroll',
|
||||||
|
'loadOlderMessages',
|
||||||
|
'loadNewerMessages',
|
||||||
|
'loadNewestMessages',
|
||||||
|
'markMessageRead',
|
||||||
|
'onBlock',
|
||||||
|
'onBlockAndReportSpam',
|
||||||
|
'onDelete',
|
||||||
|
'onUnblock',
|
||||||
|
'removeMember',
|
||||||
|
'selectMessage',
|
||||||
|
'clearSelectedMessage',
|
||||||
|
'unblurAvatar',
|
||||||
|
'updateSharedGroups',
|
||||||
|
|
||||||
|
'doubleCheckMissingQuoteReference',
|
||||||
|
'onHeightChange',
|
||||||
|
'checkForAccount',
|
||||||
|
'reactToMessage',
|
||||||
|
'replyToMessage',
|
||||||
|
'retrySend',
|
||||||
|
'showForwardMessageModal',
|
||||||
|
'deleteMessage',
|
||||||
|
'deleteMessageForEveryone',
|
||||||
|
'showMessageDetail',
|
||||||
|
'openConversation',
|
||||||
|
'showContactDetail',
|
||||||
|
'showContactModal',
|
||||||
|
'kickOffAttachmentDownload',
|
||||||
|
'markAttachmentAsCorrupted',
|
||||||
|
'showVisualAttachment',
|
||||||
|
'downloadAttachment',
|
||||||
|
'displayTapToViewMessage',
|
||||||
|
'openLink',
|
||||||
|
'scrollToQuotedMessage',
|
||||||
|
'showExpiredIncomingTapToViewToast',
|
||||||
|
'showExpiredOutgoingTapToViewToast',
|
||||||
|
|
||||||
|
'showIdentity',
|
||||||
|
|
||||||
|
'downloadNewVersion',
|
||||||
|
|
||||||
|
'contactSupport',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const safe: AssertProps<PropsActionsType, typeof unsafe> = unsafe;
|
||||||
|
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@ const renderUniversalTimerNotification = () => (
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationAccepted: true,
|
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
interactionMode: 'keyboard' as const,
|
interactionMode: 'keyboard' as const,
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
import { LocalizerType, ThemeType } from '../../types/Util';
|
import { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
|
|
||||||
import { InteractionModeType } from '../../state/ducks/conversations';
|
import { InteractionModeType } from '../../state/ducks/conversations';
|
||||||
|
@ -152,7 +154,6 @@ export type TimelineItemType =
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
conversationAccepted: boolean;
|
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
id: string;
|
id: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
@ -201,7 +202,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
if (item.type === 'message') {
|
if (item.type === 'message') {
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
{...this.props}
|
{...omit(this.props, ['item'])}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { ReactNode, CSSProperties, FunctionComponent } from 'react';
|
import React, { ReactNode, FunctionComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isBoolean, isNumber } from 'lodash';
|
import { isBoolean, isNumber } from 'lodash';
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ type PropsType = {
|
||||||
messageStatusIcon?: ReactNode;
|
messageStatusIcon?: ReactNode;
|
||||||
messageText?: ReactNode;
|
messageText?: ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
style: CSSProperties;
|
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -77,7 +76,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
style,
|
|
||||||
title,
|
title,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
|
@ -198,7 +196,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
||||||
{ [`${BASE_CLASS_NAME}--is-checkbox--disabled`]: disabled }
|
{ [`${BASE_CLASS_NAME}--is-checkbox--disabled`]: disabled }
|
||||||
)}
|
)}
|
||||||
data-id={id ? cleanId(id) : undefined}
|
data-id={id ? cleanId(id) : undefined}
|
||||||
style={style}
|
|
||||||
// `onClick` is will double-fire if we're enabled. We want it to fire when we're
|
// `onClick` is will double-fire if we're enabled. We want it to fire when we're
|
||||||
// disabled so we can show any "can't add contact" modals, etc. This won't
|
// disabled so we can show any "can't add contact" modals, etc. This won't
|
||||||
// work for keyboard users, though, because labels are not tabbable.
|
// work for keyboard users, though, because labels are not tabbable.
|
||||||
|
@ -219,7 +216,6 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
||||||
data-id={id ? cleanId(id) : undefined}
|
data-id={id ? cleanId(id) : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={style}
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{contents}
|
{contents}
|
||||||
|
@ -228,11 +224,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={commonClassNames} data-id={id ? cleanId(id) : undefined}>
|
||||||
className={commonClassNames}
|
|
||||||
data-id={id ? cleanId(id) : undefined}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent, ReactNode } from 'react';
|
import React, { FunctionComponent, ReactNode } from 'react';
|
||||||
|
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
@ -38,7 +38,6 @@ export type PropsDataType = {
|
||||||
|
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
style: CSSProperties;
|
|
||||||
onClick: (
|
onClick: (
|
||||||
id: string,
|
id: string,
|
||||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||||
|
@ -63,7 +62,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
style,
|
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
|
@ -114,7 +112,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
sharedGroupNames={sharedGroupNames}
|
sharedGroupNames={sharedGroupNames}
|
||||||
style={style}
|
|
||||||
title={title}
|
title={title}
|
||||||
unblurredAvatarPath={unblurredAvatarPath}
|
unblurredAvatarPath={unblurredAvatarPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
@ -28,7 +28,6 @@ export type PropsDataType = Pick<
|
||||||
|
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
style: CSSProperties;
|
|
||||||
onClick?: (id: string) => void;
|
onClick?: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,7 +47,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
style,
|
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
|
@ -85,7 +83,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
sharedGroupNames={sharedGroupNames}
|
sharedGroupNames={sharedGroupNames}
|
||||||
style={style}
|
|
||||||
title={title}
|
title={title}
|
||||||
unblurredAvatarPath={unblurredAvatarPath}
|
unblurredAvatarPath={unblurredAvatarPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, FunctionComponent, ReactNode } from 'react';
|
||||||
useCallback,
|
|
||||||
CSSProperties,
|
|
||||||
FunctionComponent,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -62,7 +57,6 @@ export type PropsData = Pick<
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
style: CSSProperties;
|
|
||||||
onClick: (id: string) => void;
|
onClick: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,7 +82,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
shouldShowDraft,
|
shouldShowDraft,
|
||||||
style,
|
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
typingContact,
|
typingContact,
|
||||||
|
@ -197,7 +190,6 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
sharedGroupNames={sharedGroupNames}
|
sharedGroupNames={sharedGroupNames}
|
||||||
style={style}
|
|
||||||
title={title}
|
title={title}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
unblurredAvatarPath={unblurredAvatarPath}
|
unblurredAvatarPath={unblurredAvatarPath}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
@ -9,11 +9,10 @@ import { LocalizerType } from '../../types/Util';
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
style: CSSProperties;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
||||||
({ i18n, onClick, style }) => {
|
({ i18n, onClick }) => {
|
||||||
const title = i18n('createNewGroupButton');
|
const title = i18n('createNewGroupButton');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,7 +25,6 @@ export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
style={style}
|
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -55,7 +55,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
'isSearchingInConversation',
|
'isSearchingInConversation',
|
||||||
overrideProps.isSearchingInConversation || false
|
overrideProps.isSearchingInConversation || false
|
||||||
),
|
),
|
||||||
style: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Default', () => {
|
story.add('Default', () => {
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, FunctionComponent, ReactNode } from 'react';
|
||||||
useCallback,
|
|
||||||
CSSProperties,
|
|
||||||
FunctionComponent,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
import { escapeRegExp } from 'lodash';
|
import { escapeRegExp } from 'lodash';
|
||||||
|
|
||||||
import { MessageBodyHighlight } from './MessageBodyHighlight';
|
import { MessageBodyHighlight } from './MessageBodyHighlight';
|
||||||
|
@ -59,7 +54,6 @@ type PropsHousekeepingType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
style: CSSProperties;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsType = PropsDataType & PropsHousekeepingType;
|
export type PropsType = PropsDataType & PropsHousekeepingType;
|
||||||
|
@ -159,7 +153,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
sentAt,
|
sentAt,
|
||||||
snippet,
|
snippet,
|
||||||
style,
|
|
||||||
to,
|
to,
|
||||||
}) => {
|
}) => {
|
||||||
const onClickItem = useCallback(() => {
|
const onClickItem = useCallback(() => {
|
||||||
|
@ -167,7 +160,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||||
}, [openConversationInternal, conversationId, id]);
|
}, [openConversationInternal, conversationId, id]);
|
||||||
|
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
return <div style={style} />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNoteToSelf = from.isMe && to.isMe;
|
const isNoteToSelf = from.isMe && to.isMe;
|
||||||
|
@ -213,7 +206,6 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber={from.phoneNumber}
|
phoneNumber={from.phoneNumber}
|
||||||
profileName={from.profileName}
|
profileName={from.profileName}
|
||||||
sharedGroupNames={from.sharedGroupNames}
|
sharedGroupNames={from.sharedGroupNames}
|
||||||
style={style}
|
|
||||||
title={from.title}
|
title={from.title}
|
||||||
unblurredAvatarPath={from.unblurredAvatarPath}
|
unblurredAvatarPath={from.unblurredAvatarPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = Record<string, never>;
|
||||||
style: CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchResultsLoadingFakeHeader: FunctionComponent<PropsType> = ({
|
export const SearchResultsLoadingFakeHeader: FunctionComponent<PropsType> = () => (
|
||||||
style,
|
<div className="module-SearchResultsLoadingFakeHeader" />
|
||||||
}) => <div className="module-SearchResultsLoadingFakeHeader" style={style} />;
|
);
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = Record<string, never>;
|
||||||
style: CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchResultsLoadingFakeRow: FunctionComponent<PropsType> = ({
|
export const SearchResultsLoadingFakeRow: FunctionComponent<PropsType> = () => (
|
||||||
style,
|
<div className="module-SearchResultsLoadingFakeRow">
|
||||||
}) => (
|
|
||||||
<div className="module-SearchResultsLoadingFakeRow" style={style}>
|
|
||||||
<div className="module-SearchResultsLoadingFakeRow__avatar" />
|
<div className="module-SearchResultsLoadingFakeRow__avatar" />
|
||||||
<div className="module-SearchResultsLoadingFakeRow__content">
|
<div className="module-SearchResultsLoadingFakeRow__content">
|
||||||
<div className="module-SearchResultsLoadingFakeRow__content__header" />
|
<div className="module-SearchResultsLoadingFakeRow__content__header" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
import React, { FunctionComponent, useCallback } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BaseConversationListItem,
|
BaseConversationListItem,
|
||||||
|
@ -19,18 +19,21 @@ type PropsData = {
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
style: CSSProperties;
|
onClick: (phoneNumber: string) => void;
|
||||||
onClick: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
||||||
({ i18n, onClick, phoneNumber, style }) => {
|
({ i18n, onClick, phoneNumber }) => {
|
||||||
const messageText = (
|
const messageText = (
|
||||||
<div className={TEXT_CLASS_NAME}>{i18n('startConversation')}</div>
|
<div className={TEXT_CLASS_NAME}>{i18n('startConversation')}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const boundOnClick = useCallback(() => {
|
||||||
|
onClick(phoneNumber);
|
||||||
|
}, [onClick, phoneNumber]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseConversationListItem
|
<BaseConversationListItem
|
||||||
acceptedMessageRequest={false}
|
acceptedMessageRequest={false}
|
||||||
|
@ -41,10 +44,9 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
||||||
isMe={false}
|
isMe={false}
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
messageText={messageText}
|
messageText={messageText}
|
||||||
onClick={onClick}
|
onClick={boundOnClick}
|
||||||
phoneNumber={phoneNumber}
|
phoneNumber={phoneNumber}
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
style={style}
|
|
||||||
title={phoneNumber}
|
title={phoneNumber}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -357,21 +357,18 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return {
|
return {
|
||||||
sentAt: this.get('sent_at'),
|
sentAt: this.get('sent_at'),
|
||||||
receivedAt: this.getReceivedAt(),
|
receivedAt: this.getReceivedAt(),
|
||||||
message: getPropsForMessage(
|
message: getPropsForMessage(this.attributes, {
|
||||||
this.attributes,
|
conversationSelector: findAndFormatContact,
|
||||||
findAndFormatContact,
|
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
window.textsecure.storage.user.getNumber(),
|
ourNumber: window.textsecure.storage.user.getNumber(),
|
||||||
this.OUR_UUID,
|
ourUuid: this.OUR_UUID,
|
||||||
undefined,
|
regionCode: window.storage.get('regionCode', 'ZZ'),
|
||||||
undefined,
|
accountSelector: (identifier?: string) => {
|
||||||
window.storage.get('regionCode', 'ZZ'),
|
|
||||||
(identifier?: string) => {
|
|
||||||
const state = window.reduxStore.getState();
|
const state = window.reduxStore.getState();
|
||||||
const accountSelector = getAccountSelector(state);
|
const accountSelector = getAccountSelector(state);
|
||||||
return accountSelector(identifier);
|
return accountSelector(identifier);
|
||||||
}
|
},
|
||||||
),
|
}),
|
||||||
errors,
|
errors,
|
||||||
contacts,
|
contacts,
|
||||||
};
|
};
|
||||||
|
@ -621,12 +618,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
if (isCallHistory(attributes)) {
|
if (isCallHistory(attributes)) {
|
||||||
const state = window.reduxStore.getState();
|
const state = window.reduxStore.getState();
|
||||||
const callingNotification = getPropsForCallHistory(
|
const callingNotification = getPropsForCallHistory(attributes, {
|
||||||
attributes,
|
conversationSelector: findAndFormatContact,
|
||||||
findAndFormatContact,
|
callSelector: getCallSelector(state),
|
||||||
getCallSelector(state),
|
activeCall: getActiveCall(state),
|
||||||
getActiveCall(state)
|
});
|
||||||
);
|
|
||||||
if (callingNotification) {
|
if (callingNotification) {
|
||||||
return {
|
return {
|
||||||
text: getCallingNotificationText(callingNotification, window.i18n),
|
text: getCallingNotificationText(callingNotification, window.i18n),
|
||||||
|
@ -679,10 +675,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const body = (this.get('body') || '').trim();
|
const body = (this.get('body') || '').trim();
|
||||||
const { attributes } = this;
|
const { attributes } = this;
|
||||||
|
|
||||||
const bodyRanges = processBodyRanges(
|
const bodyRanges = processBodyRanges(attributes, {
|
||||||
attributes.bodyRanges,
|
conversationSelector: findAndFormatContact,
|
||||||
findAndFormatContact
|
});
|
||||||
);
|
|
||||||
if (bodyRanges) {
|
if (bodyRanges) {
|
||||||
return getTextWithMentions(bodyRanges, body);
|
return getTextWithMentions(bodyRanges, body);
|
||||||
}
|
}
|
||||||
|
@ -696,10 +691,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
let modifiedText = text;
|
let modifiedText = text;
|
||||||
|
|
||||||
const bodyRanges = processBodyRanges(
|
const bodyRanges = processBodyRanges(attributes, {
|
||||||
attributes.bodyRanges,
|
conversationSelector: findAndFormatContact,
|
||||||
findAndFormatContact
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (bodyRanges && bodyRanges.length) {
|
if (bodyRanges && bodyRanges.length) {
|
||||||
modifiedText = getTextWithMentions(bodyRanges, modifiedText);
|
modifiedText = getTextWithMentions(bodyRanges, modifiedText);
|
||||||
|
|
|
@ -153,10 +153,15 @@ export type ConversationType = {
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isFetchingUUID?: boolean;
|
isFetchingUUID?: boolean;
|
||||||
typingContact?: {
|
typingContact?: {
|
||||||
|
acceptedMessageRequest: boolean;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
color?: AvatarColorType;
|
||||||
|
isMe: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
sharedGroupNames: Array<string>;
|
||||||
|
title: string;
|
||||||
} | null;
|
} | null;
|
||||||
recentMediaItems?: Array<MediaItemType>;
|
recentMediaItems?: Array<MediaItemType>;
|
||||||
profileSharing?: boolean;
|
profileSharing?: boolean;
|
||||||
|
|
|
@ -712,19 +712,18 @@ export const getMessageSelector = createSelector(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageSelector(
|
return messageSelector(message, {
|
||||||
message,
|
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
regionCode,
|
regionCode,
|
||||||
selectedMessage ? selectedMessage.id : undefined,
|
selectedMessageId: selectedMessage?.id,
|
||||||
selectedMessage ? selectedMessage.counter : undefined,
|
selectedMessageCounter: selectedMessage?.counter,
|
||||||
callSelector,
|
callSelector,
|
||||||
activeCall,
|
activeCall,
|
||||||
accountSelector
|
accountSelector,
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isNumber, isObject, map, omit, reduce } from 'lodash';
|
import {
|
||||||
|
identity,
|
||||||
|
isEqual,
|
||||||
|
isNumber,
|
||||||
|
isObject,
|
||||||
|
map,
|
||||||
|
omit,
|
||||||
|
pick,
|
||||||
|
reduce,
|
||||||
|
} from 'lodash';
|
||||||
|
import { createSelector, createSelectorCreator } from 'reselect';
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +39,7 @@ import { QuotedAttachmentType } from '../../components/conversation/Quote';
|
||||||
import { getDomain, isStickerPack } from '../../types/LinkPreview';
|
import { getDomain, isStickerPack } from '../../types/LinkPreview';
|
||||||
|
|
||||||
import { ContactType, contactSelector } from '../../types/Contact';
|
import { ContactType, contactSelector } from '../../types/Contact';
|
||||||
import { BodyRangesType } from '../../types/Util';
|
import { AssertProps, BodyRangesType } from '../../types/Util';
|
||||||
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||||
import { ConversationColors } from '../../types/Colors';
|
import { ConversationColors } from '../../types/Colors';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
|
@ -37,6 +47,7 @@ import { SignalService as Proto } from '../../protobuf';
|
||||||
import { AttachmentType, isVoiceMessage } from '../../types/Attachment';
|
import { AttachmentType, isVoiceMessage } from '../../types/Attachment';
|
||||||
|
|
||||||
import { CallingNotificationType } from '../../util/callingNotification';
|
import { CallingNotificationType } from '../../util/callingNotification';
|
||||||
|
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { isNotNil } from '../../util/isNotNil';
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
import { isMoreRecentThan } from '../../util/timestamp';
|
import { isMoreRecentThan } from '../../util/timestamp';
|
||||||
|
@ -78,46 +89,40 @@ type PropsForUnsupportedMessage = {
|
||||||
contact: FormattedContact;
|
contact: FormattedContact;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetPropsForBubbleOptions = Readonly<{
|
||||||
|
conversationSelector: GetConversationByIdType;
|
||||||
|
ourConversationId: string;
|
||||||
|
ourNumber?: string;
|
||||||
|
ourUuid?: string;
|
||||||
|
selectedMessageId?: string;
|
||||||
|
selectedMessageCounter?: number;
|
||||||
|
regionCode: string;
|
||||||
|
callSelector: CallSelectorType;
|
||||||
|
activeCall?: CallStateType;
|
||||||
|
accountSelector: (identifier?: string) => boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
// Top-level prop generation for the message bubble
|
// Top-level prop generation for the message bubble
|
||||||
export function getPropsForBubble(
|
export function getPropsForBubble(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
options: GetPropsForBubbleOptions
|
||||||
ourConversationId: string,
|
|
||||||
ourNumber: string | undefined,
|
|
||||||
ourUuid: string | undefined,
|
|
||||||
regionCode: string,
|
|
||||||
selectedMessageId: string | undefined,
|
|
||||||
selectedMessageCounter: number | undefined,
|
|
||||||
callSelector: CallSelectorType,
|
|
||||||
activeCall: CallStateType | undefined,
|
|
||||||
accountSelector: (identifier?: string) => boolean
|
|
||||||
): TimelineItemType {
|
): TimelineItemType {
|
||||||
if (isUnsupportedMessage(message)) {
|
if (isUnsupportedMessage(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'unsupportedMessage',
|
type: 'unsupportedMessage',
|
||||||
data: getPropsForUnsupportedMessage(
|
data: getPropsForUnsupportedMessage(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isGroupV2Change(message)) {
|
if (isGroupV2Change(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'groupV2Change',
|
type: 'groupV2Change',
|
||||||
data: getPropsForGroupV2Change(
|
data: getPropsForGroupV2Change(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isGroupV1Migration(message)) {
|
if (isGroupV1Migration(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'groupV1Migration',
|
type: 'groupV1Migration',
|
||||||
data: getPropsForGroupV1Migration(message, conversationSelector),
|
data: getPropsForGroupV1Migration(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isMessageHistoryUnsynced(message)) {
|
if (isMessageHistoryUnsynced(message)) {
|
||||||
|
@ -129,35 +134,25 @@ export function getPropsForBubble(
|
||||||
if (isExpirationTimerUpdate(message)) {
|
if (isExpirationTimerUpdate(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'timerNotification',
|
type: 'timerNotification',
|
||||||
data: getPropsForTimerNotification(
|
data: getPropsForTimerNotification(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isKeyChange(message)) {
|
if (isKeyChange(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'safetyNumberNotification',
|
type: 'safetyNumberNotification',
|
||||||
data: getPropsForSafetyNumberNotification(message, conversationSelector),
|
data: getPropsForSafetyNumberNotification(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isVerifiedChange(message)) {
|
if (isVerifiedChange(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'verificationNotification',
|
type: 'verificationNotification',
|
||||||
data: getPropsForVerificationNotification(message, conversationSelector),
|
data: getPropsForVerificationNotification(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isGroupUpdate(message)) {
|
if (isGroupUpdate(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'groupNotification',
|
type: 'groupNotification',
|
||||||
data: getPropsForGroupNotification(
|
data: getPropsForGroupNotification(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isEndSession(message)) {
|
if (isEndSession(message)) {
|
||||||
|
@ -169,18 +164,13 @@ export function getPropsForBubble(
|
||||||
if (isCallHistory(message)) {
|
if (isCallHistory(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: getPropsForCallHistory(
|
data: getPropsForCallHistory(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
callSelector,
|
|
||||||
activeCall
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isProfileChange(message)) {
|
if (isProfileChange(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'profileChange',
|
type: 'profileChange',
|
||||||
data: getPropsForProfileChange(message, conversationSelector),
|
data: getPropsForProfileChange(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isUniversalTimerNotification(message)) {
|
if (isUniversalTimerNotification(message)) {
|
||||||
|
@ -192,7 +182,7 @@ export function getPropsForBubble(
|
||||||
if (isChangeNumberNotification(message)) {
|
if (isChangeNumberNotification(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'changeNumberNotification',
|
type: 'changeNumberNotification',
|
||||||
data: getPropsForChangeNumberNotification(message, conversationSelector),
|
data: getPropsForChangeNumberNotification(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isChatSessionRefreshed(message)) {
|
if (isChatSessionRefreshed(message)) {
|
||||||
|
@ -204,23 +194,13 @@ export function getPropsForBubble(
|
||||||
if (isDeliveryIssue(message)) {
|
if (isDeliveryIssue(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'deliveryIssue',
|
type: 'deliveryIssue',
|
||||||
data: getPropsForDeliveryIssue(message, conversationSelector),
|
data: getPropsForDeliveryIssue(message, options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: getPropsForMessage(
|
data: getPropsForMessage(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid,
|
|
||||||
selectedMessageId,
|
|
||||||
selectedMessageCounter,
|
|
||||||
regionCode,
|
|
||||||
accountSelector
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,12 +290,20 @@ export function getContactId(
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetContactOptions = Pick<
|
||||||
|
GetPropsForBubbleOptions,
|
||||||
|
'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourUuid'
|
||||||
|
>;
|
||||||
|
|
||||||
|
// TODO: DESKTOP-2145
|
||||||
export function getContact(
|
export function getContact(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
{
|
||||||
ourConversationId: string,
|
conversationSelector,
|
||||||
ourNumber: string | undefined,
|
ourConversationId,
|
||||||
ourUuid: string | undefined
|
ourNumber,
|
||||||
|
ourUuid,
|
||||||
|
}: GetContactOptions
|
||||||
): ConversationType {
|
): ConversationType {
|
||||||
const source = getSource(message, ourNumber);
|
const source = getSource(message, ourNumber);
|
||||||
const sourceUuid = getSourceUuid(message, ourUuid);
|
const sourceUuid = getSourceUuid(message, ourUuid);
|
||||||
|
@ -336,112 +324,257 @@ export function getConversation(
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
|
|
||||||
export function getPropsForMessage(
|
export const getAttachmentsForMessage = createSelectorCreator(memoizeByRoot)(
|
||||||
message: MessageAttributesType,
|
// `memoizeByRoot` requirement
|
||||||
conversationSelector: GetConversationByIdType,
|
identity,
|
||||||
ourConversationId: string,
|
|
||||||
ourNumber: string | undefined,
|
|
||||||
ourUuid: string | undefined,
|
|
||||||
selectedMessageId: string | undefined,
|
|
||||||
selectedMessageCounter: number | undefined,
|
|
||||||
regionCode: string,
|
|
||||||
accountSelector: (identifier?: string) => boolean
|
|
||||||
): Omit<PropsForMessage, 'renderingContext'> {
|
|
||||||
const contact = getContact(
|
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid
|
|
||||||
);
|
|
||||||
|
|
||||||
const { expireTimer, expirationStartTimestamp } = message;
|
({ sticker }: MessageAttributesType) => sticker,
|
||||||
const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
|
({ attachments }: MessageAttributesType) => attachments,
|
||||||
const expirationTimestamp =
|
(
|
||||||
expirationStartTimestamp && expirationLength
|
_: MessageAttributesType,
|
||||||
? expirationStartTimestamp + expirationLength
|
sticker: MessageAttributesType['sticker'],
|
||||||
: undefined;
|
attachments: MessageAttributesType['attachments'] = []
|
||||||
|
): Array<AttachmentType> => {
|
||||||
|
if (sticker && sticker.data) {
|
||||||
|
const { data } = sticker;
|
||||||
|
|
||||||
const conversation = getConversation(message, conversationSelector);
|
// We don't show anything if we don't have the sticker or the blurhash...
|
||||||
const isGroup = conversation.type === 'group';
|
if (!data.blurHash && (data.pending || !data.path)) {
|
||||||
const { sticker } = message;
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const isMessageTapToView = isTapToView(message);
|
return [
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
// We want to show the blurhash for stickers, not the spinner
|
||||||
|
pending: false,
|
||||||
|
url: data.path
|
||||||
|
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const reactions = (message.reactions || []).map(re => {
|
return attachments
|
||||||
const c = conversationSelector(re.fromId);
|
.filter(attachment => !attachment.error)
|
||||||
|
.map(attachment => getPropsForAttachment(attachment))
|
||||||
return {
|
.filter(isNotNil);
|
||||||
emoji: re.emoji,
|
|
||||||
timestamp: re.timestamp,
|
|
||||||
from: c,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedReaction = (
|
|
||||||
(message.reactions || []).find(re => re.fromId === ourConversationId) || {}
|
|
||||||
).emoji;
|
|
||||||
|
|
||||||
const isSelected = message.id === selectedMessageId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
attachments: getAttachmentsForMessage(message),
|
|
||||||
author: contact,
|
|
||||||
bodyRanges: processBodyRanges(message.bodyRanges, conversationSelector),
|
|
||||||
canDeleteForEveryone: canDeleteForEveryone(message),
|
|
||||||
canDownload: canDownload(message, conversationSelector),
|
|
||||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
|
||||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
|
||||||
conversationColor: conversation?.conversationColor ?? ConversationColors[0],
|
|
||||||
conversationId: message.conversationId,
|
|
||||||
conversationType: isGroup ? 'group' : 'direct',
|
|
||||||
customColor: conversation?.customColor,
|
|
||||||
deletedForEveryone: message.deletedForEveryone || false,
|
|
||||||
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
|
||||||
expirationLength,
|
|
||||||
expirationTimestamp,
|
|
||||||
id: message.id,
|
|
||||||
isBlocked: conversation.isBlocked || false,
|
|
||||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
|
||||||
isSelected,
|
|
||||||
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
|
||||||
isSticker: Boolean(sticker),
|
|
||||||
isTapToView: isMessageTapToView,
|
|
||||||
isTapToViewError:
|
|
||||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
|
||||||
isTapToViewExpired: isMessageTapToView && message.isErased,
|
|
||||||
previews: getPropsForPreview(message),
|
|
||||||
quote: getPropsForQuote(message, conversationSelector, ourConversationId),
|
|
||||||
reactions,
|
|
||||||
selectedReaction,
|
|
||||||
status: getMessagePropStatus(message, ourConversationId),
|
|
||||||
text: createNonBreakingLastSeparator(message.body),
|
|
||||||
textPending: message.bodyPending,
|
|
||||||
timestamp: message.sent_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processBodyRanges(
|
|
||||||
bodyRanges: BodyRangesType | undefined,
|
|
||||||
conversationSelector: GetConversationByIdType
|
|
||||||
): BodyRangesType | undefined {
|
|
||||||
if (!bodyRanges) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return bodyRanges
|
export const processBodyRanges = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
.filter(range => range.mentionUuid)
|
// `memoizeByRoot` requirement
|
||||||
.map(range => {
|
identity,
|
||||||
const conversation = conversationSelector(range.mentionUuid);
|
|
||||||
|
(
|
||||||
|
{ bodyRanges }: Pick<MessageAttributesType, 'bodyRanges'>,
|
||||||
|
{ conversationSelector }: { conversationSelector: GetConversationByIdType }
|
||||||
|
): BodyRangesType | undefined => {
|
||||||
|
if (!bodyRanges) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyRanges
|
||||||
|
.filter(range => range.mentionUuid)
|
||||||
|
.map(range => {
|
||||||
|
const conversation = conversationSelector(range.mentionUuid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...range,
|
||||||
|
conversationID: conversation.id,
|
||||||
|
replacementText: conversation.title,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.start - a.start);
|
||||||
|
},
|
||||||
|
(_: MessageAttributesType, ranges?: BodyRangesType) => ranges
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getAuthorForMessage = createSelectorCreator(
|
||||||
|
memoizeByRoot,
|
||||||
|
isEqual
|
||||||
|
)(
|
||||||
|
// `memoizeByRoot` requirement
|
||||||
|
identity,
|
||||||
|
|
||||||
|
(
|
||||||
|
message: MessageAttributesType,
|
||||||
|
options: GetContactOptions
|
||||||
|
): PropsData['author'] => {
|
||||||
|
const unsafe = pick(getContact(message, options), [
|
||||||
|
'acceptedMessageRequest',
|
||||||
|
'avatarPath',
|
||||||
|
'color',
|
||||||
|
'id',
|
||||||
|
'isMe',
|
||||||
|
'name',
|
||||||
|
'phoneNumber',
|
||||||
|
'profileName',
|
||||||
|
'sharedGroupNames',
|
||||||
|
'title',
|
||||||
|
'unblurredAvatarPath',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;
|
||||||
|
|
||||||
|
return safe;
|
||||||
|
},
|
||||||
|
(_: MessageAttributesType, author: PropsData['author']) => author
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getPreviewsForMessage = createSelectorCreator(memoizeByRoot)(
|
||||||
|
// `memoizeByRoot` requirement
|
||||||
|
identity,
|
||||||
|
|
||||||
|
({ preview }: MessageAttributesType) => preview,
|
||||||
|
|
||||||
|
(
|
||||||
|
_: MessageAttributesType,
|
||||||
|
previews: MessageAttributesType['preview'] = []
|
||||||
|
): Array<LinkPreviewType> => {
|
||||||
|
return previews.map(preview => ({
|
||||||
|
...preview,
|
||||||
|
isStickerPack: isStickerPack(preview.url),
|
||||||
|
domain: getDomain(preview.url),
|
||||||
|
image: preview.image ? getPropsForAttachment(preview.image) : null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getReactionsForMessage = createSelectorCreator(
|
||||||
|
memoizeByRoot,
|
||||||
|
isEqual
|
||||||
|
)(
|
||||||
|
// `memoizeByRoot` requirement
|
||||||
|
identity,
|
||||||
|
|
||||||
|
(
|
||||||
|
{ reactions = [] }: MessageAttributesType,
|
||||||
|
{ conversationSelector }: { conversationSelector: GetConversationByIdType }
|
||||||
|
) => {
|
||||||
|
return reactions.map(re => {
|
||||||
|
const c = conversationSelector(re.fromId);
|
||||||
|
|
||||||
|
type From = NonNullable<PropsData['reactions']>[0]['from'];
|
||||||
|
|
||||||
|
const unsafe = pick(c, [
|
||||||
|
'acceptedMessageRequest',
|
||||||
|
'avatarPath',
|
||||||
|
'color',
|
||||||
|
'id',
|
||||||
|
'isMe',
|
||||||
|
'name',
|
||||||
|
'phoneNumber',
|
||||||
|
'profileName',
|
||||||
|
'sharedGroupNames',
|
||||||
|
'title',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const from: AssertProps<From, typeof unsafe> = unsafe;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...range,
|
emoji: re.emoji,
|
||||||
conversationID: conversation.id,
|
timestamp: re.timestamp,
|
||||||
replacementText: conversation.title,
|
from,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.sort((a, b) => b.start - a.start);
|
},
|
||||||
}
|
|
||||||
|
(_: MessageAttributesType, reactions: PropsData['reactions']) => reactions
|
||||||
|
);
|
||||||
|
|
||||||
|
export type GetPropsForMessageOptions = Pick<
|
||||||
|
GetPropsForBubbleOptions,
|
||||||
|
| 'conversationSelector'
|
||||||
|
| 'ourConversationId'
|
||||||
|
| 'selectedMessageId'
|
||||||
|
| 'selectedMessageCounter'
|
||||||
|
| 'regionCode'
|
||||||
|
| 'accountSelector'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const getPropsForMessage = createSelector(
|
||||||
|
(message: MessageAttributesType) => message,
|
||||||
|
getAttachmentsForMessage,
|
||||||
|
processBodyRanges,
|
||||||
|
getAuthorForMessage,
|
||||||
|
getPreviewsForMessage,
|
||||||
|
getReactionsForMessage,
|
||||||
|
(_: unknown, options: GetPropsForMessageOptions) => options,
|
||||||
|
(
|
||||||
|
message: MessageAttributesType,
|
||||||
|
attachments: Array<AttachmentType>,
|
||||||
|
bodyRanges: BodyRangesType | undefined,
|
||||||
|
author: PropsData['author'],
|
||||||
|
previews: Array<LinkPreviewType>,
|
||||||
|
reactions: PropsData['reactions'],
|
||||||
|
{
|
||||||
|
conversationSelector,
|
||||||
|
ourConversationId,
|
||||||
|
selectedMessageId,
|
||||||
|
selectedMessageCounter,
|
||||||
|
regionCode,
|
||||||
|
accountSelector,
|
||||||
|
}: GetPropsForMessageOptions
|
||||||
|
): Omit<PropsForMessage, 'renderingContext'> => {
|
||||||
|
const { expireTimer, expirationStartTimestamp } = message;
|
||||||
|
const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
|
||||||
|
const expirationTimestamp =
|
||||||
|
expirationStartTimestamp && expirationLength
|
||||||
|
? expirationStartTimestamp + expirationLength
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const conversation = getConversation(message, conversationSelector);
|
||||||
|
const isGroup = conversation.type === 'group';
|
||||||
|
const { sticker } = message;
|
||||||
|
|
||||||
|
const isMessageTapToView = isTapToView(message);
|
||||||
|
|
||||||
|
const selectedReaction = (
|
||||||
|
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
|
||||||
|
{}
|
||||||
|
).emoji;
|
||||||
|
|
||||||
|
const isSelected = message.id === selectedMessageId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
attachments,
|
||||||
|
author,
|
||||||
|
bodyRanges,
|
||||||
|
canDeleteForEveryone: canDeleteForEveryone(message),
|
||||||
|
canDownload: canDownload(message, conversationSelector),
|
||||||
|
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||||
|
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||||
|
conversationColor:
|
||||||
|
conversation?.conversationColor ?? ConversationColors[0],
|
||||||
|
conversationId: message.conversationId,
|
||||||
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
|
customColor: conversation?.customColor,
|
||||||
|
deletedForEveryone: message.deletedForEveryone || false,
|
||||||
|
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
||||||
|
expirationLength,
|
||||||
|
expirationTimestamp,
|
||||||
|
id: message.id,
|
||||||
|
isBlocked: conversation.isBlocked || false,
|
||||||
|
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||||
|
isSelected,
|
||||||
|
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
||||||
|
isSticker: Boolean(sticker),
|
||||||
|
isTapToView: isMessageTapToView,
|
||||||
|
isTapToViewError:
|
||||||
|
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||||
|
isTapToViewExpired: isMessageTapToView && message.isErased,
|
||||||
|
previews,
|
||||||
|
quote: getPropsForQuote(message, conversationSelector, ourConversationId),
|
||||||
|
reactions,
|
||||||
|
selectedReaction,
|
||||||
|
status: getMessagePropStatus(message, ourConversationId),
|
||||||
|
text: createNonBreakingLastSeparator(message.body),
|
||||||
|
textPending: message.bodyPending,
|
||||||
|
timestamp: message.sent_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Unsupported Message
|
// Unsupported Message
|
||||||
|
|
||||||
|
@ -458,10 +591,7 @@ export function isUnsupportedMessage(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForUnsupportedMessage(
|
function getPropsForUnsupportedMessage(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
options: GetContactOptions
|
||||||
ourConversationId: string,
|
|
||||||
ourNumber: string | undefined,
|
|
||||||
ourUuid: string | undefined
|
|
||||||
): PropsForUnsupportedMessage {
|
): PropsForUnsupportedMessage {
|
||||||
const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
|
const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
|
||||||
|
|
||||||
|
@ -474,13 +604,7 @@ function getPropsForUnsupportedMessage(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canProcessNow,
|
canProcessNow,
|
||||||
contact: getContact(
|
contact: getContact(message, options),
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,8 +616,7 @@ export function isGroupV2Change(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForGroupV2Change(
|
function getPropsForGroupV2Change(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
{ conversationSelector, ourConversationId }: GetPropsForBubbleOptions
|
||||||
ourConversationId: string
|
|
||||||
): GroupsV2Props {
|
): GroupsV2Props {
|
||||||
const change = message.groupV2Change;
|
const change = message.groupV2Change;
|
||||||
|
|
||||||
|
@ -518,7 +641,7 @@ export function isGroupV1Migration(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForGroupV1Migration(
|
function getPropsForGroupV1Migration(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): GroupV1MigrationPropsType {
|
): GroupV1MigrationPropsType {
|
||||||
const migration = message.groupMigration;
|
const migration = message.groupMigration;
|
||||||
if (!migration) {
|
if (!migration) {
|
||||||
|
@ -581,8 +704,7 @@ export function isExpirationTimerUpdate(
|
||||||
|
|
||||||
function getPropsForTimerNotification(
|
function getPropsForTimerNotification(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
{ ourConversationId, conversationSelector }: GetPropsForBubbleOptions
|
||||||
ourConversationId: string
|
|
||||||
): TimerNotificationProps {
|
): TimerNotificationProps {
|
||||||
const timerUpdate = message.expirationTimerUpdate;
|
const timerUpdate = message.expirationTimerUpdate;
|
||||||
if (!timerUpdate) {
|
if (!timerUpdate) {
|
||||||
|
@ -633,7 +755,7 @@ export function isKeyChange(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForSafetyNumberNotification(
|
function getPropsForSafetyNumberNotification(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): SafetyNumberNotificationProps {
|
): SafetyNumberNotificationProps {
|
||||||
const conversation = getConversation(message, conversationSelector);
|
const conversation = getConversation(message, conversationSelector);
|
||||||
const isGroup = conversation?.type === 'group';
|
const isGroup = conversation?.type === 'group';
|
||||||
|
@ -654,7 +776,7 @@ export function isVerifiedChange(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForVerificationNotification(
|
function getPropsForVerificationNotification(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): VerificationNotificationProps {
|
): VerificationNotificationProps {
|
||||||
const type = message.verified ? 'markVerified' : 'markNotVerified';
|
const type = message.verified ? 'markVerified' : 'markNotVerified';
|
||||||
const isLocal = message.local || false;
|
const isLocal = message.local || false;
|
||||||
|
@ -677,10 +799,7 @@ export function isGroupUpdate(
|
||||||
|
|
||||||
function getPropsForGroupNotification(
|
function getPropsForGroupNotification(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
options: GetContactOptions
|
||||||
ourConversationId: string,
|
|
||||||
ourNumber: string | undefined,
|
|
||||||
ourUuid: string | undefined
|
|
||||||
): GroupNotificationProps {
|
): GroupNotificationProps {
|
||||||
const groupUpdate = message.group_update;
|
const groupUpdate = message.group_update;
|
||||||
if (!groupUpdate) {
|
if (!groupUpdate) {
|
||||||
|
@ -689,6 +808,8 @@ function getPropsForGroupNotification(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { conversationSelector } = options;
|
||||||
|
|
||||||
const changes = [];
|
const changes = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -741,13 +862,7 @@ function getPropsForGroupNotification(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = getContact(
|
const from = getContact(message, options);
|
||||||
message,
|
|
||||||
conversationSelector,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from,
|
from,
|
||||||
|
@ -771,11 +886,18 @@ export function isCallHistory(message: MessageAttributesType): boolean {
|
||||||
return message.type === 'call-history';
|
return message.type === 'call-history';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetPropsForCallHistoryOptions = Pick<
|
||||||
|
GetPropsForBubbleOptions,
|
||||||
|
'conversationSelector' | 'callSelector' | 'activeCall'
|
||||||
|
>;
|
||||||
|
|
||||||
export function getPropsForCallHistory(
|
export function getPropsForCallHistory(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType,
|
{
|
||||||
callSelector: CallSelectorType,
|
conversationSelector,
|
||||||
activeCall: CallStateType | undefined
|
callSelector,
|
||||||
|
activeCall,
|
||||||
|
}: GetPropsForCallHistoryOptions
|
||||||
): CallingNotificationType {
|
): CallingNotificationType {
|
||||||
const { callHistoryDetails } = message;
|
const { callHistoryDetails } = message;
|
||||||
if (!callHistoryDetails) {
|
if (!callHistoryDetails) {
|
||||||
|
@ -833,7 +955,7 @@ export function isProfileChange(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForProfileChange(
|
function getPropsForProfileChange(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): ProfileChangeNotificationPropsType {
|
): ProfileChangeNotificationPropsType {
|
||||||
const change = message.profileChange;
|
const change = message.profileChange;
|
||||||
const { changedId } = message;
|
const { changedId } = message;
|
||||||
|
@ -869,7 +991,7 @@ export function isChangeNumberNotification(
|
||||||
|
|
||||||
function getPropsForChangeNumberNotification(
|
function getPropsForChangeNumberNotification(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): ChangeNumberNotificationProps {
|
): ChangeNumberNotificationProps {
|
||||||
return {
|
return {
|
||||||
sender: conversationSelector(message.sourceUuid),
|
sender: conversationSelector(message.sourceUuid),
|
||||||
|
@ -895,7 +1017,7 @@ export function isDeliveryIssue(message: MessageAttributesType): boolean {
|
||||||
|
|
||||||
function getPropsForDeliveryIssue(
|
function getPropsForDeliveryIssue(
|
||||||
message: MessageAttributesType,
|
message: MessageAttributesType,
|
||||||
conversationSelector: GetConversationByIdType
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
): DeliveryIssuePropsType {
|
): DeliveryIssuePropsType {
|
||||||
const sender = conversationSelector(message.sourceUuid);
|
const sender = conversationSelector(message.sourceUuid);
|
||||||
const conversation = conversationSelector(message.conversationId);
|
const conversation = conversationSelector(message.conversationId);
|
||||||
|
@ -1046,19 +1168,6 @@ export function getPropsForAttachment(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPropsForPreview(
|
|
||||||
message: MessageAttributesType
|
|
||||||
): Array<LinkPreviewType> {
|
|
||||||
const previews = message.preview || [];
|
|
||||||
|
|
||||||
return previews.map(preview => ({
|
|
||||||
...preview,
|
|
||||||
isStickerPack: isStickerPack(preview.url),
|
|
||||||
domain: getDomain(preview.url),
|
|
||||||
image: preview.image ? getPropsForAttachment(preview.image) : null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPropsForQuote(
|
export function getPropsForQuote(
|
||||||
message: Pick<MessageAttributesType, 'conversationId' | 'quote'>,
|
message: Pick<MessageAttributesType, 'conversationId' | 'quote'>,
|
||||||
conversationSelector: GetConversationByIdType,
|
conversationSelector: GetConversationByIdType,
|
||||||
|
@ -1072,7 +1181,6 @@ export function getPropsForQuote(
|
||||||
const {
|
const {
|
||||||
author,
|
author,
|
||||||
authorUuid,
|
authorUuid,
|
||||||
bodyRanges,
|
|
||||||
id: sentAt,
|
id: sentAt,
|
||||||
isViewOnce,
|
isViewOnce,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
|
@ -1097,7 +1205,7 @@ export function getPropsForQuote(
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
authorProfileName,
|
authorProfileName,
|
||||||
authorTitle,
|
authorTitle,
|
||||||
bodyRanges: processBodyRanges(bodyRanges, conversationSelector),
|
bodyRanges: processBodyRanges(quote, { conversationSelector }),
|
||||||
conversationColor: conversation.conversationColor ?? ConversationColors[0],
|
conversationColor: conversation.conversationColor ?? ConversationColors[0],
|
||||||
customColor: conversation.customColor,
|
customColor: conversation.customColor,
|
||||||
isFromMe,
|
isFromMe,
|
||||||
|
@ -1240,37 +1348,6 @@ export function canDownload(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAttachmentsForMessage(
|
|
||||||
message: MessageAttributesType
|
|
||||||
): Array<AttachmentType> {
|
|
||||||
const { sticker } = message;
|
|
||||||
if (sticker && sticker.data) {
|
|
||||||
const { data } = sticker;
|
|
||||||
|
|
||||||
// We don't show anything if we don't have the sticker or the blurhash...
|
|
||||||
if (!data.blurHash && (data.pending || !data.path)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
// We want to show the blurhash for stickers, not the spinner
|
|
||||||
pending: false,
|
|
||||||
url: data.path
|
|
||||||
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachments = message.attachments || [];
|
|
||||||
return attachments
|
|
||||||
.filter(attachment => !attachment.error)
|
|
||||||
.map(attachment => getPropsForAttachment(attachment))
|
|
||||||
.filter(isNotNil);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLastChallengeError(
|
export function getLastChallengeError(
|
||||||
message: Pick<MessageAttributesType, 'errors'>
|
message: Pick<MessageAttributesType, 'errors'>
|
||||||
): ShallowChallengeError | undefined {
|
): ShallowChallengeError | undefined {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { CSSProperties } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
@ -59,11 +59,8 @@ function renderExpiredBuildDialog(): JSX.Element {
|
||||||
function renderMainHeader(): JSX.Element {
|
function renderMainHeader(): JSX.Element {
|
||||||
return <SmartMainHeader />;
|
return <SmartMainHeader />;
|
||||||
}
|
}
|
||||||
function renderMessageSearchResult(
|
function renderMessageSearchResult(id: string): JSX.Element {
|
||||||
id: string,
|
return <FilteredSmartMessageSearchResult id={id} />;
|
||||||
style: CSSProperties
|
|
||||||
): JSX.Element {
|
|
||||||
return <FilteredSmartMessageSearchResult id={id} style={style} />;
|
|
||||||
}
|
}
|
||||||
function renderNetworkStatus(): JSX.Element {
|
function renderNetworkStatus(): JSX.Element {
|
||||||
return <SmartNetworkStatus />;
|
return <SmartNetworkStatus />;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import memoizee from 'memoizee';
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import {
|
import {
|
||||||
|
PropsActionsType as TimelineActionsType,
|
||||||
ContactSpoofingReviewPropType,
|
ContactSpoofingReviewPropType,
|
||||||
Timeline,
|
Timeline,
|
||||||
WarningType as TimelineWarningType,
|
WarningType as TimelineWarningType,
|
||||||
|
@ -44,13 +45,6 @@ import {
|
||||||
} from '../../util/groupMemberNameCollisions';
|
} from '../../util/groupMemberNameCollisions';
|
||||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||||
|
|
||||||
// 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;
|
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@ -72,10 +66,10 @@ function renderItem(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
onHeightChange: (messageId: string) => unknown,
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actionProps: Record<string, unknown>
|
actionProps: TimelineActionsType
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<FilteredSmartTimelineItem
|
<SmartTimelineItem
|
||||||
{...actionProps}
|
{...actionProps}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
id={messageId}
|
id={messageId}
|
||||||
|
@ -109,7 +103,7 @@ function renderLoadingRow(id: string): JSX.Element {
|
||||||
return <SmartTimelineLoadingRow id={id} />;
|
return <SmartTimelineLoadingRow id={id} />;
|
||||||
}
|
}
|
||||||
function renderTypingBubble(id: string): JSX.Element {
|
function renderTypingBubble(id: string): JSX.Element {
|
||||||
return <FilteredSmartTypingBubble id={id} />;
|
return <SmartTypingBubble id={id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWarning = (
|
const getWarning = (
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
@ -21,6 +22,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
throw new Error(`Did not find conversation ${id} in state!`);
|
throw new Error(`Did not find conversation ${id} in state!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strictAssert(conversation.typingContact, 'Missing typingContact');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...conversation.typingContact,
|
...conversation.typingContact,
|
||||||
conversationType: conversation.type,
|
conversationType: conversation.type,
|
||||||
|
|
|
@ -1236,6 +1236,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1260,6 +1261,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1284,6 +1286,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1308,6 +1311,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1332,6 +1336,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1380,6 +1385,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1405,6 +1411,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1430,6 +1437,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1480,6 +1488,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1504,6 +1513,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1528,6 +1538,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1553,6 +1564,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
@ -1577,6 +1589,7 @@ describe('both/state/selectors/conversations', () => {
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
...getDefaultConversation(),
|
||||||
name: 'Someone There',
|
name: 'Someone There',
|
||||||
phoneNumber: '+18005551111',
|
phoneNumber: '+18005551111',
|
||||||
},
|
},
|
||||||
|
|
62
ts/test-both/util/memoizeByRoot.ts
Normal file
62
ts/test-both/util/memoizeByRoot.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
|
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
||||||
|
|
||||||
|
class Root {}
|
||||||
|
|
||||||
|
describe('memoizeByRoot', () => {
|
||||||
|
it('should memoize by last passed arguments', () => {
|
||||||
|
const root = new Root();
|
||||||
|
|
||||||
|
const stub = sinon.stub();
|
||||||
|
stub.withArgs(sinon.match.same(root), 1).returns(1);
|
||||||
|
stub.withArgs(sinon.match.same(root), 2).returns(2);
|
||||||
|
|
||||||
|
const fn = memoizeByRoot(stub);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(root, 1), 1);
|
||||||
|
assert.strictEqual(fn(root, 1), 1);
|
||||||
|
assert.isTrue(stub.calledOnce);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(root, 2), 2);
|
||||||
|
assert.strictEqual(fn(root, 2), 2);
|
||||||
|
assert.isTrue(stub.calledTwice);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(root, 1), 1);
|
||||||
|
assert.strictEqual(fn(root, 1), 1);
|
||||||
|
assert.isTrue(stub.calledThrice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should memoize results by root', () => {
|
||||||
|
const rootA = new Root();
|
||||||
|
const rootB = new Root();
|
||||||
|
|
||||||
|
const stub = sinon.stub();
|
||||||
|
stub.withArgs(sinon.match.same(rootA), 1).returns(1);
|
||||||
|
stub.withArgs(sinon.match.same(rootA), 2).returns(2);
|
||||||
|
stub.withArgs(sinon.match.same(rootB), 1).returns(3);
|
||||||
|
stub.withArgs(sinon.match.same(rootB), 2).returns(4);
|
||||||
|
|
||||||
|
const fn = memoizeByRoot(stub);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(rootA, 1), 1);
|
||||||
|
assert.strictEqual(fn(rootB, 1), 3);
|
||||||
|
assert.strictEqual(fn(rootA, 1), 1);
|
||||||
|
assert.strictEqual(fn(rootB, 1), 3);
|
||||||
|
assert.isTrue(stub.calledTwice);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(rootA, 2), 2);
|
||||||
|
assert.strictEqual(fn(rootB, 2), 4);
|
||||||
|
assert.strictEqual(fn(rootA, 2), 2);
|
||||||
|
assert.strictEqual(fn(rootB, 2), 4);
|
||||||
|
assert.strictEqual(stub.callCount, 4);
|
||||||
|
|
||||||
|
assert.strictEqual(fn(rootA, 1), 1);
|
||||||
|
assert.strictEqual(fn(rootB, 1), 3);
|
||||||
|
assert.strictEqual(stub.callCount, 6);
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,3 +35,19 @@ export enum ScrollBehavior {
|
||||||
Default = 'default',
|
Default = 'default',
|
||||||
Hard = 'hard',
|
Hard = 'hard',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InternalAssertProps<
|
||||||
|
Result,
|
||||||
|
Value,
|
||||||
|
Missing = Omit<Result, keyof Value>
|
||||||
|
> = keyof Missing extends never
|
||||||
|
? Result
|
||||||
|
: Result &
|
||||||
|
{
|
||||||
|
[key in keyof Required<Missing>]: [
|
||||||
|
never,
|
||||||
|
'AssertProps: missing property'
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssertProps<Result, Value> = InternalAssertProps<Result, Value>;
|
||||||
|
|
47
ts/util/memoizeByRoot.ts
Normal file
47
ts/util/memoizeByRoot.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { defaultMemoize } from 'reselect';
|
||||||
|
|
||||||
|
import { strictAssert } from './assert';
|
||||||
|
|
||||||
|
// The difference between the function below and `defaultMemoize` from
|
||||||
|
// `reselect` is that it supports multiple "root" states. `reselect` is designed
|
||||||
|
// to interact with a single redux store and by default it memoizes only the
|
||||||
|
// last result of the selector (matched by its arguments). This works well when
|
||||||
|
// applied to singular entities living in the redux's state, but we need to
|
||||||
|
// apply selector to multitide of conversations and messages.
|
||||||
|
//
|
||||||
|
// The way it works is that it adds an extra memoization step that uses the
|
||||||
|
// first argument ("root") as a key in a weak map, and then applies the default
|
||||||
|
// `reselect`'s memoization function to the rest of the arguments. This way
|
||||||
|
// we essentially get a weak map of selectors by the "root".
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export function memoizeByRoot<F extends Function>(
|
||||||
|
fn: F,
|
||||||
|
equalityCheck?: <T>(a: T, b: T, index: number) => boolean
|
||||||
|
): F {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
const cache = new WeakMap<object, Function>();
|
||||||
|
|
||||||
|
const wrap = (root: unknown, ...rest: Array<unknown>): unknown => {
|
||||||
|
strictAssert(
|
||||||
|
typeof root === 'object' && root !== null,
|
||||||
|
'Root is not object'
|
||||||
|
);
|
||||||
|
|
||||||
|
let partial = cache.get(root);
|
||||||
|
if (!partial) {
|
||||||
|
partial = defaultMemoize((...args: Array<unknown>): unknown => {
|
||||||
|
return fn(root, ...args);
|
||||||
|
}, equalityCheck);
|
||||||
|
|
||||||
|
cache.set(root, partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
return partial(...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (wrap as unknown) as F;
|
||||||
|
}
|
|
@ -4,6 +4,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import nodePath from 'path';
|
import nodePath from 'path';
|
||||||
|
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AttachmentDraftType,
|
AttachmentDraftType,
|
||||||
AttachmentType,
|
AttachmentType,
|
||||||
|
@ -3922,25 +3924,27 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
window.log.info('Send pre-checks took', sendDelta, 'milliseconds');
|
window.log.info('Send pre-checks took', sendDelta, 'milliseconds');
|
||||||
|
|
||||||
model.sendMessage(
|
batchedUpdates(() => {
|
||||||
message,
|
model.sendMessage(
|
||||||
attachments,
|
message,
|
||||||
this.quote,
|
attachments,
|
||||||
this.getLinkPreview(),
|
this.quote,
|
||||||
undefined, // sticker
|
this.getLinkPreview(),
|
||||||
mentions,
|
undefined, // sticker
|
||||||
{
|
mentions,
|
||||||
sendHQImages,
|
{
|
||||||
timestamp,
|
sendHQImages,
|
||||||
}
|
timestamp,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.compositionApi.current.reset();
|
this.compositionApi.current.reset();
|
||||||
model.setMarkedUnread(false);
|
model.setMarkedUnread(false);
|
||||||
this.setQuoteMessage(null);
|
this.setQuoteMessage(null);
|
||||||
this.resetLinkPreview();
|
this.resetLinkPreview();
|
||||||
this.clearAttachments();
|
this.clearAttachments();
|
||||||
window.reduxActions.composer.resetComposer();
|
window.reduxActions.composer.resetComposer();
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Error pulling attached files before send',
|
'Error pulling attached files before send',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue