@planningcenter/chat-react-native 3.35.0-rc.4 → 3.35.0-rc.6

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 (48) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +18 -15
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/components/conversation/messages_disabled_banners.d.ts +1 -0
  5. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  6. package/build/components/conversation/messages_disabled_banners.js +4 -0
  7. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  8. package/build/hooks/use_conversation.d.ts.map +1 -1
  9. package/build/hooks/use_conversation.js +1 -0
  10. package/build/hooks/use_conversation.js.map +1 -1
  11. package/build/screens/conversation_screen.d.ts.map +1 -1
  12. package/build/screens/conversation_screen.js +16 -12
  13. package/build/screens/conversation_screen.js.map +1 -1
  14. package/build/screens/group_notification_settings_screen.d.ts.map +1 -1
  15. package/build/screens/group_notification_settings_screen.js +6 -3
  16. package/build/screens/group_notification_settings_screen.js.map +1 -1
  17. package/build/screens/notification_settings_screen.js +2 -2
  18. package/build/screens/notification_settings_screen.js.map +1 -1
  19. package/build/screens/preferred_app_selection_screen.js +3 -3
  20. package/build/screens/preferred_app_selection_screen.js.map +1 -1
  21. package/build/types/resources/conversation.d.ts +1 -0
  22. package/build/types/resources/conversation.d.ts.map +1 -1
  23. package/build/types/resources/conversation.js.map +1 -1
  24. package/build/utils/can_submit_message.d.ts +10 -0
  25. package/build/utils/can_submit_message.d.ts.map +1 -0
  26. package/build/utils/can_submit_message.js +12 -0
  27. package/build/utils/can_submit_message.js.map +1 -0
  28. package/build/utils/is_input_editable.d.ts +6 -0
  29. package/build/utils/is_input_editable.d.ts.map +1 -0
  30. package/build/utils/is_input_editable.js +4 -0
  31. package/build/utils/is_input_editable.js.map +1 -0
  32. package/build/utils/request/conversation.d.ts.map +1 -1
  33. package/build/utils/request/conversation.js +1 -0
  34. package/build/utils/request/conversation.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/components/conversation/message_form.tsx +19 -9
  37. package/src/components/conversation/messages_disabled_banners.tsx +11 -0
  38. package/src/hooks/use_conversation.ts +1 -0
  39. package/src/screens/conversation_screen.tsx +47 -22
  40. package/src/screens/group_notification_settings_screen.tsx +6 -3
  41. package/src/screens/notification_settings_screen.tsx +2 -2
  42. package/src/screens/preferred_app_selection_screen.tsx +3 -3
  43. package/src/types/resources/conversation.ts +1 -0
  44. package/src/utils/__tests__/can_submit_message.test.ts +32 -0
  45. package/src/utils/__tests__/is_input_editable.test.ts +17 -0
  46. package/src/utils/can_submit_message.ts +23 -0
  47. package/src/utils/is_input_editable.ts +8 -0
  48. package/src/utils/request/conversation.ts +1 -0
@@ -38,6 +38,8 @@ import {
38
38
  platformPressedOpacityStyle,
39
39
  } from '../../utils'
40
40
  import { pickAttachmentPreviewKind } from '../../utils/attachment_kind'
41
+ import { canSubmitMessage } from '../../utils/can_submit_message'
42
+ import { isInputEditable } from '../../utils/is_input_editable'
41
43
  import {
42
44
  DocumentPicker,
43
45
  DocumentPickerResult,
@@ -73,6 +75,7 @@ const MessageFormContext = React.createContext<{
73
75
  setText: (text: string) => void
74
76
  onSubmit: () => void
75
77
  disabled: boolean
78
+ inputEditable: boolean
76
79
  canGiphy: boolean
77
80
  usingGiphy: boolean
78
81
  setUsingGiphy: (usingGiphy: boolean) => void
@@ -86,6 +89,7 @@ const MessageFormContext = React.createContext<{
86
89
  setText: (_text: string) => {},
87
90
  onSubmit: () => {},
88
91
  disabled: false,
92
+ inputEditable: true,
89
93
  canGiphy: false,
90
94
  usingGiphy: false,
91
95
  setUsingGiphy: (_usingGiphy: boolean) => {},
@@ -180,15 +184,19 @@ function MessageFormRoot({
180
184
  return () => clearTimeout(timeoutId)
181
185
  }, [text, attachmentUploader.attachments, saveDraft, currentlyEditingMessage])
182
186
 
183
- const canSubmit = (() => {
184
- if (isPending) return false
185
- if (attachmentUploader?.pendingUploads) return false
186
- if (attachmentUploader?.flaggedAttachmentCount) return false
187
- if (text.length > 0) return true
188
- if (attachmentUploader?.attachments?.length) return true
189
- return false
190
- })()
187
+ const canSubmit = canSubmitMessage({
188
+ isPending,
189
+ conversationDisabled: conversation.disabled ?? false,
190
+ pendingUploads: !!attachmentUploader?.pendingUploads,
191
+ hasFlaggedAttachments: !!attachmentUploader?.flaggedAttachmentCount,
192
+ hasText: text.length > 0,
193
+ hasSendableAttachments: !!attachmentUploader?.attachmentIds?.length,
194
+ })
191
195
  const disabled = !canSubmit
196
+ const inputEditable = isInputEditable({
197
+ isPending,
198
+ conversationDisabled: conversation.disabled ?? false,
199
+ })
192
200
 
193
201
  /*
194
202
  Opening a FormSheet on Android while the keyboard is visible breaks the FormSheet's height & position.
@@ -252,6 +260,7 @@ function MessageFormRoot({
252
260
  setText,
253
261
  onSubmit: handleSubmit,
254
262
  disabled,
263
+ inputEditable,
255
264
  canGiphy,
256
265
  usingGiphy,
257
266
  setUsingGiphy,
@@ -398,7 +407,7 @@ function MessageFormInput() {
398
407
  const styles = useMessageFormStyles()
399
408
  const theme = useTheme()
400
409
  const fontScale = useFontScale()
401
- const { text, setText, onSubmit, usingGiphy, attachmentUploader, replyRootId } =
410
+ const { text, setText, onSubmit, inputEditable, usingGiphy, attachmentUploader, replyRootId } =
402
411
  React.useContext(MessageFormContext)
403
412
  const attachmentError = attachmentUploader?.errorMessage
404
413
 
@@ -445,6 +454,7 @@ function MessageFormInput() {
445
454
  maxLength={usingGiphy ? GIPHY_MAX_LENGTH : MAX_MESSAGE_LENGTH}
446
455
  onSubmitEditing={onSubmit}
447
456
  submitBehavior={usingGiphy ? 'submit' : 'newline'}
457
+ editable={inputEditable}
448
458
  />
449
459
  </View>
450
460
  </View>
@@ -20,6 +20,17 @@ export const MemberMessagesDisabledBanner = () => {
20
20
  )
21
21
  }
22
22
 
23
+ export const ConversationDisabledBanner = () => {
24
+ const styles = useStyles()
25
+
26
+ return (
27
+ <MessagesDisabledBanner
28
+ description="Conversations with teens require at least 2 adults. This chat is paused until another adult is added."
29
+ style={styles.memberBanner}
30
+ />
31
+ )
32
+ }
33
+
23
34
  interface MessagesDisabledBannerProps {
24
35
  description: string
25
36
  style?: ViewStyle
@@ -28,6 +28,7 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
28
28
  'custom_avatar_key',
29
29
  'custom_avatar_color',
30
30
  'custom_avatar_image_url',
31
+ 'disabled',
31
32
  'member_ability',
32
33
  'muted',
33
34
  'replies_disabled',
@@ -18,6 +18,7 @@ import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_bu
18
18
  import { Message } from '../components/conversation/message'
19
19
  import { MessageForm } from '../components/conversation/message_form'
20
20
  import {
21
+ ConversationDisabledBanner,
21
22
  LeaderMessagesDisabledBanner,
22
23
  MemberMessagesDisabledBanner,
23
24
  } from '../components/conversation/messages_disabled_banners'
@@ -48,7 +49,9 @@ import {
48
49
  } from '../hooks/use_product_analytics'
49
50
  import { useScrollTracking } from '../hooks/use_scroll_tracking'
50
51
  import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
52
+ import { ConversationResource } from '../types/resources/conversation'
51
53
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
54
+ import { MessageResource } from '../types/resources/message'
52
55
  import { getRelativeDateStatus } from '../utils/date'
53
56
  import {
54
57
  groupMessages,
@@ -319,33 +322,55 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
319
322
  />
320
323
  {!noMessages && <TypingIndicator />}
321
324
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
322
- {canReply ? (
323
- <MessageForm.Root
324
- replyRootAuthorFirstName={replyRootAuthorFirstName}
325
- conversation={conversation}
326
- replyRootId={replyRootId}
327
- currentlyEditingMessage={currentlyEditingMessage}
328
- // We use a separate key so that it remounts component when switching between new
329
- // and edit message. This simplifies internal state handling.
330
- key={
331
- currentlyEditingMessage
332
- ? `edit-message-form-${currentlyEditingMessage.id}`
333
- : 'new-message-form'
334
- }
335
- >
336
- <MessageForm.AttachmentPicker />
337
- <MessageForm.Commands />
338
- <MessageForm.TextInput />
339
- <MessageForm.SubmitButton />
340
- </MessageForm.Root>
341
- ) : (
342
- <MemberMessagesDisabledBanner />
343
- )}
325
+ <ConversationBottomBar
326
+ conversation={conversation}
327
+ canReply={canReply}
328
+ replyRootAuthorFirstName={replyRootAuthorFirstName}
329
+ replyRootId={replyRootId}
330
+ currentlyEditingMessage={currentlyEditingMessage}
331
+ />
344
332
  </KeyboardView>
345
333
  </View>
346
334
  )
347
335
  }
348
336
 
337
+ interface ConversationBottomBarProps {
338
+ conversation: ConversationResource
339
+ canReply: boolean | undefined
340
+ replyRootAuthorFirstName: string | undefined
341
+ replyRootId: string | null | undefined
342
+ currentlyEditingMessage: MessageResource | undefined
343
+ }
344
+
345
+ function ConversationBottomBar({
346
+ conversation,
347
+ canReply,
348
+ replyRootAuthorFirstName,
349
+ replyRootId,
350
+ currentlyEditingMessage,
351
+ }: ConversationBottomBarProps) {
352
+ if (conversation.disabled) return <ConversationDisabledBanner />
353
+ if (!canReply) return <MemberMessagesDisabledBanner />
354
+ return (
355
+ <MessageForm.Root
356
+ replyRootAuthorFirstName={replyRootAuthorFirstName}
357
+ conversation={conversation}
358
+ replyRootId={replyRootId}
359
+ currentlyEditingMessage={currentlyEditingMessage}
360
+ key={
361
+ currentlyEditingMessage
362
+ ? `edit-message-form-${currentlyEditingMessage.id}`
363
+ : 'new-message-form'
364
+ }
365
+ >
366
+ <MessageForm.AttachmentPicker />
367
+ <MessageForm.Commands />
368
+ <MessageForm.TextInput />
369
+ <MessageForm.SubmitButton />
370
+ </MessageForm.Root>
371
+ )
372
+ }
373
+
349
374
  function InlineDateSeparator({ date }: DateSeparator) {
350
375
  const styles = useDateSeparatorStyles()
351
376
  const { isThisYear } = getRelativeDateStatus(date)
@@ -41,12 +41,14 @@ export function GroupNotificationSettingsScreen({ route }: GroupNotificationSett
41
41
  <View style={styles.container}>
42
42
  <View style={styles.sectionOuter}>
43
43
  <View style={styles.sectionInner}>
44
- <Heading variant="h3" style={styles.sectionHeading}>
44
+ <Heading variant="h2" style={styles.sectionHeading}>
45
45
  Group notification settings
46
46
  </Heading>
47
47
  <Text variant="tertiary" style={styles.sectionSubtitle}>
48
48
  The settings are applied to all conversations in{' '}
49
- <Text style={styles.groupNameBold}>{group.name || title}</Text>
49
+ <Text variant="tertiary" style={styles.groupNameBold}>
50
+ {group.name || title}
51
+ </Text>
50
52
  </Text>
51
53
  </View>
52
54
  </View>
@@ -98,10 +100,11 @@ const useStyles = () => {
98
100
  borderBottomColor: colors.borderColorDefaultBase,
99
101
  },
100
102
  sectionHeading: {
101
- paddingBottom: 4,
103
+ paddingBottom: 12,
102
104
  },
103
105
  sectionSubtitle: {
104
106
  color: colors.textColorDefaultSecondary,
107
+ paddingBottom: 2,
105
108
  },
106
109
  groupNameBold: {
107
110
  fontWeight: platformFontWeightBold,
@@ -90,7 +90,7 @@ export function NotificationSettingsScreen({}: NotificationSettingsScreenProps)
90
90
  ...chatTypes.map(type => ({
91
91
  type: SectionTypes.link,
92
92
  data: {
93
- title: `${type.title} Conversations`,
93
+ title: `${type.title} conversations`,
94
94
  rightLabel: type.preferredApp,
95
95
  onPress: () =>
96
96
  navigation.navigate('PreferredAppSelection', {
@@ -284,7 +284,7 @@ function LinkRow({ title, subtitle, rightLabel, onPress }: LinkRowProps) {
284
284
  <Text style={styles.title} numberOfLines={2}>
285
285
  {title}
286
286
  </Text>
287
- {Boolean(subtitle) && <Text variant="footnote">{subtitle}</Text>}
287
+ {Boolean(subtitle) && <Text variant="tertiary">{subtitle}</Text>}
288
288
  </View>
289
289
  <View style={styles.rightContent}>
290
290
  {isSourceType ? (
@@ -41,12 +41,12 @@ export function PreferredAppSelectionScreen({ route }: PreferredAppSelectionScre
41
41
  updateChatType(app)
42
42
  }
43
43
 
44
- const sectionTitle = `${chatType?.title} Conversations`
44
+ const sectionTitle = `${chatType?.title} conversations`
45
45
 
46
46
  return (
47
47
  <View style={styles.container}>
48
48
  <View style={styles.section}>
49
- <Heading variant="h3" style={styles.sectionHeading}>
49
+ <Heading variant="h2" style={styles.sectionHeading}>
50
50
  {sectionTitle}
51
51
  </Heading>
52
52
  {preferredAppOptions?.sort(sortPreferredApp('asc')).map((option, key) => (
@@ -120,7 +120,7 @@ const styles = StyleSheet.create({
120
120
  flex: 1,
121
121
  },
122
122
  section: {
123
- paddingTop: 16,
123
+ paddingTop: 24,
124
124
  },
125
125
  sectionHeading: {
126
126
  paddingHorizontal: 16,
@@ -12,6 +12,7 @@ export interface ConversationResource {
12
12
  conversationMembership?: Partial<ConversationMembershipResource>
13
13
  createdAt: string
14
14
  deleted?: boolean
15
+ disabled?: boolean
15
16
  genderOption?: string | null
16
17
  groups?: GroupResource[]
17
18
  previewAvatarUrls?: string[]
@@ -0,0 +1,32 @@
1
+ import { canSubmitMessage } from '../can_submit_message'
2
+
3
+ const base = {
4
+ isPending: false,
5
+ conversationDisabled: false,
6
+ pendingUploads: false,
7
+ hasFlaggedAttachments: false,
8
+ hasText: false,
9
+ hasSendableAttachments: false,
10
+ }
11
+
12
+ describe('canSubmitMessage', () => {
13
+ it.each([
14
+ ['empty form', base, false],
15
+ ['has text', { ...base, hasText: true }, true],
16
+ ['has attachments', { ...base, hasSendableAttachments: true }, true],
17
+ ['pending request blocks submit', { ...base, hasText: true, isPending: true }, false],
18
+ [
19
+ 'conversation disabled blocks submit',
20
+ { ...base, hasText: true, conversationDisabled: true },
21
+ false,
22
+ ],
23
+ ['pending uploads block submit', { ...base, hasText: true, pendingUploads: true }, false],
24
+ [
25
+ 'flagged attachments block submit',
26
+ { ...base, hasText: true, hasFlaggedAttachments: true },
27
+ false,
28
+ ],
29
+ ])('%s', (_label, args, expected) => {
30
+ expect(canSubmitMessage(args)).toBe(expected)
31
+ })
32
+ })
@@ -0,0 +1,17 @@
1
+ import { isInputEditable } from '../is_input_editable'
2
+
3
+ const base = {
4
+ isPending: false,
5
+ conversationDisabled: false,
6
+ }
7
+
8
+ describe('isInputEditable', () => {
9
+ it.each([
10
+ ['active conversation with no text', base, true],
11
+ ['active conversation with text', base, true],
12
+ ['pending request locks input', { ...base, isPending: true }, false],
13
+ ['disabled conversation locks input', { ...base, conversationDisabled: true }, false],
14
+ ])('%s', (_label, args, expected) => {
15
+ expect(isInputEditable(args)).toBe(expected)
16
+ })
17
+ })
@@ -0,0 +1,23 @@
1
+ export interface CanSubmitMessageArgs {
2
+ isPending: boolean
3
+ conversationDisabled: boolean
4
+ pendingUploads: boolean
5
+ hasFlaggedAttachments: boolean
6
+ hasText: boolean
7
+ hasSendableAttachments: boolean
8
+ }
9
+
10
+ export function canSubmitMessage({
11
+ isPending,
12
+ conversationDisabled,
13
+ pendingUploads,
14
+ hasFlaggedAttachments,
15
+ hasText,
16
+ hasSendableAttachments,
17
+ }: CanSubmitMessageArgs): boolean {
18
+ if (isPending) return false
19
+ if (conversationDisabled) return false
20
+ if (pendingUploads) return false
21
+ if (hasFlaggedAttachments) return false
22
+ return hasText || hasSendableAttachments
23
+ }
@@ -0,0 +1,8 @@
1
+ export interface IsInputEditableArgs {
2
+ isPending: boolean
3
+ conversationDisabled: boolean
4
+ }
5
+
6
+ export function isInputEditable({ isPending, conversationDisabled }: IsInputEditableArgs): boolean {
7
+ return !isPending && !conversationDisabled
8
+ }
@@ -31,6 +31,7 @@ export const getConversationsRequestArgs = ({
31
31
  Conversation: [
32
32
  'created_at',
33
33
  'badges',
34
+ 'disabled',
34
35
  'groups',
35
36
  'last_message_author_id',
36
37
  'last_message_author_name',