app-tutor-ai-consumer 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.github/workflows/staging-staging.yml +148 -0
  2. package/.github/workflows/staging.yml +1 -2
  3. package/CHANGELOG.md +13 -0
  4. package/config/rspack/rspack.config.js +5 -1
  5. package/config/vitest/__mocks__/icons.tsx +3 -0
  6. package/config/vitest/__mocks__/intersection-observer.ts +10 -0
  7. package/config/vitest/__mocks__/sparkie.tsx +2 -11
  8. package/config/vitest/__mocks__/use-init-sparkie.tsx +14 -0
  9. package/config/vitest/vitest.config.mts +13 -8
  10. package/environments/.env.test +2 -0
  11. package/package.json +3 -3
  12. package/public/index.html +3 -4
  13. package/src/config/styles/global.css +2 -2
  14. package/src/config/tanstack/query-client.ts +2 -1
  15. package/src/config/tests/utils.tsx +3 -2
  16. package/src/config/tests/wrappers.tsx +4 -1
  17. package/src/index.tsx +22 -0
  18. package/src/lib/components/icons/arrow-down.svg +5 -0
  19. package/src/lib/components/icons/chevron-down.svg +4 -0
  20. package/src/lib/components/icons/icon-names.d.ts +1 -1
  21. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +7 -9
  22. package/src/lib/hooks/index.ts +3 -0
  23. package/src/lib/hooks/use-intersection-observer-reverse-scroll/index.ts +2 -0
  24. package/src/lib/hooks/use-intersection-observer-reverse-scroll/use-intersection-observer-reverse-scroll.tsx +147 -0
  25. package/src/lib/hooks/use-ref-client-height/index.ts +2 -0
  26. package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +38 -0
  27. package/src/lib/hooks/use-scroll-to-ref/index.ts +2 -0
  28. package/src/lib/hooks/use-scroll-to-ref/use-scroll-to-ref.tsx +14 -0
  29. package/src/lib/hooks/use-throttle/index.ts +3 -0
  30. package/src/lib/hooks/use-throttle/types.ts +13 -0
  31. package/src/lib/hooks/use-throttle/use-throttle.spec.tsx +296 -0
  32. package/src/lib/hooks/use-throttle/use-throttle.tsx +91 -0
  33. package/src/main/main.spec.tsx +9 -0
  34. package/src/modules/cursor/__tests__/icursor-update.builder.ts +42 -0
  35. package/src/modules/cursor/hooks/index.ts +1 -0
  36. package/src/modules/cursor/hooks/use-update-cursor/index.ts +2 -0
  37. package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.spec.tsx +23 -0
  38. package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.ts +11 -0
  39. package/src/modules/cursor/index.ts +2 -0
  40. package/src/modules/cursor/service.ts +15 -0
  41. package/src/modules/cursor/types.ts +9 -0
  42. package/src/modules/global-providers/index.ts +1 -0
  43. package/src/modules/messages/__tests__/parsed-message.builder.ts +164 -0
  44. package/src/modules/messages/components/message-item/message-item.spec.tsx +2 -2
  45. package/src/modules/messages/components/message-item/message-item.tsx +14 -1
  46. package/src/modules/messages/components/message-item-end-of-scroll/index.ts +2 -0
  47. package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx +14 -0
  48. package/src/modules/messages/components/message-item-error/index.ts +2 -0
  49. package/src/modules/messages/components/message-item-error/message-item-error.tsx +25 -0
  50. package/src/modules/messages/components/message-item-loading/index.ts +2 -0
  51. package/src/modules/messages/components/message-item-loading/message-item-loading.tsx +16 -0
  52. package/src/modules/messages/components/messages-list/index.ts +1 -1
  53. package/src/modules/messages/components/messages-list/messages-list.tsx +69 -39
  54. package/src/modules/messages/constants.ts +1 -0
  55. package/src/modules/messages/hooks/index.ts +3 -0
  56. package/src/modules/messages/hooks/use-all-messages/index.ts +2 -0
  57. package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +30 -0
  58. package/src/modules/messages/hooks/use-infinite-get-messages/index.ts +2 -0
  59. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +58 -0
  60. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +97 -0
  61. package/src/modules/messages/hooks/use-manage-scroll/index.ts +2 -0
  62. package/src/modules/messages/hooks/use-manage-scroll/use-manage-scroll.tsx +66 -0
  63. package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.spec.tsx +58 -0
  64. package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.ts +30 -0
  65. package/src/modules/messages/utils/has-to-update-cursor/index.ts +2 -0
  66. package/src/modules/sparkie/__tests__/sparkie.mock.ts +33 -0
  67. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +6 -0
  68. package/src/modules/widget/components/chat-page/chat-page.spec.tsx +28 -0
  69. package/src/modules/widget/components/chat-page/chat-page.tsx +1 -3
  70. package/src/modules/widget/components/container/container.tsx +20 -14
  71. package/src/modules/widget/components/index.ts +1 -0
  72. package/src/modules/widget/components/scroll-to-bottom-button/index.ts +2 -0
  73. package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +32 -0
  74. package/src/modules/widget/events.ts +4 -0
  75. package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +8 -6
  76. package/src/modules/widget/store/index.ts +1 -0
  77. package/src/modules/widget/store/widget-container-intrinsic-height.atom.ts +13 -0
@@ -0,0 +1,23 @@
1
+ import { chance, renderHook, waitFor } from '@/src/config/tests'
2
+
3
+ import useUpdateCursor from './use-update-cursor'
4
+
5
+ describe('useUpdateCursor', () => {
6
+ const conversationId = chance.guid()
7
+ const createHook = () => renderHook(useUpdateCursor)
8
+
9
+ it('should update the cursor given a conversationId', async () => {
10
+ const { result } = createHook()
11
+
12
+ result.current.mutate(conversationId)
13
+
14
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
15
+
16
+ expect(result.current.data).toMatchObject({
17
+ threadId: expect.any(String) as string,
18
+ contactId: expect.any(String) as string,
19
+ cursor: expect.any(Number) as number,
20
+ conversationId: expect.any(String) as string
21
+ })
22
+ })
23
+ })
@@ -0,0 +1,11 @@
1
+ import { useMutation } from '@tanstack/react-query'
2
+
3
+ import { CursorService } from '../..'
4
+
5
+ function useUpdateCursor() {
6
+ return useMutation({
7
+ mutationFn: (conversationId: string) => CursorService.updateCursor(conversationId)
8
+ })
9
+ }
10
+
11
+ export default useUpdateCursor
@@ -0,0 +1,2 @@
1
+ export { default as CursorService } from './service'
2
+ export * from './types'
@@ -0,0 +1,15 @@
1
+ import { SparkieService } from '../sparkie'
2
+
3
+ import type { ICursorUpdate } from './types'
4
+
5
+ class CursorService {
6
+ constructor(private sparkie = SparkieService.sparkieInstance) {}
7
+
8
+ async updateCursor(conversationId: string): Promise<ICursorUpdate | null> {
9
+ const data = await this.sparkie.cursorService?.update(conversationId)
10
+
11
+ return data ?? null
12
+ }
13
+ }
14
+
15
+ export default new CursorService()
@@ -0,0 +1,9 @@
1
+ export interface ICursor {
2
+ threadId: string
3
+ contactId: string
4
+ cursor: number
5
+ }
6
+
7
+ export interface ICursorUpdate extends ICursor {
8
+ conversationId?: string
9
+ }
@@ -1 +1,2 @@
1
+ export * from './global-providers'
1
2
  export { default as GlobalProviders } from './global-providers'
@@ -0,0 +1,164 @@
1
+ import type { ParsedMessage } from '../types'
2
+ import { msgParser } from '../utils'
3
+
4
+ import IMessageWithSenderDataBuilder from './imessage-with-sender-data.builder'
5
+
6
+ const msg = new IMessageWithSenderDataBuilder()
7
+ const parsedMsg = msgParser(msg, msg.contactId)
8
+
9
+ class ParsedMessageBuilder implements ParsedMessage {
10
+ from: string
11
+ id: string
12
+ isRead: boolean
13
+ time: string
14
+ contact: { id: string; name?: string; picture?: string; userId?: number }
15
+ metadata: Record<string, unknown> & {
16
+ author: 'ai' | 'user'
17
+ sessionId: string
18
+ externalId: string
19
+ correlationId: string
20
+ }
21
+ parentId: string
22
+ sending: boolean
23
+ threadId: string
24
+ timestamp: number
25
+ type: string
26
+ text?: string
27
+ name?: string
28
+ url?: string
29
+ dimensions?: { width: number; height: number }
30
+ thumbnails?: { lg: string; md: string; sm: string }
31
+ caption?: string
32
+ loading?: boolean
33
+
34
+ constructor() {
35
+ this.from = parsedMsg.from
36
+ this.id = parsedMsg.id
37
+ this.isRead = parsedMsg.isRead
38
+ this.time = parsedMsg.time
39
+ this.contact = parsedMsg.contact
40
+ this.metadata = parsedMsg.metadata
41
+ this.parentId = parsedMsg.parentId ?? 'no-parent'
42
+ this.sending = parsedMsg.sending ?? false
43
+ this.threadId = parsedMsg.threadId
44
+ this.timestamp = parsedMsg.timestamp
45
+ this.type = parsedMsg.type
46
+ this.text = parsedMsg.text
47
+ this.name = parsedMsg.name
48
+ this.url = parsedMsg.url
49
+ this.dimensions = parsedMsg.dimensions
50
+ this.thumbnails = parsedMsg.thumbnails
51
+ this.caption = parsedMsg.caption
52
+ this.loading = parsedMsg.loading
53
+ }
54
+
55
+ withFrom(from: typeof this.from) {
56
+ this.from = from
57
+
58
+ return this
59
+ }
60
+
61
+ withId(id: typeof this.id) {
62
+ this.id = id
63
+
64
+ return this
65
+ }
66
+
67
+ withIsRead(isRead: typeof this.isRead) {
68
+ this.isRead = isRead
69
+
70
+ return this
71
+ }
72
+
73
+ withTime(time: typeof this.time) {
74
+ this.time = time
75
+
76
+ return this
77
+ }
78
+
79
+ withContact(contact: typeof this.contact) {
80
+ this.contact = contact
81
+
82
+ return this
83
+ }
84
+
85
+ withMetadata(metadata: typeof this.metadata) {
86
+ this.metadata = metadata
87
+
88
+ return this
89
+ }
90
+
91
+ withParentId(parentId: typeof this.parentId) {
92
+ this.parentId = parentId
93
+
94
+ return this
95
+ }
96
+
97
+ withSending(sending: typeof this.sending) {
98
+ this.sending = sending
99
+
100
+ return this
101
+ }
102
+
103
+ withThreadId(threadId: typeof this.threadId) {
104
+ this.threadId = threadId
105
+
106
+ return this
107
+ }
108
+
109
+ withTimestamp(timestamp: typeof this.timestamp) {
110
+ this.timestamp = timestamp
111
+
112
+ return this
113
+ }
114
+
115
+ withType(type: typeof this.type) {
116
+ this.type = type
117
+
118
+ return this
119
+ }
120
+
121
+ withText(text: typeof this.text) {
122
+ this.text = text
123
+
124
+ return this
125
+ }
126
+
127
+ withName(name: typeof this.name) {
128
+ this.name = name
129
+
130
+ return this
131
+ }
132
+
133
+ withUrl(url: typeof this.url) {
134
+ this.url = url
135
+
136
+ return this
137
+ }
138
+
139
+ withDimensions(dimensions: typeof this.dimensions) {
140
+ this.dimensions = dimensions
141
+
142
+ return this
143
+ }
144
+
145
+ withThumbnails(thumbnails: typeof this.thumbnails) {
146
+ this.thumbnails = thumbnails
147
+
148
+ return this
149
+ }
150
+
151
+ withCaption(caption: typeof this.caption) {
152
+ this.caption = caption
153
+
154
+ return this
155
+ }
156
+
157
+ withLoading(loading: typeof this.loading) {
158
+ this.loading = loading
159
+
160
+ return this
161
+ }
162
+ }
163
+
164
+ export default ParsedMessageBuilder
@@ -1,11 +1,11 @@
1
1
  import { render, screen } from '@/src/config/tests'
2
2
  import { TEST_MARKDOWN_STUB } from '@/src/lib/components/markdownrenderer/__tests__/markdown.stub'
3
- import type { ParsedMessage } from '../../types'
3
+ import ParsedMessageBuilder from '../../__tests__/parsed-message.builder'
4
4
 
5
5
  import MessageItem from './message-item'
6
6
 
7
7
  describe('MessageItem', () => {
8
- const message = { text: TEST_MARKDOWN_STUB } as ParsedMessage
8
+ const message = new ParsedMessageBuilder().withText(TEST_MARKDOWN_STUB)
9
9
 
10
10
  const renderComponent = (props = { message }) => render(<MessageItem {...props} />)
11
11
 
@@ -1,3 +1,4 @@
1
+ import clsx from 'clsx'
1
2
  import type { Components } from 'react-markdown'
2
3
 
3
4
  import { MarkdownRenderer } from '@/src/lib/components'
@@ -9,7 +10,19 @@ const imgComponent: Components['img'] = ({ src }) => {
9
10
  }
10
11
 
11
12
  function MessageItem({ message }: { message: ParsedMessage }) {
12
- return <MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
13
+ return (
14
+ <div
15
+ data-test='messages-item'
16
+ className={clsx(
17
+ 'max-w-[min(80%,52rem)] overflow-x-hidden rounded-lg px-3 text-sm/normal text-neutral-50',
18
+ {
19
+ 'self-end bg-neutral-800': message.metadata.author === 'user',
20
+ 'bg-ai-chat-response': message.metadata.author === 'ai'
21
+ }
22
+ )}>
23
+ <MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
24
+ </div>
25
+ )
13
26
  }
14
27
 
15
28
  export default MessageItem
@@ -0,0 +1,2 @@
1
+ export * from './message-item-end-of-scroll'
2
+ export { default as MessageItemEndOfScroll } from './message-item-end-of-scroll'
@@ -0,0 +1,14 @@
1
+ export type MessageItemEndOfScrollProps = { show?: boolean }
2
+
3
+ // TODO: [PLACEHOLDER] Refactor using the PD choice for End of Scroll
4
+ function MessageItemEndOfScroll({ show = true }: MessageItemEndOfScrollProps) {
5
+ if (!show) return null
6
+
7
+ return (
8
+ <div className='rounded bg-gray-800/50 p-4 text-center text-gray-400'>
9
+ 🎉 You&apos;ve reached the beginning of the conversation!
10
+ </div>
11
+ )
12
+ }
13
+
14
+ export default MessageItemEndOfScroll
@@ -0,0 +1,2 @@
1
+ export * from './message-item-error'
2
+ export { default as MessageItemError } from './message-item-error'
@@ -0,0 +1,25 @@
1
+ import type { MouseEventHandler, ReactNode } from 'react'
2
+
3
+ export type MessageItemErrorProps = {
4
+ message?: ReactNode
5
+ retry?: MouseEventHandler<HTMLButtonElement>
6
+ show?: boolean
7
+ }
8
+
9
+ // TODO: [PLACEHOLDER] Refactor using the PD choice for Error Handling
10
+ function MessageItemError({ message, retry, show = true }: MessageItemErrorProps) {
11
+ if (!show) return null
12
+
13
+ return (
14
+ <div className='rounded bg-red-900/20 p-4 text-center text-red-400'>
15
+ {message}
16
+ <button
17
+ onClick={retry}
18
+ className='ml-2 cursor-pointer rounded bg-danger-600 px-3 py-1 text-sm text-danger-100 transition-colors hover:bg-danger-700'>
19
+ Retry
20
+ </button>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ export default MessageItemError
@@ -0,0 +1,2 @@
1
+ export * from './message-item-loading'
2
+ export { default as MessageItemLoading } from './message-item-loading'
@@ -0,0 +1,16 @@
1
+ import { Spinner } from '@/src/lib/components'
2
+
3
+ export type MessageItemLoadingProps = { show?: boolean }
4
+
5
+ // TODO: [PLACEHOLDER] Refactor using the PD choice for loading new messages
6
+ function MessageItemLoading({ show = true }: MessageItemLoadingProps) {
7
+ if (!show) return null
8
+
9
+ return (
10
+ <div className='flex flex-col items-center justify-center p-8'>
11
+ <Spinner className='inline-flex h-6 w-6 animate-spin text-neutral-200' />
12
+ </div>
13
+ )
14
+ }
15
+
16
+ export default MessageItemLoading
@@ -1,2 +1,2 @@
1
- export * from './messages-list'
2
1
  export { default as MessagesList } from './messages-list'
2
+ export * from './messages-list'
@@ -1,51 +1,81 @@
1
- import { useRef } from 'react'
1
+ import { lazy, useCallback, useRef } from 'react'
2
2
  import clsx from 'clsx'
3
3
 
4
- import { useGetProfile } from '@/src/modules/profile'
5
- import { useWidgetSettingsAtomValue } from '@/src/modules/widget'
6
- import { useFetchMessages } from '../../hooks'
4
+ import { useRefClientHeight } from '@/src/lib/hooks'
5
+ import { useAllMessages, useManageScroll } from '../../hooks'
7
6
  import { MessageItem } from '../message-item'
8
7
 
8
+ const MessageItemError = lazy(() => import('../message-item-error/message-item-error'))
9
+
10
+ const MessageItemLoading = lazy(() => import('../message-item-loading/message-item-loading'))
11
+
12
+ const MessageItemEndOfScroll = lazy(
13
+ () => import('../message-item-end-of-scroll/message-item-end-of-scroll')
14
+ )
15
+
16
+ const ScrollToBottomButton = lazy(
17
+ () => import('@/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button')
18
+ )
19
+
9
20
  function MessagesList() {
10
- const loadFirstPage = useRef(true)
11
- const currentMessages = useRef([])
12
- const settings = useWidgetSettingsAtomValue()
13
- const profileQuery = useGetProfile()
21
+ const scrollerRef = useRef<HTMLDivElement>(null)
22
+ const scrollerClientHeight = useRefClientHeight(scrollerRef)
23
+ const scrollToButtonRef = useRef<HTMLButtonElement>(null)
24
+ const { allMessages, messagesQuery } = useAllMessages()
25
+
26
+ const { showScrollButton } = useManageScroll(scrollerRef)
14
27
 
15
- const messagesQuery = useFetchMessages({
16
- conversationId: settings?.conversationId ?? '',
17
- currentMessages: currentMessages.current,
18
- loadFirstPage: loadFirstPage?.current ?? true,
19
- profileId: profileQuery.data?.id ?? '',
20
- enabled: Boolean(settings?.conversationId && profileQuery.data?.id)
21
- })
28
+ const scrollToBottom = useCallback(() => {
29
+ const { current: scroller } = scrollerRef
22
30
 
23
- if (messagesQuery.isLoading) return <h3>Loading...</h3>
31
+ if (!scroller) return
32
+
33
+ scroller.scrollTo({
34
+ top: scroller.scrollHeight,
35
+ behavior: 'smooth'
36
+ })
37
+ }, [])
24
38
 
25
39
  return (
26
- <div className='flex flex-1 flex-col justify-center gap-6'>
27
- {messagesQuery.data?.messages &&
28
- Array.from(messagesQuery.data.messages).map(([publishingDate, messages], i) => (
29
- <div key={i} className='flex flex-1 flex-col justify-center gap-6'>
30
- <span className='self-center rounded-full border border-neutral-700 bg-neutral-800 px-4 py-2 text-xs capitalize text-neutral-50'>
31
- {publishingDate}
32
- </span>
33
- {messages.map((msg, k) => (
34
- <div
35
- key={`${msg.id}-${k}`}
36
- className={clsx(
37
- 'max-w-[min(80%,52rem)] rounded-lg p-3 text-sm/normal text-neutral-0',
38
- {
39
- 'self-end bg-neutral-800': msg.from === 'me' || msg.metadata.author === 'user',
40
- 'bg-ai-chat-response':
41
- msg.from !== profileQuery?.data?.id || msg.metadata.author === 'ai'
42
- }
43
- )}>
44
- <MessageItem message={msg} />
45
- </div>
46
- ))}
47
- </div>
48
- ))}
40
+ <div
41
+ ref={scrollerRef}
42
+ className={clsx(
43
+ 'relative mx-2 flex flex-col gap-2 overflow-auto p-4',
44
+ `h-[${scrollerClientHeight}]`
45
+ )}>
46
+ <MessageItemLoading show={messagesQuery.isFetching} />
47
+
48
+ <MessageItemEndOfScroll
49
+ show={!messagesQuery.isFetching && !messagesQuery.hasNextPage && allMessages.length > 0}
50
+ />
51
+
52
+ <ScrollToBottomButton
53
+ ref={scrollToButtonRef}
54
+ top={Math.abs(
55
+ Number(scrollerRef.current?.clientHeight) -
56
+ Number(scrollToButtonRef.current?.clientHeight) -
57
+ 24
58
+ )}
59
+ show={showScrollButton}
60
+ onClick={scrollToBottom}
61
+ />
62
+
63
+ {allMessages?.map(([publishingDate, messages], i) => (
64
+ <div key={i} className='flex flex-1 flex-col justify-center gap-6'>
65
+ <span className='self-center rounded-full border border-neutral-700 bg-neutral-800 px-4 py-2 text-xs capitalize text-neutral-50'>
66
+ {publishingDate}
67
+ </span>
68
+ {messages.map((msg, k) => (
69
+ <MessageItem key={`${msg.id}-${k}`} message={msg} />
70
+ ))}
71
+ </div>
72
+ ))}
73
+
74
+ <MessageItemError
75
+ show={Boolean(messagesQuery.error)}
76
+ message={`❌ Error loading messages: ${messagesQuery.error?.message ?? ''}`}
77
+ retry={() => void messagesQuery.refetch()}
78
+ />
49
79
  </div>
50
80
  )
51
81
  }
@@ -1 +1,2 @@
1
1
  export const MSG_MAX_COUNT = 20
2
+ export const MSG_MAX_PAGES = 20
@@ -1 +1,4 @@
1
+ export * from './use-all-messages'
1
2
  export * from './use-fetch-messages'
3
+ export * from './use-infinite-get-messages'
4
+ export * from './use-manage-scroll'
@@ -0,0 +1,2 @@
1
+ export * from './use-all-messages'
2
+ export { default as useAllMessages } from './use-all-messages'
@@ -0,0 +1,30 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import { useGetProfile } from '@/src/modules/profile'
4
+ import { useWidgetSettingsAtomValue } from '@/src/modules/widget'
5
+ import { useInfiniteGetMessages } from '../use-infinite-get-messages'
6
+
7
+ function useAllMessages() {
8
+ const settings = useWidgetSettingsAtomValue()
9
+ const profileQuery = useGetProfile()
10
+
11
+ const messagesQuery = useInfiniteGetMessages({
12
+ conversationId: settings?.conversationId ?? '',
13
+ profileId: profileQuery.data?.id ?? '',
14
+ enabled: Boolean(settings?.conversationId && profileQuery.data?.id)
15
+ })
16
+
17
+ const allMessages = useMemo(
18
+ () =>
19
+ messagesQuery.data && Number(messagesQuery.data?.size) > 0
20
+ ? Array.from(messagesQuery.data)
21
+ : [],
22
+ [messagesQuery.data]
23
+ )
24
+
25
+ const hasMessages = useMemo(() => Number(allMessages.length) > 0, [allMessages.length])
26
+
27
+ return { messagesQuery, allMessages, hasMessages }
28
+ }
29
+
30
+ export default useAllMessages
@@ -0,0 +1,2 @@
1
+ export * from './use-infinite-get-messages'
2
+ export { default as useInfiniteGetMessages } from './use-infinite-get-messages'
@@ -0,0 +1,58 @@
1
+ import { act, renderHook, waitFor } from '@/src/config/tests'
2
+ import { SparkieMessageServiceMock } from '@/src/modules/sparkie/__tests__/sparkie.mock'
3
+ import IMessageWithSenderDataMock from '../../__tests__/imessage-with-sender-data.mock'
4
+ import type { IMessageWithSenderData } from '../../types'
5
+
6
+ import useInfiniteGetMessages from './use-infinite-get-messages'
7
+
8
+ describe('useInfiniteGetMessages', () => {
9
+ const mockConversationId = 'conversation-123'
10
+ const mockProfileId = 'profile-456'
11
+ const mockEnabled = true
12
+
13
+ beforeEach(() => {
14
+ SparkieMessageServiceMock.getAll.mockClear()
15
+ })
16
+
17
+ const createHook = (
18
+ props = {
19
+ conversationId: mockConversationId,
20
+ profileId: mockProfileId,
21
+ enabled: mockEnabled
22
+ }
23
+ ) => renderHook(() => useInfiniteGetMessages(props))
24
+
25
+ it('should return a map with the right size when fetch messages request is successful', async () => {
26
+ const { result } = createHook()
27
+
28
+ await waitFor(() => expect(result.current.data).toBeDefined())
29
+
30
+ expect(result.current.data?.size).toBe(10)
31
+ })
32
+
33
+ it('should be able to fetch next pages', async () => {
34
+ SparkieMessageServiceMock.getAll.mockRestore()
35
+ SparkieMessageServiceMock.getAll.mockReturnValueOnce(
36
+ new IMessageWithSenderDataMock().getMany(2) as IMessageWithSenderData[]
37
+ )
38
+
39
+ const { result } = createHook()
40
+
41
+ await waitFor(() => {
42
+ expect(result.current.isSuccess).toBe(true)
43
+ })
44
+
45
+ expect(result.current.data?.size).toBe(10)
46
+
47
+ act(() => {
48
+ void result.current.fetchNextPage()
49
+ })
50
+
51
+ await waitFor(() => {
52
+ expect(result.current.fetchStatus).toBe('idle')
53
+ })
54
+
55
+ expect(result.current.data?.size).toBe(2)
56
+ expect(result.current.hasNextPage).toBe(false)
57
+ })
58
+ })