Improve layout of various message bubbles

This commit is contained in:
Scott Nonnenberg 2022-04-07 09:58:15 -07:00 committed by GitHub
parent 933c07c9ce
commit b50c96c0b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 25 deletions

View File

@ -443,6 +443,22 @@
} }
} }
.module-message__container--deleted-for-everyone {
@include light-theme {
color: $color-gray-90;
border: 1px solid $color-gray-25;
background-color: $color-white;
background-image: none;
}
@include dark-theme {
color: $color-gray-05;
border: 1px solid $color-gray-75;
background-color: $color-gray-95;
background-image: none;
}
}
.module-message__tap-to-view { .module-message__tap-to-view {
margin-top: 2px; margin-top: 2px;
display: flex; display: flex;
@ -992,6 +1008,14 @@
.module-message__text--error { .module-message__text--error {
@include font-body-1-italic; @include font-body-1-italic;
} }
.module-message__text--delete-for-everyone {
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
.module-message__metadata { .module-message__metadata {
align-items: center; align-items: center;
@ -999,6 +1023,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
margin-top: 3px; margin-top: 3px;
font-style: normal;
&--inline { &--inline {
float: right; float: right;
@ -1021,6 +1046,15 @@
pointer-events: none; pointer-events: none;
} }
.module-message__metadata--deleted-for-everyone {
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-message__metadata__date { .module-message__metadata__date {
@include font-caption; @include font-caption;
user-select: none; user-select: none;
@ -1054,6 +1088,14 @@
color: $color-white-alpha-80; color: $color-white-alpha-80;
} }
} }
.module-message__metadata__date--deleted-for-everyone {
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-message__metadata__date.module-message__metadata__date--incoming-with-tap-to-view-expired { .module-message__metadata__date.module-message__metadata__date--incoming-with-tap-to-view-expired {
color: $color-gray-75; color: $color-gray-75;
@ -1076,6 +1118,8 @@
height: 12px; height: 12px;
display: inline-block; display: inline-block;
margin-left: 6px; margin-left: 6px;
// High margin to leave space for the increase when we go to two checks
margin-right: 6px;
margin-bottom: 2px; margin-bottom: 2px;
} }
@ -1102,6 +1146,8 @@
} }
} }
.module-message__metadata__status-icon--delivered { .module-message__metadata__status-icon--delivered {
// We reduce the margin size to keep the overall width the same
margin-right: 0px;
width: 18px; width: 18px;
@include light-theme { @include light-theme {
@ -1113,6 +1159,8 @@
} }
.module-message__metadata__status-icon--read, .module-message__metadata__status-icon--read,
.module-message__metadata__status-icon--viewed { .module-message__metadata__status-icon--viewed {
// We reduce the margin size to keep the overall width the same
margin-right: 0px;
width: 18px; width: 18px;
@include light-theme { @include light-theme {
@ -1138,6 +1186,15 @@
} }
} }
.module-message__metadata__status-icon--deleted-for-everyone {
@include light-theme {
background-color: $color-gray-60;
}
@include dark-theme {
background-color: $color-gray-25;
}
}
.module-message__metadata__spinner-container { .module-message__metadata__spinner-container {
margin-left: 6px; margin-left: 6px;
} }
@ -1367,6 +1424,15 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
.module-expire-timer--deleted-for-everyone {
@include light-theme {
background-color: $color-gray-60;
}
@include dark-theme {
background-color: $color-gray-25;
}
}
.module-about { .module-about {
&__container { &__container {
margin-left: auto; margin-left: auto;
@ -7845,6 +7911,8 @@ button.module-image__border-overlay:focus {
// To limit messages with things forcing them wider, like long attachment names // To limit messages with things forcing them wider, like long attachment names
.module-message__container { .module-message__container {
max-width: 100%;
&--incoming { &--incoming {
align-self: flex-start; align-self: flex-start;
} }

View File

@ -17,6 +17,7 @@ import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment'; import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment';
import { landscapeGreenUrl } from '../storybook/Fixtures'; import { landscapeGreenUrl } from '../storybook/Fixtures';
import { RecordingState } from '../state/ducks/audioRecorder'; import { RecordingState } from '../state/ducks/audioRecorder';
import { ConversationColors } from '../types/Colors';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -183,3 +184,18 @@ story.add('Announcements Only group', () => (
})} })}
/> />
)); ));
story.add('Quote', () => (
<CompositionArea
{...useProps({
quotedMessageProps: {
text: 'something',
conversationColor: ConversationColors[10],
isViewOnce: false,
referencedMessageNotFound: false,
authorTitle: 'Someone',
isFromMe: false,
},
})}
/>
));

View File

@ -8,12 +8,13 @@ import { getIncrement, getTimerBucket } from '../../util/timer';
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
export type Props = { export type Props = {
deletedForEveryone?: boolean;
direction?: 'incoming' | 'outgoing';
expirationLength: number;
expirationTimestamp?: number;
withImageNoCaption?: boolean; withImageNoCaption?: boolean;
withSticker?: boolean; withSticker?: boolean;
withTapToViewExpired?: boolean; withTapToViewExpired?: boolean;
expirationLength: number;
expirationTimestamp: number;
direction?: 'incoming' | 'outgoing';
}; };
export class ExpireTimer extends React.Component<Props> { export class ExpireTimer extends React.Component<Props> {
@ -46,6 +47,7 @@ export class ExpireTimer extends React.Component<Props> {
public override render(): JSX.Element { public override render(): JSX.Element {
const { const {
deletedForEveryone,
direction, direction,
expirationLength, expirationLength,
expirationTimestamp, expirationTimestamp,
@ -62,6 +64,9 @@ export class ExpireTimer extends React.Component<Props> {
'module-expire-timer', 'module-expire-timer',
`module-expire-timer--${bucket}`, `module-expire-timer--${bucket}`,
direction ? `module-expire-timer--${direction}` : null, direction ? `module-expire-timer--${direction}` : null,
deletedForEveryone
? 'module-expire-timer--deleted-for-everyone'
: null,
withTapToViewExpired withTapToViewExpired
? `module-expire-timer--${direction}-with-tap-to-view-expired` ? `module-expire-timer--${direction}-with-tap-to-view-expired`
: null, : null,

View File

@ -381,6 +381,16 @@ story.add('Expiring', () => {
return renderBothDirections(props); return renderBothDirections(props);
}); });
story.add('Will expire but still sending', () => {
const props = createProps({
status: 'sending',
expirationLength: 30 * 1000,
text: 'For outgoing messages, we show timer immediately. Incoming, we wait until expirationStartTimestamp is present.',
});
return renderBothDirections(props);
});
story.add('Pending', () => { story.add('Pending', () => {
const props = createProps({ const props = createProps({
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.', text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
@ -607,12 +617,12 @@ story.add('Sticker', () => {
story.add('Deleted', () => { story.add('Deleted', () => {
const propsSent = createProps({ const propsSent = createProps({
conversationType: 'group', conversationType: 'direct',
deletedForEveryone: true, deletedForEveryone: true,
status: 'sent', status: 'sent',
}); });
const propsSending = createProps({ const propsSending = createProps({
conversationType: 'group', conversationType: 'direct',
deletedForEveryone: true, deletedForEveryone: true,
status: 'sending', status: 'sending',
}); });
@ -645,6 +655,7 @@ story.add('Deleted with error', () => {
conversationType: 'group', conversationType: 'group',
deletedForEveryone: true, deletedForEveryone: true,
status: 'partial-sent', status: 'partial-sent',
direction: 'outgoing',
}); });
const propsError = createProps({ const propsError = createProps({
timestamp: Date.now() - 60 * 1000, timestamp: Date.now() - 60 * 1000,
@ -652,12 +663,13 @@ story.add('Deleted with error', () => {
conversationType: 'group', conversationType: 'group',
deletedForEveryone: true, deletedForEveryone: true,
status: 'error', status: 'error',
direction: 'outgoing',
}); });
return ( return (
<> <>
{renderBothDirections(propsPartialError)} <Message {...propsPartialError} />
{renderBothDirections(propsError)} <Message {...propsError} />
</> </>
); );
}); });

View File

@ -548,6 +548,7 @@ export class Message extends React.PureComponent<Props, State> {
private getMetadataPlacement( private getMetadataPlacement(
{ {
attachments, attachments,
deletedForEveryone,
expirationLength, expirationLength,
expirationTimestamp, expirationTimestamp,
shouldHideMetadata, shouldHideMetadata,
@ -567,7 +568,7 @@ export class Message extends React.PureComponent<Props, State> {
return MetadataPlacement.NotRendered; return MetadataPlacement.NotRendered;
} }
if (!text) { if (!text && !deletedForEveryone) {
return isAudio(attachments) return isAudio(attachments)
? MetadataPlacement.RenderedByMessageAudioComponent ? MetadataPlacement.RenderedByMessageAudioComponent
: MetadataPlacement.Bottom; : MetadataPlacement.Bottom;
@ -598,7 +599,9 @@ export class Message extends React.PureComponent<Props, State> {
let result = GUESS_METADATA_WIDTH_TIMESTAMP_SIZE; let result = GUESS_METADATA_WIDTH_TIMESTAMP_SIZE;
const hasExpireTimer = Boolean(expirationLength && expirationTimestamp); const hasExpireTimer = Boolean(
expirationLength && (expirationTimestamp || direction === 'outgoing')
);
if (hasExpireTimer) { if (hasExpireTimer) {
result += GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE; result += GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE;
} }
@ -1464,6 +1467,9 @@ export class Message extends React.PureComponent<Props, State> {
`module-message__text--${direction}`, `module-message__text--${direction}`,
status === 'error' && direction === 'incoming' status === 'error' && direction === 'incoming'
? 'module-message__text--error' ? 'module-message__text--error'
: null,
deletedForEveryone
? 'module-message__text--delete-for-everyone'
: null : null
)} )}
dir={isRTL ? 'rtl' : undefined} dir={isRTL ? 'rtl' : undefined}

View File

@ -91,6 +91,8 @@ export const MessageMetadata = ({
className={classNames({ className={classNames({
'module-message__metadata__date': true, 'module-message__metadata__date': true,
'module-message__metadata__date--with-sticker': isSticker, '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--${direction}`]: !isSticker,
'module-message__metadata__date--with-image-no-caption': 'module-message__metadata__date--with-image-no-caption':
withImageNoCaption, withImageNoCaption,
@ -105,6 +107,7 @@ export const MessageMetadata = ({
i18n={i18n} i18n={i18n}
timestamp={timestamp} timestamp={timestamp}
direction={metadataDirection} direction={metadataDirection}
deletedForEveryone={deletedForEveryone}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}
withSticker={isSticker} withSticker={isSticker}
withTapToViewExpired={isTapToViewExpired} withTapToViewExpired={isTapToViewExpired}
@ -117,14 +120,16 @@ export const MessageMetadata = ({
const className = classNames( const className = classNames(
'module-message__metadata', 'module-message__metadata',
isInline && 'module-message__metadata--inline', isInline && 'module-message__metadata--inline',
withImageNoCaption && 'module-message__metadata--with-image-no-caption' withImageNoCaption && 'module-message__metadata--with-image-no-caption',
deletedForEveryone && 'module-message__metadata--deleted-for-everyone'
); );
const children = ( const children = (
<> <>
{timestampNode} {timestampNode}
{expirationLength && expirationTimestamp ? ( {expirationLength && (expirationTimestamp || direction === 'outgoing') ? (
<ExpireTimer <ExpireTimer
direction={metadataDirection} direction={metadataDirection}
deletedForEveryone={deletedForEveryone}
expirationLength={expirationLength} expirationLength={expirationLength}
expirationTimestamp={expirationTimestamp} expirationTimestamp={expirationTimestamp}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}
@ -152,6 +157,9 @@ export const MessageMetadata = ({
withImageNoCaption withImageNoCaption
? 'module-message__metadata__status-icon--with-image-no-caption' ? 'module-message__metadata__status-icon--with-image-no-caption'
: null, : null,
deletedForEveryone
? 'module-message__metadata__status-icon--deleted-for-everyone'
: null,
isTapToViewExpired isTapToViewExpired
? 'module-message__metadata__status-icon--with-tap-to-view-expired' ? 'module-message__metadata__status-icon--with-tap-to-view-expired'
: null : null

View File

@ -12,16 +12,18 @@ import { Time } from '../Time';
import { useNowThatUpdatesEveryMinute } from '../../hooks/useNowThatUpdatesEveryMinute'; import { useNowThatUpdatesEveryMinute } from '../../hooks/useNowThatUpdatesEveryMinute';
export type Props = { export type Props = {
timestamp: number; deletedForEveryone?: boolean;
direction?: 'incoming' | 'outgoing';
i18n: LocalizerType;
module?: string; module?: string;
timestamp: number;
withImageNoCaption?: boolean; withImageNoCaption?: boolean;
withSticker?: boolean; withSticker?: boolean;
withTapToViewExpired?: boolean; withTapToViewExpired?: boolean;
direction?: 'incoming' | 'outgoing';
i18n: LocalizerType;
}; };
export function MessageTimestamp({ export function MessageTimestamp({
deletedForEveryone,
direction, direction,
i18n, i18n,
module, module,
@ -42,7 +44,8 @@ export function MessageTimestamp({
? `${moduleName}--${direction}-with-tap-to-view-expired` ? `${moduleName}--${direction}-with-tap-to-view-expired`
: null, : null,
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null, withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
withSticker ? `${moduleName}--with-sticker` : null withSticker ? `${moduleName}--with-sticker` : null,
deletedForEveryone ? `${moduleName}--deleted-for-everyone` : null
)} )}
timestamp={timestamp} timestamp={timestamp}
> >

View File

@ -3669,16 +3669,22 @@ export class ConversationModel extends window.Backbone
sticker?: WhatIsThis sticker?: WhatIsThis
): Promise<WhatIsThis> { ): Promise<WhatIsThis> {
if (attachments && attachments.length) { if (attachments && attachments.length) {
const validAttachments = filter( const attachmentsToUse = Array.from(take(attachments, 1));
attachments,
attachment => attachment && !attachment.pending && !attachment.error
);
const attachmentsToUse = Array.from(take(validAttachments, 1));
const isGIFQuote = isGIF(attachmentsToUse); const isGIFQuote = isGIF(attachmentsToUse);
return Promise.all( return Promise.all(
map(attachmentsToUse, async attachment => { map(attachmentsToUse, async attachment => {
const { fileName, thumbnail, contentType } = attachment; const { path, fileName, thumbnail, contentType } = attachment;
if (!path) {
return {
contentType: isGIFQuote ? IMAGE_GIF : contentType,
// Our protos library complains about this field being undefined, so we
// force it to null
fileName: fileName || null,
thumbnail: null,
};
}
return { return {
contentType: isGIFQuote ? IMAGE_GIF : contentType, contentType: isGIFQuote ? IMAGE_GIF : contentType,
@ -3697,12 +3703,22 @@ export class ConversationModel extends window.Backbone
} }
if (preview && preview.length) { if (preview && preview.length) {
const validPreviews = filter(preview, item => item && item.image); const previewsToUse = take(preview, 1);
const previewsToUse = take(validPreviews, 1);
return Promise.all( return Promise.all(
map(previewsToUse, async attachment => { map(previewsToUse, async attachment => {
const { image } = attachment; const { image } = attachment;
if (!image) {
return {
contentType: IMAGE_JPEG,
// Our protos library complains about these fields being undefined, so we
// force them to null
fileName: null,
thumbnail: null,
};
}
const { contentType } = image; const { contentType } = image;
return { return {
@ -4428,7 +4444,7 @@ export class ConversationModel extends window.Backbone
return false; return false;
} }
if (this.isGroupV1AndDisabled()) { if (!isSetByOther && this.isGroupV1AndDisabled()) {
throw new Error( throw new Error(
'updateExpirationTimer: GroupV1 is deprecated; cannot update expiration timer' 'updateExpirationTimer: GroupV1 is deprecated; cannot update expiration timer'
); );

View File

@ -11,7 +11,14 @@ export function getIncrement(length: number): number {
return Math.ceil(length / 12); return Math.ceil(length / 12);
} }
export function getTimerBucket(expiration: number, length: number): string { export function getTimerBucket(
expiration: number | undefined,
length: number
): string {
if (!expiration) {
return '60';
}
const delta = expiration - Date.now(); const delta = expiration - Date.now();
if (delta < 0) { if (delta < 0) {
return '00'; return '00';