Improve layout of various message bubbles
This commit is contained in:
parent
933c07c9ce
commit
b50c96c0b5
9 changed files with 166 additions and 25 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue