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;