Skip to content

Commit

Permalink
CRS-279 separate TypingIndicator (#535)
Browse files Browse the repository at this point in the history
* TypingIndicator pick up data from context

* fix TypingIndicator usages in sample apps

* remove console.log

* style: run linter

* TypingIndicator accepts avatar size prop

* feat: MessageList support TypingIndicator

* fix: thread skip rendering TypingIndicator

* feat: VirtualizedMessageList support TypingIndicator

* fix: TypingIndicator sample usage

* test: update VirtualizedMessageList snapshot

* fix watch-styles build-styles script

* update styles TypingIndicator

Co-authored-by: Jaap <jaap@getstream.io>
  • Loading branch information
Amin Mahboubi and Jaap authored Sep 24, 2020
1 parent ad736a8 commit 195d3f3
Show file tree
Hide file tree
Showing 15 changed files with 122 additions and 105 deletions.
8 changes: 1 addition & 7 deletions examples/commerce/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
MessageInputFlat,
MessageCommerce,
ChannelHeader,
TypingIndicator,
Window,
} from 'stream-chat-react';
import 'stream-chat-react/dist/css/index.css';
Expand Down Expand Up @@ -69,12 +68,7 @@ class App extends Component {
>
<Window>
<ChannelHeader />
{this.state.open && (
<MessageList
TypingIndicator={TypingIndicator}
Message={MessageCommerce}
/>
)}
{this.state.open && <MessageList Message={MessageCommerce} />}
<MessageInput
onFocus={!this.state.open ? this.toggleDemo : null}
Input={MessageInputFlat}
Expand Down
3 changes: 1 addition & 2 deletions examples/messaging/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
ChannelList,
Window,
Thread,
TypingIndicator,
} from 'stream-chat-react';
import 'stream-chat-react/dist/css/index.css';
import './App.css';
Expand Down Expand Up @@ -68,7 +67,7 @@ class App extends Component {
<Channel>
<Window>
<ChannelHeader />
<MessageList TypingIndicator={TypingIndicator} />
<MessageList />
<MessageInput Input={MessageInputFlat} focus />
</Window>
<Thread Message={MessageSimple} />
Expand Down
3 changes: 1 addition & 2 deletions examples/typescript-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
ChannelList,
Window,
Thread,
TypingIndicator,
} from 'stream-chat-react';
import 'stream-chat-react/dist/css/index.css';
import './App.css';
Expand Down Expand Up @@ -68,7 +67,7 @@ class App extends Component {
<Channel>
<Window>
<ChannelHeader />
<MessageList TypingIndicator={TypingIndicator} />
<MessageList />
<MessageInput Input={MessageInputFlat} focus />
</Window>
<Thread Message={MessageSimple} />
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@
"types": "tsc --strict",
"docs": "styleguidist build",
"docs-server": "styleguidist server",
"watch-styles": "sass --watch src/styles/index.scss dist/styles/styles.css",
"build-styles": "sass src/styles/index.scss dist/styles/styles.css --style compressed",
"watch-styles": "sass --watch src/styles/index.scss dist/css/index.css",
"build-styles": "sass src/styles/index.scss dist/css/index.css --style compressed",
"prettier": "prettier --list-different '**/*.{js,ts,md,json}' .eslintrc.json .prettierrc .babelrc",
"prettier-fix": "prettier --write '**/*.{js,ts,md,json}' .eslintrc.json .prettierrc .babelrc",
"eslint": "eslint '**/*.{js,md}' --max-warnings 0",
Expand Down
9 changes: 9 additions & 0 deletions src/components/MessageList/MessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyState
import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
import { EventComponent } from '../EventComponent';
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator';

/**
* MessageList - The message list components renders a list of messages. Its a consumer of [Channel Context](https://getstream.github.io/stream-chat-react/#channel)
Expand Down Expand Up @@ -228,6 +229,7 @@ class MessageList extends PureComponent {
>
<MessageListInner
EmptyStateIndicator={this.props.EmptyStateIndicator}
TypingIndicator={this.props.TypingIndicator}
MessageSystem={this.props.MessageSystem}
HeaderComponent={this.props.HeaderComponent}
headerPosition={this.props.headerPosition}
Expand Down Expand Up @@ -376,6 +378,12 @@ MessageList.propTypes = {
* Defaults to and accepts same props as: [EventComponent](https://github.com/GetStream/stream-chat-react/blob/master/src/components/EventComponent.js)
*/
MessageSystem: PropTypes.elementType,
/**
* Typing indicator UI component to render
*
* Defaults to and accepts same props as: [TypingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/TypingIndicator/TypingIndicator.js)
* */
TypingIndicator: PropTypes.elementType,
/**
* The UI Indicator to use when MessagerList or ChannelList is empty
* */
Expand Down Expand Up @@ -427,6 +435,7 @@ MessageList.defaultProps = {
Attachment,
DateSeparator: DefaultDateSeparator,
LoadingIndicator: DefaultLoadingIndicator,
TypingIndicator: DefaultTypingIndicator,
EmptyStateIndicator: DefaultEmptyStateIndicator,
unsafeHTML: false,
noGroupByUser: false,
Expand Down
3 changes: 2 additions & 1 deletion src/components/MessageList/MessageListInner.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const getGroupStyles = (

const MessageListInner = (props) => {
const {
TypingIndicator,
EmptyStateIndicator,
MessageSystem,
DateSeparator,
Expand Down Expand Up @@ -289,7 +290,7 @@ const MessageListInner = (props) => {
{...internalInfiniteScrollProps}
>
<ul className="str-chat__ul">{elements}</ul>

{!threadList && <TypingIndicator />}
<div key="bottom" ref={bottomRef} />
</InfiniteScroll>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/MessageList/VirtualizedMessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ChannelContext, TranslationContext } from '../../context';
import { EventComponent } from '../EventComponent';
import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator';
import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator';
import {
FixedHeightMessage,
MessageDeleted as DefaultMessageDeleted,
Expand All @@ -37,6 +38,7 @@ const VirtualizedMessageList = ({
Message = FixedHeightMessage,
MessageSystem = EventComponent,
MessageDeleted = DefaultMessageDeleted,
TypingIndicator = DefaultTypingIndicator,
LoadingIndicator = DefaultLoadingIndicator,
EmptyStateIndicator = DefaultEmptyStateIndicator,
}) => {
Expand Down Expand Up @@ -122,6 +124,7 @@ const VirtualizedMessageList = ({
<LoadingIndicator size={20} />
</div>
)}
footer={() => TypingIndicator && <TypingIndicator avatarSize={24} />}
startReached={() => {
// mounted.current prevents immediate loadMore on first render
if (mounted.current && hasMore) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ jest.mock('../../Message', () => ({
FixedHeightMessage: jest.fn(() => <div>FixedHeightMessage</div>),
}));

jest.mock('../../TypingIndicator', () => ({
TypingIndicator: jest.fn(() => <div>TypingIndicator</div>),
}));

async function createChannel() {
const user1 = generateUser();
const user2 = generateUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ exports[`VirtualizedMessageList should render empty list of messages 1`] = `
FixedHeightMessage
</div>
</div>
<footer>
<div>
TypingIndicator
</div>
</footer>
</div>
</div>
<div
Expand Down
54 changes: 21 additions & 33 deletions src/components/TypingIndicator/TypingIndicator.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
// @ts-check
import React from 'react';
import PropTypes from 'prop-types';
import React, { useContext } from 'react';

import { ChannelContext } from '../../context';
import { Avatar } from '../Avatar';

/**
* TypingIndicator lists users currently typing
* TypingIndicator lists users currently typing, it needs to be a child of Channel component
* @typedef {import('types').TypingIndicatorProps} Props
* @type {React.FC<Props>}
*/
const TypingIndicator = (props) => {
const typing = Object.values(props.typing);
let show;
if (
typing.length === 0 ||
(typing.length === 1 && typing[0].user.id === props.client?.user?.id)
) {
show = false;
} else {
show = true;
}
const TypingIndicator = ({ avatarSize = 32 }) => {
const { typing, client } = useContext(ChannelContext);

if (!typing || !client) return null;

const users = Object.values(typing).filter(
({ user }) => user?.id !== client.user?.id,
);

return (
<div
className={`str-chat__typing-indicator ${
show ? 'str-chat__typing-indicator--typing' : ''
users.length ? 'str-chat__typing-indicator--typing' : ''
}`}
>
<div className="str-chat__typing-indicator__avatars">
{typing
.filter(({ user }) => user.id !== props.client?.user?.id)
.map(({ user }) => (
<Avatar
image={user.image}
size={32}
name={user.name || user.id}
key={user.id}
/>
))}
{users.map(({ user }) => (
<Avatar
image={user?.image}
size={avatarSize}
name={user?.name || user?.id}
key={user?.id}
/>
))}
</div>
<div className="str-chat__typing-indicator__dots">
<span className="str-chat__typing-indicator__dot" />
Expand All @@ -47,12 +43,4 @@ const TypingIndicator = (props) => {
);
};

TypingIndicator.propTypes = {
/** @see See [chat context](https://getstream.github.io/stream-chat-react/#chatcontext) doc */
// @ts-ignore
client: PropTypes.object,
/** @see See [channel context](https://getstream.github.io/stream-chat-react/#channelcontext) doc */
typing: PropTypes.object.isRequired,
};

export default React.memo(TypingIndicator);
93 changes: 52 additions & 41 deletions src/components/TypingIndicator/__tests__/TypingIndicator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,47 @@ import renderer from 'react-test-renderer';
import { cleanup, render } from '@testing-library/react';
import '@testing-library/jest-dom';

import { getTestClientWithUser } from 'mock-builders';
import { getTestClientWithUser, generateUser } from 'mock-builders';

import { ChannelContext } from '../../../context';
import TypingIndicator from '../TypingIndicator';

afterEach(cleanup); // eslint-disable-line

const alice = generateUser();

async function renderComponent(typing = {}) {
const client = await getTestClientWithUser(alice);

return render(
<ChannelContext.Provider value={{ client, typing }}>
<TypingIndicator />
</ChannelContext.Provider>,
);
}

describe('TypingIndicator', () => {
it('should render with default props', () => {
const tree = renderer.create(<TypingIndicator typing={{}} />).toJSON();
it('should render null without proper context values', () => {
const tree = renderer
.create(
<ChannelContext.Provider value={{}}>
<TypingIndicator />
</ChannelContext.Provider>,
)
.toJSON();
expect(tree).toMatchInlineSnapshot(`null`);
});

it('should render hidden indicator with empty typing', async () => {
const client = await getTestClientWithUser(alice);
const tree = renderer
.create(
<ChannelContext.Provider value={{ client, typing: {} }}>
<TypingIndicator />
</ChannelContext.Provider>,
)
.toJSON();

expect(tree).toMatchInlineSnapshot(`
<div
className="str-chat__typing-indicator "
Expand All @@ -38,13 +70,7 @@ describe('TypingIndicator', () => {
});

it("should not render TypingIndicator when it's just you typing", async () => {
const fritsClient = await getTestClientWithUser({ id: 'frits' });
const { container } = render(
<TypingIndicator
client={fritsClient}
typing={{ frits: { user: { id: 'frits' } } }}
/>,
);
const { container } = await renderComponent({ alice: { user: alice } });
expect(
container.firstChild.classList.contains(
'str-chat__typing-indicator--typing',
Expand All @@ -53,15 +79,10 @@ describe('TypingIndicator', () => {
});

it('should render TypingIndicator when someone else is typing', async () => {
const fritsClient = await getTestClientWithUser({ id: 'frits' });
const { container, getByTestId } = render(
<TypingIndicator
client={fritsClient}
typing={{
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
}}
/>,
);
const { container, getByTestId } = await renderComponent({
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
});

expect(
container.firstChild.classList.contains(
'str-chat__typing-indicator--typing',
Expand All @@ -71,37 +92,27 @@ describe('TypingIndicator', () => {
});

it('should render TypingIndicator when you and someone else are typing', async () => {
const fritsClient = await getTestClientWithUser({ id: 'frits' });
const { container, getByTestId } = render(
<TypingIndicator
client={fritsClient}
typing={{
frits: { user: { id: 'frits' } },
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
}}
/>,
);
const { container, getByTestId, getAllByTestId } = await renderComponent({
alice: { user: alice },
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
});

expect(
container.firstChild.classList.contains(
'str-chat__typing-indicator--typing',
),
).toBe(true);
expect(getAllByTestId('avatar-img')).toHaveLength(1);
expect(getByTestId('avatar-img')).toHaveAttribute('src', 'jessica.jpg');
});

it('should render multiple avatars', async () => {
const fritsClient = await getTestClientWithUser({ id: 'frits' });
const { getAllByTestId } = render(
<TypingIndicator
client={fritsClient}
typing={{
frits: { user: { id: 'frits' } },
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
joris: { user: { id: 'joris', image: 'joris.jpg' } },
margriet: { user: { id: 'margriet', image: 'margriet.jpg' } },
}}
/>,
);
const { getAllByTestId } = await renderComponent({
alice: { user: alice },
jessica: { user: { id: 'jessica', image: 'jessica.jpg' } },
joris: { user: { id: 'joris', image: 'joris.jpg' } },
margriet: { user: { id: 'margriet', image: 'margriet.jpg' } },
});
expect(getAllByTestId('avatar-img')).toHaveLength(3);
});
});
Loading

0 comments on commit 195d3f3

Please sign in to comment.