Optimize Message rendering
This commit is contained in:
parent
2d6f13a8c5
commit
5a98fc2f4c
10 changed files with 242 additions and 133 deletions
55
ts/components/PureComponentProfiler.tsx
Normal file
55
ts/components/PureComponentProfiler.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export abstract class PureComponentProfiler<
|
||||||
|
Props extends Record<string, unknown>,
|
||||||
|
State extends Record<string, unknown>
|
||||||
|
> extends React.Component<Props, State> {
|
||||||
|
public override shouldComponentUpdate(
|
||||||
|
nextProps: Props,
|
||||||
|
nextState: State
|
||||||
|
): boolean {
|
||||||
|
console.group(`PureComponentProfiler(${this.props.id})`);
|
||||||
|
|
||||||
|
const propKeys = new Set([
|
||||||
|
...Object.keys(nextProps),
|
||||||
|
...Object.keys(this.props),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stateKeys = new Set([
|
||||||
|
...Object.keys(nextState ?? {}),
|
||||||
|
...Object.keys(this.state ?? {}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = false;
|
||||||
|
for (const key of propKeys) {
|
||||||
|
if (nextProps[key] !== this.props[key]) {
|
||||||
|
console.error(
|
||||||
|
`propUpdated(${key})`,
|
||||||
|
this.props[key],
|
||||||
|
'=>',
|
||||||
|
nextProps[key]
|
||||||
|
);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of stateKeys) {
|
||||||
|
if (nextState[key] !== this.state[key]) {
|
||||||
|
console.error(
|
||||||
|
`stateUpdated(${key}):`,
|
||||||
|
this.state[key],
|
||||||
|
'=>',
|
||||||
|
nextState[key]
|
||||||
|
);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -584,7 +584,7 @@ function ReplyOrReactionMessage({
|
||||||
conversationType="group"
|
conversationType="group"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
deletedForEveryone={reply.deletedForEveryone}
|
deletedForEveryone={reply.deletedForEveryone}
|
||||||
menu={undefined}
|
renderMenu={undefined}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -92,7 +92,7 @@ import { Emojify } from './Emojify';
|
||||||
import { getPaymentEventDescription } from '../../messages/helpers';
|
import { getPaymentEventDescription } from '../../messages/helpers';
|
||||||
import { PanelType } from '../../types/Panels';
|
import { PanelType } from '../../types/Panels';
|
||||||
|
|
||||||
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
|
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
|
||||||
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||||
const GUESS_METADATA_WIDTH_OUTGOING_SIZE: Record<MessageStatusType, number> = {
|
const GUESS_METADATA_WIDTH_OUTGOING_SIZE: Record<MessageStatusType, number> = {
|
||||||
delivered: 24,
|
delivered: 24,
|
||||||
|
@ -274,8 +274,10 @@ export type PropsData = {
|
||||||
isMessageRequestAccepted: boolean;
|
isMessageRequestAccepted: boolean;
|
||||||
bodyRanges?: HydratedBodyRangesType;
|
bodyRanges?: HydratedBodyRangesType;
|
||||||
|
|
||||||
menu: JSX.Element | undefined;
|
renderMenu?: () => JSX.Element | undefined;
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
|
||||||
|
item?: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsHousekeeping = {
|
export type PropsHousekeeping = {
|
||||||
|
@ -2531,7 +2533,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isSticker,
|
isSticker,
|
||||||
shouldCollapseAbove,
|
shouldCollapseAbove,
|
||||||
shouldCollapseBelow,
|
shouldCollapseBelow,
|
||||||
menu,
|
renderMenu,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { expired, expiring, isSelected, imageBroken } = this.state;
|
const { expired, expiring, isSelected, imageBroken } = this.state;
|
||||||
|
@ -2565,7 +2567,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
{this.renderError()}
|
{this.renderError()}
|
||||||
{this.renderAvatar()}
|
{this.renderAvatar()}
|
||||||
{this.renderContainer()}
|
{this.renderContainer()}
|
||||||
{menu}
|
{renderMenu?.()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ const defaultMessage: MessageDataPropsType = {
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
id: 'my-message',
|
id: 'my-message',
|
||||||
renderingContext: 'storybook',
|
renderingContext: 'storybook',
|
||||||
menu: undefined,
|
renderMenu: undefined,
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
previews: [],
|
previews: [],
|
||||||
|
|
|
@ -324,7 +324,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
contactNameColor={contactNameColor}
|
contactNameColor={contactNameColor}
|
||||||
containerElementRef={this.messageContainerRef}
|
containerElementRef={this.messageContainerRef}
|
||||||
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||||
menu={undefined}
|
renderMenu={undefined}
|
||||||
disableScroll
|
disableScroll
|
||||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||||
|
|
|
@ -214,6 +214,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
shouldRenderDateHeader,
|
shouldRenderDateHeader,
|
||||||
startCallingLobby,
|
startCallingLobby,
|
||||||
theme,
|
theme,
|
||||||
|
...reducedProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -230,7 +231,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
if (item.type === 'message') {
|
if (item.type === 'message') {
|
||||||
itemContents = (
|
itemContents = (
|
||||||
<TimelineMessage
|
<TimelineMessage
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
shouldCollapseAbove={shouldCollapseAbove}
|
shouldCollapseAbove={shouldCollapseAbove}
|
||||||
|
@ -247,7 +248,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
|
|
||||||
if (item.type === 'unsupportedMessage') {
|
if (item.type === 'unsupportedMessage') {
|
||||||
notification = (
|
notification = (
|
||||||
<UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />
|
<UnsupportedMessage {...reducedProps} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'callHistory') {
|
} else if (item.type === 'callHistory') {
|
||||||
notification = (
|
notification = (
|
||||||
|
@ -262,26 +263,26 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
);
|
);
|
||||||
} else if (item.type === 'chatSessionRefreshed') {
|
} else if (item.type === 'chatSessionRefreshed') {
|
||||||
notification = (
|
notification = (
|
||||||
<ChatSessionRefreshedNotification {...this.props} i18n={i18n} />
|
<ChatSessionRefreshedNotification {...reducedProps} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'deliveryIssue') {
|
} else if (item.type === 'deliveryIssue') {
|
||||||
notification = (
|
notification = (
|
||||||
<DeliveryIssueNotification
|
<DeliveryIssueNotification
|
||||||
{...item.data}
|
{...item.data}
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (item.type === 'timerNotification') {
|
} else if (item.type === 'timerNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<TimerNotification {...this.props} {...item.data} i18n={i18n} />
|
<TimerNotification {...reducedProps} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'universalTimerNotification') {
|
} else if (item.type === 'universalTimerNotification') {
|
||||||
notification = renderUniversalTimerNotification();
|
notification = renderUniversalTimerNotification();
|
||||||
} else if (item.type === 'changeNumberNotification') {
|
} else if (item.type === 'changeNumberNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<ChangeNumberNotification
|
<ChangeNumberNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -289,7 +290,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} else if (item.type === 'safetyNumberNotification') {
|
} else if (item.type === 'safetyNumberNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<SafetyNumberNotification
|
<SafetyNumberNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -297,27 +298,33 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} else if (item.type === 'verificationNotification') {
|
} else if (item.type === 'verificationNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<VerificationNotification
|
<VerificationNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (item.type === 'groupNotification') {
|
} else if (item.type === 'groupNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<GroupNotification {...this.props} {...item.data} i18n={i18n} />
|
<GroupNotification {...reducedProps} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'groupV2Change') {
|
} else if (item.type === 'groupV2Change') {
|
||||||
notification = (
|
notification = (
|
||||||
<GroupV2Change {...this.props} {...item.data} i18n={i18n} />
|
<GroupV2Change {...reducedProps} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'groupV1Migration') {
|
} else if (item.type === 'groupV1Migration') {
|
||||||
notification = (
|
notification = (
|
||||||
<GroupV1Migration {...this.props} {...item.data} i18n={i18n} />
|
<GroupV1Migration
|
||||||
|
{...reducedProps}
|
||||||
|
{...item.data}
|
||||||
|
i18n={i18n}
|
||||||
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else if (item.type === 'conversationMerge') {
|
} else if (item.type === 'conversationMerge') {
|
||||||
notification = (
|
notification = (
|
||||||
<ConversationMergeNotification
|
<ConversationMergeNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -325,17 +332,19 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} else if (item.type === 'phoneNumberDiscovery') {
|
} else if (item.type === 'phoneNumberDiscovery') {
|
||||||
notification = (
|
notification = (
|
||||||
<PhoneNumberDiscoveryNotification
|
<PhoneNumberDiscoveryNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (item.type === 'resetSessionNotification') {
|
} else if (item.type === 'resetSessionNotification') {
|
||||||
notification = <ResetSessionNotification {...this.props} i18n={i18n} />;
|
notification = (
|
||||||
|
<ResetSessionNotification {...reducedProps} i18n={i18n} />
|
||||||
|
);
|
||||||
} else if (item.type === 'profileChange') {
|
} else if (item.type === 'profileChange') {
|
||||||
notification = (
|
notification = (
|
||||||
<ProfileChangeNotification
|
<ProfileChangeNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -343,7 +352,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} else if (item.type === 'paymentEvent') {
|
} else if (item.type === 'paymentEvent') {
|
||||||
notification = (
|
notification = (
|
||||||
<PaymentEventNotification
|
<PaymentEventNotification
|
||||||
{...this.props}
|
{...reducedProps}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { Ref } from 'react';
|
import type { Ref } from 'react';
|
||||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||||
import ReactDOM, { createPortal } from 'react-dom';
|
import ReactDOM, { createPortal } from 'react-dom';
|
||||||
|
@ -106,6 +106,8 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
kickOffAttachmentDownload,
|
||||||
|
saveAttachment,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
const [reactionPickerRoot, setReactionPickerRoot] = useState<
|
||||||
|
@ -116,27 +118,28 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
const isWindowWidthNotNarrow =
|
const isWindowWidthNotNarrow =
|
||||||
containerWidthBreakpoint !== WidthBreakpoint.Narrow;
|
containerWidthBreakpoint !== WidthBreakpoint.Narrow;
|
||||||
|
|
||||||
function popperPreventOverflowModifier(): Partial<PreventOverflowModifier> {
|
const popperPreventOverflowModifier =
|
||||||
return {
|
useCallback((): Partial<PreventOverflowModifier> => {
|
||||||
name: 'preventOverflow',
|
return {
|
||||||
options: {
|
name: 'preventOverflow',
|
||||||
altAxis: true,
|
options: {
|
||||||
boundary: containerElementRef.current || undefined,
|
altAxis: true,
|
||||||
padding: {
|
boundary: containerElementRef.current || undefined,
|
||||||
bottom: 16,
|
padding: {
|
||||||
left: 8,
|
bottom: 16,
|
||||||
right: 8,
|
left: 8,
|
||||||
top: 16,
|
right: 8,
|
||||||
|
top: 16,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
}, [containerElementRef]);
|
||||||
}
|
|
||||||
|
|
||||||
// This id is what connects our triple-dot click with our associated pop-up menu.
|
// This id is what connects our triple-dot click with our associated pop-up menu.
|
||||||
// It needs to be unique.
|
// It needs to be unique.
|
||||||
const triggerId = String(id || `${author.id}-${timestamp}`);
|
const triggerId = String(id || `${author.id}-${timestamp}`);
|
||||||
|
|
||||||
const toggleReactionPicker = React.useCallback(
|
const toggleReactionPicker = useCallback(
|
||||||
(onlyRemove = false): void => {
|
(onlyRemove = false): void => {
|
||||||
if (reactionPickerRoot) {
|
if (reactionPickerRoot) {
|
||||||
document.body.removeChild(reactionPickerRoot);
|
document.body.removeChild(reactionPickerRoot);
|
||||||
|
@ -173,42 +176,46 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const openGenericAttachment = (event?: React.MouseEvent): void => {
|
const openGenericAttachment = useCallback(
|
||||||
const { kickOffAttachmentDownload, saveAttachment } = props;
|
(event?: React.MouseEvent): void => {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
if (event) {
|
if (!attachments || attachments.length !== 1) {
|
||||||
event.preventDefault();
|
return;
|
||||||
event.stopPropagation();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!attachments || attachments.length !== 1) {
|
const attachment = attachments[0];
|
||||||
return;
|
if (!isDownloaded(attachment)) {
|
||||||
}
|
kickOffAttachmentDownload({
|
||||||
|
attachment,
|
||||||
|
messageId: id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const attachment = attachments[0];
|
saveAttachment(attachment, timestamp);
|
||||||
if (!isDownloaded(attachment)) {
|
},
|
||||||
kickOffAttachmentDownload({
|
[kickOffAttachmentDownload, saveAttachment, attachments, id, timestamp]
|
||||||
attachment,
|
);
|
||||||
messageId: id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAttachment(attachment, timestamp);
|
const handleContextMenu = React.useCallback(
|
||||||
};
|
(event: React.MouseEvent<HTMLDivElement>): void => {
|
||||||
|
const selection = window.getSelection();
|
||||||
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>): void => {
|
if (selection && !selection.isCollapsed) {
|
||||||
const selection = window.getSelection();
|
return;
|
||||||
if (selection && !selection.isCollapsed) {
|
}
|
||||||
return;
|
if (event.target instanceof HTMLAnchorElement) {
|
||||||
}
|
return;
|
||||||
if (event.target instanceof HTMLAnchorElement) {
|
}
|
||||||
return;
|
if (menuTriggerRef.current) {
|
||||||
}
|
menuTriggerRef.current.handleContextClick(event);
|
||||||
if (menuTriggerRef.current) {
|
}
|
||||||
menuTriggerRef.current.handleContextClick(event);
|
},
|
||||||
}
|
[menuTriggerRef]
|
||||||
};
|
);
|
||||||
|
|
||||||
const canForward =
|
const canForward =
|
||||||
!isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment;
|
!isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment;
|
||||||
|
@ -229,11 +236,18 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
? openGenericAttachment
|
? openGenericAttachment
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const handleReplyToMessage = canReply
|
const handleReplyToMessage = useCallback(() => {
|
||||||
? () => setQuoteByMessageId(conversationId, id)
|
if (!canReply) {
|
||||||
: undefined;
|
return;
|
||||||
|
}
|
||||||
|
setQuoteByMessageId(conversationId, id);
|
||||||
|
}, [canReply, conversationId, id, setQuoteByMessageId]);
|
||||||
|
|
||||||
const handleReact = canReact ? () => toggleReactionPicker() : undefined;
|
const handleReact = useCallback(() => {
|
||||||
|
if (canReact) {
|
||||||
|
toggleReactionPicker();
|
||||||
|
}
|
||||||
|
}, [canReact, toggleReactionPicker]);
|
||||||
|
|
||||||
const [hasDOEConfirmation, setHasDOEConfirmation] = useState(false);
|
const [hasDOEConfirmation, setHasDOEConfirmation] = useState(false);
|
||||||
const [hasDeleteConfirmation, setHasDeleteConfirmation] = useState(false);
|
const [hasDeleteConfirmation, setHasDeleteConfirmation] = useState(false);
|
||||||
|
@ -252,6 +266,71 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
};
|
};
|
||||||
}, [isSelected, toggleReactionPickerKeyboard]);
|
}, [isSelected, toggleReactionPickerKeyboard]);
|
||||||
|
|
||||||
|
const renderMenu = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<MessageMenu
|
||||||
|
i18n={i18n}
|
||||||
|
triggerId={triggerId}
|
||||||
|
isWindowWidthNotNarrow={isWindowWidthNotNarrow}
|
||||||
|
direction={direction}
|
||||||
|
menuTriggerRef={menuTriggerRef}
|
||||||
|
showMenu={handleContextMenu}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onReplyToMessage={handleReplyToMessage}
|
||||||
|
onReact={handleReact}
|
||||||
|
/>
|
||||||
|
{reactionPickerRoot &&
|
||||||
|
createPortal(
|
||||||
|
<Popper
|
||||||
|
placement="top"
|
||||||
|
modifiers={[
|
||||||
|
offsetDistanceModifier(4),
|
||||||
|
popperPreventOverflowModifier(),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{({ ref, style }) =>
|
||||||
|
renderReactionPicker({
|
||||||
|
ref,
|
||||||
|
style,
|
||||||
|
selected: selectedReaction,
|
||||||
|
onClose: toggleReactionPicker,
|
||||||
|
onPick: emoji => {
|
||||||
|
toggleReactionPicker(true);
|
||||||
|
reactToMessage(id, {
|
||||||
|
emoji,
|
||||||
|
remove: emoji === selectedReaction,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderEmojiPicker,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Popper>,
|
||||||
|
reactionPickerRoot
|
||||||
|
)}
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
i18n,
|
||||||
|
triggerId,
|
||||||
|
isWindowWidthNotNarrow,
|
||||||
|
direction,
|
||||||
|
menuTriggerRef,
|
||||||
|
handleContextMenu,
|
||||||
|
handleDownload,
|
||||||
|
|
||||||
|
handleReplyToMessage,
|
||||||
|
handleReact,
|
||||||
|
reactionPickerRoot,
|
||||||
|
popperPreventOverflowModifier,
|
||||||
|
renderReactionPicker,
|
||||||
|
selectedReaction,
|
||||||
|
reactToMessage,
|
||||||
|
renderEmojiPicker,
|
||||||
|
toggleReactionPicker,
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasDOEConfirmation && canDeleteForEveryone && (
|
{hasDOEConfirmation && canDeleteForEveryone && (
|
||||||
|
@ -305,49 +384,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||||
{...props}
|
{...props}
|
||||||
renderingContext="conversation/TimelineItem"
|
renderingContext="conversation/TimelineItem"
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
menu={
|
renderMenu={renderMenu}
|
||||||
<Manager>
|
|
||||||
<MessageMenu
|
|
||||||
i18n={i18n}
|
|
||||||
triggerId={triggerId}
|
|
||||||
isWindowWidthNotNarrow={isWindowWidthNotNarrow}
|
|
||||||
direction={direction}
|
|
||||||
menuTriggerRef={menuTriggerRef}
|
|
||||||
showMenu={handleContextMenu}
|
|
||||||
onDownload={handleDownload}
|
|
||||||
onReplyToMessage={handleReplyToMessage}
|
|
||||||
onReact={handleReact}
|
|
||||||
/>
|
|
||||||
{reactionPickerRoot &&
|
|
||||||
createPortal(
|
|
||||||
<Popper
|
|
||||||
placement="top"
|
|
||||||
modifiers={[
|
|
||||||
offsetDistanceModifier(4),
|
|
||||||
popperPreventOverflowModifier(),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{({ ref, style }) =>
|
|
||||||
renderReactionPicker({
|
|
||||||
ref,
|
|
||||||
style,
|
|
||||||
selected: selectedReaction,
|
|
||||||
onClose: toggleReactionPicker,
|
|
||||||
onPick: emoji => {
|
|
||||||
toggleReactionPicker(true);
|
|
||||||
reactToMessage(id, {
|
|
||||||
emoji,
|
|
||||||
remove: emoji === selectedReaction,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
renderEmojiPicker,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Popper>,
|
|
||||||
reactionPickerRoot
|
|
||||||
)}
|
|
||||||
</Manager>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4101,6 +4101,10 @@ export class ConversationModel extends window.Backbone
|
||||||
// Perform asynchronous tasks before entering the batching mode
|
// Perform asynchronous tasks before entering the batching mode
|
||||||
await this.beforeAddSingleMessage(model);
|
await this.beforeAddSingleMessage(model);
|
||||||
|
|
||||||
|
if (sticker) {
|
||||||
|
await addStickerPackReference(model.id, sticker.packId);
|
||||||
|
}
|
||||||
|
|
||||||
this.isInReduxBatch = true;
|
this.isInReduxBatch = true;
|
||||||
batchDispatch(() => {
|
batchDispatch(() => {
|
||||||
try {
|
try {
|
||||||
|
@ -4146,10 +4150,6 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sticker) {
|
|
||||||
await addStickerPackReference(model.id, sticker.packId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderDuration = Date.now() - renderStart;
|
const renderDuration = Date.now() - renderStart;
|
||||||
|
|
||||||
if (renderDuration > SEND_REPORTING_THRESHOLD_MS) {
|
if (renderDuration > SEND_REPORTING_THRESHOLD_MS) {
|
||||||
|
|
|
@ -226,20 +226,22 @@ function sendMultiMediaMessage(
|
||||||
toastType,
|
toastType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
dispatch(setComposerDisabledState(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!message.length &&
|
||||||
|
!hasDraftAttachments(conversation.attributes.draftAttachments, {
|
||||||
|
includePending: false,
|
||||||
|
}) &&
|
||||||
|
!voiceNoteAttachment
|
||||||
|
) {
|
||||||
|
dispatch(setComposerDisabledState(false));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
|
||||||
!message.length &&
|
|
||||||
!hasDraftAttachments(conversation.attributes.draftAttachments, {
|
|
||||||
includePending: false,
|
|
||||||
}) &&
|
|
||||||
!voiceNoteAttachment
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachments: Array<AttachmentType> = [];
|
let attachments: Array<AttachmentType> = [];
|
||||||
if (voiceNoteAttachment) {
|
if (voiceNoteAttachment) {
|
||||||
attachments = [voiceNoteAttachment];
|
attachments = [voiceNoteAttachment];
|
||||||
|
@ -285,6 +287,7 @@ function sendMultiMediaMessage(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
dispatch(resetComposer());
|
dispatch(resetComposer());
|
||||||
|
dispatch(setComposerDisabledState(false));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -293,7 +296,6 @@ function sendMultiMediaMessage(
|
||||||
'Error pulling attached files before send',
|
'Error pulling attached files before send',
|
||||||
Errors.toLogFormat(error)
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
dispatch(setComposerDisabledState(false));
|
dispatch(setComposerDisabledState(false));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -291,6 +291,10 @@ export function reducer(
|
||||||
if (action.type === 'items/PUT_EXTERNAL') {
|
if (action.type === 'items/PUT_EXTERNAL') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
|
|
||||||
|
if (state[payload.key] === payload.value) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[payload.key]: payload.value,
|
[payload.key]: payload.value,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue