-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add useChatLogger hook (#2793)
* feat: add useChatLogger hook * chore: adjust Chat type annotation * chore: add missing peer/dev deps
- Loading branch information
jb-twilio
committed
Nov 17, 2022
1 parent
bd6f229
commit 80cb7ce
Showing
17 changed files
with
484 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@twilio-paste/chat-log': minor | ||
'@twilio-paste/core': minor | ||
--- | ||
|
||
[ChatLog]: add useChatLogger hook |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/paste-core/components/chat-log/__tests__/ChatLogger.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as React from 'react'; | ||
import {screen, render} from '@testing-library/react'; | ||
|
||
import {ChatLogger, ChatMessage, ChatBubble} from '../src'; | ||
import type {Chat} from '../src/useChatLogger'; | ||
|
||
const chats: Chat[] = [ | ||
{ | ||
id: 'uid1', | ||
variant: 'inbound', | ||
content: ( | ||
<ChatMessage variant="inbound"> | ||
<ChatBubble>hi</ChatBubble> | ||
</ChatMessage> | ||
), | ||
}, | ||
{ | ||
id: 'uid2', | ||
variant: 'outbound', | ||
content: ( | ||
<ChatMessage variant="outbound"> | ||
<ChatBubble>hello</ChatBubble> | ||
</ChatMessage> | ||
), | ||
}, | ||
]; | ||
|
||
describe('ChatLogger', () => { | ||
it('should render', () => { | ||
render(<ChatLogger chats={chats} />); | ||
expect(screen.getByRole('log')).toBeDefined(); | ||
expect(screen.getByRole('list')).toBeDefined(); | ||
expect(screen.getAllByRole('listitem')).toHaveLength(2); | ||
}); | ||
}); |
88 changes: 88 additions & 0 deletions
88
packages/paste-core/components/chat-log/__tests__/useChatLogger.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import * as React from 'react'; | ||
import {renderHook, act} from '@testing-library/react-hooks'; | ||
|
||
import {useChatLogger, ChatBubble, ChatMessage} from '../src'; | ||
|
||
const chat = { | ||
id: 'custom-id-123', | ||
variant: 'inbound', | ||
content: ( | ||
<ChatMessage variant="inbound"> | ||
<ChatBubble>hi</ChatBubble> | ||
</ChatMessage> | ||
), | ||
} as const; | ||
|
||
describe('useChatLogger', () => { | ||
it('returns expected result with defaults', () => { | ||
const {result} = renderHook(() => useChatLogger()); | ||
|
||
expect(result.current).toMatchObject({ | ||
chats: [], | ||
pop: expect.any(Function), | ||
push: expect.any(Function), | ||
}); | ||
}); | ||
|
||
it('returns expected result with initialization', () => { | ||
const {result} = renderHook(() => useChatLogger(chat)); | ||
|
||
expect(result.current.chats).toHaveLength(1); | ||
expect(result.current.pop).toBeInstanceOf(Function); | ||
expect(result.current.push).toBeInstanceOf(Function); | ||
expect(result.current.chats[0]).toMatchObject(chat); | ||
}); | ||
|
||
describe('push', () => { | ||
it('pushes new chats with an id', () => { | ||
const {result} = renderHook(() => useChatLogger()); | ||
expect(result.current.chats).toHaveLength(0); | ||
|
||
act(() => { | ||
result.current.push(chat); | ||
}); | ||
|
||
expect(result.current.chats).toHaveLength(1); | ||
expect(result.current.chats[0]).toMatchObject(chat); | ||
}); | ||
|
||
it('pushes new chats without an id', () => { | ||
const {result} = renderHook(() => useChatLogger()); | ||
expect(result.current.chats).toHaveLength(0); | ||
|
||
act(() => { | ||
const chatWithoutCustomId = {...chat, id: undefined}; | ||
result.current.push(chatWithoutCustomId); | ||
}); | ||
|
||
expect(result.current.chats).toHaveLength(1); | ||
expect(result.current.chats[0]).toMatchObject({ | ||
id: expect.stringMatching(/^uid/), | ||
}); | ||
}); | ||
}); | ||
|
||
describe('pop', () => { | ||
it('pops chats with an id', () => { | ||
const {result} = renderHook(() => useChatLogger(chat)); | ||
expect(result.current.chats).toHaveLength(1); | ||
|
||
act(() => { | ||
result.current.pop(chat.id); | ||
}); | ||
|
||
expect(result.current.chats).toHaveLength(0); | ||
}); | ||
|
||
it('pops chats without an id', () => { | ||
const {result} = renderHook(() => useChatLogger(chat)); | ||
expect(result.current.chats).toHaveLength(1); | ||
|
||
act(() => { | ||
result.current.pop(); | ||
}); | ||
|
||
expect(result.current.chats).toHaveLength(0); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
packages/paste-core/components/chat-log/src/ChatLogger.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import * as React from 'react'; | ||
import {Box} from '@twilio-paste/box'; | ||
import {useTransition, animated, useReducedMotion} from '@twilio-paste/animation-library'; | ||
|
||
import {ChatLog} from './ChatLog'; | ||
import type {Chat} from './useChatLogger'; | ||
|
||
const AnimatedChat = animated(Box); | ||
type StyleProps = React.ComponentProps<typeof AnimatedChat>['style']; | ||
|
||
export interface ChatLoggerProps { | ||
chats: Chat[]; | ||
children?: never; | ||
} | ||
|
||
const buildTransitionX = (chat: Chat): number => { | ||
if (chat.variant === 'inbound') return -100; | ||
if (chat.variant === 'outbound') return 100; | ||
return 0; | ||
}; | ||
|
||
const ChatLogger: React.FC<ChatLoggerProps> = ({chats}) => { | ||
const transitions = useTransition(chats, { | ||
keys: (chat: Chat) => chat.id, | ||
from: (chat: Chat): StyleProps => ({opacity: 0, x: buildTransitionX(chat)}), | ||
enter: {opacity: 1, x: 0}, | ||
leave: (chat: Chat): StyleProps => ({opacity: 0, x: buildTransitionX(chat)}), | ||
config: { | ||
mass: 1, | ||
tension: 150, | ||
friction: 20, | ||
}, | ||
}); | ||
|
||
const animatedChats = useReducedMotion() | ||
? chats.map((chat) => React.cloneElement(chat.content, {key: chat.id})) | ||
: transitions((styles: StyleProps, chat: Chat, {key}: {key: string}) => ( | ||
<AnimatedChat as="div" style={styles} key={key}> | ||
{chat.content} | ||
</AnimatedChat> | ||
)); | ||
|
||
return <ChatLog>{animatedChats}</ChatLog>; | ||
}; | ||
|
||
ChatLogger.displayName = 'ChatLogger'; | ||
|
||
export {ChatLogger}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
packages/paste-core/components/chat-log/src/useChatLogger.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import * as React from 'react'; | ||
import {uid} from '@twilio-paste/uid-library'; | ||
|
||
import type {MessageVariants} from './MessageVariantContext'; | ||
|
||
type PushChat = (chat: PartialIDChat) => void; | ||
type PopChat = (id?: string) => void; | ||
|
||
export type Chat = { | ||
id: string; | ||
variant?: MessageVariants; | ||
content: React.ReactElement; | ||
}; | ||
|
||
export type PartialIDChat = Omit<Chat, 'id'> & Partial<Pick<Chat, 'id'>>; | ||
|
||
export type UseChatLogger = (...initialChats: PartialIDChat[]) => { | ||
chats: Chat[]; | ||
push: PushChat; | ||
pop: PopChat; | ||
}; | ||
|
||
const chatWithId = (chat: PartialIDChat): Chat => ({...chat, id: chat.id || uid(chat.content)}); | ||
|
||
export const useChatLogger: UseChatLogger = (...initialChats) => { | ||
const parsedInitialChats = React.useMemo(() => initialChats.map(chatWithId), [initialChats]); | ||
|
||
const [chats, setChats] = React.useState<Chat[]>(parsedInitialChats); | ||
|
||
const push: PushChat = React.useCallback( | ||
(next) => { | ||
setChats((prev) => prev.concat(chatWithId(next))); | ||
}, | ||
[setChats] | ||
); | ||
|
||
const pop: PopChat = React.useCallback( | ||
(id) => { | ||
setChats((prev) => (id ? prev.filter((chat) => chat.id !== id) : prev.slice(0, -1))); | ||
}, | ||
[setChats] | ||
); | ||
|
||
return {push, pop, chats}; | ||
}; |
Oops, something went wrong.