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.
- package/CHANGELOG.md +37 -0
- package/config/vitest/__mocks__/sparkie.tsx +1 -1
- package/package.json +1 -1
- package/src/bootstrap.ts +40 -0
- package/src/config/tests/handlers.ts +5 -4
- package/src/config/theme/init-theme.ts +11 -5
- package/src/index.tsx +22 -12
- package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
- package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
- package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
- package/src/lib/components/dropdown-actions/index.ts +1 -0
- package/src/lib/components/dropdown-actions/types.ts +16 -0
- package/src/lib/components/errors/generic/generic-error.tsx +11 -8
- package/src/lib/components/icons/document.svg +3 -0
- package/src/lib/components/icons/file.svg +3 -0
- package/src/lib/components/icons/icon-names.d.ts +8 -0
- package/src/lib/components/icons/image.svg +3 -0
- package/src/lib/components/icons/pdf.svg +3 -0
- package/src/lib/components/icons/plus.svg +3 -0
- package/src/lib/components/icons/retry.svg +3 -0
- package/src/lib/components/icons/spreadsheet.svg +3 -0
- package/src/lib/components/icons/tutor-logo.svg +9 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
- package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
- package/src/lib/hooks/use-click-outside/index.ts +1 -0
- package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
- package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/lib/utils/is-theme-dark.ts +21 -0
- package/src/main/hooks/use-initial-store/index.ts +1 -0
- package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
- package/src/main/hooks/use-initial-tab/index.ts +1 -0
- package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
- package/src/main/index.ts +1 -0
- package/src/main/main-content.tsx +14 -0
- package/src/main/main-wrapper.tsx +16 -0
- package/src/main/main.spec.tsx +5 -3
- package/src/main/main.tsx +7 -16
- package/src/main/types.ts +5 -0
- package/src/modules/global-providers/global-providers.tsx +1 -15
- package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
- package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
- package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
- package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
- package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
- package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
- package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
- package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
- package/src/modules/messages/components/index.ts +1 -0
- package/src/modules/messages/components/message-item/message-item.tsx +1 -2
- package/src/modules/messages/constants.ts +2 -1
- package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
- package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
- package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
- package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
- package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
- package/src/modules/messages/service.direct.ts +18 -0
- package/src/modules/messages/service.ts +1 -2
- package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
- package/src/modules/messages/types.ts +14 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
- package/src/modules/sparkie/service.ts +2 -1
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
- package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
- package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
- package/src/modules/widget/components/container/container.tsx +14 -0
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
- package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
- package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
- package/src/modules/widget/hooks/index.ts +0 -1
- package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
- package/src/modules/widget/store/create-store.ts +7 -0
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
- package/src/types.ts +1 -0
- package/src/wrapper.tsx +39 -19
- package/src/lib/hooks/use-response-timeout/index.ts +0 -1
- package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
- package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
- 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'
|
|
@@ -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.
|
|
60
|
+
placeholder={t('send_message.field.placeholder_v2')}
|
|
61
61
|
value={value}
|
|
62
62
|
onChange={handleChange}
|
|
63
63
|
onKeyDown={handleKeyDown}
|
|
@@ -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
|
+
}
|
package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx
CHANGED
|
@@ -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)
|
|
36
|
+
new IMessageWithSenderDataMock().getMany(allMessages)
|
|
38
37
|
)
|
|
39
38
|
|
|
40
39
|
const { result } = createHook({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
32
|
+
const messagesQueryConfig = useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
getAllMessagesQuery({
|
|
35
|
+
conversationId,
|
|
36
|
+
profileId,
|
|
37
|
+
limit
|
|
38
|
+
}),
|
|
39
|
+
[conversationId, limit, profileId]
|
|
94
40
|
)
|
|
95
41
|
|
|
96
|
-
const
|
|
42
|
+
const messageReceived = useCallback(
|
|
97
43
|
(data: IMessageWithSenderData) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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,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()
|
|
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(
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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()
|