diff --git a/ts/components/ConversationListItem.md b/ts/components/ConversationListItem.md
deleted file mode 100644
index 6d5e9a393cd9..000000000000
--- a/ts/components/ConversationListItem.md
+++ /dev/null
@@ -1,615 +0,0 @@
-#### With name and profile
-
-```jsx
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-```
-
-#### Profile, with name, no avatar
-
-```jsx
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-```
-
-#### Conversation with yourself
-
-```jsx
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-```
-
-#### All types of status
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Is typing
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Message Request
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Selected
-
-#### With unread
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Selected
-
-```jsx
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-```
-
-#### With emoji/links in message, no status
-
-We don't want Jumbomoji or links.
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Long content
-
-We only show one line.
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### More narrow
-
-On platforms that show scrollbars all the time, this is true all the time.
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### With various ages
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
-
-#### Missing data
-
-```jsx
-
-
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
- console.log('onClick', result)}
- i18n={util.i18n}
- />
-
-
-```
diff --git a/ts/components/ConversationListItem.stories.tsx b/ts/components/ConversationListItem.stories.tsx
new file mode 100644
index 000000000000..428f08993ec4
--- /dev/null
+++ b/ts/components/ConversationListItem.stories.tsx
@@ -0,0 +1,248 @@
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+
+import {
+ ConversationListItem,
+ MessageStatuses,
+ Props,
+} from './ConversationListItem';
+
+// tslint:disable-next-line
+import 'draft-js/dist/Draft.css';
+
+// @ts-ignore
+import { setup as setupI18n } from '../../js/modules/i18n';
+
+// @ts-ignore
+import enMessages from '../../_locales/en/messages.json';
+import { action } from '@storybook/addon-actions';
+import { boolean, date, select, text } from '@storybook/addon-knobs';
+
+const i18n = setupI18n('en', enMessages);
+
+const story = storiesOf('Components/ConversationListItem', module);
+
+story.addDecorator(storyFn => (
+
{storyFn()}
+));
+
+const createProps = (overrideProps: Partial = {}): Props => ({
+ ...overrideProps,
+ i18n,
+ isAccepted: boolean(
+ 'isAccepted',
+ overrideProps.isAccepted !== undefined ? overrideProps.isAccepted : true
+ ),
+ isMe: boolean('isMe', overrideProps.isMe || false),
+ avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
+ id: overrideProps.id || '',
+ isSelected: boolean('isSelected', overrideProps.isSelected || false),
+ title: text('title', overrideProps.title || 'Some Person'),
+ name: overrideProps.name || 'Some Person',
+ type: overrideProps.type || 'direct',
+ onClick: action('onClick'),
+ lastMessage: overrideProps.lastMessage || {
+ text: text('lastMessage.text', 'Hi there!'),
+ status: select(
+ 'status',
+ MessageStatuses.reduce((m, s) => ({ ...m, [s]: s }), {}),
+ 'read'
+ ),
+ },
+ lastUpdated: date(
+ 'lastUpdated',
+ new Date(overrideProps.lastUpdated || Date.now() - 5 * 60 * 1000)
+ ),
+});
+
+story.add('Name', () => {
+ const props = createProps();
+
+ return ;
+});
+
+story.add('Name and Avatar', () => {
+ const props = createProps({
+ avatarPath: '/fixtures/kitten-1-64-64.jpg',
+ });
+
+ return ;
+});
+
+story.add('Conversation with Yourself', () => {
+ const props = createProps({
+ lastMessage: {
+ text: 'Just a second',
+ status: 'read',
+ },
+ name: 'Myself',
+ title: 'Myself',
+ isMe: true,
+ });
+
+ return ;
+});
+
+story.add('Message Statuses', () => {
+ return MessageStatuses.map(status => {
+ const props = createProps({
+ lastMessage: {
+ text: status,
+ status,
+ },
+ });
+
+ return ;
+ });
+});
+
+story.add('Typing Status', () => {
+ const props = createProps({
+ typingContact: {
+ name: 'Someone Here',
+ },
+ });
+
+ return ;
+});
+
+story.add('Message Request', () => {
+ const props = createProps({
+ isAccepted: false,
+ lastMessage: {
+ text: 'A Message',
+ status: 'delivered',
+ },
+ });
+
+ return ;
+});
+
+story.add('Unread', () => {
+ const counts = [4, 10, 250];
+
+ return counts.map(unreadCount => {
+ const props = createProps({
+ lastMessage: {
+ text: 'Hey there!',
+ status: 'delivered',
+ },
+ unreadCount,
+ });
+
+ return ;
+ });
+});
+
+story.add('Selected', () => {
+ const props = createProps({
+ lastMessage: {
+ text: 'Hey there!',
+ status: 'read',
+ },
+ isSelected: true,
+ });
+
+ return ;
+});
+
+story.add('Emoji in Message', () => {
+ const props = createProps({
+ lastMessage: {
+ text: '🔥',
+ status: 'read',
+ },
+ });
+
+ return ;
+});
+
+story.add('Link in Message', () => {
+ const props = createProps({
+ lastMessage: {
+ text: 'Download at http://signal.org',
+ status: 'read',
+ },
+ });
+
+ return ;
+});
+
+story.add('Long Name', () => {
+ const name =
+ 'Long contact name. Esquire. The third. And stuff. And more! And more!';
+
+ const props = createProps({
+ name,
+ title: name,
+ });
+
+ return ;
+});
+
+story.add('Long Message', () => {
+ const messages = [
+ "Long line. This is a really really really long line. Really really long. Because that's just how it is",
+ `Many lines. This is a many-line message.
+Line 2 is really exciting but it shouldn't be seen.
+Line three is even better.
+Line 4, well.`,
+ ];
+
+ return messages.map(message => {
+ const props = createProps({
+ name,
+ lastMessage: {
+ text: message,
+ status: 'read',
+ },
+ });
+
+ return ;
+ });
+});
+
+story.add('Various Times', () => {
+ const times: Array<[number, string]> = [
+ [Date.now() - 5 * 60 * 60 * 1000, 'Five hours ago'],
+ [Date.now() - 24 * 60 * 60 * 1000, 'One day ago'],
+ [Date.now() - 7 * 24 * 60 * 60 * 1000, 'One week ago'],
+ [Date.now() - 365 * 24 * 60 * 60 * 1000, 'One year ago'],
+ ];
+
+ return times.map(([lastUpdated, messageText]) => {
+ const props = createProps({
+ name,
+ lastUpdated,
+ lastMessage: {
+ text: messageText,
+ status: 'read',
+ },
+ });
+
+ return ;
+ });
+});
+
+story.add('Missing Date', () => {
+ const props = createProps();
+
+ return ;
+});
+
+story.add('Missing Message', () => {
+ const props = createProps();
+
+ return ;
+});
+
+story.add('Missing Text', () => {
+ const props = createProps();
+
+ return (
+
+ );
+});
diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx
index 4cad6b6a42af..fc5ff66ea330 100644
--- a/ts/components/ConversationListItem.tsx
+++ b/ts/components/ConversationListItem.tsx
@@ -12,6 +12,17 @@ import { cleanId } from './_util';
import { LocalizerType } from '../types/Util';
import { ColorType } from '../types/Colors';
+export const MessageStatuses = [
+ 'sending',
+ 'sent',
+ 'delivered',
+ 'read',
+ 'error',
+ 'partial-sent',
+] as const;
+
+export type MessageStatusType = typeof MessageStatuses[number];
+
export type PropsData = {
id: string;
phoneNumber?: string;
@@ -33,13 +44,7 @@ export type PropsData = {
typingContact?: Object;
lastMessage?: {
- status:
- | 'sending'
- | 'sent'
- | 'delivered'
- | 'read'
- | 'error'
- | 'partial-sent';
+ status: MessageStatusType;
text: string;
deletedForEveryone?: boolean;
};
@@ -51,7 +56,7 @@ type PropsHousekeeping = {
onClick?: (id: string) => void;
};
-type Props = PropsData & PropsHousekeeping;
+export type Props = PropsData & PropsHousekeeping;
export class ConversationListItem extends React.PureComponent {
public renderAvatar() {