ConversationView in React

This commit is contained in:
Josh Perez 2021-10-05 12:47:06 -04:00 committed by GitHub
parent dddb3129cc
commit 5fdfa1c632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 703 additions and 786 deletions

View File

@ -83,18 +83,7 @@
</script> </script>
<script type="text/x-tmpl-mustache" id="conversation"> <script type="text/x-tmpl-mustache" id="conversation">
<div class='conversation-header'></div> <div class="ConversationView__template"></div>
<div class='main panel'>
<div class='timeline-placeholder' aria-live='polite'></div>
<div class='bottom-bar' id='footer'>
<div class='compose'>
<form class='send clearfix file-input'>
<input type="file" class="file-input" multiple="multiple">
<div class='CompositionArea__placeholder'></div>
</form>
</div>
</div>
</div>
</script> </script>
<script type="text/x-tmpl-mustache" id="recorder"> <script type="text/x-tmpl-mustache" id="recorder">

View File

@ -59,19 +59,12 @@ const {
const { WhatsNew } = require('../../ts/components/WhatsNew'); const { WhatsNew } = require('../../ts/components/WhatsNew');
// State // State
const { createTimeline } = require('../../ts/state/roots/createTimeline');
const { const {
createChatColorPicker, createChatColorPicker,
} = require('../../ts/state/roots/createChatColorPicker'); } = require('../../ts/state/roots/createChatColorPicker');
const {
createCompositionArea,
} = require('../../ts/state/roots/createCompositionArea');
const { const {
createConversationDetails, createConversationDetails,
} = require('../../ts/state/roots/createConversationDetails'); } = require('../../ts/state/roots/createConversationDetails');
const {
createConversationHeader,
} = require('../../ts/state/roots/createConversationHeader');
const { createApp } = require('../../ts/state/roots/createApp'); const { createApp } = require('../../ts/state/roots/createApp');
const { const {
createForwardMessageModal, createForwardMessageModal,
@ -352,9 +345,7 @@ exports.setup = (options = {}) => {
const Roots = { const Roots = {
createApp, createApp,
createChatColorPicker, createChatColorPicker,
createCompositionArea,
createConversationDetails, createConversationDetails,
createConversationHeader,
createForwardMessageModal, createForwardMessageModal,
createGroupLinkManagement, createGroupLinkManagement,
createGroupV1MigrationModal, createGroupV1MigrationModal,
@ -368,7 +359,6 @@ exports.setup = (options = {}) => {
createShortcutGuideModal, createShortcutGuideModal,
createStickerManager, createStickerManager,
createStickerPreviewModal, createStickerPreviewModal,
createTimeline,
}; };
const Ducks = { const Ducks = {

View File

@ -6,12 +6,10 @@
@keyframes panel--in { @keyframes panel--in {
from { from {
transform: translateX(500px); transform: translateX(500px);
opacity: 0;
} }
to { to {
transform: translateX(0); transform: translateX(0);
opacity: 1;
} }
} }
@ -32,12 +30,12 @@
.panel { .panel {
height: calc(100% - #{$header-height} - var(--title-bar-drag-area-height)); height: calc(100% - #{$header-height} - var(--title-bar-drag-area-height));
overflow-y: scroll;
z-index: 1;
position: absolute;
left: 0; left: 0;
overflow-y: overlay;
position: absolute;
top: calc(#{$header-height} + var(--title-bar-drag-area-height)); top: calc(#{$header-height} + var(--title-bar-drag-area-height));
width: 100%; width: 100%;
z-index: 1;
@include light-theme() { @include light-theme() {
background-color: $color-white; background-color: $color-white;
@ -50,7 +48,7 @@
.panel { .panel {
&:not(.main) { &:not(.main) {
animation: panel--in 250ms ease-out; animation: panel--in 350ms cubic-bezier(0.17, 0.17, 0, 1);
} }
&--static { &--static {
@ -58,43 +56,8 @@
} }
&--remove { &--remove {
transform: translateX(500px); transform: translateX(100%);
opacity: 0; transition: transform 350ms cubic-bezier(0.17, 0.17, 0, 1);
transition: all 250ms ease-out;
}
.container {
padding-top: 20px;
max-width: 750px;
margin: 0 auto;
padding: 20px;
}
}
.main.panel {
display: flex;
flex-direction: column;
overflow: initial;
}
.main.panel {
.timeline-placeholder {
flex-grow: 1;
position: relative;
max-width: 100%;
margin: 0;
.timeline-wrapper {
-webkit-padding-start: 0px;
position: absolute;
top: 0;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow-y: auto;
overflow-x: hidden;
}
} }
} }
} }
@ -120,95 +83,6 @@
padding-bottom: 40px; padding-bottom: 40px;
} }
// We need to use the wrapper because the conversation view calculates the height of all
// things in the composition area. A margin on an inner div won't be included in that
// height calculation.
.bottom-bar .quote-wrapper {
margin-left: 18px;
margin-right: 18px;
margin-top: 3px;
}
.bottom-bar .preview-wrapper {
margin-top: 3px;
margin-left: 12px;
margin-right: 12px;
margin-bottom: 2px;
}
.bottom-bar {
box-sizing: content-box;
$button-width: 36px;
form.active {
textarea {
border: solid 1px $color-ultramarine;
}
}
form.send {
margin-bottom: 0px;
@include light-theme {
background: $color-white;
}
@include dark-theme {
background-color: $color-gray-95;
}
}
.flex {
display: flex;
flex-direction: row;
.send-message {
flex-grow: 1;
}
}
.choose-file {
float: left;
height: 36px;
}
.send-message {
display: block;
max-height: 100px;
padding: 10px;
margin-top: 3px;
margin-bottom: 6px;
border-radius: 20px;
resize: none;
font-size: 1em;
font-family: inherit;
@include light-theme {
background-color: $color-gray-02;
color: $color-gray-95;
border: 1px solid $color-black-alpha-20;
outline: 0;
}
@include dark-theme {
background-color: $color-gray-90;
color: $color-gray-02;
border: 1px solid $color-gray-60;
outline: 0;
}
&[disabled='disabled'] {
background: transparent;
}
}
.capture-audio {
float: right;
height: 36px;
}
.android-length-warning {
padding: 10px;
max-width: 150px;
}
}
.permissions-popup, .permissions-popup,
.debug-log-window { .debug-log-window {
.modal { .modal {

View File

@ -159,55 +159,6 @@ a {
color: $color-ultramarine; color: $color-ultramarine;
} }
.file-input {
position: relative;
.choose-file {
cursor: pointer;
}
.paperclip {
width: 32px;
height: 32px;
padding: 0;
opacity: 0.5;
border: none;
background: transparent;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&:before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
@include light-theme {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-75);
}
@include dark-theme {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-15);
}
}
}
input[type='file'] {
display: none;
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
top: 0;
left: 0;
cursor: pointer;
z-index: 1;
}
}
.group-member-list { .group-member-list {
.container { .container {
outline: none; outline: none;

View File

@ -183,4 +183,34 @@
} }
} }
} }
&__attach-file {
width: 32px;
height: 32px;
padding: 0;
opacity: 0.5;
border: none;
background: transparent;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
&:before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
@include light-theme {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-75);
}
@include dark-theme {
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-15);
}
}
}
} }

View File

@ -0,0 +1,56 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.ConversationView {
display: flex;
flex-direction: column;
overflow: initial;
&__pane {
display: flex;
flex-direction: column;
height: 100%;
overflow: initial;
position: absolute;
width: 100%;
}
&__timeline {
&--container {
flex-grow: 1;
margin: 0;
max-width: 100%;
position: relative;
}
-webkit-padding-start: 0px;
height: 100%;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
padding: 0;
position: absolute;
top: 0;
width: 100%;
}
&__composition-area {
margin-bottom: 6px;
// We need to use the wrapper because the conversation view calculates the height of all
// things in the composition area. A margin on an inner div won't be included in that
// height calculation.
.quote-wrapper {
margin-left: 18px;
margin-right: 18px;
margin-top: 3px;
}
.preview-wrapper {
margin-top: 3px;
margin-left: 12px;
margin-right: 12px;
margin-bottom: 2px;
}
}
}

View File

@ -53,6 +53,7 @@
@import './components/ContactSpoofingReviewDialog.scss'; @import './components/ContactSpoofingReviewDialog.scss';
@import './components/ContactSpoofingReviewDialogPerson.scss'; @import './components/ContactSpoofingReviewDialogPerson.scss';
@import './components/ConversationHeader.scss'; @import './components/ConversationHeader.scss';
@import './components/ConversationView.scss';
@import './components/CustomColorEditor.scss'; @import './components/CustomColorEditor.scss';
@import './components/CustomizingPreferredReactionsModal.scss'; @import './components/CustomizingPreferredReactionsModal.scss';
@import './components/DisappearingTimeDialog.scss'; @import './components/DisappearingTimeDialog.scss';

View File

@ -51,18 +51,7 @@
</script> </script>
<script type="text/x-tmpl-mustache" id="conversation"> <script type="text/x-tmpl-mustache" id="conversation">
<div class='conversation-header'></div> <div id="ConversationView__template"></div>
<div class='main panel'>
<div class='timeline-placeholder' aria-live='polite'></div>
<div class='bottom-bar' id='footer'>
<div class='compose'>
<form class='send clearfix file-input'>
<input type="file" class="file-input" multiple="multiple">
<div class='CompositionArea__placeholder'></div>
</form>
</div>
</div>
</div>
</script> </script>
<script type="text/x-tmpl-mustache" id="recorder"> <script type="text/x-tmpl-mustache" id="recorder">

View File

@ -51,13 +51,15 @@ import { Quote, Props as QuoteProps } from './conversation/Quote';
import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { countStickers } from './stickers/lib'; import { countStickers } from './stickers/lib';
export type CompositionAPIType = { export type CompositionAPIType =
focusInput: () => void; | {
isDirty: () => boolean; focusInput: () => void;
setDisabled: (disabled: boolean) => void; isDirty: () => boolean;
reset: InputApi['reset']; setDisabled: (disabled: boolean) => void;
resetEmojiResults: InputApi['resetEmojiResults']; reset: InputApi['reset'];
}; resetEmojiResults: InputApi['resetEmojiResults'];
}
| undefined;
export type OwnProps = Readonly<{ export type OwnProps = Readonly<{
acceptedMessageRequest?: boolean; acceptedMessageRequest?: boolean;
@ -96,7 +98,7 @@ export type OwnProps = Readonly<{
linkPreviewResult?: LinkPreviewWithDomain; linkPreviewResult?: LinkPreviewWithDomain;
messageRequestsEnabled?: boolean; messageRequestsEnabled?: boolean;
onClearAttachments(): unknown; onClearAttachments(): unknown;
onClickAttachment(): unknown; onClickAttachment(att: AttachmentType): unknown;
onClickQuotedMessage(): unknown; onClickQuotedMessage(): unknown;
onCloseLinkPreview(): unknown; onCloseLinkPreview(): unknown;
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown; processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
@ -325,7 +327,7 @@ export const CompositionArea = ({
setLarge(l => !l); setLarge(l => !l);
}, [setLarge]); }, [setLarge]);
const shouldShowMicrophone = !draftAttachments.length && !draftText; const shouldShowMicrophone = !large && !draftAttachments.length && !draftText;
const showMediaQualitySelector = draftAttachments.some(isImageAttachment); const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
@ -373,14 +375,12 @@ export const CompositionArea = ({
const attButton = ( const attButton = (
<div className="CompositionArea__button-cell"> <div className="CompositionArea__button-cell">
<div className="choose-file"> <button
<button type="button"
type="button" className="CompositionArea__attach-file"
className="paperclip thumbnail" onClick={launchAttachmentPicker}
onClick={launchAttachmentPicker} aria-label={i18n('CompositionArea--attach-file')}
aria-label={i18n('CompositionArea--attach-file')} />
/>
</div>
</div> </div>
); );

View File

@ -0,0 +1,34 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
export type PropsType = {
renderCompositionArea: () => JSX.Element;
renderConversationHeader: () => JSX.Element;
renderTimeline: () => JSX.Element;
};
export const ConversationView = ({
renderCompositionArea,
renderConversationHeader,
renderTimeline,
}: PropsType): JSX.Element => {
return (
<div className="ConversationView">
<div className="ConversationView__header">
{renderConversationHeader()}
</div>
<div className="ConversationView__pane main panel">
<div className="ConversationView__timeline--container">
<div aria-live="polite" className="ConversationView__timeline">
{renderTimeline()}
</div>
</div>
<div className="ConversationView__composition-area">
{renderCompositionArea()}
</div>
</div>
</div>
);
};

View File

@ -157,7 +157,7 @@ export type PropsActionsType = {
clearSelectedMessage: () => unknown; clearSelectedMessage: () => unknown;
unblurAvatar: () => void; unblurAvatar: () => void;
updateSharedGroups: () => unknown; updateSharedGroups: () => unknown;
} & MessageActionsType & } & Omit<MessageActionsType, 'onHeightChange'> &
SafetyNumberActionsType & SafetyNumberActionsType &
UnsupportedMessageActionsType & UnsupportedMessageActionsType &
ChatSessionRefreshedNotificationActionsType; ChatSessionRefreshedNotificationActionsType;
@ -251,7 +251,6 @@ const getActions = createSelector(
'updateSharedGroups', 'updateSharedGroups',
'doubleCheckMissingQuoteReference', 'doubleCheckMissingQuoteReference',
'onHeightChange',
'checkForAccount', 'checkForAccount',
'reactToMessage', 'reactToMessage',
'replyToMessage', 'replyToMessage',

View File

@ -1,24 +0,0 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { SmartCompositionArea } from '../smart/CompositionArea';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredCompositionArea = SmartCompositionArea as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createCompositionArea = (
store: Store,
props: Record<string, unknown>
): React.ReactElement => (
<Provider store={store}>
<FilteredCompositionArea {...props} />
</Provider>
);

View File

@ -1,17 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Store } from 'redux';
import { Provider } from 'react-redux';
import { SmartConversationHeader, OwnProps } from '../smart/ConversationHeader';
export const createConversationHeader = (
store: Store,
props: OwnProps
): React.ReactElement => (
<Provider store={store}>
<SmartConversationHeader {...props} />
</Provider>
);

View File

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
@ -6,19 +6,19 @@ import { Provider } from 'react-redux';
import { Store } from 'redux'; import { Store } from 'redux';
import { SmartTimeline } from '../smart/Timeline'; import { SmartConversationView, PropsType } from '../smart/ConversationView';
// 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 */
const FilteredTimeline = SmartTimeline as any; const FilteredConversationView = SmartConversationView as any;
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export const createTimeline = ( export const createConversationView = (
store: Store, store: Store,
props: Record<string, unknown> props: PropsType
): React.ReactElement => ( ): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredTimeline {...props} /> <FilteredConversationView {...props} />
</Provider> </Provider>
); );

View File

@ -4,7 +4,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { get } from 'lodash'; import { get } from 'lodash';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { CompositionArea } from '../../components/CompositionArea'; import {
CompositionArea,
Props as ComponentPropsType,
} from '../../components/CompositionArea';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { dropNull } from '../../util/dropNull'; import { dropNull } from '../../util/dropNull';
@ -29,11 +32,13 @@ import {
type ExternalProps = { type ExternalProps = {
id: string; id: string;
onClickQuotedMessage: (id: string) => unknown; handleClickQuotedMessage: (id: string) => unknown;
}; };
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
const mapStateToProps = (state: StateType, props: ExternalProps) => { const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id, onClickQuotedMessage } = props; const { id, handleClickQuotedMessage } = props;
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const conversation = conversationSelector(id); const conversation = conversationSelector(id);
@ -101,7 +106,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
onClickQuotedMessage: () => { onClickQuotedMessage: () => {
const messageId = quotedMessage?.quote?.messageId; const messageId = quotedMessage?.quote?.messageId;
if (messageId) { if (messageId) {
onClickQuotedMessage(messageId); handleClickQuotedMessage(messageId);
} }
}, },
// Emojis // Emojis

View File

@ -26,8 +26,11 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
export type OwnProps = { export type OwnProps = {
id: string; id: string;
onArchive: () => void;
onDeleteMessages: () => void; onDeleteMessages: () => void;
onGoBack: () => void; onGoBack: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onOutgoingAudioCallInConversation: () => void; onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void; onOutgoingVideoCallInConversation: () => void;
onResetSession: () => void; onResetSession: () => void;
@ -38,13 +41,9 @@ export type OwnProps = {
onShowAllMedia: () => void; onShowAllMedia: () => void;
onShowChatColorEditor: () => void; onShowChatColorEditor: () => void;
onShowContactModal: (contactId: string) => void; onShowContactModal: (contactId: string) => void;
onShowGroupMembers: () => void;
onArchive: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onShowSafetyNumber: () => void;
onShowConversationDetails: () => void; onShowConversationDetails: () => void;
onShowGroupMembers: () => void;
onShowSafetyNumber: () => void;
}; };
const getOutgoingCallButtonStyle = ( const getOutgoingCallButtonStyle = (

View File

@ -0,0 +1,69 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { ConversationView } from '../../components/conversation/ConversationView';
import { StateType } from '../reducer';
import {
SmartCompositionArea,
CompositionAreaPropsType,
} from './CompositionArea';
import {
SmartConversationHeader,
OwnProps as ConversationHeaderPropsType,
} from './ConversationHeader';
import { SmartTimeline, TimelinePropsType } from './Timeline';
export type PropsType = {
compositionAreaProps: Pick<
CompositionAreaPropsType,
| 'clearQuotedMessage'
| 'compositionApi'
| 'getQuotedMessage'
| 'handleClickQuotedMessage'
| 'id'
| 'onAccept'
| 'onBlock'
| 'onBlockAndReportSpam'
| 'onCancelJoinRequest'
| 'onClearAttachments'
| 'onClickAddPack'
| 'onClickAttachment'
| 'onCloseLinkPreview'
| 'onDelete'
| 'onEditorStateChange'
| 'onPickSticker'
| 'onSelectMediaQuality'
| 'onSendMessage'
| 'onStartGroupMigration'
| 'onTextTooLong'
| 'onUnblock'
| 'openConversation'
>;
conversationHeaderProps: ConversationHeaderPropsType;
timelineProps: TimelinePropsType;
};
const mapStateToProps = (_state: StateType, props: PropsType) => {
const {
compositionAreaProps,
conversationHeaderProps,
timelineProps,
} = props;
return {
renderCompositionArea: () => (
<SmartCompositionArea {...compositionAreaProps} />
),
renderConversationHeader: () => (
<SmartConversationHeader {...conversationHeaderProps} />
),
renderTimeline: () => <SmartTimeline {...timelineProps} />,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationView = smart(ConversationView);

View File

@ -12,6 +12,7 @@ import {
ContactSpoofingReviewPropType, ContactSpoofingReviewPropType,
Timeline, Timeline,
WarningType as TimelineWarningType, WarningType as TimelineWarningType,
PropsType as ComponentPropsType,
} from '../../components/conversation/Timeline'; } from '../../components/conversation/Timeline';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { ConversationType } from '../ducks/conversations'; import { ConversationType } from '../ducks/conversations';
@ -53,6 +54,48 @@ type ExternalProps = {
// are provided by ConversationView in setupTimeline(). // are provided by ConversationView in setupTimeline().
}; };
export type TimelinePropsType = ExternalProps &
Pick<
ComponentPropsType,
| 'acknowledgeGroupMemberNameCollisions'
| 'contactSupport'
| 'deleteMessage'
| 'deleteMessageForEveryone'
| 'displayTapToViewMessage'
| 'downloadAttachment'
| 'downloadNewVersion'
| 'kickOffAttachmentDownload'
| 'learnMoreAboutDeliveryIssue'
| 'loadAndScroll'
| 'loadNewerMessages'
| 'loadNewestMessages'
| 'loadOlderMessages'
| 'markAttachmentAsCorrupted'
| 'markMessageRead'
| 'markViewed'
| 'onBlock'
| 'onBlockAndReportSpam'
| 'onDelete'
| 'onUnblock'
| 'openConversation'
| 'openLink'
| 'reactToMessage'
| 'removeMember'
| 'replyToMessage'
| 'retrySend'
| 'scrollToQuotedMessage'
| 'showContactDetail'
| 'showContactModal'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
| 'showForwardMessageModal'
| 'showIdentity'
| 'showMessageDetail'
| 'showVisualAttachment'
| 'unblurAvatar'
| 'updateSharedGroups'
>;
const createBoundOnHeightChange = memoizee( const createBoundOnHeightChange = memoizee(
( (
onHeightChange: (messageId: string) => unknown, onHeightChange: (messageId: string) => unknown,

View File

@ -13544,20 +13544,6 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z" "updated": "2021-09-15T21:07:50.995Z"
}, },
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "ts/views/inbox_view.js", "path": "ts/views/inbox_view.js",
@ -13642,20 +13628,6 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z" "updated": "2021-09-15T21:07:50.995Z"
}, },
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.ts",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.ts",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "ts/views/inbox_view.ts", "path": "ts/views/inbox_view.ts",
@ -14328,4 +14300,4 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-09-17T21:02:59.414Z" "updated": "2021-09-17T21:02:59.414Z"
} }
] ]

View File

@ -69,6 +69,7 @@ import * as VisualAttachment from '../types/VisualAttachment';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d'; import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d';
import type { EmbeddedContactType } from '../types/EmbeddedContact'; import type { EmbeddedContactType } from '../types/EmbeddedContact';
import { createConversationView } from '../state/roots/createConversationView';
import { AttachmentToastType } from '../types/AttachmentToastType'; import { AttachmentToastType } from '../types/AttachmentToastType';
import { CompositionAPIType } from '../components/CompositionArea'; import { CompositionAPIType } from '../components/CompositionArea';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
@ -225,7 +226,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// Composing messages // Composing messages
private compositionApi: { private compositionApi: {
current?: CompositionAPIType; current: CompositionAPIType;
} = { current: undefined }; } = { current: undefined };
private sendStart?: number; private sendStart?: number;
@ -242,14 +243,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// Sub-views // Sub-views
private captionEditorView?: Backbone.View; private captionEditorView?: Backbone.View;
private compositionAreaView?: Backbone.View;
private contactModalView?: Backbone.View; private contactModalView?: Backbone.View;
private conversationView?: BasicReactWrapperViewClass;
private forwardMessageModal?: Backbone.View; private forwardMessageModal?: Backbone.View;
private lightboxView?: BasicReactWrapperViewClass; private lightboxView?: BasicReactWrapperViewClass;
private migrationDialog?: Backbone.View; private migrationDialog?: Backbone.View;
private stickerPreviewModalView?: Backbone.View; private stickerPreviewModalView?: Backbone.View;
private timelineView?: Backbone.View;
private titleView?: Backbone.View;
// Panel support // Panel support
private panels: Array<AnyViewClass> = []; private panels: Array<AnyViewClass> = [];
@ -314,17 +313,13 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.render(); this.render();
this.setupHeader(); this.setupConversationView();
this.setupTimeline();
this.setupCompositionArea();
this.updateAttachmentsView(); this.updateAttachmentsView();
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
events(): Record<string, string> { events(): Record<string, string> {
return { return {
'change input.file-input': 'onChoseAttachment',
drop: 'onDrop', drop: 'onDrop',
paste: 'onPaste', paste: 'onPaste',
}; };
@ -382,407 +377,116 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
} }
setupHeader(): void { setupConversationView(): void {
this.titleView = new Whisper.ReactWrapperView({ // setupHeader
className: 'title-wrapper', const conversationHeaderProps = {
JSX: window.Signal.State.Roots.createConversationHeader(
window.reduxStore,
{
id: this.model.id,
onShowContactModal: this.showContactModal.bind(this),
onSetDisappearingMessages: (seconds: number) =>
this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(),
onResetSession: () => this.endSession(),
onSearchInConversation: () => {
const { searchInConversation } = window.reduxActions.search;
const name = isMe(this.model.attributes)
? window.i18n('noteToSelf')
: this.model.getTitle();
searchInConversation(this.model.id, name);
},
onSetMuteNotifications: this.setMuteExpiration.bind(this),
onSetPin: this.setPin.bind(this),
// These are view only and don't update the Conversation model, so they
// need a manual update call.
onOutgoingAudioCallInConversation: async () => {
log.info(
'onOutgoingAudioCallInConversation: about to start an audio call'
);
const isVideoCall = false;
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onOutgoingVideoCallInConversation: async () => {
log.info(
'onOutgoingVideoCallInConversation: about to start a video call'
);
const isVideoCall = true;
if (
this.model.get('announcementsOnly') &&
!this.model.areWeAdmin()
) {
showToast(ToastCannotStartGroupCall);
return;
}
if (await this.isCallSafe()) {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onShowChatColorEditor: () => {
this.showChatColorEditor();
},
onShowConversationDetails: () => {
this.showConversationDetails();
},
onShowSafetyNumber: () => {
this.showSafetyNumber();
},
onShowAllMedia: () => {
this.showAllMedia();
},
onShowGroupMembers: () => {
this.showGV1Members();
},
onGoBack: () => {
this.resetPanel();
},
onArchive: () => {
this.model.setArchived(true);
this.model.trigger('unload', 'archive');
showToast(ToastConversationArchived);
},
onMarkUnread: () => {
this.model.setMarkedUnread(true);
showToast(ToastConversationMarkedUnread);
},
onMoveToInbox: () => {
this.model.setArchived(false);
showToast(ToastConversationUnarchived);
},
}
),
});
this.$('.conversation-header').append(this.titleView.el);
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
}
setupCompositionArea(): void {
window.reduxActions.composer.resetComposer();
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
const props = {
id: this.model.id, id: this.model.id,
compositionApi: this.compositionApi,
onClickAddPack: () => this.showStickerManager(), onShowContactModal: this.showContactModal.bind(this),
onPickSticker: (packId: string, stickerId: number) => onSetDisappearingMessages: (seconds: number) =>
this.sendStickerMessage({ packId, stickerId }), this.setDisappearingMessages(seconds),
onEditorStateChange: ( onDeleteMessages: () => this.destroyMessages(),
msg: string, onResetSession: () => this.endSession(),
bodyRanges: Array<BodyRangeType>, onSearchInConversation: () => {
caretLocation?: number const { searchInConversation } = window.reduxActions.search;
) => this.onEditorStateChange(msg, bodyRanges, caretLocation), const name = isMe(this.model.attributes)
onTextTooLong: () => showToast(ToastMessageBodyTooLong), ? window.i18n('noteToSelf')
getQuotedMessage: () => this.model.get('quotedMessageId'), : this.model.getTitle();
clearQuotedMessage: () => this.setQuoteMessage(null), searchInConversation(this.model.id, name);
onAccept: () => { },
this.syncMessageRequestResponse( onSetMuteNotifications: this.setMuteExpiration.bind(this),
'onAccept', onSetPin: this.setPin.bind(this),
this.model, // These are view only and don't update the Conversation model, so they
messageRequestEnum.ACCEPT // need a manual update call.
onOutgoingAudioCallInConversation: async () => {
log.info(
'onOutgoingAudioCallInConversation: about to start an audio call'
); );
const isVideoCall = false;
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
}, },
onBlock: () => {
this.syncMessageRequestResponse( onOutgoingVideoCallInConversation: async () => {
'onBlock', log.info(
this.model, 'onOutgoingVideoCallInConversation: about to start a video call'
messageRequestEnum.BLOCK
); );
}, const isVideoCall = true;
onUnblock: () => {
this.syncMessageRequestResponse( if (this.model.get('announcementsOnly') && !this.model.areWeAdmin()) {
'onUnblock', showToast(ToastCannotStartGroupCall);
this.model, return;
messageRequestEnum.ACCEPT }
);
}, if (await this.isCallSafe()) {
onDelete: () => { log.info(
this.syncMessageRequestResponse( 'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
'onDelete', );
this.model, await window.Signal.Services.calling.startCallingLobby(
messageRequestEnum.DELETE this.model.id,
); isVideoCall
}, );
onBlockAndReportSpam: () => { log.info('onOutgoingVideoCallInConversation: started the call');
this.blockAndReportSpam(this.model); } else {
}, log.info(
onStartGroupMigration: () => this.startMigrationToGV2(), 'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
onCancelJoinRequest: async () => { );
await window.showConfirmationDialog({ }
message: window.i18n(
'GroupV2--join--cancel-request-to-join--confirmation'
),
okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'),
cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'),
resolve: () => {
this.longRunningTaskWrapper({
name: 'onCancelJoinRequest',
task: async () => this.model.cancelJoinRequest(),
});
},
});
}, },
onClickAttachment: this.onClickAttachment.bind(this), onShowChatColorEditor: () => {
onClearAttachments: this.clearAttachments.bind(this), this.showChatColorEditor();
onSelectMediaQuality: (isHQ: boolean) => { },
window.reduxActions.composer.setMediaQualitySetting(isHQ); onShowConversationDetails: () => {
this.showConversationDetails();
},
onShowSafetyNumber: () => {
this.showSafetyNumber();
},
onShowAllMedia: () => {
this.showAllMedia();
},
onShowGroupMembers: () => {
this.showGV1Members();
},
onGoBack: () => {
this.resetPanel();
}, },
onClickQuotedMessage: (id: string) => this.scrollToMessage(id), onArchive: () => {
this.model.setArchived(true);
this.model.trigger('unload', 'archive');
onCloseLinkPreview: () => { showToast(ToastConversationArchived);
this.disableLinkPreviews = true;
this.removeLinkPreview();
}, },
onMarkUnread: () => {
this.model.setMarkedUnread(true);
openConversation: this.openConversation.bind(this), showToast(ToastConversationMarkedUnread);
},
onMoveToInbox: () => {
this.model.setArchived(false);
onSendMessage: ({ showToast(ToastConversationUnarchived);
draftAttachments,
mentions = [],
message = '',
timestamp,
voiceNoteAttachment,
}: {
draftAttachments?: ReadonlyArray<AttachmentType>;
mentions?: BodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
}): void => {
this.sendMessage(message, mentions, {
draftAttachments,
timestamp,
voiceNoteAttachment,
});
}, },
}; };
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
this.compositionAreaView = new Whisper.ReactWrapperView({ // setupTimeline
className: 'composition-area-wrapper',
JSX: window.Signal.State.Roots.createCompositionArea(
window.reduxStore,
props
),
});
// Finally, add it to the DOM
this.$('.CompositionArea__placeholder').append(this.compositionAreaView.el);
}
async longRunningTaskWrapper<T>({
name,
task,
}: {
name: string;
task: () => Promise<T>;
}): Promise<T> {
const idForLogging = this.model.idForLogging();
return window.Signal.Util.longRunningTaskWrapper({
name,
idForLogging,
task,
});
}
getMessageActions(): MessageActionsType {
const reactToMessage = (
messageId: string,
reaction: { emoji: string; remove: boolean }
) => {
this.sendReactionMessage(messageId, reaction);
};
const replyToMessage = (messageId: string) => {
this.setQuoteMessage(messageId);
};
const retrySend = retryMessageSend;
const deleteMessage = (messageId: string) => {
this.deleteMessage(messageId);
};
const deleteMessageForEveryone = (messageId: string) => {
this.deleteMessageForEveryone(messageId);
};
const showMessageDetail = (messageId: string) => {
this.showMessageDetail(messageId);
};
const showContactModal = (contactId: string) => {
this.showContactModal(contactId);
};
const openConversation = (conversationId: string, messageId?: string) => {
this.openConversation(conversationId, messageId);
};
const showContactDetail = (options: {
contact: EmbeddedContactType;
signalAccount?: string;
}) => {
this.showContactDetail(options);
};
const kickOffAttachmentDownload = async (
options: Readonly<{ messageId: string }>
) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
}
await message.queueAttachmentDownloads();
};
const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment);
};
const onMarkViewed = (messageId: string): void => {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`onMarkViewed: Message ${messageId} missing!`);
}
if (message.get('readStatus') === ReadStatus.Viewed) {
return;
}
const senderE164 = message.get('source');
const senderUuid = message.get('sourceUuid');
const timestamp = message.get('sent_at');
message.set(markViewed(message.attributes, Date.now()));
viewedReceiptsJobQueue.add({
viewedReceipt: {
messageId,
senderE164,
senderUuid,
timestamp,
},
});
viewSyncJobQueue.add({
viewSyncs: [
{
messageId,
senderE164,
senderUuid,
timestamp,
},
],
});
};
const showVisualAttachment = (options: {
attachment: AttachmentType;
messageId: string;
showSingle?: boolean;
}) => {
this.showLightbox(options);
};
const downloadAttachment = (options: {
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}) => {
this.downloadAttachment(options);
};
const displayTapToViewMessage = (messageId: string) =>
this.displayTapToViewMessage(messageId);
const showIdentity = (conversationId: string) => {
this.showSafetyNumber(conversationId);
};
const openLink = openLinkInWebBrowser;
const downloadNewVersion = () => {
openLinkInWebBrowser('https://signal.org/download');
};
const showSafetyNumber = (contactId: string) => {
this.showSafetyNumber(contactId);
};
const showExpiredIncomingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an incoming message');
showToast(ToastTapToViewExpiredIncoming);
};
const showExpiredOutgoingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an outgoing message');
showToast(ToastTapToViewExpiredOutgoing);
};
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
return {
deleteMessage,
deleteMessageForEveryone,
displayTapToViewMessage,
downloadAttachment,
downloadNewVersion,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed: onMarkViewed,
openConversation,
openLink,
reactToMessage,
replyToMessage,
retrySend,
showContactDetail,
showContactModal,
showSafetyNumber,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showForwardMessageModal,
showIdentity,
showMessageDetail,
showVisualAttachment,
};
}
setupTimeline(): void {
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
const contactSupport = () => { const contactSupport = () => {
@ -965,65 +669,335 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.syncMessageRequestResponse(name, conversation, enumValue); this.syncMessageRequestResponse(name, conversation, enumValue);
}; };
this.timelineView = new Whisper.ReactWrapperView({ const timelineProps = {
className: 'timeline-wrapper', id: this.model.id,
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
id: this.model.id,
...this.getMessageActions(), ...this.getMessageActions(),
acknowledgeGroupMemberNameCollisions: ( acknowledgeGroupMemberNameCollisions: (
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle> groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
): void => { ): void => {
this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions); this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
}, },
contactSupport, contactSupport,
learnMoreAboutDeliveryIssue, learnMoreAboutDeliveryIssue,
loadNewerMessages, loadNewerMessages,
loadNewestMessages: this.loadNewestMessages.bind(this), loadNewestMessages: this.loadNewestMessages.bind(this),
loadAndScroll: this.loadAndScroll.bind(this), loadAndScroll: this.loadAndScroll.bind(this),
loadOlderMessages, loadOlderMessages,
markMessageRead, markMessageRead,
onBlock: createMessageRequestResponseHandler( onBlock: createMessageRequestResponseHandler(
'onBlock', 'onBlock',
messageRequestEnum.BLOCK messageRequestEnum.BLOCK
), ),
onBlockAndReportSpam: (conversationId: string) => { onBlockAndReportSpam: (conversationId: string) => {
const conversation = window.ConversationController.get( const conversation = window.ConversationController.get(conversationId);
conversationId if (!conversation) {
log.error(
`onBlockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
); );
if (!conversation) { return;
log.error( }
`onBlockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.` this.blockAndReportSpam(conversation);
); },
return; onDelete: createMessageRequestResponseHandler(
} 'onDelete',
this.blockAndReportSpam(conversation); messageRequestEnum.DELETE
}, ),
onDelete: createMessageRequestResponseHandler( onUnblock: createMessageRequestResponseHandler(
'onDelete', 'onUnblock',
messageRequestEnum.DELETE messageRequestEnum.ACCEPT
), ),
onUnblock: createMessageRequestResponseHandler( removeMember: (conversationId: string) => {
'onUnblock', this.longRunningTaskWrapper({
name: 'removeMember',
task: () => this.model.removeFromGroupV2(conversationId),
});
},
scrollToQuotedMessage,
unblurAvatar: () => {
this.model.unblurAvatar();
},
updateSharedGroups: () => this.model.throttledUpdateSharedGroups?.(),
};
// setupCompositionArea
window.reduxActions.composer.resetComposer();
const compositionAreaProps = {
id: this.model.id,
compositionApi: this.compositionApi,
onClickAddPack: () => this.showStickerManager(),
onPickSticker: (packId: string, stickerId: number) =>
this.sendStickerMessage({ packId, stickerId }),
onEditorStateChange: (
msg: string,
bodyRanges: Array<BodyRangeType>,
caretLocation?: number
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
getQuotedMessage: () => this.model.get('quotedMessageId'),
clearQuotedMessage: () => this.setQuoteMessage(null),
onAccept: () => {
this.syncMessageRequestResponse(
'onAccept',
this.model,
messageRequestEnum.ACCEPT messageRequestEnum.ACCEPT
), );
onShowContactModal: this.showContactModal.bind(this), },
removeMember: (conversationId: string) => { onBlock: () => {
this.longRunningTaskWrapper({ this.syncMessageRequestResponse(
name: 'removeMember', 'onBlock',
task: () => this.model.removeFromGroupV2(conversationId), this.model,
}); messageRequestEnum.BLOCK
}, );
scrollToQuotedMessage, },
unblurAvatar: () => { onUnblock: () => {
this.model.unblurAvatar(); this.syncMessageRequestResponse(
}, 'onUnblock',
updateSharedGroups: this.model.throttledUpdateSharedGroups, this.model,
}), messageRequestEnum.ACCEPT
);
},
onDelete: () => {
this.syncMessageRequestResponse(
'onDelete',
this.model,
messageRequestEnum.DELETE
);
},
onBlockAndReportSpam: () => {
this.blockAndReportSpam(this.model);
},
onStartGroupMigration: () => this.startMigrationToGV2(),
onCancelJoinRequest: async () => {
await window.showConfirmationDialog({
message: window.i18n(
'GroupV2--join--cancel-request-to-join--confirmation'
),
okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'),
cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'),
resolve: () => {
this.longRunningTaskWrapper({
name: 'onCancelJoinRequest',
task: async () => this.model.cancelJoinRequest(),
});
},
});
},
onClickAttachment: this.onClickAttachment.bind(this),
onClearAttachments: this.clearAttachments.bind(this),
onSelectMediaQuality: (isHQ: boolean) => {
window.reduxActions.composer.setMediaQualitySetting(isHQ);
},
handleClickQuotedMessage: (id: string) => this.scrollToMessage(id),
onCloseLinkPreview: () => {
this.disableLinkPreviews = true;
this.removeLinkPreview();
},
openConversation: this.openConversation.bind(this),
onSendMessage: ({
draftAttachments,
mentions = [],
message = '',
timestamp,
voiceNoteAttachment,
}: {
draftAttachments?: ReadonlyArray<AttachmentType>;
mentions?: BodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
}): void => {
this.sendMessage(message, mentions, {
draftAttachments,
timestamp,
voiceNoteAttachment,
});
},
};
// createConversationView root
const JSX = createConversationView(window.reduxStore, {
compositionAreaProps,
conversationHeaderProps,
timelineProps,
}); });
this.$('.timeline-placeholder').append(this.timelineView.el); this.conversationView = new Whisper.ReactWrapperView({ JSX });
this.$('.ConversationView__template').append(this.conversationView.el);
}
async longRunningTaskWrapper<T>({
name,
task,
}: {
name: string;
task: () => Promise<T>;
}): Promise<T> {
const idForLogging = this.model.idForLogging();
return window.Signal.Util.longRunningTaskWrapper({
name,
idForLogging,
task,
});
}
getMessageActions(): MessageActionsType {
const reactToMessage = (
messageId: string,
reaction: { emoji: string; remove: boolean }
) => {
this.sendReactionMessage(messageId, reaction);
};
const replyToMessage = (messageId: string) => {
this.setQuoteMessage(messageId);
};
const retrySend = retryMessageSend;
const deleteMessage = (messageId: string) => {
this.deleteMessage(messageId);
};
const deleteMessageForEveryone = (messageId: string) => {
this.deleteMessageForEveryone(messageId);
};
const showMessageDetail = (messageId: string) => {
this.showMessageDetail(messageId);
};
const showContactModal = (contactId: string) => {
this.showContactModal(contactId);
};
const openConversation = (conversationId: string, messageId?: string) => {
this.openConversation(conversationId, messageId);
};
const showContactDetail = (options: {
contact: EmbeddedContactType;
signalAccount?: string;
}) => {
this.showContactDetail(options);
};
const kickOffAttachmentDownload = async (
options: Readonly<{ messageId: string }>
) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
}
await message.queueAttachmentDownloads();
};
const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment);
};
const onMarkViewed = (messageId: string): void => {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`onMarkViewed: Message ${messageId} missing!`);
}
if (message.get('readStatus') === ReadStatus.Viewed) {
return;
}
const senderE164 = message.get('source');
const senderUuid = message.get('sourceUuid');
const timestamp = message.get('sent_at');
message.set(markViewed(message.attributes, Date.now()));
viewedReceiptsJobQueue.add({
viewedReceipt: {
messageId,
senderE164,
senderUuid,
timestamp,
},
});
viewSyncJobQueue.add({
viewSyncs: [
{
messageId,
senderE164,
senderUuid,
timestamp,
},
],
});
};
const showVisualAttachment = (options: {
attachment: AttachmentType;
messageId: string;
showSingle?: boolean;
}) => {
this.showLightbox(options);
};
const downloadAttachment = (options: {
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}) => {
this.downloadAttachment(options);
};
const displayTapToViewMessage = (messageId: string) =>
this.displayTapToViewMessage(messageId);
const showIdentity = (conversationId: string) => {
this.showSafetyNumber(conversationId);
};
const openLink = openLinkInWebBrowser;
const downloadNewVersion = () => {
openLinkInWebBrowser('https://signal.org/download');
};
const showSafetyNumber = (contactId: string) => {
this.showSafetyNumber(contactId);
};
const showExpiredIncomingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an incoming message');
showToast(ToastTapToViewExpiredIncoming);
};
const showExpiredOutgoingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an outgoing message');
showToast(ToastTapToViewExpiredOutgoing);
};
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
return {
deleteMessage,
deleteMessageForEveryone,
displayTapToViewMessage,
downloadAttachment,
downloadNewVersion,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed: onMarkViewed,
openConversation,
openLink,
reactToMessage,
replyToMessage,
retrySend,
showContactDetail,
showContactModal,
showSafetyNumber,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showForwardMessageModal,
showIdentity,
showMessageDetail,
showVisualAttachment,
};
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
@ -1397,9 +1371,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.model.updateLastMessage(); this.model.updateLastMessage();
} }
this.titleView?.remove(); this.conversationView?.remove();
this.timelineView?.remove();
this.compositionAreaView?.remove();
if (this.captionEditorView) { if (this.captionEditorView) {
this.captionEditorView.remove(); this.captionEditorView.remove();

View File

@ -216,13 +216,4 @@ Whisper.InboxView = Whisper.View.extend({
searchInput?.focus?.(); searchInput?.focus?.();
} }
}, },
closeRecording(e: MouseEvent) {
if (e && this.$(e.target).closest('.capture-audio').length > 0) {
return;
}
this.$('.conversation:first .recorder').trigger('close');
},
onClick(e: MouseEvent) {
this.closeRecording(e);
},
}); });

6
ts/window.d.ts vendored
View File

@ -40,9 +40,7 @@ import { ReduxActions } from './state/types';
import { createStore } from './state/createStore'; import { createStore } from './state/createStore';
import { createApp } from './state/roots/createApp'; import { createApp } from './state/roots/createApp';
import { createChatColorPicker } from './state/roots/createChatColorPicker'; import { createChatColorPicker } from './state/roots/createChatColorPicker';
import { createCompositionArea } from './state/roots/createCompositionArea';
import { createConversationDetails } from './state/roots/createConversationDetails'; import { createConversationDetails } from './state/roots/createConversationDetails';
import { createConversationHeader } from './state/roots/createConversationHeader';
import { createForwardMessageModal } from './state/roots/createForwardMessageModal'; import { createForwardMessageModal } from './state/roots/createForwardMessageModal';
import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement'; import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
@ -56,7 +54,6 @@ import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal'; import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import { createStickerManager } from './state/roots/createStickerManager'; import { createStickerManager } from './state/roots/createStickerManager';
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal'; import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import { createTimeline } from './state/roots/createTimeline';
import * as appDuck from './state/ducks/app'; import * as appDuck from './state/ducks/app';
import * as callingDuck from './state/ducks/calling'; import * as callingDuck from './state/ducks/calling';
import * as conversationsDuck from './state/ducks/conversations'; import * as conversationsDuck from './state/ducks/conversations';
@ -423,9 +420,7 @@ declare global {
Roots: { Roots: {
createApp: typeof createApp; createApp: typeof createApp;
createChatColorPicker: typeof createChatColorPicker; createChatColorPicker: typeof createChatColorPicker;
createCompositionArea: typeof createCompositionArea;
createConversationDetails: typeof createConversationDetails; createConversationDetails: typeof createConversationDetails;
createConversationHeader: typeof createConversationHeader;
createForwardMessageModal: typeof createForwardMessageModal; createForwardMessageModal: typeof createForwardMessageModal;
createGroupLinkManagement: typeof createGroupLinkManagement; createGroupLinkManagement: typeof createGroupLinkManagement;
createGroupV1MigrationModal: typeof createGroupV1MigrationModal; createGroupV1MigrationModal: typeof createGroupV1MigrationModal;
@ -439,7 +434,6 @@ declare global {
createShortcutGuideModal: typeof createShortcutGuideModal; createShortcutGuideModal: typeof createShortcutGuideModal;
createStickerManager: typeof createStickerManager; createStickerManager: typeof createStickerManager;
createStickerPreviewModal: typeof createStickerPreviewModal; createStickerPreviewModal: typeof createStickerPreviewModal;
createTimeline: typeof createTimeline;
}; };
Ducks: { Ducks: {
app: typeof appDuck; app: typeof appDuck;