signal-desktop/ts/views/conversation_view.tsx

201 lines
5.7 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable camelcase */
import type * as Backbone from 'backbone';
2021-08-30 21:32:56 +00:00
import { render } from 'mustache';
2021-08-11 16:23:21 +00:00
import type { ConversationModel } from '../models/conversations';
import { getMessageById } from '../messages/getMessageById';
2021-08-30 21:32:56 +00:00
import { strictAssert } from '../util/assert';
import { isGroup } from '../util/whatTypeOfConversation';
import { ReactWrapperView } from './ReactWrapperView';
2021-09-29 20:23:06 +00:00
import * as log from '../logging/log';
2021-10-05 16:47:06 +00:00
import { createConversationView } from '../state/roots/createConversationView';
2022-06-17 00:48:57 +00:00
import {
removeLinkPreview,
suspendLinkPreviews,
} from '../services/LinkPreview';
import { UUIDKind } from '../types/UUID';
2020-09-28 23:46:31 +00:00
2021-08-30 21:32:56 +00:00
export class ConversationView extends window.Backbone.View<ConversationModel> {
// Sub-views
private conversationView?: Backbone.View;
2021-08-30 21:32:56 +00:00
// 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();
2021-10-05 16:47:06 +00:00
this.setupConversationView();
window.reduxActions.composer.replaceAttachments(
this.model.get('id'),
this.model.get('draftAttachments') || []
);
2021-08-30 21:32:56 +00:00
}
// 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;
}
2021-10-05 16:47:06 +00:00
setupConversationView(): void {
// setupCompositionArea
window.reduxActions.composer.resetComposer();
// createConversationView root
const JSX = createConversationView(window.reduxStore, {
conversationId: this.model.id,
});
this.conversationView = new ReactWrapperView({ JSX });
2021-10-05 16:47:06 +00:00
this.$('.ConversationView__template').append(this.conversationView.el);
2021-08-30 21:32:56 +00:00
}
2021-08-30 21:32:56 +00:00
unload(reason: string): void {
log.info(
'unloading conversation',
2021-08-30 21:32:56 +00:00
this.model.idForLogging(),
'due to:',
reason
);
const { conversationUnloaded } = window.reduxActions.conversations;
if (conversationUnloaded) {
2021-08-30 21:32:56 +00:00
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();
}
2021-10-05 16:47:06 +00:00
this.conversationView?.remove();
2022-06-17 00:48:57 +00:00
removeLinkPreview();
suspendLinkPreviews();
this.remove();
2021-08-30 21:32:56 +00:00
}
2021-08-30 21:32:56 +00:00
async onOpened(messageId: string): Promise<void> {
2022-01-20 00:40:29 +00:00
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}`);
}
2021-05-28 19:11:19 +00:00
const { retryPlaceholders } = window.Signal.Services;
if (retryPlaceholders) {
2021-08-30 21:32:56 +00:00
await retryPlaceholders.findByConversationAndMarkOpened(this.model.id);
2021-05-28 19:11:19 +00:00
}
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);
2021-08-30 21:32:56 +00:00
const quotedMessageId = this.model.get('quotedMessageId');
if (quotedMessageId) {
window.reduxActions.composer.setQuoteByMessageId(
this.model.id,
quotedMessageId
);
}
void this.model.fetchLatestGroupV2Data();
2021-08-30 21:32:56 +00:00
strictAssert(
this.model.throttledMaybeMigrateV1Group !== undefined,
2021-05-28 19:11:19 +00:00
'Conversation model should be initialized'
);
void this.model.throttledMaybeMigrateV1Group();
2021-08-30 21:32:56 +00:00
strictAssert(
this.model.throttledFetchSMSOnlyUUID !== undefined,
'Conversation model should be initialized'
);
void this.model.throttledFetchSMSOnlyUUID();
2021-08-30 21:32:56 +00:00
const ourUuid = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
if (
!isGroup(this.model.attributes) ||
2022-07-08 20:46:25 +00:00
(ourUuid && this.model.hasMember(ourUuid))
) {
strictAssert(
this.model.throttledGetProfiles !== undefined,
'Conversation model should be initialized'
);
await this.model.throttledGetProfiles();
}
2021-08-30 21:32:56 +00:00
void this.model.updateVerified();
2021-08-30 21:32:56 +00:00
}
}
window.Whisper.ConversationView = ConversationView;