Profile name spoofing dialog
This commit is contained in:
parent
814255c10e
commit
e7ef3de6d0
21 changed files with 893 additions and 15 deletions
|
@ -5164,5 +5164,35 @@
|
|||
"ForwardMessageModal--continue": {
|
||||
"message": "Continue",
|
||||
"description": "aria-label for the 'next' button in the forward a message modal dialog"
|
||||
},
|
||||
"ContactSpoofing__same-name": {
|
||||
"message": "Review requests carefully. Signal found another contact with the same name. $link$",
|
||||
"description": "Shown in the timeline warning when you have a message request from someone with the same name as someone else",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1",
|
||||
"example": "Review request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ContactSpoofing__same-name__link": {
|
||||
"message": "Review request",
|
||||
"description": "Shown in the timeline warning when you have a message request from someone with the same name as someone else"
|
||||
},
|
||||
"ContactSpoofingReviewDialog__title": {
|
||||
"message": "Review request",
|
||||
"description": "Title for the contact name spoofing review dialog"
|
||||
},
|
||||
"ContactSpoofingReviewDialog__description": {
|
||||
"message": "If you're not sure who the request is from, review the contacts below and take action.",
|
||||
"description": "Description for the contact spoofing review dialog"
|
||||
},
|
||||
"ContactSpoofingReviewDialog__possibly-unsafe-title": {
|
||||
"message": "Request",
|
||||
"description": "Header in the contact spoofing review dialog, shown above the potentially-unsafe user"
|
||||
},
|
||||
"ContactSpoofingReviewDialog__safe-title": {
|
||||
"message": "Your contact",
|
||||
"description": "Header in the contact spoofing review dialog, shown above the \"safe\" user"
|
||||
}
|
||||
}
|
||||
|
|
40
stylesheets/components/ContactSpoofingReviewDialog.scss
Normal file
40
stylesheets/components/ContactSpoofingReviewDialog.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
.module-ContactSpoofingReviewDialog {
|
||||
user-select: none;
|
||||
|
||||
p {
|
||||
@include font-body-2;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include font-body-1-bold;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-90;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-top: 8px;
|
||||
|
||||
.module-Button:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
.module-ContactSpoofingReviewDialogPerson {
|
||||
display: flex;
|
||||
|
||||
&:is(button) {
|
||||
@include button-reset;
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-left: 12px;
|
||||
|
||||
&__contact-name {
|
||||
@include font-body-1;
|
||||
}
|
||||
|
||||
&__property {
|
||||
@include font-body-2;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
stylesheets/components/TimelineWarning.scss
Normal file
76
stylesheets/components/TimelineWarning.scss
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-TimelineWarning {
|
||||
@mixin icon($icon) {
|
||||
@include light-theme {
|
||||
@include color-svg($icon, $color-gray-60);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg($icon, $color-gray-20);
|
||||
}
|
||||
}
|
||||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
user-select: none;
|
||||
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
&:last-child {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-65;
|
||||
background: $color-gray-02;
|
||||
border-color: $color-gray-15;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
color: $color-gray-15;
|
||||
background: $color-gray-80;
|
||||
border-color: $color-gray-65;
|
||||
}
|
||||
|
||||
&__generic-icon {
|
||||
@include icon('../images/icons/v2/info-outline-24.svg');
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include font-body-2;
|
||||
flex-grow: 1;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
|
||||
&__link {
|
||||
@include button-reset;
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
|
||||
@include light-theme {
|
||||
color: $ultramarine-brand-light;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-ios-blue-tint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@include button-reset;
|
||||
|
||||
&::after {
|
||||
@include icon('../images/icons/v2/x-24.svg');
|
||||
content: '';
|
||||
display: block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
13
stylesheets/components/TimelineWarnings.scss
Normal file
13
stylesheets/components/TimelineWarnings.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-TimelineWarnings {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
|
@ -32,6 +32,8 @@
|
|||
@import './components/Button.scss';
|
||||
@import './components/ContactPill.scss';
|
||||
@import './components/ContactPills.scss';
|
||||
@import './components/ContactSpoofingReviewDialog.scss';
|
||||
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
||||
@import './components/ConversationHeader.scss';
|
||||
@import './components/EditConversationAttributesModal.scss';
|
||||
@import './components/ForwardMessageModal.scss';
|
||||
|
@ -43,3 +45,5 @@
|
|||
@import './components/SafetyNumberViewer.scss';
|
||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||
@import './components/TimelineWarning.scss';
|
||||
@import './components/TimelineWarnings.scss';
|
||||
|
|
|
@ -13,6 +13,7 @@ type PropsType = {
|
|||
children: ReactNode;
|
||||
hasXButton?: boolean;
|
||||
i18n: LocalizerType;
|
||||
moduleClassName?: string;
|
||||
onClose?: () => void;
|
||||
title?: ReactNode;
|
||||
theme?: Theme;
|
||||
|
@ -22,6 +23,7 @@ export function Modal({
|
|||
children,
|
||||
hasXButton,
|
||||
i18n,
|
||||
moduleClassName,
|
||||
onClose = noop,
|
||||
title,
|
||||
theme,
|
||||
|
@ -35,7 +37,8 @@ export function Modal({
|
|||
<div
|
||||
className={classNames(
|
||||
'module-Modal',
|
||||
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header'
|
||||
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header',
|
||||
moduleClassName
|
||||
)}
|
||||
>
|
||||
{hasHeader && (
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/Conversation/ContactSpoofingReviewDialog',
|
||||
module
|
||||
);
|
||||
|
||||
story.add('Default', () => (
|
||||
<ContactSpoofingReviewDialog
|
||||
i18n={i18n}
|
||||
onBlock={action('onBlock')}
|
||||
onBlockAndDelete={action('onBlockAndDelete')}
|
||||
onClose={action('onClose')}
|
||||
onDelete={action('onDelete')}
|
||||
onShowContactModal={action('onShowContactModal')}
|
||||
onUnblock={action('onUnblock')}
|
||||
possiblyUnsafeConversation={getDefaultConversation()}
|
||||
safeConversation={getDefaultConversation()}
|
||||
/>
|
||||
));
|
117
ts/components/conversation/ContactSpoofingReviewDialog.tsx
Normal file
117
ts/components/conversation/ContactSpoofingReviewDialog.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
import { ContactSpoofingReviewDialogPerson } from './ContactSpoofingReviewDialogPerson';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onBlock: () => unknown;
|
||||
onBlockAndDelete: () => unknown;
|
||||
onClose: () => void;
|
||||
onDelete: () => unknown;
|
||||
onShowContactModal: (contactId: string) => unknown;
|
||||
onUnblock: () => unknown;
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
};
|
||||
|
||||
export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> = ({
|
||||
i18n,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onClose,
|
||||
onDelete,
|
||||
onShowContactModal,
|
||||
onUnblock,
|
||||
possiblyUnsafeConversation,
|
||||
safeConversation,
|
||||
}) => {
|
||||
assert(
|
||||
possiblyUnsafeConversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "possibly unsafe" conversation'
|
||||
);
|
||||
assert(
|
||||
safeConversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialog> expected a direct conversation for the "safe" conversation'
|
||||
);
|
||||
|
||||
const [messageRequestState, setMessageRequestState] = useState(
|
||||
MessageRequestState.default
|
||||
);
|
||||
|
||||
if (messageRequestState !== MessageRequestState.default) {
|
||||
return (
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onUnblock={onUnblock}
|
||||
onDelete={onDelete}
|
||||
name={possiblyUnsafeConversation.name}
|
||||
profileName={possiblyUnsafeConversation.profileName}
|
||||
phoneNumber={possiblyUnsafeConversation.phoneNumber}
|
||||
title={possiblyUnsafeConversation.title}
|
||||
conversationType="direct"
|
||||
state={messageRequestState}
|
||||
onChangeState={setMessageRequestState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
moduleClassName="module-ContactSpoofingReviewDialog"
|
||||
onClose={onClose}
|
||||
title={i18n('ContactSpoofingReviewDialog__title')}
|
||||
>
|
||||
<p>{i18n('ContactSpoofingReviewDialog__description')}</p>
|
||||
<h2>{i18n('ContactSpoofingReviewDialog__possibly-unsafe-title')}</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={possiblyUnsafeConversation}
|
||||
i18n={i18n}
|
||||
>
|
||||
<div className="module-ContactSpoofingReviewDialog__buttons">
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setMessageRequestState(MessageRequestState.deleting);
|
||||
}}
|
||||
>
|
||||
{i18n('MessageRequests--delete')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
onClick={() => {
|
||||
setMessageRequestState(MessageRequestState.blocking);
|
||||
}}
|
||||
>
|
||||
{i18n('MessageRequests--block')}
|
||||
</Button>
|
||||
</div>
|
||||
</ContactSpoofingReviewDialogPerson>
|
||||
<hr />
|
||||
<h2>{i18n('ContactSpoofingReviewDialog__safe-title')}</h2>
|
||||
<ContactSpoofingReviewDialogPerson
|
||||
conversation={safeConversation}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
onShowContactModal(safeConversation.id);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
|
||||
type PropsType = {
|
||||
children?: ReactNode;
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const ContactSpoofingReviewDialogPerson: FunctionComponent<PropsType> = ({
|
||||
children,
|
||||
conversation,
|
||||
i18n,
|
||||
onClick,
|
||||
}) => {
|
||||
assert(
|
||||
conversation.type === 'direct',
|
||||
'<ContactSpoofingReviewDialogPerson> expected a direct conversation'
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<Avatar
|
||||
{...conversation}
|
||||
conversationType={conversation.type}
|
||||
size={AvatarSize.FIFTY_TWO}
|
||||
className="module-ContactSpoofingReviewDialogPerson__avatar"
|
||||
i18n={i18n}
|
||||
/>
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info">
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
module="module-ContactSpoofingReviewDialogPerson__info__contact-name"
|
||||
title={conversation.title}
|
||||
/>
|
||||
{conversation.phoneNumber ? (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
{conversation.phoneNumber}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-ContactSpoofingReviewDialogPerson__info__property">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={conversation.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-ContactSpoofingReviewDialogPerson"
|
||||
onClick={onClick}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-ContactSpoofingReviewDialogPerson">{contents}</div>
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import enMessages from '../../../_locales/en/messages.json';
|
|||
import { PropsType, Timeline } from './Timeline';
|
||||
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { LastSeenIndicator } from './LastSeenIndicator';
|
||||
import { TimelineLoadingRow } from './TimelineLoadingRow';
|
||||
import { TypingBubble } from './TypingBubble';
|
||||
|
@ -260,6 +261,16 @@ const actions = () => ({
|
|||
returnToActiveCall: action('returnToActiveCall'),
|
||||
|
||||
contactSupport: action('contactSupport'),
|
||||
|
||||
closeContactSpoofingReview: action('closeContactSpoofingReview'),
|
||||
reviewMessageRequestNameCollision: action(
|
||||
'reviewMessageRequestNameCollision'
|
||||
),
|
||||
|
||||
onBlock: action('onBlock'),
|
||||
onBlockAndDelete: action('onBlockAndDelete'),
|
||||
onDelete: action('onDelete'),
|
||||
onUnblock: action('onUnblock'),
|
||||
});
|
||||
|
||||
const renderItem = (id: string) => (
|
||||
|
@ -330,6 +341,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
undefined,
|
||||
invitedContactsForNewlyCreatedGroup:
|
||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||
warning: overrideProps.warning,
|
||||
|
||||
id: '',
|
||||
renderItem,
|
||||
|
@ -419,3 +431,14 @@ story.add('With invited contacts for a newly-created group', () => {
|
|||
|
||||
return <Timeline {...props} />;
|
||||
});
|
||||
|
||||
story.add('With "same name" warning', () => {
|
||||
const props = createProps({
|
||||
warning: {
|
||||
safeConversation: getDefaultConversation(),
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { debounce, get, isNumber } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import React, { CSSProperties, ReactNode } from 'react';
|
||||
import {
|
||||
AutoSizer,
|
||||
CellMeasurer,
|
||||
|
@ -11,6 +11,7 @@ import {
|
|||
List,
|
||||
Grid,
|
||||
} from 'react-virtualized';
|
||||
import Measure from 'react-measure';
|
||||
|
||||
import { ScrollDownButton } from './ScrollDownButton';
|
||||
|
||||
|
@ -18,10 +19,15 @@ import { GlobalAudioProvider } from '../GlobalAudioContext';
|
|||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
import { PropsActions as MessageActionsType } from './Message';
|
||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||
import { Intl } from '../Intl';
|
||||
import { TimelineWarning } from './TimelineWarning';
|
||||
import { TimelineWarnings } from './TimelineWarnings';
|
||||
import { NewlyCreatedGroupInvitedContactsDialog } from '../NewlyCreatedGroupInvitedContactsDialog';
|
||||
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||
|
||||
const AT_BOTTOM_THRESHOLD = 15;
|
||||
const NEAR_BOTTOM_THRESHOLD = 15;
|
||||
|
@ -30,6 +36,10 @@ const LOAD_MORE_THRESHOLD = 30;
|
|||
const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
|
||||
export const LOAD_COUNTDOWN = 1;
|
||||
|
||||
export type WarningType = {
|
||||
safeConversation: ConversationType;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
|
@ -54,6 +64,12 @@ type PropsHousekeepingType = {
|
|||
selectedMessageId?: string;
|
||||
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
|
||||
|
||||
warning?: WarningType;
|
||||
contactSpoofingReview?: {
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
};
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
renderItem: (
|
||||
|
@ -74,17 +90,27 @@ type PropsHousekeepingType = {
|
|||
type PropsActionsType = {
|
||||
clearChangedMessages: (conversationId: string) => unknown;
|
||||
clearInvitedConversationsForNewlyCreatedGroup: () => void;
|
||||
closeContactSpoofingReview: () => void;
|
||||
setLoadCountdownStart: (
|
||||
conversationId: string,
|
||||
loadCountdownStart?: number
|
||||
) => unknown;
|
||||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||
reviewMessageRequestNameCollision: (
|
||||
_: Readonly<{
|
||||
safeConversationId: string;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
loadAndScroll: (messageId: string) => unknown;
|
||||
loadOlderMessages: (messageId: string) => unknown;
|
||||
loadNewerMessages: (messageId: string) => unknown;
|
||||
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
|
||||
markMessageRead: (messageId: string) => unknown;
|
||||
onBlock: () => unknown;
|
||||
onBlockAndDelete: () => unknown;
|
||||
onDelete: () => unknown;
|
||||
onUnblock: () => unknown;
|
||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||
clearSelectedMessage: () => unknown;
|
||||
updateSharedGroups: () => unknown;
|
||||
|
@ -142,6 +168,9 @@ type StateType = {
|
|||
|
||||
shouldShowScrollDownButton: boolean;
|
||||
areUnreadBelowCurrentPosition: boolean;
|
||||
|
||||
hasDismissedWarning: boolean;
|
||||
lastMeasuredWarningHeight: number;
|
||||
};
|
||||
|
||||
export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||
|
@ -178,6 +207,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
prevPropScrollToIndex: scrollToIndex,
|
||||
shouldShowScrollDownButton: false,
|
||||
areUnreadBelowCurrentPosition: false,
|
||||
hasDismissedWarning: false,
|
||||
lastMeasuredWarningHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -554,6 +585,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
renderTypingBubble,
|
||||
updateSharedGroups,
|
||||
} = this.props;
|
||||
const { lastMeasuredWarningHeight } = this.state;
|
||||
|
||||
const styleWithWidth = {
|
||||
...style,
|
||||
|
@ -562,11 +594,14 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
const row = index;
|
||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||
const typingBubbleRow = this.getTypingBubbleRow();
|
||||
let rowContents;
|
||||
let rowContents: ReactNode;
|
||||
|
||||
if (haveOldest && row === 0) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={styleWithWidth} role="row">
|
||||
{this.getWarning() ? (
|
||||
<div style={{ height: lastMeasuredWarningHeight }} />
|
||||
) : null}
|
||||
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
|
||||
</div>
|
||||
);
|
||||
|
@ -802,7 +837,10 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
window.unregisterForActive(this.updateWithVisibleRows);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: PropsType): void {
|
||||
public componentDidUpdate(
|
||||
prevProps: Readonly<PropsType>,
|
||||
prevState: Readonly<StateType>
|
||||
): void {
|
||||
const {
|
||||
id,
|
||||
clearChangedMessages,
|
||||
|
@ -814,6 +852,15 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
typingContact,
|
||||
} = this.props;
|
||||
|
||||
// Warnings can increase the size of the first row (adding padding for the floating
|
||||
// warning), so we recompute it when the warnings change.
|
||||
const hadWarning = Boolean(
|
||||
prevProps.warning && !prevState.hasDismissedWarning
|
||||
);
|
||||
if (hadWarning !== Boolean(this.getWarning())) {
|
||||
this.recomputeRowHeights(0);
|
||||
}
|
||||
|
||||
// There are a number of situations which can necessitate that we forget about row
|
||||
// heights previously calculated. We reset the minimum number of rows to minimize
|
||||
// unexpected changes to the scroll position. Those changes happen because
|
||||
|
@ -1071,11 +1118,19 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
public render(): JSX.Element | null {
|
||||
const {
|
||||
clearInvitedConversationsForNewlyCreatedGroup,
|
||||
closeContactSpoofingReview,
|
||||
contactSpoofingReview,
|
||||
i18n,
|
||||
id,
|
||||
items,
|
||||
isGroupV1AndDisabled,
|
||||
invitedContactsForNewlyCreatedGroup,
|
||||
isGroupV1AndDisabled,
|
||||
items,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onDelete,
|
||||
onUnblock,
|
||||
showContactModal,
|
||||
reviewMessageRequestNameCollision,
|
||||
} = this.props;
|
||||
const {
|
||||
shouldShowScrollDownButton,
|
||||
|
@ -1127,6 +1182,57 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
</AutoSizer>
|
||||
);
|
||||
|
||||
const warning = this.getWarning();
|
||||
let timelineWarning: ReactNode;
|
||||
if (warning) {
|
||||
timelineWarning = (
|
||||
<Measure
|
||||
bounds
|
||||
onResize={({ bounds }) => {
|
||||
if (!bounds) {
|
||||
assert(false, 'We should be measuring the bounds');
|
||||
return;
|
||||
}
|
||||
this.setState({ lastMeasuredWarningHeight: bounds.height });
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<TimelineWarnings ref={measureRef}>
|
||||
<TimelineWarning
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
this.setState({ hasDismissedWarning: true });
|
||||
}}
|
||||
>
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
<TimelineWarning.Text>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="ContactSpoofing__same-name"
|
||||
components={{
|
||||
link: (
|
||||
<TimelineWarning.Link
|
||||
onClick={() => {
|
||||
reviewMessageRequestNameCollision({
|
||||
safeConversationId: warning.safeConversation.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('ContactSpoofing__same-name__link')}
|
||||
</TimelineWarning.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TimelineWarning.Text>
|
||||
</TimelineWarning>
|
||||
</TimelineWarnings>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
@ -1139,6 +1245,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
>
|
||||
{timelineWarning}
|
||||
|
||||
<GlobalAudioProvider conversationId={id}>
|
||||
{autoSizer}
|
||||
</GlobalAudioProvider>
|
||||
|
@ -1159,7 +1267,33 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
onClose={clearInvitedConversationsForNewlyCreatedGroup}
|
||||
/>
|
||||
)}
|
||||
|
||||
{contactSpoofingReview && (
|
||||
<ContactSpoofingReviewDialog
|
||||
i18n={i18n}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onClose={closeContactSpoofingReview}
|
||||
onDelete={onDelete}
|
||||
onShowContactModal={showContactModal}
|
||||
onUnblock={onUnblock}
|
||||
possiblyUnsafeConversation={
|
||||
contactSpoofingReview.possiblyUnsafeConversation
|
||||
}
|
||||
safeConversation={contactSpoofingReview.safeConversation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private getWarning(): undefined | WarningType {
|
||||
const { hasDismissedWarning } = this.state;
|
||||
if (hasDismissedWarning) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { warning } = this.props;
|
||||
return warning;
|
||||
}
|
||||
}
|
||||
|
|
65
ts/components/conversation/TimelineWarning.tsx
Normal file
65
ts/components/conversation/TimelineWarning.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
const CLASS_NAME = 'module-TimelineWarning';
|
||||
const ICON_CONTAINER_CLASS_NAME = `${CLASS_NAME}__icon-container`;
|
||||
const GENERIC_ICON_CLASS_NAME = `${CLASS_NAME}__generic-icon`;
|
||||
const TEXT_CLASS_NAME = `${CLASS_NAME}__text`;
|
||||
const LINK_CLASS_NAME = `${TEXT_CLASS_NAME}__link`;
|
||||
const CLOSE_BUTTON_CLASS_NAME = `${CLASS_NAME}__close-button`;
|
||||
|
||||
type PropsType = {
|
||||
children: ReactNode;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function TimelineWarning({
|
||||
children,
|
||||
i18n,
|
||||
onClose,
|
||||
}: Readonly<PropsType>): JSX.Element {
|
||||
return (
|
||||
<div className={CLASS_NAME}>
|
||||
{children}
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className={CLOSE_BUTTON_CLASS_NAME}
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TimelineWarning.IconContainer = ({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): JSX.Element => (
|
||||
<div className={ICON_CONTAINER_CLASS_NAME}>{children}</div>
|
||||
);
|
||||
|
||||
TimelineWarning.GenericIcon = () => <div className={GENERIC_ICON_CLASS_NAME} />;
|
||||
|
||||
TimelineWarning.Text = ({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): JSX.Element => (
|
||||
<div className={TEXT_CLASS_NAME}>{children}</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
TimelineWarning.Link = ({
|
||||
children,
|
||||
onClick,
|
||||
}: Readonly<LinkProps>): JSX.Element => (
|
||||
<button className={LINK_CLASS_NAME} onClick={onClick} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
18
ts/components/conversation/TimelineWarnings.tsx
Normal file
18
ts/components/conversation/TimelineWarnings.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { forwardRef, ReactNode } from 'react';
|
||||
|
||||
const CLASS_NAME = 'module-TimelineWarnings';
|
||||
|
||||
type PropsType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const TimelineWarnings = forwardRef<HTMLDivElement, PropsType>(
|
||||
({ children }, ref) => (
|
||||
<div className={CLASS_NAME} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
|
@ -275,6 +275,10 @@ type ComposerStateType =
|
|||
| { isCreating: true; hasError: false }
|
||||
));
|
||||
|
||||
type ContactSpoofingReviewStateType = {
|
||||
safeConversationId: string;
|
||||
};
|
||||
|
||||
export type ConversationsStateType = {
|
||||
preJoinConversation?: PreJoinConversationType;
|
||||
invitedConversationIdsForNewlyCreatedGroup?: Array<string>;
|
||||
|
@ -289,6 +293,7 @@ export type ConversationsStateType = {
|
|||
selectedConversationPanelDepth: number;
|
||||
showArchived: boolean;
|
||||
composer?: ComposerStateType;
|
||||
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
||||
|
||||
// Note: it's very important that both of these locations are always kept up to date
|
||||
messagesLookup: MessageLookupType;
|
||||
|
@ -335,6 +340,9 @@ type ClearInvitedConversationsForNewlyCreatedGroupActionType = {
|
|||
type CloseCantAddContactToGroupModalActionType = {
|
||||
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
|
||||
};
|
||||
type CloseContactSpoofingReviewActionType = {
|
||||
type: 'CLOSE_CONTACT_SPOOFING_REVIEW';
|
||||
};
|
||||
type CloseMaximumGroupSizeModalActionType = {
|
||||
type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL';
|
||||
};
|
||||
|
@ -512,6 +520,12 @@ export type SelectedConversationChangedActionType = {
|
|||
messageId?: string;
|
||||
};
|
||||
};
|
||||
type ReviewMessageRequestNameCollisionActionType = {
|
||||
type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION';
|
||||
payload: {
|
||||
safeConversationId: string;
|
||||
};
|
||||
};
|
||||
type ShowInboxActionType = {
|
||||
type: 'SHOW_INBOX';
|
||||
payload: null;
|
||||
|
@ -569,6 +583,7 @@ export type ConversationActionType =
|
|||
| ClearSelectedMessageActionType
|
||||
| ClearUnreadMetricsActionType
|
||||
| CloseCantAddContactToGroupModalActionType
|
||||
| CloseContactSpoofingReviewActionType
|
||||
| CloseMaximumGroupSizeModalActionType
|
||||
| CloseRecommendedGroupSizeModalActionType
|
||||
| ConversationAddedActionType
|
||||
|
@ -587,6 +602,7 @@ export type ConversationActionType =
|
|||
| RemoveAllConversationsActionType
|
||||
| RepairNewestMessageActionType
|
||||
| RepairOldestMessageActionType
|
||||
| ReviewMessageRequestNameCollisionActionType
|
||||
| ScrollToMessageActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
|
@ -617,6 +633,7 @@ export const actions = {
|
|||
clearSelectedMessage,
|
||||
clearUnreadMetrics,
|
||||
closeCantAddContactToGroupModal,
|
||||
closeContactSpoofingReview,
|
||||
closeRecommendedGroupSizeModal,
|
||||
closeMaximumGroupSizeModal,
|
||||
conversationAdded,
|
||||
|
@ -634,6 +651,7 @@ export const actions = {
|
|||
removeAllConversations,
|
||||
repairNewestMessage,
|
||||
repairOldestMessage,
|
||||
reviewMessageRequestNameCollision,
|
||||
scrollToMessage,
|
||||
selectMessage,
|
||||
setComposeGroupAvatar,
|
||||
|
@ -863,6 +881,14 @@ function repairOldestMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function reviewMessageRequestNameCollision(
|
||||
payload: Readonly<{
|
||||
safeConversationId: string;
|
||||
}>
|
||||
): ReviewMessageRequestNameCollisionActionType {
|
||||
return { type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION', payload };
|
||||
}
|
||||
|
||||
function messagesReset(
|
||||
conversationId: string,
|
||||
messages: Array<MessageType>,
|
||||
|
@ -977,6 +1003,9 @@ function clearUnreadMetrics(
|
|||
function closeCantAddContactToGroupModal(): CloseCantAddContactToGroupModalActionType {
|
||||
return { type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL' };
|
||||
}
|
||||
function closeContactSpoofingReview(): CloseContactSpoofingReviewActionType {
|
||||
return { type: 'CLOSE_CONTACT_SPOOFING_REVIEW' };
|
||||
}
|
||||
function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
||||
return { type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL' };
|
||||
}
|
||||
|
@ -1343,6 +1372,10 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'CLOSE_CONTACT_SPOOFING_REVIEW') {
|
||||
return omit(state, 'contactSpoofingReview');
|
||||
}
|
||||
|
||||
if (action.type === 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL') {
|
||||
return closeComposerModal(state, 'maximumGroupSizeModalState' as const);
|
||||
}
|
||||
|
@ -1379,7 +1412,8 @@ export function reducer(
|
|||
const { id, data } = payload;
|
||||
const { conversationLookup } = state;
|
||||
|
||||
let { showArchived, selectedConversationId } = state;
|
||||
const { selectedConversationId } = state;
|
||||
let { showArchived } = state;
|
||||
|
||||
const existing = conversationLookup[id];
|
||||
// In the change case we only modify the lookup if we already had that conversation
|
||||
|
@ -1387,6 +1421,8 @@ export function reducer(
|
|||
return state;
|
||||
}
|
||||
|
||||
const keysToOmit: Array<keyof ConversationsStateType> = [];
|
||||
|
||||
if (selectedConversationId === id) {
|
||||
// Archived -> Inbox: we go back to the normal inbox view
|
||||
if (existing.isArchived && !data.isArchived) {
|
||||
|
@ -1397,12 +1433,16 @@ export function reducer(
|
|||
// behavior - no selected conversation in the left pane, but a conversation show
|
||||
// in the right pane.
|
||||
if (!existing.isArchived && data.isArchived) {
|
||||
selectedConversationId = undefined;
|
||||
keysToOmit.push('selectedConversationId');
|
||||
}
|
||||
|
||||
if (!existing.isBlocked && data.isBlocked) {
|
||||
keysToOmit.push('contactSpoofingReview');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...omit(state, keysToOmit),
|
||||
selectedConversationId,
|
||||
showArchived,
|
||||
conversationLookup: {
|
||||
|
@ -1444,7 +1484,7 @@ export function reducer(
|
|||
: undefined;
|
||||
|
||||
return {
|
||||
...state,
|
||||
...omit(state, 'contactSpoofingReview'),
|
||||
selectedConversationId,
|
||||
selectedConversationPanelDepth: 0,
|
||||
messagesLookup: omit(state.messagesLookup, messageIds),
|
||||
|
@ -1879,6 +1919,13 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION') {
|
||||
return {
|
||||
...state,
|
||||
contactSpoofingReview: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'MESSAGES_ADDED') {
|
||||
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
||||
const { messagesByConversation, messagesLookup } = state;
|
||||
|
@ -2059,7 +2106,7 @@ export function reducer(
|
|||
const { id } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
...omit(state, 'contactSpoofingReview'),
|
||||
selectedConversationId: id,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -89,6 +89,18 @@ export const getConversationsByGroupId = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
const getAllConversations = createSelector(
|
||||
getConversationLookup,
|
||||
(lookup): Array<ConversationType> => Object.values(lookup)
|
||||
);
|
||||
|
||||
export const getConversationsByTitleSelector = createSelector(
|
||||
getAllConversations,
|
||||
(conversations): ((title: string) => Array<ConversationType>) => (
|
||||
title: string
|
||||
) => conversations.filter(conversation => conversation.title === title)
|
||||
);
|
||||
|
||||
export const getSelectedConversationId = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): string | undefined => {
|
||||
|
|
|
@ -5,13 +5,18 @@ import { pick } from 'lodash';
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { Timeline } from '../../components/conversation/Timeline';
|
||||
import {
|
||||
Timeline,
|
||||
WarningType as TimelineWarningType,
|
||||
} from '../../components/conversation/Timeline';
|
||||
import { StateType } from '../reducer';
|
||||
import { ConversationType } from '../ducks/conversations';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
getConversationsByTitleSelector,
|
||||
getInvitedContactsForNewlyCreatedGroup,
|
||||
getSelectedMessage,
|
||||
} from '../selectors/conversations';
|
||||
|
@ -24,6 +29,8 @@ import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
|||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
@ -80,6 +87,60 @@ function renderTypingBubble(id: string): JSX.Element {
|
|||
return <FilteredSmartTypingBubble id={id} />;
|
||||
}
|
||||
|
||||
const getWarning = (
|
||||
conversation: Readonly<ConversationType>,
|
||||
state: Readonly<StateType>
|
||||
): undefined | TimelineWarningType => {
|
||||
if (
|
||||
conversation.type === 'direct' &&
|
||||
!conversation.acceptedMessageRequest &&
|
||||
!conversation.isBlocked
|
||||
) {
|
||||
const getConversationsWithTitle = getConversationsByTitleSelector(state);
|
||||
const conversationsWithSameTitle = getConversationsWithTitle(
|
||||
conversation.title
|
||||
);
|
||||
assert(
|
||||
conversationsWithSameTitle.length,
|
||||
'Expected at least 1 conversation with the same title (this one)'
|
||||
);
|
||||
|
||||
const safeConversation = conversationsWithSameTitle.find(
|
||||
otherConversation =>
|
||||
otherConversation.acceptedMessageRequest &&
|
||||
otherConversation.type === 'direct' &&
|
||||
otherConversation.id !== conversation.id
|
||||
);
|
||||
|
||||
return safeConversation ? { safeConversation } : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getContactSpoofingReview = (
|
||||
selectedConversationId: string,
|
||||
state: Readonly<StateType>
|
||||
):
|
||||
| undefined
|
||||
| {
|
||||
possiblyUnsafeConversation: ConversationType;
|
||||
safeConversation: ConversationType;
|
||||
} => {
|
||||
const { contactSpoofingReview } = state.conversations;
|
||||
if (!contactSpoofingReview) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
return {
|
||||
possiblyUnsafeConversation: conversationSelector(selectedConversationId),
|
||||
safeConversation: conversationSelector(
|
||||
contactSpoofingReview.safeConversationId
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id, ...actions } = props;
|
||||
|
||||
|
@ -99,6 +160,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
state
|
||||
),
|
||||
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
|
||||
|
||||
warning: getWarning(conversation, state),
|
||||
contactSpoofingReview: getContactSpoofingReview(id, state),
|
||||
|
||||
i18n: getIntl(state),
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
getMaximumGroupSizeModalState,
|
||||
getPlaceholderContact,
|
||||
getRecommendedGroupSizeModalState,
|
||||
getConversationsByTitleSelector,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationId,
|
||||
hasGroupCreationError,
|
||||
|
@ -1288,6 +1289,35 @@ describe('both/state/selectors/conversations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getConversationsByTitleSelector', () => {
|
||||
it('returns a selector that finds conversations by title', () => {
|
||||
const state = {
|
||||
...getEmptyRootState(),
|
||||
conversations: {
|
||||
...getEmptyState(),
|
||||
conversationLookup: {
|
||||
abc: { ...getDefaultConversation('abc'), title: 'Janet' },
|
||||
def: { ...getDefaultConversation('def'), title: 'Janet' },
|
||||
geh: { ...getDefaultConversation('geh'), title: 'Rick' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const selector = getConversationsByTitleSelector(state);
|
||||
|
||||
assert.sameMembers(
|
||||
selector('Janet').map(c => c.id),
|
||||
['abc', 'def']
|
||||
);
|
||||
assert.sameMembers(
|
||||
selector('Rick').map(c => c.id),
|
||||
['geh']
|
||||
);
|
||||
assert.isEmpty(selector('abc'));
|
||||
assert.isEmpty(selector('xyz'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSelectedConversationId', () => {
|
||||
it('returns undefined if no conversation is selected', () => {
|
||||
const state = {
|
||||
|
|
|
@ -31,6 +31,7 @@ const {
|
|||
clearGroupCreationError,
|
||||
clearInvitedConversationsForNewlyCreatedGroup,
|
||||
closeCantAddContactToGroupModal,
|
||||
closeContactSpoofingReview,
|
||||
closeMaximumGroupSizeModal,
|
||||
closeRecommendedGroupSizeModal,
|
||||
createGroup,
|
||||
|
@ -47,6 +48,7 @@ const {
|
|||
startComposing,
|
||||
showChooseGroupMembers,
|
||||
startSettingGroupMetadata,
|
||||
reviewMessageRequestNameCollision,
|
||||
toggleConversationInChooseMembers,
|
||||
} = actions;
|
||||
|
||||
|
@ -550,6 +552,29 @@ describe('both/state/ducks/conversations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('CLOSE_CONTACT_SPOOFING_REVIEW', () => {
|
||||
it('closes the contact spoofing review modal if it was open', () => {
|
||||
const state = {
|
||||
...getEmptyState(),
|
||||
contactSpoofingReview: {
|
||||
safeConversationId: 'abc123',
|
||||
},
|
||||
};
|
||||
const action = closeContactSpoofingReview();
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.isUndefined(actual.contactSpoofingReview);
|
||||
});
|
||||
|
||||
it("does nothing if the modal wasn't already open", () => {
|
||||
const state = getEmptyState();
|
||||
const action = closeContactSpoofingReview();
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.deepEqual(actual, state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLOSE_MAXIMUM_GROUP_SIZE_MODAL', () => {
|
||||
it('closes the maximum group size modal if it was open', () => {
|
||||
const state = {
|
||||
|
@ -1151,6 +1176,20 @@ describe('both/state/ducks/conversations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('REVIEW_MESSAGE_REQUEST_NAME_COLLISION', () => {
|
||||
it('starts reviewing a message request name collision', () => {
|
||||
const state = getEmptyState();
|
||||
const action = reviewMessageRequestNameCollision({
|
||||
safeConversationId: 'def',
|
||||
});
|
||||
const actual = reducer(state, action);
|
||||
|
||||
assert.deepEqual(actual.contactSpoofingReview, {
|
||||
safeConversationId: 'def',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_COMPOSE_GROUP_AVATAR', () => {
|
||||
it("can clear the composer's group avatar", () => {
|
||||
const state = {
|
||||
|
|
|
@ -16654,7 +16654,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Timeline.js",
|
||||
"line": " this.listRef = react_1.default.createRef();",
|
||||
"lineNumber": 33,
|
||||
"lineNumber": 39,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||
|
@ -16927,4 +16927,4 @@
|
|||
"updated": "2021-01-08T15:46:32.143Z",
|
||||
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
|
||||
}
|
||||
]
|
||||
]
|
|
@ -779,6 +779,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
setupTimeline() {
|
||||
const { id } = this.model;
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const contactSupport = () => {
|
||||
const baseUrl =
|
||||
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
||||
|
@ -950,6 +953,28 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
loadAndScroll: this.loadAndScroll.bind(this),
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
onBlock: () => {
|
||||
this.syncMessageRequestResponse('onBlock', messageRequestEnum.BLOCK);
|
||||
},
|
||||
onBlockAndDelete: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onBlockAndDelete',
|
||||
messageRequestEnum.BLOCK_AND_DELETE
|
||||
);
|
||||
},
|
||||
onDelete: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onDelete',
|
||||
messageRequestEnum.DELETE
|
||||
);
|
||||
},
|
||||
onUnblock: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onUnblock',
|
||||
messageRequestEnum.ACCEPT
|
||||
);
|
||||
},
|
||||
onShowContactModal: this.showContactModal.bind(this),
|
||||
scrollToQuotedMessage,
|
||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||
}),
|
||||
|
|
Loading…
Reference in a new issue