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>
|
||||||
|
|
||||||
<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">
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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/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';
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
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;
|
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',
|
||||||
|
|
|
@ -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
|
// 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>
|
||||||
);
|
);
|
|
@ -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
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
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,
|
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,
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
6
ts/window.d.ts
vendored
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue