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 = () => ({
|
||||
conversationId: 'fake-conversation-id',
|
||||
i18n,
|
||||
isNextItemCallingNotification: false,
|
||||
messageId: 'fake-message-id',
|
||||
nextItem: undefined,
|
||||
now: Date.now(),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
startCallingLobby: action('startCallingLobby'),
|
||||
|
@ -70,7 +70,7 @@ story.add('Two incoming direct calls back-to-back', () => {
|
|||
<CallingNotification
|
||||
{...getCommonProps()}
|
||||
{...call1}
|
||||
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
|
||||
isNextItemCallingNotification
|
||||
/>
|
||||
<CallingNotification {...getCommonProps()} {...call2} />
|
||||
</>
|
||||
|
@ -99,7 +99,7 @@ story.add('Two outgoing direct calls back-to-back', () => {
|
|||
<CallingNotification
|
||||
{...getCommonProps()}
|
||||
{...call1}
|
||||
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
|
||||
isNextItemCallingNotification
|
||||
/>
|
||||
<CallingNotification {...getCommonProps()} {...call2} />
|
||||
</>
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
} from '../../util/callingNotification';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
export type PropsActionsType = {
|
||||
|
@ -31,7 +30,7 @@ export type PropsActionsType = {
|
|||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
conversationId: string;
|
||||
nextItem: undefined | TimelineItemType;
|
||||
isNextItemCallingNotification: boolean;
|
||||
};
|
||||
|
||||
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
||||
|
@ -86,12 +85,12 @@ function renderCallingNotificationButton(
|
|||
activeCallConversationId,
|
||||
conversationId,
|
||||
i18n,
|
||||
nextItem,
|
||||
isNextItemCallingNotification,
|
||||
returnToActiveCall,
|
||||
startCallingLobby,
|
||||
} = props;
|
||||
|
||||
if (nextItem?.type === 'callHistory') {
|
||||
if (isNextItemCallingNotification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -171,6 +171,15 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
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'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
|
@ -202,9 +211,9 @@ const renderMany = (propsArray: ReadonlyArray<Props>) =>
|
|||
<Message
|
||||
key={message.text}
|
||||
{...message}
|
||||
previousItem={createTimelineItem(propsArray[index - 1])}
|
||||
shouldCollapseAbove={Boolean(propsArray[index - 1])}
|
||||
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 * as KeyboardLayout from '../../services/keyboardLayout';
|
||||
import { StopPropagation } from '../StopPropagation';
|
||||
import {
|
||||
areMessagesInSameGroup,
|
||||
UnreadIndicatorPlacement,
|
||||
} from '../../util/timelineUtil';
|
||||
|
||||
type Trigger = {
|
||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
|
@ -269,14 +265,14 @@ export type PropsHousekeeping = {
|
|||
i18n: LocalizerType;
|
||||
interactionMode: InteractionModeType;
|
||||
item?: TimelineItemType;
|
||||
nextItem?: TimelineItemType;
|
||||
previousItem?: TimelineItemType;
|
||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
||||
renderReactionPicker: (
|
||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||
) => JSX.Element;
|
||||
shouldCollapseAbove: boolean;
|
||||
shouldCollapseBelow: boolean;
|
||||
shouldHideMetadata: boolean;
|
||||
theme: ThemeType;
|
||||
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
|
||||
};
|
||||
|
||||
export type PropsActions = {
|
||||
|
@ -554,6 +550,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
attachments,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
shouldHideMetadata,
|
||||
status,
|
||||
text,
|
||||
textDirection,
|
||||
|
@ -565,7 +562,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
!expirationLength &&
|
||||
!expirationTimestamp &&
|
||||
(!status || SENT_STATUSES.has(status)) &&
|
||||
this.isCollapsedBelow()
|
||||
shouldHideMetadata
|
||||
) {
|
||||
return MetadataPlacement.NotRendered;
|
||||
}
|
||||
|
@ -688,34 +685,14 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
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 {
|
||||
const { author, conversationType, direction } = this.props;
|
||||
const { author, conversationType, direction, shouldCollapseAbove } =
|
||||
this.props;
|
||||
return Boolean(
|
||||
direction === 'incoming' &&
|
||||
conversationType === 'group' &&
|
||||
author.title &&
|
||||
!this.isCollapsedAbove()
|
||||
!shouldCollapseAbove
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -850,6 +827,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
renderingContext,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
status,
|
||||
text,
|
||||
textPending,
|
||||
|
@ -925,10 +904,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<ImageGrid
|
||||
attachments={attachments}
|
||||
withContentAbove={
|
||||
isSticker || withContentAbove || this.isCollapsedAbove()
|
||||
isSticker || withContentAbove || shouldCollapseAbove
|
||||
}
|
||||
withContentBelow={
|
||||
isSticker || withContentBelow || this.isCollapsedBelow()
|
||||
isSticker || withContentBelow || shouldCollapseBelow
|
||||
}
|
||||
isSticker={isSticker}
|
||||
stickerSize={STICKER_SIZE}
|
||||
|
@ -1223,6 +1202,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
id,
|
||||
quote,
|
||||
scrollToQuotedMessage,
|
||||
shouldCollapseAbove,
|
||||
} = this.props;
|
||||
|
||||
if (!quote) {
|
||||
|
@ -1248,11 +1228,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
curveTopLeft = false;
|
||||
curveTopRight = false;
|
||||
} else if (isIncoming) {
|
||||
curveTopLeft = !this.isCollapsedAbove();
|
||||
curveTopLeft = !shouldCollapseAbove;
|
||||
curveTopRight = true;
|
||||
} else {
|
||||
curveTopLeft = true;
|
||||
curveTopRight = !this.isCollapsedAbove();
|
||||
curveTopRight = !shouldCollapseAbove;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1285,6 +1265,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
direction,
|
||||
i18n,
|
||||
storyReplyContext,
|
||||
shouldCollapseAbove,
|
||||
} = this.props;
|
||||
|
||||
if (!storyReplyContext) {
|
||||
|
@ -1299,11 +1280,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
curveTopLeft = false;
|
||||
curveTopRight = false;
|
||||
} else if (isIncoming) {
|
||||
curveTopLeft = !this.isCollapsedAbove();
|
||||
curveTopLeft = !shouldCollapseAbove;
|
||||
curveTopRight = true;
|
||||
} else {
|
||||
curveTopLeft = true;
|
||||
curveTopRight = !this.isCollapsedAbove();
|
||||
curveTopRight = !shouldCollapseAbove;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1400,6 +1381,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
direction,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
shouldCollapseBelow,
|
||||
showContactModal,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
@ -1415,7 +1397,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
this.hasReactions(),
|
||||
})}
|
||||
>
|
||||
{this.isCollapsedBelow() ? (
|
||||
{shouldCollapseBelow ? (
|
||||
<AvatarSpacer size={GROUP_AVATAR_SIZE} />
|
||||
) : (
|
||||
<Avatar
|
||||
|
@ -2660,8 +2642,16 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public override render(): JSX.Element | null {
|
||||
const { author, attachments, direction, id, isSticker, timestamp } =
|
||||
this.props;
|
||||
const {
|
||||
author,
|
||||
attachments,
|
||||
direction,
|
||||
id,
|
||||
isSticker,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
timestamp,
|
||||
} = this.props;
|
||||
const { expired, expiring, imageBroken, isSelected } = this.state;
|
||||
|
||||
// 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(
|
||||
'module-message',
|
||||
`module-message--${direction}`,
|
||||
this.isCollapsedAbove() && 'module-message--collapsed-above',
|
||||
this.isCollapsedBelow() && 'module-message--collapsed-below',
|
||||
shouldCollapseAbove && 'module-message--collapsed-above',
|
||||
shouldCollapseBelow && 'module-message--collapsed-below',
|
||||
isSelected ? 'module-message--selected' : null,
|
||||
expiring ? 'module-message--expired' : null
|
||||
)}
|
||||
|
|
|
@ -345,6 +345,9 @@ export class MessageDetail extends React.Component<Props> {
|
|||
replyToMessage={replyToMessage}
|
||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||
retrySend={retrySend}
|
||||
shouldCollapseAbove={false}
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
showForwardMessageModal={showForwardMessageModal}
|
||||
scrollToQuotedMessage={() => {
|
||||
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
||||
|
|
|
@ -82,6 +82,9 @@ const defaultMessageProps: MessagesProps = {
|
|||
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
|
||||
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||
selectMessage: action('default--selectMessage'),
|
||||
shouldCollapseAbove: false,
|
||||
shouldCollapseBelow: false,
|
||||
shouldHideMetadata: false,
|
||||
showContactDetail: action('default--showContactDetail'),
|
||||
showContactModal: action('default--showContactModal'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
|
|
|
@ -423,25 +423,21 @@ const renderItem = ({
|
|||
messageId,
|
||||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
isOldestTimelineItem,
|
||||
}: {
|
||||
messageId: string;
|
||||
containerElementRef: React.RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
isOldestTimelineItem: boolean;
|
||||
}) => (
|
||||
<TimelineItem
|
||||
getPreferredBadge={() => undefined}
|
||||
id=""
|
||||
isOldestTimelineItem={isOldestTimelineItem}
|
||||
isSelected={false}
|
||||
renderEmojiPicker={() => <div />}
|
||||
renderReactionPicker={() => <div />}
|
||||
item={items[messageId]}
|
||||
previousItem={undefined}
|
||||
nextItem={undefined}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
isNextItemCallingNotification={false}
|
||||
theme={ThemeType.light}
|
||||
containerElementRef={containerElementRef}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
|
@ -451,6 +447,10 @@ const renderItem = ({
|
|||
<div>*UniversalTimerNotification*</div>
|
||||
)}
|
||||
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
||||
shouldCollapseAbove={false}
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
shouldRenderDateHeader={false}
|
||||
{...actions()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -53,7 +53,7 @@ const getDefaultProps = () => ({
|
|||
conversationId: 'conversation-id',
|
||||
getPreferredBadge: () => undefined,
|
||||
id: 'asdf',
|
||||
isOldestTimelineItem: false,
|
||||
isNextItemCallingNotification: false,
|
||||
isSelected: false,
|
||||
interactionMode: 'keyboard' as const,
|
||||
theme: ThemeType.light,
|
||||
|
@ -94,8 +94,11 @@ const getDefaultProps = () => ({
|
|||
showIdentity: action('showIdentity'),
|
||||
startCallingLobby: action('startCallingLobby'),
|
||||
returnToActiveCall: action('returnToActiveCall'),
|
||||
previousItem: undefined,
|
||||
nextItem: undefined,
|
||||
shouldCollapseAbove: false,
|
||||
shouldCollapseBelow: false,
|
||||
shouldHideMetadata: false,
|
||||
shouldRenderDateHeader: false,
|
||||
|
||||
now: Date.now(),
|
||||
|
||||
renderContact,
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { ReactChild, RefObject } from 'react';
|
|||
import React from 'react';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { isSameDay } from '../../util/timestamp';
|
||||
|
||||
import type { InteractionModeType } from '../../state/ducks/conversations';
|
||||
import { TimelineDateHeader } from './TimelineDateHeader';
|
||||
|
@ -56,7 +55,6 @@ import type { SmartContactRendererType } from '../../groupChange';
|
|||
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
|
||||
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
||||
import type { FullJSXType } from '../Intl';
|
||||
|
||||
type CallHistoryType = {
|
||||
|
@ -148,17 +146,15 @@ type PropsLocalType = {
|
|||
conversationId: string;
|
||||
item?: TimelineItemType;
|
||||
id: string;
|
||||
isNextItemCallingNotification: boolean;
|
||||
isSelected: boolean;
|
||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||
shouldRenderDateHeader: boolean;
|
||||
renderContact: SmartContactRendererType<FullJSXType>;
|
||||
renderUniversalTimerNotification: () => JSX.Element;
|
||||
i18n: LocalizerType;
|
||||
interactionMode: InteractionModeType;
|
||||
isOldestTimelineItem: boolean;
|
||||
theme: ThemeType;
|
||||
previousItem: undefined | TimelineItemType;
|
||||
nextItem: undefined | TimelineItemType;
|
||||
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
|
||||
};
|
||||
|
||||
type PropsActionsType = MessageActionsType &
|
||||
|
@ -178,6 +174,9 @@ export type PropsType = PropsLocalType &
|
|||
| 'renderEmojiPicker'
|
||||
| 'renderAudioAttachment'
|
||||
| 'renderReactionPicker'
|
||||
| 'shouldCollapseAbove'
|
||||
| 'shouldCollapseBelow'
|
||||
| 'shouldHideMetadata'
|
||||
>;
|
||||
|
||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||
|
@ -186,19 +185,20 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
containerElementRef,
|
||||
conversationId,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
id,
|
||||
isOldestTimelineItem,
|
||||
isNextItemCallingNotification,
|
||||
isSelected,
|
||||
item,
|
||||
i18n,
|
||||
theme,
|
||||
nextItem,
|
||||
previousItem,
|
||||
renderUniversalTimerNotification,
|
||||
returnToActiveCall,
|
||||
selectMessage,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
shouldHideMetadata,
|
||||
shouldRenderDateHeader,
|
||||
startCallingLobby,
|
||||
unreadIndicatorPlacement,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!item) {
|
||||
|
@ -217,12 +217,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
<Message
|
||||
{...this.props}
|
||||
{...item.data}
|
||||
shouldCollapseAbove={shouldCollapseAbove}
|
||||
shouldCollapseBelow={shouldCollapseBelow}
|
||||
shouldHideMetadata={shouldHideMetadata}
|
||||
containerElementRef={containerElementRef}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
renderingContext="conversation/TimelineItem"
|
||||
unreadIndicatorPlacement={unreadIndicatorPlacement}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -237,7 +239,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
<CallingNotification
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
nextItem={nextItem}
|
||||
isNextItemCallingNotification={isNextItemCallingNotification}
|
||||
returnToActiveCall={returnToActiveCall}
|
||||
startCallingLobby={startCallingLobby}
|
||||
{...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) {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -16,10 +16,15 @@ import {
|
|||
getMessageSelector,
|
||||
getSelectedMessage,
|
||||
} from '../selectors/conversations';
|
||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
||||
import {
|
||||
areMessagesInSameGroup,
|
||||
shouldCurrentMessageHideMetadata,
|
||||
UnreadIndicatorPlacement,
|
||||
} from '../../util/timelineUtil';
|
||||
|
||||
import { SmartContactName } from './ContactName';
|
||||
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
||||
import { isSameDay } from '../../util/timestamp';
|
||||
|
||||
type ExternalProps = {
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
|
@ -65,24 +70,52 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
|
||||
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 {
|
||||
item,
|
||||
previousItem,
|
||||
nextItem,
|
||||
id: messageId,
|
||||
containerElementRef,
|
||||
conversationId,
|
||||
conversationColor: conversation?.conversationColor,
|
||||
customColor: conversation?.customColor,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
isOldestTimelineItem,
|
||||
isNextItemCallingNotification,
|
||||
isSelected,
|
||||
renderContact,
|
||||
renderUniversalTimerNotification,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
shouldHideMetadata,
|
||||
shouldRenderDateHeader,
|
||||
i18n: getIntl(state),
|
||||
interactionMode: getInteractionMode(state),
|
||||
theme: getTheme(state),
|
||||
unreadIndicatorPlacement,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
import { assert } from 'chai';
|
||||
import { times } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { LastMessageStatus } from '../../model-types.d';
|
||||
import { MINUTE, SECOND } from '../../util/durations';
|
||||
import type { MaybeMessageTimelineItemType } from '../../util/timelineUtil';
|
||||
import {
|
||||
ScrollAnchor,
|
||||
areMessagesInSameGroup,
|
||||
getScrollAnchorBeforeUpdate,
|
||||
shouldCurrentMessageHideMetadata,
|
||||
TimelineMessageLoadingState,
|
||||
} from '../../util/timelineUtil';
|
||||
|
||||
|
@ -118,60 +120,123 @@ describe('<Timeline> utilities', () => {
|
|||
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', () => {
|
||||
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', () => {
|
||||
const fakeItems = (count: number) => times(count, () => uuid());
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
import * as log from '../logging/log';
|
||||
import type { PropsType as TimelinePropsType } from '../components/conversation/Timeline';
|
||||
import type { TimelineItemType } from '../components/conversation/TimelineItem';
|
||||
import { WidthBreakpoint } from '../components/_util';
|
||||
|
@ -54,8 +55,52 @@ const getMessageTimelineItemData = (
|
|||
): undefined | MessageTimelineItemDataType =>
|
||||
timelineItem?.type === 'message' ? timelineItem.data : undefined;
|
||||
|
||||
function isDelivered(status?: LastMessageStatus) {
|
||||
return status === 'delivered' || status === 'read' || status === 'viewed';
|
||||
export function shouldCurrentMessageHideMetadata(
|
||||
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(
|
||||
|
@ -77,20 +122,12 @@ export function areMessagesInSameGroup(
|
|||
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(
|
||||
!olderMessage.reactions?.length &&
|
||||
olderMessage.author.id === newerMessage.author.id &&
|
||||
newerMessage.timestamp >= olderMessage.timestamp &&
|
||||
newerMessage.timestamp - olderMessage.timestamp < COLLAPSE_WITHIN &&
|
||||
isSameDay(olderMessage.timestamp, newerMessage.timestamp) &&
|
||||
(olderMessage.status === newerMessage.status ||
|
||||
(isDelivered(newerMessage.status) && isDelivered(olderMessage.status)))
|
||||
isSameDay(olderMessage.timestamp, newerMessage.timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue