Removes Backbone.View and jQuery
This commit is contained in:
parent
26d689982a
commit
5e6eeecede
33 changed files with 1113 additions and 7637 deletions
|
@ -1349,29 +1349,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## jquery
|
||||
|
||||
Copyright JS Foundation and other contributors, https://js.foundation/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## js-yaml
|
||||
|
||||
(The MIT License)
|
||||
|
@ -1767,20 +1744,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## mustache
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2009 Chris Wanstrath (Ruby)
|
||||
Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
|
||||
Copyright (c) 2010-2015 The mustache.js community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## node-fetch
|
||||
|
||||
License: MIT
|
||||
|
|
|
@ -94,40 +94,6 @@
|
|||
type="text/css"
|
||||
/>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<!--
|
||||
When making changes to these templates, be sure to update test/index.html as well
|
||||
-->
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="app-loading-screen">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--150"></div>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'>{{ message }}</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
<div class="ConversationView__template"></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="recorder">
|
||||
<button class='close' tabIndex='2'><span class='icon'></span></button>
|
||||
<span class='time'>0:00</span>
|
||||
<button class='finish' tabIndex='1'><span class='icon'></span></button>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="group-member-list">
|
||||
<div class='container' tabindex='0'>
|
||||
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }}
|
||||
</div>
|
||||
</script>
|
||||
</head>
|
||||
<body class="overflow-hidden">
|
||||
<div id="app-container">
|
||||
|
@ -158,7 +124,6 @@
|
|||
type="text/javascript"
|
||||
src="ts/manage_full_screen_class.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="ts/backbone/backboneJquery.js"></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="ts/backbone/reliable_trigger.js"
|
||||
|
|
|
@ -122,7 +122,6 @@
|
|||
"history": "4.9.0",
|
||||
"humanize-duration": "3.27.1",
|
||||
"intl-tel-input": "17.0.13",
|
||||
"jquery": "3.5.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"linkify-it": "2.2.0",
|
||||
"lodash": "4.17.21",
|
||||
|
@ -132,7 +131,6 @@
|
|||
"memoizee": "0.4.14",
|
||||
"moment": "2.29.4",
|
||||
"mp4box": "0.5.2",
|
||||
"mustache": "2.3.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"normalize-path": "3.0.0",
|
||||
"p-map": "2.1.0",
|
||||
|
@ -220,7 +218,6 @@
|
|||
"@types/history": "4.7.2",
|
||||
"@types/humanize-duration": "3.18.1",
|
||||
"@types/intl-tel-input": "17.0.4",
|
||||
"@types/jquery": "3.5.6",
|
||||
"@types/js-yaml": "3.12.0",
|
||||
"@types/json-to-ast": "2.1.2",
|
||||
"@types/linkify-it": "2.1.0",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -15,9 +15,5 @@
|
|||
<div id="root"></div>
|
||||
<script type="text/javascript" src="../../js/components.js"></script>
|
||||
<script type="text/javascript" src="../../ts/set_os_class.js"></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../../ts/backbone/backbonejQuery.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -12,46 +12,7 @@
|
|||
<div id="mocha"></div>
|
||||
<div id="tests"></div>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="app-loading-screen">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--150"></div>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'>{{ message }}</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="conversation">
|
||||
<div id="ConversationView__template"></div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="recorder">
|
||||
<button class='finish'><span class='icon'></span></button>
|
||||
<span class='time'>0:00</span>
|
||||
<button class='close'><span class='icon'></span></button>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="file-size-modal">
|
||||
{{ file-size-warning }}
|
||||
({{ limit }}{{ units }})
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="group-member-list">
|
||||
<div class='container'>
|
||||
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../js/components.js"></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../ts/backbone/backboneJquery.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../ts/backbone/reliable_trigger.js"
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// we are requiring backbone in preload.js, and we need to tell backbone where
|
||||
// jquery is after it's loaded.
|
||||
window.Backbone.$ = window.Backbone.$ || window.$;
|
|
@ -149,7 +149,6 @@ function trigger<
|
|||
|
||||
[
|
||||
window.Backbone.Model.prototype,
|
||||
window.Backbone.View.prototype,
|
||||
window.Backbone.Collection.prototype,
|
||||
window.Backbone.Events,
|
||||
].forEach(proto => {
|
||||
|
|
|
@ -1155,12 +1155,13 @@ export async function startApp(): Promise<void> {
|
|||
conversationChanged,
|
||||
conversationRemoved,
|
||||
removeAllConversations,
|
||||
onConversationClosed,
|
||||
} = window.reduxActions.conversations;
|
||||
|
||||
convoCollection.on('remove', conversation => {
|
||||
const { id } = conversation || {};
|
||||
|
||||
conversation.trigger('unload', 'removed');
|
||||
onConversationClosed(id, 'removed');
|
||||
conversationRemoved(id);
|
||||
});
|
||||
convoCollection.on('add', conversation => {
|
||||
|
@ -1580,7 +1581,7 @@ export async function startApp(): Promise<void> {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
conversation.trigger('unload', 'keyboard shortcut close');
|
||||
onConversationClosed(conversation.id, 'keyboard shortcut close');
|
||||
window.reduxActions.conversations.showConversation({
|
||||
conversationId: undefined,
|
||||
messageId: undefined,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Globals } from '@react-spring/web';
|
||||
import classNames from 'classnames';
|
||||
|
@ -10,10 +9,9 @@ import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
|||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||
import type { ToastType } from '../types/Toast';
|
||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import type { ReplacementValuesType } from '../types/Util';
|
||||
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { AppViewType } from '../state/ducks/app';
|
||||
import { Inbox } from './Inbox';
|
||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { TitleBarContainer } from './TitleBarContainer';
|
||||
|
@ -28,6 +26,7 @@ type PropsType = {
|
|||
renderCallManager: () => JSX.Element;
|
||||
renderGlobalModalContainer: () => JSX.Element;
|
||||
isShowingStoriesView: boolean;
|
||||
i18n: LocalizerType;
|
||||
renderStories: (closeView: () => unknown) => JSX.Element;
|
||||
hasSelectedStoryData: boolean;
|
||||
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
||||
|
@ -57,41 +56,33 @@ type PropsType = {
|
|||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||
toggleStoriesView: () => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
} & ComponentProps<typeof Inbox>;
|
||||
renderInbox: () => JSX.Element;
|
||||
};
|
||||
|
||||
export function App({
|
||||
appView,
|
||||
executeMenuAction,
|
||||
executeMenuRole,
|
||||
hasInitialLoadCompleted,
|
||||
hasCustomTitleBar,
|
||||
hasSelectedStoryData,
|
||||
hideMenuBar,
|
||||
hideToast,
|
||||
i18n,
|
||||
isCustomizingPreferredReactions,
|
||||
isFullScreen,
|
||||
isMaximized,
|
||||
isShowingStoriesView,
|
||||
hasCustomTitleBar,
|
||||
menuOptions,
|
||||
onUndoArchive,
|
||||
openInbox,
|
||||
openFileInFolder,
|
||||
openInbox,
|
||||
registerSingleDevice,
|
||||
renderCallManager,
|
||||
renderCustomizingPreferredReactionsModal,
|
||||
renderGlobalModalContainer,
|
||||
renderLeftPane,
|
||||
renderInbox,
|
||||
renderLightbox,
|
||||
renderStories,
|
||||
renderStoryViewer,
|
||||
requestVerification,
|
||||
scrollToMessage,
|
||||
selectedConversationId,
|
||||
selectedMessage,
|
||||
selectedMessageSource,
|
||||
showConversation,
|
||||
showWhatsNewModal,
|
||||
theme,
|
||||
titleBarDoubleClick,
|
||||
toast,
|
||||
|
@ -115,23 +106,7 @@ export function App({
|
|||
/>
|
||||
);
|
||||
} else if (appView === AppViewType.Inbox) {
|
||||
contents = (
|
||||
<Inbox
|
||||
hasInitialLoadCompleted={hasInitialLoadCompleted}
|
||||
i18n={i18n}
|
||||
isCustomizingPreferredReactions={isCustomizingPreferredReactions}
|
||||
renderCustomizingPreferredReactionsModal={
|
||||
renderCustomizingPreferredReactionsModal
|
||||
}
|
||||
renderLeftPane={renderLeftPane}
|
||||
scrollToMessage={scrollToMessage}
|
||||
selectedConversationId={selectedConversationId}
|
||||
selectedMessage={selectedMessage}
|
||||
selectedMessageSource={selectedMessageSource}
|
||||
showConversation={showConversation}
|
||||
showWhatsNewModal={showWhatsNewModal}
|
||||
/>
|
||||
);
|
||||
contents = renderInbox();
|
||||
}
|
||||
|
||||
// This are here so that themes are properly applied to anything that is
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type * as Backbone from 'backbone';
|
||||
|
||||
type PropsType = {
|
||||
View: typeof Backbone.View;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function BackboneHost({ View, className }: PropsType): JSX.Element {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<Backbone.View | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const view = new View({
|
||||
el: hostRef.current,
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
if (!viewRef || !viewRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewRef.current.remove();
|
||||
viewRef.current = undefined;
|
||||
};
|
||||
}, [View]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className} ref={hostRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,11 +2,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import type { ShowConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationView } from '../views/conversation_view';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
import * as log from '../logging/log';
|
||||
|
@ -16,11 +14,15 @@ import { WhatsNewLink } from './WhatsNewLink';
|
|||
import { showToast } from '../util/showToast';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { SelectedMessageSource } from '../state/ducks/conversationsEnums';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
|
||||
export type PropsType = {
|
||||
hasInitialLoadCompleted: boolean;
|
||||
i18n: LocalizerType;
|
||||
isCustomizingPreferredReactions: boolean;
|
||||
onConversationClosed: (id: string, reason: string) => unknown;
|
||||
onConversationOpened: (id: string, messageId?: string) => unknown;
|
||||
renderConversationView: () => JSX.Element;
|
||||
renderCustomizingPreferredReactionsModal: () => JSX.Element;
|
||||
renderLeftPane: () => JSX.Element;
|
||||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||
|
@ -35,6 +37,9 @@ export function Inbox({
|
|||
hasInitialLoadCompleted,
|
||||
i18n,
|
||||
isCustomizingPreferredReactions,
|
||||
onConversationClosed,
|
||||
onConversationOpened,
|
||||
renderConversationView,
|
||||
renderCustomizingPreferredReactionsModal,
|
||||
renderLeftPane,
|
||||
scrollToMessage,
|
||||
|
@ -48,14 +53,28 @@ export function Inbox({
|
|||
const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
|
||||
useState(hasInitialLoadCompleted);
|
||||
|
||||
const conversationMountRef = useRef<HTMLDivElement | null>(null);
|
||||
const conversationViewRef = useRef<ConversationView | null>(null);
|
||||
|
||||
const [prevConversation, setPrevConversation] = useState<
|
||||
ConversationModel | undefined
|
||||
>();
|
||||
const prevConversationId = usePrevious(
|
||||
selectedConversationId,
|
||||
selectedConversationId
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevConversationId !== selectedConversationId) {
|
||||
if (prevConversationId) {
|
||||
onConversationClosed(prevConversationId, 'opened another conversation');
|
||||
}
|
||||
|
||||
if (selectedConversationId) {
|
||||
onConversationOpened(selectedConversationId, selectedMessage);
|
||||
}
|
||||
} else if (
|
||||
selectedConversationId &&
|
||||
selectedMessage &&
|
||||
selectedMessageSource !== SelectedMessageSource.Focus
|
||||
) {
|
||||
scrollToMessage(selectedConversationId, selectedMessage);
|
||||
}
|
||||
|
||||
if (!selectedConversationId) {
|
||||
return;
|
||||
}
|
||||
|
@ -66,53 +85,16 @@ export function Inbox({
|
|||
strictAssert(conversation, 'Conversation must be found');
|
||||
|
||||
conversation.setMarkedUnread(false);
|
||||
|
||||
if (!prevConversation || prevConversation.id !== selectedConversationId) {
|
||||
// We create a mount point because when calling .remove() on the Backbone
|
||||
// view it'll also remove the mount point along with it.
|
||||
const viewMountNode = document.createElement('div');
|
||||
conversationMountRef.current?.appendChild(viewMountNode);
|
||||
|
||||
// Make sure to unload the previous conversation along with calling
|
||||
// Backbone's remove so that it is taken out of the DOM.
|
||||
if (prevConversation) {
|
||||
prevConversation.trigger('unload', 'opened another conversation');
|
||||
}
|
||||
conversationViewRef.current?.remove();
|
||||
|
||||
// Can't import ConversationView directly because conversation_view
|
||||
// needs access to window.Signal first.
|
||||
const view = new window.Whisper.ConversationView({
|
||||
el: viewMountNode,
|
||||
model: conversation,
|
||||
});
|
||||
conversationViewRef.current = view;
|
||||
|
||||
setPrevConversation(conversation);
|
||||
|
||||
conversation.trigger('opened', selectedMessage);
|
||||
} else if (
|
||||
selectedMessage &&
|
||||
selectedMessageSource !== SelectedMessageSource.Focus
|
||||
) {
|
||||
scrollToMessage(conversation.id, selectedMessage);
|
||||
}
|
||||
}, [
|
||||
prevConversation,
|
||||
onConversationClosed,
|
||||
onConversationOpened,
|
||||
prevConversationId,
|
||||
scrollToMessage,
|
||||
selectedConversationId,
|
||||
selectedMessage,
|
||||
selectedMessageSource,
|
||||
]);
|
||||
|
||||
// Whenever the selectedConversationId is cleared we should also ensure
|
||||
// that prevConversation is cleared too.
|
||||
useEffect(() => {
|
||||
if (prevConversation && !selectedConversationId) {
|
||||
setPrevConversation(undefined);
|
||||
}
|
||||
}, [prevConversation, selectedConversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
function refreshConversation({
|
||||
newId,
|
||||
|
@ -121,7 +103,7 @@ export function Inbox({
|
|||
newId: string;
|
||||
oldId: string;
|
||||
}) {
|
||||
if (prevConversation && prevConversation.get('id') === oldId) {
|
||||
if (prevConversationId === oldId) {
|
||||
showConversation({ conversationId: newId });
|
||||
}
|
||||
}
|
||||
|
@ -129,10 +111,10 @@ export function Inbox({
|
|||
// Close current opened conversation to reload the group information once
|
||||
// linked.
|
||||
function unload() {
|
||||
if (!prevConversation) {
|
||||
if (!prevConversationId) {
|
||||
return;
|
||||
}
|
||||
prevConversation.trigger('unload', 'force unload requested');
|
||||
onConversationClosed(prevConversationId, 'force unload requested');
|
||||
}
|
||||
|
||||
function packInstallFailed() {
|
||||
|
@ -150,7 +132,7 @@ export function Inbox({
|
|||
window.Whisper.events.off('refreshConversation', refreshConversation);
|
||||
window.Whisper.events.off('setupAsNewDevice', unload);
|
||||
};
|
||||
}, [prevConversation, showConversation]);
|
||||
}, [onConversationClosed, prevConversationId, showConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (internalHasInitialLoadCompleted) {
|
||||
|
@ -226,8 +208,15 @@ export function Inbox({
|
|||
|
||||
<div className="conversation-stack">
|
||||
<div id="toast" />
|
||||
<div className="conversation" ref={conversationMountRef} />
|
||||
{!prevConversation && (
|
||||
{selectedConversationId && (
|
||||
<div
|
||||
className="conversation"
|
||||
id={`conversation-${selectedConversationId}`}
|
||||
>
|
||||
{renderConversationView()}
|
||||
</div>
|
||||
)}
|
||||
{!prevConversationId && (
|
||||
<div className="no-conversation-open">
|
||||
<div className="module-splash-screen__logo module-img--128 module-logo-blue" />
|
||||
<h3>{i18n('welcomeToSignal')}</h3>
|
||||
|
|
|
@ -236,9 +236,9 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
throttledBumpTyping?: () => void;
|
||||
|
||||
throttledFetchSMSOnlyUUID?: () => Promise<void> | void;
|
||||
throttledFetchSMSOnlyUUID?: () => Promise<void> | undefined;
|
||||
|
||||
throttledMaybeMigrateV1Group?: () => Promise<void> | void;
|
||||
throttledMaybeMigrateV1Group?: () => Promise<void> | undefined;
|
||||
|
||||
throttledGetProfiles?: () => Promise<void>;
|
||||
|
||||
|
@ -2286,7 +2286,10 @@ export class ConversationModel extends window.Backbone
|
|||
void this.updateLastMessage();
|
||||
|
||||
if (isLocalAction) {
|
||||
this.trigger('unload', 'deleted from message request');
|
||||
window.reduxActions.conversations.onConversationClosed(
|
||||
this.id,
|
||||
'deleted from message request'
|
||||
);
|
||||
|
||||
if (isGroupV1(this.attributes)) {
|
||||
await this.leaveGroup();
|
||||
|
@ -2305,7 +2308,10 @@ export class ConversationModel extends window.Backbone
|
|||
void this.updateLastMessage();
|
||||
|
||||
if (isLocalAction) {
|
||||
this.trigger('unload', 'blocked and deleted from message request');
|
||||
window.reduxActions.conversations.onConversationClosed(
|
||||
this.id,
|
||||
'blocked and deleted from message request'
|
||||
);
|
||||
|
||||
if (isGroupV1(this.attributes)) {
|
||||
await this.leaveGroup();
|
||||
|
|
|
@ -9,11 +9,8 @@ import { copyFileSync, readFileSync, writeFileSync } from 'fs';
|
|||
console.log('Concatenating...');
|
||||
|
||||
const BASE_BOWER = join(__dirname, '../../components');
|
||||
const BASE_NODE = join(__dirname, '../../node_modules');
|
||||
const CONCAT_TARGET = join(__dirname, '../../js/components.js');
|
||||
const CONCAT_SOURCES = [
|
||||
join(BASE_NODE, 'jquery/dist/jquery.js'),
|
||||
join(BASE_NODE, 'mustache/mustache.js'),
|
||||
join(BASE_BOWER, 'webaudiorecorder/lib/WebAudioRecorder.js'),
|
||||
];
|
||||
|
||||
|
|
|
@ -103,12 +103,12 @@ type AddPendingAttachmentActionType = {
|
|||
payload: AttachmentDraftType;
|
||||
};
|
||||
|
||||
type ReplaceAttachmentsActionType = {
|
||||
export type ReplaceAttachmentsActionType = {
|
||||
type: typeof REPLACE_ATTACHMENTS;
|
||||
payload: ReadonlyArray<AttachmentDraftType>;
|
||||
};
|
||||
|
||||
type ResetComposerActionType = {
|
||||
export type ResetComposerActionType = {
|
||||
type: typeof RESET_COMPOSER;
|
||||
};
|
||||
|
||||
|
@ -117,7 +117,7 @@ type SetComposerDisabledStateActionType = {
|
|||
payload: boolean;
|
||||
};
|
||||
|
||||
type SetFocusActionType = {
|
||||
export type SetFocusActionType = {
|
||||
type: typeof SET_FOCUS;
|
||||
};
|
||||
|
||||
|
@ -126,7 +126,7 @@ type SetHighQualitySettingActionType = {
|
|||
payload: boolean;
|
||||
};
|
||||
|
||||
type SetQuotedMessageActionType = {
|
||||
export type SetQuotedMessageActionType = {
|
||||
type: typeof SET_QUOTED_MESSAGE;
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
|
||||
};
|
||||
|
@ -473,7 +473,7 @@ function getAttachmentsFromConversationModel(
|
|||
return conversation?.get('draftAttachments') || [];
|
||||
}
|
||||
|
||||
function setQuoteByMessageId(
|
||||
export function setQuoteByMessageId(
|
||||
conversationId: string,
|
||||
messageId: string | undefined
|
||||
): ThunkAction<
|
||||
|
@ -627,7 +627,7 @@ function addPendingAttachment(
|
|||
};
|
||||
}
|
||||
|
||||
function setComposerFocus(
|
||||
export function setComposerFocus(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, SetFocusActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
|
@ -894,7 +894,7 @@ function removeAttachment(
|
|||
};
|
||||
}
|
||||
|
||||
function replaceAttachments(
|
||||
export function replaceAttachments(
|
||||
conversationId: string,
|
||||
attachments: ReadonlyArray<AttachmentDraftType>
|
||||
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||
|
@ -954,7 +954,7 @@ function reactToMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function resetComposer(): ResetComposerActionType {
|
||||
export function resetComposer(): ResetComposerActionType {
|
||||
return {
|
||||
type: RESET_COMPOSER,
|
||||
};
|
||||
|
|
|
@ -132,6 +132,23 @@ import { DAY } from '../../util/durations';
|
|||
import { isNotNil } from '../../util/isNotNil';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import { startConversation } from '../../util/startConversation';
|
||||
import { UUIDKind } from '../../types/UUID';
|
||||
import {
|
||||
removeLinkPreview,
|
||||
suspendLinkPreviews,
|
||||
} from '../../services/LinkPreview';
|
||||
import type {
|
||||
ReplaceAttachmentsActionType,
|
||||
SetFocusActionType,
|
||||
SetQuotedMessageActionType,
|
||||
ResetComposerActionType,
|
||||
} from './composer';
|
||||
import {
|
||||
replaceAttachments,
|
||||
setComposerFocus,
|
||||
setQuoteByMessageId,
|
||||
resetComposer,
|
||||
} from './composer';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -884,6 +901,8 @@ export type ConversationActionType =
|
|||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
onConversationOpened,
|
||||
onConversationClosed,
|
||||
acceptConversation,
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
addMembersToGroup,
|
||||
|
@ -909,7 +928,6 @@ export const actions = {
|
|||
conversationChanged,
|
||||
conversationRemoved,
|
||||
conversationStoppedByMissingVerification,
|
||||
conversationUnloaded,
|
||||
createGroup,
|
||||
deleteAvatarFromDisk,
|
||||
deleteConversation,
|
||||
|
@ -1003,23 +1021,37 @@ export const useConversationsActions = (): BoundActionCreatorsMapObject<
|
|||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function onArchive(conversationId: string): ShowToastActionType {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('onArchive: Conversation not found!');
|
||||
}
|
||||
function onArchive(
|
||||
conversationId: string
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
ConversationUnloadedActionType | ShowToastActionType
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('onArchive: Conversation not found!');
|
||||
}
|
||||
|
||||
conversation.setArchived(true);
|
||||
conversation.trigger('unload', 'archive');
|
||||
conversation.setArchived(true);
|
||||
|
||||
return {
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.ConversationArchived,
|
||||
parameters: {
|
||||
conversationId,
|
||||
onConversationClosed(conversationId, 'archive')(
|
||||
dispatch,
|
||||
getState,
|
||||
undefined
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SHOW_TOAST,
|
||||
payload: {
|
||||
toastType: ToastType.ConversationArchived,
|
||||
parameters: {
|
||||
conversationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
function onUndoArchive(
|
||||
|
@ -1533,8 +1565,13 @@ function deleteMessage({
|
|||
|
||||
function destroyMessages(
|
||||
conversationId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
ConversationUnloadedActionType | NoopActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('destroyMessages: No conversation found');
|
||||
|
@ -1544,9 +1581,14 @@ function destroyMessages(
|
|||
name: 'destroymessages',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: async () => {
|
||||
conversation.trigger('unload', 'delete messages');
|
||||
onConversationClosed(conversationId, 'delete messages')(
|
||||
dispatch,
|
||||
getState,
|
||||
undefined
|
||||
);
|
||||
|
||||
await conversation.destroyMessages();
|
||||
void conversation.updateLastMessage();
|
||||
drop(conversation.updateLastMessage());
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2240,14 +2282,6 @@ function conversationRemoved(id: string): ConversationRemovedActionType {
|
|||
},
|
||||
};
|
||||
}
|
||||
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
||||
return {
|
||||
type: CONVERSATION_UNLOADED,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGroup(
|
||||
createGroupV2 = groups.createGroupV2
|
||||
|
@ -3131,7 +3165,7 @@ export function scrollToMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
void conversation.loadAndScroll(messageId);
|
||||
drop(conversation.loadAndScroll(messageId));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3457,6 +3491,152 @@ function showConversation({
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function onConversationOpened(
|
||||
conversationId: string,
|
||||
messageId?: string
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
| ReplaceAttachmentsActionType
|
||||
| ResetComposerActionType
|
||||
| SetFocusActionType
|
||||
| SetQuotedMessageActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('onConversationOpened: Conversation not found');
|
||||
}
|
||||
|
||||
conversation.onOpenStart();
|
||||
|
||||
if (messageId) {
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (message) {
|
||||
drop(conversation.loadAndScroll(messageId));
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`onOpened: Did not find message ${messageId}`);
|
||||
}
|
||||
|
||||
const { retryPlaceholders } = window.Signal.Services;
|
||||
if (retryPlaceholders) {
|
||||
await retryPlaceholders.findByConversationAndMarkOpened(conversation.id);
|
||||
}
|
||||
|
||||
const loadAndUpdate = async () => {
|
||||
drop(
|
||||
Promise.all([
|
||||
conversation.loadNewestMessages(undefined, undefined),
|
||||
conversation.updateLastMessage(),
|
||||
conversation.updateUnread(),
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
drop(loadAndUpdate());
|
||||
|
||||
dispatch(setComposerFocus(conversation.id));
|
||||
|
||||
const quotedMessageId = conversation.get('quotedMessageId');
|
||||
if (quotedMessageId) {
|
||||
setQuoteByMessageId(conversation.id, quotedMessageId)(
|
||||
dispatch,
|
||||
getState,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
drop(conversation.fetchLatestGroupV2Data());
|
||||
strictAssert(
|
||||
conversation.throttledMaybeMigrateV1Group !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
drop(conversation.throttledMaybeMigrateV1Group());
|
||||
strictAssert(
|
||||
conversation.throttledFetchSMSOnlyUUID !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
drop(conversation.throttledFetchSMSOnlyUUID());
|
||||
|
||||
const ourUuid = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
|
||||
if (
|
||||
!isGroup(conversation.attributes) ||
|
||||
(ourUuid && conversation.hasMember(ourUuid))
|
||||
) {
|
||||
strictAssert(
|
||||
conversation.throttledGetProfiles !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
await conversation.throttledGetProfiles();
|
||||
}
|
||||
|
||||
drop(conversation.updateVerified());
|
||||
|
||||
replaceAttachments(
|
||||
conversation.get('id'),
|
||||
conversation.get('draftAttachments') || []
|
||||
)(dispatch, getState, undefined);
|
||||
dispatch(resetComposer());
|
||||
};
|
||||
}
|
||||
|
||||
function onConversationClosed(
|
||||
conversationId: string,
|
||||
reason: string
|
||||
): ThunkAction<void, RootStateType, unknown, ConversationUnloadedActionType> {
|
||||
return async dispatch => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('onConversationClosed: Conversation not found');
|
||||
}
|
||||
|
||||
log.info(
|
||||
'unloading conversation',
|
||||
conversation.idForLogging(),
|
||||
'due to:',
|
||||
reason
|
||||
);
|
||||
|
||||
if (conversation.get('draftChanged')) {
|
||||
if (conversation.hasDraft()) {
|
||||
const now = Date.now();
|
||||
const activeAt = conversation.get('active_at') || now;
|
||||
|
||||
conversation.set({
|
||||
active_at: activeAt,
|
||||
draftChanged: false,
|
||||
draftTimestamp: now,
|
||||
timestamp: now,
|
||||
});
|
||||
} else {
|
||||
conversation.set({
|
||||
draftChanged: false,
|
||||
draftTimestamp: null,
|
||||
});
|
||||
}
|
||||
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
|
||||
drop(conversation.updateLastMessage());
|
||||
}
|
||||
|
||||
removeLinkPreview();
|
||||
suspendLinkPreviews();
|
||||
|
||||
dispatch({
|
||||
type: CONVERSATION_UNLOADED,
|
||||
payload: {
|
||||
id: conversationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function showArchivedConversations(): ShowArchivedConversationsActionType {
|
||||
return {
|
||||
type: 'SHOW_ARCHIVED_CONVERSATIONS',
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import type { Store } from 'redux';
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
|
||||
import type { PropsType } from '../smart/ConversationView';
|
||||
import { SmartConversationView } from '../smart/ConversationView';
|
||||
|
||||
export const createConversationView = (
|
||||
store: Store,
|
||||
props: PropsType
|
||||
): React.ReactElement => (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
name="createConversationView"
|
||||
closeView={() => {
|
||||
window.reduxActions.conversations.showConversation({
|
||||
conversationId: undefined,
|
||||
messageId: undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SmartConversationView {...props} />
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
);
|
|
@ -8,9 +8,7 @@ import type { MenuItemConstructorOptions } from 'electron';
|
|||
import type { MenuActionType } from '../../types/menu';
|
||||
import { App } from '../../components/App';
|
||||
import { SmartCallManager } from './CallManager';
|
||||
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
|
||||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||
import { SmartLeftPane } from './LeftPane';
|
||||
import { SmartLightbox } from './Lightbox';
|
||||
import { SmartStories } from './Stories';
|
||||
import { SmartStoryViewer } from './StoryViewer';
|
||||
|
@ -28,10 +26,14 @@ import {
|
|||
shouldShowStoriesView,
|
||||
} from '../selectors/stories';
|
||||
import { getHideMenuBar } from '../selectors/items';
|
||||
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { ModalContainer } from '../../components/ModalContainer';
|
||||
import { SmartInbox } from './Inbox';
|
||||
|
||||
function renderInbox(): JSX.Element {
|
||||
return <SmartInbox />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const i18n = getIntl(state);
|
||||
|
@ -40,7 +42,6 @@ const mapStateToProps = (state: StateType) => {
|
|||
...state.app,
|
||||
i18n,
|
||||
localeMessages: getLocaleMessages(state),
|
||||
isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state),
|
||||
isMaximized: getIsMainWindowMaximized(state),
|
||||
isFullScreen: getIsMainWindowFullScreen(state),
|
||||
menuOptions: getMenuOptions(state),
|
||||
|
@ -51,11 +52,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
<SmartCallManager />
|
||||
</ModalContainer>
|
||||
),
|
||||
renderCustomizingPreferredReactionsModal: () => (
|
||||
<SmartCustomizingPreferredReactionsModal />
|
||||
),
|
||||
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
|
||||
renderLeftPane: () => <SmartLeftPane />,
|
||||
renderLightbox: () => <SmartLightbox />,
|
||||
isShowingStoriesView: shouldShowStoriesView(state),
|
||||
renderStories: (closeView: () => unknown) => (
|
||||
|
@ -69,6 +66,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
<SmartStoryViewer />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
renderInbox,
|
||||
requestVerification: (
|
||||
type: 'sms' | 'voice',
|
||||
number: string,
|
||||
|
@ -85,9 +83,6 @@ const mapStateToProps = (state: StateType) => {
|
|||
registerSingleDevice: (number: string, code: string): Promise<void> => {
|
||||
return window.getAccountManager().registerSingleDevice(number, code);
|
||||
},
|
||||
selectedConversationId: state.conversations.selectedConversationId,
|
||||
selectedMessage: state.conversations.selectedMessage,
|
||||
selectedMessageSource: state.conversations.selectedMessageSource,
|
||||
theme: getTheme(state),
|
||||
|
||||
executeMenuRole: (role: MenuItemConstructorOptions['role']): void => {
|
||||
|
|
|
@ -23,17 +23,20 @@ import { SmartPendingInvites } from './PendingInvites';
|
|||
import { SmartStickerManager } from './StickerManager';
|
||||
import { SmartTimeline } from './Timeline';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getTopPanel } from '../selectors/conversations';
|
||||
import {
|
||||
getSelectedConversationId,
|
||||
getTopPanel,
|
||||
} from '../selectors/conversations';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
};
|
||||
export function SmartConversationView(): JSX.Element {
|
||||
const conversationId = useSelector(getSelectedConversationId);
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error('SmartConversationView: No selected conversation');
|
||||
}
|
||||
|
||||
export function SmartConversationView({
|
||||
conversationId,
|
||||
}: PropsType): JSX.Element {
|
||||
const topPanel = useSelector<StateType, PanelRenderType | undefined>(
|
||||
getTopPanel
|
||||
);
|
||||
|
|
70
ts/state/smart/Inbox.tsx
Normal file
70
ts/state/smart/Inbox.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { AppStateType } from '../ducks/app';
|
||||
import type { ConversationsStateType } from '../ducks/conversations';
|
||||
import type { StateType } from '../reducer';
|
||||
import { Inbox } from '../../components/Inbox';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { SmartConversationView } from './ConversationView';
|
||||
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
|
||||
import { SmartLeftPane } from './LeftPane';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
|
||||
|
||||
function renderConversationView() {
|
||||
return <SmartConversationView />;
|
||||
}
|
||||
|
||||
function renderCustomizingPreferredReactionsModal() {
|
||||
return <SmartCustomizingPreferredReactionsModal />;
|
||||
}
|
||||
|
||||
function renderLeftPane() {
|
||||
return <SmartLeftPane />;
|
||||
}
|
||||
|
||||
export function SmartInbox(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const isCustomizingPreferredReactions = useSelector(
|
||||
getIsCustomizingPreferredReactions
|
||||
);
|
||||
const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>(
|
||||
state => state.app
|
||||
);
|
||||
const { selectedConversationId, selectedMessage, selectedMessageSource } =
|
||||
useSelector<StateType, ConversationsStateType>(
|
||||
state => state.conversations
|
||||
);
|
||||
const {
|
||||
onConversationClosed,
|
||||
onConversationOpened,
|
||||
scrollToMessage,
|
||||
showConversation,
|
||||
} = useConversationsActions();
|
||||
const { showWhatsNewModal } = useGlobalModalActions();
|
||||
|
||||
return (
|
||||
<Inbox
|
||||
hasInitialLoadCompleted={hasInitialLoadCompleted}
|
||||
i18n={i18n}
|
||||
isCustomizingPreferredReactions={isCustomizingPreferredReactions}
|
||||
onConversationClosed={onConversationClosed}
|
||||
onConversationOpened={onConversationOpened}
|
||||
renderConversationView={renderConversationView}
|
||||
renderCustomizingPreferredReactionsModal={
|
||||
renderCustomizingPreferredReactionsModal
|
||||
}
|
||||
renderLeftPane={renderLeftPane}
|
||||
scrollToMessage={scrollToMessage}
|
||||
selectedConversationId={selectedConversationId}
|
||||
selectedMessage={selectedMessage}
|
||||
selectedMessageSource={selectedMessageSource}
|
||||
showConversation={showConversation}
|
||||
showWhatsNewModal={showWhatsNewModal}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -131,15 +131,14 @@ void (async () => {
|
|||
}
|
||||
|
||||
const timeline = window.locator(
|
||||
'.timeline-wrapper, .ConversationView__template .react-wrapper'
|
||||
'.timeline-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
|
||||
const deltaList = new Array<number>();
|
||||
for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) {
|
||||
debug('finding composition input and clicking it');
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
|
||||
const input = composeArea.locator('_react=CompositionInput');
|
||||
|
|
|
@ -83,15 +83,14 @@ void (async () => {
|
|||
}
|
||||
|
||||
const timeline = window.locator(
|
||||
'.timeline-wrapper, .ConversationView__template .react-wrapper'
|
||||
'.timeline-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
|
||||
const deltaList = new Array<number>();
|
||||
for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) {
|
||||
debug('finding composition input and clicking it');
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const input = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
|
|
@ -94,8 +94,7 @@ describe('pnp/learn', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
|
|
@ -100,8 +100,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
@ -202,8 +201,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
@ -306,8 +304,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
@ -367,8 +364,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactB');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
@ -444,8 +440,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
@ -536,8 +531,7 @@ describe('pnp/PNI Change', function needsName() {
|
|||
debug('Send message to contactA');
|
||||
{
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const compositionInput = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
|
|
@ -110,8 +110,7 @@ describe('pnp/PNI Signature', function needsName() {
|
|||
const leftPane = window.locator('.left-pane-wrapper');
|
||||
const conversationStack = window.locator('.conversation-stack');
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
|
||||
debug('creating a stranger');
|
||||
|
@ -269,8 +268,7 @@ describe('pnp/PNI Signature', function needsName() {
|
|||
|
||||
const leftPane = window.locator('.left-pane-wrapper');
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
|
||||
debug('opening conversation with the pni contact');
|
||||
|
|
|
@ -122,8 +122,7 @@ describe('storage service', function needsName() {
|
|||
|
||||
debug('Enter message text');
|
||||
const composeArea = window.locator(
|
||||
'.composition-area-wrapper, ' +
|
||||
'.ConversationView__template .react-wrapper'
|
||||
'.composition-area-wrapper, .conversation .ConversationView'
|
||||
);
|
||||
const input = composeArea.locator('_react=CompositionInput');
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ describe('storage service', function needsName() {
|
|||
|
||||
const leftPane = window.locator('.left-pane-wrapper');
|
||||
const conversationView = window.locator(
|
||||
'.ConversationView__template > .react-wrapper'
|
||||
'.conversation > .ConversationView'
|
||||
);
|
||||
|
||||
debug('sending two sticker pack links');
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,101 +19,6 @@
|
|||
"expression": "\\bdocument.write(ln)?\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-$(",
|
||||
"expression": "\\$\\(",
|
||||
"reason": "Potential XSS",
|
||||
"excludedModules": ["node_modules/prelude-ls"]
|
||||
},
|
||||
{
|
||||
"name": "jQuery-html(",
|
||||
"expression": "\\bhtml\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-append(",
|
||||
"expression": "\\bappend\\(",
|
||||
"reason": "Potential XSS",
|
||||
"excludedModules": [
|
||||
"components/bytebuffer",
|
||||
"components/protobuf",
|
||||
"node_modules/google-libphonenumber",
|
||||
"node_modules/handlebars"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "jQuery-appendTo(",
|
||||
"expression": "\\bappendTo\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-insertAfter(",
|
||||
"expression": "\\binsertAfter\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-insertBefore(",
|
||||
"expression": "\\binsertBefore\\(",
|
||||
"reason": "Potential XSS",
|
||||
"excludedModules": ["node_modules/react-dom"]
|
||||
},
|
||||
{
|
||||
"name": "jQuery-prepend(",
|
||||
"expression": "\\bprepend\\(",
|
||||
"reason": "Potential XSS",
|
||||
"excludedModules": ["components/bytebuffer", "node_modules/handlebars"]
|
||||
},
|
||||
{
|
||||
"name": "jQuery-prependTo(",
|
||||
"expression": "\\bprependTo\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-wrap(",
|
||||
"expression": "\\bwrap\\(",
|
||||
"reason": "Potential XSS",
|
||||
"excludedModules": [
|
||||
"components/bytebuffer",
|
||||
"components/protobuf",
|
||||
"node_modules/handlebars",
|
||||
"node_modules/lodash"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "jQuery-wrapInner(",
|
||||
"expression": "\\bwrapInner\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-wrapAll(",
|
||||
"expression": "\\bwrapAll\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-before(",
|
||||
"expression": "\\bbefore\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-after(",
|
||||
"expression": "\\bafter\\(",
|
||||
"reason": "Potential XSS"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-globalEval(",
|
||||
"expression": "\\bglobalEval\\(",
|
||||
"reason": "Arbitrary code execution"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-getScript(",
|
||||
"expression": "\\bgetScript\\(",
|
||||
"reason": "Arbitrary code execution"
|
||||
},
|
||||
{
|
||||
"name": "jQuery-load(",
|
||||
"expression": "\\bload\\(",
|
||||
"reason": "Arbitrary code execution"
|
||||
},
|
||||
{
|
||||
"name": "React-ref",
|
||||
"expression": "\\bref(\\s)*=\\b",
|
||||
|
|
|
@ -1,244 +0,0 @@
|
|||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable camelcase, max-classes-per-file */
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import * as Backbone from 'backbone';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { render } from 'mustache';
|
||||
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { isGroup } from '../util/whatTypeOfConversation';
|
||||
import * as log from '../logging/log';
|
||||
import { createConversationView } from '../state/roots/createConversationView';
|
||||
import {
|
||||
removeLinkPreview,
|
||||
suspendLinkPreviews,
|
||||
} from '../services/LinkPreview';
|
||||
import { UUIDKind } from '../types/UUID';
|
||||
|
||||
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||
// Sub-views
|
||||
private conversationView?: Backbone.View;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(...args: Array<any>) {
|
||||
super(...args);
|
||||
|
||||
// Events on Conversation model
|
||||
this.listenTo(this.model, 'destroy', this.stopListening);
|
||||
|
||||
// These are triggered by InboxView
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
this.listenTo(this.model, 'unload', (reason: string) =>
|
||||
this.unload(`model trigger - ${reason}`)
|
||||
);
|
||||
|
||||
this.render();
|
||||
|
||||
this.setupConversationView();
|
||||
|
||||
window.reduxActions.composer.replaceAttachments(
|
||||
this.model.get('id'),
|
||||
this.model.get('draftAttachments') || []
|
||||
);
|
||||
}
|
||||
|
||||
// We need this ignore because the backbone types really want this to be a string
|
||||
// property, but the property isn't set until after super() is run, meaning that this
|
||||
// classname wouldn't be applied when Backbone creates our el.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
className(): string {
|
||||
return 'conversation';
|
||||
}
|
||||
|
||||
// Same situation as className().
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
id(): string {
|
||||
return `conversation-${this.model.cid}`;
|
||||
}
|
||||
|
||||
// Backbone.View<ConversationModel> is demanded as the return type here, and we can't
|
||||
// satisfy it because of the above difference in signature: className is a function
|
||||
// when it should be a plain string property.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
render(): ConversationView {
|
||||
const template = $('#conversation').html();
|
||||
this.$el.html(render(template, {}));
|
||||
return this;
|
||||
}
|
||||
|
||||
setupConversationView(): void {
|
||||
// setupCompositionArea
|
||||
window.reduxActions.composer.resetComposer();
|
||||
|
||||
// createConversationView root
|
||||
const JSX = createConversationView(window.reduxStore, {
|
||||
conversationId: this.model.id,
|
||||
});
|
||||
|
||||
this.conversationView = new ReactWrapperView({ JSX });
|
||||
this.$('.ConversationView__template').append(this.conversationView.el);
|
||||
}
|
||||
|
||||
unload(reason: string): void {
|
||||
log.info(
|
||||
'unloading conversation',
|
||||
this.model.idForLogging(),
|
||||
'due to:',
|
||||
reason
|
||||
);
|
||||
|
||||
const { conversationUnloaded } = window.reduxActions.conversations;
|
||||
if (conversationUnloaded) {
|
||||
conversationUnloaded(this.model.id);
|
||||
}
|
||||
|
||||
if (this.model.get('draftChanged')) {
|
||||
if (this.model.hasDraft()) {
|
||||
const now = Date.now();
|
||||
const active_at = this.model.get('active_at') || now;
|
||||
|
||||
this.model.set({
|
||||
active_at,
|
||||
draftChanged: false,
|
||||
draftTimestamp: now,
|
||||
timestamp: now,
|
||||
});
|
||||
} else {
|
||||
this.model.set({
|
||||
draftChanged: false,
|
||||
draftTimestamp: null,
|
||||
});
|
||||
}
|
||||
|
||||
window.Signal.Data.updateConversation(this.model.attributes);
|
||||
|
||||
void this.model.updateLastMessage();
|
||||
}
|
||||
|
||||
this.conversationView?.remove();
|
||||
|
||||
removeLinkPreview();
|
||||
suspendLinkPreviews();
|
||||
|
||||
this.remove();
|
||||
}
|
||||
|
||||
async onOpened(messageId: string): Promise<void> {
|
||||
this.model.onOpenStart();
|
||||
|
||||
if (messageId) {
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (message) {
|
||||
void this.model.loadAndScroll(messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`onOpened: Did not find message ${messageId}`);
|
||||
}
|
||||
|
||||
const { retryPlaceholders } = window.Signal.Services;
|
||||
if (retryPlaceholders) {
|
||||
await retryPlaceholders.findByConversationAndMarkOpened(this.model.id);
|
||||
}
|
||||
|
||||
const loadAndUpdate = async () => {
|
||||
void Promise.all([
|
||||
this.model.loadNewestMessages(undefined, undefined),
|
||||
this.model.updateLastMessage(),
|
||||
this.model.updateUnread(),
|
||||
]);
|
||||
};
|
||||
|
||||
void loadAndUpdate();
|
||||
|
||||
window.reduxActions.composer.setComposerFocus(this.model.id);
|
||||
|
||||
const quotedMessageId = this.model.get('quotedMessageId');
|
||||
if (quotedMessageId) {
|
||||
window.reduxActions.composer.setQuoteByMessageId(
|
||||
this.model.id,
|
||||
quotedMessageId
|
||||
);
|
||||
}
|
||||
|
||||
void this.model.fetchLatestGroupV2Data();
|
||||
strictAssert(
|
||||
this.model.throttledMaybeMigrateV1Group !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
void this.model.throttledMaybeMigrateV1Group();
|
||||
strictAssert(
|
||||
this.model.throttledFetchSMSOnlyUUID !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
void this.model.throttledFetchSMSOnlyUUID();
|
||||
|
||||
const ourUuid = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
|
||||
if (
|
||||
!isGroup(this.model.attributes) ||
|
||||
(ourUuid && this.model.hasMember(ourUuid))
|
||||
) {
|
||||
strictAssert(
|
||||
this.model.throttledGetProfiles !== undefined,
|
||||
'Conversation model should be initialized'
|
||||
);
|
||||
await this.model.throttledGetProfiles();
|
||||
}
|
||||
|
||||
void this.model.updateVerified();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
window.Whisper.ConversationView = ConversationView;
|
10
ts/window.d.ts
vendored
10
ts/window.d.ts
vendored
|
@ -77,7 +77,6 @@ import type { Address } from './types/Address';
|
|||
import type { QualifiedAddress } from './types/QualifiedAddress';
|
||||
import type { CI } from './CI';
|
||||
import type { IPCEventsType } from './util/createIPCEvents';
|
||||
import type { ConversationView } from './views/conversation_view';
|
||||
import type { SignalContextType } from './windows/context';
|
||||
import type * as Message2 from './types/Message2';
|
||||
import type { initializeMigrations } from './signal';
|
||||
|
@ -358,13 +357,4 @@ export type WhisperType = {
|
|||
deliveryReceiptQueue: PQueue;
|
||||
deliveryReceiptBatcher: BatcherType<Receipt>;
|
||||
events: Backbone.Events;
|
||||
|
||||
// Backbone views
|
||||
|
||||
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.
|
||||
|
||||
InboxView: typeof Backbone.View;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,5 @@
|
|||
import '../../models/messages';
|
||||
import '../../models/conversations';
|
||||
|
||||
import '../../views/conversation_view';
|
||||
import '../../SignalProtocolStore';
|
||||
import '../../background';
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -3441,7 +3441,7 @@
|
|||
jest-matcher-utils "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/jquery@*", "@types/jquery@3.5.6":
|
||||
"@types/jquery@*":
|
||||
version "3.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0"
|
||||
integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==
|
||||
|
@ -11248,11 +11248,6 @@ jpeg-js@^0.4.1, jpeg-js@^0.4.2:
|
|||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
jquery@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9"
|
||||
integrity sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a"
|
||||
|
@ -12705,11 +12700,6 @@ multicast-dns@^6.0.1:
|
|||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
mustache@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0"
|
||||
integrity sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=
|
||||
|
||||
mz@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
|
||||
|
|
Loading…
Reference in a new issue