Revert "When you send a message, scroll it into view"
This reverts commit a3525c16ef
.
This commit is contained in:
parent
3601279287
commit
a52530262f
14 changed files with 438 additions and 387 deletions
|
@ -68,6 +68,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
scrollToBottom: action('scrollToBottom'),
|
||||
sortedGroupMembers: [],
|
||||
// EmojiButton
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
|
|
|
@ -123,6 +123,7 @@ export type OwnProps = Readonly<{
|
|||
setQuotedMessage(message: undefined): unknown;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
startRecording: () => unknown;
|
||||
scrollToBottom: (converstionId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
}>;
|
||||
|
||||
|
@ -202,6 +203,7 @@ export const CompositionArea = ({
|
|||
clearQuotedMessage,
|
||||
getPreferredBadge,
|
||||
getQuotedMessage,
|
||||
scrollToBottom,
|
||||
sortedGroupMembers,
|
||||
// EmojiButton
|
||||
onPickEmoji,
|
||||
|
@ -628,13 +630,14 @@ export const CompositionArea = ({
|
|||
{!large ? leftHandSideButtonsFragment : null}
|
||||
<div className="CompositionArea__input">
|
||||
<CompositionInput
|
||||
i18n={i18n}
|
||||
conversationId={conversationId}
|
||||
clearQuotedMessage={clearQuotedMessage}
|
||||
disabled={disabled}
|
||||
draftBodyRanges={draftBodyRanges}
|
||||
draftText={draftText}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
getQuotedMessage={getQuotedMessage}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
large={large}
|
||||
onDirtyChange={setDirty}
|
||||
|
@ -642,6 +645,7 @@ export const CompositionArea = ({
|
|||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={handleSubmit}
|
||||
onTextTooLong={onTextTooLong}
|
||||
scrollToBottom={scrollToBottom}
|
||||
skinTone={skinTone}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={theme}
|
||||
|
|
|
@ -21,6 +21,7 @@ const story = storiesOf('Components/CompositionInput', module);
|
|||
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
conversationId: 'conversation-id',
|
||||
disabled: boolean('disabled', overrideProps.disabled || false),
|
||||
onSubmit: action('onSubmit'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
|
@ -32,6 +33,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
getQuotedMessage: action('getQuotedMessage'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
large: boolean('large', overrideProps.large || false),
|
||||
scrollToBottom: action('scrollToBottom'),
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers || [],
|
||||
skinTone: select(
|
||||
'skinTone',
|
||||
|
|
|
@ -62,6 +62,7 @@ export type InputApi = {
|
|||
|
||||
export type Props = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly conversationId: string;
|
||||
readonly disabled?: boolean;
|
||||
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
||||
readonly large?: boolean;
|
||||
|
@ -87,6 +88,7 @@ export type Props = {
|
|||
): unknown;
|
||||
getQuotedMessage(): unknown;
|
||||
clearQuotedMessage(): unknown;
|
||||
scrollToBottom: (converstionId: string) => unknown;
|
||||
};
|
||||
|
||||
const MAX_LENGTH = 64 * 1024;
|
||||
|
@ -95,6 +97,7 @@ const BASE_CLASS_NAME = 'module-composition-input';
|
|||
export function CompositionInput(props: Props): React.ReactElement {
|
||||
const {
|
||||
i18n,
|
||||
conversationId,
|
||||
disabled,
|
||||
large,
|
||||
inputApi,
|
||||
|
@ -107,6 +110,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
getPreferredBadge,
|
||||
getQuotedMessage,
|
||||
clearQuotedMessage,
|
||||
scrollToBottom,
|
||||
sortedGroupMembers,
|
||||
theme,
|
||||
} = props;
|
||||
|
@ -237,6 +241,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
`CompositionInput: Submitting message ${timestamp} with ${mentions.length} mentions`
|
||||
);
|
||||
onSubmit(text, mentions, timestamp);
|
||||
scrollToBottom(conversationId);
|
||||
};
|
||||
|
||||
if (inputApi) {
|
||||
|
|
|
@ -44,6 +44,7 @@ const candidateConversations = Array.from(Array(100), () =>
|
|||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
attachments: overrideProps.attachments,
|
||||
conversationId: 'conversation-id',
|
||||
candidateConversations,
|
||||
doForwardMessage: action('doForwardMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
|
|
|
@ -41,6 +41,7 @@ import { useAnimated } from '../hooks/useAnimated';
|
|||
export type DataPropsType = {
|
||||
attachments?: Array<AttachmentDraftType>;
|
||||
candidateConversations: ReadonlyArray<ConversationType>;
|
||||
conversationId: string;
|
||||
doForwardMessage: (
|
||||
selectedContacts: Array<string>,
|
||||
messageBody?: string,
|
||||
|
@ -76,6 +77,7 @@ const MAX_FORWARD = 5;
|
|||
export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||
attachments,
|
||||
candidateConversations,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
|
@ -186,10 +188,10 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
}, [candidateConversations]);
|
||||
|
||||
const toggleSelectedConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
(selectedConversationId: string) => {
|
||||
let removeContact = false;
|
||||
const nextSelectedContacts = selectedContacts.filter(contact => {
|
||||
if (contact.id === conversationId) {
|
||||
if (contact.id === selectedConversationId) {
|
||||
removeContact = true;
|
||||
return false;
|
||||
}
|
||||
|
@ -199,7 +201,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
setSelectedContacts(nextSelectedContacts);
|
||||
return;
|
||||
}
|
||||
const selectedContact = contactLookup.get(conversationId);
|
||||
const selectedContact = contactLookup.get(selectedConversationId);
|
||||
if (selectedContact) {
|
||||
if (selectedContact.announcementsOnly && !selectedContact.areWeAdmin) {
|
||||
setCannotMessage(true);
|
||||
|
@ -335,6 +337,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
conversationId={conversationId}
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
@ -343,6 +346,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
scrollToBottom={noop}
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
|
@ -399,7 +403,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
selectedConversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
|
@ -408,7 +412,9 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
toggleSelectedConversation(
|
||||
selectedConversationId
|
||||
);
|
||||
}
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
|
|
|
@ -473,7 +473,10 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
overrideProps.isLoadingMessages === false
|
||||
),
|
||||
items: overrideProps.items || Object.keys(items),
|
||||
loadCountdownStart: undefined,
|
||||
messageHeightChangeIndex: undefined,
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToIndex: overrideProps.scrollToIndex,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
||||
|
@ -485,6 +488,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
warning: overrideProps.warning,
|
||||
|
||||
id: uuid(),
|
||||
isNearBottom: false,
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
renderHeroRow,
|
||||
|
|
|
@ -78,13 +78,14 @@ export type PropsDataType = {
|
|||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
isLoadingMessages: boolean;
|
||||
isNearBottom?: boolean;
|
||||
isNearBottom: boolean;
|
||||
items: ReadonlyArray<string>;
|
||||
loadCountdownStart?: number;
|
||||
messageHeightChangeIndex?: number;
|
||||
oldestUnreadIndex?: number;
|
||||
loadCountdownStart: number | undefined;
|
||||
messageHeightChangeIndex: number | undefined;
|
||||
oldestUnreadIndex: number | undefined;
|
||||
resetCounter: number;
|
||||
scrollToIndex?: number;
|
||||
scrollToBottomCounter: number;
|
||||
scrollToIndex: number | undefined;
|
||||
scrollToIndexCounter: number;
|
||||
totalUnread: number;
|
||||
};
|
||||
|
@ -959,7 +960,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
this.scrollDown(false);
|
||||
};
|
||||
|
||||
public scrollDown = (setFocus?: boolean): void => {
|
||||
public scrollDown = (setFocus?: boolean, forceScrollDown?: boolean): void => {
|
||||
const {
|
||||
haveNewest,
|
||||
id,
|
||||
|
@ -976,7 +977,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
const lastId = items[items.length - 1];
|
||||
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
||||
|
||||
if (!this.visibleRows) {
|
||||
if (!this.visibleRows || forceScrollDown) {
|
||||
if (haveNewest) {
|
||||
this.scrollToBottom(setFocus);
|
||||
} else if (!isLoadingMessages) {
|
||||
|
@ -1033,6 +1034,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
messageHeightChangeIndex,
|
||||
oldestUnreadIndex,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToIndex,
|
||||
typingContactId,
|
||||
} = this.props;
|
||||
|
@ -1050,6 +1052,10 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
this.resizeHeroRow();
|
||||
}
|
||||
|
||||
if (scrollToBottomCounter !== prevProps.scrollToBottomCounter) {
|
||||
this.scrollDown(false, true);
|
||||
}
|
||||
|
||||
// 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
|
||||
// unexpected changes to the scroll position. Those changes happen because
|
||||
|
|
|
@ -101,7 +101,6 @@ import { getAvatarData } from '../util/getAvatarData';
|
|||
import { createIdenticon } from '../util/createIdenticon';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { isMessageUnread } from '../util/isMessageUnread';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -117,13 +116,7 @@ const {
|
|||
upgradeMessageSchema,
|
||||
writeNewAttachmentData,
|
||||
} = window.Signal.Migrations;
|
||||
const {
|
||||
addStickerPackReference,
|
||||
getOlderMessagesByConversation,
|
||||
getMessageMetricsForConversation,
|
||||
getMessageById,
|
||||
getNewerMessagesByConversation,
|
||||
} = window.Signal.Data;
|
||||
const { addStickerPackReference } = window.Signal.Data;
|
||||
|
||||
const THREE_HOURS = durations.HOUR * 3;
|
||||
const FIVE_MINUTES = durations.MINUTE * 5;
|
||||
|
@ -131,8 +124,6 @@ const FIVE_MINUTES = durations.MINUTE * 5;
|
|||
const JOB_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([
|
||||
'profileLastFetchedAt',
|
||||
'needsStorageServiceSync',
|
||||
|
@ -172,7 +163,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
inProgressFetch?: Promise<unknown>;
|
||||
|
||||
newMessageQueue?: typeof window.PQueueType;
|
||||
incomingMessageQueue?: typeof window.PQueueType;
|
||||
|
||||
jobQueue?: typeof window.PQueueType;
|
||||
|
||||
|
@ -1312,340 +1303,34 @@ export class ConversationModel extends window.Backbone
|
|||
this.debouncedUpdateLastMessage!();
|
||||
}
|
||||
|
||||
addIncomingMessage(message: MessageModel): void {
|
||||
this.addSingleMessage(message);
|
||||
addSingleMessage(message: MessageModel): void {
|
||||
const { messagesAdded } = window.reduxActions.conversations;
|
||||
const isNewMessage = true;
|
||||
messagesAdded(
|
||||
this.id,
|
||||
[{ ...message.attributes }],
|
||||
isNewMessage,
|
||||
window.isActive()
|
||||
);
|
||||
}
|
||||
|
||||
// New messages might arrive while we're in the middle of a bulk fetch from the
|
||||
// database. We'll wait until that is done before moving forward.
|
||||
async addSingleMessage(
|
||||
message: MessageModel,
|
||||
{ isJustSent }: { isJustSent: boolean } = { isJustSent: false }
|
||||
): Promise<void> {
|
||||
if (!this.newMessageQueue) {
|
||||
this.newMessageQueue = new window.PQueue({
|
||||
// For incoming messages, they might arrive while we're in the middle of a bulk fetch
|
||||
// from the database. We'll wait until that is done to process this newly-arrived
|
||||
// message.
|
||||
addIncomingMessage(message: MessageModel): void {
|
||||
if (!this.incomingMessageQueue) {
|
||||
this.incomingMessageQueue = new window.PQueue({
|
||||
concurrency: 1,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
}
|
||||
|
||||
// We use a queue here to ensure messages are added to the UI in the order received
|
||||
await this.newMessageQueue.add(async () => {
|
||||
this.incomingMessageQueue.add(async () => {
|
||||
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 {
|
||||
|
@ -3974,7 +3659,7 @@ export class ConversationModel extends window.Backbone
|
|||
const enableProfileSharing = Boolean(
|
||||
mandatoryProfileSharingEnabled && !this.get('profileSharing')
|
||||
);
|
||||
this.addSingleMessage(model, { isJustSent: true });
|
||||
this.addSingleMessage(model);
|
||||
|
||||
const draftProperties = dontClearDraft
|
||||
? {}
|
||||
|
|
|
@ -248,6 +248,7 @@ export type ConversationMessageType = {
|
|||
messageIds: Array<string>;
|
||||
metrics: MessageMetricsType;
|
||||
resetCounter: number;
|
||||
scrollToBottomCounter: number;
|
||||
scrollToMessageId?: string;
|
||||
scrollToMessageCounter: number;
|
||||
};
|
||||
|
@ -551,10 +552,9 @@ export type MessagesAddedActionType = {
|
|||
type: 'MESSAGES_ADDED';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
isActive: boolean;
|
||||
isJustSent: boolean;
|
||||
isNewMessage: boolean;
|
||||
messages: Array<MessageAttributesType>;
|
||||
isNewMessage: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -611,6 +611,12 @@ export type SetSelectedConversationPanelDepthActionType = {
|
|||
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
|
||||
payload: { panelDepth: number };
|
||||
};
|
||||
export type ScrollToBpttomActionType = {
|
||||
type: 'SCROLL_TO_BOTTOM';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
export type ScrollToMessageActionType = {
|
||||
type: 'SCROLL_TO_MESSAGE';
|
||||
payload: {
|
||||
|
@ -767,6 +773,7 @@ export type ConversationActionType =
|
|||
| ReplaceAvatarsActionType
|
||||
| ReviewGroupMemberNameCollisionActionType
|
||||
| ReviewMessageRequestNameCollisionActionType
|
||||
| ScrollToBpttomActionType
|
||||
| ScrollToMessageActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
|
@ -838,6 +845,7 @@ export const actions = {
|
|||
reviewMessageRequestNameCollision,
|
||||
saveAvatarToDisk,
|
||||
saveUsername,
|
||||
scrollToBottom,
|
||||
scrollToMessage,
|
||||
selectMessage,
|
||||
setComposeGroupAvatar,
|
||||
|
@ -1548,27 +1556,19 @@ function messageSizeChanged(
|
|||
},
|
||||
};
|
||||
}
|
||||
function messagesAdded({
|
||||
conversationId,
|
||||
isActive,
|
||||
isJustSent,
|
||||
isNewMessage,
|
||||
messages,
|
||||
}: {
|
||||
conversationId: string;
|
||||
isActive: boolean;
|
||||
isJustSent: boolean;
|
||||
isNewMessage: boolean;
|
||||
messages: Array<MessageAttributesType>;
|
||||
}): MessagesAddedActionType {
|
||||
function messagesAdded(
|
||||
conversationId: string,
|
||||
messages: Array<MessageAttributesType>,
|
||||
isNewMessage: boolean,
|
||||
isActive: boolean
|
||||
): MessagesAddedActionType {
|
||||
return {
|
||||
type: 'MESSAGES_ADDED',
|
||||
payload: {
|
||||
conversationId,
|
||||
isActive,
|
||||
isJustSent,
|
||||
isNewMessage,
|
||||
messages,
|
||||
isNewMessage,
|
||||
isActive,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1734,6 +1734,15 @@ function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
|||
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
|
||||
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
|
||||
}
|
||||
function scrollToBottom(conversationId: string): ScrollToBpttomActionType {
|
||||
return {
|
||||
type: 'SCROLL_TO_BOTTOM',
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function scrollToMessage(
|
||||
conversationId: string,
|
||||
messageId: string
|
||||
|
@ -2648,6 +2657,9 @@ export function reducer(
|
|||
scrollToMessageCounter: existingConversation
|
||||
? existingConversation.scrollToMessageCounter + 1
|
||||
: 0,
|
||||
scrollToBottomCounter: existingConversation
|
||||
? existingConversation.scrollToBottomCounter
|
||||
: 0,
|
||||
messageIds,
|
||||
metrics: {
|
||||
...metrics,
|
||||
|
@ -2727,6 +2739,28 @@ 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') {
|
||||
const { payload } = action;
|
||||
const { conversationId, messageId } = payload;
|
||||
|
@ -2913,8 +2947,7 @@ export function reducer(
|
|||
}
|
||||
|
||||
if (action.type === 'MESSAGES_ADDED') {
|
||||
const { conversationId, isActive, isJustSent, isNewMessage, messages } =
|
||||
action.payload;
|
||||
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
||||
const { messagesByConversation, messagesLookup } = state;
|
||||
|
||||
const existingConversation = messagesByConversation[conversationId];
|
||||
|
@ -2961,12 +2994,6 @@ export function reducer(
|
|||
// won't add new messages to our message list.
|
||||
const haveLatest = newest && newest.id === lastMessageId;
|
||||
if (!haveLatest) {
|
||||
if (isJustSent) {
|
||||
log.warn(
|
||||
'reducer/MESSAGES_ADDED: isJustSent is true, but haveLatest is false'
|
||||
);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
@ -3028,7 +3055,7 @@ export function reducer(
|
|||
isLoadingMessages: false,
|
||||
messageIds,
|
||||
heightChangeMessageIds,
|
||||
scrollToMessageId: isJustSent ? last.id : undefined,
|
||||
scrollToMessageId: undefined,
|
||||
metrics: {
|
||||
...existingConversation.metrics,
|
||||
newest,
|
||||
|
|
|
@ -849,6 +849,7 @@ export function _conversationMessagesSelector(
|
|||
messageIds,
|
||||
metrics,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToMessageId,
|
||||
scrollToMessageCounter,
|
||||
} = conversation;
|
||||
|
@ -887,7 +888,7 @@ export function _conversationMessagesSelector(
|
|||
isLoadingMessages,
|
||||
loadCountdownStart,
|
||||
items,
|
||||
isNearBottom,
|
||||
isNearBottom: isNearBottom || false,
|
||||
messageHeightChangeIndex:
|
||||
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
||||
? messageHeightChangeIndex
|
||||
|
@ -897,6 +898,7 @@ export function _conversationMessagesSelector(
|
|||
? oldestUnreadIndex
|
||||
: undefined,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToIndex:
|
||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||
scrollToIndexCounter: scrollToMessageCounter,
|
||||
|
@ -932,10 +934,16 @@ export const getConversationMessagesSelector = createSelector(
|
|||
haveNewest: false,
|
||||
haveOldest: false,
|
||||
isLoadingMessages: false,
|
||||
isNearBottom: false,
|
||||
items: [],
|
||||
loadCountdownStart: undefined,
|
||||
messageHeightChangeIndex: undefined,
|
||||
oldestUnreadIndex: undefined,
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToIndex: undefined,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import type { AttachmentDraftType } from '../../types/Attachment';
|
|||
|
||||
export type SmartForwardMessageModalProps = {
|
||||
attachments?: Array<AttachmentDraftType>;
|
||||
conversationId: string;
|
||||
doForwardMessage: (
|
||||
selectedContacts: Array<string>,
|
||||
messageBody?: string,
|
||||
|
@ -41,6 +42,7 @@ const mapStateToProps = (
|
|||
): DataPropsType => {
|
||||
const {
|
||||
attachments,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
isSticker,
|
||||
messageBody,
|
||||
|
@ -57,6 +59,7 @@ const mapStateToProps = (
|
|||
return {
|
||||
attachments,
|
||||
candidateConversations,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
|
|
|
@ -337,6 +337,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
totalUnread: 0,
|
||||
},
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToMessageCounter: 0,
|
||||
};
|
||||
}
|
||||
|
@ -839,6 +840,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
messageIds: [messageId],
|
||||
metrics: { totalUnread: 0 },
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToMessageCounter: 0,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
|||
import type { ConversationModel } from '../models/conversations';
|
||||
import type {
|
||||
GroupV2PendingMemberType,
|
||||
MessageModelCollectionType,
|
||||
MessageAttributesType,
|
||||
ConversationModelCollectionType,
|
||||
QuotedMessageType,
|
||||
|
@ -48,6 +49,7 @@ import {
|
|||
isOutgoing,
|
||||
isTapToView,
|
||||
} from '../state/selectors/message';
|
||||
import { isMessageUnread } from '../util/isMessageUnread';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getMessagesByConversation,
|
||||
|
@ -136,7 +138,13 @@ const {
|
|||
upgradeMessageSchema,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
const { getMessageById, getMessagesBySentAt } = window.Signal.Data;
|
||||
const {
|
||||
getOlderMessagesByConversation,
|
||||
getMessageMetricsForConversation,
|
||||
getMessageById,
|
||||
getMessagesBySentAt,
|
||||
getNewerMessagesByConversation,
|
||||
} = window.Signal.Data;
|
||||
|
||||
type MessageActionsType = {
|
||||
deleteMessage: (messageId: string) => unknown;
|
||||
|
@ -466,6 +474,107 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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) => {
|
||||
if (!window.isActive()) {
|
||||
return;
|
||||
|
@ -506,10 +615,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
},
|
||||
contactSupport,
|
||||
learnMoreAboutDeliveryIssue,
|
||||
loadNewerMessages: this.model.loadNewerMessages.bind(this.model),
|
||||
loadNewestMessages: this.model.loadNewestMessages.bind(this.model),
|
||||
loadAndScroll: this.model.loadAndScroll.bind(this.model),
|
||||
loadOlderMessages: this.model.loadOlderMessages.bind(this.model),
|
||||
loadNewerMessages,
|
||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||
loadAndScroll: this.loadAndScroll.bind(this),
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
onBlock: createMessageRequestResponseHandler(
|
||||
'onBlock',
|
||||
|
@ -882,6 +991,38 @@ 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> {
|
||||
const message = await getMessageById(messageId, {
|
||||
Message: Whisper.Message,
|
||||
|
@ -912,7 +1053,162 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.model.loadAndScroll(messageId);
|
||||
this.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> {
|
||||
|
@ -1206,7 +1502,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
});
|
||||
|
||||
if (message) {
|
||||
this.model.loadAndScroll(messageId);
|
||||
this.loadAndScroll(messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1218,7 +1514,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
await retryPlaceholders.findByConversationAndMarkOpened(this.model.id);
|
||||
}
|
||||
|
||||
this.model.loadNewestMessages(undefined, undefined);
|
||||
this.loadNewestMessages(undefined, undefined);
|
||||
this.model.updateLastMessage();
|
||||
|
||||
this.focusMessageField();
|
||||
|
@ -1286,6 +1582,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
window.reduxStore,
|
||||
{
|
||||
attachments: draftAttachments,
|
||||
conversationId: this.model.id,
|
||||
doForwardMessage: async (
|
||||
conversationIds: Array<string>,
|
||||
messageBody?: string,
|
||||
|
|
Loading…
Reference in a new issue