@planningcenter/chat-react-native 4.2.1-qa-staging.5 → 4.2.1-rc.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 (185) hide show
  1. package/build/components/conversation/message_form.d.ts +1 -2
  2. package/build/components/conversation/message_form.d.ts.map +1 -1
  3. package/build/components/conversation/message_form.js +51 -54
  4. package/build/components/conversation/message_form.js.map +1 -1
  5. package/build/components/conversation/message_list.d.ts +2 -3
  6. package/build/components/conversation/message_list.d.ts.map +1 -1
  7. package/build/components/conversation/message_list.js +66 -152
  8. package/build/components/conversation/message_list.js.map +1 -1
  9. package/build/contexts/api_provider.d.ts.map +1 -1
  10. package/build/contexts/api_provider.js +1 -2
  11. package/build/contexts/api_provider.js.map +1 -1
  12. package/build/contexts/conversation_context.d.ts +1 -3
  13. package/build/contexts/conversation_context.d.ts.map +1 -1
  14. package/build/contexts/conversation_context.js +18 -23
  15. package/build/contexts/conversation_context.js.map +1 -1
  16. package/build/hooks/index.d.ts +0 -1
  17. package/build/hooks/index.d.ts.map +1 -1
  18. package/build/hooks/index.js +0 -1
  19. package/build/hooks/index.js.map +1 -1
  20. package/build/hooks/paginator_meta.d.ts +1 -3
  21. package/build/hooks/paginator_meta.d.ts.map +1 -1
  22. package/build/hooks/paginator_meta.js +8 -25
  23. package/build/hooks/paginator_meta.js.map +1 -1
  24. package/build/hooks/use_api.d.ts.map +1 -1
  25. package/build/hooks/use_api.js +8 -5
  26. package/build/hooks/use_api.js.map +1 -1
  27. package/build/hooks/use_conversation_messages.d.ts +2 -9
  28. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  29. package/build/hooks/use_conversation_messages.js +45 -84
  30. package/build/hooks/use_conversation_messages.js.map +1 -1
  31. package/build/hooks/use_conversation_messages_jolt_events.d.ts +1 -17
  32. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  33. package/build/hooks/use_conversation_messages_jolt_events.js +80 -89
  34. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  35. package/build/hooks/use_mark_latest_message_read.d.ts +1 -2
  36. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  37. package/build/hooks/use_mark_latest_message_read.js +3 -4
  38. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  39. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  40. package/build/hooks/use_message_create_or_update.js +80 -91
  41. package/build/hooks/use_message_create_or_update.js.map +1 -1
  42. package/build/hooks/use_message_reaction_toggle.d.ts.map +1 -1
  43. package/build/hooks/use_message_reaction_toggle.js +40 -45
  44. package/build/hooks/use_message_reaction_toggle.js.map +1 -1
  45. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  46. package/build/hooks/use_suspense_api.js +8 -5
  47. package/build/hooks/use_suspense_api.js.map +1 -1
  48. package/build/screens/conversation_new/components/services_form.js +8 -9
  49. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  50. package/build/screens/conversation_screen.d.ts +0 -1
  51. package/build/screens/conversation_screen.d.ts.map +1 -1
  52. package/build/screens/conversation_screen.js +254 -481
  53. package/build/screens/conversation_screen.js.map +1 -1
  54. package/build/screens/message_actions_screen.d.ts.map +1 -1
  55. package/build/screens/message_actions_screen.js +26 -21
  56. package/build/screens/message_actions_screen.js.map +1 -1
  57. package/build/screens/message_report_screen.d.ts.map +1 -1
  58. package/build/screens/message_report_screen.js +44 -17
  59. package/build/screens/message_report_screen.js.map +1 -1
  60. package/build/screens/reactions_screen.d.ts.map +1 -1
  61. package/build/screens/reactions_screen.js +88 -70
  62. package/build/screens/reactions_screen.js.map +1 -1
  63. package/build/types/api_primitives.d.ts +0 -2
  64. package/build/types/api_primitives.d.ts.map +1 -1
  65. package/build/types/api_primitives.js.map +1 -1
  66. package/build/types/resources/member.d.ts +0 -1
  67. package/build/types/resources/member.d.ts.map +1 -1
  68. package/build/types/resources/member.js.map +1 -1
  69. package/build/utils/cache/messages_cache.d.ts +0 -1
  70. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  71. package/build/utils/cache/messages_cache.js +0 -3
  72. package/build/utils/cache/messages_cache.js.map +1 -1
  73. package/build/utils/cache/optimistically_update_message.d.ts +1 -2
  74. package/build/utils/cache/optimistically_update_message.d.ts.map +1 -1
  75. package/build/utils/cache/optimistically_update_message.js +4 -4
  76. package/build/utils/cache/optimistically_update_message.js.map +1 -1
  77. package/build/utils/client/types.d.ts +0 -1
  78. package/build/utils/client/types.d.ts.map +1 -1
  79. package/build/utils/client/types.js.map +1 -1
  80. package/build/utils/generate_placeholder_ulid.d.ts +6 -0
  81. package/build/utils/generate_placeholder_ulid.d.ts.map +1 -1
  82. package/build/utils/generate_placeholder_ulid.js +2 -2
  83. package/build/utils/generate_placeholder_ulid.js.map +1 -1
  84. package/build/utils/request/get_messages.d.ts +6 -18
  85. package/build/utils/request/get_messages.d.ts.map +1 -1
  86. package/build/utils/request/get_messages.js +12 -10
  87. package/build/utils/request/get_messages.js.map +1 -1
  88. package/package.json +3 -3
  89. package/src/__tests__/hooks/paginator_meta.test.ts +2 -73
  90. package/src/components/conversation/__tests__/message_list.test.tsx +0 -42
  91. package/src/components/conversation/message_form.tsx +0 -3
  92. package/src/components/conversation/message_list.tsx +4 -58
  93. package/src/contexts/api_provider.tsx +1 -2
  94. package/src/contexts/conversation_context.tsx +1 -6
  95. package/src/hooks/index.ts +0 -1
  96. package/src/hooks/paginator_meta.ts +5 -29
  97. package/src/hooks/use_api.ts +5 -4
  98. package/src/hooks/use_conversation_messages.ts +11 -52
  99. package/src/hooks/use_conversation_messages_jolt_events.ts +43 -62
  100. package/src/hooks/use_mark_latest_message_read.ts +3 -4
  101. package/src/hooks/use_message_create_or_update.ts +8 -8
  102. package/src/hooks/use_message_reaction_toggle.ts +1 -3
  103. package/src/hooks/use_suspense_api.ts +5 -4
  104. package/src/screens/conversation_new/components/services_form.tsx +5 -5
  105. package/src/screens/conversation_screen.tsx +14 -123
  106. package/src/screens/message_actions_screen.tsx +5 -3
  107. package/src/screens/message_report_screen.tsx +3 -2
  108. package/src/screens/reactions_screen.tsx +4 -2
  109. package/src/types/api_primitives.ts +0 -2
  110. package/src/types/resources/member.ts +0 -1
  111. package/src/utils/cache/__tests__/messages_cache.test.ts +1 -14
  112. package/src/utils/cache/messages_cache.ts +0 -3
  113. package/src/utils/cache/optimistically_update_message.ts +5 -2
  114. package/src/utils/client/types.ts +0 -1
  115. package/src/utils/generate_placeholder_ulid.ts +2 -2
  116. package/src/utils/request/get_messages.ts +14 -12
  117. package/build/components/conversation/anchor_not_found_boundary.d.ts +0 -18
  118. package/build/components/conversation/anchor_not_found_boundary.d.ts.map +0 -1
  119. package/build/components/conversation/anchor_not_found_boundary.js +0 -20
  120. package/build/components/conversation/anchor_not_found_boundary.js.map +0 -1
  121. package/build/hooks/use_conversation_message_lookup.d.ts +0 -7
  122. package/build/hooks/use_conversation_message_lookup.d.ts.map +0 -1
  123. package/build/hooks/use_conversation_message_lookup.js +0 -52
  124. package/build/hooks/use_conversation_message_lookup.js.map +0 -1
  125. package/build/hooks/use_direct_message_eligibility.d.ts +0 -14
  126. package/build/hooks/use_direct_message_eligibility.d.ts.map +0 -1
  127. package/build/hooks/use_direct_message_eligibility.js +0 -87
  128. package/build/hooks/use_direct_message_eligibility.js.map +0 -1
  129. package/build/hooks/use_fetch_newer_messages.d.ts +0 -9
  130. package/build/hooks/use_fetch_newer_messages.d.ts.map +0 -1
  131. package/build/hooks/use_fetch_newer_messages.js +0 -69
  132. package/build/hooks/use_fetch_newer_messages.js.map +0 -1
  133. package/build/hooks/use_fetch_newer_on_foreground.d.ts +0 -8
  134. package/build/hooks/use_fetch_newer_on_foreground.d.ts.map +0 -1
  135. package/build/hooks/use_fetch_newer_on_foreground.js +0 -42
  136. package/build/hooks/use_fetch_newer_on_foreground.js.map +0 -1
  137. package/build/hooks/use_held_back_messages.d.ts +0 -11
  138. package/build/hooks/use_held_back_messages.d.ts.map +0 -1
  139. package/build/hooks/use_held_back_messages.js +0 -24
  140. package/build/hooks/use_held_back_messages.js.map +0 -1
  141. package/build/utils/cache/find_message_in_caches.d.ts +0 -4
  142. package/build/utils/cache/find_message_in_caches.d.ts.map +0 -1
  143. package/build/utils/cache/find_message_in_caches.js +0 -16
  144. package/build/utils/cache/find_message_in_caches.js.map +0 -1
  145. package/build/utils/cache/invalidate_refetchable_queries.d.ts +0 -3
  146. package/build/utils/cache/invalidate_refetchable_queries.d.ts.map +0 -1
  147. package/build/utils/cache/invalidate_refetchable_queries.js +0 -8
  148. package/build/utils/cache/invalidate_refetchable_queries.js.map +0 -1
  149. package/build/utils/cache/prepend_newer_page.d.ts +0 -7
  150. package/build/utils/cache/prepend_newer_page.d.ts.map +0 -1
  151. package/build/utils/cache/prepend_newer_page.js +0 -15
  152. package/build/utils/cache/prepend_newer_page.js.map +0 -1
  153. package/build/utils/gated_message_cache.d.ts +0 -7
  154. package/build/utils/gated_message_cache.d.ts.map +0 -1
  155. package/build/utils/gated_message_cache.js +0 -13
  156. package/build/utils/gated_message_cache.js.map +0 -1
  157. package/build/utils/ulid.d.ts +0 -3
  158. package/build/utils/ulid.d.ts.map +0 -1
  159. package/build/utils/ulid.js +0 -7
  160. package/build/utils/ulid.js.map +0 -1
  161. package/src/__tests__/hooks/use_conversation_messages.test.ts +0 -22
  162. package/src/__tests__/hooks/use_conversation_messages_jolt_events.test.ts +0 -33
  163. package/src/__tests__/hooks/use_direct_message_eligibility.test.tsx +0 -83
  164. package/src/__tests__/screens/conversation_new/components/services_form.test.tsx +0 -91
  165. package/src/__tests__/utils/gated_message_cache.test.ts +0 -26
  166. package/src/__tests__/utils/request/get_messages.test.ts +0 -24
  167. package/src/__tests__/utils/ulid.test.ts +0 -15
  168. package/src/components/conversation/__tests__/anchor_not_found_boundary.test.tsx +0 -51
  169. package/src/components/conversation/anchor_not_found_boundary.tsx +0 -21
  170. package/src/hooks/__tests__/use_fetch_newer_messages.test.tsx +0 -95
  171. package/src/hooks/__tests__/use_held_back_messages.test.tsx +0 -60
  172. package/src/hooks/__tests__/use_message_reaction_toggle.test.tsx +0 -73
  173. package/src/hooks/use_conversation_message_lookup.ts +0 -29
  174. package/src/hooks/use_direct_message_eligibility.ts +0 -40
  175. package/src/hooks/use_fetch_newer_messages.ts +0 -52
  176. package/src/hooks/use_fetch_newer_on_foreground.ts +0 -25
  177. package/src/hooks/use_held_back_messages.ts +0 -33
  178. package/src/utils/cache/__tests__/find_message_in_caches.test.ts +0 -37
  179. package/src/utils/cache/__tests__/invalidate_refetchable_queries.test.ts +0 -23
  180. package/src/utils/cache/__tests__/prepend_newer_page.test.ts +0 -34
  181. package/src/utils/cache/find_message_in_caches.ts +0 -20
  182. package/src/utils/cache/invalidate_refetchable_queries.ts +0 -9
  183. package/src/utils/cache/prepend_newer_page.ts +0 -19
  184. package/src/utils/gated_message_cache.ts +0 -19
  185. package/src/utils/ulid.ts +0 -11
@@ -1,95 +0,0 @@
1
- import { InfiniteData, QueryClientProvider } from '@tanstack/react-query'
2
- import { act, renderHook } from '@testing-library/react-native'
3
- import React from 'react'
4
- import { buildTestQueryClient } from '../../__utils__/query_client'
5
- import type { ApiCollection, MessageResource } from '../../types'
6
- import { getMessagesQueryKey } from '../../utils/request/get_messages'
7
- import { ApiClient, useApiClient } from '../use_api_client'
8
- import { useFetchNewerMessages } from '../use_fetch_newer_messages'
9
-
10
- jest.mock('../use_api_client')
11
-
12
- type QueryData = InfiniteData<ApiCollection<MessageResource>>
13
-
14
- const page = (ids: string[], next?: { idGt: string }) => ({
15
- data: ids.map(id => ({ id, type: 'Message' }) as unknown as MessageResource),
16
- links: {},
17
- meta: { count: ids.length, totalCount: ids.length, next },
18
- })
19
-
20
- const queryKey = getMessagesQueryKey({ conversation_id: 1 })
21
- let queryClient = buildTestQueryClient()
22
- const get = jest.fn()
23
-
24
- const wrapper = ({ children }: { children: React.ReactNode }) => (
25
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
26
- )
27
-
28
- const cachedIds = () =>
29
- queryClient.getQueryData<QueryData>(queryKey)?.pages.flatMap(p => p.data.map(m => m.id))
30
-
31
- beforeEach(() => {
32
- queryClient = buildTestQueryClient()
33
- queryClient.setQueryData<QueryData>(queryKey, {
34
- pages: [page(['01C', '01B']), page(['01A'])],
35
- pageParams: [{}, { where: { id_lt: '01B' } }],
36
- })
37
- get.mockReset()
38
- ;(useApiClient as jest.MockedFunction<typeof useApiClient>).mockReturnValue({
39
- chat: { get },
40
- } as unknown as ApiClient)
41
- })
42
-
43
- describe('useFetchNewerMessages', () => {
44
- it('follows meta.next until everything newer is in the cache', async () => {
45
- get
46
- .mockResolvedValueOnce(page(['01D', '01E'], { idGt: '01E' }))
47
- .mockResolvedValueOnce(page(['01F']))
48
- const { result } = renderHook(() => useFetchNewerMessages({ conversation_id: 1, queryKey }), {
49
- wrapper,
50
- })
51
-
52
- await act(() => result.current('01C'))
53
-
54
- expect(get).toHaveBeenCalledTimes(2)
55
- expect(get.mock.calls[1][0].data.where).toEqual({ id_gt: '01E' })
56
- expect(cachedIds()).toEqual(['01D', '01E', '01F', '01C', '01B', '01A'])
57
- })
58
-
59
- it('rejects when the request fails so the caller can try again later', async () => {
60
- get.mockRejectedValueOnce(new Error('offline'))
61
- const { result } = renderHook(() => useFetchNewerMessages({ conversation_id: 1, queryKey }), {
62
- wrapper,
63
- })
64
-
65
- await expect(act(() => result.current('01C'))).rejects.toThrow('offline')
66
- expect(cachedIds()).toEqual(['01C', '01B', '01A'])
67
- })
68
-
69
- it('hands off instead of refetching when more is left than it will walk', async () => {
70
- for (let i = 0; i < 5; i++) get.mockResolvedValueOnce(page([`01D${i}`], { idGt: `01D${i}` }))
71
- const onTooFarBehind = jest.fn()
72
- const invalidate = jest.spyOn(queryClient, 'invalidateQueries')
73
- const { result } = renderHook(
74
- () => useFetchNewerMessages({ conversation_id: 1, queryKey, onTooFarBehind }),
75
- { wrapper }
76
- )
77
-
78
- await act(() => result.current('01C'))
79
-
80
- expect(onTooFarBehind).toHaveBeenCalledTimes(1)
81
- expect(invalidate).not.toHaveBeenCalled()
82
- })
83
-
84
- it('refetches the live cache when more is left and nobody takes over', async () => {
85
- for (let i = 0; i < 5; i++) get.mockResolvedValueOnce(page([`01D${i}`], { idGt: `01D${i}` }))
86
- const invalidate = jest.spyOn(queryClient, 'invalidateQueries')
87
- const { result } = renderHook(() => useFetchNewerMessages({ conversation_id: 1, queryKey }), {
88
- wrapper,
89
- })
90
-
91
- await act(() => result.current('01C'))
92
-
93
- expect(invalidate).toHaveBeenCalledWith({ queryKey })
94
- })
95
- })
@@ -1,60 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react-native'
2
- import { useHeldBackMessages } from '../use_held_back_messages'
3
-
4
- type Props = { newestMessageId?: string; readingHistory: boolean; caughtUp: boolean }
5
-
6
- const setup = (fetchNewer: jest.Mock, initial: Props) =>
7
- renderHook((props: Props) => useHeldBackMessages({ fetchNewer, ...props }), {
8
- initialProps: initial,
9
- })
10
-
11
- describe('useHeldBackMessages', () => {
12
- it('catches up from where the first skip happened, not from the newest message', async () => {
13
- const fetchNewer = jest.fn().mockResolvedValue(undefined)
14
- const { result, rerender } = setup(fetchNewer, {
15
- newestMessageId: 'A',
16
- readingHistory: true,
17
- caughtUp: true,
18
- })
19
-
20
- act(() => result.current.onSkippedNewMessage())
21
- rerender({ newestMessageId: 'B', readingHistory: true, caughtUp: true })
22
- await act(async () => rerender({ newestMessageId: 'B', readingHistory: false, caughtUp: true }))
23
-
24
- expect(fetchNewer).toHaveBeenCalledTimes(1)
25
- expect(fetchNewer).toHaveBeenCalledWith('A')
26
- })
27
-
28
- it('keeps the cursor when the catch-up fails and tries again next time', async () => {
29
- const fetchNewer = jest
30
- .fn()
31
- .mockRejectedValueOnce(new Error('offline'))
32
- .mockResolvedValue(undefined)
33
- const { result, rerender } = setup(fetchNewer, {
34
- newestMessageId: 'A',
35
- readingHistory: true,
36
- caughtUp: true,
37
- })
38
-
39
- act(() => result.current.onSkippedNewMessage())
40
- await act(async () => rerender({ newestMessageId: 'A', readingHistory: false, caughtUp: true }))
41
- rerender({ newestMessageId: 'C', readingHistory: true, caughtUp: true })
42
- await act(async () => rerender({ newestMessageId: 'C', readingHistory: false, caughtUp: true }))
43
-
44
- expect(fetchNewer).toHaveBeenCalledTimes(2)
45
- expect(fetchNewer).toHaveBeenLastCalledWith('A')
46
- })
47
-
48
- it('does nothing when nothing was held back', async () => {
49
- const fetchNewer = jest.fn()
50
- const { rerender } = setup(fetchNewer, {
51
- newestMessageId: 'A',
52
- readingHistory: true,
53
- caughtUp: true,
54
- })
55
-
56
- await act(async () => rerender({ newestMessageId: 'A', readingHistory: false, caughtUp: true }))
57
-
58
- expect(fetchNewer).not.toHaveBeenCalled()
59
- })
60
- })
@@ -1,73 +0,0 @@
1
- import { InfiniteData, QueryClientProvider } from '@tanstack/react-query'
2
- import { act, renderHook } from '@testing-library/react-native'
3
- import React from 'react'
4
- import { buildTestQueryClient } from '../../__utils__/query_client'
5
- import { ConversationContextProvider } from '../../contexts/conversation_context'
6
- import type { ApiCollection, MessageResource } from '../../types'
7
- import { getMessagesQueryKey } from '../../utils/request/get_messages'
8
- import { ApiClient, useApiClient } from '../use_api_client'
9
- import { useMessageReactionToggle } from '../use_message_reaction_toggle'
10
-
11
- jest.mock('../use_api_client')
12
-
13
- type QueryData = InfiniteData<ApiCollection<MessageResource>>
14
-
15
- const message = { id: '01M', type: 'Message', reactionCounts: [] } as unknown as MessageResource
16
- const liveKey = getMessagesQueryKey({ conversation_id: 1 })
17
- const anchoredKey = getMessagesQueryKey({ conversation_id: 1, anchor: '01A' })
18
- const seed = (): QueryData => ({
19
- pages: [{ data: [message], links: {}, meta: { count: 1, totalCount: 1 } }],
20
- pageParams: [{}],
21
- })
22
-
23
- const heartsIn = (queryClient: ReturnType<typeof buildTestQueryClient>, key: unknown[]) =>
24
- queryClient
25
- .getQueryData<QueryData>(key)
26
- ?.pages[0].data[0].reactionCounts.map(r => `${r.value}:${r.count}`)
27
-
28
- describe('useMessageReactionToggle', () => {
29
- beforeEach(() => {
30
- ;(useApiClient as jest.MockedFunction<typeof useApiClient>).mockReturnValue({
31
- chat: { post: jest.fn(() => new Promise(() => {})) },
32
- } as unknown as ApiClient)
33
- })
34
-
35
- it('applies the optimistic reaction to the cache the screen is showing', async () => {
36
- const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
37
- queryClient.setQueryData<QueryData>(anchoredKey, seed())
38
- const wrapper = ({ children }: { children: React.ReactNode }) => (
39
- <QueryClientProvider client={queryClient}>
40
- <ConversationContextProvider
41
- conversationId={1}
42
- currentPageReplyRootId={null}
43
- messagesQueryKey={anchoredKey}
44
- >
45
- {children}
46
- </ConversationContextProvider>
47
- </QueryClientProvider>
48
- )
49
- const { result } = renderHook(() => useMessageReactionToggle({ conversation_id: 1, message }), {
50
- wrapper,
51
- })
52
-
53
- await act(async () => result.current.handleReactionToggle({ value: 'heart', mine: false }))
54
-
55
- expect(heartsIn(queryClient, anchoredKey)).toEqual(['heart:1'])
56
- expect(queryClient.getQueryData(liveKey)).toBeUndefined()
57
- })
58
-
59
- it('falls back to the live cache when no screen key is provided', async () => {
60
- const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } })
61
- queryClient.setQueryData<QueryData>(liveKey, seed())
62
- const wrapper = ({ children }: { children: React.ReactNode }) => (
63
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
64
- )
65
- const { result } = renderHook(() => useMessageReactionToggle({ conversation_id: 1, message }), {
66
- wrapper,
67
- })
68
-
69
- await act(async () => result.current.handleReactionToggle({ value: 'heart', mine: false }))
70
-
71
- expect(heartsIn(queryClient, liveKey)).toEqual(['heart:1'])
72
- })
73
- })
@@ -1,29 +0,0 @@
1
- import { useQueryClient } from '@tanstack/react-query'
2
- import { useCallback, useSyncExternalStore } from 'react'
3
- import { findMessageInCaches } from '../utils/cache/find_message_in_caches'
4
- import { useConversationMessage } from './use_conversation_message'
5
-
6
- type Options = {
7
- conversation_id: number
8
- message_id: string
9
- }
10
-
11
- export const useConversationMessageLookup = ({ conversation_id, message_id }: Options) => {
12
- const queryClient = useQueryClient()
13
- const subscribe = useCallback(
14
- (onChange: () => void) => queryClient.getQueryCache().subscribe(onChange),
15
- [queryClient]
16
- )
17
- const getSnapshot = useCallback(
18
- () => findMessageInCaches(queryClient, conversation_id, message_id),
19
- [conversation_id, message_id, queryClient]
20
- )
21
- const cached = useSyncExternalStore(subscribe, getSnapshot)
22
- const fetched = useConversationMessage({
23
- conversation_id,
24
- messageId: message_id,
25
- enabled: !cached,
26
- })
27
-
28
- return cached ?? fetched.message
29
- }
@@ -1,40 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { ApiResource, MemberResource } from '../types'
3
- import { useApiClient } from './use_api_client'
4
-
5
- export interface DirectMessageEligibility {
6
- canDirectMessage: boolean
7
- directMessagesDisabled: boolean
8
- }
9
-
10
- export function useDirectMessageEligibility({
11
- groupId,
12
- personId,
13
- enabled = true,
14
- }: {
15
- groupId: string
16
- personId: number
17
- enabled?: boolean
18
- }) {
19
- const apiClient = useApiClient()
20
- const url = `/me/groups/${groupId}/members/${personId}`
21
-
22
- const { data, isLoading, isError } = useQuery({
23
- queryKey: ['chat', url, 'direct_message_eligibility'],
24
- queryFn: () =>
25
- apiClient.chat.get({
26
- url,
27
- data: { fields: { Member: ['can_direct_message', 'direct_messages_disabled'] } },
28
- }) as Promise<ApiResource<MemberResource>>,
29
- enabled,
30
- retry: false,
31
- })
32
-
33
- const member = data?.data
34
- const eligibility: DirectMessageEligibility | undefined = member && {
35
- canDirectMessage: member.canDirectMessage ?? false,
36
- directMessagesDisabled: member.directMessagesDisabled ?? false,
37
- }
38
-
39
- return { eligibility, isLoading, isError }
40
- }
@@ -1,52 +0,0 @@
1
- import { useQueryClient } from '@tanstack/react-query'
2
- import { useCallback } from 'react'
3
- import { ApiCollection, MessageResource } from '../types'
4
- import { prependNewerPage } from '../utils/cache/prepend_newer_page'
5
- import { getMessagesRequestArgs } from '../utils/request/get_messages'
6
- import { useApiClient } from './use_api_client'
7
-
8
- const MAX_PAGES = 5
9
-
10
- type Options = {
11
- conversation_id: number
12
- reply_root_id?: string | null
13
- queryKey: unknown[]
14
- onTooFarBehind?: () => void
15
- }
16
-
17
- export const useFetchNewerMessages = ({
18
- conversation_id,
19
- reply_root_id,
20
- queryKey,
21
- onTooFarBehind,
22
- }: Options) => {
23
- const queryClient = useQueryClient()
24
- const apiClient = useApiClient()
25
-
26
- return useCallback(
27
- async (newestMessageId: string) => {
28
- const { url, data } = getMessagesRequestArgs({ conversation_id, reply_root_id })
29
- const pages: ApiCollection<MessageResource>[] = []
30
- let cursor: string | undefined = newestMessageId
31
-
32
- while (cursor && pages.length < MAX_PAGES) {
33
- const page: ApiCollection<MessageResource> = await apiClient.chat.get({
34
- url,
35
- data: { ...data, where: { id_gt: cursor }, order: 'asc' },
36
- })
37
- pages.push(page)
38
- cursor = page.meta?.next?.idGt
39
- }
40
-
41
- queryClient.setQueryData<Parameters<typeof prependNewerPage<MessageResource>>[0]>(
42
- queryKey,
43
- prev => prependNewerPage(prev, pages)
44
- )
45
- const moreThanWeFetched = !!cursor
46
- if (!moreThanWeFetched) return
47
- if (onTooFarBehind) onTooFarBehind()
48
- else await queryClient.invalidateQueries({ queryKey })
49
- },
50
- [apiClient, conversation_id, onTooFarBehind, queryClient, queryKey, reply_root_id]
51
- )
52
- }
@@ -1,25 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
- import { useAppState } from './use_app_state'
3
-
4
- type Options = {
5
- fetchNewer: (newestMessageId: string) => Promise<void>
6
- newestMessageId?: string
7
- enabled: boolean
8
- }
9
-
10
- export const useFetchNewerOnForeground = ({ fetchNewer, newestMessageId, enabled }: Options) => {
11
- const appState = useAppState()
12
- const wasInBackground = useRef(false)
13
-
14
- useEffect(() => {
15
- if (appState !== 'active') {
16
- wasInBackground.current = true
17
- return
18
- }
19
- const returnedToForeground = wasInBackground.current
20
- wasInBackground.current = false
21
- if (!returnedToForeground || !enabled || !newestMessageId) return
22
-
23
- fetchNewer(newestMessageId).catch(() => {})
24
- }, [appState, enabled, fetchNewer, newestMessageId])
25
- }
@@ -1,33 +0,0 @@
1
- import { useCallback, useEffect, useRef } from 'react'
2
-
3
- type Options = {
4
- fetchNewer: (newestMessageId: string) => Promise<void>
5
- newestMessageId?: string
6
- readingHistory: boolean
7
- caughtUp: boolean
8
- }
9
-
10
- export const useHeldBackMessages = ({
11
- fetchNewer,
12
- newestMessageId,
13
- readingHistory,
14
- caughtUp,
15
- }: Options) => {
16
- const heldBackSince = useRef<string | null>(null)
17
-
18
- const onSkippedNewMessage = useCallback(() => {
19
- heldBackSince.current ??= newestMessageId ?? null
20
- }, [newestMessageId])
21
-
22
- useEffect(() => {
23
- const since = heldBackSince.current
24
- if (readingHistory || !caughtUp || !since) return
25
-
26
- heldBackSince.current = null
27
- fetchNewer(since).catch(() => {
28
- heldBackSince.current ??= since
29
- })
30
- }, [caughtUp, fetchNewer, readingHistory])
31
-
32
- return { onSkippedNewMessage }
33
- }
@@ -1,37 +0,0 @@
1
- import { QueryClient } from '@tanstack/react-query'
2
- import { getMessagesQueryKey } from '../../request/get_messages'
3
- import { findMessageInCaches } from '../find_message_in_caches'
4
-
5
- const page = (ids: string[]) => ({
6
- data: ids.map(id => ({ id, type: 'Message' })),
7
- links: {},
8
- meta: { count: ids.length, totalCount: ids.length },
9
- })
10
-
11
- const seed = (queryClient: QueryClient, key: unknown[], ids: string[]) =>
12
- queryClient.setQueryData(key, { pages: [page(ids)], pageParams: [{}] })
13
-
14
- describe('findMessageInCaches', () => {
15
- const queryClient = new QueryClient()
16
-
17
- beforeEach(() => {
18
- queryClient.clear()
19
- seed(queryClient, getMessagesQueryKey({ conversation_id: 1 }), ['01C', '01B'])
20
- seed(queryClient, getMessagesQueryKey({ conversation_id: 1, anchor: '01A' }), ['01A', '00Z'])
21
- seed(queryClient, getMessagesQueryKey({ conversation_id: 1, reply_root_id: '01B' }), ['01R'])
22
- seed(queryClient, getMessagesQueryKey({ conversation_id: 2 }), ['02A'])
23
- })
24
-
25
- it('finds a message in the live cache', () => {
26
- expect(findMessageInCaches(queryClient, 1, '01B')?.id).toBe('01B')
27
- })
28
-
29
- it('finds a message that only the anchored cache holds', () => {
30
- expect(findMessageInCaches(queryClient, 1, '00Z')?.id).toBe('00Z')
31
- })
32
-
33
- it('ignores reply threads and other conversations', () => {
34
- expect(findMessageInCaches(queryClient, 1, '01R')).toBeUndefined()
35
- expect(findMessageInCaches(queryClient, 1, '02A')).toBeUndefined()
36
- })
37
- })
@@ -1,23 +0,0 @@
1
- import { QueryClient } from '@tanstack/react-query'
2
- import { getMessagesQueryKey } from '../../request/get_messages'
3
- import { invalidateRefetchableQueries } from '../invalidate_refetchable_queries'
4
-
5
- describe('invalidateRefetchableQueries', () => {
6
- const queryClient = new QueryClient()
7
- const live = getMessagesQueryKey({ conversation_id: 1 })
8
- const anchored = getMessagesQueryKey({ conversation_id: 1, anchor: '01A' })
9
- const other = ['/me/conversations']
10
-
11
- beforeEach(() => {
12
- queryClient.clear()
13
- for (const key of [live, anchored, other]) queryClient.setQueryData(key, { pages: [], pageParams: [] })
14
- })
15
-
16
- it('invalidates everything except anchored message windows', async () => {
17
- await invalidateRefetchableQueries(queryClient)
18
-
19
- expect(queryClient.getQueryState(live)?.isInvalidated).toBe(true)
20
- expect(queryClient.getQueryState(other)?.isInvalidated).toBe(true)
21
- expect(queryClient.getQueryState(anchored)?.isInvalidated).toBe(false)
22
- })
23
- })
@@ -1,34 +0,0 @@
1
- import { prependNewerPage } from '../prepend_newer_page'
2
-
3
- const page = (ids: string[], next?: { idGt: string }) => ({
4
- data: ids.map(id => ({ id, type: 'Message' })),
5
- links: {},
6
- meta: { count: ids.length, totalCount: ids.length, next },
7
- })
8
-
9
- describe('prependNewerPage', () => {
10
- const cached = { pages: [page(['01C', '01B']), page(['01A'])], pageParams: [{}, { offset: 1 }] }
11
-
12
- it('merges the new rows into the first page and leaves the page params alone', () => {
13
- const result = prependNewerPage(cached, [page(['01C', '01D', '01E'])])
14
-
15
- expect(result?.pages).toHaveLength(2)
16
- expect(result?.pages[0].data.map(m => m.id)).toEqual(['01D', '01E', '01C', '01B'])
17
- expect(result?.pageParams).toEqual(cached.pageParams)
18
- })
19
-
20
- it('merges every fetched batch, newest last', () => {
21
- const result = prependNewerPage(cached, [page(['01D', '01E']), page(['01F'])])
22
-
23
- expect(result?.pages[0].data.map(m => m.id)).toEqual(['01D', '01E', '01F', '01C', '01B'])
24
- })
25
-
26
- it('returns the same data when nothing new came back', () => {
27
- expect(prependNewerPage(cached, [page(['01C'])])).toBe(cached)
28
- expect(prependNewerPage(cached, [])).toBe(cached)
29
- })
30
-
31
- it('does nothing without cached data', () => {
32
- expect(prependNewerPage(undefined, [page(['01D'])])).toBeUndefined()
33
- })
34
- })
@@ -1,20 +0,0 @@
1
- import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
- import { ApiCollection, MessageResource } from '../../types'
3
- import { getMessagesRequestArgs } from '../request/get_messages'
4
-
5
- type QueryData = InfiniteData<ApiCollection<MessageResource>>
6
-
7
- export const findMessageInCaches = (
8
- queryClient: QueryClient,
9
- conversation_id: number,
10
- message_id: string
11
- ): MessageResource | undefined => {
12
- const { url } = getMessagesRequestArgs({ conversation_id })
13
-
14
- for (const [, data] of queryClient.getQueriesData<QueryData>({ queryKey: [url] })) {
15
- const found = data?.pages.flatMap(page => page.data).find(message => message.id === message_id)
16
- if (found) return found
17
- }
18
-
19
- return undefined
20
- }
@@ -1,9 +0,0 @@
1
- import { Query, QueryClient } from '@tanstack/react-query'
2
-
3
- const isAnchoredMessagesWindow = (query: Query) => {
4
- const [, params] = query.queryKey as [unknown, { where?: { id_around?: string } } | undefined]
5
- return !!params?.where?.id_around
6
- }
7
-
8
- export const invalidateRefetchableQueries = (queryClient: QueryClient) =>
9
- queryClient.invalidateQueries({ predicate: query => !isAnchoredMessagesWindow(query) })
@@ -1,19 +0,0 @@
1
- import { InfiniteData } from '@tanstack/react-query'
2
- import { ApiCollection, ResourceObject } from '../../types'
3
- import { RequestData } from '../client'
4
-
5
- type PagesData<T extends ResourceObject> = InfiniteData<ApiCollection<T>, Partial<RequestData>>
6
-
7
- export function prependNewerPage<T extends ResourceObject>(
8
- data: PagesData<T> | undefined,
9
- pages: ApiCollection<T>[]
10
- ): PagesData<T> | undefined {
11
- if (!data || data.pages.length === 0) return data
12
-
13
- const knownIds = new Set(data.pages.flatMap(page => page.data.map(record => record.id)))
14
- const fresh = pages.flatMap(page => page.data).filter(record => !knownIds.has(record.id))
15
- if (fresh.length === 0) return data
16
-
17
- const [first, ...rest] = data.pages
18
- return { ...data, pages: [{ ...first, data: [...fresh, ...first.data] }, ...rest] }
19
- }
@@ -1,19 +0,0 @@
1
- import type { MessagesCache } from '../hooks/use_conversation_messages_jolt_events'
2
-
3
- export const gatedMessageCache = ({
4
- queryKey,
5
- acceptNewMessages,
6
- onSkippedNewMessage,
7
- }: {
8
- queryKey: unknown[]
9
- acceptNewMessages: boolean
10
- onSkippedNewMessage?: () => void
11
- }): MessagesCache => ({
12
- queryKey,
13
- canUpdate: event => {
14
- if (event !== 'message.created' || acceptNewMessages) return true
15
-
16
- onSkippedNewMessage?.()
17
- return false
18
- },
19
- })
package/src/utils/ulid.ts DELETED
@@ -1,11 +0,0 @@
1
- export const ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
2
-
3
- const TIME_LENGTH = 10
4
-
5
- export function decodeUlidTime(ulid: string): number {
6
- return ulid
7
- .slice(0, TIME_LENGTH)
8
- .toUpperCase()
9
- .split('')
10
- .reduce((time, char) => time * 32 + ULID_CHARS.indexOf(char), 0)
11
- }