Reintroduce inline metadata with full RTL support

This commit is contained in:
Scott Nonnenberg 2022-03-23 13:23:28 -07:00 committed by GitHub
parent 801c70b298
commit bb066d4a84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 185 additions and 9 deletions

View file

@ -12,7 +12,7 @@ import { SignalService } from '../../protobuf';
import { ConversationColors } from '../../types/Colors';
import { EmojiPicker } from '../emoji/EmojiPicker';
import type { Props, AudioAttachmentProps } from './Message';
import { Message } from './Message';
import { TextDirection, Message } from './Message';
import {
AUDIO_MP3,
IMAGE_JPEG,
@ -184,6 +184,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showVisualAttachment: action('showVisualAttachment'),
status: overrideProps.status || 'sent',
text: overrideProps.text || text('text', ''),
textDirection: overrideProps.textDirection || TextDirection.Default,
textPending: boolean('textPending', overrideProps.textPending || false),
theme: ThemeType.light,
timestamp: number('timestamp', overrideProps.timestamp || Date.now()),
@ -228,6 +229,7 @@ story.add('Plain Message', () => {
story.add('Plain RTL Message', () => {
const props = createProps({
text: 'الأسانسير، علشان القطط ماتاكلش منها. وننساها، ونعود الى أوراقنا موصدين الباب بإحكام. نتنحنح، ونقول: البتاع. كلمة تدلّ على لا شيء، وعلى كلّ شيء. وهي مركز أبحاث شعبية كثيرة، تتعجّب من غرابتها والقومية المصرية الخاصة التي تعكسها، الى جانب الشيء الكثير من العفوية وحلاوة الروح. نعم، نحن قرأنا وسمعنا وعرفنا كل هذا. لكنه محلّ اهتمامنا اليوم لأسباب غير تلك الأسباب. كذلك، فإننا لعاقدون عزمنا على أن نتجاوز قضية الفصحى والعامية، وثنائية النخبة والرعاع، التي كثيراً ما ينحو نحوها الحديث عن الكلمة المذكورة. وفوق هذا كله، لسنا بصدد تفسير معاني "البتاع" كما تأتي في قصيدة الحاج أحمد فؤاد نجم، ولا التحذلق والتفذلك في الألغاز والأسرار المكنونة. هذا البتاع - أم هذه البت',
textDirection: TextDirection.RightToLeft,
});
return renderBothDirections(props);

View file

@ -25,6 +25,7 @@ import {
MessageBodyReadMore,
} from './MessageBodyReadMore';
import { MessageMetadata } from './MessageMetadata';
import { MessageTextMetadataSpacer } from './MessageTextMetadataSpacer';
import { ImageGrid } from './ImageGrid';
import { GIF } from './GIF';
import { Image } from './Image';
@ -91,6 +92,19 @@ type Trigger = {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
};
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
const GUESS_METADATA_WIDTH_OUTGOING_SIZE: Record<MessageStatusType, number> = {
delivered: 24,
error: 24,
paused: 18,
'partial-sent': 24,
read: 24,
sending: 18,
sent: 24,
viewed: 24,
};
const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600;
const GROUP_AVATAR_SIZE = AvatarSize.TWENTY_EIGHT;
@ -109,9 +123,17 @@ const SENT_STATUSES = new Set<MessageStatusType>([
enum MetadataPlacement {
NotRendered,
RenderedByMessageAudioComponent,
InlineWithText,
Bottom,
}
export enum TextDirection {
LeftToRight = 'LeftToRight',
RightToLeft = 'RightToLeft',
Default = 'Default',
None = 'None',
}
export const MessageStatuses = [
'delivered',
'error',
@ -161,6 +183,7 @@ export type PropsData = {
conversationId: string;
displayLimit?: number;
text?: string;
textDirection: TextDirection;
textPending?: boolean;
isSticker?: boolean;
isSelected?: boolean;
@ -318,6 +341,8 @@ export type Props = PropsData &
Pick<ReactionPickerProps, 'renderEmojiPicker'>;
type State = {
metadataWidth: number;
expiring: boolean;
expired: boolean;
imageBroken: boolean;
@ -355,6 +380,8 @@ export class Message extends React.PureComponent<Props, State> {
super(props);
this.state = {
metadataWidth: this.guessMetadataWidth(),
expiring: false,
expired: false,
imageBroken: false,
@ -529,8 +556,11 @@ export class Message extends React.PureComponent<Props, State> {
expirationTimestamp,
status,
text,
textDirection,
}: Readonly<Props> = this.props
): MetadataPlacement {
const isRTL = textDirection === TextDirection.RightToLeft;
if (
!expirationLength &&
!expirationTimestamp &&
@ -550,7 +580,37 @@ export class Message extends React.PureComponent<Props, State> {
return MetadataPlacement.Bottom;
}
return MetadataPlacement.Bottom;
if (isRTL) {
return MetadataPlacement.Bottom;
}
return MetadataPlacement.InlineWithText;
}
/**
* A lot of the time, we add an invisible inline spacer for messages. This spacer is the
* same size as the message metadata. Unfortunately, we don't know how wide it is until
* we render it.
*
* This will probably guess wrong, but it's valuable to get close to the real value
* because it can reduce layout jumpiness.
*/
private guessMetadataWidth(): number {
const { direction, expirationLength, expirationTimestamp, status } =
this.props;
let result = GUESS_METADATA_WIDTH_TIMESTAMP_SIZE;
const hasExpireTimer = Boolean(expirationLength && expirationTimestamp);
if (hasExpireTimer) {
result += GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE;
}
if (direction === 'outgoing' && status) {
result += GUESS_METADATA_WIDTH_OUTGOING_SIZE[status];
}
return result;
}
public startSelectedTimer(): void {
@ -672,13 +732,31 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private updateMetadataWidth = (newMetadataWidth: number): void => {
this.setState(({ metadataWidth }) => ({
// We don't want text to jump around if the metadata shrinks, but we want to make
// sure we have enough room.
metadataWidth: Math.max(metadataWidth, newMetadataWidth),
}));
};
private renderMetadata(): ReactNode {
let isInline: boolean;
const metadataPlacement = this.getMetadataPlacement();
if (
metadataPlacement === MetadataPlacement.NotRendered ||
metadataPlacement === MetadataPlacement.RenderedByMessageAudioComponent
) {
return null;
switch (metadataPlacement) {
case MetadataPlacement.NotRendered:
case MetadataPlacement.RenderedByMessageAudioComponent:
return null;
case MetadataPlacement.InlineWithText:
isInline = true;
break;
case MetadataPlacement.Bottom:
isInline = false;
break;
default:
log.error(missingCaseError(metadataPlacement));
isInline = false;
break;
}
const {
@ -708,9 +786,11 @@ export class Message extends React.PureComponent<Props, State> {
hasText={Boolean(text)}
i18n={i18n}
id={id}
isInline={isInline}
isShowingImage={this.isShowingImage()}
isSticker={isStickerLike}
isTapToViewExpired={isTapToViewExpired}
onWidthMeasured={isInline ? this.updateMetadataWidth : undefined}
showMessageDetail={showMessageDetail}
status={status}
textPending={textPending}
@ -1378,8 +1458,11 @@ export class Message extends React.PureComponent<Props, State> {
openConversation,
status,
text,
textDirection,
textPending,
} = this.props;
const { metadataWidth } = this.state;
const isRTL = textDirection === TextDirection.RightToLeft;
// eslint-disable-next-line no-nested-ternary
const contents = deletedForEveryone
@ -1401,7 +1484,7 @@ export class Message extends React.PureComponent<Props, State> {
? 'module-message__text--error'
: null
)}
dir="auto"
dir={isRTL ? 'rtl' : undefined}
>
<MessageBodyReadMore
bodyRanges={bodyRanges}
@ -1415,6 +1498,10 @@ export class Message extends React.PureComponent<Props, State> {
text={contents || ''}
textPending={textPending}
/>
{!isRTL &&
this.getMetadataPlacement() === MetadataPlacement.InlineWithText && (
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
)}
</div>
);
}

View file

@ -8,6 +8,7 @@ import { number } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import type { PropsData as MessageDataPropsType } from './Message';
import { TextDirection } from './Message';
import type { Props } from './MessageDetail';
import { MessageDetail } from './MessageDetail';
import { SendStatus } from '../../messages/MessageSendState';
@ -45,6 +46,7 @@ const defaultMessage: MessageDataPropsType = {
readStatus: ReadStatus.Read,
status: 'sent',
text: 'A message from Max',
textDirection: TextDirection.Default,
timestamp: Date.now(),
};

View file

@ -0,0 +1,13 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
const SPACING = 10;
export const MessageTextMetadataSpacer = ({
metadataWidth,
}: Readonly<{ metadataWidth: number }>): ReactElement => (
<span style={{ display: 'inline-block', width: metadataWidth + SPACING }} />
);

View file

@ -11,7 +11,7 @@ import { storiesOf } from '@storybook/react';
import { ConversationColors } from '../../types/Colors';
import { pngUrl } from '../../storybook/Fixtures';
import type { Props as MessagesProps } from './Message';
import { Message } from './Message';
import { Message, TextDirection } from './Message';
import {
AUDIO_MP3,
IMAGE_PNG,
@ -95,6 +95,7 @@ const defaultMessageProps: MessagesProps = {
showVisualAttachment: action('default--showVisualAttachment'),
status: 'sent',
text: 'This is really interesting.',
textDirection: TextDirection.Default,
theme: ThemeType.light,
timestamp: Date.now(),
};

View file

@ -24,6 +24,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import { ReadStatus } from '../../messages/MessageReadStatus';
import type { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { TextDirection } from './Message';
const i18n = setupI18n('en', enMessages);
@ -60,6 +61,7 @@ const items: Record<string, TimelineItemType> = {
previews: [],
readStatus: ReadStatus.Read,
text: '🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -84,6 +86,7 @@ const items: Record<string, TimelineItemType> = {
previews: [],
readStatus: ReadStatus.Read,
text: 'Hello there from the new world! http://somewhere.com',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -122,6 +125,7 @@ const items: Record<string, TimelineItemType> = {
previews: [],
readStatus: ReadStatus.Read,
text: 'Hello there from the new world!',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -222,6 +226,7 @@ const items: Record<string, TimelineItemType> = {
readStatus: ReadStatus.Read,
status: 'sent',
text: '🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -247,6 +252,7 @@ const items: Record<string, TimelineItemType> = {
readStatus: ReadStatus.Read,
status: 'read',
text: 'Hello there from the new world! http://somewhere.com',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -272,6 +278,7 @@ const items: Record<string, TimelineItemType> = {
readStatus: ReadStatus.Read,
status: 'sent',
text: 'Hello there from the new world! 🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -297,6 +304,7 @@ const items: Record<string, TimelineItemType> = {
readStatus: ReadStatus.Read,
status: 'sent',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
@ -322,6 +330,7 @@ const items: Record<string, TimelineItemType> = {
readStatus: ReadStatus.Read,
status: 'read',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),