Group disparate status together, but show metadata if different
This commit is contained in:
parent
2602db97f0
commit
1ad284d22c
12 changed files with 282 additions and 146 deletions
|
@ -18,8 +18,8 @@ const story = storiesOf('Components/Conversation/CallingNotification', module);
|
||||||
const getCommonProps = () => ({
|
const getCommonProps = () => ({
|
||||||
conversationId: 'fake-conversation-id',
|
conversationId: 'fake-conversation-id',
|
||||||
i18n,
|
i18n,
|
||||||
|
isNextItemCallingNotification: false,
|
||||||
messageId: 'fake-message-id',
|
messageId: 'fake-message-id',
|
||||||
nextItem: undefined,
|
|
||||||
now: Date.now(),
|
now: Date.now(),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
|
@ -70,7 +70,7 @@ story.add('Two incoming direct calls back-to-back', () => {
|
||||||
<CallingNotification
|
<CallingNotification
|
||||||
{...getCommonProps()}
|
{...getCommonProps()}
|
||||||
{...call1}
|
{...call1}
|
||||||
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
|
isNextItemCallingNotification
|
||||||
/>
|
/>
|
||||||
<CallingNotification {...getCommonProps()} {...call2} />
|
<CallingNotification {...getCommonProps()} {...call2} />
|
||||||
</>
|
</>
|
||||||
|
@ -99,7 +99,7 @@ story.add('Two outgoing direct calls back-to-back', () => {
|
||||||
<CallingNotification
|
<CallingNotification
|
||||||
{...getCommonProps()}
|
{...getCommonProps()}
|
||||||
{...call1}
|
{...call1}
|
||||||
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
|
isNextItemCallingNotification
|
||||||
/>
|
/>
|
||||||
<CallingNotification {...getCommonProps()} {...call2} />
|
<CallingNotification {...getCommonProps()} {...call2} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
} from '../../util/callingNotification';
|
} from '../../util/callingNotification';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||||
import type { TimelineItemType } from './TimelineItem';
|
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
|
@ -31,7 +30,7 @@ export type PropsActionsType = {
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
nextItem: undefined | TimelineItemType;
|
isNextItemCallingNotification: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
||||||
|
@ -86,12 +85,12 @@ function renderCallingNotificationButton(
|
||||||
activeCallConversationId,
|
activeCallConversationId,
|
||||||
conversationId,
|
conversationId,
|
||||||
i18n,
|
i18n,
|
||||||
nextItem,
|
isNextItemCallingNotification,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
startCallingLobby,
|
startCallingLobby,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (nextItem?.type === 'callHistory') {
|
if (isNextItemCallingNotification) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,15 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
|
shouldCollapseAbove: isBoolean(overrideProps.shouldCollapseAbove)
|
||||||
|
? overrideProps.shouldCollapseAbove
|
||||||
|
: false,
|
||||||
|
shouldCollapseBelow: isBoolean(overrideProps.shouldCollapseBelow)
|
||||||
|
? overrideProps.shouldCollapseBelow
|
||||||
|
: false,
|
||||||
|
shouldHideMetadata: isBoolean(overrideProps.shouldHideMetadata)
|
||||||
|
? overrideProps.shouldHideMetadata
|
||||||
|
: false,
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
showContactModal: action('showContactModal'),
|
showContactModal: action('showContactModal'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
@ -202,9 +211,9 @@ const renderMany = (propsArray: ReadonlyArray<Props>) =>
|
||||||
<Message
|
<Message
|
||||||
key={message.text}
|
key={message.text}
|
||||||
{...message}
|
{...message}
|
||||||
previousItem={createTimelineItem(propsArray[index - 1])}
|
shouldCollapseAbove={Boolean(propsArray[index - 1])}
|
||||||
item={createTimelineItem(message)}
|
item={createTimelineItem(message)}
|
||||||
nextItem={createTimelineItem(propsArray[index + 1])}
|
shouldCollapseBelow={Boolean(propsArray[index + 1])}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -83,10 +83,6 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle';
|
||||||
import { offsetDistanceModifier } from '../../util/popperUtil';
|
import { offsetDistanceModifier } from '../../util/popperUtil';
|
||||||
import * as KeyboardLayout from '../../services/keyboardLayout';
|
import * as KeyboardLayout from '../../services/keyboardLayout';
|
||||||
import { StopPropagation } from '../StopPropagation';
|
import { StopPropagation } from '../StopPropagation';
|
||||||
import {
|
|
||||||
areMessagesInSameGroup,
|
|
||||||
UnreadIndicatorPlacement,
|
|
||||||
} from '../../util/timelineUtil';
|
|
||||||
|
|
||||||
type Trigger = {
|
type Trigger = {
|
||||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
@ -269,14 +265,14 @@ export type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
nextItem?: TimelineItemType;
|
|
||||||
previousItem?: TimelineItemType;
|
|
||||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
||||||
renderReactionPicker: (
|
renderReactionPicker: (
|
||||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
shouldCollapseAbove: boolean;
|
||||||
|
shouldCollapseBelow: boolean;
|
||||||
|
shouldHideMetadata: boolean;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
|
@ -554,6 +550,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
attachments,
|
attachments,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
|
shouldHideMetadata,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
textDirection,
|
textDirection,
|
||||||
|
@ -565,7 +562,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
!expirationLength &&
|
!expirationLength &&
|
||||||
!expirationTimestamp &&
|
!expirationTimestamp &&
|
||||||
(!status || SENT_STATUSES.has(status)) &&
|
(!status || SENT_STATUSES.has(status)) &&
|
||||||
this.isCollapsedBelow()
|
shouldHideMetadata
|
||||||
) {
|
) {
|
||||||
return MetadataPlacement.NotRendered;
|
return MetadataPlacement.NotRendered;
|
||||||
}
|
}
|
||||||
|
@ -688,34 +685,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return isMessageRequestAccepted && !isBlocked;
|
return isMessageRequestAccepted && !isBlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isCollapsedAbove(
|
|
||||||
{ item, previousItem, unreadIndicatorPlacement }: Readonly<Props> = this
|
|
||||||
.props
|
|
||||||
): boolean {
|
|
||||||
return areMessagesInSameGroup(
|
|
||||||
previousItem,
|
|
||||||
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustAbove,
|
|
||||||
item
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCollapsedBelow(
|
|
||||||
{ item, nextItem, unreadIndicatorPlacement }: Readonly<Props> = this.props
|
|
||||||
): boolean {
|
|
||||||
return areMessagesInSameGroup(
|
|
||||||
item,
|
|
||||||
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustBelow,
|
|
||||||
nextItem
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldRenderAuthor(): boolean {
|
private shouldRenderAuthor(): boolean {
|
||||||
const { author, conversationType, direction } = this.props;
|
const { author, conversationType, direction, shouldCollapseAbove } =
|
||||||
|
this.props;
|
||||||
return Boolean(
|
return Boolean(
|
||||||
direction === 'incoming' &&
|
direction === 'incoming' &&
|
||||||
conversationType === 'group' &&
|
conversationType === 'group' &&
|
||||||
author.title &&
|
author.title &&
|
||||||
!this.isCollapsedAbove()
|
!shouldCollapseAbove
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,6 +827,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
renderingContext,
|
renderingContext,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
|
shouldCollapseAbove,
|
||||||
|
shouldCollapseBelow,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
textPending,
|
textPending,
|
||||||
|
@ -925,10 +904,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
withContentAbove={
|
withContentAbove={
|
||||||
isSticker || withContentAbove || this.isCollapsedAbove()
|
isSticker || withContentAbove || shouldCollapseAbove
|
||||||
}
|
}
|
||||||
withContentBelow={
|
withContentBelow={
|
||||||
isSticker || withContentBelow || this.isCollapsedBelow()
|
isSticker || withContentBelow || shouldCollapseBelow
|
||||||
}
|
}
|
||||||
isSticker={isSticker}
|
isSticker={isSticker}
|
||||||
stickerSize={STICKER_SIZE}
|
stickerSize={STICKER_SIZE}
|
||||||
|
@ -1223,6 +1202,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
id,
|
id,
|
||||||
quote,
|
quote,
|
||||||
scrollToQuotedMessage,
|
scrollToQuotedMessage,
|
||||||
|
shouldCollapseAbove,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
|
@ -1248,11 +1228,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
curveTopLeft = false;
|
curveTopLeft = false;
|
||||||
curveTopRight = false;
|
curveTopRight = false;
|
||||||
} else if (isIncoming) {
|
} else if (isIncoming) {
|
||||||
curveTopLeft = !this.isCollapsedAbove();
|
curveTopLeft = !shouldCollapseAbove;
|
||||||
curveTopRight = true;
|
curveTopRight = true;
|
||||||
} else {
|
} else {
|
||||||
curveTopLeft = true;
|
curveTopLeft = true;
|
||||||
curveTopRight = !this.isCollapsedAbove();
|
curveTopRight = !shouldCollapseAbove;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1285,6 +1265,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
storyReplyContext,
|
storyReplyContext,
|
||||||
|
shouldCollapseAbove,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!storyReplyContext) {
|
if (!storyReplyContext) {
|
||||||
|
@ -1299,11 +1280,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
curveTopLeft = false;
|
curveTopLeft = false;
|
||||||
curveTopRight = false;
|
curveTopRight = false;
|
||||||
} else if (isIncoming) {
|
} else if (isIncoming) {
|
||||||
curveTopLeft = !this.isCollapsedAbove();
|
curveTopLeft = !shouldCollapseAbove;
|
||||||
curveTopRight = true;
|
curveTopRight = true;
|
||||||
} else {
|
} else {
|
||||||
curveTopLeft = true;
|
curveTopLeft = true;
|
||||||
curveTopRight = !this.isCollapsedAbove();
|
curveTopRight = !shouldCollapseAbove;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1400,6 +1381,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
direction,
|
direction,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
|
shouldCollapseBelow,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
theme,
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -1415,7 +1397,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
this.hasReactions(),
|
this.hasReactions(),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{this.isCollapsedBelow() ? (
|
{shouldCollapseBelow ? (
|
||||||
<AvatarSpacer size={GROUP_AVATAR_SIZE} />
|
<AvatarSpacer size={GROUP_AVATAR_SIZE} />
|
||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -2660,8 +2642,16 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): JSX.Element | null {
|
public override render(): JSX.Element | null {
|
||||||
const { author, attachments, direction, id, isSticker, timestamp } =
|
const {
|
||||||
this.props;
|
author,
|
||||||
|
attachments,
|
||||||
|
direction,
|
||||||
|
id,
|
||||||
|
isSticker,
|
||||||
|
shouldCollapseAbove,
|
||||||
|
shouldCollapseBelow,
|
||||||
|
timestamp,
|
||||||
|
} = this.props;
|
||||||
const { expired, expiring, imageBroken, isSelected } = this.state;
|
const { expired, expiring, imageBroken, isSelected } = this.state;
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -2681,8 +2671,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message',
|
'module-message',
|
||||||
`module-message--${direction}`,
|
`module-message--${direction}`,
|
||||||
this.isCollapsedAbove() && 'module-message--collapsed-above',
|
shouldCollapseAbove && 'module-message--collapsed-above',
|
||||||
this.isCollapsedBelow() && 'module-message--collapsed-below',
|
shouldCollapseBelow && 'module-message--collapsed-below',
|
||||||
isSelected ? 'module-message--selected' : null,
|
isSelected ? 'module-message--selected' : null,
|
||||||
expiring ? 'module-message--expired' : null
|
expiring ? 'module-message--expired' : null
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -345,6 +345,9 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
replyToMessage={replyToMessage}
|
replyToMessage={replyToMessage}
|
||||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||||
retrySend={retrySend}
|
retrySend={retrySend}
|
||||||
|
shouldCollapseAbove={false}
|
||||||
|
shouldCollapseBelow={false}
|
||||||
|
shouldHideMetadata={false}
|
||||||
showForwardMessageModal={showForwardMessageModal}
|
showForwardMessageModal={showForwardMessageModal}
|
||||||
scrollToQuotedMessage={() => {
|
scrollToQuotedMessage={() => {
|
||||||
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
||||||
|
|
|
@ -82,6 +82,9 @@ const defaultMessageProps: MessagesProps = {
|
||||||
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
|
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
|
||||||
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||||
selectMessage: action('default--selectMessage'),
|
selectMessage: action('default--selectMessage'),
|
||||||
|
shouldCollapseAbove: false,
|
||||||
|
shouldCollapseBelow: false,
|
||||||
|
shouldHideMetadata: false,
|
||||||
showContactDetail: action('default--showContactDetail'),
|
showContactDetail: action('default--showContactDetail'),
|
||||||
showContactModal: action('default--showContactModal'),
|
showContactModal: action('default--showContactModal'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
|
|
@ -423,25 +423,21 @@ const renderItem = ({
|
||||||
messageId,
|
messageId,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
isOldestTimelineItem,
|
|
||||||
}: {
|
}: {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
containerElementRef: React.RefObject<HTMLElement>;
|
containerElementRef: React.RefObject<HTMLElement>;
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
isOldestTimelineItem: boolean;
|
|
||||||
}) => (
|
}) => (
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
getPreferredBadge={() => undefined}
|
getPreferredBadge={() => undefined}
|
||||||
id=""
|
id=""
|
||||||
isOldestTimelineItem={isOldestTimelineItem}
|
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
renderEmojiPicker={() => <div />}
|
renderEmojiPicker={() => <div />}
|
||||||
renderReactionPicker={() => <div />}
|
renderReactionPicker={() => <div />}
|
||||||
item={items[messageId]}
|
item={items[messageId]}
|
||||||
previousItem={undefined}
|
|
||||||
nextItem={undefined}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode="keyboard"
|
interactionMode="keyboard"
|
||||||
|
isNextItemCallingNotification={false}
|
||||||
theme={ThemeType.light}
|
theme={ThemeType.light}
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
|
@ -451,6 +447,10 @@ const renderItem = ({
|
||||||
<div>*UniversalTimerNotification*</div>
|
<div>*UniversalTimerNotification*</div>
|
||||||
)}
|
)}
|
||||||
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
||||||
|
shouldCollapseAbove={false}
|
||||||
|
shouldCollapseBelow={false}
|
||||||
|
shouldHideMetadata={false}
|
||||||
|
shouldRenderDateHeader={false}
|
||||||
{...actions()}
|
{...actions()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,7 +53,7 @@ const getDefaultProps = () => ({
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isOldestTimelineItem: false,
|
isNextItemCallingNotification: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
interactionMode: 'keyboard' as const,
|
interactionMode: 'keyboard' as const,
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
|
@ -94,8 +94,11 @@ const getDefaultProps = () => ({
|
||||||
showIdentity: action('showIdentity'),
|
showIdentity: action('showIdentity'),
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
previousItem: undefined,
|
shouldCollapseAbove: false,
|
||||||
nextItem: undefined,
|
shouldCollapseBelow: false,
|
||||||
|
shouldHideMetadata: false,
|
||||||
|
shouldRenderDateHeader: false,
|
||||||
|
|
||||||
now: Date.now(),
|
now: Date.now(),
|
||||||
|
|
||||||
renderContact,
|
renderContact,
|
||||||
|
|
|
@ -5,7 +5,6 @@ import type { ReactChild, RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { isSameDay } from '../../util/timestamp';
|
|
||||||
|
|
||||||
import type { InteractionModeType } from '../../state/ducks/conversations';
|
import type { InteractionModeType } from '../../state/ducks/conversations';
|
||||||
import { TimelineDateHeader } from './TimelineDateHeader';
|
import { TimelineDateHeader } from './TimelineDateHeader';
|
||||||
|
@ -56,7 +55,6 @@ import type { SmartContactRendererType } from '../../groupChange';
|
||||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||||
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
||||||
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
|
||||||
import type { FullJSXType } from '../Intl';
|
import type { FullJSXType } from '../Intl';
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
|
@ -148,17 +146,15 @@ type PropsLocalType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
id: string;
|
id: string;
|
||||||
|
isNextItemCallingNotification: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
|
shouldRenderDateHeader: boolean;
|
||||||
renderContact: SmartContactRendererType<FullJSXType>;
|
renderContact: SmartContactRendererType<FullJSXType>;
|
||||||
renderUniversalTimerNotification: () => JSX.Element;
|
renderUniversalTimerNotification: () => JSX.Element;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
isOldestTimelineItem: boolean;
|
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
previousItem: undefined | TimelineItemType;
|
|
||||||
nextItem: undefined | TimelineItemType;
|
|
||||||
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActionsType = MessageActionsType &
|
type PropsActionsType = MessageActionsType &
|
||||||
|
@ -178,6 +174,9 @@ export type PropsType = PropsLocalType &
|
||||||
| 'renderEmojiPicker'
|
| 'renderEmojiPicker'
|
||||||
| 'renderAudioAttachment'
|
| 'renderAudioAttachment'
|
||||||
| 'renderReactionPicker'
|
| 'renderReactionPicker'
|
||||||
|
| 'shouldCollapseAbove'
|
||||||
|
| 'shouldCollapseBelow'
|
||||||
|
| 'shouldHideMetadata'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
|
@ -186,19 +185,20 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
|
i18n,
|
||||||
id,
|
id,
|
||||||
isOldestTimelineItem,
|
isNextItemCallingNotification,
|
||||||
isSelected,
|
isSelected,
|
||||||
item,
|
item,
|
||||||
i18n,
|
|
||||||
theme,
|
|
||||||
nextItem,
|
|
||||||
previousItem,
|
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
|
shouldCollapseAbove,
|
||||||
|
shouldCollapseBelow,
|
||||||
|
shouldHideMetadata,
|
||||||
|
shouldRenderDateHeader,
|
||||||
startCallingLobby,
|
startCallingLobby,
|
||||||
unreadIndicatorPlacement,
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -217,12 +217,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<Message
|
<Message
|
||||||
{...this.props}
|
{...this.props}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
|
shouldCollapseAbove={shouldCollapseAbove}
|
||||||
|
shouldCollapseBelow={shouldCollapseBelow}
|
||||||
|
shouldHideMetadata={shouldHideMetadata}
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
renderingContext="conversation/TimelineItem"
|
renderingContext="conversation/TimelineItem"
|
||||||
unreadIndicatorPlacement={unreadIndicatorPlacement}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -237,7 +239,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<CallingNotification
|
<CallingNotification
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
nextItem={nextItem}
|
isNextItemCallingNotification={isNextItemCallingNotification}
|
||||||
returnToActiveCall={returnToActiveCall}
|
returnToActiveCall={returnToActiveCall}
|
||||||
startCallingLobby={startCallingLobby}
|
startCallingLobby={startCallingLobby}
|
||||||
{...item.data}
|
{...item.data}
|
||||||
|
@ -340,14 +342,6 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRenderDateHeader =
|
|
||||||
isOldestTimelineItem ||
|
|
||||||
Boolean(
|
|
||||||
previousItem &&
|
|
||||||
// This comparison avoids strange header behavior for out-of-order messages.
|
|
||||||
item.timestamp > previousItem.timestamp &&
|
|
||||||
!isSameDay(previousItem.timestamp, item.timestamp)
|
|
||||||
);
|
|
||||||
if (shouldRenderDateHeader) {
|
if (shouldRenderDateHeader) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -16,10 +16,15 @@ import {
|
||||||
getMessageSelector,
|
getMessageSelector,
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
import {
|
||||||
|
areMessagesInSameGroup,
|
||||||
|
shouldCurrentMessageHideMetadata,
|
||||||
|
UnreadIndicatorPlacement,
|
||||||
|
} from '../../util/timelineUtil';
|
||||||
|
|
||||||
import { SmartContactName } from './ContactName';
|
import { SmartContactName } from './ContactName';
|
||||||
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
|
import { isSameDay } from '../../util/timestamp';
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
|
@ -65,24 +70,52 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
|
||||||
const conversation = getConversationSelector(state)(conversationId);
|
const conversation = getConversationSelector(state)(conversationId);
|
||||||
|
|
||||||
|
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
|
||||||
|
|
||||||
|
const shouldCollapseAbove = areMessagesInSameGroup(
|
||||||
|
previousItem,
|
||||||
|
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustAbove,
|
||||||
|
item
|
||||||
|
);
|
||||||
|
const shouldCollapseBelow = areMessagesInSameGroup(
|
||||||
|
item,
|
||||||
|
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustBelow,
|
||||||
|
nextItem
|
||||||
|
);
|
||||||
|
const shouldHideMetadata = shouldCurrentMessageHideMetadata(
|
||||||
|
shouldCollapseBelow,
|
||||||
|
item,
|
||||||
|
nextItem
|
||||||
|
);
|
||||||
|
const shouldRenderDateHeader =
|
||||||
|
isOldestTimelineItem ||
|
||||||
|
Boolean(
|
||||||
|
item &&
|
||||||
|
previousItem &&
|
||||||
|
// This comparison avoids strange header behavior for out-of-order messages.
|
||||||
|
item.timestamp > previousItem.timestamp &&
|
||||||
|
!isSameDay(previousItem.timestamp, item.timestamp)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item,
|
item,
|
||||||
previousItem,
|
|
||||||
nextItem,
|
|
||||||
id: messageId,
|
id: messageId,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
conversationId,
|
conversationId,
|
||||||
conversationColor: conversation?.conversationColor,
|
conversationColor: conversation?.conversationColor,
|
||||||
customColor: conversation?.customColor,
|
customColor: conversation?.customColor,
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||||
isOldestTimelineItem,
|
isNextItemCallingNotification,
|
||||||
isSelected,
|
isSelected,
|
||||||
renderContact,
|
renderContact,
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
|
shouldCollapseAbove,
|
||||||
|
shouldCollapseBelow,
|
||||||
|
shouldHideMetadata,
|
||||||
|
shouldRenderDateHeader,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
interactionMode: getInteractionMode(state),
|
interactionMode: getInteractionMode(state),
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
unreadIndicatorPlacement,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import type { LastMessageStatus } from '../../model-types.d';
|
||||||
import { MINUTE, SECOND } from '../../util/durations';
|
import { MINUTE, SECOND } from '../../util/durations';
|
||||||
import type { MaybeMessageTimelineItemType } from '../../util/timelineUtil';
|
import type { MaybeMessageTimelineItemType } from '../../util/timelineUtil';
|
||||||
import {
|
import {
|
||||||
ScrollAnchor,
|
ScrollAnchor,
|
||||||
areMessagesInSameGroup,
|
areMessagesInSameGroup,
|
||||||
getScrollAnchorBeforeUpdate,
|
getScrollAnchorBeforeUpdate,
|
||||||
|
shouldCurrentMessageHideMetadata,
|
||||||
TimelineMessageLoadingState,
|
TimelineMessageLoadingState,
|
||||||
} from '../../util/timelineUtil';
|
} from '../../util/timelineUtil';
|
||||||
|
|
||||||
|
@ -118,60 +120,123 @@ describe('<Timeline> utilities', () => {
|
||||||
assert.isFalse(areMessagesInSameGroup(defaultOlder, true, defaultNewer));
|
assert.isFalse(areMessagesInSameGroup(defaultOlder, true, defaultNewer));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false if they don't have matching sent status (and not delivered)", () => {
|
|
||||||
const older = {
|
|
||||||
...defaultOlder,
|
|
||||||
data: { ...defaultOlder.data, status: 'sent' as const },
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isFalse(areMessagesInSameGroup(older, false, defaultNewer));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false if newer is deletedForEveryone and older isn't", () => {
|
|
||||||
const newer = {
|
|
||||||
...defaultNewer,
|
|
||||||
data: { ...defaultNewer.data, deletedForEveryone: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isFalse(areMessagesInSameGroup(defaultOlder, false, newer));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true if older is deletedForEveryone and newer isn't", () => {
|
|
||||||
const older = {
|
|
||||||
...defaultOlder,
|
|
||||||
data: { ...defaultOlder.data, deletedForEveryone: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isTrue(areMessagesInSameGroup(older, false, defaultNewer));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true if both are deletedForEveryone', () => {
|
|
||||||
const older = {
|
|
||||||
...defaultOlder,
|
|
||||||
data: { ...defaultOlder.data, deletedForEveryone: true },
|
|
||||||
};
|
|
||||||
const newer = {
|
|
||||||
...defaultNewer,
|
|
||||||
data: { ...defaultNewer.data, deletedForEveryone: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isTrue(areMessagesInSameGroup(older, false, newer));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true if they have delivered status or above', () => {
|
|
||||||
const older = {
|
|
||||||
...defaultOlder,
|
|
||||||
data: { ...defaultOlder.data, status: 'read' as const },
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isTrue(areMessagesInSameGroup(older, false, defaultNewer));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true if everything above works out', () => {
|
it('returns true if everything above works out', () => {
|
||||||
assert.isTrue(areMessagesInSameGroup(defaultOlder, false, defaultNewer));
|
assert.isTrue(areMessagesInSameGroup(defaultOlder, false, defaultNewer));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('shouldCurrentMessageHideMetadata', () => {
|
||||||
|
const defaultNewer: MaybeMessageTimelineItemType = {
|
||||||
|
type: 'message' as const,
|
||||||
|
data: {
|
||||||
|
author: { id: uuid() },
|
||||||
|
timestamp: new Date(1998, 10, 21, 12, 34, 56, 123).valueOf(),
|
||||||
|
status: 'delivered',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const defaultCurrent: MaybeMessageTimelineItemType = {
|
||||||
|
type: 'message' as const,
|
||||||
|
data: {
|
||||||
|
author: { id: uuid() },
|
||||||
|
timestamp: defaultNewer.data.timestamp - MINUTE,
|
||||||
|
status: 'delivered',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("returns false if messages aren't grouped", () => {
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(false, defaultCurrent, defaultNewer)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if newer item is missing', () => {
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(true, defaultCurrent, undefined)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if newer item is not a message', () => {
|
||||||
|
const linkNotification = {
|
||||||
|
type: 'linkNotification' as const,
|
||||||
|
data: null,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(true, defaultCurrent, linkNotification)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if newer is deletedForEveryone', () => {
|
||||||
|
const newer = {
|
||||||
|
...defaultNewer,
|
||||||
|
data: { ...defaultNewer.data, deletedForEveryone: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(true, defaultCurrent, newer)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if current message is unsent, even if its status matches the newer one', () => {
|
||||||
|
const statuses: ReadonlyArray<LastMessageStatus> = [
|
||||||
|
'paused',
|
||||||
|
'error',
|
||||||
|
'partial-sent',
|
||||||
|
'sending',
|
||||||
|
];
|
||||||
|
for (const status of statuses) {
|
||||||
|
const sameStatusNewer = {
|
||||||
|
...defaultNewer,
|
||||||
|
data: { ...defaultNewer.data, status },
|
||||||
|
};
|
||||||
|
const current = {
|
||||||
|
...defaultCurrent,
|
||||||
|
data: { ...defaultCurrent.data, status },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(true, current, defaultNewer)
|
||||||
|
);
|
||||||
|
assert.isFalse(
|
||||||
|
shouldCurrentMessageHideMetadata(true, current, sameStatusNewer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if all messages are sent (but no higher)', () => {
|
||||||
|
const newer = {
|
||||||
|
...defaultNewer,
|
||||||
|
data: { ...defaultNewer.data, status: 'sent' as const },
|
||||||
|
};
|
||||||
|
const current = {
|
||||||
|
...defaultCurrent,
|
||||||
|
data: { ...defaultCurrent.data, status: 'sent' as const },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isTrue(shouldCurrentMessageHideMetadata(true, current, newer));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if all three have delivered status or above', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
shouldCurrentMessageHideMetadata(true, defaultCurrent, defaultNewer)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if both the current and next messages are deleted for everyone', () => {
|
||||||
|
const current = {
|
||||||
|
...defaultCurrent,
|
||||||
|
data: { ...defaultCurrent.data, deletedForEveryone: true },
|
||||||
|
};
|
||||||
|
const newer = {
|
||||||
|
...defaultNewer,
|
||||||
|
data: { ...defaultNewer.data, deletedForEveryone: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isTrue(shouldCurrentMessageHideMetadata(true, current, newer));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getScrollAnchorBeforeUpdate', () => {
|
describe('getScrollAnchorBeforeUpdate', () => {
|
||||||
const fakeItems = (count: number) => times(count, () => uuid());
|
const fakeItems = (count: number) => times(count, () => uuid());
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
import * as log from '../logging/log';
|
||||||
import type { PropsType as TimelinePropsType } from '../components/conversation/Timeline';
|
import type { PropsType as TimelinePropsType } from '../components/conversation/Timeline';
|
||||||
import type { TimelineItemType } from '../components/conversation/TimelineItem';
|
import type { TimelineItemType } from '../components/conversation/TimelineItem';
|
||||||
import { WidthBreakpoint } from '../components/_util';
|
import { WidthBreakpoint } from '../components/_util';
|
||||||
|
@ -54,8 +55,52 @@ const getMessageTimelineItemData = (
|
||||||
): undefined | MessageTimelineItemDataType =>
|
): undefined | MessageTimelineItemDataType =>
|
||||||
timelineItem?.type === 'message' ? timelineItem.data : undefined;
|
timelineItem?.type === 'message' ? timelineItem.data : undefined;
|
||||||
|
|
||||||
function isDelivered(status?: LastMessageStatus) {
|
export function shouldCurrentMessageHideMetadata(
|
||||||
return status === 'delivered' || status === 'read' || status === 'viewed';
|
areMessagesGrouped: boolean,
|
||||||
|
item: MaybeMessageTimelineItemType,
|
||||||
|
newerTimelineItem: MaybeMessageTimelineItemType
|
||||||
|
): boolean {
|
||||||
|
if (!areMessagesGrouped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = getMessageTimelineItemData(item);
|
||||||
|
if (!message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newerMessage = getMessageTimelineItemData(newerTimelineItem);
|
||||||
|
if (!newerMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If newer message is deleted, but current isn't, we'll show metadata.
|
||||||
|
if (newerMessage.deletedForEveryone && !message.deletedForEveryone) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.status) {
|
||||||
|
case undefined:
|
||||||
|
return true;
|
||||||
|
case 'paused':
|
||||||
|
case 'error':
|
||||||
|
case 'partial-sent':
|
||||||
|
case 'sending':
|
||||||
|
return false;
|
||||||
|
case 'sent':
|
||||||
|
return newerMessage.status === 'sent';
|
||||||
|
case 'delivered':
|
||||||
|
case 'read':
|
||||||
|
case 'viewed':
|
||||||
|
return (
|
||||||
|
newerMessage.status === 'delivered' ||
|
||||||
|
newerMessage.status === 'read' ||
|
||||||
|
newerMessage.status === 'viewed'
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
log.error(missingCaseError(message.status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function areMessagesInSameGroup(
|
export function areMessagesInSameGroup(
|
||||||
|
@ -77,20 +122,12 @@ export function areMessagesInSameGroup(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We definitely don't want to group if we transition from non-deleted to deleted, since
|
|
||||||
// deleted messages don't show status.
|
|
||||||
if (newerMessage.deletedForEveryone && !olderMessage.deletedForEveryone) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
!olderMessage.reactions?.length &&
|
!olderMessage.reactions?.length &&
|
||||||
olderMessage.author.id === newerMessage.author.id &&
|
olderMessage.author.id === newerMessage.author.id &&
|
||||||
newerMessage.timestamp >= olderMessage.timestamp &&
|
newerMessage.timestamp >= olderMessage.timestamp &&
|
||||||
newerMessage.timestamp - olderMessage.timestamp < COLLAPSE_WITHIN &&
|
newerMessage.timestamp - olderMessage.timestamp < COLLAPSE_WITHIN &&
|
||||||
isSameDay(olderMessage.timestamp, newerMessage.timestamp) &&
|
isSameDay(olderMessage.timestamp, newerMessage.timestamp)
|
||||||
(olderMessage.status === newerMessage.status ||
|
|
||||||
(isDelivered(newerMessage.status) && isDelivered(olderMessage.status)))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue