@planningcenter/chat-react-native 3.24.4 → 3.24.5-qa-563.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 (101) hide show
  1. package/build/components/conversation/message.d.ts +1 -2
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +5 -5
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +1 -4
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  9. package/build/components/conversation/messages_disabled_banners.js +2 -14
  10. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  11. package/build/components/conversations/conversation_actions.js +3 -3
  12. package/build/components/conversations/conversation_actions.js.map +1 -1
  13. package/build/components/conversations/conversations.d.ts.map +1 -1
  14. package/build/components/conversations/conversations.js +1 -0
  15. package/build/components/conversations/conversations.js.map +1 -1
  16. package/build/hooks/use_conversation.d.ts.map +1 -1
  17. package/build/hooks/use_conversation.js +13 -1
  18. package/build/hooks/use_conversation.js.map +1 -1
  19. package/build/hooks/use_conversation_membership.d.ts +5 -0
  20. package/build/hooks/use_conversation_membership.d.ts.map +1 -0
  21. package/build/hooks/use_conversation_membership.js +63 -0
  22. package/build/hooks/use_conversation_membership.js.map +1 -0
  23. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  24. package/build/hooks/use_conversations_actions.js +4 -0
  25. package/build/hooks/use_conversations_actions.js.map +1 -1
  26. package/build/hooks/use_features.d.ts +1 -1
  27. package/build/hooks/use_features.js +1 -1
  28. package/build/hooks/use_features.js.map +1 -1
  29. package/build/navigation/index.d.ts +10 -0
  30. package/build/navigation/index.d.ts.map +1 -1
  31. package/build/navigation/index.js +12 -2
  32. package/build/navigation/index.js.map +1 -1
  33. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  34. package/build/screens/conversation_details_screen.js +51 -19
  35. package/build/screens/conversation_details_screen.js.map +1 -1
  36. package/build/screens/conversation_notification_level_select_screen.d.ts +8 -0
  37. package/build/screens/conversation_notification_level_select_screen.d.ts.map +1 -0
  38. package/build/screens/conversation_notification_level_select_screen.js +49 -0
  39. package/build/screens/conversation_notification_level_select_screen.js.map +1 -0
  40. package/build/screens/conversation_screen.d.ts +4 -3
  41. package/build/screens/conversation_screen.d.ts.map +1 -1
  42. package/build/screens/conversation_screen.js +20 -14
  43. package/build/screens/conversation_screen.js.map +1 -1
  44. package/build/screens/group_notification_level_select_screen.d.ts +8 -0
  45. package/build/screens/group_notification_level_select_screen.d.ts.map +1 -0
  46. package/build/screens/group_notification_level_select_screen.js +40 -0
  47. package/build/screens/group_notification_level_select_screen.js.map +1 -0
  48. package/build/screens/group_notification_settings_screen.d.ts.map +1 -1
  49. package/build/screens/group_notification_settings_screen.js +30 -17
  50. package/build/screens/group_notification_settings_screen.js.map +1 -1
  51. package/build/screens/message_actions_screen.js +1 -2
  52. package/build/screens/message_actions_screen.js.map +1 -1
  53. package/build/screens/notification_settings/hooks/groups.d.ts +6 -5
  54. package/build/screens/notification_settings/hooks/groups.d.ts.map +1 -1
  55. package/build/screens/notification_settings/hooks/groups.js +63 -36
  56. package/build/screens/notification_settings/hooks/groups.js.map +1 -1
  57. package/build/screens/notification_settings_screen.d.ts.map +1 -1
  58. package/build/screens/notification_settings_screen.js +25 -11
  59. package/build/screens/notification_settings_screen.js.map +1 -1
  60. package/build/types/resources/conversation.d.ts +2 -3
  61. package/build/types/resources/conversation.d.ts.map +1 -1
  62. package/build/types/resources/conversation.js.map +1 -1
  63. package/build/types/resources/conversation_membership.d.ts +16 -0
  64. package/build/types/resources/conversation_membership.d.ts.map +1 -0
  65. package/build/types/resources/conversation_membership.js +2 -0
  66. package/build/types/resources/conversation_membership.js.map +1 -0
  67. package/build/types/resources/group_membership.d.ts +7 -0
  68. package/build/types/resources/group_membership.d.ts.map +1 -1
  69. package/build/types/resources/group_membership.js.map +1 -1
  70. package/build/types/resources/index.d.ts +1 -0
  71. package/build/types/resources/index.d.ts.map +1 -1
  72. package/build/types/resources/index.js +1 -0
  73. package/build/types/resources/index.js.map +1 -1
  74. package/build/utils/deep_snake_case_keys.d.ts +4 -0
  75. package/build/utils/deep_snake_case_keys.d.ts.map +1 -0
  76. package/build/utils/deep_snake_case_keys.js +13 -0
  77. package/build/utils/deep_snake_case_keys.js.map +1 -0
  78. package/package.json +2 -2
  79. package/src/components/conversation/message.tsx +4 -9
  80. package/src/components/conversation/message_form.tsx +1 -4
  81. package/src/components/conversation/messages_disabled_banners.tsx +9 -17
  82. package/src/components/conversations/conversation_actions.tsx +3 -3
  83. package/src/components/conversations/conversations.tsx +1 -0
  84. package/src/hooks/use_conversation.ts +13 -1
  85. package/src/hooks/use_conversation_membership.ts +91 -0
  86. package/src/hooks/use_conversations_actions.ts +4 -0
  87. package/src/hooks/use_features.ts +1 -1
  88. package/src/navigation/index.tsx +19 -1
  89. package/src/screens/conversation_details_screen.tsx +88 -25
  90. package/src/screens/conversation_notification_level_select_screen.tsx +73 -0
  91. package/src/screens/conversation_screen.tsx +20 -17
  92. package/src/screens/group_notification_level_select_screen.tsx +62 -0
  93. package/src/screens/group_notification_settings_screen.tsx +53 -18
  94. package/src/screens/message_actions_screen.tsx +1 -2
  95. package/src/screens/notification_settings/hooks/groups.ts +76 -37
  96. package/src/screens/notification_settings_screen.tsx +24 -21
  97. package/src/types/resources/conversation.ts +2 -3
  98. package/src/types/resources/conversation_membership.ts +17 -0
  99. package/src/types/resources/group_membership.ts +7 -0
  100. package/src/types/resources/index.ts +1 -0
  101. package/src/utils/deep_snake_case_keys.ts +16 -0
@@ -39,7 +39,6 @@ import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
39
39
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
40
40
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
41
41
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
42
- import { availableFeatures, useFeatures } from '../hooks/use_features'
43
42
  import { ConversationContextProvider } from '../contexts/conversation_context'
44
43
 
45
44
  export type ConversationRouteProps = {
@@ -53,6 +52,7 @@ export type ConversationRouteProps = {
53
52
  subtitle?: string
54
53
  badge?: ConversationBadgeResource
55
54
  deleted?: boolean
55
+ muted?: boolean
56
56
  }
57
57
 
58
58
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
@@ -91,12 +91,9 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
91
91
  useConversationMessagesJoltEvents({ conversationId: conversation_id })
92
92
  useEnsureConversationsRouteExists()
93
93
  useMarkLatestMessageRead({ conversation, messages })
94
- const { featureEnabled } = useFeatures()
95
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
96
94
  const messagesWithSeparators = groupMessages({
97
95
  ms: messages,
98
96
  inReplyScreen: !!reply_root_id,
99
- repliesEnabled,
100
97
  })
101
98
  const noMessages = messagesWithSeparators.length === 0
102
99
 
@@ -109,6 +106,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
109
106
  const replyHeaderTitle = replyRootAuthorFirstName
110
107
  ? `Reply to ${replyRootAuthorFirstName}`
111
108
  : 'Reply'
109
+ // Prefer the membership for optimistic updates.
110
+ const muted = conversation.conversationMembership?.muted ?? conversation.muted
112
111
 
113
112
  const listRef = useRef<FlatList>(null)
114
113
  const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
@@ -134,9 +133,10 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
134
133
  title: title,
135
134
  badge: badges?.[0],
136
135
  deleted: conversation?.deleted,
136
+ muted,
137
137
  })
138
138
  }
139
- }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle])
139
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])
140
140
 
141
141
  if (!conversation || conversation.deleted) {
142
142
  return (
@@ -195,7 +195,6 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
195
195
  conversation_id={conversation_id}
196
196
  latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
197
197
  inReplyScreen={!!reply_root_id}
198
- repliesEnabled={repliesEnabled}
199
198
  />
200
199
  )
201
200
  }}
@@ -284,14 +283,9 @@ type ReplyShadowMessage = {
284
283
  interface GroupMessagesProps {
285
284
  ms: MessageResource[]
286
285
  inReplyScreen?: boolean
287
- repliesEnabled?: boolean
288
286
  }
289
287
 
290
- export const groupMessages = ({
291
- ms,
292
- inReplyScreen,
293
- repliesEnabled = false,
294
- }: GroupMessagesProps) => {
288
+ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
295
289
  let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
296
290
  let encounteredOneOfMyMessages = false
297
291
 
@@ -335,7 +329,6 @@ export const groupMessages = ({
335
329
  prevMessageDifferentThread ||
336
330
  prevMessageIsDateSeparator)
337
331
  const nextIsReplyShadowMessage =
338
- repliesEnabled &&
339
332
  nextMessageInThread &&
340
333
  !nextMessageThreadRoot &&
341
334
  (nextMessageDifferentThread || nextMessageIsDateSeparator)
@@ -369,7 +362,7 @@ export const groupMessages = ({
369
362
 
370
363
  enrichedMessages.push(message)
371
364
 
372
- if (insertReplyShadowMessage && repliesEnabled) {
365
+ if (insertReplyShadowMessage) {
373
366
  enrichedMessages.push({
374
367
  type: 'ReplyShadowMessage',
375
368
  id: `${message.id}-${message.replyRootId}`,
@@ -390,6 +383,7 @@ interface ConversationScreenTitleProps extends HeaderTitleProps {
390
383
  conversation_id: number
391
384
  badge?: ConversationBadgeResource
392
385
  deleted?: boolean
386
+ muted?: boolean
393
387
  }
394
388
 
395
389
  export const ConversationScreenTitle = ({
@@ -398,6 +392,7 @@ export const ConversationScreenTitle = ({
398
392
  children,
399
393
  style,
400
394
  deleted,
395
+ muted,
401
396
  }: ConversationScreenTitleProps) => {
402
397
  const styles = usePressableHeaderStyle()
403
398
  const navigation = useNavigation()
@@ -416,9 +411,12 @@ export const ConversationScreenTitle = ({
416
411
  }}
417
412
  >
418
413
  <View style={styles.titleWrapper}>
419
- <HeaderTitle maxFontSizeMultiplier={1} style={style}>
420
- {children}
421
- </HeaderTitle>
414
+ <View style={styles.titleTextContainer}>
415
+ <HeaderTitle maxFontSizeMultiplier={1} style={style}>
416
+ {children}
417
+ </HeaderTitle>
418
+ </View>
419
+ {muted && <Icon name="general.bellMuted" size={12} />}
422
420
  {!deleted && <Icon name="general.downChevron" size={12} />}
423
421
  </View>
424
422
  <Badge
@@ -438,6 +436,7 @@ const usePressableHeaderStyle = () => {
438
436
  container: {
439
437
  alignItems: Platform.select({ android: 'flex-start', default: 'center' }),
440
438
  marginRight: Platform.select({ ios: 20, default: 16 }),
439
+ flex: 1,
441
440
  },
442
441
  titleWrapper: {
443
442
  alignItems: 'center',
@@ -445,6 +444,10 @@ const usePressableHeaderStyle = () => {
445
444
  flexDirection: 'row',
446
445
  flexShrink: 1,
447
446
  },
447
+ titleTextContainer: {
448
+ flexShrink: 1,
449
+ minWidth: 0,
450
+ },
448
451
  badge: {
449
452
  alignSelf: Platform.select({ android: 'flex-start', default: 'center' }),
450
453
  marginTop: 2,
@@ -0,0 +1,62 @@
1
+ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
+ import React from 'react'
3
+ import { StyleSheet } from 'react-native'
4
+ import { PressableRow } from '../components'
5
+ import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
6
+ import { useTheme } from '../hooks'
7
+ import { useGroup, useGroupMembershipUpdate } from './notification_settings/hooks/groups'
8
+
9
+ export const GroupNotificationLevelSelectScreenOptions = getFormSheetScreenOptions({
10
+ headerTitle: 'Notification level',
11
+ sheetAllowedDetents: [0.35],
12
+ })
13
+
14
+ export type GroupNotificationLevelSelectScreenProps = StaticScreenProps<{
15
+ groupId: number | string
16
+ }>
17
+
18
+ export function GroupNotificationLevelSelectScreen({
19
+ route,
20
+ }: GroupNotificationLevelSelectScreenProps) {
21
+ const { groupId } = route.params
22
+ const navigation = useNavigation()
23
+ const styles = useStyles()
24
+ const { colors } = useTheme()
25
+ const { data: group } = useGroup({ groupId })
26
+ const { mutate: updateNotificationLevel } = useGroupMembershipUpdate({ groupId })
27
+
28
+ const notificationLevelOptions = group.myGroupMembership?.notificationLevelOptions ?? []
29
+
30
+ const handleSelect = (value: string, isActive: boolean) => {
31
+ if (!isActive) {
32
+ updateNotificationLevel({ notificationLevel: value })
33
+ }
34
+ navigation.goBack()
35
+ }
36
+
37
+ return (
38
+ <FormSheet.Root contentStyle={styles.content}>
39
+ {notificationLevelOptions.map(option => (
40
+ <PressableRow
41
+ key={option.value}
42
+ text={option.description}
43
+ isActive={option.enabled}
44
+ onPress={() => handleSelect(option.value, option.enabled)}
45
+ iconColor={colors.statusSuccessIcon}
46
+ style={styles.row}
47
+ />
48
+ ))}
49
+ </FormSheet.Root>
50
+ )
51
+ }
52
+
53
+ const useStyles = () => {
54
+ return StyleSheet.create({
55
+ content: {
56
+ paddingTop: 20,
57
+ },
58
+ row: {
59
+ borderBottomWidth: 0,
60
+ },
61
+ })
62
+ }
@@ -1,10 +1,17 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
1
2
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
3
+ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
2
4
  import React, { useEffect } from 'react'
3
- import { StyleSheet, View } from 'react-native'
4
- import { Heading, Switch, Text } from '../components'
5
+ import { Platform, StyleSheet, View } from 'react-native'
6
+ import { Heading, Icon, Text } from '../components'
5
7
  import { useTheme } from '../hooks'
6
8
  import { platformFontWeightBold } from '../utils/styles'
7
- import { useGroup, useGroupMembershipUpdate } from './notification_settings/hooks/groups'
9
+ import { useGroup } from './notification_settings/hooks/groups'
10
+ import type { GroupNotificationLevelSelectScreenProps } from './group_notification_level_select_screen'
11
+
12
+ type GroupNotificationSettingsStackParamList = {
13
+ GroupNotificationLevelSelect: GroupNotificationLevelSelectScreenProps['route']['params']
14
+ }
8
15
 
9
16
  export type GroupNotificationSettingsScreenProps = StaticScreenProps<{
10
17
  groupId: number | string
@@ -13,12 +20,12 @@ export type GroupNotificationSettingsScreenProps = StaticScreenProps<{
13
20
 
14
21
  export function GroupNotificationSettingsScreen({ route }: GroupNotificationSettingsScreenProps) {
15
22
  const { groupId, title } = route.params
16
- const navigation = useNavigation()
23
+ const navigation =
24
+ useNavigation<NativeStackNavigationProp<GroupNotificationSettingsStackParamList>>()
17
25
  const styles = useStyles()
18
26
  const { data: group } = useGroup({ groupId })
19
- const { mutate: updateNotificationLevel } = useGroupMembershipUpdate({ groupId })
20
27
 
21
- const notificationsEnabled = group.myGroupMembership?.notificationLevel === 'everything'
28
+ const notificationLevelDescription = group.myGroupMembership?.notificationLevelDescription
22
29
 
23
30
  useEffect(() => {
24
31
  if (!group.name || title === group.name) return
@@ -26,9 +33,8 @@ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSett
26
33
  navigation.setOptions({ title: group.name })
27
34
  }, [group.name, title, navigation])
28
35
 
29
- const handleToggle = (value: boolean) => {
30
- const notificationLevel = value ? 'everything' : 'nothing'
31
- updateNotificationLevel(notificationLevel)
36
+ const handleOpenSelector = () => {
37
+ navigation.navigate('GroupNotificationLevelSelect', { groupId })
32
38
  }
33
39
 
34
40
  return (
@@ -44,10 +50,30 @@ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSett
44
50
  </Text>
45
51
  </View>
46
52
  </View>
47
- <View style={styles.settingRow}>
48
- <Text>Enable notifications</Text>
49
- <Switch value={notificationsEnabled} onValueChange={handleToggle} />
50
- </View>
53
+ <PlatformPressable
54
+ style={styles.settingRowPressable}
55
+ onPress={handleOpenSelector}
56
+ accessibilityRole="button"
57
+ accessibilityLabel="Notify me for"
58
+ accessibilityHint={`Currently set to ${notificationLevelDescription}. Tap to change.`}
59
+ >
60
+ <View style={styles.settingRowInner}>
61
+ <View style={styles.settingRowText}>
62
+ <Text>Notify me for</Text>
63
+ {Boolean(notificationLevelDescription) && (
64
+ <Text variant="tertiary">{notificationLevelDescription}</Text>
65
+ )}
66
+ </View>
67
+ {Platform.OS === 'ios' && (
68
+ <Icon
69
+ name="general.rightChevron"
70
+ size={16}
71
+ style={styles.chevron}
72
+ accessibilityElementsHidden
73
+ />
74
+ )}
75
+ </View>
76
+ </PlatformPressable>
51
77
  </View>
52
78
  )
53
79
  }
@@ -66,7 +92,7 @@ const useStyles = () => {
66
92
  },
67
93
  sectionInner: {
68
94
  paddingRight: 16,
69
- paddingTop: 16,
95
+ paddingTop: 24,
70
96
  paddingBottom: 12,
71
97
  borderBottomWidth: 1,
72
98
  borderBottomColor: colors.borderColorDefaultBase,
@@ -80,13 +106,22 @@ const useStyles = () => {
80
106
  groupNameBold: {
81
107
  fontWeight: platformFontWeightBold,
82
108
  },
83
- settingRow: {
109
+ settingRowPressable: {
110
+ paddingLeft: 16,
111
+ },
112
+ settingRowInner: {
84
113
  flexDirection: 'row',
85
- justifyContent: 'space-between',
86
114
  alignItems: 'center',
87
- paddingHorizontal: 16,
115
+ justifyContent: 'space-between',
116
+ paddingRight: 16,
88
117
  paddingVertical: 12,
89
- backgroundColor: colors.surfaceColor100,
118
+ },
119
+ settingRowText: {
120
+ flex: 1,
121
+ gap: 2,
122
+ },
123
+ chevron: {
124
+ color: colors.iconColorDefaultDisabled,
90
125
  },
91
126
  })
92
127
  }
@@ -78,7 +78,6 @@ function MessageActionsScreenContent({
78
78
  const apiClient = useApiClient()
79
79
  const styles = useStyles()
80
80
  const { featureEnabled } = useFeatures()
81
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
82
81
  const expandedLink = message.attachments.find(attachment => attachment.type === 'ExpandedLink')
83
82
  const hasExpandedLink = !!expandedLink
84
83
 
@@ -242,7 +241,7 @@ function MessageActionsScreenContent({
242
241
  ))}
243
242
  </View>
244
243
  <View style={styles.actions}>
245
- {repliesEnabled && !inReplyScreen && (
244
+ {!inReplyScreen && (
246
245
  <FormSheet.Action
247
246
  onPress={handleReplyPress}
248
247
  title="Reply to message"
@@ -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
+ }