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 {
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;
}

View File

@ -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', () => (
<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';
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<Props> {
@ -46,6 +47,7 @@ export class ExpireTimer extends React.Component<Props> {
public override render(): JSX.Element {
const {
deletedForEveryone,
direction,
expirationLength,
expirationTimestamp,
@ -62,6 +64,9 @@ export class ExpireTimer extends React.Component<Props> {
'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,

View File

@ -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)}
<Message {...propsPartialError} />
<Message {...propsError} />
</>
);
});

View File

@ -548,6 +548,7 @@ export class Message extends React.PureComponent<Props, State> {
private getMetadataPlacement(
{
attachments,
deletedForEveryone,
expirationLength,
expirationTimestamp,
shouldHideMetadata,
@ -567,7 +568,7 @@ export class Message extends React.PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
`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}

View File

@ -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') ? (
<ExpireTimer
direction={metadataDirection}
deletedForEveryone={deletedForEveryone}
expirationLength={expirationLength}
expirationTimestamp={expirationTimestamp}
withImageNoCaption={withImageNoCaption}
@ -152,6 +157,9 @@ export const MessageMetadata = ({
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

View File

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

View File

@ -3669,16 +3669,22 @@ export class ConversationModel extends window.Backbone
sticker?: WhatIsThis
): Promise<WhatIsThis> {
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'
);

View File

@ -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';