@planningcenter/chat-react-native 3.2.0-rc.25 → 3.2.0-rc.27

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 (102) hide show
  1. package/build/components/conversation/message_form/message_form_attachment_image.d.ts +13 -0
  2. package/build/components/conversation/message_form/message_form_attachment_image.d.ts.map +1 -0
  3. package/build/components/conversation/message_form/message_form_attachment_image.js +78 -0
  4. package/build/components/conversation/message_form/message_form_attachment_image.js.map +1 -0
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +128 -16
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversations/conversation_actions.d.ts +2 -2
  9. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  10. package/build/components/conversations/conversation_actions.js.map +1 -1
  11. package/build/components/conversations/conversation_preview.d.ts +3 -1
  12. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  13. package/build/components/conversations/conversation_preview.js +2 -2
  14. package/build/components/conversations/conversation_preview.js.map +1 -1
  15. package/build/components/group_conversation_list.d.ts +19 -0
  16. package/build/components/group_conversation_list.d.ts.map +1 -0
  17. package/build/components/group_conversation_list.js +48 -0
  18. package/build/components/group_conversation_list.js.map +1 -0
  19. package/build/components/index.d.ts +1 -0
  20. package/build/components/index.d.ts.map +1 -1
  21. package/build/components/index.js +1 -0
  22. package/build/components/index.js.map +1 -1
  23. package/build/contexts/conversations_context.js +1 -1
  24. package/build/contexts/conversations_context.js.map +1 -1
  25. package/build/hooks/attachments/supported_extensions.d.ts +2 -0
  26. package/build/hooks/attachments/supported_extensions.d.ts.map +1 -0
  27. package/build/hooks/attachments/supported_extensions.js +48 -0
  28. package/build/hooks/attachments/supported_extensions.js.map +1 -0
  29. package/build/hooks/index.d.ts +4 -0
  30. package/build/hooks/index.d.ts.map +1 -1
  31. package/build/hooks/index.js +4 -0
  32. package/build/hooks/index.js.map +1 -1
  33. package/build/hooks/use_api.d.ts +2 -2
  34. package/build/hooks/use_api.d.ts.map +1 -1
  35. package/build/hooks/use_api.js.map +1 -1
  36. package/build/hooks/use_attachment_uploader.d.ts +26 -0
  37. package/build/hooks/use_attachment_uploader.d.ts.map +1 -0
  38. package/build/hooks/use_attachment_uploader.js +111 -0
  39. package/build/hooks/use_attachment_uploader.js.map +1 -0
  40. package/build/hooks/use_upload_client.d.ts +28 -0
  41. package/build/hooks/use_upload_client.d.ts.map +1 -0
  42. package/build/hooks/use_upload_client.js +32 -0
  43. package/build/hooks/use_upload_client.js.map +1 -0
  44. package/build/index.d.ts +1 -0
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +1 -0
  47. package/build/index.js.map +1 -1
  48. package/build/navigation/index.d.ts +2 -2
  49. package/build/navigation/index.d.ts.map +1 -1
  50. package/build/navigation/index.js +2 -2
  51. package/build/navigation/index.js.map +1 -1
  52. package/build/screens/conversation_new/components/groups_form.d.ts +3 -1
  53. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  54. package/build/screens/conversation_new/components/groups_form.js +7 -9
  55. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  56. package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
  57. package/build/screens/conversation_new/conversation_new_screen.js +2 -2
  58. package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
  59. package/build/screens/conversation_screen.d.ts.map +1 -1
  60. package/build/screens/conversation_screen.js +27 -2
  61. package/build/screens/conversation_screen.js.map +1 -1
  62. package/build/screens/conversations/conversations_screen.js +1 -1
  63. package/build/screens/conversations/conversations_screen.js.map +1 -1
  64. package/build/utils/native_adapters/configuration.d.ts +4 -1
  65. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  66. package/build/utils/native_adapters/configuration.js +13 -1
  67. package/build/utils/native_adapters/configuration.js.map +1 -1
  68. package/build/utils/native_adapters/image_picker.d.ts +25 -0
  69. package/build/utils/native_adapters/image_picker.d.ts.map +1 -0
  70. package/build/utils/native_adapters/image_picker.js +9 -0
  71. package/build/utils/native_adapters/image_picker.js.map +1 -0
  72. package/build/utils/native_adapters/index.d.ts +1 -0
  73. package/build/utils/native_adapters/index.d.ts.map +1 -1
  74. package/build/utils/native_adapters/index.js +1 -0
  75. package/build/utils/native_adapters/index.js.map +1 -1
  76. package/build/utils/upload_uri.d.ts +23 -0
  77. package/build/utils/upload_uri.d.ts.map +1 -0
  78. package/build/utils/upload_uri.js +60 -0
  79. package/build/utils/upload_uri.js.map +1 -0
  80. package/package.json +2 -2
  81. package/src/components/conversation/message_form/message_form_attachment_image.tsx +121 -0
  82. package/src/components/conversation/message_form.tsx +197 -31
  83. package/src/components/conversations/conversation_actions.tsx +2 -2
  84. package/src/components/conversations/conversation_preview.tsx +8 -2
  85. package/src/components/group_conversation_list.tsx +82 -0
  86. package/src/components/index.tsx +1 -0
  87. package/src/contexts/conversations_context.tsx +1 -1
  88. package/src/hooks/attachments/supported_extensions.ts +47 -0
  89. package/src/hooks/index.ts +4 -0
  90. package/src/hooks/use_api.ts +2 -2
  91. package/src/hooks/use_attachment_uploader.ts +179 -0
  92. package/src/hooks/use_upload_client.ts +67 -0
  93. package/src/index.tsx +1 -0
  94. package/src/navigation/index.tsx +2 -5
  95. package/src/screens/conversation_new/components/groups_form.tsx +11 -11
  96. package/src/screens/conversation_new/conversation_new_screen.tsx +6 -2
  97. package/src/screens/conversation_screen.tsx +31 -1
  98. package/src/screens/conversations/conversations_screen.tsx +1 -1
  99. package/src/utils/native_adapters/configuration.ts +15 -1
  100. package/src/utils/native_adapters/image_picker.ts +31 -0
  101. package/src/utils/native_adapters/index.ts +1 -0
  102. package/src/utils/upload_uri.ts +69 -0
@@ -0,0 +1,60 @@
1
+ import DeviceInfo from 'react-native-device-info';
2
+ const brand = DeviceInfo.getBrand();
3
+ const model = DeviceInfo.getModel();
4
+ const systemName = DeviceInfo.getSystemName();
5
+ const systemVersion = DeviceInfo.getSystemVersion();
6
+ const readableVersion = DeviceInfo.getReadableVersion();
7
+ const appName = DeviceInfo.getApplicationName();
8
+ /**
9
+ * This is for accessing https://github.com/planningcenter/upload
10
+ */
11
+ export class UploadUri {
12
+ session;
13
+ app;
14
+ constructor({ session }) {
15
+ this.session = session;
16
+ }
17
+ get schema() {
18
+ if (this.env === 'development') {
19
+ return 'http';
20
+ }
21
+ else {
22
+ return 'https';
23
+ }
24
+ }
25
+ get host() {
26
+ return `${this.subdomain}.${this.domain}.${this.tld}`;
27
+ }
28
+ get subdomain() {
29
+ switch (this.env) {
30
+ case 'staging':
31
+ return 'upload-staging';
32
+ default:
33
+ return 'upload';
34
+ }
35
+ }
36
+ get domain() {
37
+ return this.env === 'development' ? 'pco' : 'planningcenteronline';
38
+ }
39
+ get tld() {
40
+ switch (this.env) {
41
+ case 'development':
42
+ return 'test';
43
+ default:
44
+ return 'com';
45
+ }
46
+ }
47
+ get env() {
48
+ return this.session?.env || 'production';
49
+ }
50
+ get baseUrl() {
51
+ return `${this.schema}://${this.host}`;
52
+ }
53
+ get headers() {
54
+ return {
55
+ 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,
56
+ Authorization: `Bearer ${this.session.token?.access_token}`,
57
+ };
58
+ }
59
+ }
60
+ //# sourceMappingURL=upload_uri.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload_uri.js","sourceRoot":"","sources":["../../src/utils/upload_uri.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,0BAA0B,CAAA;AAEjD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;AACnC,MAAM,UAAU,GAAG,UAAU,CAAC,aAAa,EAAE,CAAA;AAC7C,MAAM,aAAa,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;AACnD,MAAM,eAAe,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AACvD,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AAE/C;;GAEG;AACH,MAAM,OAAO,SAAS;IACpB,OAAO,CAAS;IAChB,GAAG,CAAS;IAEZ,YAAY,EAAE,OAAO,EAAwB;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,GAAG,KAAK,aAAa,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAA;QACf,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAA;IACvD,CAAC;IAED,IAAI,SAAS;QACX,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC;YACjB,KAAK,SAAS;gBACZ,OAAO,gBAAgB,CAAA;YACzB;gBACE,OAAO,QAAQ,CAAA;QACnB,CAAC;IACH,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,sBAAsB,CAAA;IACpE,CAAC;IAED,IAAI,GAAG;QACL,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC;YACjB,KAAK,aAAa;gBAChB,OAAO,MAAM,CAAA;YACf;gBACE,OAAO,KAAK,CAAA;QAChB,CAAC;IACH,CAAC;IAED,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,YAAY,CAAA;IAC1C,CAAC;IAED,IAAI,OAAO;QACT,OAAO,GAAG,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;IACxC,CAAC;IAED,IAAI,OAAO;QACT,OAAO;YACL,YAAY,EAAE,GAAG,OAAO,IAAI,eAAe,KAAK,KAAK,KAAK,KAAK,KAAK,UAAU,KAAK,aAAa,GAAG;YACnG,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE;SAC5D,CAAA;IACH,CAAC;CACF","sourcesContent":["import DeviceInfo from 'react-native-device-info'\nimport { Session } from './session'\nconst brand = DeviceInfo.getBrand()\nconst model = DeviceInfo.getModel()\nconst systemName = DeviceInfo.getSystemName()\nconst systemVersion = DeviceInfo.getSystemVersion()\nconst readableVersion = DeviceInfo.getReadableVersion()\nconst appName = DeviceInfo.getApplicationName()\n\n/**\n * This is for accessing https://github.com/planningcenter/upload\n */\nexport class UploadUri {\n session: Session\n app?: string\n\n constructor({ session }: { session: Session }) {\n this.session = session\n }\n\n get schema() {\n if (this.env === 'development') {\n return 'http'\n } else {\n return 'https'\n }\n }\n\n get host() {\n return `${this.subdomain}.${this.domain}.${this.tld}`\n }\n\n get subdomain() {\n switch (this.env) {\n case 'staging':\n return 'upload-staging'\n default:\n return 'upload'\n }\n }\n\n get domain(): 'pco' | 'planningcenteronline' {\n return this.env === 'development' ? 'pco' : 'planningcenteronline'\n }\n\n get tld() {\n switch (this.env) {\n case 'development':\n return 'test'\n default:\n return 'com'\n }\n }\n\n get env() {\n return this.session?.env || 'production'\n }\n\n get baseUrl() {\n return `${this.schema}://${this.host}`\n }\n\n get headers() {\n return {\n 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,\n Authorization: `Bearer ${this.session.token?.access_token}`,\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.2.0-rc.25",
3
+ "version": "3.2.0-rc.27",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "prettier": "^3.4.2",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "5341fc7d2489707a08f20ce39b3fef50cbe9d5c5"
58
+ "gitHead": "38c6eb6290424c017191c143c533a192de92d02d"
59
59
  }
@@ -0,0 +1,121 @@
1
+ import React from 'react'
2
+ import {
3
+ AnimatableNumericValue,
4
+ DimensionValue,
5
+ Image as ReactNativeImage,
6
+ StyleSheet,
7
+ View,
8
+ } from 'react-native'
9
+ import { FileAttachment } from '../../../hooks/use_attachment_uploader'
10
+ import { useTheme } from '../../../hooks'
11
+ import { Icon, IconButton, Spinner } from '../../display'
12
+
13
+ interface Props {
14
+ uri: string
15
+ alt: string
16
+ status: FileAttachment['status']
17
+ width?: number
18
+ height?: number
19
+ removeAttachment: () => void
20
+ }
21
+
22
+ export function MessageFormAttachmentImage({ uri, alt, status, removeAttachment }: Props) {
23
+ function opacity() {
24
+ if (status === 'uploading') {
25
+ return 0.5
26
+ }
27
+ return 1
28
+ }
29
+ const styles = useStyles({
30
+ width: 50,
31
+ height: 50,
32
+ borderRadius: 8,
33
+ })
34
+ const loading = status === 'uploading'
35
+ const error = status === 'error'
36
+ const ready = status === 'success'
37
+
38
+ return (
39
+ <View
40
+ accessible={Boolean(alt)}
41
+ accessibilityRole="image"
42
+ accessibilityState={{ busy: loading }}
43
+ >
44
+ <ReactNativeImage source={{ uri }} style={[styles.image, { opacity: opacity() }]} alt={alt} />
45
+ {ready && (
46
+ <IconButton
47
+ name="general.x"
48
+ accessibilityLabel="Remove Attachment"
49
+ size="md"
50
+ style={styles.removeAttachmentIcon}
51
+ onPress={removeAttachment}
52
+ />
53
+ )}
54
+ {loading && (
55
+ <View style={[styles.loadingBackground]}>
56
+ <Spinner size={24} />
57
+ </View>
58
+ )}
59
+ {error && (
60
+ <View style={styles.errorBackground}>
61
+ <View style={styles.errorIconBackground}>
62
+ <Icon name="churchCenter.exclamationCircle" size={18} color="red" />
63
+ </View>
64
+ </View>
65
+ )}
66
+ </View>
67
+ )
68
+ }
69
+
70
+ interface Styles {
71
+ width: DimensionValue
72
+ height: DimensionValue
73
+ borderRadius: AnimatableNumericValue | string
74
+ }
75
+
76
+ const useStyles = ({ width, height, borderRadius }: Styles) => {
77
+ const { colors } = useTheme()
78
+
79
+ return StyleSheet.create({
80
+ image: {
81
+ backgroundColor: colors.fillColorNeutral070,
82
+ width,
83
+ height,
84
+ borderRadius,
85
+ },
86
+ removeAttachmentIcon: {
87
+ position: 'absolute',
88
+ top: 4,
89
+ right: 4,
90
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
91
+ borderRadius: 24,
92
+ width: 24,
93
+ height: 24,
94
+ justifyContent: 'center',
95
+ alignItems: 'center',
96
+ },
97
+ loadingBackground: {
98
+ position: 'absolute',
99
+ top: 0,
100
+ left: 0,
101
+ borderRadius,
102
+ width,
103
+ height,
104
+ },
105
+ errorBackground: {
106
+ position: 'absolute',
107
+ top: 0,
108
+ left: 0,
109
+ borderRadius,
110
+ width,
111
+ height,
112
+ justifyContent: 'center',
113
+ alignItems: 'center',
114
+ },
115
+ errorIconBackground: {
116
+ backgroundColor: 'white',
117
+ padding: 2,
118
+ borderRadius: 50,
119
+ },
120
+ })
121
+ }
@@ -1,5 +1,5 @@
1
1
  import { useNavigation, useTheme as useNavigationTheme, useRoute } from '@react-navigation/native'
2
- import React, { useContext, useEffect, useState } from 'react'
2
+ import React, { useCallback, useContext, useEffect, useState } from 'react'
3
3
  import { StyleSheet, TextInput, View, ViewProps } from 'react-native'
4
4
  import { IconButton, Text } from '../../components'
5
5
  import { useTheme } from '../../hooks'
@@ -7,6 +7,13 @@ import { ConversationResource } from '../../types'
7
7
  import { useMessageCreate } from '../../hooks/use_message_create'
8
8
  import { ConversationScreenProps } from '../../screens/conversation_screen'
9
9
  import { ChatContext } from '../../contexts/chat_context'
10
+ import { ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
11
+ import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
12
+ import {
13
+ DenormalizedAttachmentResourceForCreate,
14
+ DenormalizedMessageAttachmentResourceForCreate,
15
+ } from '../../types/resources/denormalized_attachment_resource'
16
+ import { MessageFormAttachmentImage } from './message_form/message_form_attachment_image'
10
17
 
11
18
  export const MessageForm = {
12
19
  Root: MessageFormRoot,
@@ -20,7 +27,16 @@ interface MessagesFormRootProps extends ViewProps {
20
27
  conversation: ConversationResource
21
28
  }
22
29
 
23
- const MessageFormContext = React.createContext({
30
+ const MessageFormContext = React.createContext<{
31
+ text: string
32
+ setText: (text: string) => void
33
+ onSubmit: () => void
34
+ disabled: boolean
35
+ canGiphy: boolean
36
+ usingGiphy: boolean
37
+ setUsingGiphy: (usingGiphy: boolean) => void
38
+ attachmentUploader?: ReturnType<typeof useAttachmentUploader>
39
+ }>({
24
40
  text: '',
25
41
  setText: (_text: string) => {},
26
42
  onSubmit: () => {},
@@ -38,7 +54,25 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
38
54
  const [usingGiphy, setUsingGiphy] = useState(false)
39
55
  const navigation = useNavigation()
40
56
  const route = useRoute() as ConversationScreenProps['route']
41
- const { status, isPending, reset, mutate } = useMessageCreate({ conversationId: conversation.id })
57
+ const {
58
+ status,
59
+ isPending,
60
+ reset: resetMutation,
61
+ mutate,
62
+ } = useMessageCreate({
63
+ conversationId: conversation.id,
64
+ })
65
+ const attachmentUploader = useAttachmentUploader({
66
+ conversationId: conversation.id,
67
+ })
68
+ const resetAttachmentUploader = attachmentUploader.reset
69
+
70
+ const reset = useCallback(() => {
71
+ resetAttachmentUploader()
72
+ resetMutation()
73
+ setText('')
74
+ setUsingGiphy(false)
75
+ }, [resetAttachmentUploader, resetMutation])
42
76
 
43
77
  useEffect(() => {
44
78
  if (canGiphy && !usingGiphy && text.startsWith('/giphy ')) {
@@ -50,7 +84,6 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
50
84
  useEffect(() => {
51
85
  switch (status) {
52
86
  case 'success':
53
- setText('')
54
87
  reset()
55
88
  break
56
89
  }
@@ -58,15 +91,16 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
58
91
 
59
92
  useEffect(() => {
60
93
  if (route.params.clear_input) {
61
- setText('')
62
- setUsingGiphy(false)
94
+ reset()
63
95
  navigation.setParams({ ...route.params, clear_input: false })
64
96
  }
65
- }, [navigation, route.params])
97
+ }, [reset, navigation, route.params])
66
98
 
67
99
  const canSubmit = (() => {
68
100
  if (isPending) return false
101
+ if (attachmentUploader?.pendingUploads) return false
69
102
  if (text.length > 0) return true
103
+ if (attachmentUploader?.attachments?.length) return true
70
104
  return false
71
105
  })()
72
106
  const disabled = !canSubmit
@@ -81,7 +115,16 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
81
115
  search_term: text,
82
116
  })
83
117
  } else {
84
- mutate({ text })
118
+ let attachmentsForSubmit: DenormalizedAttachmentResourceForCreate[] = []
119
+ if (attachmentUploader?.attachmentIds) {
120
+ attachmentsForSubmit = attachmentUploader.attachmentIds.map(
121
+ (id: string): DenormalizedMessageAttachmentResourceForCreate => ({
122
+ type: 'MessageAttachment',
123
+ id,
124
+ })
125
+ )
126
+ }
127
+ mutate({ text, attachments: attachmentsForSubmit })
85
128
  }
86
129
  }
87
130
 
@@ -95,6 +138,7 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
95
138
  canGiphy,
96
139
  usingGiphy,
97
140
  setUsingGiphy,
141
+ attachmentUploader,
98
142
  }}
99
143
  >
100
144
  <View style={styles.textInputContainer}>{children}</View>
@@ -102,25 +146,63 @@ function MessageFormRoot({ conversation, children }: MessagesFormRootProps) {
102
146
  )
103
147
  }
104
148
 
149
+ function MessageFormAttachments() {
150
+ const styles = useMessageFormStyles()
151
+ const { attachmentUploader } = React.useContext(MessageFormContext)
152
+ const numberOfAttachments = attachmentUploader?.attachments?.length || 0
153
+ const attachments = attachmentUploader?.attachments || []
154
+
155
+ if (numberOfAttachments === 0) {
156
+ return null
157
+ }
158
+
159
+ return (
160
+ <View style={styles.messageFormAttachments}>
161
+ {attachments.map(attachment => {
162
+ return (
163
+ <MessageFormAttachmentImage
164
+ key={attachment.file.uri}
165
+ uri={attachment.file.uri}
166
+ alt={attachment.file.name}
167
+ status={attachment.status}
168
+ width={attachment.file.width}
169
+ height={attachment.file.height}
170
+ removeAttachment={() => {
171
+ attachmentUploader?.removeAttachment(attachment)
172
+ }}
173
+ />
174
+ )
175
+ })}
176
+ </View>
177
+ )
178
+ }
179
+
105
180
  function MessageFormInput() {
106
181
  const styles = useMessageFormStyles()
107
- const { text, setText, onSubmit, usingGiphy } = React.useContext(MessageFormContext)
182
+ const { text, setText, onSubmit, usingGiphy, attachmentUploader } =
183
+ React.useContext(MessageFormContext)
184
+ const attachmentError = attachmentUploader?.errorMessage
108
185
 
109
186
  return (
110
- <View style={styles.textInput}>
111
- {usingGiphy ? (
112
- <View style={styles.giphyBadge}>
113
- <Text>/Giphy</Text>
114
- </View>
115
- ) : null}
116
-
117
- <TextInput
118
- aria-disabled={true}
119
- placeholder="Send a message"
120
- onChangeText={setText}
121
- value={text}
122
- onSubmitEditing={onSubmit}
123
- />
187
+ <View style={styles.textInputBoundary}>
188
+ <MessageFormAttachments />
189
+ <View style={styles.textInput}>
190
+ {usingGiphy ? (
191
+ <View style={styles.giphyBadge}>
192
+ <Text>/Giphy</Text>
193
+ </View>
194
+ ) : null}
195
+
196
+ <TextInput
197
+ aria-disabled={true}
198
+ placeholder="Send a message"
199
+ onChangeText={setText}
200
+ value={text}
201
+ onSubmitEditing={onSubmit}
202
+ />
203
+ </View>
204
+
205
+ {attachmentError ? <Text style={styles.inputErrorMessage}>{attachmentError}</Text> : null}
124
206
  </View>
125
207
  )
126
208
  }
@@ -143,19 +225,82 @@ function MessageFormSubmitBtn() {
143
225
  }
144
226
 
145
227
  function MessageFormAttachmentPicker() {
146
- const { usingGiphy } = React.useContext(MessageFormContext)
228
+ const styles = useMessageFormStyles()
229
+ const { usingGiphy, attachmentUploader } = React.useContext(MessageFormContext)
230
+ const [isOpen, setIsOpen] = useState(false)
231
+
232
+ function uploadImagePickerResult(result: ImagePickerResult) {
233
+ if (result.canceled) {
234
+ return
235
+ }
236
+
237
+ const filteredAssets = result.assets
238
+ .filter(asset => {
239
+ return asset.fileSize && asset.fileName && asset.mimeType
240
+ })
241
+ .map(asset => {
242
+ return {
243
+ uri: asset.uri,
244
+ name: asset.fileName as string,
245
+ type: asset.mimeType as string,
246
+ size: asset.fileSize as number,
247
+ height: asset.height,
248
+ width: asset.width,
249
+ }
250
+ })
251
+
252
+ attachmentUploader?.handleFilesAttached(filteredAssets)
253
+ }
254
+
255
+ const openCamera = async () => {
256
+ setIsOpen(false)
257
+ let result = await ImagePicker.openCameraAsync()
258
+ if (!result.canceled) {
259
+ uploadImagePickerResult(result)
260
+ }
261
+ }
262
+
263
+ const pickImage = async () => {
264
+ setIsOpen(false)
265
+ let result = await ImagePicker.openImageLibraryAsync()
266
+ if (!result.canceled) {
267
+ uploadImagePickerResult(result)
268
+ }
269
+ }
147
270
 
148
271
  if (usingGiphy) {
149
272
  return null
150
273
  }
151
274
 
152
275
  return (
153
- <IconButton
154
- accessibilityLabel="Shazam"
155
- size="md"
156
- appearance="neutral"
157
- name={'general.paperclip'}
158
- />
276
+ // TODO: Design Pass
277
+ <View style={styles.attachmentPicker}>
278
+ {isOpen && (
279
+ <View style={styles.attachmentPickerButtons}>
280
+ <IconButton
281
+ accessibilityLabel="Take a photo"
282
+ size="md"
283
+ appearance="neutral"
284
+ name={'general.videoCamera'}
285
+ onPress={openCamera}
286
+ />
287
+ <IconButton
288
+ accessibilityLabel="Choose a photo"
289
+ size="md"
290
+ appearance="neutral"
291
+ name={'churchCenter.photosIos'}
292
+ onPress={pickImage}
293
+ />
294
+ </View>
295
+ )}
296
+ <IconButton
297
+ accessibilityLabel="File Menu"
298
+ size="md"
299
+ appearance="neutral"
300
+ name={'general.outlinedPlusCircle'}
301
+ onPress={() => setIsOpen(!isOpen)}
302
+ />
303
+ </View>
159
304
  )
160
305
  }
161
306
 
@@ -203,13 +348,16 @@ const useMessageFormStyles = () => {
203
348
  alignItems: 'center',
204
349
  gap: 12,
205
350
  },
206
- textInput: {
351
+ textInputBoundary: {
207
352
  borderRadius: 24,
208
353
  borderWidth: 1,
209
354
  padding: 12,
210
355
  paddingHorizontal: 20,
211
356
  borderColor: theme.colors.fillColorNeutral050Base,
212
357
  flex: 1,
358
+ gap: 12,
359
+ },
360
+ textInput: {
213
361
  flexDirection: 'row',
214
362
  gap: 12,
215
363
  },
@@ -224,5 +372,23 @@ const useMessageFormStyles = () => {
224
372
  height: 36,
225
373
  width: 36,
226
374
  },
375
+ attachmentPicker: {
376
+ position: 'relative',
377
+ },
378
+ attachmentPickerButtons: {
379
+ position: 'absolute',
380
+ left: 0,
381
+ bottom: 40,
382
+ zIndex: 10,
383
+ gap: 16,
384
+ },
385
+ messageFormAttachments: {
386
+ flexDirection: 'row',
387
+ gap: 8,
388
+ },
389
+ inputErrorMessage: {
390
+ color: theme.colors.statusErrorText,
391
+ fontSize: 14,
392
+ },
227
393
  })
228
394
  }
@@ -1,5 +1,5 @@
1
1
  import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
2
- import { Platform, Pressable, StyleSheet, View, ViewStyle } from 'react-native'
2
+ import { Platform, Pressable, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
3
3
  import ReanimatedSwipeable, {
4
4
  SwipeableMethods,
5
5
  } from 'react-native-gesture-handler/ReanimatedSwipeable'
@@ -22,7 +22,7 @@ export function ConversationActions({
22
22
  children: ReactNode
23
23
  conversation: ConversationResource
24
24
  onPress: () => void
25
- style?: ViewStyle
25
+ style?: StyleProp<ViewStyle>
26
26
  }) {
27
27
  const swipeableRef = useRef<SwipeableMethods>(null)
28
28
  const styles = useStyles()
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { StyleSheet, View } from 'react-native'
2
+ import { StyleSheet, View, ViewStyle } from 'react-native'
3
3
  import { ConversationResource } from '../../types'
4
4
  import { AvatarGroup, Heading, Text, Badge } from '../display'
5
5
  import { formatDatePreview } from '../../utils/date'
@@ -12,12 +12,14 @@ interface ConversationPreviewProps {
12
12
  conversation: ConversationResource
13
13
  onPress: () => void
14
14
  showBadges?: boolean
15
+ style?: ViewStyle
15
16
  }
16
17
 
17
18
  export const ConversationPreview = ({
18
19
  conversation,
19
20
  onPress,
20
21
  showBadges = true,
22
+ style,
21
23
  }: ConversationPreviewProps) => {
22
24
  const styles = useStyles()
23
25
  const {
@@ -32,7 +34,11 @@ export const ConversationPreview = ({
32
34
  } = conversation
33
35
 
34
36
  return (
35
- <ConversationActions conversation={conversation} style={styles.previewRow} onPress={onPress}>
37
+ <ConversationActions
38
+ conversation={conversation}
39
+ style={[styles.previewRow, style]}
40
+ onPress={onPress}
41
+ >
36
42
  <AvatarGroup size="lg" sourceUris={previewAvatarUrls || []} />
37
43
  <View style={styles.conversationBody}>
38
44
  <Heading numberOfLines={1} variant="h3" style={styles.title}>
@@ -0,0 +1,82 @@
1
+ import React from 'react'
2
+ import { StyleSheet, View, ViewStyle } from 'react-native'
3
+ import { useConversations } from '../hooks/use_conversations'
4
+ import { ConversationPreview } from './conversations/conversation_preview'
5
+ import { ConversationRequestArgs } from '../utils/request/conversation'
6
+ import { useTheme } from '../hooks'
7
+ import { Icon, Text } from './display'
8
+
9
+ interface GroupConversationsProps extends Partial<ConversationRequestArgs> {
10
+ limit?: number
11
+ onConversationPress: (conversation: any) => void
12
+ style?: ViewStyle
13
+ ListHeaderComponent?:
14
+ | React.ComponentType<any>
15
+ | React.ReactElement<any, string | React.JSXElementConstructor<any>>
16
+ ListOverflowFooterComponent?:
17
+ | React.ComponentType<any>
18
+ | React.ReactElement<any, string | React.JSXElementConstructor<any>>
19
+ }
20
+
21
+ /**
22
+ * GroupConversations is a component that displays a list of conversations
23
+ * for a specific group.
24
+ *
25
+ * Originally designed for use in CCA.
26
+ */
27
+ export const GroupConversations = ({
28
+ limit,
29
+ onConversationPress,
30
+ style,
31
+ ListHeaderComponent,
32
+ ListOverflowFooterComponent,
33
+ chat_group_graph_id,
34
+ }: GroupConversationsProps) => {
35
+ const styles = useStyles()
36
+ const { conversations = [] } = useConversations({
37
+ chat_group_graph_id,
38
+ group_source_app_name: undefined,
39
+ })
40
+
41
+ return (
42
+ <View style={style}>
43
+ <>{ListHeaderComponent}</>
44
+ {conversations.length === 0 && (
45
+ <View style={styles.listEmpty}>
46
+ <Icon size={24} name="general.textMessage" style={styles.listEmptyIcon} />
47
+ <Text variant="secondary">No conversations found</Text>
48
+ </View>
49
+ )}
50
+ {conversations.slice(0, limit).map(conversation => (
51
+ <ConversationPreview
52
+ style={styles.conversation}
53
+ key={conversation.id}
54
+ showBadges={false}
55
+ conversation={conversation}
56
+ onPress={() => onConversationPress(conversation)}
57
+ />
58
+ ))}
59
+ {conversations.length > (limit || 0) && <>{ListOverflowFooterComponent}</>}
60
+ </View>
61
+ )
62
+ }
63
+
64
+ const useStyles = () => {
65
+ const { colors } = useTheme()
66
+ return StyleSheet.create({
67
+ constainer: {},
68
+ conversation: {
69
+ borderBottomWidth: 0,
70
+ },
71
+ listItem: { color: colors.fillColorNeutral020 },
72
+ listEmpty: {
73
+ justifyContent: 'center',
74
+ alignItems: 'center',
75
+ paddingVertical: 16,
76
+ gap: 8,
77
+ },
78
+ listEmptyIcon: {
79
+ color: colors.fillColorNeutral020,
80
+ },
81
+ })
82
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './conversations/conversations'
2
2
  export * from './page/error_boundary'
3
3
  export * from './display'
4
+ export * from './group_conversation_list'
@@ -36,7 +36,7 @@ const ConversationsContext = createContext<ConversationsContextValue>({
36
36
 
37
37
  export const ConversationsContextProvider = ({
38
38
  children,
39
- args,
39
+ args = {},
40
40
  }: PropsWithChildren<{ args: ConversationFiltersParams }>) => {
41
41
  const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
42
42
  const { chat_group_graph_id, group_source_app_name } = args