diff --git a/ts/components/conversation/Timeline.md b/ts/components/conversation/Timeline.md
deleted file mode 100644
index 213dbac74..000000000
--- a/ts/components/conversation/Timeline.md
+++ /dev/null
@@ -1,358 +0,0 @@
-## With oldest and newest
-
-```jsx
-window.itemLookup = {
- 'id-1': {
- type: 'message',
- data: {
- id: 'id-1',
- direction: 'incoming',
- timestamp: Date.now(),
- authorPhoneNumber: '(202) 555-2001',
- authorColor: 'green',
- text: '🔥',
- },
- },
- 'id-2': {
- type: 'message',
- data: {
- id: 'id-2',
- conversationType: 'group',
- direction: 'incoming',
- timestamp: Date.now(),
- authorColor: 'green',
- text: 'Hello there from the new world! http://somewhere.com',
- },
- },
- 'id-2.5': {
- type: 'unsupportedMessage',
- data: {
- id: 'id-2.5',
- canProcessNow: false,
- contact: {
- phoneNumber: '(202) 555-1000',
- profileName: 'Mr. Pig',
- },
- },
- },
- 'id-3': {
- type: 'message',
- data: {
- id: 'id-3',
- collapseMetadata: true,
- direction: 'incoming',
- timestamp: Date.now(),
- authorColor: 'red',
- text: 'Hello there from the new world!',
- },
- },
- 'id-4': {
- type: 'timerNotification',
- data: {
- type: 'fromMe',
- timespan: '5 minutes',
- },
- },
- 'id-5': {
- type: 'timerNotification',
- data: {
- type: 'fromOther',
- phoneNumber: '(202) 555-0000',
- timespan: '1 hour',
- },
- },
- 'id-6': {
- type: 'safetyNumberNotification',
- data: {
- contact: {
- id: '+1202555000',
- phoneNumber: '(202) 555-0000',
- profileName: 'Mr. Fire',
- },
- },
- },
- 'id-7': {
- type: 'verificationNotification',
- data: {
- contact: {
- phoneNumber: '(202) 555-0001',
- name: 'Mrs. Ice',
- },
- isLocal: true,
- type: 'markVerified',
- },
- },
- 'id-8': {
- type: 'groupNotification',
- data: {
- changes: [
- {
- type: 'name',
- newName: 'Squirrels and their uses',
- },
- {
- type: 'add',
- contacts: [
- {
- phoneNumber: '(202) 555-0002',
- },
- {
- phoneNumber: '(202) 555-0003',
- profileName: 'Ms. Water',
- },
- ],
- },
- ],
- isMe: false,
- },
- },
- 'id-9': {
- type: 'resetSessionNotification',
- data: null,
- },
- 'id-10': {
- type: 'message',
- data: {
- id: 'id-6',
- direction: 'outgoing',
- timestamp: Date.now(),
- status: 'sent',
- authorColor: 'pink',
- text: '🔥',
- },
- },
- 'id-11': {
- type: 'message',
- data: {
- id: 'id-7',
- direction: 'outgoing',
- timestamp: Date.now(),
- status: 'read',
- authorColor: 'pink',
- text: 'Hello there from the new world! http://somewhere.com',
- },
- },
- 'id-12': {
- type: 'message',
- data: {
- id: 'id-8',
- collapseMetadata: true,
- direction: 'outgoing',
- status: 'sent',
- timestamp: Date.now(),
- text: 'Hello there from the new world! 🔥',
- },
- },
- 'id-13': {
- type: 'message',
- data: {
- id: 'id-9',
- direction: 'outgoing',
- status: 'sent',
- timestamp: Date.now(),
- authorColor: 'blue',
- text:
- 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
- },
- },
- 'id-14': {
- type: 'message',
- data: {
- id: 'id-10',
- direction: 'outgoing',
- status: 'read',
- timestamp: Date.now(),
- collapseMetadata: true,
- text:
- 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
- },
- },
- 'id-15': {
- type: 'linkNotification',
- data: null,
- },
-};
-
-window.actions = {
- // For messages
- downloadAttachment: options => console.log('onDownload', options),
- replyToitem: id => console.log('onReply', id),
- showMessageDetail: id => console.log('onShowDetail', id),
- deleteMessage: id => console.log('onDelete', id),
- downloadNewVersion: () => console.log('downloadNewVersion'),
-
- // For Timeline
- clearChangedMessages: (...args) => console.log('clearChangedMessages', args),
- setLoadCountdownStart: (...args) =>
- console.log('setLoadCountdownStart', args),
-
- loadAndScroll: (...args) => console.log('loadAndScroll', args),
- loadOlderMessages: (...args) => console.log('loadOlderMessages', args),
- loadNewerMessages: (...args) => console.log('loadNewerMessages', args),
- loadNewestMessages: (...args) => console.log('loadNewestMessages', args),
- markMessageRead: (...args) => console.log('markMessageRead', args),
-};
-
-const props = {
- id: 'conversationId-1',
- haveNewest: true,
- haveOldest: true,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: null,
- resetCounter: 0,
- scrollToIndex: null,
- scrollToIndexCounter: 0,
- totalUnread: 0,
-
- renderItem: id => (
-
- ),
-};
-
-
-
-
;
-```
-
-## With last seen indicator
-
-```
-const props = {
- id: 'conversationId-1',
- haveNewest: true,
- haveOldest: true,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: 2,
- resetCounter: 0,
- scrollToIndex: null,
- scrollToIndexCounter: 0,
- totalUnread: 2,
-
- renderItem: id => (
-
- ),
- renderLastSeenIndicator: () => (
-
- ),
-};
-
-
-
-
;
-```
-
-## With target index = 0
-
-```
-const props = {
- id: 'conversationId-1',
- haveNewest: true,
- haveOldest: true,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: null,
- resetCounter: 0,
- scrollToIndex: 0,
- scrollToIndexCounter: 0,
- totalUnread: 0,
-
- renderItem: id => (
-
- ),
-};
-
-
-
-
;
-```
-
-## With typing indicator
-
-```
-const props = {
- id: 'conversationId-1',
- haveNewest: true,
- haveOldest: true,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: null,
- resetCounter: 0,
- scrollToIndex: null,
- scrollToIndexCounter: 0,
- totalUnread: 0,
-
- typingContact: true,
-
- renderItem: id => (
-
- ),
- renderTypingBubble: () => (
-
- ),
-};
-
-
-
-
;
-```
-
-## Without newest message
-
-```
-const props = {
- id: 'conversationId-1',
- haveNewest: false,
- haveOldest: true,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: null,
- resetCounter: 0,
- scrollToIndex: 3,
- scrollToIndexCounter: 0,
- totalUnread: 0,
-
- renderItem: id => (
-
- ),
-};
-
-
-
-
;
-```
-
-## Without oldest message
-
-```
-const props = {
- id: 'conversationId-1',
- haveNewest: true,
- haveOldest: false,
- isLoadingMessages: false,
- items: util._.keys(window.itemLookup),
- messagesHaveChanged: false,
- oldestUnreadIndex: null,
- resetCounter: 0,
- scrollToIndex: null,
- scrollToIndexCounter: 0,
- totalUnread: 0,
-
- renderItem: id => (
-
- ),
- renderLoadingRow: () => (
-
- ),
-};
-
-
-
-
;
-```
diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx
new file mode 100644
index 000000000..7fd10941a
--- /dev/null
+++ b/ts/components/conversation/Timeline.stories.tsx
@@ -0,0 +1,353 @@
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { boolean, number } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+
+// @ts-ignore
+import { setup as setupI18n } from '../../../js/modules/i18n';
+
+// @ts-ignore
+import enMessages from '../../../_locales/en/messages.json';
+
+import { Props, Timeline } from './Timeline';
+import { TimelineItem, TimelineItemType } from './TimelineItem';
+import { LastSeenIndicator } from './LastSeenIndicator';
+import { TimelineLoadingRow } from './TimelineLoadingRow';
+import { TypingBubble } from './TypingBubble';
+
+const i18n = setupI18n('en', enMessages);
+
+const story = storiesOf('Components/Conversation/Timeline', module);
+
+// tslint:disable-next-line
+const noop = () => {};
+
+Object.assign(window, {
+ registerForActive: noop,
+ unregisterForActive: noop,
+});
+
+const items: Record = {
+ 'id-1': {
+ type: 'message',
+ data: {
+ id: 'id-1',
+ direction: 'incoming',
+ timestamp: Date.now(),
+ authorPhoneNumber: '(202) 555-2001',
+ authorColor: 'green',
+ text: '🔥',
+ },
+ },
+ 'id-2': {
+ type: 'message',
+ data: {
+ id: 'id-2',
+ conversationType: 'group',
+ direction: 'incoming',
+ timestamp: Date.now(),
+ authorColor: 'green',
+ text: 'Hello there from the new world! http://somewhere.com',
+ },
+ },
+ 'id-2.5': {
+ type: 'unsupportedMessage',
+ data: {
+ id: 'id-2.5',
+ canProcessNow: false,
+ contact: {
+ phoneNumber: '(202) 555-1000',
+ profileName: 'Mr. Pig',
+ title: 'Mr. Pig',
+ },
+ },
+ },
+ 'id-3': {
+ type: 'message',
+ data: {
+ id: 'id-3',
+ collapseMetadata: true,
+ direction: 'incoming',
+ timestamp: Date.now(),
+ authorColor: 'red',
+ text: 'Hello there from the new world!',
+ },
+ },
+ 'id-4': {
+ type: 'timerNotification',
+ data: {
+ type: 'fromMe',
+ timespan: '5 minutes',
+ },
+ },
+ 'id-5': {
+ type: 'timerNotification',
+ data: {
+ type: 'fromOther',
+ title: '(202) 555-0000',
+ phoneNumber: '(202) 555-0000',
+ timespan: '1 hour',
+ },
+ },
+ 'id-6': {
+ type: 'safetyNumberNotification',
+ data: {
+ contact: {
+ id: '+1202555000',
+ phoneNumber: '(202) 555-0000',
+ profileName: 'Mr. Fire',
+ },
+ },
+ },
+ 'id-7': {
+ type: 'verificationNotification',
+ data: {
+ contact: {
+ phoneNumber: '(202) 555-0001',
+ name: 'Mrs. Ice',
+ },
+ isLocal: true,
+ type: 'markVerified',
+ },
+ },
+ 'id-8': {
+ type: 'groupNotification',
+ data: {
+ changes: [
+ {
+ type: 'name',
+ newName: 'Squirrels and their uses',
+ },
+ {
+ type: 'add',
+ contacts: [
+ {
+ phoneNumber: '(202) 555-0002',
+ profileName: 'Mr. Fire',
+ title: 'Mr. Fire',
+ },
+ {
+ phoneNumber: '(202) 555-0003',
+ profileName: 'Ms. Water',
+ title: 'Ms. Water',
+ },
+ ],
+ },
+ ],
+ from: {
+ phoneNumber: '(202) 555-0001',
+ name: 'Mrs. Ice',
+ title: 'Mrs. Ice',
+ },
+ isMe: false,
+ },
+ },
+ 'id-9': {
+ type: 'resetSessionNotification',
+ data: null,
+ },
+ 'id-10': {
+ type: 'message',
+ data: {
+ id: 'id-6',
+ direction: 'outgoing',
+ timestamp: Date.now(),
+ status: 'sent',
+ authorColor: 'pink',
+ text: '🔥',
+ },
+ },
+ 'id-11': {
+ type: 'message',
+ data: {
+ id: 'id-7',
+ direction: 'outgoing',
+ timestamp: Date.now(),
+ status: 'read',
+ authorColor: 'pink',
+ text: 'Hello there from the new world! http://somewhere.com',
+ },
+ },
+ 'id-12': {
+ type: 'message',
+ data: {
+ id: 'id-8',
+ collapseMetadata: true,
+ direction: 'outgoing',
+ status: 'sent',
+ timestamp: Date.now(),
+ text: 'Hello there from the new world! 🔥',
+ },
+ },
+ 'id-13': {
+ type: 'message',
+ data: {
+ id: 'id-9',
+ direction: 'outgoing',
+ status: 'sent',
+ timestamp: Date.now(),
+ authorColor: 'blue',
+ text:
+ 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
+ },
+ },
+ 'id-14': {
+ type: 'message',
+ data: {
+ id: 'id-10',
+ direction: 'outgoing',
+ status: 'read',
+ timestamp: Date.now(),
+ collapseMetadata: true,
+ text:
+ 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
+ },
+ },
+ 'id-15': {
+ type: 'linkNotification',
+ data: null,
+ },
+} as any;
+
+const actions = () => ({
+ clearChangedMessages: action('clearChangedMessages'),
+ setLoadCountdownStart: action('setLoadCountdownStart'),
+ setIsNearBottom: action('setIsNearBottom'),
+ loadAndScroll: action('loadAndScroll'),
+ loadOlderMessages: action('loadOlderMessages'),
+ loadNewerMessages: action('loadNewerMessages'),
+ loadNewestMessages: action('loadNewestMessages'),
+ markMessageRead: action('markMessageRead'),
+ selectMessage: action('selectMessage'),
+ clearSelectedMessage: action('clearSelectedMessage'),
+ updateSharedGroups: action('updateSharedGroups'),
+
+ reactToMessage: action('reactToMessage'),
+ replyToMessage: action('replyToMessage'),
+ retrySend: action('retrySend'),
+ deleteMessage: action('deleteMessage'),
+ showMessageDetail: action('showMessageDetail'),
+ openConversation: action('openConversation'),
+ showContactDetail: action('showContactDetail'),
+ showVisualAttachment: action('showVisualAttachment'),
+ downloadAttachment: action('downloadAttachment'),
+ displayTapToViewMessage: action('displayTapToViewMessage'),
+
+ openLink: action('openLink'),
+ scrollToQuotedMessage: action('scrollToQuotedMessage'),
+ showExpiredIncomingTapToViewToast: action(
+ 'showExpiredIncomingTapToViewToast'
+ ),
+ showExpiredOutgoingTapToViewToast: action(
+ 'showExpiredOutgoingTapToViewToast'
+ ),
+
+ showIdentity: action('showIdentity'),
+
+ downloadNewVersion: action('downloadNewVersion'),
+});
+
+const renderItem = (id: string) => (
+ }
+ item={items[id]}
+ i18n={i18n}
+ conversationId=""
+ conversationAccepted
+ {...actions()}
+ />
+);
+
+const renderLastSeenIndicator = () => (
+
+);
+const renderHeroRow = () => ;
+const renderLoadingRow = () => ;
+const renderTypingBubble = () => (
+
+);
+
+const createProps = (overrideProps: Partial = {}): Props => ({
+ i18n,
+
+ haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
+ haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
+ isLoadingMessages: false,
+ items: Object.keys(items),
+ resetCounter: 0,
+ scrollToIndex: overrideProps.scrollToIndex,
+ scrollToIndexCounter: 0,
+ totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
+ oldestUnreadIndex:
+ number('oldestUnreadIndex', overrideProps.oldestUnreadIndex || 0) ||
+ undefined,
+
+ id: '',
+ renderItem,
+ renderLastSeenIndicator,
+ renderHeroRow,
+ renderLoadingRow,
+ renderTypingBubble,
+ typingContact: boolean(
+ 'typingContact',
+ !!overrideProps.typingContact || false
+ ),
+
+ ...actions(),
+});
+
+story.add('Oldest and Newest', () => {
+ const props = createProps();
+
+ return ;
+});
+
+story.add('Last Seen', () => {
+ const props = createProps({
+ oldestUnreadIndex: 13,
+ totalUnread: 2,
+ });
+
+ return ;
+});
+
+story.add('Target Index to Top', () => {
+ const props = createProps({
+ scrollToIndex: 0,
+ });
+
+ return ;
+});
+
+story.add('Typing Indicator', () => {
+ const props = createProps({
+ typingContact: true,
+ });
+
+ return ;
+});
+
+story.add('Without Newest Message', () => {
+ const props = createProps({
+ haveNewest: false,
+ });
+
+ return ;
+});
+
+story.add('Without Oldest Message', () => {
+ const props = createProps({
+ haveOldest: false,
+ scrollToIndex: -1,
+ });
+
+ return ;
+});
diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx
index 616426793..8ff6052db 100644
--- a/ts/components/conversation/Timeline.tsx
+++ b/ts/components/conversation/Timeline.tsx
@@ -78,7 +78,7 @@ type PropsActionsType = {
} & MessageActionsType &
SafetyNumberActionsType;
-type Props = PropsDataType & PropsHousekeepingType & PropsActionsType;
+export type Props = PropsDataType & PropsHousekeepingType & PropsActionsType;
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
type RowRendererParamsType = {
diff --git a/ts/components/conversation/TimelineLoadingRow.md b/ts/components/conversation/TimelineLoadingRow.md
deleted file mode 100644
index 0ad2982e1..000000000
--- a/ts/components/conversation/TimelineLoadingRow.md
+++ /dev/null
@@ -1,28 +0,0 @@
-### Idle
-
-```jsx
-
-
-
-```
-
-### Countdown
-
-```jsx
-
- console.log('onComplete')}
- />
-
-```
-
-### Loading
-
-```jsx
-
-
-
-```
diff --git a/ts/components/conversation/TimelineLoadingRow.stories.tsx b/ts/components/conversation/TimelineLoadingRow.stories.tsx
new file mode 100644
index 000000000..93dff1d6e
--- /dev/null
+++ b/ts/components/conversation/TimelineLoadingRow.stories.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { date, number, select } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+
+import { Props, TimelineLoadingRow } from './TimelineLoadingRow';
+
+const story = storiesOf('Components/Conversation/TimelineLoadingRow', module);
+
+const createProps = (overrideProps: Partial = {}): Props => ({
+ state: select(
+ 'state',
+ { idle: 'idle', countdown: 'countdown', loading: 'loading' },
+ overrideProps.state || 'idle'
+ ),
+ duration: number('duration', overrideProps.duration || 0),
+ expiresAt: date('expiresAt', new Date(overrideProps.expiresAt || Date.now())),
+ onComplete: action('onComplete'),
+});
+
+story.add('Idle', () => {
+ const props = createProps();
+
+ return ;
+});
+
+story.add('Countdown', () => {
+ const props = createProps({
+ state: 'countdown',
+ duration: 40000,
+ expiresAt: Date.now() + 20000,
+ });
+
+ return ;
+});
+
+story.add('Loading', () => {
+ const props = createProps({ state: 'loading' });
+
+ return ;
+});
diff --git a/ts/components/conversation/TimelineLoadingRow.tsx b/ts/components/conversation/TimelineLoadingRow.tsx
index 6f6689c9a..ea72c6044 100644
--- a/ts/components/conversation/TimelineLoadingRow.tsx
+++ b/ts/components/conversation/TimelineLoadingRow.tsx
@@ -6,7 +6,7 @@ import { Spinner } from '../Spinner';
export type STATE_ENUM = 'idle' | 'countdown' | 'loading';
-type Props = {
+export type Props = {
state: STATE_ENUM;
duration?: number;
expiresAt?: number;