From b50c96c0b5249be43277ce9988153708b8108335 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 7 Apr 2022 09:58:15 -0700 Subject: [PATCH] Improve layout of various message bubbles --- stylesheets/_modules.scss | 68 +++++++++++++++++++ ts/components/CompositionArea.stories.tsx | 16 +++++ ts/components/conversation/ExpireTimer.tsx | 11 ++- .../conversation/Message.stories.tsx | 20 ++++-- ts/components/conversation/Message.tsx | 10 ++- .../conversation/MessageMetadata.tsx | 12 +++- .../conversation/MessageTimestamp.tsx | 11 +-- ts/models/conversations.ts | 34 +++++++--- ts/util/timer.ts | 9 ++- 9 files changed, 166 insertions(+), 25 deletions(-) diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index bb12cf24f..419aac5d1 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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 { margin-top: 2px; display: flex; @@ -992,6 +1008,14 @@ .module-message__text--error { @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 { align-items: center; @@ -999,6 +1023,7 @@ flex-direction: row; justify-content: flex-end; margin-top: 3px; + font-style: normal; &--inline { float: right; @@ -1021,6 +1046,15 @@ 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 { @include font-caption; user-select: none; @@ -1054,6 +1088,14 @@ 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 { color: $color-gray-75; @@ -1076,6 +1118,8 @@ height: 12px; display: inline-block; margin-left: 6px; + // High margin to leave space for the increase when we go to two checks + margin-right: 6px; margin-bottom: 2px; } @@ -1102,6 +1146,8 @@ } } .module-message__metadata__status-icon--delivered { + // We reduce the margin size to keep the overall width the same + margin-right: 0px; width: 18px; @include light-theme { @@ -1113,6 +1159,8 @@ } .module-message__metadata__status-icon--read, .module-message__metadata__status-icon--viewed { + // We reduce the margin size to keep the overall width the same + margin-right: 0px; width: 18px; @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 { 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 { &__container { 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 .module-message__container { + max-width: 100%; + &--incoming { align-self: flex-start; } diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index ed778ea2f..c5e40ae62 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -17,6 +17,7 @@ import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment'; import { landscapeGreenUrl } from '../storybook/Fixtures'; import { RecordingState } from '../state/ducks/audioRecorder'; +import { ConversationColors } from '../types/Colors'; const i18n = setupI18n('en', enMessages); @@ -183,3 +184,18 @@ story.add('Announcements Only group', () => ( })} /> )); + +story.add('Quote', () => ( + +)); diff --git a/ts/components/conversation/ExpireTimer.tsx b/ts/components/conversation/ExpireTimer.tsx index 786fd2e7c..c12db463a 100644 --- a/ts/components/conversation/ExpireTimer.tsx +++ b/ts/components/conversation/ExpireTimer.tsx @@ -8,12 +8,13 @@ import { getIncrement, getTimerBucket } from '../../util/timer'; import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; export type Props = { + deletedForEveryone?: boolean; + direction?: 'incoming' | 'outgoing'; + expirationLength: number; + expirationTimestamp?: number; withImageNoCaption?: boolean; withSticker?: boolean; withTapToViewExpired?: boolean; - expirationLength: number; - expirationTimestamp: number; - direction?: 'incoming' | 'outgoing'; }; export class ExpireTimer extends React.Component { @@ -46,6 +47,7 @@ export class ExpireTimer extends React.Component { public override render(): JSX.Element { const { + deletedForEveryone, direction, expirationLength, expirationTimestamp, @@ -62,6 +64,9 @@ export class ExpireTimer extends React.Component { 'module-expire-timer', `module-expire-timer--${bucket}`, direction ? `module-expire-timer--${direction}` : null, + deletedForEveryone + ? 'module-expire-timer--deleted-for-everyone' + : null, withTapToViewExpired ? `module-expire-timer--${direction}-with-tap-to-view-expired` : null, diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index eeddd4818..7a268100d 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -381,6 +381,16 @@ story.add('Expiring', () => { 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', () => { 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.', @@ -607,12 +617,12 @@ story.add('Sticker', () => { story.add('Deleted', () => { const propsSent = createProps({ - conversationType: 'group', + conversationType: 'direct', deletedForEveryone: true, status: 'sent', }); const propsSending = createProps({ - conversationType: 'group', + conversationType: 'direct', deletedForEveryone: true, status: 'sending', }); @@ -645,6 +655,7 @@ story.add('Deleted with error', () => { conversationType: 'group', deletedForEveryone: true, status: 'partial-sent', + direction: 'outgoing', }); const propsError = createProps({ timestamp: Date.now() - 60 * 1000, @@ -652,12 +663,13 @@ story.add('Deleted with error', () => { conversationType: 'group', deletedForEveryone: true, status: 'error', + direction: 'outgoing', }); return ( <> - {renderBothDirections(propsPartialError)} - {renderBothDirections(propsError)} + + ); }); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 374012282..3757216c1 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -548,6 +548,7 @@ export class Message extends React.PureComponent { private getMetadataPlacement( { attachments, + deletedForEveryone, expirationLength, expirationTimestamp, shouldHideMetadata, @@ -567,7 +568,7 @@ export class Message extends React.PureComponent { return MetadataPlacement.NotRendered; } - if (!text) { + if (!text && !deletedForEveryone) { return isAudio(attachments) ? MetadataPlacement.RenderedByMessageAudioComponent : MetadataPlacement.Bottom; @@ -598,7 +599,9 @@ export class Message extends React.PureComponent { let result = GUESS_METADATA_WIDTH_TIMESTAMP_SIZE; - const hasExpireTimer = Boolean(expirationLength && expirationTimestamp); + const hasExpireTimer = Boolean( + expirationLength && (expirationTimestamp || direction === 'outgoing') + ); if (hasExpireTimer) { result += GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE; } @@ -1464,6 +1467,9 @@ export class Message extends React.PureComponent { `module-message__text--${direction}`, status === 'error' && direction === 'incoming' ? 'module-message__text--error' + : null, + deletedForEveryone + ? 'module-message__text--delete-for-everyone' : null )} dir={isRTL ? 'rtl' : undefined} diff --git a/ts/components/conversation/MessageMetadata.tsx b/ts/components/conversation/MessageMetadata.tsx index 9b3780652..923ca3e72 100644 --- a/ts/components/conversation/MessageMetadata.tsx +++ b/ts/components/conversation/MessageMetadata.tsx @@ -91,6 +91,8 @@ export const MessageMetadata = ({ 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, @@ -105,6 +107,7 @@ export const MessageMetadata = ({ i18n={i18n} timestamp={timestamp} direction={metadataDirection} + deletedForEveryone={deletedForEveryone} withImageNoCaption={withImageNoCaption} withSticker={isSticker} withTapToViewExpired={isTapToViewExpired} @@ -117,14 +120,16 @@ export const MessageMetadata = ({ const className = classNames( 'module-message__metadata', 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 = ( <> {timestampNode} - {expirationLength && expirationTimestamp ? ( + {expirationLength && (expirationTimestamp || direction === 'outgoing') ? ( diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 774b16ddc..d8e25122d 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -3669,16 +3669,22 @@ export class ConversationModel extends window.Backbone sticker?: WhatIsThis ): Promise { if (attachments && attachments.length) { - const validAttachments = filter( - attachments, - attachment => attachment && !attachment.pending && !attachment.error - ); - const attachmentsToUse = Array.from(take(validAttachments, 1)); + const attachmentsToUse = Array.from(take(attachments, 1)); const isGIFQuote = isGIF(attachmentsToUse); return Promise.all( 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 { contentType: isGIFQuote ? IMAGE_GIF : contentType, @@ -3697,12 +3703,22 @@ export class ConversationModel extends window.Backbone } if (preview && preview.length) { - const validPreviews = filter(preview, item => item && item.image); - const previewsToUse = take(validPreviews, 1); + const previewsToUse = take(preview, 1); return Promise.all( map(previewsToUse, async 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; return { @@ -4428,7 +4444,7 @@ export class ConversationModel extends window.Backbone return false; } - if (this.isGroupV1AndDisabled()) { + if (!isSetByOther && this.isGroupV1AndDisabled()) { throw new Error( 'updateExpirationTimer: GroupV1 is deprecated; cannot update expiration timer' ); diff --git a/ts/util/timer.ts b/ts/util/timer.ts index 51167b7a5..23c4fd7bc 100644 --- a/ts/util/timer.ts +++ b/ts/util/timer.ts @@ -11,7 +11,14 @@ export function getIncrement(length: number): number { 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(); if (delta < 0) { return '00';