@planningcenter/chat-react-native 3.24.4 → 3.25.0-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.
- package/build/components/conversations/conversation_actions.js +3 -3
- package/build/components/conversations/conversation_actions.js.map +1 -1
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +1 -0
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/hooks/use_conversation.d.ts.map +1 -1
- package/build/hooks/use_conversation.js +6 -1
- package/build/hooks/use_conversation.js.map +1 -1
- package/build/hooks/use_conversation_membership.d.ts +5 -0
- package/build/hooks/use_conversation_membership.d.ts.map +1 -0
- package/build/hooks/use_conversation_membership.js +63 -0
- package/build/hooks/use_conversation_membership.js.map +1 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +2 -2
- package/build/navigation/index.js.map +1 -1
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +18 -10
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +3 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +22 -5
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/types/resources/conversation.d.ts +2 -3
- package/build/types/resources/conversation.d.ts.map +1 -1
- package/build/types/resources/conversation.js.map +1 -1
- package/build/types/resources/conversation_membership.d.ts +14 -0
- package/build/types/resources/conversation_membership.d.ts.map +1 -0
- package/build/types/resources/conversation_membership.js +2 -0
- package/build/types/resources/conversation_membership.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/build/utils/deep_snake_case_keys.d.ts +4 -0
- package/build/utils/deep_snake_case_keys.d.ts.map +1 -0
- package/build/utils/deep_snake_case_keys.js +13 -0
- package/build/utils/deep_snake_case_keys.js.map +1 -0
- package/package.json +2 -2
- package/src/components/conversations/conversation_actions.tsx +3 -3
- package/src/components/conversations/conversations.tsx +1 -0
- package/src/hooks/use_conversation.ts +6 -1
- package/src/hooks/use_conversation_membership.ts +91 -0
- package/src/navigation/index.tsx +3 -1
- package/src/screens/conversation_details_screen.tsx +34 -16
- package/src/screens/conversation_screen.tsx +24 -4
- package/src/types/resources/conversation.ts +2 -3
- package/src/types/resources/conversation_membership.ts +15 -0
- package/src/types/resources/index.ts +1 -0
- package/src/utils/deep_snake_case_keys.ts +16 -0
|
@@ -38,7 +38,12 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
|
|
|
38
38
|
'can_reply',
|
|
39
39
|
'can_delete_non_authored_messages',
|
|
40
40
|
],
|
|
41
|
-
ConversationMembership: [
|
|
41
|
+
ConversationMembership: [
|
|
42
|
+
'last_read_message_sort_key',
|
|
43
|
+
'notification_level',
|
|
44
|
+
'notification_level_description',
|
|
45
|
+
'notification_level_options',
|
|
46
|
+
],
|
|
42
47
|
ConversationBadge: ['app_name', 'pco_resource_type', 'text'],
|
|
43
48
|
Group: ['type', 'id', 'links', 'name', 'source_app_name', 'source_type'],
|
|
44
49
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Updater, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { ApiResource, ConversationMembershipResource, ConversationResource } from '../types'
|
|
3
|
+
import { useApiClient } from './use_api_client'
|
|
4
|
+
import { getConversationRequestArgs } from './use_conversation'
|
|
5
|
+
import { getRequestQueryKey } from './use_suspense_api'
|
|
6
|
+
import { deepSnakeCaseKeys } from '../utils/deep_snake_case_keys'
|
|
7
|
+
import { throwResponseError } from '../utils/response_error'
|
|
8
|
+
|
|
9
|
+
export const useConversationMembershipUpdate = ({
|
|
10
|
+
conversation_id,
|
|
11
|
+
}: {
|
|
12
|
+
conversation_id: number
|
|
13
|
+
}) => {
|
|
14
|
+
const apiClient = useApiClient()
|
|
15
|
+
const queryClient = useQueryClient()
|
|
16
|
+
const requestArgs = getConversationRequestArgs({ conversation_id })
|
|
17
|
+
const conversationMembershipFields = requestArgs.data.fields.ConversationMembership.join(',')
|
|
18
|
+
const queryKey = getRequestQueryKey(requestArgs)
|
|
19
|
+
|
|
20
|
+
return useMutation({
|
|
21
|
+
throwOnError: true,
|
|
22
|
+
onMutate: async (attributes: Partial<ConversationMembershipResource>) => {
|
|
23
|
+
queryClient.setQueryData<ApiResource<ConversationResource>>(
|
|
24
|
+
queryKey,
|
|
25
|
+
updateConversationMembershipAttributes(attributes)
|
|
26
|
+
)
|
|
27
|
+
},
|
|
28
|
+
mutationKey: ['updateConversationMembership', conversation_id],
|
|
29
|
+
mutationFn: async (attributes: Partial<ConversationMembershipResource>) => {
|
|
30
|
+
return apiClient.chat
|
|
31
|
+
.patch<ApiResource<ConversationMembershipResource>>({
|
|
32
|
+
url: `/me/conversations/${conversation_id}/conversation_membership`,
|
|
33
|
+
data: {
|
|
34
|
+
data: {
|
|
35
|
+
type: 'ConversationMembership',
|
|
36
|
+
attributes: deepSnakeCaseKeys(attributes),
|
|
37
|
+
},
|
|
38
|
+
fields: {
|
|
39
|
+
ConversationMembership: conversationMembershipFields,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
.catch(throwResponseError)
|
|
44
|
+
},
|
|
45
|
+
onSuccess: response => {
|
|
46
|
+
const membership = response.data
|
|
47
|
+
|
|
48
|
+
queryClient.setQueryData<ApiResource<ConversationResource>>(
|
|
49
|
+
queryKey,
|
|
50
|
+
updateConversationMembershipAttributes(membership)
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const updateConversationMembershipAttributes =
|
|
57
|
+
(
|
|
58
|
+
membership: Partial<ConversationMembershipResource>
|
|
59
|
+
): Updater<
|
|
60
|
+
ApiResource<ConversationResource> | undefined,
|
|
61
|
+
ApiResource<ConversationResource> | undefined
|
|
62
|
+
> =>
|
|
63
|
+
conversationData =>
|
|
64
|
+
mergeConversationMembership(conversationData, membership)
|
|
65
|
+
|
|
66
|
+
const mergeConversationMembership = (
|
|
67
|
+
conversationData: ApiResource<ConversationResource> | undefined,
|
|
68
|
+
membership: Partial<ConversationMembershipResource>
|
|
69
|
+
) => {
|
|
70
|
+
if (!conversationData) return undefined
|
|
71
|
+
if (!conversationData.data) return conversationData
|
|
72
|
+
if (!conversationData.data.conversationMembership) return conversationData
|
|
73
|
+
|
|
74
|
+
const previousMembership: Partial<ConversationMembershipResource> =
|
|
75
|
+
conversationData.data.conversationMembership || {}
|
|
76
|
+
|
|
77
|
+
const mergedMembership: Partial<ConversationMembershipResource> = {
|
|
78
|
+
...previousMembership,
|
|
79
|
+
...membership,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data: ConversationResource = {
|
|
83
|
+
...conversationData.data,
|
|
84
|
+
conversationMembership: mergedMembership,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...conversationData,
|
|
89
|
+
data,
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/navigation/index.tsx
CHANGED
|
@@ -163,13 +163,15 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
163
163
|
screen: ConversationScreen,
|
|
164
164
|
options: ({ route, navigation }) => ({
|
|
165
165
|
headerTitle: (props: NativeStackHeaderRightProps) => {
|
|
166
|
-
const { conversation_id, title, badge, deleted } =
|
|
166
|
+
const { conversation_id, title, badge, deleted, muted } =
|
|
167
|
+
route.params as ConversationRouteProps
|
|
167
168
|
|
|
168
169
|
return (
|
|
169
170
|
<ConversationScreenTitle
|
|
170
171
|
conversation_id={conversation_id}
|
|
171
172
|
badge={badge}
|
|
172
173
|
deleted={deleted}
|
|
174
|
+
muted={muted}
|
|
173
175
|
{...props}
|
|
174
176
|
>
|
|
175
177
|
{title ?? 'Conversation'}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
+
import { HeaderTitle as ElementsHeaderTitle, HeaderTitleProps } from '@react-navigation/elements'
|
|
1
2
|
import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
2
3
|
import React, {
|
|
3
4
|
useCallback,
|
|
4
5
|
useEffect,
|
|
6
|
+
useRef,
|
|
5
7
|
useState,
|
|
6
|
-
type SetStateAction,
|
|
7
8
|
type Dispatch,
|
|
8
9
|
type ReactNode,
|
|
9
|
-
|
|
10
|
+
type SetStateAction,
|
|
10
11
|
} from 'react'
|
|
11
12
|
import {
|
|
13
|
+
Alert,
|
|
12
14
|
FlatList,
|
|
15
|
+
Platform,
|
|
16
|
+
Pressable,
|
|
13
17
|
StyleSheet,
|
|
14
18
|
TextInput,
|
|
15
19
|
View,
|
|
16
|
-
type ViewStyle,
|
|
17
20
|
type ViewProps,
|
|
18
|
-
|
|
19
|
-
Alert,
|
|
20
|
-
Platform,
|
|
21
|
+
type ViewStyle,
|
|
21
22
|
} from 'react-native'
|
|
22
|
-
import { HeaderTitle as ElementsHeaderTitle, HeaderTitleProps } from '@react-navigation/elements'
|
|
23
23
|
import {
|
|
24
24
|
Badge,
|
|
25
25
|
ChildNotice,
|
|
@@ -31,21 +31,20 @@ import {
|
|
|
31
31
|
TextButton,
|
|
32
32
|
type TextStyle,
|
|
33
33
|
} from '../components'
|
|
34
|
+
import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
|
|
35
|
+
import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
|
|
34
36
|
import { useSuspensePaginator, useTheme } from '../hooks'
|
|
35
37
|
import {
|
|
36
38
|
useConversation,
|
|
37
39
|
useConversationDelete,
|
|
38
40
|
useConversationDisableReplies,
|
|
39
|
-
useConversationMute,
|
|
40
41
|
useConversationUpdate,
|
|
41
42
|
} from '../hooks/use_conversation'
|
|
43
|
+
import { useConversationMembershipUpdate } from '../hooks/use_conversation_membership'
|
|
44
|
+
import { availableFeatures, useFeatures } from '../hooks/use_features'
|
|
42
45
|
import { MemberResource, isDefined } from '../types'
|
|
43
|
-
import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
|
|
44
|
-
import { tokens } from '../vendor/tapestry/tokens'
|
|
45
|
-
import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
|
|
46
46
|
import { GroupResource } from '../types/resources/group_resource'
|
|
47
|
-
import {
|
|
48
|
-
import { availableFeatures } from '../hooks/use_features'
|
|
47
|
+
import { tokens } from '../vendor/tapestry/tokens'
|
|
49
48
|
|
|
50
49
|
// =========================================
|
|
51
50
|
// ====== Factory Constants & Types ========
|
|
@@ -91,7 +90,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
91
90
|
|
|
92
91
|
const { data: conversation } = useConversation(route.params)
|
|
93
92
|
const [title, setTitle] = useState(conversation.title)
|
|
94
|
-
const {
|
|
93
|
+
const { mutate: updateMembership } = useConversationMembershipUpdate(route.params)
|
|
95
94
|
const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params)
|
|
96
95
|
const { mutate: saveTitle } = useConversationUpdate(route.params)
|
|
97
96
|
const { mutate: deleteConversation } = useConversationDelete(route.params)
|
|
@@ -101,6 +100,10 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
101
100
|
const trimmedTitle = title.trim()
|
|
102
101
|
const emptyTitle = trimmedTitle === '' || title === null
|
|
103
102
|
|
|
103
|
+
const notificationsEnabled =
|
|
104
|
+
conversation.conversationMembership?.notificationLevel === 'everything'
|
|
105
|
+
const notificationLevelDescription =
|
|
106
|
+
conversation?.conversationMembership?.notificationLevelDescription
|
|
104
107
|
const canUpdate = conversation.memberAbility?.canUpdate || false
|
|
105
108
|
const canDelete = conversation.memberAbility?.canDelete || false
|
|
106
109
|
const isLeader = conversation.memberAbility?.leader || false
|
|
@@ -163,6 +166,15 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
163
166
|
const productName = badge?.appName
|
|
164
167
|
const name = badge?.text || undefined
|
|
165
168
|
|
|
169
|
+
const handleToggleNotificationPreferences = useCallback(
|
|
170
|
+
(value: boolean) => {
|
|
171
|
+
updateMembership({
|
|
172
|
+
notificationLevel: value ? 'everything' : 'nothing',
|
|
173
|
+
})
|
|
174
|
+
},
|
|
175
|
+
[updateMembership]
|
|
176
|
+
)
|
|
177
|
+
|
|
166
178
|
const handleDelete = useCallback(() => {
|
|
167
179
|
Alert.alert(
|
|
168
180
|
'Delete conversation',
|
|
@@ -246,8 +258,14 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
|
|
|
246
258
|
{
|
|
247
259
|
type: SectionTypes.setting,
|
|
248
260
|
data: {
|
|
249
|
-
title: '
|
|
250
|
-
|
|
261
|
+
title: 'Enable notifications',
|
|
262
|
+
subtitle: notificationLevelDescription,
|
|
263
|
+
rightItem: (
|
|
264
|
+
<Switch
|
|
265
|
+
value={notificationsEnabled}
|
|
266
|
+
onValueChange={handleToggleNotificationPreferences}
|
|
267
|
+
/>
|
|
268
|
+
),
|
|
251
269
|
},
|
|
252
270
|
showBottomBorder: true,
|
|
253
271
|
},
|
|
@@ -53,6 +53,7 @@ export type ConversationRouteProps = {
|
|
|
53
53
|
subtitle?: string
|
|
54
54
|
badge?: ConversationBadgeResource
|
|
55
55
|
deleted?: boolean
|
|
56
|
+
muted?: boolean
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
@@ -134,9 +135,18 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
134
135
|
title: title,
|
|
135
136
|
badge: badges?.[0],
|
|
136
137
|
deleted: conversation?.deleted,
|
|
138
|
+
muted: conversation?.muted,
|
|
137
139
|
})
|
|
138
140
|
}
|
|
139
|
-
}, [
|
|
141
|
+
}, [
|
|
142
|
+
navigation,
|
|
143
|
+
title,
|
|
144
|
+
badges,
|
|
145
|
+
conversation?.deleted,
|
|
146
|
+
conversation?.muted,
|
|
147
|
+
reply_root_id,
|
|
148
|
+
replyHeaderTitle,
|
|
149
|
+
])
|
|
140
150
|
|
|
141
151
|
if (!conversation || conversation.deleted) {
|
|
142
152
|
return (
|
|
@@ -390,6 +400,7 @@ interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
|
390
400
|
conversation_id: number
|
|
391
401
|
badge?: ConversationBadgeResource
|
|
392
402
|
deleted?: boolean
|
|
403
|
+
muted?: boolean
|
|
393
404
|
}
|
|
394
405
|
|
|
395
406
|
export const ConversationScreenTitle = ({
|
|
@@ -398,6 +409,7 @@ export const ConversationScreenTitle = ({
|
|
|
398
409
|
children,
|
|
399
410
|
style,
|
|
400
411
|
deleted,
|
|
412
|
+
muted,
|
|
401
413
|
}: ConversationScreenTitleProps) => {
|
|
402
414
|
const styles = usePressableHeaderStyle()
|
|
403
415
|
const navigation = useNavigation()
|
|
@@ -416,9 +428,12 @@ export const ConversationScreenTitle = ({
|
|
|
416
428
|
}}
|
|
417
429
|
>
|
|
418
430
|
<View style={styles.titleWrapper}>
|
|
419
|
-
<
|
|
420
|
-
{
|
|
421
|
-
|
|
431
|
+
<View style={styles.titleTextContainer}>
|
|
432
|
+
<HeaderTitle maxFontSizeMultiplier={1} style={style}>
|
|
433
|
+
{children}
|
|
434
|
+
</HeaderTitle>
|
|
435
|
+
</View>
|
|
436
|
+
{muted && <Icon name="general.bellMuted" size={12} />}
|
|
422
437
|
{!deleted && <Icon name="general.downChevron" size={12} />}
|
|
423
438
|
</View>
|
|
424
439
|
<Badge
|
|
@@ -438,6 +453,7 @@ const usePressableHeaderStyle = () => {
|
|
|
438
453
|
container: {
|
|
439
454
|
alignItems: Platform.select({ android: 'flex-start', default: 'center' }),
|
|
440
455
|
marginRight: Platform.select({ ios: 20, default: 16 }),
|
|
456
|
+
flex: 1,
|
|
441
457
|
},
|
|
442
458
|
titleWrapper: {
|
|
443
459
|
alignItems: 'center',
|
|
@@ -445,6 +461,10 @@ const usePressableHeaderStyle = () => {
|
|
|
445
461
|
flexDirection: 'row',
|
|
446
462
|
flexShrink: 1,
|
|
447
463
|
},
|
|
464
|
+
titleTextContainer: {
|
|
465
|
+
flexShrink: 1,
|
|
466
|
+
minWidth: 0,
|
|
467
|
+
},
|
|
448
468
|
badge: {
|
|
449
469
|
alignSelf: Platform.select({ android: 'flex-start', default: 'center' }),
|
|
450
470
|
marginTop: 2,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnalyticsMetadataResource } from './analytics_metadata'
|
|
2
2
|
import { ConversationBadgeResource } from './conversation_badge'
|
|
3
|
+
import { ConversationMembershipResource } from './conversation_membership'
|
|
3
4
|
import { GroupResource } from './group_resource'
|
|
4
5
|
import { MemberAbilityResource } from './member_ability'
|
|
5
6
|
|
|
@@ -8,9 +9,7 @@ export interface ConversationResource {
|
|
|
8
9
|
id: number
|
|
9
10
|
badges?: ConversationBadgeResource[]
|
|
10
11
|
analyticsMetadata?: AnalyticsMetadataResource
|
|
11
|
-
conversationMembership?:
|
|
12
|
-
lastReadMessageSortKey: string
|
|
13
|
-
}
|
|
12
|
+
conversationMembership?: Partial<ConversationMembershipResource>
|
|
14
13
|
createdAt: string
|
|
15
14
|
deleted?: boolean
|
|
16
15
|
groups?: GroupResource[]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ResourceObject } from '../api_primitives'
|
|
2
|
+
|
|
3
|
+
export type NotificationLevelValue = 'everything' | 'nothing'
|
|
4
|
+
export type NotificationLevelDescription = string
|
|
5
|
+
|
|
6
|
+
export interface ConversationMembershipResource extends ResourceObject {
|
|
7
|
+
type: 'ConversationMembership'
|
|
8
|
+
lastReadMessageSortKey: string
|
|
9
|
+
notificationLevel: NotificationLevelValue
|
|
10
|
+
notificationLevelDescription: NotificationLevelDescription
|
|
11
|
+
notificationLevelOptions: Array<{
|
|
12
|
+
description: NotificationLevelDescription
|
|
13
|
+
value: NotificationLevelValue
|
|
14
|
+
}>
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isArray, isObject, mapKeys, mapValues, snakeCase } from 'lodash'
|
|
2
|
+
|
|
3
|
+
type ObjType = Record<string, unknown> | unknown[] | unknown
|
|
4
|
+
|
|
5
|
+
export function deepSnakeCaseKeys<T extends ObjType>(obj: T): T {
|
|
6
|
+
if (isArray(obj)) {
|
|
7
|
+
return obj.map(deepSnakeCaseKeys) as T
|
|
8
|
+
} else if (isObject(obj)) {
|
|
9
|
+
return mapValues(
|
|
10
|
+
// @ts-ignore This mutates the object, but we don't care about the type here
|
|
11
|
+
mapKeys(obj, (_value: string, key: string) => snakeCase(key)),
|
|
12
|
+
deepSnakeCaseKeys
|
|
13
|
+
) as T
|
|
14
|
+
}
|
|
15
|
+
return obj
|
|
16
|
+
}
|