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": {
|
"ForwardMessageModal--continue": {
|
||||||
"message": "Continue",
|
"message": "Continue",
|
||||||
"description": "aria-label for the 'next' button in the forward a message modal dialog"
|
"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/Button.scss';
|
||||||
@import './components/ContactPill.scss';
|
@import './components/ContactPill.scss';
|
||||||
@import './components/ContactPills.scss';
|
@import './components/ContactPills.scss';
|
||||||
|
@import './components/ContactSpoofingReviewDialog.scss';
|
||||||
|
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
||||||
@import './components/ConversationHeader.scss';
|
@import './components/ConversationHeader.scss';
|
||||||
@import './components/EditConversationAttributesModal.scss';
|
@import './components/EditConversationAttributesModal.scss';
|
||||||
@import './components/ForwardMessageModal.scss';
|
@import './components/ForwardMessageModal.scss';
|
||||||
|
@ -43,3 +45,5 @@
|
||||||
@import './components/SafetyNumberViewer.scss';
|
@import './components/SafetyNumberViewer.scss';
|
||||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||||
|
@import './components/TimelineWarning.scss';
|
||||||
|
@import './components/TimelineWarnings.scss';
|
||||||
|
|
|
@ -13,6 +13,7 @@ type PropsType = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
hasXButton?: boolean;
|
hasXButton?: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
moduleClassName?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
|
@ -22,6 +23,7 @@ export function Modal({
|
||||||
children,
|
children,
|
||||||
hasXButton,
|
hasXButton,
|
||||||
i18n,
|
i18n,
|
||||||
|
moduleClassName,
|
||||||
onClose = noop,
|
onClose = noop,
|
||||||
title,
|
title,
|
||||||
theme,
|
theme,
|
||||||
|
@ -35,7 +37,8 @@ export function Modal({
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-Modal',
|
'module-Modal',
|
||||||
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header'
|
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header',
|
||||||
|
moduleClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{hasHeader && (
|
{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 { PropsType, Timeline } from './Timeline';
|
||||||
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
||||||
import { ConversationHero } from './ConversationHero';
|
import { ConversationHero } from './ConversationHero';
|
||||||
|
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||||
import { LastSeenIndicator } from './LastSeenIndicator';
|
import { LastSeenIndicator } from './LastSeenIndicator';
|
||||||
import { TimelineLoadingRow } from './TimelineLoadingRow';
|
import { TimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
import { TypingBubble } from './TypingBubble';
|
import { TypingBubble } from './TypingBubble';
|
||||||
|
@ -260,6 +261,16 @@ const actions = () => ({
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
|
|
||||||
contactSupport: action('contactSupport'),
|
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) => (
|
const renderItem = (id: string) => (
|
||||||
|
@ -330,6 +341,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
undefined,
|
undefined,
|
||||||
invitedContactsForNewlyCreatedGroup:
|
invitedContactsForNewlyCreatedGroup:
|
||||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||||
|
warning: overrideProps.warning,
|
||||||
|
|
||||||
id: '',
|
id: '',
|
||||||
renderItem,
|
renderItem,
|
||||||
|
@ -419,3 +431,14 @@ story.add('With invited contacts for a newly-created group', () => {
|
||||||
|
|
||||||
return <Timeline {...props} />;
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { debounce, get, isNumber } from 'lodash';
|
import { debounce, get, isNumber } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
AutoSizer,
|
AutoSizer,
|
||||||
CellMeasurer,
|
CellMeasurer,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
List,
|
List,
|
||||||
Grid,
|
Grid,
|
||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
|
import Measure from 'react-measure';
|
||||||
|
|
||||||
import { ScrollDownButton } from './ScrollDownButton';
|
import { ScrollDownButton } from './ScrollDownButton';
|
||||||
|
|
||||||
|
@ -18,10 +19,15 @@ import { GlobalAudioProvider } from '../GlobalAudioContext';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
import { assert } from '../../util/assert';
|
||||||
|
|
||||||
import { PropsActions as MessageActionsType } from './Message';
|
import { PropsActions as MessageActionsType } from './Message';
|
||||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||||
|
import { Intl } from '../Intl';
|
||||||
|
import { TimelineWarning } from './TimelineWarning';
|
||||||
|
import { TimelineWarnings } from './TimelineWarnings';
|
||||||
import { NewlyCreatedGroupInvitedContactsDialog } from '../NewlyCreatedGroupInvitedContactsDialog';
|
import { NewlyCreatedGroupInvitedContactsDialog } from '../NewlyCreatedGroupInvitedContactsDialog';
|
||||||
|
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||||
|
|
||||||
const AT_BOTTOM_THRESHOLD = 15;
|
const AT_BOTTOM_THRESHOLD = 15;
|
||||||
const NEAR_BOTTOM_THRESHOLD = 15;
|
const NEAR_BOTTOM_THRESHOLD = 15;
|
||||||
|
@ -30,6 +36,10 @@ const LOAD_MORE_THRESHOLD = 30;
|
||||||
const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
|
const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
|
||||||
export const LOAD_COUNTDOWN = 1;
|
export const LOAD_COUNTDOWN = 1;
|
||||||
|
|
||||||
|
export type WarningType = {
|
||||||
|
safeConversation: ConversationType;
|
||||||
|
};
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
haveNewest: boolean;
|
haveNewest: boolean;
|
||||||
haveOldest: boolean;
|
haveOldest: boolean;
|
||||||
|
@ -54,6 +64,12 @@ type PropsHousekeepingType = {
|
||||||
selectedMessageId?: string;
|
selectedMessageId?: string;
|
||||||
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
|
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
|
||||||
|
|
||||||
|
warning?: WarningType;
|
||||||
|
contactSpoofingReview?: {
|
||||||
|
possiblyUnsafeConversation: ConversationType;
|
||||||
|
safeConversation: ConversationType;
|
||||||
|
};
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
renderItem: (
|
renderItem: (
|
||||||
|
@ -74,17 +90,27 @@ type PropsHousekeepingType = {
|
||||||
type PropsActionsType = {
|
type PropsActionsType = {
|
||||||
clearChangedMessages: (conversationId: string) => unknown;
|
clearChangedMessages: (conversationId: string) => unknown;
|
||||||
clearInvitedConversationsForNewlyCreatedGroup: () => void;
|
clearInvitedConversationsForNewlyCreatedGroup: () => void;
|
||||||
|
closeContactSpoofingReview: () => void;
|
||||||
setLoadCountdownStart: (
|
setLoadCountdownStart: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
loadCountdownStart?: number
|
loadCountdownStart?: number
|
||||||
) => unknown;
|
) => unknown;
|
||||||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||||
|
reviewMessageRequestNameCollision: (
|
||||||
|
_: Readonly<{
|
||||||
|
safeConversationId: string;
|
||||||
|
}>
|
||||||
|
) => void;
|
||||||
|
|
||||||
loadAndScroll: (messageId: string) => unknown;
|
loadAndScroll: (messageId: string) => unknown;
|
||||||
loadOlderMessages: (messageId: string) => unknown;
|
loadOlderMessages: (messageId: string) => unknown;
|
||||||
loadNewerMessages: (messageId: string) => unknown;
|
loadNewerMessages: (messageId: string) => unknown;
|
||||||
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
|
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
|
||||||
markMessageRead: (messageId: string) => unknown;
|
markMessageRead: (messageId: string) => unknown;
|
||||||
|
onBlock: () => unknown;
|
||||||
|
onBlockAndDelete: () => unknown;
|
||||||
|
onDelete: () => unknown;
|
||||||
|
onUnblock: () => unknown;
|
||||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
clearSelectedMessage: () => unknown;
|
clearSelectedMessage: () => unknown;
|
||||||
updateSharedGroups: () => unknown;
|
updateSharedGroups: () => unknown;
|
||||||
|
@ -142,6 +168,9 @@ type StateType = {
|
||||||
|
|
||||||
shouldShowScrollDownButton: boolean;
|
shouldShowScrollDownButton: boolean;
|
||||||
areUnreadBelowCurrentPosition: boolean;
|
areUnreadBelowCurrentPosition: boolean;
|
||||||
|
|
||||||
|
hasDismissedWarning: boolean;
|
||||||
|
lastMeasuredWarningHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Timeline extends React.PureComponent<PropsType, StateType> {
|
export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
|
@ -178,6 +207,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
prevPropScrollToIndex: scrollToIndex,
|
prevPropScrollToIndex: scrollToIndex,
|
||||||
shouldShowScrollDownButton: false,
|
shouldShowScrollDownButton: false,
|
||||||
areUnreadBelowCurrentPosition: false,
|
areUnreadBelowCurrentPosition: false,
|
||||||
|
hasDismissedWarning: false,
|
||||||
|
lastMeasuredWarningHeight: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,6 +585,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
renderTypingBubble,
|
renderTypingBubble,
|
||||||
updateSharedGroups,
|
updateSharedGroups,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { lastMeasuredWarningHeight } = this.state;
|
||||||
|
|
||||||
const styleWithWidth = {
|
const styleWithWidth = {
|
||||||
...style,
|
...style,
|
||||||
|
@ -562,11 +594,14 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
const row = index;
|
const row = index;
|
||||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||||
const typingBubbleRow = this.getTypingBubbleRow();
|
const typingBubbleRow = this.getTypingBubbleRow();
|
||||||
let rowContents;
|
let rowContents: ReactNode;
|
||||||
|
|
||||||
if (haveOldest && row === 0) {
|
if (haveOldest && row === 0) {
|
||||||
rowContents = (
|
rowContents = (
|
||||||
<div data-row={row} style={styleWithWidth} role="row">
|
<div data-row={row} style={styleWithWidth} role="row">
|
||||||
|
{this.getWarning() ? (
|
||||||
|
<div style={{ height: lastMeasuredWarningHeight }} />
|
||||||
|
) : null}
|
||||||
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
|
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -802,7 +837,10 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
window.unregisterForActive(this.updateWithVisibleRows);
|
window.unregisterForActive(this.updateWithVisibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: PropsType): void {
|
public componentDidUpdate(
|
||||||
|
prevProps: Readonly<PropsType>,
|
||||||
|
prevState: Readonly<StateType>
|
||||||
|
): void {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
clearChangedMessages,
|
clearChangedMessages,
|
||||||
|
@ -814,6 +852,15 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
typingContact,
|
typingContact,
|
||||||
} = this.props;
|
} = 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
|
// 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
|
// heights previously calculated. We reset the minimum number of rows to minimize
|
||||||
// unexpected changes to the scroll position. Those changes happen because
|
// 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 {
|
public render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
clearInvitedConversationsForNewlyCreatedGroup,
|
clearInvitedConversationsForNewlyCreatedGroup,
|
||||||
|
closeContactSpoofingReview,
|
||||||
|
contactSpoofingReview,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
items,
|
|
||||||
isGroupV1AndDisabled,
|
|
||||||
invitedContactsForNewlyCreatedGroup,
|
invitedContactsForNewlyCreatedGroup,
|
||||||
|
isGroupV1AndDisabled,
|
||||||
|
items,
|
||||||
|
onBlock,
|
||||||
|
onBlockAndDelete,
|
||||||
|
onDelete,
|
||||||
|
onUnblock,
|
||||||
|
showContactModal,
|
||||||
|
reviewMessageRequestNameCollision,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
shouldShowScrollDownButton,
|
shouldShowScrollDownButton,
|
||||||
|
@ -1127,6 +1182,57 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
</AutoSizer>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
@ -1139,6 +1245,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
>
|
>
|
||||||
|
{timelineWarning}
|
||||||
|
|
||||||
<GlobalAudioProvider conversationId={id}>
|
<GlobalAudioProvider conversationId={id}>
|
||||||
{autoSizer}
|
{autoSizer}
|
||||||
</GlobalAudioProvider>
|
</GlobalAudioProvider>
|
||||||
|
@ -1159,7 +1267,33 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
onClose={clearInvitedConversationsForNewlyCreatedGroup}
|
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 }
|
| { isCreating: true; hasError: false }
|
||||||
));
|
));
|
||||||
|
|
||||||
|
type ContactSpoofingReviewStateType = {
|
||||||
|
safeConversationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConversationsStateType = {
|
export type ConversationsStateType = {
|
||||||
preJoinConversation?: PreJoinConversationType;
|
preJoinConversation?: PreJoinConversationType;
|
||||||
invitedConversationIdsForNewlyCreatedGroup?: Array<string>;
|
invitedConversationIdsForNewlyCreatedGroup?: Array<string>;
|
||||||
|
@ -289,6 +293,7 @@ export type ConversationsStateType = {
|
||||||
selectedConversationPanelDepth: number;
|
selectedConversationPanelDepth: number;
|
||||||
showArchived: boolean;
|
showArchived: boolean;
|
||||||
composer?: ComposerStateType;
|
composer?: ComposerStateType;
|
||||||
|
contactSpoofingReview?: ContactSpoofingReviewStateType;
|
||||||
|
|
||||||
// Note: it's very important that both of these locations are always kept up to date
|
// Note: it's very important that both of these locations are always kept up to date
|
||||||
messagesLookup: MessageLookupType;
|
messagesLookup: MessageLookupType;
|
||||||
|
@ -335,6 +340,9 @@ type ClearInvitedConversationsForNewlyCreatedGroupActionType = {
|
||||||
type CloseCantAddContactToGroupModalActionType = {
|
type CloseCantAddContactToGroupModalActionType = {
|
||||||
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
|
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
|
||||||
};
|
};
|
||||||
|
type CloseContactSpoofingReviewActionType = {
|
||||||
|
type: 'CLOSE_CONTACT_SPOOFING_REVIEW';
|
||||||
|
};
|
||||||
type CloseMaximumGroupSizeModalActionType = {
|
type CloseMaximumGroupSizeModalActionType = {
|
||||||
type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL';
|
type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL';
|
||||||
};
|
};
|
||||||
|
@ -512,6 +520,12 @@ export type SelectedConversationChangedActionType = {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type ReviewMessageRequestNameCollisionActionType = {
|
||||||
|
type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION';
|
||||||
|
payload: {
|
||||||
|
safeConversationId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
type ShowInboxActionType = {
|
type ShowInboxActionType = {
|
||||||
type: 'SHOW_INBOX';
|
type: 'SHOW_INBOX';
|
||||||
payload: null;
|
payload: null;
|
||||||
|
@ -569,6 +583,7 @@ export type ConversationActionType =
|
||||||
| ClearSelectedMessageActionType
|
| ClearSelectedMessageActionType
|
||||||
| ClearUnreadMetricsActionType
|
| ClearUnreadMetricsActionType
|
||||||
| CloseCantAddContactToGroupModalActionType
|
| CloseCantAddContactToGroupModalActionType
|
||||||
|
| CloseContactSpoofingReviewActionType
|
||||||
| CloseMaximumGroupSizeModalActionType
|
| CloseMaximumGroupSizeModalActionType
|
||||||
| CloseRecommendedGroupSizeModalActionType
|
| CloseRecommendedGroupSizeModalActionType
|
||||||
| ConversationAddedActionType
|
| ConversationAddedActionType
|
||||||
|
@ -587,6 +602,7 @@ export type ConversationActionType =
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
| RepairNewestMessageActionType
|
| RepairNewestMessageActionType
|
||||||
| RepairOldestMessageActionType
|
| RepairOldestMessageActionType
|
||||||
|
| ReviewMessageRequestNameCollisionActionType
|
||||||
| ScrollToMessageActionType
|
| ScrollToMessageActionType
|
||||||
| SelectedConversationChangedActionType
|
| SelectedConversationChangedActionType
|
||||||
| SetComposeGroupAvatarActionType
|
| SetComposeGroupAvatarActionType
|
||||||
|
@ -617,6 +633,7 @@ export const actions = {
|
||||||
clearSelectedMessage,
|
clearSelectedMessage,
|
||||||
clearUnreadMetrics,
|
clearUnreadMetrics,
|
||||||
closeCantAddContactToGroupModal,
|
closeCantAddContactToGroupModal,
|
||||||
|
closeContactSpoofingReview,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
conversationAdded,
|
conversationAdded,
|
||||||
|
@ -634,6 +651,7 @@ export const actions = {
|
||||||
removeAllConversations,
|
removeAllConversations,
|
||||||
repairNewestMessage,
|
repairNewestMessage,
|
||||||
repairOldestMessage,
|
repairOldestMessage,
|
||||||
|
reviewMessageRequestNameCollision,
|
||||||
scrollToMessage,
|
scrollToMessage,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
setComposeGroupAvatar,
|
setComposeGroupAvatar,
|
||||||
|
@ -863,6 +881,14 @@ function repairOldestMessage(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reviewMessageRequestNameCollision(
|
||||||
|
payload: Readonly<{
|
||||||
|
safeConversationId: string;
|
||||||
|
}>
|
||||||
|
): ReviewMessageRequestNameCollisionActionType {
|
||||||
|
return { type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION', payload };
|
||||||
|
}
|
||||||
|
|
||||||
function messagesReset(
|
function messagesReset(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messages: Array<MessageType>,
|
messages: Array<MessageType>,
|
||||||
|
@ -977,6 +1003,9 @@ function clearUnreadMetrics(
|
||||||
function closeCantAddContactToGroupModal(): CloseCantAddContactToGroupModalActionType {
|
function closeCantAddContactToGroupModal(): CloseCantAddContactToGroupModalActionType {
|
||||||
return { type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL' };
|
return { type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL' };
|
||||||
}
|
}
|
||||||
|
function closeContactSpoofingReview(): CloseContactSpoofingReviewActionType {
|
||||||
|
return { type: 'CLOSE_CONTACT_SPOOFING_REVIEW' };
|
||||||
|
}
|
||||||
function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
||||||
return { type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL' };
|
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') {
|
if (action.type === 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL') {
|
||||||
return closeComposerModal(state, 'maximumGroupSizeModalState' as const);
|
return closeComposerModal(state, 'maximumGroupSizeModalState' as const);
|
||||||
}
|
}
|
||||||
|
@ -1379,7 +1412,8 @@ export function reducer(
|
||||||
const { id, data } = payload;
|
const { id, data } = payload;
|
||||||
const { conversationLookup } = state;
|
const { conversationLookup } = state;
|
||||||
|
|
||||||
let { showArchived, selectedConversationId } = state;
|
const { selectedConversationId } = state;
|
||||||
|
let { showArchived } = state;
|
||||||
|
|
||||||
const existing = conversationLookup[id];
|
const existing = conversationLookup[id];
|
||||||
// In the change case we only modify the lookup if we already had that conversation
|
// In the change case we only modify the lookup if we already had that conversation
|
||||||
|
@ -1387,6 +1421,8 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keysToOmit: Array<keyof ConversationsStateType> = [];
|
||||||
|
|
||||||
if (selectedConversationId === id) {
|
if (selectedConversationId === id) {
|
||||||
// Archived -> Inbox: we go back to the normal inbox view
|
// Archived -> Inbox: we go back to the normal inbox view
|
||||||
if (existing.isArchived && !data.isArchived) {
|
if (existing.isArchived && !data.isArchived) {
|
||||||
|
@ -1397,12 +1433,16 @@ export function reducer(
|
||||||
// behavior - no selected conversation in the left pane, but a conversation show
|
// behavior - no selected conversation in the left pane, but a conversation show
|
||||||
// in the right pane.
|
// in the right pane.
|
||||||
if (!existing.isArchived && data.isArchived) {
|
if (!existing.isArchived && data.isArchived) {
|
||||||
selectedConversationId = undefined;
|
keysToOmit.push('selectedConversationId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.isBlocked && data.isBlocked) {
|
||||||
|
keysToOmit.push('contactSpoofingReview');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...omit(state, keysToOmit),
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
showArchived,
|
showArchived,
|
||||||
conversationLookup: {
|
conversationLookup: {
|
||||||
|
@ -1444,7 +1484,7 @@ export function reducer(
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...omit(state, 'contactSpoofingReview'),
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
selectedConversationPanelDepth: 0,
|
selectedConversationPanelDepth: 0,
|
||||||
messagesLookup: omit(state.messagesLookup, messageIds),
|
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') {
|
if (action.type === 'MESSAGES_ADDED') {
|
||||||
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
@ -2059,7 +2106,7 @@ export function reducer(
|
||||||
const { id } = payload;
|
const { id } = payload;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...omit(state, 'contactSpoofingReview'),
|
||||||
selectedConversationId: id,
|
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(
|
export const getSelectedConversationId = createSelector(
|
||||||
getConversations,
|
getConversations,
|
||||||
(state: ConversationsStateType): string | undefined => {
|
(state: ConversationsStateType): string | undefined => {
|
||||||
|
|
|
@ -5,13 +5,18 @@ import { pick } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { Timeline } from '../../components/conversation/Timeline';
|
import {
|
||||||
|
Timeline,
|
||||||
|
WarningType as TimelineWarningType,
|
||||||
|
} from '../../components/conversation/Timeline';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
import { ConversationType } from '../ducks/conversations';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import {
|
import {
|
||||||
getConversationMessagesSelector,
|
getConversationMessagesSelector,
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
|
getConversationsByTitleSelector,
|
||||||
getInvitedContactsForNewlyCreatedGroup,
|
getInvitedContactsForNewlyCreatedGroup,
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
|
@ -24,6 +29,8 @@ import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
|
|
||||||
|
import { assert } from '../../util/assert';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
@ -80,6 +87,60 @@ function renderTypingBubble(id: string): JSX.Element {
|
||||||
return <FilteredSmartTypingBubble id={id} />;
|
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 mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const { id, ...actions } = props;
|
const { id, ...actions } = props;
|
||||||
|
|
||||||
|
@ -99,6 +160,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
state
|
state
|
||||||
),
|
),
|
||||||
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
|
selectedMessageId: selectedMessage ? selectedMessage.id : undefined,
|
||||||
|
|
||||||
|
warning: getWarning(conversation, state),
|
||||||
|
contactSpoofingReview: getContactSpoofingReview(id, state),
|
||||||
|
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
renderItem,
|
renderItem,
|
||||||
renderLastSeenIndicator,
|
renderLastSeenIndicator,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
getMaximumGroupSizeModalState,
|
getMaximumGroupSizeModalState,
|
||||||
getPlaceholderContact,
|
getPlaceholderContact,
|
||||||
getRecommendedGroupSizeModalState,
|
getRecommendedGroupSizeModalState,
|
||||||
|
getConversationsByTitleSelector,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
getSelectedConversationId,
|
getSelectedConversationId,
|
||||||
hasGroupCreationError,
|
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', () => {
|
describe('#getSelectedConversationId', () => {
|
||||||
it('returns undefined if no conversation is selected', () => {
|
it('returns undefined if no conversation is selected', () => {
|
||||||
const state = {
|
const state = {
|
||||||
|
|
|
@ -31,6 +31,7 @@ const {
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
clearInvitedConversationsForNewlyCreatedGroup,
|
clearInvitedConversationsForNewlyCreatedGroup,
|
||||||
closeCantAddContactToGroupModal,
|
closeCantAddContactToGroupModal,
|
||||||
|
closeContactSpoofingReview,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
createGroup,
|
createGroup,
|
||||||
|
@ -47,6 +48,7 @@ const {
|
||||||
startComposing,
|
startComposing,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
|
reviewMessageRequestNameCollision,
|
||||||
toggleConversationInChooseMembers,
|
toggleConversationInChooseMembers,
|
||||||
} = actions;
|
} = 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', () => {
|
describe('CLOSE_MAXIMUM_GROUP_SIZE_MODAL', () => {
|
||||||
it('closes the maximum group size modal if it was open', () => {
|
it('closes the maximum group size modal if it was open', () => {
|
||||||
const state = {
|
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', () => {
|
describe('SET_COMPOSE_GROUP_AVATAR', () => {
|
||||||
it("can clear the composer's group avatar", () => {
|
it("can clear the composer's group avatar", () => {
|
||||||
const state = {
|
const state = {
|
||||||
|
|
|
@ -16654,7 +16654,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Timeline.js",
|
"path": "ts/components/conversation/Timeline.js",
|
||||||
"line": " this.listRef = react_1.default.createRef();",
|
"line": " this.listRef = react_1.default.createRef();",
|
||||||
"lineNumber": 33,
|
"lineNumber": 39,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-07-31T00:19:18.696Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Timeline needs to interact with its child List directly"
|
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||||
|
@ -16927,4 +16927,4 @@
|
||||||
"updated": "2021-01-08T15:46:32.143Z",
|
"updated": "2021-01-08T15:46:32.143Z",
|
||||||
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
|
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -779,6 +779,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
setupTimeline() {
|
setupTimeline() {
|
||||||
const { id } = this.model;
|
const { id } = this.model;
|
||||||
|
|
||||||
|
const messageRequestEnum =
|
||||||
|
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||||
|
|
||||||
const contactSupport = () => {
|
const contactSupport = () => {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
'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),
|
loadAndScroll: this.loadAndScroll.bind(this),
|
||||||
loadOlderMessages,
|
loadOlderMessages,
|
||||||
markMessageRead,
|
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,
|
scrollToQuotedMessage,
|
||||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in a new issue