ConversationView in React

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

View File

@ -83,18 +83,7 @@
</script>
<script 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">

View File

@ -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 = {

View File

@ -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 {

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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>
);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
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>
);

View File

@ -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

View File

@ -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 = (

View File

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

View File

@ -12,6 +12,7 @@ import {
ContactSpoofingReviewPropType,
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,

View File

@ -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"
}
]
]

View File

@ -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();

View File

@ -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
View File

@ -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;