ConversationView in React
This commit is contained in:
parent
dddb3129cc
commit
5fdfa1c632
22 changed files with 703 additions and 786 deletions
|
@ -83,18 +83,7 @@
|
|||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
<div class='conversation-header'></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>
|
||||
<div class="ConversationView__template"></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="recorder">
|
||||
|
|
|
@ -59,19 +59,12 @@ const {
|
|||
const { WhatsNew } = require('../../ts/components/WhatsNew');
|
||||
|
||||
// State
|
||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||
const {
|
||||
createChatColorPicker,
|
||||
} = require('../../ts/state/roots/createChatColorPicker');
|
||||
const {
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
const {
|
||||
createConversationDetails,
|
||||
} = require('../../ts/state/roots/createConversationDetails');
|
||||
const {
|
||||
createConversationHeader,
|
||||
} = require('../../ts/state/roots/createConversationHeader');
|
||||
const { createApp } = require('../../ts/state/roots/createApp');
|
||||
const {
|
||||
createForwardMessageModal,
|
||||
|
@ -352,9 +345,7 @@ exports.setup = (options = {}) => {
|
|||
const Roots = {
|
||||
createApp,
|
||||
createChatColorPicker,
|
||||
createCompositionArea,
|
||||
createConversationDetails,
|
||||
createConversationHeader,
|
||||
createForwardMessageModal,
|
||||
createGroupLinkManagement,
|
||||
createGroupV1MigrationModal,
|
||||
|
@ -368,7 +359,6 @@ exports.setup = (options = {}) => {
|
|||
createShortcutGuideModal,
|
||||
createStickerManager,
|
||||
createStickerPreviewModal,
|
||||
createTimeline,
|
||||
};
|
||||
|
||||
const Ducks = {
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
@keyframes panel--in {
|
||||
from {
|
||||
transform: translateX(500px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,12 +30,12 @@
|
|||
|
||||
.panel {
|
||||
height: calc(100% - #{$header-height} - var(--title-bar-drag-area-height));
|
||||
overflow-y: scroll;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
overflow-y: overlay;
|
||||
position: absolute;
|
||||
top: calc(#{$header-height} + var(--title-bar-drag-area-height));
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
@include light-theme() {
|
||||
background-color: $color-white;
|
||||
|
@ -50,7 +48,7 @@
|
|||
|
||||
.panel {
|
||||
&:not(.main) {
|
||||
animation: panel--in 250ms ease-out;
|
||||
animation: panel--in 350ms cubic-bezier(0.17, 0.17, 0, 1);
|
||||
}
|
||||
|
||||
&--static {
|
||||
|
@ -58,43 +56,8 @@
|
|||
}
|
||||
|
||||
&--remove {
|
||||
transform: translateX(500px);
|
||||
opacity: 0;
|
||||
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;
|
||||
}
|
||||
transform: translateX(100%);
|
||||
transition: transform 350ms cubic-bezier(0.17, 0.17, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,95 +83,6 @@
|
|||
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,
|
||||
.debug-log-window {
|
||||
.modal {
|
||||
|
|
|
@ -159,55 +159,6 @@ a {
|
|||
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 {
|
||||
.container {
|
||||
outline: none;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
56
stylesheets/components/ConversationView.scss
Normal file
56
stylesheets/components/ConversationView.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@
|
|||
@import './components/ContactSpoofingReviewDialog.scss';
|
||||
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
||||
@import './components/ConversationHeader.scss';
|
||||
@import './components/ConversationView.scss';
|
||||
@import './components/CustomColorEditor.scss';
|
||||
@import './components/CustomizingPreferredReactionsModal.scss';
|
||||
@import './components/DisappearingTimeDialog.scss';
|
||||
|
|
|
@ -51,18 +51,7 @@
|
|||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
<div class='conversation-header'></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>
|
||||
<div id="ConversationView__template"></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="recorder">
|
||||
|
|
|
@ -51,13 +51,15 @@ import { Quote, Props as QuoteProps } from './conversation/Quote';
|
|||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { countStickers } from './stickers/lib';
|
||||
|
||||
export type CompositionAPIType = {
|
||||
focusInput: () => void;
|
||||
isDirty: () => boolean;
|
||||
setDisabled: (disabled: boolean) => void;
|
||||
reset: InputApi['reset'];
|
||||
resetEmojiResults: InputApi['resetEmojiResults'];
|
||||
};
|
||||
export type CompositionAPIType =
|
||||
| {
|
||||
focusInput: () => void;
|
||||
isDirty: () => boolean;
|
||||
setDisabled: (disabled: boolean) => void;
|
||||
reset: InputApi['reset'];
|
||||
resetEmojiResults: InputApi['resetEmojiResults'];
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest?: boolean;
|
||||
|
@ -96,7 +98,7 @@ export type OwnProps = Readonly<{
|
|||
linkPreviewResult?: LinkPreviewWithDomain;
|
||||
messageRequestsEnabled?: boolean;
|
||||
onClearAttachments(): unknown;
|
||||
onClickAttachment(): unknown;
|
||||
onClickAttachment(att: AttachmentType): unknown;
|
||||
onClickQuotedMessage(): unknown;
|
||||
onCloseLinkPreview(): unknown;
|
||||
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
||||
|
@ -325,7 +327,7 @@ export const CompositionArea = ({
|
|||
setLarge(l => !l);
|
||||
}, [setLarge]);
|
||||
|
||||
const shouldShowMicrophone = !draftAttachments.length && !draftText;
|
||||
const shouldShowMicrophone = !large && !draftAttachments.length && !draftText;
|
||||
|
||||
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
|
||||
|
||||
|
@ -373,14 +375,12 @@ export const CompositionArea = ({
|
|||
|
||||
const attButton = (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<div className="choose-file">
|
||||
<button
|
||||
type="button"
|
||||
className="paperclip thumbnail"
|
||||
onClick={launchAttachmentPicker}
|
||||
aria-label={i18n('CompositionArea--attach-file')}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="CompositionArea__attach-file"
|
||||
onClick={launchAttachmentPicker}
|
||||
aria-label={i18n('CompositionArea--attach-file')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
34
ts/components/conversation/ConversationView.tsx
Normal file
34
ts/components/conversation/ConversationView.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -157,7 +157,7 @@ export type PropsActionsType = {
|
|||
clearSelectedMessage: () => unknown;
|
||||
unblurAvatar: () => void;
|
||||
updateSharedGroups: () => unknown;
|
||||
} & MessageActionsType &
|
||||
} & Omit<MessageActionsType, 'onHeightChange'> &
|
||||
SafetyNumberActionsType &
|
||||
UnsupportedMessageActionsType &
|
||||
ChatSessionRefreshedNotificationActionsType;
|
||||
|
@ -251,7 +251,6 @@ const getActions = createSelector(
|
|||
'updateSharedGroups',
|
||||
|
||||
'doubleCheckMissingQuoteReference',
|
||||
'onHeightChange',
|
||||
'checkForAccount',
|
||||
'reactToMessage',
|
||||
'replyToMessage',
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
@ -6,19 +6,19 @@ import { Provider } from 'react-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()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const FilteredTimeline = SmartTimeline as any;
|
||||
const FilteredConversationView = SmartConversationView as any;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export const createTimeline = (
|
||||
export const createConversationView = (
|
||||
store: Store,
|
||||
props: Record<string, unknown>
|
||||
props: PropsType
|
||||
): React.ReactElement => (
|
||||
<Provider store={store}>
|
||||
<FilteredTimeline {...props} />
|
||||
<FilteredConversationView {...props} />
|
||||
</Provider>
|
||||
);
|
|
@ -4,7 +4,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { get } from 'lodash';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { CompositionArea } from '../../components/CompositionArea';
|
||||
import {
|
||||
CompositionArea,
|
||||
Props as ComponentPropsType,
|
||||
} from '../../components/CompositionArea';
|
||||
import { StateType } from '../reducer';
|
||||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
||||
import { dropNull } from '../../util/dropNull';
|
||||
|
@ -29,11 +32,13 @@ import {
|
|||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
onClickQuotedMessage: (id: string) => unknown;
|
||||
handleClickQuotedMessage: (id: string) => unknown;
|
||||
};
|
||||
|
||||
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id, onClickQuotedMessage } = props;
|
||||
const { id, handleClickQuotedMessage } = props;
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const conversation = conversationSelector(id);
|
||||
|
@ -101,7 +106,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
onClickQuotedMessage: () => {
|
||||
const messageId = quotedMessage?.quote?.messageId;
|
||||
if (messageId) {
|
||||
onClickQuotedMessage(messageId);
|
||||
handleClickQuotedMessage(messageId);
|
||||
}
|
||||
},
|
||||
// Emojis
|
||||
|
|
|
@ -26,8 +26,11 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
|||
export type OwnProps = {
|
||||
id: string;
|
||||
|
||||
onArchive: () => void;
|
||||
onDeleteMessages: () => void;
|
||||
onGoBack: () => void;
|
||||
onMarkUnread: () => void;
|
||||
onMoveToInbox: () => void;
|
||||
onOutgoingAudioCallInConversation: () => void;
|
||||
onOutgoingVideoCallInConversation: () => void;
|
||||
onResetSession: () => void;
|
||||
|
@ -38,13 +41,9 @@ export type OwnProps = {
|
|||
onShowAllMedia: () => void;
|
||||
onShowChatColorEditor: () => void;
|
||||
onShowContactModal: (contactId: string) => void;
|
||||
onShowGroupMembers: () => void;
|
||||
|
||||
onArchive: () => void;
|
||||
onMarkUnread: () => void;
|
||||
onMoveToInbox: () => void;
|
||||
onShowSafetyNumber: () => void;
|
||||
onShowConversationDetails: () => void;
|
||||
onShowGroupMembers: () => void;
|
||||
onShowSafetyNumber: () => void;
|
||||
};
|
||||
|
||||
const getOutgoingCallButtonStyle = (
|
||||
|
|
69
ts/state/smart/ConversationView.tsx
Normal file
69
ts/state/smart/ConversationView.tsx
Normal 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);
|
|
@ -12,6 +12,7 @@ import {
|
|||
ContactSpoofingReviewPropType,
|
||||
Timeline,
|
||||
WarningType as TimelineWarningType,
|
||||
PropsType as ComponentPropsType,
|
||||
} from '../../components/conversation/Timeline';
|
||||
import { StateType } from '../reducer';
|
||||
import { ConversationType } from '../ducks/conversations';
|
||||
|
@ -53,6 +54,48 @@ type ExternalProps = {
|
|||
// 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(
|
||||
(
|
||||
onHeightChange: (messageId: string) => unknown,
|
||||
|
|
|
@ -13544,20 +13544,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"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(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
|
@ -13642,20 +13628,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"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(",
|
||||
"path": "ts/views/inbox_view.ts",
|
||||
|
@ -14328,4 +14300,4 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-17T21:02:59.414Z"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -69,6 +69,7 @@ import * as VisualAttachment from '../types/VisualAttachment';
|
|||
import * as log from '../logging/log';
|
||||
import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d';
|
||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||
import { createConversationView } from '../state/roots/createConversationView';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import { CompositionAPIType } from '../components/CompositionArea';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
|
@ -225,7 +226,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
// Composing messages
|
||||
private compositionApi: {
|
||||
current?: CompositionAPIType;
|
||||
current: CompositionAPIType;
|
||||
} = { current: undefined };
|
||||
private sendStart?: number;
|
||||
|
||||
|
@ -242,14 +243,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
// Sub-views
|
||||
private captionEditorView?: Backbone.View;
|
||||
private compositionAreaView?: Backbone.View;
|
||||
private contactModalView?: Backbone.View;
|
||||
private conversationView?: BasicReactWrapperViewClass;
|
||||
private forwardMessageModal?: Backbone.View;
|
||||
private lightboxView?: BasicReactWrapperViewClass;
|
||||
private migrationDialog?: Backbone.View;
|
||||
private stickerPreviewModalView?: Backbone.View;
|
||||
private timelineView?: Backbone.View;
|
||||
private titleView?: Backbone.View;
|
||||
|
||||
// Panel support
|
||||
private panels: Array<AnyViewClass> = [];
|
||||
|
@ -314,17 +313,13 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
this.render();
|
||||
|
||||
this.setupHeader();
|
||||
this.setupTimeline();
|
||||
this.setupCompositionArea();
|
||||
this.setupConversationView();
|
||||
this.updateAttachmentsView();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
events(): Record<string, string> {
|
||||
return {
|
||||
'change input.file-input': 'onChoseAttachment',
|
||||
|
||||
drop: 'onDrop',
|
||||
paste: 'onPaste',
|
||||
};
|
||||
|
@ -382,407 +377,116 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
}
|
||||
}
|
||||
|
||||
setupHeader(): void {
|
||||
this.titleView = new Whisper.ReactWrapperView({
|
||||
className: 'title-wrapper',
|
||||
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 = {
|
||||
setupConversationView(): void {
|
||||
// setupHeader
|
||||
const conversationHeaderProps = {
|
||||
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
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
},
|
||||
onBlock: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onBlock',
|
||||
this.model,
|
||||
messageRequestEnum.BLOCK
|
||||
|
||||
onOutgoingVideoCallInConversation: async () => {
|
||||
log.info(
|
||||
'onOutgoingVideoCallInConversation: about to start a video call'
|
||||
);
|
||||
},
|
||||
onUnblock: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onUnblock',
|
||||
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(),
|
||||
});
|
||||
},
|
||||
});
|
||||
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'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onClickAttachment: this.onClickAttachment.bind(this),
|
||||
onClearAttachments: this.clearAttachments.bind(this),
|
||||
onSelectMediaQuality: (isHQ: boolean) => {
|
||||
window.reduxActions.composer.setMediaQualitySetting(isHQ);
|
||||
onShowChatColorEditor: () => {
|
||||
this.showChatColorEditor();
|
||||
},
|
||||
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: () => {
|
||||
this.disableLinkPreviews = true;
|
||||
this.removeLinkPreview();
|
||||
showToast(ToastConversationArchived);
|
||||
},
|
||||
onMarkUnread: () => {
|
||||
this.model.setMarkedUnread(true);
|
||||
|
||||
openConversation: this.openConversation.bind(this),
|
||||
showToast(ToastConversationMarkedUnread);
|
||||
},
|
||||
onMoveToInbox: () => {
|
||||
this.model.setArchived(false);
|
||||
|
||||
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,
|
||||
});
|
||||
showToast(ToastConversationUnarchived);
|
||||
},
|
||||
};
|
||||
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
|
||||
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
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 {
|
||||
// setupTimeline
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const contactSupport = () => {
|
||||
|
@ -965,65 +669,335 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
this.syncMessageRequestResponse(name, conversation, enumValue);
|
||||
};
|
||||
|
||||
this.timelineView = new Whisper.ReactWrapperView({
|
||||
className: 'timeline-wrapper',
|
||||
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
||||
id: this.model.id,
|
||||
const timelineProps = {
|
||||
id: this.model.id,
|
||||
|
||||
...this.getMessageActions(),
|
||||
...this.getMessageActions(),
|
||||
|
||||
acknowledgeGroupMemberNameCollisions: (
|
||||
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||
): void => {
|
||||
this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
|
||||
},
|
||||
contactSupport,
|
||||
learnMoreAboutDeliveryIssue,
|
||||
loadNewerMessages,
|
||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||
loadAndScroll: this.loadAndScroll.bind(this),
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
onBlock: createMessageRequestResponseHandler(
|
||||
'onBlock',
|
||||
messageRequestEnum.BLOCK
|
||||
),
|
||||
onBlockAndReportSpam: (conversationId: string) => {
|
||||
const conversation = window.ConversationController.get(
|
||||
conversationId
|
||||
acknowledgeGroupMemberNameCollisions: (
|
||||
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||
): void => {
|
||||
this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
|
||||
},
|
||||
contactSupport,
|
||||
learnMoreAboutDeliveryIssue,
|
||||
loadNewerMessages,
|
||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||
loadAndScroll: this.loadAndScroll.bind(this),
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
onBlock: createMessageRequestResponseHandler(
|
||||
'onBlock',
|
||||
messageRequestEnum.BLOCK
|
||||
),
|
||||
onBlockAndReportSpam: (conversationId: string) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
log.error(
|
||||
`onBlockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
||||
);
|
||||
if (!conversation) {
|
||||
log.error(
|
||||
`onBlockAndReportSpam: Expected a conversation to be found for ${conversationId}. Doing nothing.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.blockAndReportSpam(conversation);
|
||||
},
|
||||
onDelete: createMessageRequestResponseHandler(
|
||||
'onDelete',
|
||||
messageRequestEnum.DELETE
|
||||
),
|
||||
onUnblock: createMessageRequestResponseHandler(
|
||||
'onUnblock',
|
||||
return;
|
||||
}
|
||||
this.blockAndReportSpam(conversation);
|
||||
},
|
||||
onDelete: createMessageRequestResponseHandler(
|
||||
'onDelete',
|
||||
messageRequestEnum.DELETE
|
||||
),
|
||||
onUnblock: createMessageRequestResponseHandler(
|
||||
'onUnblock',
|
||||
messageRequestEnum.ACCEPT
|
||||
),
|
||||
removeMember: (conversationId: string) => {
|
||||
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
|
||||
),
|
||||
onShowContactModal: this.showContactModal.bind(this),
|
||||
removeMember: (conversationId: string) => {
|
||||
this.longRunningTaskWrapper({
|
||||
name: 'removeMember',
|
||||
task: () => this.model.removeFromGroupV2(conversationId),
|
||||
});
|
||||
},
|
||||
scrollToQuotedMessage,
|
||||
unblurAvatar: () => {
|
||||
this.model.unblurAvatar();
|
||||
},
|
||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onBlock: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onBlock',
|
||||
this.model,
|
||||
messageRequestEnum.BLOCK
|
||||
);
|
||||
},
|
||||
onUnblock: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onUnblock',
|
||||
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
|
||||
|
@ -1397,9 +1371,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
this.model.updateLastMessage();
|
||||
}
|
||||
|
||||
this.titleView?.remove();
|
||||
this.timelineView?.remove();
|
||||
this.compositionAreaView?.remove();
|
||||
this.conversationView?.remove();
|
||||
|
||||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
|
|
|
@ -216,13 +216,4 @@ Whisper.InboxView = Whisper.View.extend({
|
|||
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
6
ts/window.d.ts
vendored
|
@ -40,9 +40,7 @@ import { ReduxActions } from './state/types';
|
|||
import { createStore } from './state/createStore';
|
||||
import { createApp } from './state/roots/createApp';
|
||||
import { createChatColorPicker } from './state/roots/createChatColorPicker';
|
||||
import { createCompositionArea } from './state/roots/createCompositionArea';
|
||||
import { createConversationDetails } from './state/roots/createConversationDetails';
|
||||
import { createConversationHeader } from './state/roots/createConversationHeader';
|
||||
import { createForwardMessageModal } from './state/roots/createForwardMessageModal';
|
||||
import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
|
||||
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
|
||||
|
@ -56,7 +54,6 @@ import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer
|
|||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||
import { createStickerManager } from './state/roots/createStickerManager';
|
||||
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
|
||||
import { createTimeline } from './state/roots/createTimeline';
|
||||
import * as appDuck from './state/ducks/app';
|
||||
import * as callingDuck from './state/ducks/calling';
|
||||
import * as conversationsDuck from './state/ducks/conversations';
|
||||
|
@ -423,9 +420,7 @@ declare global {
|
|||
Roots: {
|
||||
createApp: typeof createApp;
|
||||
createChatColorPicker: typeof createChatColorPicker;
|
||||
createCompositionArea: typeof createCompositionArea;
|
||||
createConversationDetails: typeof createConversationDetails;
|
||||
createConversationHeader: typeof createConversationHeader;
|
||||
createForwardMessageModal: typeof createForwardMessageModal;
|
||||
createGroupLinkManagement: typeof createGroupLinkManagement;
|
||||
createGroupV1MigrationModal: typeof createGroupV1MigrationModal;
|
||||
|
@ -439,7 +434,6 @@ declare global {
|
|||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||
createStickerManager: typeof createStickerManager;
|
||||
createStickerPreviewModal: typeof createStickerPreviewModal;
|
||||
createTimeline: typeof createTimeline;
|
||||
};
|
||||
Ducks: {
|
||||
app: typeof appDuck;
|
||||
|
|
Loading…
Add table
Reference in a new issue