@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.
Files changed (49) hide show
  1. package/build/components/conversations/conversation_actions.js +3 -3
  2. package/build/components/conversations/conversation_actions.js.map +1 -1
  3. package/build/components/conversations/conversations.d.ts.map +1 -1
  4. package/build/components/conversations/conversations.js +1 -0
  5. package/build/components/conversations/conversations.js.map +1 -1
  6. package/build/hooks/use_conversation.d.ts.map +1 -1
  7. package/build/hooks/use_conversation.js +6 -1
  8. package/build/hooks/use_conversation.js.map +1 -1
  9. package/build/hooks/use_conversation_membership.d.ts +5 -0
  10. package/build/hooks/use_conversation_membership.d.ts.map +1 -0
  11. package/build/hooks/use_conversation_membership.js +63 -0
  12. package/build/hooks/use_conversation_membership.js.map +1 -0
  13. package/build/navigation/index.d.ts.map +1 -1
  14. package/build/navigation/index.js +2 -2
  15. package/build/navigation/index.js.map +1 -1
  16. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  17. package/build/screens/conversation_details_screen.js +18 -10
  18. package/build/screens/conversation_details_screen.js.map +1 -1
  19. package/build/screens/conversation_screen.d.ts +3 -1
  20. package/build/screens/conversation_screen.d.ts.map +1 -1
  21. package/build/screens/conversation_screen.js +22 -5
  22. package/build/screens/conversation_screen.js.map +1 -1
  23. package/build/types/resources/conversation.d.ts +2 -3
  24. package/build/types/resources/conversation.d.ts.map +1 -1
  25. package/build/types/resources/conversation.js.map +1 -1
  26. package/build/types/resources/conversation_membership.d.ts +14 -0
  27. package/build/types/resources/conversation_membership.d.ts.map +1 -0
  28. package/build/types/resources/conversation_membership.js +2 -0
  29. package/build/types/resources/conversation_membership.js.map +1 -0
  30. package/build/types/resources/index.d.ts +1 -0
  31. package/build/types/resources/index.d.ts.map +1 -1
  32. package/build/types/resources/index.js +1 -0
  33. package/build/types/resources/index.js.map +1 -1
  34. package/build/utils/deep_snake_case_keys.d.ts +4 -0
  35. package/build/utils/deep_snake_case_keys.d.ts.map +1 -0
  36. package/build/utils/deep_snake_case_keys.js +13 -0
  37. package/build/utils/deep_snake_case_keys.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/components/conversations/conversation_actions.tsx +3 -3
  40. package/src/components/conversations/conversations.tsx +1 -0
  41. package/src/hooks/use_conversation.ts +6 -1
  42. package/src/hooks/use_conversation_membership.ts +91 -0
  43. package/src/navigation/index.tsx +3 -1
  44. package/src/screens/conversation_details_screen.tsx +34 -16
  45. package/src/screens/conversation_screen.tsx +24 -4
  46. package/src/types/resources/conversation.ts +2 -3
  47. package/src/types/resources/conversation_membership.ts +15 -0
  48. package/src/types/resources/index.ts +1 -0
  49. 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: ['last_read_message_sort_key'],
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
+ }
@@ -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 } = route.params as ConversationRouteProps
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
- useRef,
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
- Pressable,
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 { useFeatures } from '../hooks/use_features'
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 { muted, setMuted } = useConversationMute(route.params)
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: 'Mute',
250
- rightItem: <Switch value={muted} onChange={() => setMuted(!muted)} />,
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
- }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle])
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
- <HeaderTitle maxFontSizeMultiplier={1} style={style}>
420
- {children}
421
- </HeaderTitle>
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
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './analytics_metadata'
2
2
  export * from './conversation'
3
+ export * from './conversation_membership'
3
4
  export * from './member'
4
5
  export * from './message'
5
6
  export * from './oauth_token'
@@ -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
+ }