@planningcenter/chat-react-native 3.36.2-rc.3 → 3.37.0-rc.1
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/build/components/display/action_button.d.ts +3 -1
- package/build/components/display/action_button.d.ts.map +1 -1
- package/build/components/display/action_button.js +8 -1
- package/build/components/display/action_button.js.map +1 -1
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts +168 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.js +23 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -0
- package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +0 -4
- package/build/hooks/groups/use_group_members_for_new_conversation.d.ts.map +1 -1
- package/build/hooks/groups/use_group_members_for_new_conversation.js +6 -18
- package/build/hooks/groups/use_group_members_for_new_conversation.js.map +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts +11 -3
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.js +10 -14
- package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
- package/build/hooks/services/use_services_chat_conversation_payload.d.ts +164 -0
- package/build/hooks/services/use_services_chat_conversation_payload.d.ts.map +1 -0
- package/build/hooks/services/use_services_chat_conversation_payload.js +16 -0
- package/build/hooks/services/use_services_chat_conversation_payload.js.map +1 -0
- package/build/hooks/services/use_team_members_for_new_conversation.d.ts.map +1 -1
- package/build/hooks/services/use_team_members_for_new_conversation.js +11 -4
- package/build/hooks/services/use_team_members_for_new_conversation.js.map +1 -1
- package/build/hooks/use_conversation_validate.d.ts +12 -0
- package/build/hooks/use_conversation_validate.d.ts.map +1 -0
- package/build/hooks/use_conversation_validate.js +28 -0
- package/build/hooks/use_conversation_validate.js.map +1 -0
- package/build/hooks/use_enrich_people.d.ts +13 -0
- package/build/hooks/use_enrich_people.d.ts.map +1 -0
- package/build/hooks/use_enrich_people.js +25 -0
- package/build/hooks/use_enrich_people.js.map +1 -0
- package/build/hooks/use_features.d.ts +1 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +1 -0
- package/build/hooks/use_features.js.map +1 -1
- package/build/screens/conversation_details_screen.js +1 -1
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/groups_form.js +14 -1
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +20 -2
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/build/screens/team_conversation_screen.d.ts.map +1 -1
- package/build/screens/team_conversation_screen.js +6 -3
- package/build/screens/team_conversation_screen.js.map +1 -1
- package/build/types/resources/conversation_validate.d.ts +10 -0
- package/build/types/resources/conversation_validate.d.ts.map +1 -0
- package/build/types/resources/conversation_validate.js +2 -0
- package/build/types/resources/conversation_validate.js.map +1 -0
- package/build/types/resources/index.d.ts +1 -0
- package/build/types/resources/index.d.ts.map +1 -1
- package/build/types/resources/index.js +1 -0
- package/build/types/resources/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/hooks/use_conversation_validate.test.tsx +117 -0
- package/src/__tests__/hooks/use_enrich_people.test.tsx +95 -0
- package/src/components/display/action_button.tsx +18 -0
- package/src/hooks/groups/use_group_chat_conversation_payload.ts +38 -0
- package/src/hooks/groups/use_group_members_for_new_conversation.ts +9 -23
- package/src/hooks/groups/use_groups_conversation_create.ts +1 -1
- package/src/hooks/services/use_find_or_create_services_conversation.ts +27 -24
- package/src/hooks/services/use_services_chat_conversation_payload.ts +26 -0
- package/src/hooks/services/use_team_members_for_new_conversation.ts +18 -7
- package/src/hooks/use_conversation_validate.ts +45 -0
- package/src/hooks/use_enrich_people.ts +35 -0
- package/src/hooks/use_features.ts +1 -0
- package/src/screens/conversation_details_screen.tsx +1 -1
- package/src/screens/conversation_new/components/groups_form.tsx +17 -1
- package/src/screens/conversation_new/components/services_form.tsx +26 -2
- package/src/screens/team_conversation_screen.tsx +6 -6
- package/src/types/resources/conversation_validate.ts +11 -0
- package/src/types/resources/index.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA","sourcesContent":["export * from './analytics_metadata'\nexport * from './conversation'\nexport * from './conversation_membership'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\nexport * from './groups'\nexport * from './app_grant'\nexport * from './services'\nexport * from './organization'\nexport * from './group_membership'\nexport * from './group_resource'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA","sourcesContent":["export * from './analytics_metadata'\nexport * from './conversation'\nexport * from './conversation_membership'\nexport * from './conversation_validate'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\nexport * from './groups'\nexport * from './app_grant'\nexport * from './services'\nexport * from './organization'\nexport * from './group_membership'\nexport * from './group_resource'\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.0-rc.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "./src/index.tsx",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"react-native-url-polyfill": "^2.0.0",
|
|
73
73
|
"typescript": "~5.9.2"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "7c6773155a5b589fe9a58a28921ddaac1d134e86"
|
|
76
76
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import * as useApiClientModule from '../../hooks/use_api_client'
|
|
6
|
+
import { useConversationValidate } from '../../hooks/use_conversation_validate'
|
|
7
|
+
|
|
8
|
+
const createWrapper = () => {
|
|
9
|
+
const queryClient = buildTestQueryClient()
|
|
10
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
11
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const mockChatPost = (impl: jest.Mock) => {
|
|
16
|
+
jest.spyOn(useApiClientModule, 'useApiClient').mockReturnValue({
|
|
17
|
+
chat: { post: impl },
|
|
18
|
+
} as unknown as ReturnType<typeof useApiClientModule.useApiClient>)
|
|
19
|
+
return impl
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolveWith = (warnings: { attribute: string; fullMessage: string }[]) =>
|
|
23
|
+
jest.fn().mockResolvedValue({
|
|
24
|
+
data: { type: 'ConversationValidate', id: '1', warnings },
|
|
25
|
+
links: {},
|
|
26
|
+
meta: {},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('useConversationValidate', () => {
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.restoreAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('does not fire the request when disabled', () => {
|
|
35
|
+
const post = mockChatPost(resolveWith([]))
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(
|
|
38
|
+
() => useConversationValidate({ payload: 'abc', enabled: false }),
|
|
39
|
+
{ wrapper: createWrapper() }
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(post).not.toHaveBeenCalled()
|
|
43
|
+
expect(result.current.warnings).toEqual([])
|
|
44
|
+
expect(result.current.validationPending).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not fire the request when payload is undefined', () => {
|
|
48
|
+
const post = mockChatPost(resolveWith([]))
|
|
49
|
+
|
|
50
|
+
const { result } = renderHook(
|
|
51
|
+
() => useConversationValidate({ payload: undefined, isLoadingPayload: true }),
|
|
52
|
+
{ wrapper: createWrapper() }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(post).not.toHaveBeenCalled()
|
|
56
|
+
// validationPending stays true while the upstream payload query is still loading
|
|
57
|
+
expect(result.current.validationPending).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns warnings from the response and clears validationPending', async () => {
|
|
61
|
+
const warnings = [{ attribute: 'safety_policy', fullMessage: 'Needs a second adult.' }]
|
|
62
|
+
mockChatPost(resolveWith(warnings))
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
65
|
+
wrapper: createWrapper(),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await waitFor(() => expect(result.current.warnings).toEqual(warnings))
|
|
69
|
+
expect(result.current.validationPending).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('sends gender_id in attributes when provided', async () => {
|
|
73
|
+
const post = mockChatPost(resolveWith([]))
|
|
74
|
+
|
|
75
|
+
renderHook(() => useConversationValidate({ payload: 'abc', genderId: '42' }), {
|
|
76
|
+
wrapper: createWrapper(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
80
|
+
expect(post).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
url: '/me/conversation_validate',
|
|
83
|
+
data: {
|
|
84
|
+
data: {
|
|
85
|
+
type: 'ConversationValidate',
|
|
86
|
+
attributes: { payload: 'abc', gender_id: '42' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('omits gender_id when not provided', async () => {
|
|
94
|
+
const post = mockChatPost(resolveWith([]))
|
|
95
|
+
|
|
96
|
+
renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
97
|
+
wrapper: createWrapper(),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
101
|
+
const [call] = post.mock.calls
|
|
102
|
+
expect(call[0].data.data.attributes).toEqual({ payload: 'abc' })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('falls back to empty warnings and unblocks on request error', async () => {
|
|
106
|
+
// Documents the current fail-open behavior: if validate errors, the user is allowed to proceed.
|
|
107
|
+
const post = mockChatPost(jest.fn().mockRejectedValue(new Error('boom')))
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
110
|
+
wrapper: createWrapper(),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
114
|
+
await waitFor(() => expect(result.current.validationPending).toBe(false))
|
|
115
|
+
expect(result.current.warnings).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import { useApiClient } from '../../hooks/use_api_client'
|
|
6
|
+
import { useEnrichPeople } from '../../hooks/use_enrich_people'
|
|
7
|
+
|
|
8
|
+
jest.mock('../../hooks/use_api_client')
|
|
9
|
+
|
|
10
|
+
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
11
|
+
const mockPost = jest.fn()
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockPost.mockReset()
|
|
15
|
+
mockedUseApiClient.mockReturnValue({ chat: { post: mockPost } } as any)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const createWrapper = () => {
|
|
19
|
+
const queryClient = buildTestQueryClient()
|
|
20
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
21
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('returns empty map and skips the request when personIds is empty', () => {
|
|
26
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [] }), {
|
|
27
|
+
wrapper: createWrapper(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(result.current.size).toBe(0)
|
|
31
|
+
expect(mockPost).not.toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns enrichment data keyed by person id', async () => {
|
|
35
|
+
mockPost.mockResolvedValue({
|
|
36
|
+
data: [
|
|
37
|
+
{ id: '1', type: 'PersonEnrichment', badges: [{ title: 'Leader' }] },
|
|
38
|
+
{ id: '2', type: 'PersonEnrichment', badges: [] },
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [1, 2] }), {
|
|
43
|
+
wrapper: createWrapper(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await waitFor(() => expect(result.current.size).toBe(2))
|
|
47
|
+
|
|
48
|
+
expect(result.current.get(1)?.badges).toEqual([{ title: 'Leader' }])
|
|
49
|
+
expect(result.current.get(2)?.badges).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('includes group_id in the request when provided', async () => {
|
|
53
|
+
mockPost.mockResolvedValue({ data: [] })
|
|
54
|
+
|
|
55
|
+
renderHook(() => useEnrichPeople({ personIds: [1], groupId: 42 }), {
|
|
56
|
+
wrapper: createWrapper(),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
60
|
+
|
|
61
|
+
expect(mockPost).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
data: expect.objectContaining({
|
|
64
|
+
data: expect.objectContaining({
|
|
65
|
+
attributes: expect.objectContaining({ group_id: 42 }),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('omits group_id from the request when not provided', async () => {
|
|
73
|
+
mockPost.mockResolvedValue({ data: [] })
|
|
74
|
+
|
|
75
|
+
renderHook(() => useEnrichPeople({ personIds: [1] }), {
|
|
76
|
+
wrapper: createWrapper(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
80
|
+
|
|
81
|
+
const attributes = mockPost.mock.calls[0][0].data.data.attributes
|
|
82
|
+
expect(attributes).not.toHaveProperty('group_id')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('returns empty map when the request fails', async () => {
|
|
86
|
+
mockPost.mockRejectedValue(new Error('network error'))
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [1] }), {
|
|
89
|
+
wrapper: createWrapper(),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
93
|
+
|
|
94
|
+
expect(result.current.size).toBe(0)
|
|
95
|
+
})
|
|
@@ -3,7 +3,9 @@ import { useEffect } from 'react'
|
|
|
3
3
|
import { Animated, LayoutAnimation, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
5
5
|
import { useTheme } from '../../hooks'
|
|
6
|
+
import { ConversationWarning } from '../../types'
|
|
6
7
|
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
|
|
8
|
+
import { Banner } from './banner'
|
|
7
9
|
import { Button } from './button'
|
|
8
10
|
import { IconString } from './icon'
|
|
9
11
|
import { Text } from './text'
|
|
@@ -17,6 +19,7 @@ export const ActionButton = ({
|
|
|
17
19
|
buttonIconNameLeft,
|
|
18
20
|
secondaryButton,
|
|
19
21
|
loading = false,
|
|
22
|
+
warnings = [],
|
|
20
23
|
}: {
|
|
21
24
|
visible?: boolean
|
|
22
25
|
disabled?: boolean
|
|
@@ -26,6 +29,7 @@ export const ActionButton = ({
|
|
|
26
29
|
buttonIconNameLeft?: IconString
|
|
27
30
|
secondaryButton?: React.ReactNode
|
|
28
31
|
loading?: boolean
|
|
32
|
+
warnings?: ConversationWarning[]
|
|
29
33
|
}) => {
|
|
30
34
|
const styles = useStyles()
|
|
31
35
|
const [show, setShow] = useState(visible)
|
|
@@ -41,6 +45,17 @@ export const ActionButton = ({
|
|
|
41
45
|
|
|
42
46
|
return (
|
|
43
47
|
<Animated.View style={styles.container}>
|
|
48
|
+
{warnings.length > 0 && (
|
|
49
|
+
<View style={styles.warnings}>
|
|
50
|
+
{warnings.map((warning, index) => (
|
|
51
|
+
<Banner
|
|
52
|
+
key={`${warning.attribute}-${index}`}
|
|
53
|
+
appearance="error"
|
|
54
|
+
description={warning.fullMessage}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
)}
|
|
44
59
|
{Boolean(infoText) && (
|
|
45
60
|
<Text style={styles.infoText} variant="footnote">
|
|
46
61
|
{infoText}
|
|
@@ -95,5 +110,8 @@ const useStyles = () => {
|
|
|
95
110
|
infoText: {
|
|
96
111
|
textAlign: 'center',
|
|
97
112
|
},
|
|
113
|
+
warnings: {
|
|
114
|
+
gap: 8,
|
|
115
|
+
},
|
|
98
116
|
})
|
|
99
117
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiResource, ResourceObject } from '../../types'
|
|
3
|
+
import { useApiClient } from '../use_api_client'
|
|
4
|
+
|
|
5
|
+
const STALE_TIME_MS = 25 * 60 * 1000 // under Groups' 30-min server-side expiry
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
groupId: number
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useGroupChatConversationPayload({ groupId, enabled = true }: Props) {
|
|
13
|
+
const apiClient = useApiClient()
|
|
14
|
+
const url = `/me/groups/${groupId}/chat_conversation_payload`
|
|
15
|
+
|
|
16
|
+
const { data, ...rest } = useQuery({
|
|
17
|
+
queryKey: ['groups', url],
|
|
18
|
+
queryFn: () =>
|
|
19
|
+
apiClient.groups.post<ApiResource<GroupChatConversationPayload>>({
|
|
20
|
+
url,
|
|
21
|
+
data: {
|
|
22
|
+
data: {
|
|
23
|
+
type: 'GroupChatConversationPayload',
|
|
24
|
+
attributes: { title: '' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
staleTime: STALE_TIME_MS,
|
|
29
|
+
enabled,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return { payload: data?.data.value, ...rest }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface GroupChatConversationPayload extends ResourceObject {
|
|
36
|
+
type: 'GroupChatConversationPayload'
|
|
37
|
+
value: string
|
|
38
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { MemberResource, ResourceObject } from '../../types'
|
|
2
3
|
import { GroupsGroupMemberResource } from '../../types/resources/groups/groups_member_resource_with_person'
|
|
3
|
-
import {
|
|
4
|
+
import { useEnrichPeople } from '../use_enrich_people'
|
|
4
5
|
import { useSuspensePaginator } from '../use_suspense_api'
|
|
5
6
|
|
|
6
7
|
type UseSuspensePaginatorResult<T extends ResourceObject> = ReturnType<
|
|
@@ -16,10 +17,6 @@ export type GroupMembersForNewConversationResult = Omit<
|
|
|
16
17
|
childMembers: MemberResource[]
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
* This is specifically for the new conversation screen because we assign
|
|
21
|
-
* the "Conversation owner" badge to the current person.
|
|
22
|
-
*/
|
|
23
20
|
export function useGroupMembersForNewConversation({
|
|
24
21
|
id,
|
|
25
22
|
gender,
|
|
@@ -27,7 +24,6 @@ export function useGroupMembersForNewConversation({
|
|
|
27
24
|
id: number
|
|
28
25
|
gender: string | null
|
|
29
26
|
}) {
|
|
30
|
-
const currentPerson = useCurrentPerson()
|
|
31
27
|
const response = useSuspensePaginator<GroupsGroupMemberResource>({
|
|
32
28
|
url: `/me/groups/${id}/memberships`,
|
|
33
29
|
data: {
|
|
@@ -43,12 +39,16 @@ export function useGroupMembersForNewConversation({
|
|
|
43
39
|
})
|
|
44
40
|
|
|
45
41
|
const { data: memberships = [] } = response
|
|
42
|
+
const personIds = useMemo(() => memberships.map(m => +m.person.id), [memberships])
|
|
43
|
+
const enrichmentMap = useEnrichPeople({ personIds, groupId: id })
|
|
44
|
+
|
|
46
45
|
const members: MemberResource[] = memberships.map(membership => {
|
|
47
46
|
const { person } = membership
|
|
47
|
+
const enrichment = enrichmentMap.get(+person.id)
|
|
48
48
|
return {
|
|
49
49
|
type: 'Member',
|
|
50
50
|
avatar: person.avatarUrl,
|
|
51
|
-
badges:
|
|
51
|
+
badges: enrichment?.badges ?? [],
|
|
52
52
|
child: person.child,
|
|
53
53
|
gender: person.gender,
|
|
54
54
|
id: +person.id,
|
|
@@ -57,6 +57,7 @@ export function useGroupMembersForNewConversation({
|
|
|
57
57
|
role: membership.role,
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
|
+
|
|
60
61
|
const adultMembers = members.filter(member => !member.child)
|
|
61
62
|
const childMembers = members.filter(member => member.child)
|
|
62
63
|
|
|
@@ -67,18 +68,3 @@ export function useGroupMembersForNewConversation({
|
|
|
67
68
|
childMembers,
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
function buildBadges(
|
|
72
|
-
membership: GroupsGroupMemberResource,
|
|
73
|
-
currentPersonId: number
|
|
74
|
-
): MemberBadge[] {
|
|
75
|
-
const { person } = membership
|
|
76
|
-
const badges = []
|
|
77
|
-
if (membership.role === 'leader') {
|
|
78
|
-
badges.push({ title: 'Leader' })
|
|
79
|
-
}
|
|
80
|
-
if (person.id === currentPersonId) {
|
|
81
|
-
badges.push({ title: 'Conversation owner' })
|
|
82
|
-
}
|
|
83
|
-
return badges
|
|
84
|
-
}
|
|
@@ -26,14 +26,7 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
|
|
|
26
26
|
const apiClient = useApiClient()
|
|
27
27
|
|
|
28
28
|
const teamAndPlanParams: TeamAndPlanParams = useMemo(
|
|
29
|
-
() =>
|
|
30
|
-
omitBy(
|
|
31
|
-
{
|
|
32
|
-
team_id: teamIds?.join(',') || null,
|
|
33
|
-
plan_id: planId,
|
|
34
|
-
},
|
|
35
|
-
isNil
|
|
36
|
-
),
|
|
29
|
+
() => buildTeamAndPlanParams(teamIds, planId),
|
|
37
30
|
[teamIds, planId]
|
|
38
31
|
)
|
|
39
32
|
|
|
@@ -49,16 +42,23 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
|
|
|
49
42
|
onSuccess: ({ conversation, created }) => {
|
|
50
43
|
onSuccess?.(conversation, { created })
|
|
51
44
|
},
|
|
52
|
-
mutationFn: async () =>
|
|
45
|
+
mutationFn: async () =>
|
|
46
|
+
findOrCreateServicesConversation({ apiClient, teamIds: teamIds ?? [], planId }),
|
|
53
47
|
})
|
|
54
48
|
|
|
55
49
|
return { ...mutation, selectionHasConversation, isLoadingConversationCheck }
|
|
56
50
|
}
|
|
57
51
|
|
|
58
|
-
export const findOrCreateServicesConversation = async (
|
|
59
|
-
apiClient
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
export const findOrCreateServicesConversation = async ({
|
|
53
|
+
apiClient,
|
|
54
|
+
teamIds,
|
|
55
|
+
planId,
|
|
56
|
+
}: {
|
|
57
|
+
apiClient: ApiClient
|
|
58
|
+
teamIds: number[]
|
|
59
|
+
planId?: number
|
|
60
|
+
}): Promise<{ conversation: ConversationResource; created: boolean }> => {
|
|
61
|
+
const teamAndPlanParams = buildTeamAndPlanParams(teamIds, planId)
|
|
62
62
|
const foundConversations = await getGroupIdsFromServices(apiClient, teamAndPlanParams)
|
|
63
63
|
.then(res => res.data.groupIdentifiers)
|
|
64
64
|
.then(groupIdentifiers => findConversationWithExactTeams(apiClient, groupIdentifiers))
|
|
@@ -69,24 +69,27 @@ export const findOrCreateServicesConversation = async (
|
|
|
69
69
|
return { conversation: foundConversation, created: false }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
return
|
|
72
|
+
return getServicesChatConversationPayload({ apiClient, teamIds, planId })
|
|
73
73
|
.then(res => res.data.payload)
|
|
74
74
|
.then(payload => createConversation(apiClient, payload))
|
|
75
75
|
.then(res => ({ conversation: res.data, created: true }))
|
|
76
76
|
.catch(throwResponseError)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export const
|
|
80
|
-
apiClient
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
export const getServicesChatConversationPayload = ({
|
|
80
|
+
apiClient,
|
|
81
|
+
teamIds,
|
|
82
|
+
planId,
|
|
83
|
+
}: {
|
|
84
|
+
apiClient: ApiClient
|
|
85
|
+
teamIds: number[]
|
|
86
|
+
planId?: number
|
|
87
|
+
}) => {
|
|
83
88
|
return apiClient.services.get({
|
|
84
|
-
url:
|
|
89
|
+
url: '/chat',
|
|
85
90
|
data: {
|
|
86
|
-
...
|
|
87
|
-
fields: {
|
|
88
|
-
Chat: ['payload'],
|
|
89
|
-
},
|
|
91
|
+
...buildTeamAndPlanParams(teamIds, planId),
|
|
92
|
+
fields: { Chat: ['payload'] },
|
|
90
93
|
},
|
|
91
94
|
}) as Promise<ApiResource<ServicesChatPayloadResource>>
|
|
92
95
|
}
|
|
@@ -157,7 +160,7 @@ export const checkIfConversationWithGroupExists = (
|
|
|
157
160
|
export const buildTeamAndPlanParams = (teamIds: number[] = [], planId?: number) => {
|
|
158
161
|
return omitBy(
|
|
159
162
|
{
|
|
160
|
-
team_id: teamIds.join(','),
|
|
163
|
+
team_id: teamIds.length ? teamIds.join(',') : null,
|
|
161
164
|
plan_id: planId,
|
|
162
165
|
},
|
|
163
166
|
isNil
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useApiClient } from '../use_api_client'
|
|
3
|
+
import { getServicesChatConversationPayload } from './use_find_or_create_services_conversation'
|
|
4
|
+
|
|
5
|
+
const STALE_TIME_MS = 55 * 60 * 1000 // under Services' 1-hour server-side expiry
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
teamIds: number[]
|
|
9
|
+
planId?: number
|
|
10
|
+
enabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useServicesChatConversationPayload({ teamIds, planId, enabled = true }: Props) {
|
|
14
|
+
const apiClient = useApiClient()
|
|
15
|
+
const sortedTeamIds = [...teamIds].sort((a, b) => a - b)
|
|
16
|
+
|
|
17
|
+
const { data, ...rest } = useQuery({
|
|
18
|
+
queryKey: ['services', '/chat', { teamIds: sortedTeamIds, planId }],
|
|
19
|
+
queryFn: () =>
|
|
20
|
+
getServicesChatConversationPayload({ apiClient, teamIds: sortedTeamIds, planId }),
|
|
21
|
+
staleTime: STALE_TIME_MS,
|
|
22
|
+
enabled: enabled && sortedTeamIds.length > 0,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return { payload: data?.data.payload, ...rest }
|
|
26
|
+
}
|
|
@@ -2,6 +2,7 @@ import { isNil, omitBy } from 'lodash'
|
|
|
2
2
|
import { useMemo } from 'react'
|
|
3
3
|
import { MemberResource, TeamPeopleResource, TeamPersonResponseItem } from '../../types'
|
|
4
4
|
import { useApiGet } from '../use_api'
|
|
5
|
+
import { useEnrichPeople } from '../use_enrich_people'
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
teamIds: number[]
|
|
@@ -23,14 +24,24 @@ export function useTeamMembersForNewConversation({ teamIds, planId }: Props) {
|
|
|
23
24
|
})
|
|
24
25
|
|
|
25
26
|
const people = data?.people || stableEmptyPersonArray
|
|
27
|
+
const personIds = useMemo(() => people.map(p => p.id), [people])
|
|
28
|
+
const enrichmentMap = useEnrichPeople({ personIds })
|
|
26
29
|
|
|
27
|
-
const members: MemberResource[] = useMemo(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const members: MemberResource[] = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
people.map(person => {
|
|
33
|
+
const enrichedBadges = enrichmentMap.get(person.id)?.badges ?? []
|
|
34
|
+
const existingTitles = new Set(person.badges.map(b => b.title))
|
|
35
|
+
const newBadges = enrichedBadges.filter(b => !existingTitles.has(b.title))
|
|
36
|
+
return {
|
|
37
|
+
...person,
|
|
38
|
+
type: 'Member',
|
|
39
|
+
gender: null,
|
|
40
|
+
badges: [...person.badges, ...newBadges],
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
[people, enrichmentMap]
|
|
44
|
+
)
|
|
34
45
|
|
|
35
46
|
return {
|
|
36
47
|
members,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiResource, ConversationValidateResource } from '../types'
|
|
3
|
+
import { useApiClient } from './use_api_client'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
payload: string | undefined
|
|
7
|
+
isLoadingPayload?: boolean
|
|
8
|
+
genderId?: string | null
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useConversationValidate({
|
|
13
|
+
payload,
|
|
14
|
+
isLoadingPayload = false,
|
|
15
|
+
genderId,
|
|
16
|
+
enabled = true,
|
|
17
|
+
}: Props) {
|
|
18
|
+
const apiClient = useApiClient()
|
|
19
|
+
|
|
20
|
+
const { data, isLoading } = useQuery({
|
|
21
|
+
queryKey: ['chat', '/me/conversation_validate', { payload, genderId }],
|
|
22
|
+
queryFn: () =>
|
|
23
|
+
apiClient.chat.post<ApiResource<ConversationValidateResource>>({
|
|
24
|
+
url: '/me/conversation_validate',
|
|
25
|
+
data: {
|
|
26
|
+
data: {
|
|
27
|
+
type: 'ConversationValidate',
|
|
28
|
+
attributes: {
|
|
29
|
+
payload: payload!,
|
|
30
|
+
...(genderId ? { gender_id: genderId } : {}),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
enabled: enabled && payload != null,
|
|
36
|
+
retry: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const warnings = data?.data.warnings ?? []
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
warnings,
|
|
43
|
+
validationPending: enabled && (isLoadingPayload || isLoading),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { ApiCollection, ResourceObject } from '../types'
|
|
4
|
+
import { useApiClient } from './use_api_client'
|
|
5
|
+
|
|
6
|
+
export interface PersonEnrichmentResource extends ResourceObject {
|
|
7
|
+
type: 'PersonEnrichment'
|
|
8
|
+
id: string
|
|
9
|
+
badges: { title: string }[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useEnrichPeople({ personIds, groupId }: { personIds: number[]; groupId?: number }) {
|
|
13
|
+
const apiClient = useApiClient()
|
|
14
|
+
|
|
15
|
+
const { data } = useQuery<ApiCollection<PersonEnrichmentResource>>({
|
|
16
|
+
queryKey: ['enrich_people', [...personIds].sort((a, b) => a - b), groupId],
|
|
17
|
+
queryFn: () =>
|
|
18
|
+
apiClient.chat.post<ApiCollection<PersonEnrichmentResource>>({
|
|
19
|
+
url: '/enrich_people',
|
|
20
|
+
data: {
|
|
21
|
+
data: {
|
|
22
|
+
type: 'PersonEnrichment',
|
|
23
|
+
attributes: {
|
|
24
|
+
person_ids: personIds,
|
|
25
|
+
...(groupId !== undefined ? { group_id: groupId } : {}),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
enabled: personIds.length > 0,
|
|
31
|
+
placeholderData: keepPreviousData,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return useMemo(() => new Map(data?.data.map(e => [+e.id, e]) ?? []), [data])
|
|
35
|
+
}
|