Add user badges to typing bubbles, refactor typing logic

This commit is contained in:
Evan Hahn 2021-11-15 14:01:58 -06:00 committed by GitHub
parent ede34ecee3
commit f4e336836f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 125 additions and 189 deletions

View file

@ -3,7 +3,7 @@
import * as React from 'react';
import * as moment from 'moment';
import { isBoolean, times } from 'lodash';
import { times } from 'lodash';
import { v4 as uuid } from 'uuid';
import { storiesOf } from '@storybook/react';
import { text, boolean, number } from '@storybook/addon-knobs';
@ -25,6 +25,8 @@ import { TypingBubble } from './TypingBubble';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { ReadStatus } from '../../messages/MessageReadStatus';
import type { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { UUID } from '../../types/UUID';
const i18n = setupI18n('en', enMessages);
@ -441,12 +443,14 @@ const renderLoadingRow = () => <TimelineLoadingRow state="loading" />;
const renderTypingBubble = () => (
<TypingBubble
acceptedMessageRequest
badge={undefined}
color={getRandomColor()}
conversationType="direct"
phoneNumber="+18005552222"
i18n={i18n}
isMe={false}
title="title"
theme={ThemeType.light}
sharedGroupNames={[]}
/>
);
@ -486,10 +490,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
renderHeroRow,
renderLoadingRow,
renderTypingBubble,
typingContact: boolean(
'typingContact',
isBoolean(overrideProps.typingContact) ? overrideProps.typingContact : false
),
typingContactId: overrideProps.typingContactId,
...actions(),
});
@ -561,7 +562,7 @@ story.add('Target Index to Top', () => {
story.add('Typing Indicator', () => {
const props = createProps({
typingContact: true,
typingContactId: UUID.generate().toString(),
});
return <Timeline {...props} />;

View file

@ -94,7 +94,7 @@ type PropsHousekeepingType = {
areWeAdmin?: boolean;
isGroupV1AndDisabled?: boolean;
isIncomingMessageRequest: boolean;
typingContact?: unknown;
typingContactId?: string;
unreadCount?: number;
selectedMessageId?: string;
@ -859,7 +859,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
}
public getRowCount(): number {
const { oldestUnreadIndex, typingContact } = this.props;
const { oldestUnreadIndex, typingContactId } = this.props;
const { items } = this.props;
const itemsCount = items && items.length ? items.length : 0;
@ -870,7 +870,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
extraRows += 1;
}
if (typingContact) {
if (typingContactId) {
extraRows += 1;
}
@ -1033,7 +1033,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
resetCounter,
scrollToBottomCounter,
scrollToIndex,
typingContact,
typingContactId,
} = this.props;
// We recompute the hero row's height if:
@ -1097,7 +1097,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
if (
items !== prevProps.items ||
oldestUnreadIndex !== prevProps.oldestUnreadIndex ||
Boolean(typingContact) !== Boolean(prevProps.typingContact)
Boolean(typingContactId) !== Boolean(prevProps.typingContactId)
) {
const { atTop } = this.state;
@ -1135,13 +1135,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
const rowsIterator = Timeline.getEphemeralRows({
items,
oldestUnreadIndex,
typingContact: Boolean(typingContact),
hasTypingContact: Boolean(typingContactId),
haveOldest,
});
const prevRowsIterator = Timeline.getEphemeralRows({
items: prevProps.items,
oldestUnreadIndex: prevProps.oldestUnreadIndex,
typingContact: Boolean(prevProps.typingContact),
hasTypingContact: Boolean(prevProps.typingContactId),
haveOldest: prevProps.haveOldest,
});
@ -1578,13 +1578,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
}
private static *getEphemeralRows({
items,
typingContact,
oldestUnreadIndex,
hasTypingContact,
haveOldest,
items,
oldestUnreadIndex,
}: {
items: ReadonlyArray<string>;
typingContact: boolean;
hasTypingContact: boolean;
oldestUnreadIndex?: number;
haveOldest: boolean;
}): Iterator<string> {
@ -1597,7 +1597,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
yield `item:${items[i]}`;
}
if (typingContact) {
if (hasTypingContact) {
yield 'typing-contact';
}
}

View file

@ -10,6 +10,8 @@ import enMessages from '../../../_locales/en/messages.json';
import type { Props } from './TypingBubble';
import { TypingBubble } from './TypingBubble';
import { AvatarColors } from '../../types/Colors';
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
import { ThemeType } from '../../types/Util';
const i18n = setupI18n('en', enMessages);
@ -17,6 +19,7 @@ const story = storiesOf('Components/Conversation/TypingBubble', module);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
acceptedMessageRequest: true,
badge: overrideProps.badge,
isMe: false,
i18n,
color: select(
@ -33,6 +36,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
overrideProps.conversationType || 'direct'
),
sharedGroupNames: [],
theme: ThemeType.light,
});
story.add('Direct', () => {
@ -46,3 +50,12 @@ story.add('Group', () => {
return <TypingBubble {...props} />;
});
story.add('Group (with badge)', () => {
const props = createProps({
badge: getFakeBadge(),
conversationType: 'group',
});
return <TypingBubble {...props} />;
});

View file

@ -1,14 +1,16 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import { TypingAnimation } from './TypingAnimation';
import { Avatar } from '../Avatar';
import type { LocalizerType } from '../../types/Util';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types';
export type Props = Pick<
ConversationType,
@ -22,76 +24,69 @@ export type Props = Pick<
| 'sharedGroupNames'
| 'title'
> & {
badge: undefined | BadgeType;
conversationType: 'group' | 'direct';
i18n: LocalizerType;
theme: ThemeType;
};
export class TypingBubble extends React.PureComponent<Props> {
public renderAvatar(): JSX.Element | null {
const {
acceptedMessageRequest,
avatarPath,
color,
conversationType,
i18n,
isMe,
name,
phoneNumber,
profileName,
sharedGroupNames,
title,
} = this.props;
export function TypingBubble({
acceptedMessageRequest,
avatarPath,
badge,
color,
conversationType,
i18n,
isMe,
name,
phoneNumber,
profileName,
sharedGroupNames,
theme,
title,
}: Props): ReactElement {
const isGroup = conversationType === 'group';
if (conversationType !== 'group') {
return null;
}
return (
<div className="module-message__author-avatar-container">
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
color={color}
conversationType="direct"
i18n={i18n}
isMe={isMe}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={28}
/>
</div>
);
}
public override render(): JSX.Element {
const { i18n, conversationType } = this.props;
const isGroup = conversationType === 'group';
return (
<div
className={classNames(
'module-message',
'module-message--incoming',
isGroup ? 'module-message--group' : null
)}
>
{this.renderAvatar()}
<div className="module-message__container-outer">
<div
className={classNames(
'module-message__container',
'module-message__container--incoming'
)}
>
<div className="module-message__typing-container">
<TypingAnimation color="light" i18n={i18n} />
</div>
return (
<div
className={classNames(
'module-message',
'module-message--incoming',
isGroup ? 'module-message--group' : null
)}
>
{isGroup && (
<div className="module-message__author-avatar-container">
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
badge={badge}
color={color}
conversationType="direct"
i18n={i18n}
isMe={isMe}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
sharedGroupNames={sharedGroupNames}
size={28}
/>
</div>
)}
<div className="module-message__container-outer">
<div
className={classNames(
'module-message__container',
'module-message__container--incoming'
)}
>
<div className="module-message__typing-container">
<TypingAnimation color="light" i18n={i18n} />
</div>
</div>
</div>
);
}
</div>
);
}