app-tutor-ai-consumer 1.33.0 → 1.34.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 (122) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +1 -1
  3. package/package.json +1 -1
  4. package/src/bootstrap.ts +40 -0
  5. package/src/config/tests/handlers.ts +5 -4
  6. package/src/config/theme/init-theme.ts +11 -5
  7. package/src/index.tsx +22 -12
  8. package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
  9. package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
  10. package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
  11. package/src/lib/components/dropdown-actions/index.ts +1 -0
  12. package/src/lib/components/dropdown-actions/types.ts +16 -0
  13. package/src/lib/components/errors/generic/generic-error.tsx +11 -8
  14. package/src/lib/components/icons/document.svg +3 -0
  15. package/src/lib/components/icons/file.svg +3 -0
  16. package/src/lib/components/icons/icon-names.d.ts +8 -0
  17. package/src/lib/components/icons/image.svg +3 -0
  18. package/src/lib/components/icons/pdf.svg +3 -0
  19. package/src/lib/components/icons/plus.svg +3 -0
  20. package/src/lib/components/icons/retry.svg +3 -0
  21. package/src/lib/components/icons/spreadsheet.svg +3 -0
  22. package/src/lib/components/icons/tutor-logo.svg +9 -0
  23. package/src/lib/components/index.ts +1 -0
  24. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
  25. package/src/lib/hooks/index.ts +1 -0
  26. package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
  27. package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
  28. package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
  29. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
  30. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
  31. package/src/lib/hooks/use-click-outside/index.ts +1 -0
  32. package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
  33. package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
  34. package/src/lib/utils/index.ts +1 -0
  35. package/src/lib/utils/is-theme-dark.ts +21 -0
  36. package/src/main/hooks/use-initial-store/index.ts +1 -0
  37. package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
  38. package/src/main/hooks/use-initial-tab/index.ts +1 -0
  39. package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
  40. package/src/main/index.ts +1 -0
  41. package/src/main/main-content.tsx +14 -0
  42. package/src/main/main-wrapper.tsx +16 -0
  43. package/src/main/main.spec.tsx +5 -3
  44. package/src/main/main.tsx +7 -16
  45. package/src/main/types.ts +5 -0
  46. package/src/modules/global-providers/global-providers.tsx +1 -15
  47. package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
  48. package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
  49. package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
  50. package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
  51. package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
  52. package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
  53. package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
  54. package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
  55. package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
  56. package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
  57. package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
  58. package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
  59. package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
  60. package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
  61. package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
  62. package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
  63. package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
  64. package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
  65. package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
  66. package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
  67. package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
  68. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
  69. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
  70. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
  71. package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
  72. package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
  73. package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
  74. package/src/modules/messages/components/index.ts +1 -0
  75. package/src/modules/messages/components/message-item/message-item.tsx +1 -2
  76. package/src/modules/messages/constants.ts +2 -1
  77. package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
  78. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
  79. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
  80. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
  81. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
  82. package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
  83. package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
  84. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
  85. package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
  86. package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
  87. package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
  88. package/src/modules/messages/service.direct.ts +18 -0
  89. package/src/modules/messages/service.ts +1 -2
  90. package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
  91. package/src/modules/messages/types.ts +14 -0
  92. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
  93. package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
  94. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
  95. package/src/modules/sparkie/service.ts +2 -1
  96. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
  97. package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
  98. package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
  99. package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
  100. package/src/modules/widget/components/container/container.tsx +14 -0
  101. package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
  102. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  103. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
  104. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
  105. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
  106. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
  107. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
  108. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
  109. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  110. package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
  111. package/src/modules/widget/hooks/index.ts +0 -1
  112. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  113. package/src/modules/widget/store/create-store.ts +7 -0
  114. package/src/modules/widget/store/index.ts +1 -0
  115. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  116. package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
  117. package/src/types.ts +1 -0
  118. package/src/wrapper.tsx +39 -19
  119. package/src/lib/hooks/use-response-timeout/index.ts +0 -1
  120. package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
  121. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  122. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -0,0 +1,80 @@
1
+ import { type ChangeEvent, useRef } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { DropdownActions } from '@/src/lib/components'
5
+ import { useChatFileUpload } from '@/src/lib/hooks/use-chat-file-upload'
6
+ import { FILE_TYPES, FILE_TYPES_KEYS } from '@/src/lib/hooks/use-chat-file-upload/constants'
7
+ import type { FileType } from '@/src/lib/hooks/use-chat-file-upload/types'
8
+
9
+ import type { ChatFileUploaderProps } from './types'
10
+
11
+ function ChatFileUploader({ disabled = false, loading = false }: ChatFileUploaderProps) {
12
+ const { t } = useTranslation()
13
+ const fileInputRef = useRef<HTMLInputElement>(null)
14
+ const imageInputRef = useRef<HTMLInputElement>(null)
15
+
16
+ const acceptFileTypes = [FILE_TYPES.document, FILE_TYPES.spreadsheet, FILE_TYPES.pdf]
17
+ .flat()
18
+ .join()
19
+ const acceptImageTypes = FILE_TYPES.image.join()
20
+
21
+ const { handleSelectFile, selectedFile } = useChatFileUpload()
22
+
23
+ const triggerFileInput = (type: FileType) => {
24
+ const inputRef = type === FILE_TYPES_KEYS.DOCUMENT ? fileInputRef : imageInputRef
25
+ inputRef.current?.click()
26
+ }
27
+
28
+ const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>, type: FileType) => {
29
+ const file = e.target.files?.[0]
30
+ if (file) {
31
+ handleSelectFile(file, type)
32
+ }
33
+ e.target.value = ''
34
+ }
35
+
36
+ return (
37
+ <>
38
+ <input
39
+ type='file'
40
+ ref={fileInputRef}
41
+ onChange={(e) => handleFileInputChange(e, FILE_TYPES_KEYS.DOCUMENT)}
42
+ accept={acceptFileTypes}
43
+ className='hidden'
44
+ aria-hidden='true'
45
+ aria-label={t('general.buttons.file')}
46
+ />
47
+ <input
48
+ type='file'
49
+ ref={imageInputRef}
50
+ onChange={(e) => handleFileInputChange(e, FILE_TYPES_KEYS.IMAGE)}
51
+ accept={acceptImageTypes}
52
+ className='hidden'
53
+ aria-hidden='true'
54
+ aria-label={t('general.buttons.image')}
55
+ />
56
+ <DropdownActions
57
+ triggerIcon='plus'
58
+ disabled={disabled || loading || selectedFile !== null}
59
+ items={[
60
+ {
61
+ label: 'general.buttons.file',
62
+ icon: 'file',
63
+ visible: true,
64
+ callback: () => triggerFileInput(FILE_TYPES_KEYS.DOCUMENT)
65
+ },
66
+ {
67
+ label: 'general.buttons.image',
68
+ icon: 'image',
69
+ visible: true,
70
+ callback: () => triggerFileInput(FILE_TYPES_KEYS.IMAGE)
71
+ }
72
+ ]}
73
+ triggerClassName='rounded-full border border-neutral-300 h-10 w-10 flex items-center justify-center'
74
+ triggerIconClassName='w-3.5 h-3.5'
75
+ />
76
+ </>
77
+ )
78
+ }
79
+
80
+ export default ChatFileUploader
@@ -0,0 +1 @@
1
+ export { default as ChatFileUploader } from './chat-file-uploader'
@@ -0,0 +1,4 @@
1
+ export type ChatFileUploaderProps = {
2
+ disabled?: boolean
3
+ loading?: boolean
4
+ }
@@ -57,7 +57,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
57
57
  ),
58
58
  { 'cursor-not-allowed opacity-40': inputDisabled }
59
59
  )}
60
- placeholder={t('send_message.field.placeholder')}
60
+ placeholder={t('send_message.field.placeholder_v2')}
61
61
  value={value}
62
62
  onChange={handleChange}
63
63
  onKeyDown={handleKeyDown}
@@ -1,3 +1,4 @@
1
+ export * from './chat-file-uploader'
1
2
  export * from './chat-input'
2
3
  export * from './message-img'
3
4
  export * from './message-item'
@@ -22,8 +22,7 @@ function MessageItem({ message }: { message: ParsedMessage }) {
22
22
  <div
23
23
  data-test='messages-item'
24
24
  className={clsx('w-full overflow-x-hidden rounded-lg p-3', {
25
- 'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser,
26
- 'bg-neutral-200': messageFromAi
25
+ 'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser
27
26
  })}>
28
27
  <MessageContentTypeRenderer message={message} />
29
28
  </div>
@@ -5,5 +5,6 @@ export const MessagesEndpoints = {
5
5
  getAll: (conversationId: string) =>
6
6
  `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`,
7
7
  create: (conversationId: string) =>
8
- `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`
8
+ `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`,
9
+ getSignedUrls: () => `${process.env.API_HOTMART_TUTOR}/api/v1/files/signed-urls`
9
10
  }
@@ -0,0 +1 @@
1
+ export * from './use-get-signed-urls'
@@ -0,0 +1,27 @@
1
+ import { renderHook, waitFor } from '@/src/config/tests'
2
+
3
+ import type { UseGetSignedUrlsProps } from './use-get-signed-urls'
4
+ import { useGetSignedUrls } from './use-get-signed-urls'
5
+
6
+ describe('useGetSignedUrls', () => {
7
+ const props: UseGetSignedUrlsProps = {
8
+ chatId: 'chat-id',
9
+ fileExtension: 'pdf',
10
+ fileNameWithExtension: 'file.pdf',
11
+ productId: 123
12
+ }
13
+ const render = () => renderHook(() => useGetSignedUrls(props))
14
+
15
+ it('should get signed upload url', async () => {
16
+ const { result } = render()
17
+
18
+ await waitFor(() => expect(result.current.isSuccess).toBeTruthy())
19
+
20
+ expect(result.current.data).toMatchObject({
21
+ uploadUrl: expect.any(String) as string,
22
+ futureDownloadUrl: expect.any(String) as string,
23
+ imagePreviewUrl: expect.any(String) as string,
24
+ fileNameWithExtension: expect.any(String) as string
25
+ })
26
+ })
27
+ })
@@ -0,0 +1,38 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import { MessagesEndpoints } from '../../constants'
4
+ import DirectMessagesService from '../../service.direct'
5
+
6
+ export type UseGetSignedUrlsProps = {
7
+ chatId: string
8
+ fileExtension: string
9
+ fileNameWithExtension: string
10
+ productId: number
11
+ enabled?: boolean
12
+ }
13
+
14
+ export function useGetSignedUrls({
15
+ chatId,
16
+ fileExtension,
17
+ fileNameWithExtension,
18
+ productId,
19
+ enabled
20
+ }: UseGetSignedUrlsProps) {
21
+ return useQuery({
22
+ queryKey: [
23
+ MessagesEndpoints.getSignedUrls(),
24
+ chatId,
25
+ fileExtension,
26
+ fileNameWithExtension,
27
+ productId
28
+ ],
29
+ queryFn: () =>
30
+ DirectMessagesService.getSignedUrls({
31
+ chatId,
32
+ fileExtension,
33
+ fileNameWithExtension,
34
+ productId
35
+ }),
36
+ enabled
37
+ })
38
+ }
@@ -1,7 +1,6 @@
1
1
  import { act, MockRequest, renderHook, waitFor } from '@/src/config/tests'
2
2
  import IMessageWithSenderDataMock from '../../__tests__/imessage-with-sender-data.mock'
3
3
  import { MessagesEndpoints, MSG_MAX_COUNT } from '../../constants'
4
- import type { IMessageWithSenderData } from '../../types'
5
4
 
6
5
  import useInfiniteGetMessages from './use-infinite-get-messages'
7
6
 
@@ -34,7 +33,7 @@ describe('useInfiniteGetMessages', () => {
34
33
 
35
34
  MockRequest.mock(
36
35
  MessagesEndpoints.getAll(mockConversationId),
37
- new IMessageWithSenderDataMock().getMany(allMessages) as IMessageWithSenderData[]
36
+ new IMessageWithSenderDataMock().getMany(allMessages)
38
37
  )
39
38
 
40
39
  const { result } = createHook({
@@ -1,5 +1,4 @@
1
- import type { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
2
- import { useInfiniteQuery } from '@tanstack/react-query'
1
+ import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'
3
2
 
4
3
  import { formatTime } from '@/src/config/dayjs'
5
4
  import { api } from '@/src/config/request'
@@ -18,7 +17,7 @@ export const getAllMessagesQuery = ({
18
17
  profileId,
19
18
  limit = MSG_MAX_COUNT
20
19
  }: GetAllMessagesQueryProps) =>
21
- ({
20
+ infiniteQueryOptions({
22
21
  queryKey: ['getAllMessagesWithoutSparkie', limit, conversationId],
23
22
  queryFn: async ({ pageParam }: { pageParam?: number }) => {
24
23
  const { data: messages } = await api.get<IMessageWithSenderData[]>(
@@ -68,11 +67,7 @@ export const getAllMessagesQuery = ({
68
67
  new Map<string, ParsedMessage[]>()
69
68
  )
70
69
  }
71
- }) as UndefinedInitialDataInfiniteOptions<
72
- FetchMessagesResponse,
73
- Error,
74
- Map<string, ParsedMessage[]>
75
- >
70
+ })
76
71
 
77
72
  function useInfiniteGetMessages({ conversationId, profileId, limit }: GetAllMessagesQueryProps) {
78
73
  const query = getAllMessagesQuery({
@@ -0,0 +1 @@
1
+ export { default as usePrefetchMessages } from './use-prefetch-messages'
@@ -0,0 +1,28 @@
1
+ import { useCallback, useEffect } from 'react'
2
+ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
3
+
4
+ import { getProfileQuery } from '@/src/modules/profile'
5
+ import type { WidgetSettingProps } from '@/src/types'
6
+ import { MSG_MAX_COUNT } from '../../constants'
7
+ import { getAllMessagesQuery } from '../use-infinite-get-messages'
8
+
9
+ function usePrefetchMessages(settings: WidgetSettingProps) {
10
+ const queryClient = useQueryClient()
11
+ const profileQuery = useSuspenseQuery(getProfileQuery())
12
+
13
+ const prefetch = useCallback(async () => {
14
+ await queryClient.prefetchInfiniteQuery(
15
+ getAllMessagesQuery({
16
+ conversationId: settings.conversationId,
17
+ profileId: typeof profileQuery?.data?.id === 'string' ? profileQuery?.data?.id : undefined,
18
+ limit: MSG_MAX_COUNT
19
+ })
20
+ )
21
+ }, [profileQuery?.data?.id, queryClient, settings.conversationId])
22
+
23
+ useEffect(() => {
24
+ void prefetch()
25
+ }, [prefetch])
26
+ }
27
+
28
+ export default usePrefetchMessages
@@ -1,11 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo } from 'react'
2
- import type { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import type { InfiniteData } from '@tanstack/react-query'
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
4
  import { produce } from 'immer'
5
5
 
6
6
  import { ComponentSource, DataHubService } from '@/src/config/datahub'
7
7
  import { ViewTutorAnswerMessageSchema } from '@/src/config/datahub/schemas/tutor'
8
- import { useResponseTimeout } from '@/src/lib/hooks/use-response-timeout'
9
8
  import { useUpdateCursor } from '@/src/modules/cursor/hooks'
10
9
  import { useGetProfile } from '@/src/modules/profile'
11
10
  import { SparkieService } from '@/src/modules/sparkie'
@@ -18,59 +17,6 @@ import { useMessagesMaxCount, useUnreadMessagesSetAtom } from '../../store'
18
17
  import type { FetchMessagesResponse, IMessageWithSenderData } from '../../types'
19
18
  import { getAllMessagesQuery } from '../use-infinite-get-messages'
20
19
 
21
- export type MessageReceivedProps = {
22
- data: IMessageWithSenderData
23
- queryClient: QueryClient
24
- conversationId: string
25
- profileId: string
26
- limit: number
27
- messageIsMineCallback?: (data: IMessageWithSenderData) => void
28
- messageIsNotMineCallback?: (data: IMessageWithSenderData) => void
29
- errorCallback?: (error: unknown) => void
30
- }
31
-
32
- const messageReceivedUtil = ({
33
- queryClient,
34
- data,
35
- conversationId,
36
- profileId,
37
- limit,
38
- messageIsMineCallback,
39
- messageIsNotMineCallback,
40
- errorCallback
41
- }: MessageReceivedProps) => {
42
- try {
43
- const messagesQueryConfig = getAllMessagesQuery({ conversationId, profileId, limit })
44
- const queryData = queryClient.getQueryData<InfiniteData<FetchMessagesResponse>>(
45
- messagesQueryConfig.queryKey
46
- )
47
-
48
- const idsList = new Set(
49
- queryData?.pages.flatMap((items) => items.messages.map((msg) => msg.id))
50
- )
51
-
52
- if (idsList.has(data.id)) return
53
-
54
- queryClient.setQueryData<InfiniteData<FetchMessagesResponse>>(
55
- messagesQueryConfig.queryKey,
56
- (oldData) =>
57
- produce(oldData, (draft) => {
58
- draft?.pages.at(-1)?.messages?.push(data)
59
- return draft
60
- })
61
- )
62
-
63
- const isMine = data.contactId === profileId
64
-
65
- if (isMine) {
66
- return messageIsMineCallback?.(data)
67
- }
68
- messageIsNotMineCallback?.(data)
69
- } catch (error) {
70
- errorCallback?.(error)
71
- }
72
- }
73
-
74
20
  const useSubscribeMessageReceivedEvent = () => {
75
21
  const [settings] = useWidgetSettingsAtom()
76
22
  const [, setWidgetLoading] = useWidgetLoadingAtom()
@@ -80,78 +26,75 @@ const useSubscribeMessageReceivedEvent = () => {
80
26
  const useUpdateCursorMutation = useUpdateCursor()
81
27
  const limit = useMessagesMaxCount()
82
28
  const isAgentMode = useIsAgentParentAtomValue()
83
- const { reset: resetLoadingTimeout, startTimeout } = useResponseTimeout()
84
29
 
85
30
  const conversationId = useMemo(() => String(settings?.conversationId), [settings?.conversationId])
86
31
  const profileId = useMemo(() => String(profileQuery?.data?.id), [profileQuery?.data?.id])
87
-
88
- const messageIsMineCallback = useCallback(
89
- (data: IMessageWithSenderData) => {
90
- // The cursor should update only with my messages
91
- useUpdateCursorMutation.mutate(data.conversationId)
92
- },
93
- [useUpdateCursorMutation]
32
+ const messagesQueryConfig = useMemo(
33
+ () =>
34
+ getAllMessagesQuery({
35
+ conversationId,
36
+ profileId,
37
+ limit
38
+ }),
39
+ [conversationId, limit, profileId]
94
40
  )
95
41
 
96
- const messageIsNotMineCallback = useCallback(
42
+ const messageReceived = useCallback(
97
43
  (data: IMessageWithSenderData) => {
98
- setWidgetLoading(false)
99
-
100
- DataHubService.sendEvent({
101
- schema: new ViewTutorAnswerMessageSchema({
102
- correlationId: data.metadata.correlationId,
103
- messageId: data.id,
104
- componentSource: isAgentMode
105
- ? ComponentSource.PRODUCT_AGENT
106
- : ComponentSource.HOTMART_TUTOR
44
+ if (data.conversationId !== conversationId) return
45
+
46
+ const queryData = queryClient.getQueryData<InfiniteData<FetchMessagesResponse>>(
47
+ messagesQueryConfig.queryKey
48
+ )
49
+
50
+ const idsList = new Set(
51
+ queryData?.pages.flatMap((items) => items.messages.map((msg) => msg.id))
52
+ )
53
+
54
+ if (idsList.has(data.id)) return
55
+
56
+ queryClient.setQueryData<InfiniteData<FetchMessagesResponse>>(
57
+ messagesQueryConfig.queryKey,
58
+ (oldData) => {
59
+ return produce(oldData, (draft) => {
60
+ draft?.pages.at(-1)?.messages?.push(data)
61
+ return draft
62
+ })
63
+ }
64
+ )
65
+
66
+ const isMine = data.contactId === profileId
67
+
68
+ if (!isMine) {
69
+ DataHubService.sendEvent({
70
+ schema: new ViewTutorAnswerMessageSchema({
71
+ correlationId: data.metadata.correlationId,
72
+ messageId: data.id,
73
+ componentSource: isAgentMode
74
+ ? ComponentSource.PRODUCT_AGENT
75
+ : ComponentSource.HOTMART_TUTOR
76
+ })
107
77
  })
108
- })
109
78
 
110
- addUnreadMessagesToSet({ itemId: data.id })
111
- },
112
- [addUnreadMessagesToSet, isAgentMode, setWidgetLoading]
113
- )
114
-
115
- const errorCallback = useCallback(
116
- (error: unknown) => {
117
- if (error) {
118
- setWidgetLoading(false)
119
- console.error(error)
120
- throw new Error('Error getting realtime messages')
79
+ addUnreadMessagesToSet({ itemId: data.id })
80
+ setTimeout(() => setWidgetLoading(false), 100)
81
+ } else {
82
+ // The cursor should update only with my messages
83
+ useUpdateCursorMutation.mutate(data.conversationId)
121
84
  }
122
85
  },
123
- [setWidgetLoading]
124
- )
125
-
126
- const messageReceived = useCallback(
127
- (data: IMessageWithSenderData) => {
128
- if (data.conversationId === conversationId)
129
- messageReceivedUtil({
130
- queryClient,
131
- data,
132
- conversationId,
133
- profileId,
134
- limit,
135
- messageIsMineCallback,
136
- messageIsNotMineCallback,
137
- errorCallback
138
- })
139
- },
140
86
  [
87
+ addUnreadMessagesToSet,
141
88
  conversationId,
142
- errorCallback,
143
- limit,
144
- messageIsMineCallback,
145
- messageIsNotMineCallback,
89
+ isAgentMode,
90
+ messagesQueryConfig.queryKey,
146
91
  profileId,
147
- queryClient
92
+ queryClient,
93
+ setWidgetLoading,
94
+ useUpdateCursorMutation
148
95
  ]
149
96
  )
150
97
 
151
- const startLoadingTimeout = useCallback(() => {
152
- startTimeout(() => setWidgetLoading(false))
153
- }, [setWidgetLoading, startTimeout])
154
-
155
98
  useEffect(() => {
156
99
  SparkieService.subscribeEvents({ messageReceived })
157
100
 
@@ -159,14 +102,6 @@ const useSubscribeMessageReceivedEvent = () => {
159
102
  SparkieService.removeEventSubscription({ messageReceived })
160
103
  }
161
104
  }, [messageReceived])
162
-
163
- useEffect(() => {
164
- startLoadingTimeout()
165
-
166
- return () => {
167
- resetLoadingTimeout()
168
- }
169
- }, [resetLoadingTimeout, startLoadingTimeout])
170
105
  }
171
106
 
172
107
  export default useSubscribeMessageReceivedEvent
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export { default as useSuspenseMessages } from './use-suspense-messages'
@@ -0,0 +1,4 @@
1
+ export type UseSuspenseMessagesProps = {
2
+ conversationId?: string
3
+ limit?: number
4
+ }
@@ -0,0 +1,21 @@
1
+ import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'
2
+
3
+ import { getProfileQuery } from '@/src/modules/profile'
4
+ import { MSG_MAX_COUNT } from '../../constants'
5
+ import { getAllMessagesQuery } from '../use-infinite-get-messages'
6
+
7
+ import type { UseSuspenseMessagesProps } from './types'
8
+
9
+ function useSuspenseMessages({ conversationId, limit = MSG_MAX_COUNT }: UseSuspenseMessagesProps) {
10
+ const profileQuery = useSuspenseQuery(getProfileQuery())
11
+
12
+ return useSuspenseInfiniteQuery(
13
+ getAllMessagesQuery({
14
+ conversationId: conversationId,
15
+ profileId: typeof profileQuery?.data?.id === 'string' ? profileQuery?.data?.id : undefined,
16
+ limit
17
+ })
18
+ )
19
+ }
20
+
21
+ export default useSuspenseMessages
@@ -6,6 +6,8 @@ import { MessagesEndpoints } from './constants'
6
6
  import type {
7
7
  DirectMessagesServiceProps,
8
8
  FetchMessagesResponse,
9
+ IGetSignedUrlsPayload,
10
+ IGetSignedUrlsResponse,
9
11
  IMessageWithSenderData
10
12
  } from './types'
11
13
 
@@ -36,6 +38,22 @@ class DirectMessagesService {
36
38
 
37
39
  return data
38
40
  }
41
+
42
+ async getSignedUrls({
43
+ productId,
44
+ chatId,
45
+ fileExtension,
46
+ fileNameWithExtension
47
+ }: IGetSignedUrlsPayload): Promise<IGetSignedUrlsResponse> {
48
+ const { data } = await api.post<IGetSignedUrlsResponse>(MessagesEndpoints.getSignedUrls(), {
49
+ productId,
50
+ chatId,
51
+ fileExtension,
52
+ fileNameWithExtension
53
+ })
54
+
55
+ return data
56
+ }
39
57
  }
40
58
 
41
59
  export default new DirectMessagesService()
@@ -1,5 +1,4 @@
1
1
  import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
2
- import type MessageService from '@hotmart-org-ca/sparkie/dist/MessageService'
3
2
 
4
3
  import { ApiError } from '@/src/config/request'
5
4
  import { HttpCodes } from '@/src/lib/utils'
@@ -16,7 +15,7 @@ import type {
16
15
  } from './types'
17
16
 
18
17
  class MessagesService {
19
- async getSparkieMessageService(): Promise<MessageService> {
18
+ async getSparkieMessageService() {
20
19
  try {
21
20
  const messageService = await SparkieService.getMessageService()
22
21
 
@@ -2,7 +2,7 @@ import { atom, useAtom, useAtomValue } from 'jotai'
2
2
 
3
3
  const INITIAL_MAX_COUNT = 2
4
4
 
5
- const messagesMaxCountAtom = atom(INITIAL_MAX_COUNT)
5
+ export const messagesMaxCountAtom = atom(INITIAL_MAX_COUNT)
6
6
 
7
7
  const setMessagesMaxCountAtom = atom(
8
8
  (get) => get(messagesMaxCountAtom),
@@ -10,4 +10,4 @@ const setMessagesMaxCountAtom = atom(
10
10
  )
11
11
 
12
12
  export const useSetMessagesMaxCountAtom = () => useAtom(setMessagesMaxCountAtom)
13
- export const useMessagesMaxCount = () => useAtomValue(setMessagesMaxCountAtom)
13
+ export const useMessagesMaxCount = () => useAtomValue(messagesMaxCountAtom)
@@ -19,6 +19,13 @@ export type IGetMessagesPayload = {
19
19
  before?: number
20
20
  }
21
21
 
22
+ export type IGetSignedUrlsPayload = {
23
+ chatId: string
24
+ fileExtension: string
25
+ fileNameWithExtension: string
26
+ productId: number
27
+ }
28
+
22
29
  export type ISendTextMessagePayload = {
23
30
  conversationId: string
24
31
  content: {
@@ -63,6 +70,13 @@ export type MessageParserArgs = {
63
70
  profileId: string
64
71
  }
65
72
 
73
+ export type IGetSignedUrlsResponse = {
74
+ uploadUrl: string
75
+ futureDownloadUrl: string
76
+ imagePreviewUrl: string
77
+ fileNameWithExtension: string
78
+ }
79
+
66
80
  export type ParsedMessage = {
67
81
  from: string
68
82
  id: string
@@ -1,13 +1,14 @@
1
- import { useQuery } from '@tanstack/react-query'
1
+ import { queryOptions, useQuery } from '@tanstack/react-query'
2
2
 
3
3
  import { ProfileEndpoints } from '../../constants'
4
4
  import ProfileService from '../../service'
5
5
 
6
- export const getProfileQuery = (enabled: boolean) => ({
7
- queryKey: [ProfileEndpoints.getProfile()],
8
- queryFn: () => ProfileService.getProfile(),
9
- enabled
10
- })
6
+ export const getProfileQuery = (enabled: boolean = true) =>
7
+ queryOptions({
8
+ queryKey: [ProfileEndpoints.getProfile()],
9
+ queryFn: () => ProfileService.getProfile(),
10
+ enabled
11
+ })
11
12
 
12
13
  export function useGetProfile(enabled: boolean = true) {
13
14
  return useQuery(getProfileQuery(enabled))
@@ -1,11 +1,8 @@
1
1
  import ICursorUpdateBuilder from '../../cursor/__tests__/icursor-update.builder'
2
- import type { IMessageWithSenderData } from '../../messages'
3
2
  import IMessageWithSenderDataMock from '../../messages/__tests__/imessage-with-sender-data.mock'
4
3
 
5
4
  export const SparkieMessageServiceMock = {
6
- getAll: vi
7
- .fn()
8
- .mockReturnValue(new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]),
5
+ getAll: vi.fn().mockReturnValue(new IMessageWithSenderDataMock().getMany(10)),
9
6
  post: vi.fn(),
10
7
  postDirect: vi.fn(),
11
8
  remove: vi.fn()