@planningcenter/chat-react-native 4.0.1-qa-staging.2 → 4.0.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 (94) hide show
  1. package/build/components/conversation/message.d.ts +2 -3
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +22 -23
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_read_receipts.d.ts +4 -2
  6. package/build/components/conversation/message_read_receipts.d.ts.map +1 -1
  7. package/build/components/conversation/message_read_receipts.js +3 -2
  8. package/build/components/conversation/message_read_receipts.js.map +1 -1
  9. package/build/components/conversation/reply_shadow_message.d.ts +1 -2
  10. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  11. package/build/components/conversation/reply_shadow_message.js +46 -47
  12. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  13. package/build/components/primitive/form_sheet.d.ts +1 -2
  14. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  15. package/build/components/primitive/form_sheet.js +53 -68
  16. package/build/components/primitive/form_sheet.js.map +1 -1
  17. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  18. package/build/hooks/use_conversation_messages.js +28 -40
  19. package/build/hooks/use_conversation_messages.js.map +1 -1
  20. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  21. package/build/hooks/use_conversation_messages_jolt_events.js +1 -6
  22. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  23. package/build/hooks/use_message_create_or_update.d.ts +4 -99
  24. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  25. package/build/hooks/use_message_create_or_update.js +6 -47
  26. package/build/hooks/use_message_create_or_update.js.map +1 -1
  27. package/build/screens/conversation_new/components/groups_form.js +80 -71
  28. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  29. package/build/screens/conversation_screen.d.ts.map +1 -1
  30. package/build/screens/conversation_screen.js +365 -184
  31. package/build/screens/conversation_screen.js.map +1 -1
  32. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_select_type_screen.js +98 -82
  34. package/build/screens/conversation_select_type_screen.js.map +1 -1
  35. package/build/types/resources/message.d.ts +0 -2
  36. package/build/types/resources/message.d.ts.map +1 -1
  37. package/build/types/resources/message.js.map +1 -1
  38. package/build/utils/cache/messages_cache.d.ts +0 -1
  39. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  40. package/build/utils/cache/messages_cache.js +7 -14
  41. package/build/utils/cache/messages_cache.js.map +1 -1
  42. package/build/utils/cache/optimistically_create_message.d.ts +1 -2
  43. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  44. package/build/utils/cache/optimistically_create_message.js +2 -4
  45. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  46. package/build/utils/client/transform_response.d.ts.map +1 -1
  47. package/build/utils/client/transform_response.js +5 -9
  48. package/build/utils/client/transform_response.js.map +1 -1
  49. package/build/utils/group_messages.d.ts +0 -1
  50. package/build/utils/group_messages.d.ts.map +1 -1
  51. package/build/utils/group_messages.js +1 -10
  52. package/build/utils/group_messages.js.map +1 -1
  53. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  54. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +0 -1
  55. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  56. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  57. package/build/utils/request/messages_data_options.js +2 -2
  58. package/build/utils/request/messages_data_options.js.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/conversation/message.tsx +18 -15
  61. package/src/components/conversation/message_read_receipts.tsx +5 -3
  62. package/src/components/conversation/reply_shadow_message.tsx +7 -9
  63. package/src/components/primitive/form_sheet.tsx +3 -10
  64. package/src/hooks/use_conversation_messages.ts +1 -5
  65. package/src/hooks/use_conversation_messages_jolt_events.ts +0 -7
  66. package/src/hooks/use_message_create_or_update.ts +16 -29
  67. package/src/screens/conversation_new/components/groups_form.tsx +7 -2
  68. package/src/screens/conversation_screen.tsx +104 -11
  69. package/src/screens/conversation_select_type_screen.tsx +9 -3
  70. package/src/types/resources/message.ts +0 -2
  71. package/src/utils/__tests__/group_messages.test.ts +0 -48
  72. package/src/utils/cache/messages_cache.ts +11 -27
  73. package/src/utils/cache/optimistically_create_message.ts +0 -3
  74. package/src/utils/client/transform_response.ts +5 -7
  75. package/src/utils/group_messages.ts +1 -18
  76. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +0 -1
  77. package/src/utils/request/messages_data_options.ts +1 -2
  78. package/build/components/conversation/message_list.d.ts +0 -21
  79. package/build/components/conversation/message_list.d.ts.map +0 -1
  80. package/build/components/conversation/message_list.js +0 -273
  81. package/build/components/conversation/message_list.js.map +0 -1
  82. package/build/utils/replace_equal_by_id.d.ts +0 -6
  83. package/build/utils/replace_equal_by_id.d.ts.map +0 -1
  84. package/build/utils/replace_equal_by_id.js +0 -41
  85. package/build/utils/replace_equal_by_id.js.map +0 -1
  86. package/src/components/conversation/__tests__/message_list.test.tsx +0 -271
  87. package/src/components/conversation/__tests__/reply_shadow_message.test.tsx +0 -119
  88. package/src/components/conversation/message_list.tsx +0 -157
  89. package/src/hooks/__tests__/use_message_create_or_update.test.tsx +0 -110
  90. package/src/utils/__tests__/replace_equal_by_id.test.ts +0 -73
  91. package/src/utils/cache/__tests__/messages_cache.test.ts +0 -89
  92. package/src/utils/cache/__tests__/messages_cache_reply_roots.test.ts +0 -70
  93. package/src/utils/client/__tests__/transform_response.test.ts +0 -62
  94. package/src/utils/replace_equal_by_id.ts +0 -42
@@ -1,73 +0,0 @@
1
- import { replaceEqualById } from '../replace_equal_by_id'
2
-
3
- type Record = { type: 'Message'; id: string; text: string; attachments: string[] }
4
- type Pages = { pages: { data: Record[]; links: {}; meta: {} }[]; pageParams: unknown[] }
5
-
6
- const record = (id: string, text = id): Record => ({ type: 'Message', id, text, attachments: [] })
7
- const pagesOf = (...pages: Record[][]): Pages => ({
8
- pages: pages.map(data => ({ data, links: {}, meta: {} })),
9
- pageParams: pages.map(() => ({})),
10
- })
11
-
12
- const share = (prev: Pages, next: Pages) => replaceEqualById(prev, next) as Pages
13
-
14
- describe('replaceEqualById', () => {
15
- it('returns the previous data when nothing changed', () => {
16
- const prev = pagesOf([record('c'), record('b')], [record('a')])
17
- const next = pagesOf([record('c'), record('b')], [record('a')])
18
-
19
- expect(share(prev, next)).toBe(prev)
20
- })
21
-
22
- it('keeps every existing record when one is inserted at the head', () => {
23
- const [c, b] = [record('c'), record('b')]
24
- const prev = pagesOf([c, b])
25
- const next = pagesOf([record('d'), record('c'), record('b')])
26
-
27
- const result = share(prev, next)
28
-
29
- expect(result.pages[0].data[1]).toBe(c)
30
- expect(result.pages[0].data[2]).toBe(b)
31
- expect(result.pages[0].data[0].id).toBe('d')
32
- })
33
-
34
- it('keeps the survivors when a record is removed', () => {
35
- const [c, b, a] = [record('c'), record('b'), record('a')]
36
- const prev = pagesOf([c, b, a])
37
- const next = pagesOf([record('c'), record('a')])
38
-
39
- const result = share(prev, next)
40
-
41
- expect(result.pages[0].data).toEqual([c, a])
42
- expect(result.pages[0].data[0]).toBe(c)
43
- expect(result.pages[0].data[1]).toBe(a)
44
- })
45
-
46
- it('replaces only the changed record and keeps its untouched nested references', () => {
47
- const b = record('b')
48
- const prev = pagesOf([record('c'), b])
49
- const next = pagesOf([record('c'), { ...record('b', 'edited'), attachments: [] }])
50
-
51
- const result = share(prev, next)
52
-
53
- expect(result.pages[0].data[0]).toBe(prev.pages[0].data[0])
54
- expect(result.pages[0].data[1]).not.toBe(b)
55
- expect(result.pages[0].data[1].text).toBe('edited')
56
- expect(result.pages[0].data[1].attachments).toBe(b.attachments)
57
- })
58
-
59
- it('keeps untouched pages when only another page changed', () => {
60
- const prev = pagesOf([record('c')], [record('a')])
61
- const next = pagesOf([record('d'), record('c')], [record('a')])
62
-
63
- const result = share(prev, next)
64
-
65
- expect(result.pages[1]).toBe(prev.pages[1])
66
- })
67
-
68
- it('returns the new data when there is nothing to share with', () => {
69
- const next = pagesOf([record('a')])
70
-
71
- expect(replaceEqualById(undefined, next)).toBe(next)
72
- })
73
- })
@@ -1,89 +0,0 @@
1
- import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
- import type { ApiCollection, MessageResource } from '../../../types'
3
- import type { PersonResource } from '../../../types/resources/person'
4
- import { updateCacheWithMessage } from '../messages_cache'
5
-
6
- type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
7
-
8
- const queryKey = ['messages']
9
- const author = { type: 'Person', id: 1, name: 'Me', avatar: null } as unknown as PersonResource
10
-
11
- const message = (overrides: Partial<MessageResource>): MessageResource =>
12
- ({
13
- type: 'Message',
14
- text: 'ok',
15
- html: '',
16
- createdAt: '2026-01-01T00:00:00Z',
17
- deletedAt: null,
18
- textEditedAt: null,
19
- mine: true,
20
- attachments: [],
21
- author,
22
- reactionCounts: [],
23
- replyCount: 0,
24
- replyRootId: null,
25
- messageType: 'user',
26
- ...overrides,
27
- }) as MessageResource
28
-
29
- const seed = (queryClient: QueryClient, ...messages: MessageResource[]) =>
30
- queryClient.setQueryData<MessagesQueryData>(queryKey, {
31
- pages: [
32
- { data: messages, links: {}, meta: { count: messages.length, totalCount: messages.length } },
33
- ],
34
- pageParams: [],
35
- })
36
-
37
- const idsInCache = (queryClient: QueryClient) =>
38
- queryClient.getQueryData<MessagesQueryData>(queryKey)!.pages[0].data.map(m => m.id)
39
-
40
- const echo = (queryClient: QueryClient, overrides: Partial<MessageResource>) =>
41
- updateCacheWithMessage(queryClient, queryKey, message(overrides), 'message.created')
42
-
43
- describe('updateCacheWithMessage on message.created', () => {
44
- let queryClient: QueryClient
45
-
46
- beforeEach(() => {
47
- queryClient = new QueryClient()
48
- seed(
49
- queryClient,
50
- message({ id: '01B-temp', idempotentKey: 'key-b', pending: true }),
51
- message({ id: '01A-temp', idempotentKey: 'key-a', pending: true })
52
- )
53
- })
54
-
55
- it('swaps out only the pending message with the same idempotent key', () => {
56
- echo(queryClient, { id: '01A', idempotentKey: 'key-a' })
57
-
58
- expect(idsInCache(queryClient)).toEqual(['01A', '01B-temp'])
59
- })
60
-
61
- it('does not swap out a pending message that only shares the same text', () => {
62
- echo(queryClient, { id: '01C', idempotentKey: 'key-c', text: 'ok' })
63
-
64
- expect(idsInCache(queryClient)).toEqual(['01C', '01B-temp', '01A-temp'])
65
- })
66
-
67
- it('swaps out an attachment-only pending message, which has no text to match', () => {
68
- seed(
69
- queryClient,
70
- message({ id: '01D-temp', idempotentKey: 'key-d', text: null, pending: true })
71
- )
72
-
73
- echo(queryClient, { id: '01D', idempotentKey: 'key-d', text: null })
74
-
75
- expect(idsInCache(queryClient)).toEqual(['01D'])
76
- })
77
-
78
- it('leaves pending messages alone when the echo carries no key', () => {
79
- echo(queryClient, { id: '01E', idempotentKey: null })
80
-
81
- expect(idsInCache(queryClient)).toEqual(['01E', '01B-temp', '01A-temp'])
82
- })
83
-
84
- it("leaves pending messages alone when the echo is someone else's", () => {
85
- echo(queryClient, { id: '01F', idempotentKey: 'key-a', mine: false })
86
-
87
- expect(idsInCache(queryClient)).toEqual(['01F', '01B-temp', '01A-temp'])
88
- })
89
- })
@@ -1,70 +0,0 @@
1
- import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
- import type { ApiCollection } from '../../../types'
3
- import type { MessageResource } from '../../../types/resources/message'
4
- import type { PersonResource } from '../../../types/resources/person'
5
- import { updateCacheReplyRoots } from '../messages_cache'
6
-
7
- const author = { id: 1, type: 'Person', name: 'A', avatar: null } as unknown as PersonResource
8
-
9
- const message = (id: string, overrides: Partial<MessageResource> = {}): MessageResource =>
10
- ({
11
- type: 'Message',
12
- id,
13
- text: `msg ${id}`,
14
- html: '',
15
- createdAt: '2026-01-01T00:00:00Z',
16
- deletedAt: null,
17
- textEditedAt: null,
18
- mine: false,
19
- attachments: [],
20
- author,
21
- reactionCounts: [],
22
- replyCount: 0,
23
- replyRootId: null,
24
- messageType: 'message',
25
- ...overrides,
26
- }) as MessageResource
27
-
28
- const queryKey = ['messages']
29
-
30
- const seedPages = (queryClient: QueryClient, messages: MessageResource[]) => {
31
- queryClient.setQueryData<InfiniteData<ApiCollection<MessageResource>>>(queryKey, {
32
- pageParams: [{}],
33
- pages: [{ data: messages, links: {}, meta: { count: messages.length, totalCount: 1 } }],
34
- })
35
- }
36
-
37
- const pageMessages = (queryClient: QueryClient) =>
38
- queryClient.getQueryData<InfiniteData<ApiCollection<MessageResource>>>(queryKey)!.pages[0].data
39
-
40
- describe('updateCacheReplyRoots', () => {
41
- it('merges a root update into every reply carrying that root', () => {
42
- const queryClient = new QueryClient()
43
- const root = message('rootA', { replyRootId: 'rootA', text: 'before' })
44
- seedPages(queryClient, [
45
- message('03', { replyRootId: 'rootA', replyRoot: root }),
46
- message('02', { replyRootId: 'rootB', replyRoot: message('rootB', { text: 'other' }) }),
47
- message('01', { replyRootId: 'rootA', replyRoot: root }),
48
- ])
49
-
50
- updateCacheReplyRoots(
51
- queryClient,
52
- queryKey,
53
- message('rootA', { replyRootId: 'rootA', text: 'after', deletedAt: '2026-01-02T00:00:00Z' })
54
- )
55
-
56
- const [newest, other, oldest] = pageMessages(queryClient)
57
- expect(newest.replyRoot).toMatchObject({ text: 'after', deletedAt: '2026-01-02T00:00:00Z' })
58
- expect(oldest.replyRoot).toMatchObject({ text: 'after' })
59
- expect(other.replyRoot?.text).toBe('other')
60
- })
61
-
62
- it('ignores replies without an inline root', () => {
63
- const queryClient = new QueryClient()
64
- seedPages(queryClient, [message('01', { replyRootId: 'rootA' })])
65
-
66
- updateCacheReplyRoots(queryClient, queryKey, message('rootA', { replyRootId: 'rootA' }))
67
-
68
- expect(pageMessages(queryClient)[0].replyRoot).toBeUndefined()
69
- })
70
- })
@@ -1,62 +0,0 @@
1
- import transformResponse from '../transform_response'
2
-
3
- const person = (id: string, name: string) => ({
4
- type: 'Person',
5
- id,
6
- attributes: { name, avatar: null },
7
- })
8
-
9
- const message = (
10
- id: string,
11
- { authorId, replyRootId }: { authorId: string; replyRootId: string | null }
12
- ) => ({
13
- type: 'Message',
14
- id,
15
- attributes: { text: `msg ${id}` },
16
- relationships: {
17
- author: { data: { type: 'Person', id: authorId } },
18
- reply_root: { data: replyRootId ? { type: 'Message', id: replyRootId } : null },
19
- reaction_counts: { data: [] },
20
- },
21
- })
22
-
23
- const root = message('01ROOT', { authorId: '2', replyRootId: '01ROOT' })
24
- const reply = message('01REPLY', { authorId: '1', replyRootId: '01ROOT' })
25
-
26
- describe('transformResponse — reply_root include', () => {
27
- it('attaches the included root and its author to a reply', () => {
28
- const result = transformResponse({
29
- data: [reply],
30
- included: [root, person('1', 'Replier'), person('2', 'Root Author')],
31
- })
32
-
33
- const [transformedReply] = result.data
34
- expect(transformedReply.replyRootId).toBe('01ROOT')
35
- expect(transformedReply.replyRoot.id).toBe('01ROOT')
36
- expect(transformedReply.replyRoot.text).toBe('msg 01ROOT')
37
- expect(transformedReply.replyRoot.author.name).toBe('Root Author')
38
- })
39
-
40
- it('drops a record’s self reference so the result stays acyclic', () => {
41
- const result = transformResponse({
42
- data: [reply, root],
43
- included: [root, person('1', 'Replier'), person('2', 'Root Author')],
44
- })
45
-
46
- const [transformedReply, transformedRoot] = result.data
47
- expect(transformedReply.replyRoot.replyRoot).toBeNull()
48
- expect(transformedRoot.replyRootId).toBe('01ROOT')
49
- expect(transformedRoot.replyRoot).toBeNull()
50
- expect(() => JSON.stringify(result)).not.toThrow()
51
- })
52
-
53
- it('leaves replyRoot null when the root is not included', () => {
54
- const result = transformResponse({
55
- data: [reply],
56
- included: [person('1', 'Replier')],
57
- })
58
-
59
- expect(result.data[0].replyRootId).toBe('01ROOT')
60
- expect(result.data[0].replyRoot).toBeNull()
61
- })
62
- })
@@ -1,42 +0,0 @@
1
- import { InfiniteData, replaceEqualDeep } from '@tanstack/react-query'
2
- import { ApiCollection, ResourceObject } from '../types'
3
-
4
- type Pages = InfiniteData<ApiCollection<ResourceObject>>
5
-
6
- /** replaceEqualById
7
- * TanStack's default structural sharing compares arrays by index, so an insert or
8
- * delete gives every shifted record a new identity. Match by id instead.
9
- */
10
- export function replaceEqualById(oldData: unknown, newData: unknown): unknown {
11
- const prev = oldData as Pages | undefined
12
- const next = newData as Pages
13
- if (!prev?.pages || !next?.pages) return replaceEqualDeep(prev, next)
14
-
15
- const prevById = new Map<ResourceObject['id'], ResourceObject>()
16
- prev.pages.forEach(page => page.data.forEach(record => prevById.set(record.id, record)))
17
-
18
- let changed = prev.pages.length !== next.pages.length
19
- const pages = next.pages.map((page, index) => {
20
- const prevPage = prev.pages[index]
21
- const data = page.data.map(record => {
22
- const previous = prevById.get(record.id)
23
- return previous ? replaceEqualDeep(previous, record) : record
24
- })
25
- const links = replaceEqualDeep(prevPage?.links, page.links)
26
- const meta = replaceEqualDeep(prevPage?.meta, page.meta)
27
- const sameRecords =
28
- !!prevPage &&
29
- data.length === prevPage.data.length &&
30
- data.every((record, i) => record === prevPage.data[i])
31
-
32
- if (prevPage && sameRecords && links === prevPage.links && meta === prevPage.meta) {
33
- return prevPage
34
- }
35
- changed = true
36
- return { ...page, data, links, meta }
37
- })
38
- const pageParams = replaceEqualDeep(prev.pageParams, next.pageParams)
39
-
40
- if (!changed && pageParams === prev.pageParams) return prev
41
- return { ...next, pages, pageParams }
42
- }