diff --git a/js/modules/signal.js b/js/modules/signal.js index 4d679abec..cf0a14a84 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -26,23 +26,14 @@ const { ChatColorPicker } = require('../../ts/components/ChatColorPicker'); const { ConfirmationDialog, } = require('../../ts/components/ConfirmationDialog'); -const { - ContactDetail, -} = require('../../ts/components/conversation/ContactDetail'); const { ContactModal, } = require('../../ts/components/conversation/ContactModal'); const { Emojify } = require('../../ts/components/conversation/Emojify'); -const { ErrorModal } = require('../../ts/components/ErrorModal'); -const { Lightbox } = require('../../ts/components/Lightbox'); -const { - MediaGallery, -} = require('../../ts/components/conversation/media-gallery/MediaGallery'); const { MessageDetail, } = require('../../ts/components/conversation/MessageDetail'); const { Quote } = require('../../ts/components/conversation/Quote'); -const { ProgressModal } = require('../../ts/components/ProgressModal'); const { StagedLinkPreview, } = require('../../ts/components/conversation/StagedLinkPreview'); @@ -52,7 +43,6 @@ const { const { SystemTraySettingsCheckboxes, } = require('../../ts/components/conversation/SystemTraySettingsCheckboxes'); -const { WhatsNewLink } = require('../../ts/components/WhatsNewLink'); // State const { @@ -320,19 +310,13 @@ exports.setup = (options = {}) => { AttachmentList, ChatColorPicker, ConfirmationDialog, - ContactDetail, ContactModal, Emojify, - ErrorModal, - Lightbox, - MediaGallery, MessageDetail, Quote, - ProgressModal, StagedLinkPreview, DisappearingTimeDialog, SystemTraySettingsCheckboxes, - WhatsNewLink, }; const Roots = { diff --git a/js/views/react_wrapper_view.js b/js/views/react_wrapper_view.js deleted file mode 100644 index 57154e84c..000000000 --- a/js/views/react_wrapper_view.js +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global Backbone: false */ -/* global i18n: false */ -/* global React: false */ -/* global ReactDOM: false */ - -// eslint-disable-next-line func-names -(function () { - window.Whisper = window.Whisper || {}; - - window.Whisper.ReactWrapperView = Backbone.View.extend({ - className: 'react-wrapper', - initialize(options) { - const { - Component, - JSX, - props, - onClose, - tagName, - className, - onInitialRender, - elCallback, - } = options; - this.render(); - if (elCallback) { - elCallback(this.el); - } - - this.tagName = tagName; - this.className = className; - this.JSX = JSX; - this.Component = Component; - this.onClose = onClose; - this.onInitialRender = onInitialRender; - - this.update(props); - - this.hasRendered = false; - }, - update(propsOrJSX, cb) { - const reactElement = this.JSX - ? propsOrJSX || this.JSX - : React.createElement(this.Component, this.augmentProps(propsOrJSX)); - - ReactDOM.render(reactElement, this.el, () => { - if (cb) { - try { - cb(); - } catch (error) { - window.SignalContext.log.error( - 'ReactWrapperView.update error:', - error && error.stack ? error.stack : error - ); - } - } - - if (this.hasRendered) { - return; - } - - this.hasRendered = true; - if (this.onInitialRender) { - this.onInitialRender(); - } - }); - }, - augmentProps(props) { - return { - ...props, - close: () => { - this.remove(); - }, - i18n, - }; - }, - remove() { - if (this.onClose) { - this.onClose(); - } - ReactDOM.unmountComponentAtNode(this.el); - Backbone.View.prototype.remove.call(this); - }, - }); -})(); diff --git a/ts/background.ts b/ts/background.ts index 45721af26..e489442a5 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -134,6 +134,7 @@ import type { UUID } from './types/UUID'; import * as log from './logging/log'; import { loadRecentEmojis } from './util/loadRecentEmojis'; import { deleteAllLogs } from './util/deleteAllLogs'; +import { ReactWrapperView } from './views/ReactWrapperView'; import { ToastCaptchaFailed } from './components/ToastCaptchaFailed'; import { ToastCaptchaSolved } from './components/ToastCaptchaSolved'; import { ToastConversationArchived } from './components/ToastConversationArchived'; @@ -1114,7 +1115,7 @@ export async function startApp(): Promise { window.showKeyboardShortcuts = () => { if (!shortcutGuideView) { - shortcutGuideView = new window.Whisper.ReactWrapperView({ + shortcutGuideView = new ReactWrapperView({ className: 'shortcut-guide-wrapper', JSX: window.Signal.State.Roots.createShortcutGuideModal( window.reduxStore, diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index d57722f27..c9309cc30 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -1,4 +1,4 @@ -// Copyright 2018-2021 Signal Messenger, LLC +// Copyright 2018-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode } from 'react'; @@ -16,7 +16,7 @@ import { Avatar, AvatarSize } from './Avatar'; import type { ConversationType } from '../state/ducks/conversations'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import type { LocalizerType } from '../types/Util'; -import type { MediaItemType, MessageAttributesType } from '../types/MediaItem'; +import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import { formatDuration } from '../util/formatDuration'; import { useRestoreFocus } from '../hooks/useRestoreFocus'; import * as log from '../logging/log'; @@ -31,7 +31,7 @@ export type PropsType = { onForward?: (messageId: string) => void; onSave?: (options: { attachment: AttachmentType; - message: MessageAttributesType; + message: MediaItemMessageType; index: number; }) => void; selectedIndex?: number; @@ -676,7 +676,7 @@ function LightboxHeader({ }: { getConversation: (id: string) => ConversationType; i18n: LocalizerType; - message: MessageAttributesType; + message: MediaItemMessageType; }): JSX.Element { const conversation = getConversation(message.conversationId); diff --git a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts index bf51c6a5b..0a39f71ec 100644 --- a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts +++ b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts @@ -1,11 +1,11 @@ -// Copyright 2018-2021 Signal Messenger, LLC +// Copyright 2018-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { MessageAttributesType } from '../../../../model-types.d'; import type { AttachmentType } from '../../../../types/Attachment'; -import type { Message } from './Message'; export type ItemClickEvent = { - message: Message; + message: Pick; attachment: AttachmentType; type: 'media' | 'documents'; }; diff --git a/ts/components/conversation/media-gallery/types/Message.ts b/ts/components/conversation/media-gallery/types/Message.ts deleted file mode 100644 index b6b60056a..000000000 --- a/ts/components/conversation/media-gallery/types/Message.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { AttachmentType } from '../../../../types/Attachment'; - -export type Message = { - id: string; - attachments: Array; - // Assuming this is for the API - // eslint-disable-next-line camelcase - received_at: number; - // eslint-disable-next-line camelcase - received_at_ms: number; -}; diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.tsx similarity index 95% rename from ts/groups/joinViaLink.ts rename to ts/groups/joinViaLink.tsx index e5ad473af..39054360c 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.tsx @@ -1,6 +1,7 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import * as React from 'react'; import { applyNewAvatar, decryptGroupDescription, @@ -23,6 +24,8 @@ import type { PreJoinConversationType } from '../state/ducks/conversations'; import { SignalService as Proto } from '../protobuf'; import * as log from '../logging/log'; import { showToast } from '../util/showToast'; +import { ReactWrapperView } from '../views/ReactWrapperView'; +import { ErrorModal } from '../components/ErrorModal'; import { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember'; import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin'; import { HTTPError } from '../textsecure/Errors'; @@ -373,14 +376,13 @@ export async function joinViaLink(hash: string): Promise { log.info(`joinViaLink/${logId}: Showing modal`); - let groupV2InfoDialog: Backbone.View | undefined = - new window.Whisper.ReactWrapperView({ - className: 'group-v2-join-dialog-wrapper', - JSX: window.Signal.State.Roots.createGroupV2JoinModal(window.reduxStore, { - join, - onClose: closeDialog, - }), - }); + let groupV2InfoDialog: Backbone.View | undefined = new ReactWrapperView({ + className: 'group-v2-join-dialog-wrapper', + JSX: window.Signal.State.Roots.createGroupV2JoinModal(window.reduxStore, { + join, + onClose: closeDialog, + }), + }); // We declare a new function here so we can await but not block const fetchAvatar = async () => { @@ -427,15 +429,17 @@ export async function joinViaLink(hash: string): Promise { } function showErrorDialog(description: string, title: string) { - const errorView = new window.Whisper.ReactWrapperView({ + const errorView = new ReactWrapperView({ className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - title, - description, - onClose: () => { - errorView.remove(); - }, - }, + JSX: ( + { + errorView.remove(); + }} + /> + ), }); } diff --git a/ts/types/MediaItem.ts b/ts/types/MediaItem.ts index a90f73452..1259b3a02 100644 --- a/ts/types/MediaItem.ts +++ b/ts/types/MediaItem.ts @@ -1,27 +1,26 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { MessageAttributesType } from '../model-types.d'; import type { AttachmentType } from './Attachment'; import type { MIMEType } from './MIME'; -export type MessageAttributesType = { - attachments: Array; - conversationId: string; - id: string; - // eslint-disable-next-line camelcase - received_at: number; - // eslint-disable-next-line camelcase - received_at_ms: number; - // eslint-disable-next-line camelcase - sent_at: number; -}; +export type MediaItemMessageType = Pick< + MessageAttributesType, + | 'attachments' + | 'conversationId' + | 'id' + | 'received_at' + | 'received_at_ms' + | 'sent_at' +>; export type MediaItemType = { attachment: AttachmentType; contentType?: MIMEType; index: number; loop?: boolean; - message: MessageAttributesType; + message: MediaItemMessageType; objectURL?: string; thumbnailObjectUrl?: string; }; diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.tsx similarity index 94% rename from ts/util/createIPCEvents.ts rename to ts/util/createIPCEvents.tsx index 4075e3a76..2885d2d5c 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.tsx @@ -1,8 +1,9 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { webFrame } from 'electron'; import type { AudioDevice } from 'ringrtc'; +import * as React from 'react'; import type { ZoomFactorType } from '../types/Storage.d'; import type { @@ -15,6 +16,9 @@ import * as Stickers from '../types/Stickers'; import type { SystemTraySetting } from '../types/SystemTraySetting'; import { parseSystemTraySetting } from '../types/SystemTraySetting'; +import { ReactWrapperView } from '../views/ReactWrapperView'; +import { ErrorModal } from '../components/ErrorModal'; + import type { ConversationType } from '../state/ducks/conversations'; import { calling } from '../services/calling'; import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations'; @@ -406,7 +410,7 @@ export function createIPCEvents( }, }; - const stickerPreviewModalView = new window.Whisper.ReactWrapperView({ + const stickerPreviewModalView = new ReactWrapperView({ className: 'sticker-preview-modal-wrapper', JSX: window.Signal.State.Roots.createStickerPreviewModal( window.reduxStore, @@ -419,14 +423,16 @@ export function createIPCEvents( 'showStickerPack: Ran into an error!', error && error.stack ? error.stack : error ); - const errorView = new window.Whisper.ReactWrapperView({ + const errorView = new ReactWrapperView({ className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - onClose: () => { - errorView.remove(); - }, - }, + JSX: ( + { + errorView.remove(); + }} + /> + ), }); } }, @@ -447,16 +453,18 @@ export function createIPCEvents( 'showGroupViaLink: Ran into an error!', error && error.stack ? error.stack : error ); - const errorView = new window.Whisper.ReactWrapperView({ + const errorView = new ReactWrapperView({ className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - title: window.i18n('GroupV2--join--general-join-failure--title'), - description: window.i18n('GroupV2--join--general-join-failure'), - onClose: () => { - errorView.remove(); - }, - }, + JSX: ( + { + errorView.remove(); + }} + /> + ), }); } window.isShowingModal = false; @@ -513,14 +521,16 @@ export function createIPCEvents( } function showUnknownSgnlLinkModal(): void { - const errorView = new window.Whisper.ReactWrapperView({ + const errorView = new ReactWrapperView({ className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - description: window.i18n('unknown-sgnl-link'), - onClose: () => { - errorView.remove(); - }, - }, + JSX: ( + { + errorView.remove(); + }} + /> + ), }); } diff --git a/ts/util/getMessageTimestamp.ts b/ts/util/getMessageTimestamp.ts index acd50ee84..17e724ed6 100644 --- a/ts/util/getMessageTimestamp.ts +++ b/ts/util/getMessageTimestamp.ts @@ -1,8 +1,10 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Message } from '../components/conversation/media-gallery/types/Message'; +import type { MessageAttributesType } from '../model-types.d'; -export function getMessageTimestamp(message: Message): number { +export function getMessageTimestamp( + message: Pick +): number { return message.received_at_ms || message.received_at; } diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 1fc7b5bd5..46709cf7e 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -7967,7 +7967,7 @@ }, { "rule": "jQuery-$(", - "path": "ts/util/createIPCEvents.ts", + "path": "ts/util/createIPCEvents.tsx", "line": " if ($('.dark-overlay').length) {", "reasonCategory": "usageTrusted", "updated": "2021-08-18T18:22:55.307Z", @@ -7975,7 +7975,7 @@ }, { "rule": "jQuery-$(", - "path": "ts/util/createIPCEvents.ts", + "path": "ts/util/createIPCEvents.tsx", "line": " $(document.body).prepend('
');", "reasonCategory": "usageTrusted", "updated": "2021-08-18T18:22:55.307Z", @@ -7983,7 +7983,7 @@ }, { "rule": "jQuery-$(", - "path": "ts/util/createIPCEvents.ts", + "path": "ts/util/createIPCEvents.tsx", "line": " $('.dark-overlay').on('click', () => $('.dark-overlay').remove());", "reasonCategory": "usageTrusted", "updated": "2021-08-18T18:22:55.307Z", @@ -7991,7 +7991,7 @@ }, { "rule": "jQuery-$(", - "path": "ts/util/createIPCEvents.ts", + "path": "ts/util/createIPCEvents.tsx", "line": " removeDarkOverlay: () => $('.dark-overlay').remove(),", "reasonCategory": "usageTrusted", "updated": "2021-08-18T18:22:55.307Z", @@ -7999,7 +7999,7 @@ }, { "rule": "jQuery-prepend(", - "path": "ts/util/createIPCEvents.ts", + "path": "ts/util/createIPCEvents.tsx", "line": " $(document.body).prepend('
');", "reasonCategory": "usageTrusted", "updated": "2021-08-18T18:22:55.307Z", @@ -8021,84 +8021,84 @@ }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " template: () => $('#app-loading-screen').html(),", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.$('.message').text(message);", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " template: () => $('#two-column').html(),", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " el: this.$('.conversation-stack'),", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.$('.no-conversation-open').toggle(!isAnyConversationOpen);", "reasonCategory": "usageTrusted", "updated": "2021-10-08T17:40:22.770Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el);", "reasonCategory": "usageTrusted", "updated": "2021-10-08T17:40:22.770Z" }, { "rule": "jQuery-$(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);", "reasonCategory": "usageTrusted", "updated": "2021-10-22T20:58:48.103Z" }, { "rule": "jQuery-append(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);", "reasonCategory": "usageTrusted", "updated": "2021-10-22T20:58:48.103Z" }, { "rule": "jQuery-appendTo(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " view.$el.appendTo(this.el);", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-html(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " template: () => $('#app-loading-screen').html(),", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-html(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " template: () => $('#two-column').html(),", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" }, { "rule": "jQuery-prependTo(", - "path": "ts/views/inbox_view.ts", + "path": "ts/views/inbox_view.tsx", "line": " this.appLoadingScreen.$el.prependTo(this.el);", "reasonCategory": "usageTrusted", "updated": "2021-09-15T21:07:50.995Z" diff --git a/ts/util/longRunningTaskWrapper.ts b/ts/util/longRunningTaskWrapper.tsx similarity index 81% rename from ts/util/longRunningTaskWrapper.ts rename to ts/util/longRunningTaskWrapper.tsx index 10fd831fd..4c817330a 100644 --- a/ts/util/longRunningTaskWrapper.ts +++ b/ts/util/longRunningTaskWrapper.tsx @@ -1,6 +1,10 @@ // Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import * as React from 'react'; +import { ReactWrapperView } from '../views/ReactWrapperView'; +import { ErrorModal } from '../components/ErrorModal'; +import { ProgressModal } from '../components/ProgressModal'; import * as log from '../logging/log'; import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary'; @@ -26,9 +30,9 @@ export async function longRunningTaskWrapper({ // Note: this component uses a portal to render itself into the top-level DOM. No // need to attach it to the DOM here. - progressView = new window.Whisper.ReactWrapperView({ + progressView = new ReactWrapperView({ className: 'progress-modal-wrapper', - Component: window.Signal.Components.ProgressModal, + JSX: , }); spinnerStart = Date.now(); }, TWO_SECONDS); @@ -73,14 +77,16 @@ export async function longRunningTaskWrapper({ // Note: this component uses a portal to render itself into the top-level DOM. No // need to attach it to the DOM here. - const errorView: Backbone.View = new window.Whisper.ReactWrapperView({ + const errorView: Backbone.View = new ReactWrapperView({ className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - onClose: (): void => { - errorView.remove(); - }, - }, + JSX: ( + { + errorView.remove(); + }} + /> + ), }); } diff --git a/ts/views/ReactWrapperView.ts b/ts/views/ReactWrapperView.ts new file mode 100644 index 000000000..8f43daf8e --- /dev/null +++ b/ts/views/ReactWrapperView.ts @@ -0,0 +1,49 @@ +// Copyright 2018-2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReactElement } from 'react'; +import * as ReactDOM from 'react-dom'; +import * as Backbone from 'backbone'; + +export class ReactWrapperView extends Backbone.View { + private readonly onClose?: () => unknown; + private JSX: ReactElement; + + constructor({ + className, + onClose, + JSX, + }: Readonly<{ + className?: string; + onClose?: () => unknown; + JSX: ReactElement; + }>) { + super(); + + this.className = className ?? 'react-wrapper'; + this.JSX = JSX; + this.onClose = onClose; + + this.render(); + } + + update(JSX: ReactElement): void { + this.JSX = JSX; + this.render(); + } + + override render(): this { + this.el.className = this.className; + ReactDOM.render(this.JSX, this.el); + return this; + } + + override remove(): this { + if (this.onClose) { + this.onClose(); + } + ReactDOM.unmountComponentAtNode(this.el); + Backbone.View.prototype.remove.call(this); + return this; + } +} diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.tsx similarity index 94% rename from ts/views/conversation_view.ts rename to ts/views/conversation_view.tsx index df4f305f9..13fe470e2 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.tsx @@ -3,6 +3,9 @@ /* eslint-disable camelcase */ +import type * as Backbone from 'backbone'; +import type { ComponentProps } from 'react'; +import * as React from 'react'; import { debounce, flatten, omit, throttle } from 'lodash'; import { render } from 'mustache'; @@ -23,10 +26,7 @@ import type { QuotedMessageType, } from '../model-types.d'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; -import type { - MediaItemType, - MessageAttributesType as MediaItemMessageType, -} from '../types/MediaItem'; +import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import type { MessageModel } from '../models/messages'; import { getMessageById } from '../messages/getMessageById'; import { getContactId } from '../messages/helpers'; @@ -43,6 +43,7 @@ import { } from '../util/whatTypeOfConversation'; import { findAndFormatContact } from '../util/findAndFormatContact'; import * as Bytes from '../Bytes'; +import { getPreferredBadgeSelector } from '../state/selectors/badges'; import { canReply, getAttachmentsForMessage, @@ -55,6 +56,9 @@ import { getMessagesByConversation, } from '../state/selectors/conversations'; import { getActiveCallState } from '../state/selectors/calling'; +import { getTheme } from '../state/selectors/user'; +import { ReactWrapperView } from './ReactWrapperView'; +import { Lightbox } from '../components/Lightbox'; import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog'; import type { @@ -65,7 +69,6 @@ import type { import * as LinkPreview from '../types/LinkPreview'; 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'; @@ -116,18 +119,20 @@ import { RecordingState } from '../state/ducks/audioRecorder'; import { UUIDKind } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID'; import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone'; +import { ContactDetail } from '../components/conversation/ContactDetail'; +import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery'; +import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent'; type AttachmentOptions = { messageId: string; attachment: AttachmentType; }; +type PanelType = { view: Backbone.View; headerTitle?: string }; + const FIVE_MINUTES = 1000 * 60 * 5; const LINK_PREVIEW_TIMEOUT = 60 * 1000; -window.Whisper = window.Whisper || {}; - -const { Whisper } = window; const { Message } = window.Signal.Types; const { @@ -247,14 +252,14 @@ export class ConversationView extends window.Backbone.View { // Sub-views private contactModalView?: Backbone.View; - private conversationView?: BasicReactWrapperViewClass; + private conversationView?: Backbone.View; private forwardMessageModal?: Backbone.View; - private lightboxView?: BasicReactWrapperViewClass; + private lightboxView?: ReactWrapperView; private migrationDialog?: Backbone.View; private stickerPreviewModalView?: Backbone.View; // Panel support - private panels: Array = []; + private panels: Array = []; private previousFocus?: HTMLElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -676,7 +681,7 @@ export class ConversationView extends window.Backbone.View { timelineProps, }); - this.conversationView = new Whisper.ReactWrapperView({ JSX }); + this.conversationView = new ReactWrapperView({ JSX }); this.$('.ConversationView__template').append(this.conversationView.el); } @@ -988,7 +993,7 @@ export class ConversationView extends window.Backbone.View { (item: GroupV2PendingMemberType) => item.uuid ); - this.migrationDialog = new Whisper.ReactWrapperView({ + this.migrationDialog = new ReactWrapperView({ className: 'group-v1-migration-wrapper', JSX: window.Signal.State.Roots.createGroupV1MigrationModal( window.reduxStore, @@ -1106,7 +1111,7 @@ export class ConversationView extends window.Backbone.View { if (this.panels && this.panels.length) { for (let i = 0, max = this.panels.length; i < max; i += 1) { const panel = this.panels[i]; - panel.remove(); + panel.view.remove(); } window.reduxActions.conversations.setSelectedConversationPanelDepth(0); } @@ -1334,7 +1339,7 @@ export class ConversationView extends window.Backbone.View { } }; - this.forwardMessageModal = new Whisper.ReactWrapperView({ + this.forwardMessageModal = new ReactWrapperView({ JSX: window.Signal.State.Roots.createForwardMessageModal( window.reduxStore, { @@ -1623,20 +1628,24 @@ export class ConversationView extends window.Backbone.View { ).filter(isNotNil); // Unlike visual media, only one non-image attachment is supported - const documents = rawDocuments - .filter(message => - Boolean(message.attachments && message.attachments.length) - ) - .map(message => { - const attachments = message.attachments || []; - const attachment = attachments[0]; - return { - contentType: attachment.contentType, - index: 0, - attachment, - message, - }; + const documents: Array = []; + rawDocuments.forEach(message => { + const attachments = message.attachments || []; + const attachment = attachments[0]; + if (!attachment) { + return; + } + + documents.push({ + contentType: attachment.contentType, + index: 0, + attachment, + // We do this cast because we know there attachments (see the checks above). + message: message as MessageAttributesType & { + attachments: Array; + }, }); + }); const saveAttachment = async ({ attachment, @@ -1666,11 +1675,7 @@ export class ConversationView extends window.Backbone.View { message, attachment, type, - }: { - message: MessageAttributesType; - attachment: AttachmentType; - type: 'documents' | 'media'; - }) => { + }: ItemClickEvent) => { switch (type) { case 'documents': { saveAttachment({ message, attachment }); @@ -1719,20 +1724,22 @@ export class ConversationView extends window.Backbone.View { } }); - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', - Component: window.Signal.Components.MediaGallery, + // We present an empty panel briefly, while we wait for props to load. + JSX: <>, onClose: () => { unsubscribe(); }, }); - view.headerTitle = window.i18n('allMedia'); + const headerTitle = window.i18n('allMedia'); const update = async () => { - view.update(await getProps()); + const props = await getProps(); + view.update(); }; - this.listenBack(view); + this.addPanel({ view, headerTitle }); update(); } @@ -1758,7 +1765,7 @@ export class ConversationView extends window.Backbone.View { } showGV1Members(): void { - const { contactCollection } = this.model; + const { contactCollection, id } = this.model; const memberships = contactCollection?.map((conversation: ConversationModel) => { @@ -1768,19 +1775,29 @@ export class ConversationView extends window.Backbone.View { }; }) || []; - const view = new Whisper.ReactWrapperView({ + const reduxState = window.reduxStore.getState(); + const getPreferredBadge = getPreferredBadgeSelector(reduxState); + const theme = getTheme(reduxState); + + const view = new ReactWrapperView({ className: 'group-member-list panel', - Component: ConversationDetailsMembershipList, - props: { - canAddNewMembers: false, - i18n: window.i18n, - maxShownMemberCount: 32, - memberships, - showContactModal: this.showContactModal.bind(this), - }, + JSX: ( + { + this.showContactModal(contactId); + }} + theme={theme} + /> + ), }); - this.listenBack(view); + this.addPanel({ view }); view.render(); } @@ -1913,14 +1930,18 @@ export class ConversationView extends window.Backbone.View { this.listenTo(message, 'expired', closeLightbox); this.listenTo(message, 'change', () => { if (this.lightboxView) { - this.lightboxView.update(getProps()); + this.lightboxView.update(); } }); - const getProps = () => { + const getProps = (): ComponentProps => { const { path, contentType } = tempAttachment; return { + close: () => { + this.lightboxView?.remove(); + }, + i18n: window.i18n, media: [ { attachment: tempAttachment, @@ -1928,7 +1949,7 @@ export class ConversationView extends window.Backbone.View { contentType, index: 0, message: { - attachments: message.get('attachments'), + attachments: message.get('attachments') || [], id: message.get('id'), conversationId: message.get('conversationId'), received_at: message.get('received_at'), @@ -1946,10 +1967,9 @@ export class ConversationView extends window.Backbone.View { this.lightboxView = undefined; } - this.lightboxView = new Whisper.ReactWrapperView({ + this.lightboxView = new ReactWrapperView({ className: 'lightbox-wrapper', - Component: window.Signal.Components.Lightbox, - props: getProps(), + JSX: , onClose: closeLightbox, }); @@ -2025,7 +2045,7 @@ export class ConversationView extends window.Backbone.View { }, }; - this.stickerPreviewModalView = new Whisper.ReactWrapperView({ + this.stickerPreviewModalView = new ReactWrapperView({ className: 'sticker-preview-modal-wrapper', JSX: window.Signal.State.Roots.createStickerPreviewModal( window.reduxStore, @@ -2074,16 +2094,25 @@ export class ConversationView extends window.Backbone.View { this.lightboxView = undefined; } - this.lightboxView = new Whisper.ReactWrapperView({ + this.lightboxView = new ReactWrapperView({ className: 'lightbox-wrapper', - Component: window.Signal.Components.Lightbox, - props: { - getConversation: getConversationSelector(window.reduxStore.getState()), - media, - onForward: this.showForwardMessageModal.bind(this), - onSave, - selectedIndex: selectedIndex >= 0 ? selectedIndex : 0, - }, + JSX: ( + { + this.lightboxView?.remove(); + }} + i18n={window.i18n} + getConversation={getConversationSelector( + window.reduxStore.getState() + )} + media={media} + onForward={messageId => { + this.showForwardMessageModal(messageId); + }} + onSave={onSave} + selectedIndex={selectedIndex >= 0 ? selectedIndex : 0} + /> + ), onClose: () => window.Signal.Backbone.Views.Lightbox.hide(), }); @@ -2177,7 +2206,7 @@ export class ConversationView extends window.Backbone.View { } showGroupLinkManagement(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createGroupLinkManagement( window.reduxStore, @@ -2191,14 +2220,14 @@ export class ConversationView extends window.Backbone.View { } ), }); - view.headerTitle = window.i18n('ConversationDetails--group-link'); + const headerTitle = window.i18n('ConversationDetails--group-link'); - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } showGroupV2Permissions(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createGroupV2Permissions( window.reduxStore, @@ -2212,14 +2241,14 @@ export class ConversationView extends window.Backbone.View { } ), }); - view.headerTitle = window.i18n('permissions'); + const headerTitle = window.i18n('permissions'); - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } showPendingInvites(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, { conversationId: this.model.id, @@ -2232,14 +2261,16 @@ export class ConversationView extends window.Backbone.View { }, }), }); - view.headerTitle = window.i18n('ConversationDetails--requests-and-invites'); + const headerTitle = window.i18n( + 'ConversationDetails--requests-and-invites' + ); - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } showConversationNotificationsSettings(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createConversationNotificationsSettings( window.reduxStore, @@ -2251,23 +2282,22 @@ export class ConversationView extends window.Backbone.View { } ), }); - view.headerTitle = window.i18n('ConversationDetails--notifications'); + const headerTitle = window.i18n('ConversationDetails--notifications'); - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } showChatColorEditor(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createChatColorPicker(window.reduxStore, { conversationId: this.model.get('id'), }), }); + const headerTitle = window.i18n('ChatColorPicker__menu-title'); - view.headerTitle = window.i18n('ChatColorPicker__menu-title'); - - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } @@ -2331,16 +2361,16 @@ export class ConversationView extends window.Backbone.View { this.onOutgoingVideoCallInConversation.bind(this), }; - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'conversation-details-pane panel', JSX: window.Signal.State.Roots.createConversationDetails( window.reduxStore, props ), }); - view.headerTitle = ''; + const headerTitle = ''; - this.listenBack(view); + this.addPanel({ view, headerTitle }); view.render(); } @@ -2366,7 +2396,7 @@ export class ConversationView extends window.Backbone.View { this.resetPanel(); }; - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: 'panel message-detail-wrapper', JSX: window.Signal.State.Roots.createMessageDetail( window.reduxStore, @@ -2386,12 +2416,12 @@ export class ConversationView extends window.Backbone.View { this.listenTo(message, 'expired', onClose); // We could listen to all involved contacts, but we'll call that overkill - this.listenBack(view); + this.addPanel({ view }); view.render(); } showStickerManager(): void { - const view = new Whisper.ReactWrapperView({ + const view = new ReactWrapperView({ className: ['sticker-manager-wrapper', 'panel'].join(' '), JSX: window.Signal.State.Roots.createStickerManager(window.reduxStore), onClose: () => { @@ -2399,7 +2429,7 @@ export class ConversationView extends window.Backbone.View { }, }); - this.listenBack(view); + this.addPanel({ view }); view.render(); } @@ -2413,27 +2443,29 @@ export class ConversationView extends window.Backbone.View { uuid: UUIDStringType; }; }): void { - const view = new Whisper.ReactWrapperView({ - Component: window.Signal.Components.ContactDetail, + const view = new ReactWrapperView({ className: 'contact-detail-pane panel', - props: { - contact, - hasSignalAccount: Boolean(signalAccount), - onSendMessage: () => { - if (signalAccount) { - this.startConversation( - signalAccount.phoneNumber, - signalAccount.uuid - ); - } - }, - }, + JSX: ( + { + if (signalAccount) { + this.startConversation( + signalAccount.phoneNumber, + signalAccount.uuid + ); + } + }} + /> + ), onClose: () => { this.resetPanel(); }, }); - this.listenBack(view); + this.addPanel({ view }); } startConversation(e164: string, uuid: UUIDStringType): void { @@ -2460,24 +2492,24 @@ export class ConversationView extends window.Backbone.View { ); } - listenBack(view: AnyViewClass): void { + addPanel(panel: PanelType): void { this.panels = this.panels || []; if (this.panels.length === 0) { this.previousFocus = document.activeElement as HTMLElement; } - this.panels.unshift(view); - view.$el.insertAfter(this.$('.panel').last()); - view.$el.one('animationend', () => { - view.$el.addClass('panel--static'); + this.panels.unshift(panel); + panel.view.$el.insertAfter(this.$('.panel').last()); + panel.view.$el.one('animationend', () => { + panel.view.$el.addClass('panel--static'); }); window.reduxActions.conversations.setSelectedConversationPanelDepth( this.panels.length ); window.reduxActions.conversations.setSelectedConversationHeaderTitle( - view.headerTitle + panel.headerTitle ); } resetPanel(): void { @@ -2485,7 +2517,7 @@ export class ConversationView extends window.Backbone.View { return; } - const view = this.panels.shift(); + const panel = this.panels.shift(); if ( this.panels.length === 0 && @@ -2497,12 +2529,12 @@ export class ConversationView extends window.Backbone.View { } if (this.panels.length > 0) { - this.panels[0].$el.fadeIn(250); + this.panels[0].view.$el.fadeIn(250); } - if (view) { - view.$el.addClass('panel--remove').one('transitionend', () => { - view.remove(); + if (panel) { + panel.view.$el.addClass('panel--remove').one('transitionend', () => { + panel.view.remove(); if (this.panels.length === 0) { // Make sure poppers are positioned properly diff --git a/ts/views/inbox_view.ts b/ts/views/inbox_view.tsx similarity index 93% rename from ts/views/inbox_view.ts rename to ts/views/inbox_view.tsx index b1f6a4082..67c70ea02 100644 --- a/ts/views/inbox_view.ts +++ b/ts/views/inbox_view.tsx @@ -1,11 +1,14 @@ -// Copyright 2014-2021 Signal Messenger, LLC +// Copyright 2014-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import * as React from 'react'; import * as Backbone from 'backbone'; import * as log from '../logging/log'; import type { ConversationModel } from '../models/conversations'; +import { ReactWrapperView } from './ReactWrapperView'; import { showToast } from '../util/showToast'; import { strictAssert } from '../util/assert'; +import { WhatsNewLink } from '../components/WhatsNewLink'; import { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed'; window.Whisper = window.Whisper || {}; @@ -163,12 +166,13 @@ Whisper.InboxView = Whisper.View.extend({ return; } const { showWhatsNewModal } = window.reduxActions.globalModals; - this.whatsNewLink = new Whisper.ReactWrapperView({ - Component: window.Signal.Components.WhatsNewLink, - props: { - i18n: window.i18n, - showWhatsNewModal, - }, + this.whatsNewLink = new ReactWrapperView({ + JSX: ( + + ), }); this.$('.whats-new-placeholder').append(this.whatsNewLink.el); }, @@ -176,7 +180,7 @@ Whisper.InboxView = Whisper.View.extend({ if (this.leftPaneView) { return; } - this.leftPaneView = new Whisper.ReactWrapperView({ + this.leftPaneView = new ReactWrapperView({ className: 'left-pane-wrapper', JSX: window.Signal.State.Roots.createLeftPane(window.reduxStore), }); diff --git a/ts/window.d.ts b/ts/window.d.ts index eff55fa56..3e3b05de9 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -74,17 +74,11 @@ import { BatcherType } from './util/batcher'; import { AttachmentList } from './components/conversation/AttachmentList'; import { ChatColorPicker } from './components/ChatColorPicker'; import { ConfirmationDialog } from './components/ConfirmationDialog'; -import { ContactDetail } from './components/conversation/ContactDetail'; import { ContactModal } from './components/conversation/ContactModal'; -import { ErrorModal } from './components/ErrorModal'; -import { Lightbox } from './components/Lightbox'; -import { MediaGallery } from './components/conversation/media-gallery/MediaGallery'; import { MessageDetail } from './components/conversation/MessageDetail'; -import { ProgressModal } from './components/ProgressModal'; import { Quote } from './components/conversation/Quote'; import { StagedLinkPreview } from './components/conversation/StagedLinkPreview'; import { DisappearingTimeDialog } from './components/DisappearingTimeDialog'; -import { WhatsNewLink } from './components/WhatsNewLink'; import { DownloadedAttachmentType } from './types/Attachment'; import { ElectronLocaleType } from './util/mapToSupportLocale'; import { SignalProtocolStore } from './SignalProtocolStore'; @@ -370,17 +364,11 @@ declare global { AttachmentList: typeof AttachmentList; ChatColorPicker: typeof ChatColorPicker; ConfirmationDialog: typeof ConfirmationDialog; - ContactDetail: typeof ContactDetail; ContactModal: typeof ContactModal; DisappearingTimeDialog: typeof DisappearingTimeDialog; - ErrorModal: typeof ErrorModal; - Lightbox: typeof Lightbox; - MediaGallery: typeof MediaGallery; MessageDetail: typeof MessageDetail; - ProgressModal: typeof ProgressModal; Quote: typeof Quote; StagedLinkPreview: typeof StagedLinkPreview; - WhatsNewLink: typeof WhatsNewLink; }; OS: typeof OS; State: { @@ -506,17 +494,6 @@ export class CanvasVideoRenderer { constructor(canvas: Ref); } -export class AnyViewClass extends window.Backbone.View { - public headerTitle?: string; - static show(view: typeof AnyViewClass, element: Element): void; - - constructor(options?: any); -} - -export class BasicReactWrapperViewClass extends AnyViewClass { - public update(options: any): void; -} - export type WhisperType = { Conversation: typeof ConversationModel; ConversationCollection: typeof ConversationModelCollectionType; @@ -530,18 +507,11 @@ export type WhisperType = { // Backbone views - // Modernized ConversationView: typeof ConversationView; // Note: we can no longer use 'View.extend' once we've moved to Typescript's preferred // 'extend View' syntax. Thus, we'll need to typescriptify most of it at once. - ClearDataView: typeof AnyViewClass; - ConversationLoadingScreen: typeof AnyViewClass; - GroupMemberList: typeof AnyViewClass; - InboxView: typeof AnyViewClass; - KeyVerificationPanelView: typeof AnyViewClass; - ReactWrapperView: typeof BasicReactWrapperViewClass; - SafetyNumberChangeDialogView: typeof AnyViewClass; - View: typeof AnyViewClass; + InboxView: typeof Backbone.View; + View: typeof Backbone.View; };