Add user badges to typing bubbles, refactor typing logic
This commit is contained in:
parent
ede34ecee3
commit
f4e336836f
13 changed files with 125 additions and 189 deletions
|
@ -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} />;
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue