parent
35d1451e42
commit
3026c2ff56
3 changed files with 234 additions and 198 deletions
|
@ -193,6 +193,7 @@ export enum GiftBadgeStates {
|
|||
Opened = 'Opened',
|
||||
Redeemed = 'Redeemed',
|
||||
}
|
||||
|
||||
export type GiftBadgeType = {
|
||||
expiration: number;
|
||||
id: string | undefined;
|
||||
|
@ -390,6 +391,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
current: false,
|
||||
};
|
||||
|
||||
private metadataRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
public reactionsContainerRefMerger = createRefMerger();
|
||||
|
||||
public expirationCheckInterval: NodeJS.Timeout | undefined;
|
||||
|
@ -823,6 +826,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
isTapToViewExpired={isTapToViewExpired}
|
||||
onWidthMeasured={isInline ? this.updateMetadataWidth : undefined}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
ref={this.metadataRef}
|
||||
retryMessageSend={retryMessageSend}
|
||||
showEditHistoryModal={showEditHistoryModal}
|
||||
status={status}
|
||||
|
@ -1779,7 +1783,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<div // eslint-disable-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
className={classNames(
|
||||
'module-message__text',
|
||||
`module-message__text--${direction}`,
|
||||
|
@ -1790,6 +1794,18 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
? 'module-message__text--delete-for-everyone'
|
||||
: null
|
||||
)}
|
||||
onClick={e => {
|
||||
// Prevent metadata from being selected on triple clicks.
|
||||
const clickCount = e.detail;
|
||||
const range = window.getSelection()?.getRangeAt(0);
|
||||
if (
|
||||
clickCount === 3 &&
|
||||
this.metadataRef.current &&
|
||||
range?.intersectsNode(this.metadataRef.current)
|
||||
) {
|
||||
range.setEndBefore(this.metadataRef.current);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(event: React.MouseEvent) => {
|
||||
// Prevent double-click interefering with interactions _inside_
|
||||
// the bubble.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactChild, ReactElement } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { ReactChild } from 'react';
|
||||
import React, { forwardRef, useCallback, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ContentRect } from 'react-measure';
|
||||
import Measure from 'react-measure';
|
||||
|
@ -16,6 +16,7 @@ import { MessageTimestamp } from './MessageTimestamp';
|
|||
import { PanelType } from '../../types/Panels';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { refMerger } from '../../util/refMerger';
|
||||
|
||||
type PropsType = {
|
||||
deletedForEveryone?: boolean;
|
||||
|
@ -43,46 +44,72 @@ enum ConfirmationType {
|
|||
EditError = 'EditError',
|
||||
}
|
||||
|
||||
export function MessageMetadata({
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
hasText,
|
||||
i18n,
|
||||
id,
|
||||
isEditedMessage,
|
||||
isInline,
|
||||
isShowingImage,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
onWidthMeasured,
|
||||
pushPanelForConversation,
|
||||
retryMessageSend,
|
||||
showEditHistoryModal,
|
||||
status,
|
||||
textPending,
|
||||
timestamp,
|
||||
}: Readonly<PropsType>): ReactElement {
|
||||
const [confirmationType, setConfirmationType] = useState<
|
||||
ConfirmationType | undefined
|
||||
>();
|
||||
const withImageNoCaption = Boolean(!isSticker && !hasText && isShowingImage);
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
||||
function MessageMetadataInner(
|
||||
{
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
hasText,
|
||||
i18n,
|
||||
id,
|
||||
isEditedMessage,
|
||||
isInline,
|
||||
isShowingImage,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
onWidthMeasured,
|
||||
pushPanelForConversation,
|
||||
retryMessageSend,
|
||||
showEditHistoryModal,
|
||||
status,
|
||||
textPending,
|
||||
timestamp,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const [confirmationType, setConfirmationType] = useState<
|
||||
ConfirmationType | undefined
|
||||
>();
|
||||
const withImageNoCaption = Boolean(
|
||||
!isSticker && !hasText && isShowingImage
|
||||
);
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
|
||||
let timestampNode: ReactChild;
|
||||
{
|
||||
const isError = status === 'error' && direction === 'outgoing';
|
||||
const isPartiallySent =
|
||||
status === 'partial-sent' && direction === 'outgoing';
|
||||
const isPaused = status === 'paused';
|
||||
let timestampNode: ReactChild;
|
||||
{
|
||||
const isError = status === 'error' && direction === 'outgoing';
|
||||
const isPartiallySent =
|
||||
status === 'partial-sent' && direction === 'outgoing';
|
||||
const isPaused = status === 'paused';
|
||||
|
||||
if (isError || isPartiallySent || isPaused) {
|
||||
let statusInfo: React.ReactChild;
|
||||
if (isError) {
|
||||
if (deletedForEveryone) {
|
||||
statusInfo = i18n('icu:deleteFailed');
|
||||
} else if (isEditedMessage) {
|
||||
if (isError || isPartiallySent || isPaused) {
|
||||
let statusInfo: React.ReactChild;
|
||||
if (isError) {
|
||||
if (deletedForEveryone) {
|
||||
statusInfo = i18n('icu:deleteFailed');
|
||||
} else if (isEditedMessage) {
|
||||
statusInfo = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-message__metadata__tapable"
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setConfirmationType(ConfirmationType.EditError);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:editFailed')}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
statusInfo = i18n('icu:sendFailed');
|
||||
}
|
||||
} else if (isPaused) {
|
||||
statusInfo = i18n('icu:sendPaused');
|
||||
} else {
|
||||
statusInfo = (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -91,179 +118,164 @@ export function MessageMetadata({
|
|||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setConfirmationType(ConfirmationType.EditError);
|
||||
pushPanelForConversation({
|
||||
type: PanelType.MessageDetails,
|
||||
args: { messageId: id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:editFailed')}
|
||||
{deletedForEveryone
|
||||
? i18n('icu:partiallyDeleted')
|
||||
: i18n('icu:partiallySent')}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
statusInfo = i18n('icu:sendFailed');
|
||||
}
|
||||
} else if (isPaused) {
|
||||
statusInfo = i18n('icu:sendPaused');
|
||||
} else {
|
||||
statusInfo = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-message__metadata__tapable"
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
pushPanelForConversation({
|
||||
type: PanelType.MessageDetails,
|
||||
args: { messageId: id },
|
||||
});
|
||||
}}
|
||||
timestampNode = (
|
||||
<span
|
||||
className={classNames({
|
||||
'module-message__metadata__date': true,
|
||||
'module-message__metadata__date--with-sticker': isSticker,
|
||||
'module-message__metadata__date--deleted-for-everyone':
|
||||
deletedForEveryone,
|
||||
[`module-message__metadata__date--${direction}`]: !isSticker,
|
||||
'module-message__metadata__date--with-image-no-caption':
|
||||
withImageNoCaption,
|
||||
})}
|
||||
>
|
||||
{deletedForEveryone
|
||||
? i18n('icu:partiallyDeleted')
|
||||
: i18n('icu:partiallySent')}
|
||||
</button>
|
||||
{statusInfo}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
timestampNode = (
|
||||
<MessageTimestamp
|
||||
deletedForEveryone={deletedForEveryone}
|
||||
direction={metadataDirection}
|
||||
i18n={i18n}
|
||||
module="module-message__metadata__date"
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
timestampNode = (
|
||||
<span
|
||||
className={classNames({
|
||||
'module-message__metadata__date': true,
|
||||
'module-message__metadata__date--with-sticker': isSticker,
|
||||
'module-message__metadata__date--deleted-for-everyone':
|
||||
deletedForEveryone,
|
||||
[`module-message__metadata__date--${direction}`]: !isSticker,
|
||||
'module-message__metadata__date--with-image-no-caption':
|
||||
withImageNoCaption,
|
||||
})}
|
||||
let confirmation: JSX.Element | undefined;
|
||||
if (confirmationType === undefined) {
|
||||
// no-op
|
||||
} else if (confirmationType === ConfirmationType.EditError) {
|
||||
confirmation = (
|
||||
<ConfirmationDialog
|
||||
dialogName="MessageMetadata.confirmEditResend"
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
retryMessageSend(id);
|
||||
setConfirmationType(undefined);
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n('icu:ResendMessageEdit__button'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmationType(undefined);
|
||||
}}
|
||||
>
|
||||
{statusInfo}
|
||||
</span>
|
||||
{i18n('icu:ResendMessageEdit__body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
} else {
|
||||
timestampNode = (
|
||||
<MessageTimestamp
|
||||
deletedForEveryone={deletedForEveryone}
|
||||
direction={metadataDirection}
|
||||
i18n={i18n}
|
||||
module="module-message__metadata__date"
|
||||
timestamp={timestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
throw missingCaseError(confirmationType);
|
||||
}
|
||||
|
||||
const className = classNames(
|
||||
'module-message__metadata',
|
||||
isInline && 'module-message__metadata--inline',
|
||||
withImageNoCaption && 'module-message__metadata--with-image-no-caption',
|
||||
deletedForEveryone && 'module-message__metadata--deleted-for-everyone'
|
||||
);
|
||||
const children = (
|
||||
<>
|
||||
{isEditedMessage && showEditHistoryModal && (
|
||||
<button
|
||||
className="module-message__metadata__edited"
|
||||
onClick={() => showEditHistoryModal(id)}
|
||||
type="button"
|
||||
>
|
||||
{i18n('icu:MessageMetadata__edited')}
|
||||
</button>
|
||||
)}
|
||||
{timestampNode}
|
||||
{expirationLength ? (
|
||||
<ExpireTimer
|
||||
direction={metadataDirection}
|
||||
deletedForEveryone={deletedForEveryone}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
) : null}
|
||||
{textPending ? (
|
||||
<div className="module-message__metadata__spinner-container">
|
||||
<Spinner svgSize="small" size="14px" direction={direction} />
|
||||
</div>
|
||||
) : null}
|
||||
{(!deletedForEveryone || status === 'sending') &&
|
||||
!textPending &&
|
||||
direction === 'outgoing' &&
|
||||
status !== 'error' &&
|
||||
status !== 'partial-sent' ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
isSticker
|
||||
? 'module-message__metadata__status-icon--with-sticker'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||
: null,
|
||||
deletedForEveryone
|
||||
? 'module-message__metadata__status-icon--deleted-for-everyone'
|
||||
: null,
|
||||
isTapToViewExpired
|
||||
? 'module-message__metadata__status-icon--with-tap-to-view-expired'
|
||||
: null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{confirmation}
|
||||
</>
|
||||
);
|
||||
|
||||
const onResize = useCallback(
|
||||
({ bounds }: ContentRect) => {
|
||||
onWidthMeasured?.(bounds?.width || 0);
|
||||
},
|
||||
[onWidthMeasured]
|
||||
);
|
||||
|
||||
if (onWidthMeasured) {
|
||||
return (
|
||||
<Measure bounds onResize={onResize}>
|
||||
{({ measureRef }) => (
|
||||
<div className={className} ref={refMerger(measureRef, ref)}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let confirmation: JSX.Element | undefined;
|
||||
if (confirmationType === undefined) {
|
||||
// no-op
|
||||
} else if (confirmationType === ConfirmationType.EditError) {
|
||||
confirmation = (
|
||||
<ConfirmationDialog
|
||||
dialogName="MessageMetadata.confirmEditResend"
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
retryMessageSend(id);
|
||||
setConfirmationType(undefined);
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n('icu:ResendMessageEdit__button'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmationType(undefined);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ResendMessageEdit__body')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
} else {
|
||||
throw missingCaseError(confirmationType);
|
||||
}
|
||||
|
||||
const className = classNames(
|
||||
'module-message__metadata',
|
||||
isInline && 'module-message__metadata--inline',
|
||||
withImageNoCaption && 'module-message__metadata--with-image-no-caption',
|
||||
deletedForEveryone && 'module-message__metadata--deleted-for-everyone'
|
||||
);
|
||||
const children = (
|
||||
<>
|
||||
{isEditedMessage && showEditHistoryModal && (
|
||||
<button
|
||||
className="module-message__metadata__edited"
|
||||
onClick={() => showEditHistoryModal(id)}
|
||||
type="button"
|
||||
>
|
||||
{i18n('icu:MessageMetadata__edited')}
|
||||
</button>
|
||||
)}
|
||||
{timestampNode}
|
||||
{expirationLength ? (
|
||||
<ExpireTimer
|
||||
direction={metadataDirection}
|
||||
deletedForEveryone={deletedForEveryone}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
) : null}
|
||||
{textPending ? (
|
||||
<div className="module-message__metadata__spinner-container">
|
||||
<Spinner svgSize="small" size="14px" direction={direction} />
|
||||
</div>
|
||||
) : null}
|
||||
{(!deletedForEveryone || status === 'sending') &&
|
||||
!textPending &&
|
||||
direction === 'outgoing' &&
|
||||
status !== 'error' &&
|
||||
status !== 'partial-sent' ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
isSticker
|
||||
? 'module-message__metadata__status-icon--with-sticker'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||
: null,
|
||||
deletedForEveryone
|
||||
? 'module-message__metadata__status-icon--deleted-for-everyone'
|
||||
: null,
|
||||
isTapToViewExpired
|
||||
? 'module-message__metadata__status-icon--with-tap-to-view-expired'
|
||||
: null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{confirmation}
|
||||
</>
|
||||
);
|
||||
|
||||
const onResize = useCallback(
|
||||
({ bounds }: ContentRect) => {
|
||||
onWidthMeasured?.(bounds?.width || 0);
|
||||
},
|
||||
[onWidthMeasured]
|
||||
);
|
||||
|
||||
if (onWidthMeasured) {
|
||||
return (
|
||||
<Measure bounds onResize={onResize}>
|
||||
{({ measureRef }) => (
|
||||
<div className={className} ref={measureRef}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
<div className={className} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -2391,6 +2391,14 @@
|
|||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " private metadataRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-30T22:12:49.259Z",
|
||||
"reasonDetail": "Used for excluding the message metadata from triple-click selections."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/MessageDetail.tsx",
|
||||
|
|
Loading…
Reference in a new issue