@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
@@ -45,7 +45,6 @@ interface MessageProps extends MessageResource {
45
45
  conversation_id: number
46
46
  latestReadMessageSortKey?: string
47
47
  inReplyScreen?: boolean
48
- repliesEnabled?: boolean
49
48
  }
50
49
 
51
50
  export function Message({
@@ -53,7 +52,6 @@ export function Message({
53
52
  conversation_id,
54
53
  latestReadMessageSortKey,
55
54
  inReplyScreen,
56
- repliesEnabled,
57
55
  ...message
58
56
  }: MessageProps) {
59
57
  const { text, reactionCounts, pending, error, attachments, author } = message
@@ -85,11 +83,10 @@ export function Message({
85
83
  }
86
84
 
87
85
  const renderAuthor = (!message.mine && message.renderAuthor) || false
88
- const showReplyCountButton =
89
- !inReplyScreen && message.replyRootId === message.id && repliesEnabled
86
+ const showReplyCountButton = !inReplyScreen && message.replyRootId === message.id
90
87
  const isReplyRootMessage = message.replyRootId === message.id
91
88
  const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
92
- const replyToReplyRootMessage = repliesEnabled && !isReplyRootMessage && !!message.replyRootId
89
+ const replyToReplyRootMessage = !isReplyRootMessage && !!message.replyRootId
93
90
 
94
91
  const replyCountText = pluralize(message.replyCount, 'reply')
95
92
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
@@ -221,9 +218,7 @@ export function Message({
221
218
  ) : (
222
219
  <View style={styles.avatarPlaceholder} />
223
220
  )}
224
- {repliesEnabled && (
225
- <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
226
- )}
221
+ <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
227
222
  </View>
228
223
  )}
229
224
  <View style={[styles.messageContent, { marginBottom: messageBottomMargin }]}>
@@ -332,7 +327,7 @@ export function Message({
332
327
  </View>
333
328
  )}
334
329
  </View>
335
- {repliesEnabled && message.mine && (
330
+ {message.mine && (
336
331
  <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
337
332
  )}
338
333
  </Animated.View>
@@ -29,7 +29,6 @@ import {
29
29
  platformFontWeightMedium,
30
30
  platformPressedOpacityStyle,
31
31
  } from '../../utils'
32
- import { availableFeatures, useFeatures } from '../../hooks/use_features'
33
32
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
34
33
  import { useMessageDraft } from '../../hooks/use_message_draft'
35
34
  import {
@@ -571,11 +570,9 @@ function EditingIndicator() {
571
570
  function ReplyIndicator() {
572
571
  const { replyRootId, replyRootAuthorFirstName } = React.useContext(MessageFormContext)
573
572
  const navigation = useNavigation()
574
- const { featureEnabled } = useFeatures()
575
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
576
573
  const title = replyRootAuthorFirstName ? `Reply to ${replyRootAuthorFirstName}` : 'Reply'
577
574
 
578
- if (!repliesEnabled || !replyRootId) return null
575
+ if (!replyRootId) return null
579
576
 
580
577
  return (
581
578
  <FormIndicatorRow
@@ -2,30 +2,22 @@ import { StyleSheet, View, type ViewStyle } from 'react-native'
2
2
  import { useTheme } from '../../hooks'
3
3
  import { Text } from '../display'
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
- import { useFeatures } from '../../hooks/use_features'
6
- import { availableFeatures } from '../../hooks/use_features'
7
5
 
8
6
  export const LeaderMessagesDisabledBanner = () => {
9
- const { featureEnabled } = useFeatures()
10
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
11
-
12
- const description = repliesEnabled
13
- ? 'Only leaders can send messages in this conversation.'
14
- : 'Replies are frozen for everyone else.'
15
-
16
- return <MessagesDisabledBanner description={description} />
7
+ return (
8
+ <MessagesDisabledBanner description="Only leaders can send messages in this conversation." />
9
+ )
17
10
  }
18
11
 
19
12
  export const MemberMessagesDisabledBanner = () => {
20
13
  const styles = useStyles()
21
- const { featureEnabled } = useFeatures()
22
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
23
14
 
24
- const description = repliesEnabled
25
- ? 'Only leaders can send messages, but you can still add reactions.'
26
- : 'Replies have been disabled by a leader, but you can still add reactions.'
27
-
28
- return <MessagesDisabledBanner description={description} style={styles.memberBanner} />
15
+ return (
16
+ <MessagesDisabledBanner
17
+ description="Only leaders can send messages, but you can still add reactions."
18
+ style={styles.memberBanner}
19
+ />
20
+ )
29
21
  }
30
22
 
31
23
  interface MessagesDisabledBannerProps {
@@ -91,7 +91,7 @@ export function ConversationActions({
91
91
  const accessibilityActions = [
92
92
  {
93
93
  name: muted ? 'unmute' : 'mute',
94
- label: muted ? 'Unmute' : 'Mute',
94
+ label: muted ? 'Enable' : 'Silence',
95
95
  },
96
96
  ...readAccessibilityAction,
97
97
  ]
@@ -182,8 +182,8 @@ function LeftActions({
182
182
  const styles = useStyles()
183
183
 
184
184
  const muteToggleContent = {
185
- true: { iconName: 'general.bell', label: 'Unmute' },
186
- false: { iconName: 'general.bellMuted', label: 'Mute' },
185
+ true: { iconName: 'general.bell', label: 'Enable' },
186
+ false: { iconName: 'general.bellMuted', label: 'Silence' },
187
187
  } as const
188
188
 
189
189
  const latestMessageUnreadToggleContent = {
@@ -83,6 +83,7 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
83
83
  title: item.resource.title,
84
84
  badge: item.resource.badges?.[0],
85
85
  deleted: item.resource.deleted,
86
+ muted: item.resource.muted,
86
87
  })
87
88
  }
88
89
  showBadges={showBadges}
@@ -38,7 +38,13 @@ 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
+ 'muted',
47
+ ],
42
48
  ConversationBadge: ['app_name', 'pco_resource_type', 'text'],
43
49
  Group: ['type', 'id', 'links', 'name', 'source_app_name', 'source_type'],
44
50
  },
@@ -72,6 +78,9 @@ export const useConversationMute = ({ conversation_id }: { conversation_id: numb
72
78
  if (!prev?.data) return prev
73
79
  setValue(muted)
74
80
  prev.data.muted = muted
81
+ if (prev.data.conversationMembership) {
82
+ prev.data.conversationMembership.muted = muted
83
+ }
75
84
 
76
85
  return prev
77
86
  })
@@ -93,6 +102,9 @@ export const useConversationMute = ({ conversation_id }: { conversation_id: numb
93
102
  // Posting to the mute action endpoint can't return all the fields
94
103
  // so we need to set only the fields we require
95
104
  prev.data.muted = response.data.muted
105
+ if (prev.data.conversationMembership) {
106
+ prev.data.conversationMembership.muted = response.data.muted
107
+ }
96
108
  setValue(response.data.muted)
97
109
 
98
110
  return prev
@@ -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
+ }
@@ -67,6 +67,10 @@ export const useConversationsMute = ({ conversation }: { conversation: Conversat
67
67
  update({
68
68
  ...conversation,
69
69
  muted,
70
+ conversationMembership: {
71
+ ...conversation?.conversationMembership,
72
+ muted,
73
+ },
70
74
  })
71
75
  },
72
76
  mutationKey: ['muteConversation'],
@@ -34,8 +34,8 @@ export function useFeatures() {
34
34
  }
35
35
 
36
36
  export const availableFeatures = {
37
- threaded_replies: 'ROLLOUT_MOBILE_threaded_replies_v1',
38
37
  message_reporting: 'ROLLOUT_MOBILE_message_reporting',
38
+ granular_notifications_ui: 'ROLLOUT_granular_notification_preferences_ui',
39
39
  }
40
40
 
41
41
  const stableEmptyFeatures: ApiCollection<FeatureResource> = {
@@ -56,6 +56,14 @@ import { NotFound } from '../screens/not_found'
56
56
  import { NotificationSettingsScreen } from '../screens/notification_settings_screen'
57
57
  import { PreferredAppSelectionScreen } from '../screens/preferred_app_selection_screen'
58
58
  import { GroupNotificationSettingsScreen } from '../screens/group_notification_settings_screen'
59
+ import {
60
+ GroupNotificationLevelSelectScreen,
61
+ GroupNotificationLevelSelectScreenOptions,
62
+ } from '../screens/group_notification_level_select_screen'
63
+ import {
64
+ ConversationNotificationLevelSelectScreen,
65
+ ConversationNotificationLevelSelectScreenOptions,
66
+ } from '../screens/conversation_notification_level_select_screen'
59
67
  import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
60
68
  import { ScreenLayoutWithChatAccessGate } from './screenLayout'
61
69
  import { SendGiphyScreen, SendGiphyScreenOptions } from '../screens/send_giphy_screen'
@@ -163,13 +171,15 @@ export const ChatStack = createNativeStackNavigator({
163
171
  screen: ConversationScreen,
164
172
  options: ({ route, navigation }) => ({
165
173
  headerTitle: (props: NativeStackHeaderRightProps) => {
166
- const { conversation_id, title, badge, deleted } = route.params as ConversationRouteProps
174
+ const { conversation_id, title, badge, deleted, muted } =
175
+ route.params as ConversationRouteProps
167
176
 
168
177
  return (
169
178
  <ConversationScreenTitle
170
179
  conversation_id={conversation_id}
171
180
  badge={badge}
172
181
  deleted={deleted}
182
+ muted={muted}
173
183
  {...props}
174
184
  >
175
185
  {title ?? 'Conversation'}
@@ -259,6 +269,14 @@ export const ChatStack = createNativeStackNavigator({
259
269
  ),
260
270
  }),
261
271
  },
272
+ GroupNotificationLevelSelect: {
273
+ screen: GroupNotificationLevelSelectScreen,
274
+ options: GroupNotificationLevelSelectScreenOptions,
275
+ },
276
+ ConversationNotificationLevelSelect: {
277
+ screen: ConversationNotificationLevelSelectScreen,
278
+ options: ConversationNotificationLevelSelectScreenOptions,
279
+ },
262
280
  New: {
263
281
  screen: NewConversationStack,
264
282
  if: useQualifiedByAge,
@@ -1,25 +1,29 @@
1
+ import {
2
+ HeaderTitle as ElementsHeaderTitle,
3
+ HeaderTitleProps,
4
+ PlatformPressable,
5
+ } from '@react-navigation/elements'
1
6
  import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native'
2
7
  import React, {
3
8
  useCallback,
4
9
  useEffect,
10
+ useRef,
5
11
  useState,
6
- type SetStateAction,
7
12
  type Dispatch,
8
13
  type ReactNode,
9
- useRef,
14
+ type SetStateAction,
10
15
  } from 'react'
11
16
  import {
17
+ Alert,
12
18
  FlatList,
19
+ Platform,
20
+ Pressable,
13
21
  StyleSheet,
14
22
  TextInput,
15
23
  View,
16
- type ViewStyle,
17
24
  type ViewProps,
18
- Pressable,
19
- Alert,
20
- Platform,
25
+ type ViewStyle,
21
26
  } from 'react-native'
22
- import { HeaderTitle as ElementsHeaderTitle, HeaderTitleProps } from '@react-navigation/elements'
23
27
  import {
24
28
  Badge,
25
29
  ChildNotice,
@@ -31,6 +35,8 @@ import {
31
35
  TextButton,
32
36
  type TextStyle,
33
37
  } from '../components'
38
+ import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
39
+ import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
34
40
  import { useSuspensePaginator, useTheme } from '../hooks'
35
41
  import {
36
42
  useConversation,
@@ -39,14 +45,11 @@ import {
39
45
  useConversationMute,
40
46
  useConversationUpdate,
41
47
  } from '../hooks/use_conversation'
48
+
49
+ import { availableFeatures, useFeatures } from '../hooks/use_features'
42
50
  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
51
  import { GroupResource } from '../types/resources/group_resource'
47
- import { useFeatures } from '../hooks/use_features'
48
- import { availableFeatures } from '../hooks/use_features'
49
-
52
+ import { tokens } from '../vendor/tapestry/tokens'
50
53
  // =========================================
51
54
  // ====== Factory Constants & Types ========
52
55
  // =========================================
@@ -56,6 +59,7 @@ enum SectionTypes {
56
59
  hidden,
57
60
  members,
58
61
  loadingMembers,
62
+ navigableSetting,
59
63
  setting,
60
64
  view,
61
65
  }
@@ -64,6 +68,7 @@ type SectionListData = Array<
64
68
  | DataItem<{ title: string }, SectionTypes.header>
65
69
  | DataItem<MemberResource, SectionTypes.members>
66
70
  | DataItem<any, SectionTypes.loadingMembers>
71
+ | DataItem<NavigableSettingRowProps, SectionTypes.navigableSetting>
67
72
  | DataItem<ViewProps, SectionTypes.view>
68
73
  | DataItem<SettingRowProps, SectionTypes.setting>
69
74
  | DataItem<any, SectionTypes.hidden>
@@ -91,16 +96,19 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
91
96
 
92
97
  const { data: conversation } = useConversation(route.params)
93
98
  const [title, setTitle] = useState(conversation.title)
94
- const { muted, setMuted } = useConversationMute(route.params)
99
+ const { conversation_id } = route.params
95
100
  const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params)
96
101
  const { mutate: saveTitle } = useConversationUpdate(route.params)
97
102
  const { mutate: deleteConversation } = useConversationDelete(route.params)
98
103
  const { featureEnabled } = useFeatures()
99
- const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
104
+ const granularNotificationsEnabled = featureEnabled(availableFeatures.granular_notifications_ui)
105
+ const { muted, setMuted } = useConversationMute({ conversation_id })
100
106
 
101
107
  const trimmedTitle = title.trim()
102
108
  const emptyTitle = trimmedTitle === '' || title === null
103
109
 
110
+ const notificationLevelDescription =
111
+ conversation?.conversationMembership?.notificationLevelDescription
104
112
  const canUpdate = conversation.memberAbility?.canUpdate || false
105
113
  const canDelete = conversation.memberAbility?.canDelete || false
106
114
  const isLeader = conversation.memberAbility?.leader || false
@@ -243,19 +251,29 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
243
251
  showBottomBorder: true,
244
252
  sectionInnerStyle: styles.sectionInnerHeaderWithBottomBorder,
245
253
  },
246
- {
247
- type: SectionTypes.setting,
248
- data: {
249
- title: 'Mute',
250
- rightItem: <Switch value={muted} onChange={() => setMuted(!muted)} />,
251
- },
252
- showBottomBorder: true,
253
- },
254
+ granularNotificationsEnabled
255
+ ? {
256
+ type: SectionTypes.navigableSetting,
257
+ data: {
258
+ title: 'Notifications',
259
+ subtitle: notificationLevelDescription,
260
+ onPress: () =>
261
+ navigation.navigate('ConversationNotificationLevelSelect', { conversation_id }),
262
+ },
263
+ showBottomBorder: true,
264
+ }
265
+ : {
266
+ type: SectionTypes.setting,
267
+ data: {
268
+ title: 'Mute',
269
+ rightItem: <Switch value={muted} onValueChange={value => setMuted(value)} />,
270
+ },
271
+ showBottomBorder: true,
272
+ },
254
273
  {
255
274
  type: canUpdate ? SectionTypes.setting : SectionTypes.hidden,
256
275
  data: {
257
- title: repliesEnabled ? 'Leader messages only' : 'Freeze conversation',
258
- subtitle: repliesEnabled ? undefined : 'Disables replies for everyone except leaders.',
276
+ title: 'Only leaders can message',
259
277
  rightItem: (
260
278
  <Switch value={repliesDisabled} onChange={() => setRepliesDisabled(!repliesDisabled)} />
261
279
  ),
@@ -350,6 +368,18 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
350
368
  <Text>Loading more...</Text>
351
369
  </ListSection>
352
370
  )
371
+ case SectionTypes.navigableSetting:
372
+ return (
373
+ <ListSection
374
+ isStart={isStart}
375
+ isEnd={isEnd}
376
+ showBottomBorder={item?.showBottomBorder}
377
+ outerStyle={item?.sectionOuterStyle}
378
+ innerStyle={item?.sectionInnerStyle}
379
+ >
380
+ <NavigableSettingRow {...item.data} />
381
+ </ListSection>
382
+ )
353
383
  case SectionTypes.setting:
354
384
  return (
355
385
  <ListSection
@@ -459,6 +489,12 @@ function TitleInput({ canUpdate, title, setTitle, style, isEmpty }: InputProps)
459
489
  )
460
490
  }
461
491
 
492
+ interface NavigableSettingRowProps {
493
+ title: string
494
+ subtitle?: string
495
+ onPress: () => void
496
+ }
497
+
462
498
  interface SettingRowProps {
463
499
  title: string
464
500
  style?: ViewStyle
@@ -510,6 +546,30 @@ function SettingRow({
510
546
  )
511
547
  }
512
548
 
549
+ function NavigableSettingRow({ title, subtitle, onPress }: NavigableSettingRowProps) {
550
+ const styles = useStyles()
551
+ return (
552
+ <PlatformPressable onPress={onPress} accessibilityRole="button">
553
+ <View style={styles.settingRow}>
554
+ <View style={styles.settingRowContent}>
555
+ <Text variant="plain" style={styles.settingRowText}>
556
+ {title}
557
+ </Text>
558
+ {Boolean(subtitle) && <Text variant="footnote">{subtitle}</Text>}
559
+ </View>
560
+ {Platform.OS === 'ios' && (
561
+ <Icon
562
+ name="general.rightChevron"
563
+ size={16}
564
+ style={styles.navigableSettingChevron}
565
+ accessibilityElementsHidden
566
+ />
567
+ )}
568
+ </View>
569
+ </PlatformPressable>
570
+ )
571
+ }
572
+
513
573
  function TeamsGroup({ teams }: { teams: GroupResource[] }) {
514
574
  const styles = useStyles()
515
575
 
@@ -628,6 +688,9 @@ const useStyles = ({ isStart, isEnd }: { isStart?: boolean; isEnd?: boolean } =
628
688
  settingRowText: {
629
689
  lineHeight: 20,
630
690
  },
691
+ navigableSettingChevron: {
692
+ color: colors.iconColorDefaultDisabled,
693
+ },
631
694
  teamGroup: {
632
695
  flexDirection: 'row',
633
696
  gap: 4,
@@ -0,0 +1,73 @@
1
+ import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
+ import React from 'react'
3
+ import { StyleSheet } from 'react-native'
4
+ import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
5
+ import { PressableRow } from '../components'
6
+ import { useTheme } from '../hooks'
7
+ import { useConversation } from '../hooks/use_conversation'
8
+ import { useConversationMembershipUpdate } from '../hooks/use_conversation_membership'
9
+ import { NotificationLevelValue } from '../types'
10
+
11
+ export const ConversationNotificationLevelSelectScreenOptions = getFormSheetScreenOptions({
12
+ headerTitle: 'Notification level',
13
+ sheetAllowedDetents: [0.35],
14
+ })
15
+
16
+ export type ConversationNotificationLevelSelectScreenProps = StaticScreenProps<{
17
+ conversation_id: number
18
+ }>
19
+
20
+ export function ConversationNotificationLevelSelectScreen({
21
+ route,
22
+ }: ConversationNotificationLevelSelectScreenProps) {
23
+ const { conversation_id } = route.params
24
+ const navigation = useNavigation()
25
+ const styles = useStyles()
26
+ const { colors } = useTheme()
27
+ const { data: conversation } = useConversation({ conversation_id })
28
+ const { mutate: updateNotificationLevel } = useConversationMembershipUpdate({ conversation_id })
29
+
30
+ const notificationLevel = conversation.conversationMembership?.notificationLevel
31
+ const notificationLevelOptions =
32
+ conversation.conversationMembership?.notificationLevelOptions ?? []
33
+
34
+ const handleSelect = (value: NotificationLevelValue, isActive: boolean) => {
35
+ if (!isActive) {
36
+ const muted = value === 'nothing'
37
+ updateNotificationLevel({
38
+ muted,
39
+ notificationLevel: value as typeof notificationLevel,
40
+ })
41
+ }
42
+ navigation.goBack()
43
+ }
44
+
45
+ return (
46
+ <FormSheet.Root contentStyle={styles.content}>
47
+ {notificationLevelOptions.map(option => {
48
+ const isActive = option.enabled
49
+ return (
50
+ <PressableRow
51
+ key={option.value}
52
+ text={option.description}
53
+ isActive={isActive}
54
+ onPress={() => handleSelect(option.value, isActive)}
55
+ iconColor={colors.statusSuccessIcon}
56
+ style={styles.row}
57
+ />
58
+ )
59
+ })}
60
+ </FormSheet.Root>
61
+ )
62
+ }
63
+
64
+ const useStyles = () => {
65
+ return StyleSheet.create({
66
+ content: {
67
+ paddingTop: 20,
68
+ },
69
+ row: {
70
+ borderBottomWidth: 0,
71
+ },
72
+ })
73
+ }