@planningcenter/chat-react-native 3.24.4 → 3.25.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.
Files changed (86) 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 +13 -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/hooks/use_conversations_actions.d.ts.map +1 -1
  14. package/build/hooks/use_conversations_actions.js +4 -0
  15. package/build/hooks/use_conversations_actions.js.map +1 -1
  16. package/build/hooks/use_features.d.ts +1 -0
  17. package/build/hooks/use_features.d.ts.map +1 -1
  18. package/build/hooks/use_features.js +1 -0
  19. package/build/hooks/use_features.js.map +1 -1
  20. package/build/navigation/index.d.ts +10 -0
  21. package/build/navigation/index.d.ts.map +1 -1
  22. package/build/navigation/index.js +12 -2
  23. package/build/navigation/index.js.map +1 -1
  24. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  25. package/build/screens/conversation_details_screen.js +50 -16
  26. package/build/screens/conversation_details_screen.js.map +1 -1
  27. package/build/screens/conversation_notification_level_select_screen.d.ts +8 -0
  28. package/build/screens/conversation_notification_level_select_screen.d.ts.map +1 -0
  29. package/build/screens/conversation_notification_level_select_screen.js +49 -0
  30. package/build/screens/conversation_notification_level_select_screen.js.map +1 -0
  31. package/build/screens/conversation_screen.d.ts +3 -1
  32. package/build/screens/conversation_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_screen.js +16 -5
  34. package/build/screens/conversation_screen.js.map +1 -1
  35. package/build/screens/group_notification_level_select_screen.d.ts +8 -0
  36. package/build/screens/group_notification_level_select_screen.d.ts.map +1 -0
  37. package/build/screens/group_notification_level_select_screen.js +40 -0
  38. package/build/screens/group_notification_level_select_screen.js.map +1 -0
  39. package/build/screens/group_notification_settings_screen.d.ts.map +1 -1
  40. package/build/screens/group_notification_settings_screen.js +30 -17
  41. package/build/screens/group_notification_settings_screen.js.map +1 -1
  42. package/build/screens/notification_settings/hooks/groups.d.ts +6 -5
  43. package/build/screens/notification_settings/hooks/groups.d.ts.map +1 -1
  44. package/build/screens/notification_settings/hooks/groups.js +63 -36
  45. package/build/screens/notification_settings/hooks/groups.js.map +1 -1
  46. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  47. package/build/screens/notification_settings_screen.js +25 -11
  48. package/build/screens/notification_settings_screen.js.map +1 -1
  49. package/build/types/resources/conversation.d.ts +2 -3
  50. package/build/types/resources/conversation.d.ts.map +1 -1
  51. package/build/types/resources/conversation.js.map +1 -1
  52. package/build/types/resources/conversation_membership.d.ts +16 -0
  53. package/build/types/resources/conversation_membership.d.ts.map +1 -0
  54. package/build/types/resources/conversation_membership.js +2 -0
  55. package/build/types/resources/conversation_membership.js.map +1 -0
  56. package/build/types/resources/group_membership.d.ts +7 -0
  57. package/build/types/resources/group_membership.d.ts.map +1 -1
  58. package/build/types/resources/group_membership.js.map +1 -1
  59. package/build/types/resources/index.d.ts +1 -0
  60. package/build/types/resources/index.d.ts.map +1 -1
  61. package/build/types/resources/index.js +1 -0
  62. package/build/types/resources/index.js.map +1 -1
  63. package/build/utils/deep_snake_case_keys.d.ts +4 -0
  64. package/build/utils/deep_snake_case_keys.d.ts.map +1 -0
  65. package/build/utils/deep_snake_case_keys.js +13 -0
  66. package/build/utils/deep_snake_case_keys.js.map +1 -0
  67. package/package.json +2 -2
  68. package/src/components/conversations/conversation_actions.tsx +3 -3
  69. package/src/components/conversations/conversations.tsx +1 -0
  70. package/src/hooks/use_conversation.ts +13 -1
  71. package/src/hooks/use_conversation_membership.ts +91 -0
  72. package/src/hooks/use_conversations_actions.ts +4 -0
  73. package/src/hooks/use_features.ts +1 -0
  74. package/src/navigation/index.tsx +19 -1
  75. package/src/screens/conversation_details_screen.tsx +87 -22
  76. package/src/screens/conversation_notification_level_select_screen.tsx +73 -0
  77. package/src/screens/conversation_screen.tsx +18 -4
  78. package/src/screens/group_notification_level_select_screen.tsx +62 -0
  79. package/src/screens/group_notification_settings_screen.tsx +53 -18
  80. package/src/screens/notification_settings/hooks/groups.ts +76 -37
  81. package/src/screens/notification_settings_screen.tsx +24 -21
  82. package/src/types/resources/conversation.ts +2 -3
  83. package/src/types/resources/conversation_membership.ts +17 -0
  84. package/src/types/resources/group_membership.ts +7 -0
  85. package/src/types/resources/index.ts +1 -0
  86. package/src/utils/deep_snake_case_keys.ts +16 -0
@@ -1,9 +1,11 @@
1
- import { useMutation, useQueryClient } from '@tanstack/react-query'
1
+ import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import { merge } from 'lodash'
2
3
  import { getRequestQueryKey, useApiClient } from '../../../hooks'
3
4
  import { useSuspenseGet, useSuspensePaginator } from '../../../hooks'
4
- import { ApiResource } from '../../../types'
5
+ import { ApiCollection, ApiResource } from '../../../types'
5
6
  import { GroupMembership, GroupResourceWithMembership } from '../../../types/resources'
6
7
  import { throwResponseError } from '../../../utils/response_error'
8
+ import { updateRecordInPagesData } from '../../../utils'
7
9
 
8
10
  export const getGroupsRequestArgs = () => ({
9
11
  url: '/me/groups',
@@ -13,7 +15,11 @@ export const getGroupsRequestArgs = () => ({
13
15
  filter: 'user_settable_notification_level',
14
16
  fields: {
15
17
  Group: [],
16
- GroupMembership: ['notification_level', 'notification_level_description'],
18
+ GroupMembership: [
19
+ 'notification_level',
20
+ 'notification_level_description',
21
+ 'notification_level_options',
22
+ ],
17
23
  },
18
24
  order: 'name',
19
25
  },
@@ -25,7 +31,11 @@ export const getGroupRequestArgs = ({ groupId }: { groupId: number | string }) =
25
31
  include: ['my_group_membership'],
26
32
  fields: {
27
33
  Group: ['name', 'source_app_name', 'source_type', 'my_group_membership'],
28
- GroupMembership: ['notification_level', 'notification_level_description'],
34
+ GroupMembership: [
35
+ 'notification_level',
36
+ 'notification_level_description',
37
+ 'notification_level_options',
38
+ ],
29
39
  },
30
40
  },
31
41
  })
@@ -39,50 +49,77 @@ export const useGroups = () => {
39
49
  export const useGroup = ({ groupId }: { groupId: number | string }) => {
40
50
  const args = getGroupRequestArgs({ groupId })
41
51
 
42
- return useSuspenseGet<GroupResourceWithMembership>(args)
52
+ return useSuspenseGet<GroupResourceWithMembership>(args, { refetchOnMount: false })
43
53
  }
44
54
 
55
+ type GroupsQueryData = InfiniteData<ApiCollection<GroupResourceWithMembership>>
56
+
45
57
  export const useGroupMembershipUpdate = ({ groupId }: { groupId: number | string }) => {
46
58
  const apiClient = useApiClient()
47
59
  const queryClient = useQueryClient()
48
- const requestArgs = getGroupRequestArgs({ groupId })
49
- const queryKey = getRequestQueryKey(requestArgs)
60
+ const groupRequestArgs = getGroupRequestArgs({ groupId })
61
+ const groupsRequestArgs = getGroupsRequestArgs()
62
+ const groupQueryKey = getRequestQueryKey(groupRequestArgs)
50
63
 
51
- return useMutation({
52
- throwOnError: true,
53
- onMutate: notificationLevel => {
54
- queryClient.setQueryData<ApiResource<GroupResourceWithMembership>>(queryKey, groupData => {
55
- if (!groupData?.data.myGroupMembership) return groupData
64
+ const enrichGroupMembership = (
65
+ membership: Partial<GroupMembership>
66
+ ): Partial<GroupMembership> => {
67
+ const cachedGroup =
68
+ queryClient.getQueryData<ApiResource<GroupResourceWithMembership>>(groupQueryKey)
69
+ const currentOptions = cachedGroup?.data.myGroupMembership?.notificationLevelOptions ?? []
70
+ const { notificationLevel } = membership
71
+ const selectedOption = notificationLevel
72
+ ? currentOptions.find(o => o.value === notificationLevel)
73
+ : undefined
74
+ const notificationLevelDescription =
75
+ membership.notificationLevelDescription ?? selectedOption?.description
56
76
 
57
- return {
58
- ...groupData,
59
- data: {
60
- ...groupData.data,
61
- myGroupMembership: {
62
- ...groupData.data.myGroupMembership,
63
- notificationLevel,
64
- },
65
- },
66
- }
77
+ return {
78
+ ...membership,
79
+ notificationLevelDescription,
80
+ ...(notificationLevel && {
81
+ notificationLevelOptions: currentOptions.map(o => ({
82
+ ...o,
83
+ enabled: o.value === notificationLevel,
84
+ })),
85
+ }),
86
+ }
87
+ }
88
+
89
+ const updateGroupsMembershipCache = (membership: Partial<GroupMembership>) => {
90
+ queryClient.setQueryData<GroupsQueryData>(getRequestQueryKey(groupsRequestArgs), prev =>
91
+ updateRecordInPagesData({
92
+ data: prev,
93
+ record: { id: groupId } as GroupResourceWithMembership,
94
+ processRecord: (_record, current) => merge({}, current, { myGroupMembership: membership }),
67
95
  })
96
+ )
97
+ }
98
+
99
+ const updateGroupMembershipCache = (membership: Partial<GroupMembership>) => {
100
+ queryClient.setQueryData<ApiResource<GroupResourceWithMembership>>(groupQueryKey, groupData => {
101
+ if (!groupData?.data.myGroupMembership) return groupData
102
+ return merge({}, groupData, { data: { myGroupMembership: membership } })
103
+ })
104
+ }
105
+
106
+ return useMutation({
107
+ throwOnError: true,
108
+ onMutate: membership => {
109
+ const enriched = enrichGroupMembership(membership)
110
+ updateGroupsMembershipCache(enriched)
111
+ updateGroupMembershipCache(enriched)
68
112
  },
69
113
  onSuccess: response => {
70
- queryClient.setQueryData<ApiResource<GroupResourceWithMembership>>(queryKey, groupData => {
71
- if (!groupData?.data.myGroupMembership) return groupData
114
+ const enriched = enrichGroupMembership(response.data)
115
+ updateGroupsMembershipCache(enriched)
116
+ updateGroupMembershipCache(enriched)
72
117
 
73
- return {
74
- ...groupData,
75
- data: {
76
- ...groupData.data,
77
- myGroupMembership: {
78
- ...groupData.data.myGroupMembership,
79
- ...response.data,
80
- },
81
- },
82
- }
83
- })
118
+ // Updating conversations without access to the current filters
119
+ // is a hard itch to scratch so we just need to refetch.
120
+ queryClient.invalidateQueries({ queryKey: ['/me/conversations'] })
84
121
  },
85
- mutationFn: (notificationLevel: string) => {
122
+ mutationFn: (membership: Partial<GroupMembership>) => {
86
123
  return apiClient.chat
87
124
  .patch<ApiResource<GroupMembership>>({
88
125
  url: `/me/groups/${groupId}/my_group_membership`,
@@ -90,7 +127,9 @@ export const useGroupMembershipUpdate = ({ groupId }: { groupId: number | string
90
127
  data: {
91
128
  type: 'GroupMembership',
92
129
  attributes: {
93
- notification_level: notificationLevel,
130
+ ...(membership.notificationLevel !== undefined && {
131
+ notification_level: membership.notificationLevel,
132
+ }),
94
133
  },
95
134
  },
96
135
  },
@@ -1,27 +1,19 @@
1
1
  import { PlatformPressable } from '@react-navigation/elements'
2
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
- import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
4
3
  import React, { useCallback, useEffect, type ReactNode } from 'react'
5
4
  import { FlatList, Platform, StyleSheet, View, type ViewProps, type ViewStyle } from 'react-native'
6
5
  import { Badge, Heading, Icon, Text } from '../components'
7
6
  import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
8
7
  import { useTheme } from '../hooks'
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
9
9
  import { isDefined } from '../types'
10
- import type { GroupNotificationSettingsScreenProps } from './group_notification_settings_screen'
11
10
  import { useGroups } from './notification_settings/hooks/groups'
12
11
  import { useChatTypes } from './preferred_app/hooks/use_chat_types'
13
- import type { PreferredAppSelectionScreenProps } from './preferred_app_selection_screen'
14
12
 
15
13
  // =========================================
16
14
  // ====== Factory Constants & Types ========
17
15
  // =========================================
18
16
 
19
- type NotificationSettingsStackParamList = {
20
- NotificationSettings: {}
21
- PreferredAppSelection: PreferredAppSelectionScreenProps['route']['params']
22
- GroupNotificationSettings: GroupNotificationSettingsScreenProps['route']['params']
23
- }
24
-
25
17
  enum SectionTypes {
26
18
  header,
27
19
  hidden,
@@ -53,7 +45,7 @@ interface DataItem<T, TName extends SectionTypes> {
53
45
  export type NotificationSettingsScreenProps = StaticScreenProps<{}>
54
46
 
55
47
  export function NotificationSettingsScreen({}: NotificationSettingsScreenProps) {
56
- const navigation = useNavigation<NativeStackNavigationProp<NotificationSettingsStackParamList>>()
48
+ const navigation = useNavigation()
57
49
  const styles = useStyles()
58
50
  const { data: chatTypes } = useChatTypes()
59
51
  const { data: groups } = useGroups()
@@ -109,14 +101,14 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
109
101
  showBottomBorder: true,
110
102
  })),
111
103
  {
112
- type: SectionTypes.header,
104
+ type: groups.length === 0 ? SectionTypes.hidden : SectionTypes.header,
113
105
  data: {
114
106
  title: 'Manage chat settings',
115
107
  },
116
108
  sectionInnerStyle: styles.sectionInnerHeader,
117
109
  },
118
110
  {
119
- type: SectionTypes.view,
111
+ type: groups.length === 0 ? SectionTypes.hidden : SectionTypes.view,
120
112
  data: {
121
113
  children: (
122
114
  <Text variant="tertiary" style={styles.sectionDescription}>
@@ -130,6 +122,7 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
130
122
  type: SectionTypes.link,
131
123
  data: {
132
124
  title: group.name,
125
+ subtitle: group.myGroupMembership?.notificationLevelDescription,
133
126
  rightLabel: group.sourceType,
134
127
  onPress: () =>
135
128
  navigation.navigate('GroupNotificationSettings', {
@@ -206,6 +199,7 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
206
199
  <LinkRow {...item.data} />
207
200
  </ListSection>
208
201
  )
202
+ case SectionTypes.hidden:
209
203
  default:
210
204
  return null
211
205
  }
@@ -268,11 +262,12 @@ function SettingRow({ title, style, rightLabel, rightItem, rightItemStyle = {} }
268
262
 
269
263
  interface LinkRowProps {
270
264
  title: string
265
+ subtitle?: string
271
266
  rightLabel: string
272
267
  onPress: () => void
273
268
  }
274
269
 
275
- function LinkRow({ title, rightLabel, onPress }: LinkRowProps) {
270
+ function LinkRow({ title, subtitle, rightLabel, onPress }: LinkRowProps) {
276
271
  const styles = useLinkRowStyles()
277
272
  const isSourceType = rightLabel === 'Team' || rightLabel === 'Group' || rightLabel === 'PlanTeam'
278
273
 
@@ -285,9 +280,12 @@ function LinkRow({ title, rightLabel, onPress }: LinkRowProps) {
285
280
  accessibilityHint={`Navigate to ${title} settings`}
286
281
  >
287
282
  <View style={styles.innerContainer}>
288
- <Text style={styles.title} numberOfLines={2}>
289
- {title}
290
- </Text>
283
+ <View style={styles.leftContent}>
284
+ <Text style={styles.title} numberOfLines={2}>
285
+ {title}
286
+ </Text>
287
+ {Boolean(subtitle) && <Text variant="footnote">{subtitle}</Text>}
288
+ </View>
291
289
  <View style={styles.rightContent}>
292
290
  {isSourceType ? (
293
291
  <Badge label={rightLabel} appearance="neutral" variant="meta" />
@@ -311,13 +309,16 @@ function LinkRow({ title, rightLabel, onPress }: LinkRowProps) {
311
309
 
312
310
  const useStyles = ({}: { isStart?: boolean; isEnd?: boolean } = {}) => {
313
311
  const { colors } = useTheme()
312
+ const { bottom } = useSafeAreaInsets()
314
313
  const headerBottomPadding = 0
315
314
 
316
315
  return StyleSheet.create({
317
316
  listContainer: {
318
317
  flex: 1,
319
318
  },
320
- contentContainer: {},
319
+ contentContainer: {
320
+ paddingBottom: bottom,
321
+ },
321
322
  sectionOuterBase: {
322
323
  paddingLeft: 16,
323
324
  },
@@ -326,6 +327,7 @@ const useStyles = ({}: { isStart?: boolean; isEnd?: boolean } = {}) => {
326
327
  paddingVertical: 16,
327
328
  },
328
329
  sectionInnerHeader: {
330
+ paddingTop: 24,
329
331
  paddingBottom: headerBottomPadding,
330
332
  },
331
333
  sectionInnerBottomBorder: {
@@ -364,15 +366,16 @@ const useLinkRowStyles = () => {
364
366
  justifyContent: 'space-between',
365
367
  gap: 12,
366
368
  },
369
+ leftContent: {
370
+ flex: 1,
371
+ gap: 4,
372
+ },
367
373
  rightContent: {
368
374
  flexDirection: 'row',
369
375
  alignItems: 'center',
370
376
  gap: 8,
371
377
  },
372
- title: {
373
- flexShrink: 1,
374
- alignSelf: 'center',
375
- },
378
+ title: {},
376
379
  rightLabelText: {
377
380
  color: theme.colors.textColorDefaultSecondary,
378
381
  },
@@ -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,17 @@
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
+ muted: boolean
10
+ notificationLevel: NotificationLevelValue
11
+ notificationLevelDescription: NotificationLevelDescription
12
+ notificationLevelOptions: Array<{
13
+ description: NotificationLevelDescription
14
+ enabled: boolean
15
+ value: NotificationLevelValue
16
+ }>
17
+ }
@@ -1,6 +1,13 @@
1
1
  import { ResourceObject } from '../api_primitives'
2
2
 
3
+ interface GroupNotificationLevelOption {
4
+ value: string
5
+ description: string
6
+ enabled: boolean
7
+ }
8
+
3
9
  export interface GroupMembership extends ResourceObject {
4
10
  notificationLevel: string
5
11
  notificationLevelDescription: string
12
+ notificationLevelOptions: GroupNotificationLevelOption[]
6
13
  }
@@ -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
+ }