When you send a message, scroll it into view
This commit is contained in:
parent
b9518ed0c5
commit
a3525c16ef
14 changed files with 387 additions and 438 deletions
|
@ -68,7 +68,6 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
clearQuotedMessage: action('clearQuotedMessage'),
|
clearQuotedMessage: action('clearQuotedMessage'),
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
getQuotedMessage: action('getQuotedMessage'),
|
getQuotedMessage: action('getQuotedMessage'),
|
||||||
scrollToBottom: action('scrollToBottom'),
|
|
||||||
sortedGroupMembers: [],
|
sortedGroupMembers: [],
|
||||||
// EmojiButton
|
// EmojiButton
|
||||||
onPickEmoji: action('onPickEmoji'),
|
onPickEmoji: action('onPickEmoji'),
|
||||||
|
|
|
@ -123,7 +123,6 @@ export type OwnProps = Readonly<{
|
||||||
setQuotedMessage(message: undefined): unknown;
|
setQuotedMessage(message: undefined): unknown;
|
||||||
shouldSendHighQualityAttachments: boolean;
|
shouldSendHighQualityAttachments: boolean;
|
||||||
startRecording: () => unknown;
|
startRecording: () => unknown;
|
||||||
scrollToBottom: (converstionId: string) => unknown;
|
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -203,7 +202,6 @@ export const CompositionArea = ({
|
||||||
clearQuotedMessage,
|
clearQuotedMessage,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
getQuotedMessage,
|
getQuotedMessage,
|
||||||
scrollToBottom,
|
|
||||||
sortedGroupMembers,
|
sortedGroupMembers,
|
||||||
// EmojiButton
|
// EmojiButton
|
||||||
onPickEmoji,
|
onPickEmoji,
|
||||||
|
@ -630,14 +628,13 @@ export const CompositionArea = ({
|
||||||
{!large ? leftHandSideButtonsFragment : null}
|
{!large ? leftHandSideButtonsFragment : null}
|
||||||
<div className="CompositionArea__input">
|
<div className="CompositionArea__input">
|
||||||
<CompositionInput
|
<CompositionInput
|
||||||
i18n={i18n}
|
|
||||||
conversationId={conversationId}
|
|
||||||
clearQuotedMessage={clearQuotedMessage}
|
clearQuotedMessage={clearQuotedMessage}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
draftBodyRanges={draftBodyRanges}
|
draftBodyRanges={draftBodyRanges}
|
||||||
draftText={draftText}
|
draftText={draftText}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
getQuotedMessage={getQuotedMessage}
|
getQuotedMessage={getQuotedMessage}
|
||||||
|
i18n={i18n}
|
||||||
inputApi={inputApiRef}
|
inputApi={inputApiRef}
|
||||||
large={large}
|
large={large}
|
||||||
onDirtyChange={setDirty}
|
onDirtyChange={setDirty}
|
||||||
|
@ -645,7 +642,6 @@ export const CompositionArea = ({
|
||||||
onPickEmoji={onPickEmoji}
|
onPickEmoji={onPickEmoji}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onTextTooLong={onTextTooLong}
|
onTextTooLong={onTextTooLong}
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
sortedGroupMembers={sortedGroupMembers}
|
sortedGroupMembers={sortedGroupMembers}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
@ -21,7 +21,6 @@ const story = storiesOf('Components/CompositionInput', module);
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
i18n,
|
i18n,
|
||||||
conversationId: 'conversation-id',
|
|
||||||
disabled: boolean('disabled', overrideProps.disabled || false),
|
disabled: boolean('disabled', overrideProps.disabled || false),
|
||||||
onSubmit: action('onSubmit'),
|
onSubmit: action('onSubmit'),
|
||||||
onEditorStateChange: action('onEditorStateChange'),
|
onEditorStateChange: action('onEditorStateChange'),
|
||||||
|
@ -33,7 +32,6 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
getQuotedMessage: action('getQuotedMessage'),
|
getQuotedMessage: action('getQuotedMessage'),
|
||||||
onPickEmoji: action('onPickEmoji'),
|
onPickEmoji: action('onPickEmoji'),
|
||||||
large: boolean('large', overrideProps.large || false),
|
large: boolean('large', overrideProps.large || false),
|
||||||
scrollToBottom: action('scrollToBottom'),
|
|
||||||
sortedGroupMembers: overrideProps.sortedGroupMembers || [],
|
sortedGroupMembers: overrideProps.sortedGroupMembers || [],
|
||||||
skinTone: select(
|
skinTone: select(
|
||||||
'skinTone',
|
'skinTone',
|
||||||
|
|
|
@ -62,7 +62,6 @@ export type InputApi = {
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
readonly conversationId: string;
|
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
readonly large?: boolean;
|
readonly large?: boolean;
|
||||||
|
@ -88,7 +87,6 @@ export type Props = {
|
||||||
): unknown;
|
): unknown;
|
||||||
getQuotedMessage(): unknown;
|
getQuotedMessage(): unknown;
|
||||||
clearQuotedMessage(): unknown;
|
clearQuotedMessage(): unknown;
|
||||||
scrollToBottom: (converstionId: string) => unknown;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_LENGTH = 64 * 1024;
|
const MAX_LENGTH = 64 * 1024;
|
||||||
|
@ -97,7 +95,6 @@ const BASE_CLASS_NAME = 'module-composition-input';
|
||||||
export function CompositionInput(props: Props): React.ReactElement {
|
export function CompositionInput(props: Props): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
conversationId,
|
|
||||||
disabled,
|
disabled,
|
||||||
large,
|
large,
|
||||||
inputApi,
|
inputApi,
|
||||||
|
@ -110,7 +107,6 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
getQuotedMessage,
|
getQuotedMessage,
|
||||||
clearQuotedMessage,
|
clearQuotedMessage,
|
||||||
scrollToBottom,
|
|
||||||
sortedGroupMembers,
|
sortedGroupMembers,
|
||||||
theme,
|
theme,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -241,7 +237,6 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
`CompositionInput: Submitting message ${timestamp} with ${mentions.length} mentions`
|
`CompositionInput: Submitting message ${timestamp} with ${mentions.length} mentions`
|
||||||
);
|
);
|
||||||
onSubmit(text, mentions, timestamp);
|
onSubmit(text, mentions, timestamp);
|
||||||
scrollToBottom(conversationId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inputApi) {
|
if (inputApi) {
|
||||||
|
|
|
@ -44,7 +44,6 @@ const candidateConversations = Array.from(Array(100), () =>
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
attachments: overrideProps.attachments,
|
attachments: overrideProps.attachments,
|
||||||
conversationId: 'conversation-id',
|
|
||||||
candidateConversations,
|
candidateConversations,
|
||||||
doForwardMessage: action('doForwardMessage'),
|
doForwardMessage: action('doForwardMessage'),
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
|
|
|
@ -41,7 +41,6 @@ import { useAnimated } from '../hooks/useAnimated';
|
||||||
export type DataPropsType = {
|
export type DataPropsType = {
|
||||||
attachments?: Array<AttachmentDraftType>;
|
attachments?: Array<AttachmentDraftType>;
|
||||||
candidateConversations: ReadonlyArray<ConversationType>;
|
candidateConversations: ReadonlyArray<ConversationType>;
|
||||||
conversationId: string;
|
|
||||||
doForwardMessage: (
|
doForwardMessage: (
|
||||||
selectedContacts: Array<string>,
|
selectedContacts: Array<string>,
|
||||||
messageBody?: string,
|
messageBody?: string,
|
||||||
|
@ -77,7 +76,6 @@ const MAX_FORWARD = 5;
|
||||||
export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
attachments,
|
attachments,
|
||||||
candidateConversations,
|
candidateConversations,
|
||||||
conversationId,
|
|
||||||
doForwardMessage,
|
doForwardMessage,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -188,10 +186,10 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
}, [candidateConversations]);
|
}, [candidateConversations]);
|
||||||
|
|
||||||
const toggleSelectedConversation = useCallback(
|
const toggleSelectedConversation = useCallback(
|
||||||
(selectedConversationId: string) => {
|
(conversationId: string) => {
|
||||||
let removeContact = false;
|
let removeContact = false;
|
||||||
const nextSelectedContacts = selectedContacts.filter(contact => {
|
const nextSelectedContacts = selectedContacts.filter(contact => {
|
||||||
if (contact.id === selectedConversationId) {
|
if (contact.id === conversationId) {
|
||||||
removeContact = true;
|
removeContact = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -201,7 +199,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
setSelectedContacts(nextSelectedContacts);
|
setSelectedContacts(nextSelectedContacts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedContact = contactLookup.get(selectedConversationId);
|
const selectedContact = contactLookup.get(conversationId);
|
||||||
if (selectedContact) {
|
if (selectedContact) {
|
||||||
if (selectedContact.announcementsOnly && !selectedContact.areWeAdmin) {
|
if (selectedContact.announcementsOnly && !selectedContact.areWeAdmin) {
|
||||||
setCannotMessage(true);
|
setCannotMessage(true);
|
||||||
|
@ -337,7 +335,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
) : null}
|
) : null}
|
||||||
<div className="module-ForwardMessageModal__text-edit-area">
|
<div className="module-ForwardMessageModal__text-edit-area">
|
||||||
<CompositionInput
|
<CompositionInput
|
||||||
conversationId={conversationId}
|
|
||||||
clearQuotedMessage={shouldNeverBeCalled}
|
clearQuotedMessage={shouldNeverBeCalled}
|
||||||
draftText={messageBodyText}
|
draftText={messageBodyText}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
@ -346,7 +343,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
inputApi={inputApiRef}
|
inputApi={inputApiRef}
|
||||||
large
|
large
|
||||||
moduleClassName="module-ForwardMessageModal__input"
|
moduleClassName="module-ForwardMessageModal__input"
|
||||||
scrollToBottom={noop}
|
|
||||||
onEditorStateChange={(
|
onEditorStateChange={(
|
||||||
messageText,
|
messageText,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
|
@ -403,7 +399,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClickArchiveButton={shouldNeverBeCalled}
|
onClickArchiveButton={shouldNeverBeCalled}
|
||||||
onClickContactCheckbox={(
|
onClickContactCheckbox={(
|
||||||
selectedConversationId: string,
|
conversationId: string,
|
||||||
disabledReason:
|
disabledReason:
|
||||||
| undefined
|
| undefined
|
||||||
| ContactCheckboxDisabledReason
|
| ContactCheckboxDisabledReason
|
||||||
|
@ -412,9 +408,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
disabledReason !==
|
disabledReason !==
|
||||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||||
) {
|
) {
|
||||||
toggleSelectedConversation(
|
toggleSelectedConversation(conversationId);
|
||||||
selectedConversationId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSelectConversation={shouldNeverBeCalled}
|
onSelectConversation={shouldNeverBeCalled}
|
||||||
|
|
|
@ -471,10 +471,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
overrideProps.isLoadingMessages === false
|
overrideProps.isLoadingMessages === false
|
||||||
),
|
),
|
||||||
items: overrideProps.items || Object.keys(items),
|
items: overrideProps.items || Object.keys(items),
|
||||||
loadCountdownStart: undefined,
|
|
||||||
messageHeightChangeIndex: undefined,
|
|
||||||
resetCounter: 0,
|
resetCounter: 0,
|
||||||
scrollToBottomCounter: 0,
|
|
||||||
scrollToIndex: overrideProps.scrollToIndex,
|
scrollToIndex: overrideProps.scrollToIndex,
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
||||||
|
@ -486,7 +483,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
warning: overrideProps.warning,
|
warning: overrideProps.warning,
|
||||||
|
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
isNearBottom: false,
|
|
||||||
renderItem,
|
renderItem,
|
||||||
renderLastSeenIndicator,
|
renderLastSeenIndicator,
|
||||||
renderHeroRow,
|
renderHeroRow,
|
||||||
|
|
|
@ -77,14 +77,13 @@ export type PropsDataType = {
|
||||||
haveNewest: boolean;
|
haveNewest: boolean;
|
||||||
haveOldest: boolean;
|
haveOldest: boolean;
|
||||||
isLoadingMessages: boolean;
|
isLoadingMessages: boolean;
|
||||||
isNearBottom: boolean;
|
isNearBottom?: boolean;
|
||||||
items: ReadonlyArray<string>;
|
items: ReadonlyArray<string>;
|
||||||
loadCountdownStart: number | undefined;
|
loadCountdownStart?: number;
|
||||||
messageHeightChangeIndex: number | undefined;
|
messageHeightChangeIndex?: number;
|
||||||
oldestUnreadIndex: number | undefined;
|
oldestUnreadIndex?: number;
|
||||||
resetCounter: number;
|
resetCounter: number;
|
||||||
scrollToBottomCounter: number;
|
scrollToIndex?: number;
|
||||||
scrollToIndex: number | undefined;
|
|
||||||
scrollToIndexCounter: number;
|
scrollToIndexCounter: number;
|
||||||
totalUnread: number;
|
totalUnread: number;
|
||||||
};
|
};
|
||||||
|
@ -957,7 +956,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
this.scrollDown(false);
|
this.scrollDown(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
public scrollDown = (setFocus?: boolean, forceScrollDown?: boolean): void => {
|
public scrollDown = (setFocus?: boolean): void => {
|
||||||
const {
|
const {
|
||||||
haveNewest,
|
haveNewest,
|
||||||
id,
|
id,
|
||||||
|
@ -974,7 +973,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
const lastId = items[items.length - 1];
|
const lastId = items[items.length - 1];
|
||||||
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
||||||
|
|
||||||
if (!this.visibleRows || forceScrollDown) {
|
if (!this.visibleRows) {
|
||||||
if (haveNewest) {
|
if (haveNewest) {
|
||||||
this.scrollToBottom(setFocus);
|
this.scrollToBottom(setFocus);
|
||||||
} else if (!isLoadingMessages) {
|
} else if (!isLoadingMessages) {
|
||||||
|
@ -1031,7 +1030,6 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
messageHeightChangeIndex,
|
messageHeightChangeIndex,
|
||||||
oldestUnreadIndex,
|
oldestUnreadIndex,
|
||||||
resetCounter,
|
resetCounter,
|
||||||
scrollToBottomCounter,
|
|
||||||
scrollToIndex,
|
scrollToIndex,
|
||||||
typingContactId,
|
typingContactId,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -1049,10 +1047,6 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
this.resizeHeroRow();
|
this.resizeHeroRow();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToBottomCounter !== prevProps.scrollToBottomCounter) {
|
|
||||||
this.scrollDown(false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// There are a number of situations which can necessitate that we forget about row
|
// There are a number of situations which can necessitate that we forget about row
|
||||||
// heights previously calculated. We reset the minimum number of rows to minimize
|
// heights previously calculated. We reset the minimum number of rows to minimize
|
||||||
// unexpected changes to the scroll position. Those changes happen because
|
// unexpected changes to the scroll position. Those changes happen because
|
||||||
|
|
|
@ -101,6 +101,7 @@ import { getAvatarData } from '../util/getAvatarData';
|
||||||
import { createIdenticon } from '../util/createIdenticon';
|
import { createIdenticon } from '../util/createIdenticon';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
|
import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -116,7 +117,13 @@ const {
|
||||||
upgradeMessageSchema,
|
upgradeMessageSchema,
|
||||||
writeNewAttachmentData,
|
writeNewAttachmentData,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
const { addStickerPackReference } = window.Signal.Data;
|
const {
|
||||||
|
addStickerPackReference,
|
||||||
|
getOlderMessagesByConversation,
|
||||||
|
getMessageMetricsForConversation,
|
||||||
|
getMessageById,
|
||||||
|
getNewerMessagesByConversation,
|
||||||
|
} = window.Signal.Data;
|
||||||
|
|
||||||
const THREE_HOURS = durations.HOUR * 3;
|
const THREE_HOURS = durations.HOUR * 3;
|
||||||
const FIVE_MINUTES = durations.MINUTE * 5;
|
const FIVE_MINUTES = durations.MINUTE * 5;
|
||||||
|
@ -124,6 +131,8 @@ const FIVE_MINUTES = durations.MINUTE * 5;
|
||||||
const JOB_REPORTING_THRESHOLD_MS = 25;
|
const JOB_REPORTING_THRESHOLD_MS = 25;
|
||||||
const SEND_REPORTING_THRESHOLD_MS = 25;
|
const SEND_REPORTING_THRESHOLD_MS = 25;
|
||||||
|
|
||||||
|
const MESSAGE_LOAD_CHUNK_SIZE = 30;
|
||||||
|
|
||||||
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
|
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
|
||||||
'profileLastFetchedAt',
|
'profileLastFetchedAt',
|
||||||
'needsStorageServiceSync',
|
'needsStorageServiceSync',
|
||||||
|
@ -163,7 +172,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
inProgressFetch?: Promise<unknown>;
|
inProgressFetch?: Promise<unknown>;
|
||||||
|
|
||||||
incomingMessageQueue?: typeof window.PQueueType;
|
newMessageQueue?: typeof window.PQueueType;
|
||||||
|
|
||||||
jobQueue?: typeof window.PQueueType;
|
jobQueue?: typeof window.PQueueType;
|
||||||
|
|
||||||
|
@ -1303,34 +1312,340 @@ export class ConversationModel extends window.Backbone
|
||||||
this.debouncedUpdateLastMessage!();
|
this.debouncedUpdateLastMessage!();
|
||||||
}
|
}
|
||||||
|
|
||||||
addSingleMessage(message: MessageModel): void {
|
addIncomingMessage(message: MessageModel): void {
|
||||||
const { messagesAdded } = window.reduxActions.conversations;
|
this.addSingleMessage(message);
|
||||||
const isNewMessage = true;
|
|
||||||
messagesAdded(
|
|
||||||
this.id,
|
|
||||||
[{ ...message.attributes }],
|
|
||||||
isNewMessage,
|
|
||||||
window.isActive()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For incoming messages, they might arrive while we're in the middle of a bulk fetch
|
// New messages might arrive while we're in the middle of a bulk fetch from the
|
||||||
// from the database. We'll wait until that is done to process this newly-arrived
|
// database. We'll wait until that is done before moving forward.
|
||||||
// message.
|
async addSingleMessage(
|
||||||
addIncomingMessage(message: MessageModel): void {
|
message: MessageModel,
|
||||||
if (!this.incomingMessageQueue) {
|
{ isJustSent }: { isJustSent: boolean } = { isJustSent: false }
|
||||||
this.incomingMessageQueue = new window.PQueue({
|
): Promise<void> {
|
||||||
|
if (!this.newMessageQueue) {
|
||||||
|
this.newMessageQueue = new window.PQueue({
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
timeout: 1000 * 60 * 2,
|
timeout: 1000 * 60 * 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use a queue here to ensure messages are added to the UI in the order received
|
// We use a queue here to ensure messages are added to the UI in the order received
|
||||||
this.incomingMessageQueue.add(async () => {
|
await this.newMessageQueue.add(async () => {
|
||||||
await this.inProgressFetch;
|
await this.inProgressFetch;
|
||||||
|
|
||||||
this.addSingleMessage(message);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { messagesAdded } = window.reduxActions.conversations;
|
||||||
|
const { conversations } = window.reduxStore.getState();
|
||||||
|
const { messagesByConversation } = conversations;
|
||||||
|
|
||||||
|
const conversationId = this.id;
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
const newestId = existingConversation?.metrics?.newest?.id;
|
||||||
|
const messageIds = existingConversation?.messageIds;
|
||||||
|
|
||||||
|
const isLatestInMemory =
|
||||||
|
newestId && messageIds && messageIds[messageIds.length - 1] === newestId;
|
||||||
|
|
||||||
|
if (!isJustSent || isLatestInMemory) {
|
||||||
|
messagesAdded({
|
||||||
|
conversationId,
|
||||||
|
messages: [{ ...message.attributes }],
|
||||||
|
isActive: window.isActive(),
|
||||||
|
isJustSent,
|
||||||
|
isNewMessage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadNewestMessages(undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInProgressFetch(): () => unknown {
|
||||||
|
let resolvePromise: (value?: unknown) => void;
|
||||||
|
this.inProgressFetch = new Promise(resolve => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
resolvePromise();
|
||||||
|
this.inProgressFetch = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNewestMessages(
|
||||||
|
newestMessageId: string | undefined,
|
||||||
|
setFocus: boolean | undefined
|
||||||
|
): Promise<void> {
|
||||||
|
const { messagesReset, setMessagesLoading } =
|
||||||
|
window.reduxActions.conversations;
|
||||||
|
const conversationId = this.id;
|
||||||
|
|
||||||
|
setMessagesLoading(conversationId, true);
|
||||||
|
const finish = this.setInProgressFetch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let scrollToLatestUnread = true;
|
||||||
|
|
||||||
|
if (newestMessageId) {
|
||||||
|
const newestInMemoryMessage = await getMessageById(newestMessageId, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
if (newestInMemoryMessage) {
|
||||||
|
// If newest in-memory message is unread, scrolling down would mean going to
|
||||||
|
// the very bottom, not the oldest unread.
|
||||||
|
if (isMessageUnread(newestInMemoryMessage.attributes)) {
|
||||||
|
scrollToLatestUnread = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`loadNewestMessages: did not find message ${newestMessageId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = await getMessageMetricsForConversation(conversationId);
|
||||||
|
|
||||||
|
// If this is a message request that has not yet been accepted, we always show the
|
||||||
|
// oldest messages, to ensure that the ConversationHero is shown. We don't want to
|
||||||
|
// scroll directly to the oldest message, because that could scroll the hero off
|
||||||
|
// the screen.
|
||||||
|
if (!newestMessageId && !this.getAccepted() && metrics.oldest) {
|
||||||
|
this.loadAndScroll(metrics.oldest.id, { disableScroll: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollToLatestUnread && metrics.oldestUnread) {
|
||||||
|
this.loadAndScroll(metrics.oldestUnread.id, {
|
||||||
|
disableScroll: !setFocus,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await getOlderMessagesByConversation(conversationId, {
|
||||||
|
limit: MESSAGE_LOAD_CHUNK_SIZE,
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleaned: Array<MessageModel> = await this.cleanModels(messages);
|
||||||
|
const scrollToMessageId =
|
||||||
|
setFocus && metrics.newest ? metrics.newest.id : undefined;
|
||||||
|
|
||||||
|
// Because our `getOlderMessages` fetch above didn't specify a receivedAt, we got
|
||||||
|
// the most recent N messages in the conversation. If it has a conflict with
|
||||||
|
// metrics, fetched a bit before, that's likely a race condition. So we tell our
|
||||||
|
// reducer to trust the message set we just fetched for determining if we have
|
||||||
|
// the newest message loaded.
|
||||||
|
const unboundedFetch = true;
|
||||||
|
messagesReset(
|
||||||
|
conversationId,
|
||||||
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
|
...messageModel.attributes,
|
||||||
|
})),
|
||||||
|
metrics,
|
||||||
|
scrollToMessageId,
|
||||||
|
unboundedFetch
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setMessagesLoading(conversationId, false);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async loadOlderMessages(oldestMessageId: string): Promise<void> {
|
||||||
|
const { messagesAdded, setMessagesLoading, repairOldestMessage } =
|
||||||
|
window.reduxActions.conversations;
|
||||||
|
const conversationId = this.id;
|
||||||
|
|
||||||
|
setMessagesLoading(conversationId, true);
|
||||||
|
const finish = this.setInProgressFetch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await getMessageById(oldestMessageId, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(
|
||||||
|
`loadOlderMessages: failed to load message ${oldestMessageId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedAt = message.get('received_at');
|
||||||
|
const sentAt = message.get('sent_at');
|
||||||
|
const models = await getOlderMessagesByConversation(conversationId, {
|
||||||
|
receivedAt,
|
||||||
|
sentAt,
|
||||||
|
messageId: oldestMessageId,
|
||||||
|
limit: MESSAGE_LOAD_CHUNK_SIZE,
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (models.length < 1) {
|
||||||
|
log.warn('loadOlderMessages: requested, but loaded no messages');
|
||||||
|
repairOldestMessage(conversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = await this.cleanModels(models);
|
||||||
|
messagesAdded({
|
||||||
|
conversationId,
|
||||||
|
messages: cleaned.map((messageModel: MessageModel) => ({
|
||||||
|
...messageModel.attributes,
|
||||||
|
})),
|
||||||
|
isActive: window.isActive(),
|
||||||
|
isJustSent: false,
|
||||||
|
isNewMessage: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setMessagesLoading(conversationId, true);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNewerMessages(newestMessageId: string): Promise<void> {
|
||||||
|
const { messagesAdded, setMessagesLoading, repairNewestMessage } =
|
||||||
|
window.reduxActions.conversations;
|
||||||
|
const conversationId = this.id;
|
||||||
|
|
||||||
|
setMessagesLoading(conversationId, true);
|
||||||
|
const finish = this.setInProgressFetch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await getMessageById(newestMessageId, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(
|
||||||
|
`loadNewerMessages: failed to load message ${newestMessageId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedAt = message.get('received_at');
|
||||||
|
const sentAt = message.get('sent_at');
|
||||||
|
const models = await getNewerMessagesByConversation(conversationId, {
|
||||||
|
receivedAt,
|
||||||
|
sentAt,
|
||||||
|
limit: MESSAGE_LOAD_CHUNK_SIZE,
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (models.length < 1) {
|
||||||
|
log.warn('loadNewerMessages: requested, but loaded no messages');
|
||||||
|
repairNewestMessage(conversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = await this.cleanModels(models);
|
||||||
|
messagesAdded({
|
||||||
|
conversationId,
|
||||||
|
messages: cleaned.map((messageModel: MessageModel) => ({
|
||||||
|
...messageModel.attributes,
|
||||||
|
})),
|
||||||
|
isActive: window.isActive(),
|
||||||
|
isJustSent: false,
|
||||||
|
isNewMessage: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setMessagesLoading(conversationId, false);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAndScroll(
|
||||||
|
messageId: string,
|
||||||
|
options?: { disableScroll?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
const { messagesReset, setMessagesLoading } =
|
||||||
|
window.reduxActions.conversations;
|
||||||
|
const conversationId = this.id;
|
||||||
|
|
||||||
|
setMessagesLoading(conversationId, true);
|
||||||
|
const finish = this.setInProgressFetch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await getMessageById(messageId, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(
|
||||||
|
`loadMoreAndScroll: failed to load message ${messageId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedAt = message.get('received_at');
|
||||||
|
const sentAt = message.get('sent_at');
|
||||||
|
const older = await getOlderMessagesByConversation(conversationId, {
|
||||||
|
limit: MESSAGE_LOAD_CHUNK_SIZE,
|
||||||
|
receivedAt,
|
||||||
|
sentAt,
|
||||||
|
messageId,
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
const newer = await getNewerMessagesByConversation(conversationId, {
|
||||||
|
limit: MESSAGE_LOAD_CHUNK_SIZE,
|
||||||
|
receivedAt,
|
||||||
|
sentAt,
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
const metrics = await getMessageMetricsForConversation(conversationId);
|
||||||
|
|
||||||
|
const all = [...older.models, message, ...newer.models];
|
||||||
|
|
||||||
|
const cleaned: Array<MessageModel> = await this.cleanModels(all);
|
||||||
|
const scrollToMessageId =
|
||||||
|
options && options.disableScroll ? undefined : messageId;
|
||||||
|
|
||||||
|
messagesReset(
|
||||||
|
conversationId,
|
||||||
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
|
...messageModel.attributes,
|
||||||
|
})),
|
||||||
|
metrics,
|
||||||
|
scrollToMessageId
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setMessagesLoading(conversationId, false);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanModels(
|
||||||
|
collection: MessageModelCollectionType | Array<MessageModel>
|
||||||
|
): Promise<Array<MessageModel>> {
|
||||||
|
const result = collection
|
||||||
|
.filter((message: MessageModel) => Boolean(message.id))
|
||||||
|
.map((message: MessageModel) =>
|
||||||
|
window.MessageController.register(message.id, message)
|
||||||
|
);
|
||||||
|
|
||||||
|
const eliminated = collection.length - result.length;
|
||||||
|
if (eliminated > 0) {
|
||||||
|
log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let max = result.length, i = 0; i < max; i += 1) {
|
||||||
|
const message = result[i];
|
||||||
|
const { attributes } = message;
|
||||||
|
const { schemaVersion } = attributes;
|
||||||
|
|
||||||
|
if (schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY) {
|
||||||
|
// Yep, we really do want to wait for each of these
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const upgradedMessage = await upgradeMessageSchema(attributes);
|
||||||
|
message.set(upgradedMessage);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await window.Signal.Data.saveMessage(upgradedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
format(): ConversationType {
|
format(): ConversationType {
|
||||||
|
@ -3659,7 +3974,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const enableProfileSharing = Boolean(
|
const enableProfileSharing = Boolean(
|
||||||
mandatoryProfileSharingEnabled && !this.get('profileSharing')
|
mandatoryProfileSharingEnabled && !this.get('profileSharing')
|
||||||
);
|
);
|
||||||
this.addSingleMessage(model);
|
this.addSingleMessage(model, { isJustSent: true });
|
||||||
|
|
||||||
const draftProperties = dontClearDraft
|
const draftProperties = dontClearDraft
|
||||||
? {}
|
? {}
|
||||||
|
|
|
@ -248,7 +248,6 @@ export type ConversationMessageType = {
|
||||||
messageIds: Array<string>;
|
messageIds: Array<string>;
|
||||||
metrics: MessageMetricsType;
|
metrics: MessageMetricsType;
|
||||||
resetCounter: number;
|
resetCounter: number;
|
||||||
scrollToBottomCounter: number;
|
|
||||||
scrollToMessageId?: string;
|
scrollToMessageId?: string;
|
||||||
scrollToMessageCounter: number;
|
scrollToMessageCounter: number;
|
||||||
};
|
};
|
||||||
|
@ -552,9 +551,10 @@ export type MessagesAddedActionType = {
|
||||||
type: 'MESSAGES_ADDED';
|
type: 'MESSAGES_ADDED';
|
||||||
payload: {
|
payload: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messages: Array<MessageAttributesType>;
|
|
||||||
isNewMessage: boolean;
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isJustSent: boolean;
|
||||||
|
isNewMessage: boolean;
|
||||||
|
messages: Array<MessageAttributesType>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -611,12 +611,6 @@ export type SetSelectedConversationPanelDepthActionType = {
|
||||||
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
|
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
|
||||||
payload: { panelDepth: number };
|
payload: { panelDepth: number };
|
||||||
};
|
};
|
||||||
export type ScrollToBpttomActionType = {
|
|
||||||
type: 'SCROLL_TO_BOTTOM';
|
|
||||||
payload: {
|
|
||||||
conversationId: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type ScrollToMessageActionType = {
|
export type ScrollToMessageActionType = {
|
||||||
type: 'SCROLL_TO_MESSAGE';
|
type: 'SCROLL_TO_MESSAGE';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -773,7 +767,6 @@ export type ConversationActionType =
|
||||||
| ReplaceAvatarsActionType
|
| ReplaceAvatarsActionType
|
||||||
| ReviewGroupMemberNameCollisionActionType
|
| ReviewGroupMemberNameCollisionActionType
|
||||||
| ReviewMessageRequestNameCollisionActionType
|
| ReviewMessageRequestNameCollisionActionType
|
||||||
| ScrollToBpttomActionType
|
|
||||||
| ScrollToMessageActionType
|
| ScrollToMessageActionType
|
||||||
| SelectedConversationChangedActionType
|
| SelectedConversationChangedActionType
|
||||||
| SetComposeGroupAvatarActionType
|
| SetComposeGroupAvatarActionType
|
||||||
|
@ -845,7 +838,6 @@ export const actions = {
|
||||||
reviewMessageRequestNameCollision,
|
reviewMessageRequestNameCollision,
|
||||||
saveAvatarToDisk,
|
saveAvatarToDisk,
|
||||||
saveUsername,
|
saveUsername,
|
||||||
scrollToBottom,
|
|
||||||
scrollToMessage,
|
scrollToMessage,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
setComposeGroupAvatar,
|
setComposeGroupAvatar,
|
||||||
|
@ -1556,19 +1548,27 @@ function messageSizeChanged(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function messagesAdded(
|
function messagesAdded({
|
||||||
conversationId: string,
|
conversationId,
|
||||||
messages: Array<MessageAttributesType>,
|
isActive,
|
||||||
isNewMessage: boolean,
|
isJustSent,
|
||||||
isActive: boolean
|
isNewMessage,
|
||||||
): MessagesAddedActionType {
|
messages,
|
||||||
|
}: {
|
||||||
|
conversationId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isJustSent: boolean;
|
||||||
|
isNewMessage: boolean;
|
||||||
|
messages: Array<MessageAttributesType>;
|
||||||
|
}): MessagesAddedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'MESSAGES_ADDED',
|
type: 'MESSAGES_ADDED',
|
||||||
payload: {
|
payload: {
|
||||||
conversationId,
|
conversationId,
|
||||||
messages,
|
|
||||||
isNewMessage,
|
|
||||||
isActive,
|
isActive,
|
||||||
|
isJustSent,
|
||||||
|
isNewMessage,
|
||||||
|
messages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1734,15 +1734,6 @@ function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
||||||
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
|
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
|
||||||
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
|
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
|
||||||
}
|
}
|
||||||
function scrollToBottom(conversationId: string): ScrollToBpttomActionType {
|
|
||||||
return {
|
|
||||||
type: 'SCROLL_TO_BOTTOM',
|
|
||||||
payload: {
|
|
||||||
conversationId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToMessage(
|
function scrollToMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messageId: string
|
messageId: string
|
||||||
|
@ -2657,9 +2648,6 @@ export function reducer(
|
||||||
scrollToMessageCounter: existingConversation
|
scrollToMessageCounter: existingConversation
|
||||||
? existingConversation.scrollToMessageCounter + 1
|
? existingConversation.scrollToMessageCounter + 1
|
||||||
: 0,
|
: 0,
|
||||||
scrollToBottomCounter: existingConversation
|
|
||||||
? existingConversation.scrollToBottomCounter
|
|
||||||
: 0,
|
|
||||||
messageIds,
|
messageIds,
|
||||||
metrics: {
|
metrics: {
|
||||||
...metrics,
|
...metrics,
|
||||||
|
@ -2739,28 +2727,6 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'SCROLL_TO_BOTTOM') {
|
|
||||||
const { payload } = action;
|
|
||||||
const { conversationId } = payload;
|
|
||||||
const { messagesByConversation } = state;
|
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
|
||||||
|
|
||||||
if (!existingConversation) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
messagesByConversation: {
|
|
||||||
...messagesByConversation,
|
|
||||||
[conversationId]: {
|
|
||||||
...existingConversation,
|
|
||||||
scrollToBottomCounter: existingConversation.scrollToBottomCounter + 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === 'SCROLL_TO_MESSAGE') {
|
if (action.type === 'SCROLL_TO_MESSAGE') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { conversationId, messageId } = payload;
|
const { conversationId, messageId } = payload;
|
||||||
|
@ -2947,7 +2913,8 @@ export function reducer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'MESSAGES_ADDED') {
|
if (action.type === 'MESSAGES_ADDED') {
|
||||||
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
const { conversationId, isActive, isJustSent, isNewMessage, messages } =
|
||||||
|
action.payload;
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
@ -2994,6 +2961,12 @@ export function reducer(
|
||||||
// won't add new messages to our message list.
|
// won't add new messages to our message list.
|
||||||
const haveLatest = newest && newest.id === lastMessageId;
|
const haveLatest = newest && newest.id === lastMessageId;
|
||||||
if (!haveLatest) {
|
if (!haveLatest) {
|
||||||
|
if (isJustSent) {
|
||||||
|
log.warn(
|
||||||
|
'reducer/MESSAGES_ADDED: isJustSent is true, but haveLatest is false'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3055,7 +3028,7 @@ export function reducer(
|
||||||
isLoadingMessages: false,
|
isLoadingMessages: false,
|
||||||
messageIds,
|
messageIds,
|
||||||
heightChangeMessageIds,
|
heightChangeMessageIds,
|
||||||
scrollToMessageId: undefined,
|
scrollToMessageId: isJustSent ? last.id : undefined,
|
||||||
metrics: {
|
metrics: {
|
||||||
...existingConversation.metrics,
|
...existingConversation.metrics,
|
||||||
newest,
|
newest,
|
||||||
|
|
|
@ -849,7 +849,6 @@ export function _conversationMessagesSelector(
|
||||||
messageIds,
|
messageIds,
|
||||||
metrics,
|
metrics,
|
||||||
resetCounter,
|
resetCounter,
|
||||||
scrollToBottomCounter,
|
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
scrollToMessageCounter,
|
scrollToMessageCounter,
|
||||||
} = conversation;
|
} = conversation;
|
||||||
|
@ -888,7 +887,7 @@ export function _conversationMessagesSelector(
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
loadCountdownStart,
|
loadCountdownStart,
|
||||||
items,
|
items,
|
||||||
isNearBottom: isNearBottom || false,
|
isNearBottom,
|
||||||
messageHeightChangeIndex:
|
messageHeightChangeIndex:
|
||||||
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
||||||
? messageHeightChangeIndex
|
? messageHeightChangeIndex
|
||||||
|
@ -898,7 +897,6 @@ export function _conversationMessagesSelector(
|
||||||
? oldestUnreadIndex
|
? oldestUnreadIndex
|
||||||
: undefined,
|
: undefined,
|
||||||
resetCounter,
|
resetCounter,
|
||||||
scrollToBottomCounter,
|
|
||||||
scrollToIndex:
|
scrollToIndex:
|
||||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||||
scrollToIndexCounter: scrollToMessageCounter,
|
scrollToIndexCounter: scrollToMessageCounter,
|
||||||
|
@ -934,16 +932,10 @@ export const getConversationMessagesSelector = createSelector(
|
||||||
haveNewest: false,
|
haveNewest: false,
|
||||||
haveOldest: false,
|
haveOldest: false,
|
||||||
isLoadingMessages: false,
|
isLoadingMessages: false,
|
||||||
isNearBottom: false,
|
|
||||||
items: [],
|
|
||||||
loadCountdownStart: undefined,
|
|
||||||
messageHeightChangeIndex: undefined,
|
|
||||||
oldestUnreadIndex: undefined,
|
|
||||||
resetCounter: 0,
|
resetCounter: 0,
|
||||||
scrollToBottomCounter: 0,
|
|
||||||
scrollToIndex: undefined,
|
|
||||||
scrollToIndexCounter: 0,
|
scrollToIndexCounter: 0,
|
||||||
totalUnread: 0,
|
totalUnread: 0,
|
||||||
|
items: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ import type { AttachmentDraftType } from '../../types/Attachment';
|
||||||
|
|
||||||
export type SmartForwardMessageModalProps = {
|
export type SmartForwardMessageModalProps = {
|
||||||
attachments?: Array<AttachmentDraftType>;
|
attachments?: Array<AttachmentDraftType>;
|
||||||
conversationId: string;
|
|
||||||
doForwardMessage: (
|
doForwardMessage: (
|
||||||
selectedContacts: Array<string>,
|
selectedContacts: Array<string>,
|
||||||
messageBody?: string,
|
messageBody?: string,
|
||||||
|
@ -42,7 +41,6 @@ const mapStateToProps = (
|
||||||
): DataPropsType => {
|
): DataPropsType => {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
conversationId,
|
|
||||||
doForwardMessage,
|
doForwardMessage,
|
||||||
isSticker,
|
isSticker,
|
||||||
messageBody,
|
messageBody,
|
||||||
|
@ -59,7 +57,6 @@ const mapStateToProps = (
|
||||||
return {
|
return {
|
||||||
attachments,
|
attachments,
|
||||||
candidateConversations,
|
candidateConversations,
|
||||||
conversationId,
|
|
||||||
doForwardMessage,
|
doForwardMessage,
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
|
|
|
@ -337,7 +337,6 @@ describe('both/state/ducks/conversations', () => {
|
||||||
totalUnread: 0,
|
totalUnread: 0,
|
||||||
},
|
},
|
||||||
resetCounter: 0,
|
resetCounter: 0,
|
||||||
scrollToBottomCounter: 0,
|
|
||||||
scrollToMessageCounter: 0,
|
scrollToMessageCounter: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -840,7 +839,6 @@ describe('both/state/ducks/conversations', () => {
|
||||||
messageIds: [messageId],
|
messageIds: [messageId],
|
||||||
metrics: { totalUnread: 0 },
|
metrics: { totalUnread: 0 },
|
||||||
resetCounter: 0,
|
resetCounter: 0,
|
||||||
scrollToBottomCounter: 0,
|
|
||||||
scrollToMessageCounter: 0,
|
scrollToMessageCounter: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
import type {
|
import type {
|
||||||
GroupV2PendingMemberType,
|
GroupV2PendingMemberType,
|
||||||
MessageModelCollectionType,
|
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
ConversationModelCollectionType,
|
ConversationModelCollectionType,
|
||||||
QuotedMessageType,
|
QuotedMessageType,
|
||||||
|
@ -49,7 +48,6 @@ import {
|
||||||
isOutgoing,
|
isOutgoing,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
} from '../state/selectors/message';
|
} from '../state/selectors/message';
|
||||||
import { isMessageUnread } from '../util/isMessageUnread';
|
|
||||||
import {
|
import {
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getMessagesByConversation,
|
getMessagesByConversation,
|
||||||
|
@ -138,13 +136,7 @@ const {
|
||||||
upgradeMessageSchema,
|
upgradeMessageSchema,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
const {
|
const { getMessageById, getMessagesBySentAt } = window.Signal.Data;
|
||||||
getOlderMessagesByConversation,
|
|
||||||
getMessageMetricsForConversation,
|
|
||||||
getMessageById,
|
|
||||||
getMessagesBySentAt,
|
|
||||||
getNewerMessagesByConversation,
|
|
||||||
} = window.Signal.Data;
|
|
||||||
|
|
||||||
type MessageActionsType = {
|
type MessageActionsType = {
|
||||||
deleteMessage: (messageId: string) => unknown;
|
deleteMessage: (messageId: string) => unknown;
|
||||||
|
@ -474,107 +466,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.scrollToMessage(message.id);
|
this.scrollToMessage(message.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadOlderMessages = async (oldestMessageId: string) => {
|
|
||||||
const { messagesAdded, setMessagesLoading, repairOldestMessage } =
|
|
||||||
window.reduxActions.conversations;
|
|
||||||
const conversationId = this.model.id;
|
|
||||||
|
|
||||||
setMessagesLoading(conversationId, true);
|
|
||||||
const finish = this.setInProgressFetch();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await getMessageById(oldestMessageId, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
if (!message) {
|
|
||||||
throw new Error(
|
|
||||||
`loadOlderMessages: failed to load message ${oldestMessageId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedAt = message.get('received_at');
|
|
||||||
const sentAt = message.get('sent_at');
|
|
||||||
const models = await getOlderMessagesByConversation(conversationId, {
|
|
||||||
receivedAt,
|
|
||||||
sentAt,
|
|
||||||
messageId: oldestMessageId,
|
|
||||||
limit: 30,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (models.length < 1) {
|
|
||||||
log.warn('loadOlderMessages: requested, but loaded no messages');
|
|
||||||
repairOldestMessage(conversationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleaned = await this.cleanModels(models);
|
|
||||||
const isNewMessage = false;
|
|
||||||
messagesAdded(
|
|
||||||
this.model.id,
|
|
||||||
cleaned.map((messageModel: MessageModel) => ({
|
|
||||||
...messageModel.attributes,
|
|
||||||
})),
|
|
||||||
isNewMessage,
|
|
||||||
window.isActive()
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setMessagesLoading(conversationId, true);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const loadNewerMessages = async (newestMessageId: string) => {
|
|
||||||
const { messagesAdded, setMessagesLoading, repairNewestMessage } =
|
|
||||||
window.reduxActions.conversations;
|
|
||||||
const conversationId = this.model.id;
|
|
||||||
|
|
||||||
setMessagesLoading(conversationId, true);
|
|
||||||
const finish = this.setInProgressFetch();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await getMessageById(newestMessageId, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
if (!message) {
|
|
||||||
throw new Error(
|
|
||||||
`loadNewerMessages: failed to load message ${newestMessageId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedAt = message.get('received_at');
|
|
||||||
const sentAt = message.get('sent_at');
|
|
||||||
const models = await getNewerMessagesByConversation(conversationId, {
|
|
||||||
receivedAt,
|
|
||||||
sentAt,
|
|
||||||
limit: 30,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (models.length < 1) {
|
|
||||||
log.warn('loadNewerMessages: requested, but loaded no messages');
|
|
||||||
repairNewestMessage(conversationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleaned = await this.cleanModels(models);
|
|
||||||
const isNewMessage = false;
|
|
||||||
messagesAdded(
|
|
||||||
conversationId,
|
|
||||||
cleaned.map((messageModel: MessageModel) => ({
|
|
||||||
...messageModel.attributes,
|
|
||||||
})),
|
|
||||||
isNewMessage,
|
|
||||||
window.isActive()
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setMessagesLoading(conversationId, false);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const markMessageRead = async (messageId: string) => {
|
const markMessageRead = async (messageId: string) => {
|
||||||
if (!window.isActive()) {
|
if (!window.isActive()) {
|
||||||
return;
|
return;
|
||||||
|
@ -615,10 +506,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
},
|
},
|
||||||
contactSupport,
|
contactSupport,
|
||||||
learnMoreAboutDeliveryIssue,
|
learnMoreAboutDeliveryIssue,
|
||||||
loadNewerMessages,
|
loadNewerMessages: this.model.loadNewerMessages.bind(this.model),
|
||||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
loadNewestMessages: this.model.loadNewestMessages.bind(this.model),
|
||||||
loadAndScroll: this.loadAndScroll.bind(this),
|
loadAndScroll: this.model.loadAndScroll.bind(this.model),
|
||||||
loadOlderMessages,
|
loadOlderMessages: this.model.loadOlderMessages.bind(this.model),
|
||||||
markMessageRead,
|
markMessageRead,
|
||||||
onBlock: createMessageRequestResponseHandler(
|
onBlock: createMessageRequestResponseHandler(
|
||||||
'onBlock',
|
'onBlock',
|
||||||
|
@ -991,38 +882,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanModels(
|
|
||||||
collection: MessageModelCollectionType | Array<MessageModel>
|
|
||||||
): Promise<Array<MessageModel>> {
|
|
||||||
const result = collection
|
|
||||||
.filter((message: MessageModel) => Boolean(message.id))
|
|
||||||
.map((message: MessageModel) =>
|
|
||||||
window.MessageController.register(message.id, message)
|
|
||||||
);
|
|
||||||
|
|
||||||
const eliminated = collection.length - result.length;
|
|
||||||
if (eliminated > 0) {
|
|
||||||
log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let max = result.length, i = 0; i < max; i += 1) {
|
|
||||||
const message = result[i];
|
|
||||||
const { attributes } = message;
|
|
||||||
const { schemaVersion } = attributes;
|
|
||||||
|
|
||||||
if (schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY) {
|
|
||||||
// Yep, we really do want to wait for each of these
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const upgradedMessage = await upgradeMessageSchema(attributes);
|
|
||||||
message.set(upgradedMessage);
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await window.Signal.Data.saveMessage(upgradedMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async scrollToMessage(messageId: string): Promise<void> {
|
async scrollToMessage(messageId: string): Promise<void> {
|
||||||
const message = await getMessageById(messageId, {
|
const message = await getMessageById(messageId, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
|
@ -1053,162 +912,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadAndScroll(messageId);
|
this.model.loadAndScroll(messageId);
|
||||||
}
|
|
||||||
|
|
||||||
setInProgressFetch(): () => unknown {
|
|
||||||
let resolvePromise: (value?: unknown) => void;
|
|
||||||
this.model.inProgressFetch = new Promise(resolve => {
|
|
||||||
resolvePromise = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
resolvePromise();
|
|
||||||
this.model.inProgressFetch = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
return finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadAndScroll(
|
|
||||||
messageId: string,
|
|
||||||
options?: { disableScroll?: boolean }
|
|
||||||
): Promise<void> {
|
|
||||||
const { messagesReset, setMessagesLoading } =
|
|
||||||
window.reduxActions.conversations;
|
|
||||||
const conversationId = this.model.id;
|
|
||||||
|
|
||||||
setMessagesLoading(conversationId, true);
|
|
||||||
const finish = this.setInProgressFetch();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await getMessageById(messageId, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
if (!message) {
|
|
||||||
throw new Error(
|
|
||||||
`loadMoreAndScroll: failed to load message ${messageId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const receivedAt = message.get('received_at');
|
|
||||||
const sentAt = message.get('sent_at');
|
|
||||||
const older = await getOlderMessagesByConversation(conversationId, {
|
|
||||||
limit: 30,
|
|
||||||
receivedAt,
|
|
||||||
sentAt,
|
|
||||||
messageId,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
const newer = await getNewerMessagesByConversation(conversationId, {
|
|
||||||
limit: 30,
|
|
||||||
receivedAt,
|
|
||||||
sentAt,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
const metrics = await getMessageMetricsForConversation(conversationId);
|
|
||||||
|
|
||||||
const all = [...older.models, message, ...newer.models];
|
|
||||||
|
|
||||||
const cleaned: Array<MessageModel> = await this.cleanModels(all);
|
|
||||||
const scrollToMessageId =
|
|
||||||
options && options.disableScroll ? undefined : messageId;
|
|
||||||
|
|
||||||
messagesReset(
|
|
||||||
conversationId,
|
|
||||||
cleaned.map((messageModel: MessageModel) => ({
|
|
||||||
...messageModel.attributes,
|
|
||||||
})),
|
|
||||||
metrics,
|
|
||||||
scrollToMessageId
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setMessagesLoading(conversationId, false);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNewestMessages(
|
|
||||||
newestMessageId: string | undefined,
|
|
||||||
setFocus: boolean | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
const { messagesReset, setMessagesLoading } =
|
|
||||||
window.reduxActions.conversations;
|
|
||||||
const conversationId = this.model.id;
|
|
||||||
|
|
||||||
setMessagesLoading(conversationId, true);
|
|
||||||
const finish = this.setInProgressFetch();
|
|
||||||
|
|
||||||
try {
|
|
||||||
let scrollToLatestUnread = true;
|
|
||||||
|
|
||||||
if (newestMessageId) {
|
|
||||||
const newestInMemoryMessage = await getMessageById(newestMessageId, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
if (newestInMemoryMessage) {
|
|
||||||
// If newest in-memory message is unread, scrolling down would mean going to
|
|
||||||
// the very bottom, not the oldest unread.
|
|
||||||
if (isMessageUnread(newestInMemoryMessage.attributes)) {
|
|
||||||
scrollToLatestUnread = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn(
|
|
||||||
`loadNewestMessages: did not find message ${newestMessageId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const metrics = await getMessageMetricsForConversation(conversationId);
|
|
||||||
|
|
||||||
// If this is a message request that has not yet been accepted, we always show the
|
|
||||||
// oldest messages, to ensure that the ConversationHero is shown. We don't want to
|
|
||||||
// scroll directly to the oldest message, because that could scroll the hero off
|
|
||||||
// the screen.
|
|
||||||
if (!newestMessageId && !this.model.getAccepted() && metrics.oldest) {
|
|
||||||
this.loadAndScroll(metrics.oldest.id, { disableScroll: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollToLatestUnread && metrics.oldestUnread) {
|
|
||||||
this.loadAndScroll(metrics.oldestUnread.id, {
|
|
||||||
disableScroll: !setFocus,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = await getOlderMessagesByConversation(conversationId, {
|
|
||||||
limit: 30,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cleaned: Array<MessageModel> = await this.cleanModels(messages);
|
|
||||||
const scrollToMessageId =
|
|
||||||
setFocus && metrics.newest ? metrics.newest.id : undefined;
|
|
||||||
|
|
||||||
// Because our `getOlderMessages` fetch above didn't specify a receivedAt, we got
|
|
||||||
// the most recent 30 messages in the conversation. If it has a conflict with
|
|
||||||
// metrics, fetched a bit before, that's likely a race condition. So we tell our
|
|
||||||
// reducer to trust the message set we just fetched for determining if we have
|
|
||||||
// the newest message loaded.
|
|
||||||
const unboundedFetch = true;
|
|
||||||
messagesReset(
|
|
||||||
conversationId,
|
|
||||||
cleaned.map((messageModel: MessageModel) => ({
|
|
||||||
...messageModel.attributes,
|
|
||||||
})),
|
|
||||||
metrics,
|
|
||||||
scrollToMessageId,
|
|
||||||
unboundedFetch
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setMessagesLoading(conversationId, false);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async startMigrationToGV2(): Promise<void> {
|
async startMigrationToGV2(): Promise<void> {
|
||||||
|
@ -1502,7 +1206,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
this.loadAndScroll(messageId);
|
this.model.loadAndScroll(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1514,7 +1218,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
await retryPlaceholders.findByConversationAndMarkOpened(this.model.id);
|
await retryPlaceholders.findByConversationAndMarkOpened(this.model.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadNewestMessages(undefined, undefined);
|
this.model.loadNewestMessages(undefined, undefined);
|
||||||
this.model.updateLastMessage();
|
this.model.updateLastMessage();
|
||||||
|
|
||||||
this.focusMessageField();
|
this.focusMessageField();
|
||||||
|
@ -1582,7 +1286,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
window.reduxStore,
|
window.reduxStore,
|
||||||
{
|
{
|
||||||
attachments: draftAttachments,
|
attachments: draftAttachments,
|
||||||
conversationId: this.model.id,
|
|
||||||
doForwardMessage: async (
|
doForwardMessage: async (
|
||||||
conversationIds: Array<string>,
|
conversationIds: Array<string>,
|
||||||
messageBody?: string,
|
messageBody?: string,
|
||||||
|
|
Loading…
Add table
Reference in a new issue