@planningcenter/chat-react-native 3.1.0-rc.9 → 3.2.0-rc.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 (298) hide show
  1. package/build/components/conversation/attachments/attachment_card.d.ts +9 -0
  2. package/build/components/conversation/attachments/attachment_card.d.ts.map +1 -0
  3. package/build/components/conversation/attachments/attachment_card.js +35 -0
  4. package/build/components/conversation/attachments/attachment_card.js.map +1 -0
  5. package/build/components/conversation/attachments/audio_attachment.d.ts +6 -0
  6. package/build/components/conversation/attachments/audio_attachment.d.ts.map +1 -0
  7. package/build/components/conversation/attachments/audio_attachment.js +84 -0
  8. package/build/components/conversation/attachments/audio_attachment.js.map +1 -0
  9. package/build/components/conversation/attachments/constants.d.ts +3 -0
  10. package/build/components/conversation/attachments/constants.d.ts.map +1 -0
  11. package/build/components/conversation/attachments/constants.js +4 -0
  12. package/build/components/conversation/attachments/constants.js.map +1 -0
  13. package/build/components/conversation/attachments/download_attachment_button.d.ts +9 -0
  14. package/build/components/conversation/attachments/download_attachment_button.d.ts.map +1 -0
  15. package/build/components/conversation/attachments/download_attachment_button.js +29 -0
  16. package/build/components/conversation/attachments/download_attachment_button.js.map +1 -0
  17. package/build/components/conversation/attachments/expanded_link.d.ts +5 -0
  18. package/build/components/conversation/attachments/expanded_link.d.ts.map +1 -0
  19. package/build/components/conversation/attachments/expanded_link.js +44 -0
  20. package/build/components/conversation/attachments/expanded_link.js.map +1 -0
  21. package/build/components/conversation/attachments/generic_file_attachment.d.ts +7 -0
  22. package/build/components/conversation/attachments/generic_file_attachment.d.ts.map +1 -0
  23. package/build/components/conversation/attachments/generic_file_attachment.js +63 -0
  24. package/build/components/conversation/attachments/generic_file_attachment.js.map +1 -0
  25. package/build/components/conversation/attachments/giphy_attachment.d.ts +6 -0
  26. package/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -0
  27. package/build/components/conversation/attachments/giphy_attachment.js +42 -0
  28. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -0
  29. package/build/components/conversation/attachments/image_attachment.d.ts +5 -0
  30. package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -0
  31. package/build/components/conversation/attachments/image_attachment.js +24 -0
  32. package/build/components/conversation/attachments/image_attachment.js.map +1 -0
  33. package/build/components/conversation/attachments/video_attachment.d.ts +6 -0
  34. package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -0
  35. package/build/components/conversation/attachments/video_attachment.js +64 -0
  36. package/build/components/conversation/attachments/video_attachment.js.map +1 -0
  37. package/build/components/conversation/message.d.ts +1 -1
  38. package/build/components/conversation/message.d.ts.map +1 -1
  39. package/build/components/conversation/message.js +13 -7
  40. package/build/components/conversation/message.js.map +1 -1
  41. package/build/components/conversation/message_attachments.d.ts +6 -0
  42. package/build/components/conversation/message_attachments.d.ts.map +1 -0
  43. package/build/components/conversation/message_attachments.js +52 -0
  44. package/build/components/conversation/message_attachments.js.map +1 -0
  45. package/build/components/conversation/message_markdown.d.ts +7 -0
  46. package/build/components/conversation/message_markdown.d.ts.map +1 -0
  47. package/build/components/conversation/message_markdown.js +38 -0
  48. package/build/components/conversation/message_markdown.js.map +1 -0
  49. package/build/components/conversations/conversation_preview.d.ts +9 -0
  50. package/build/components/conversations/conversation_preview.d.ts.map +1 -0
  51. package/build/components/conversations/conversation_preview.js +58 -0
  52. package/build/components/conversations/conversation_preview.js.map +1 -0
  53. package/build/components/conversations/conversations.d.ts +8 -0
  54. package/build/components/conversations/conversations.d.ts.map +1 -0
  55. package/build/components/conversations/conversations.js +40 -0
  56. package/build/components/conversations/conversations.js.map +1 -0
  57. package/build/components/conversations/unread_count_badge.d.ts +5 -0
  58. package/build/components/conversations/unread_count_badge.d.ts.map +1 -0
  59. package/build/components/conversations/unread_count_badge.js +33 -0
  60. package/build/components/conversations/unread_count_badge.js.map +1 -0
  61. package/build/components/display/badge.d.ts +9 -1
  62. package/build/components/display/badge.d.ts.map +1 -1
  63. package/build/components/display/badge.js +22 -10
  64. package/build/components/display/badge.js.map +1 -1
  65. package/build/components/display/button.d.ts +1 -1
  66. package/build/components/display/button.d.ts.map +1 -1
  67. package/build/components/display/button.js.map +1 -1
  68. package/build/components/display/child_notice.js +2 -2
  69. package/build/components/display/child_notice.js.map +1 -1
  70. package/build/components/display/icon.d.ts.map +1 -1
  71. package/build/components/display/icon.js +10 -8
  72. package/build/components/display/icon.js.map +1 -1
  73. package/build/components/display/text.js +2 -8
  74. package/build/components/display/text.js.map +1 -1
  75. package/build/components/index.d.ts +1 -1
  76. package/build/components/index.d.ts.map +1 -1
  77. package/build/components/index.js +1 -1
  78. package/build/components/index.js.map +1 -1
  79. package/build/components/page/error_boundary.d.ts +1 -1
  80. package/build/hooks/use_api.d.ts +6 -4
  81. package/build/hooks/use_api.d.ts.map +1 -1
  82. package/build/hooks/use_api.js +5 -2
  83. package/build/hooks/use_api.js.map +1 -1
  84. package/build/hooks/use_api_client.d.ts +1 -2
  85. package/build/hooks/use_api_client.d.ts.map +1 -1
  86. package/build/hooks/use_api_client.js.map +1 -1
  87. package/build/hooks/use_conversation.d.ts +76 -0
  88. package/build/hooks/use_conversation.d.ts.map +1 -1
  89. package/build/hooks/use_conversation.js +47 -3
  90. package/build/hooks/use_conversation.js.map +1 -1
  91. package/build/hooks/use_conversation_jolt_events.js +1 -1
  92. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  93. package/build/hooks/use_conversation_messages_jolt_events.js +1 -1
  94. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  95. package/build/hooks/use_conversations.d.ts +2 -3
  96. package/build/hooks/use_conversations.d.ts.map +1 -1
  97. package/build/hooks/use_conversations.js +3 -29
  98. package/build/hooks/use_conversations.js.map +1 -1
  99. package/build/hooks/use_groups.d.ts +214 -0
  100. package/build/hooks/use_groups.d.ts.map +1 -0
  101. package/build/hooks/use_groups.js +22 -0
  102. package/build/hooks/use_groups.js.map +1 -0
  103. package/build/hooks/use_groups_groups.d.ts +208 -0
  104. package/build/hooks/use_groups_groups.d.ts.map +1 -0
  105. package/build/hooks/use_groups_groups.js +18 -0
  106. package/build/hooks/use_groups_groups.js.map +1 -0
  107. package/build/hooks/use_services_team.d.ts +4 -0
  108. package/build/hooks/use_services_team.d.ts.map +1 -0
  109. package/build/hooks/use_services_team.js +22 -0
  110. package/build/hooks/use_services_team.js.map +1 -0
  111. package/build/hooks/use_suspense_api.d.ts +2 -1
  112. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  113. package/build/hooks/use_suspense_api.js +2 -1
  114. package/build/hooks/use_suspense_api.js.map +1 -1
  115. package/build/hooks/use_teams.d.ts +208 -0
  116. package/build/hooks/use_teams.d.ts.map +1 -0
  117. package/build/hooks/use_teams.js +22 -0
  118. package/build/hooks/use_teams.js.map +1 -0
  119. package/build/navigation/index.d.ts +4 -0
  120. package/build/navigation/index.d.ts.map +1 -1
  121. package/build/navigation/index.js +5 -0
  122. package/build/navigation/index.js.map +1 -1
  123. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  124. package/build/screens/conversation_details_screen.js +139 -59
  125. package/build/screens/conversation_details_screen.js.map +1 -1
  126. package/build/screens/conversation_filters/components/conversation_filters.d.ts +3 -0
  127. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -0
  128. package/build/screens/conversation_filters/components/conversation_filters.js +173 -0
  129. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -0
  130. package/build/screens/conversation_filters/components/rows.d.ts +31 -0
  131. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -0
  132. package/build/screens/conversation_filters/components/rows.js +111 -0
  133. package/build/screens/conversation_filters/components/rows.js.map +1 -0
  134. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts +23 -0
  135. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -0
  136. package/build/screens/conversation_filters/context/conversation_filter_context.js +51 -0
  137. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -0
  138. package/build/screens/conversation_filters/filter_types.d.ts +7 -0
  139. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -0
  140. package/build/screens/conversation_filters/filter_types.js +8 -0
  141. package/build/screens/conversation_filters/filter_types.js.map +1 -0
  142. package/build/screens/conversation_filters/group_filters.d.ts +6 -0
  143. package/build/screens/conversation_filters/group_filters.d.ts.map +1 -0
  144. package/build/screens/conversation_filters/group_filters.js +15 -0
  145. package/build/screens/conversation_filters/group_filters.js.map +1 -0
  146. package/build/screens/conversation_filters/hooks/filters.d.ts +415 -0
  147. package/build/screens/conversation_filters/hooks/filters.d.ts.map +1 -0
  148. package/build/screens/conversation_filters/hooks/filters.js +41 -0
  149. package/build/screens/conversation_filters/hooks/filters.js.map +1 -0
  150. package/build/screens/conversation_filters/screen_props.d.ts +13 -0
  151. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -0
  152. package/build/screens/conversation_filters/screen_props.js +2 -0
  153. package/build/screens/conversation_filters/screen_props.js.map +1 -0
  154. package/build/screens/conversation_filters/team_filters.d.ts +6 -0
  155. package/build/screens/conversation_filters/team_filters.d.ts.map +1 -0
  156. package/build/screens/conversation_filters/team_filters.js +15 -0
  157. package/build/screens/conversation_filters/team_filters.js.map +1 -0
  158. package/build/screens/conversation_filters_screen.d.ts +6 -0
  159. package/build/screens/conversation_filters_screen.d.ts.map +1 -0
  160. package/build/screens/conversation_filters_screen.js +125 -0
  161. package/build/screens/conversation_filters_screen.js.map +1 -0
  162. package/build/screens/conversation_screen.js +10 -4
  163. package/build/screens/conversation_screen.js.map +1 -1
  164. package/build/screens/conversations_screen.d.ts +4 -2
  165. package/build/screens/conversations_screen.d.ts.map +1 -1
  166. package/build/screens/conversations_screen.js +139 -11
  167. package/build/screens/conversations_screen.js.map +1 -1
  168. package/build/screens/create/conversation_select_recipients_screen.d.ts.map +1 -1
  169. package/build/screens/create/conversation_select_recipients_screen.js +3 -11
  170. package/build/screens/create/conversation_select_recipients_screen.js.map +1 -1
  171. package/build/types/resources/group_resource.d.ts +4 -2
  172. package/build/types/resources/group_resource.d.ts.map +1 -1
  173. package/build/types/resources/group_resource.js.map +1 -1
  174. package/build/types/resources/index.d.ts +1 -0
  175. package/build/types/resources/index.d.ts.map +1 -1
  176. package/build/types/resources/index.js +1 -0
  177. package/build/types/resources/index.js.map +1 -1
  178. package/build/types/resources/member_ability.d.ts +1 -0
  179. package/build/types/resources/member_ability.d.ts.map +1 -1
  180. package/build/types/resources/member_ability.js.map +1 -1
  181. package/build/types/resources/services/index.d.ts +2 -0
  182. package/build/types/resources/services/index.d.ts.map +1 -0
  183. package/build/types/resources/services/index.js +2 -0
  184. package/build/types/resources/services/index.js.map +1 -0
  185. package/build/types/resources/services/team_resource.d.ts +48 -0
  186. package/build/types/resources/services/team_resource.d.ts.map +1 -0
  187. package/build/types/resources/services/team_resource.js +7 -0
  188. package/build/types/resources/services/team_resource.js.map +1 -0
  189. package/build/utils/index.d.ts +1 -0
  190. package/build/utils/index.d.ts.map +1 -1
  191. package/build/utils/index.js +1 -0
  192. package/build/utils/index.js.map +1 -1
  193. package/build/utils/native_adapters/audio.d.ts +13 -0
  194. package/build/utils/native_adapters/audio.d.ts.map +1 -0
  195. package/build/utils/native_adapters/audio.js +7 -0
  196. package/build/utils/native_adapters/audio.js.map +1 -0
  197. package/build/utils/native_adapters/configuration.d.ts +7 -1
  198. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  199. package/build/utils/native_adapters/configuration.js +17 -1
  200. package/build/utils/native_adapters/configuration.js.map +1 -1
  201. package/build/utils/native_adapters/index.d.ts +2 -0
  202. package/build/utils/native_adapters/index.d.ts.map +1 -1
  203. package/build/utils/native_adapters/index.js +2 -0
  204. package/build/utils/native_adapters/index.js.map +1 -1
  205. package/build/utils/native_adapters/video.d.ts +25 -0
  206. package/build/utils/native_adapters/video.d.ts.map +1 -0
  207. package/build/utils/native_adapters/video.js +7 -0
  208. package/build/utils/native_adapters/video.js.map +1 -0
  209. package/build/utils/parse_simple_markdown.d.ts +7 -0
  210. package/build/utils/parse_simple_markdown.d.ts.map +1 -0
  211. package/build/utils/parse_simple_markdown.js +32 -0
  212. package/build/utils/parse_simple_markdown.js.map +1 -0
  213. package/build/utils/pluralize.d.ts +2 -0
  214. package/build/utils/pluralize.d.ts.map +1 -0
  215. package/build/utils/pluralize.js +10 -0
  216. package/build/utils/pluralize.js.map +1 -0
  217. package/build/utils/request/conversation.d.ts +10 -0
  218. package/build/utils/request/conversation.d.ts.map +1 -0
  219. package/build/utils/request/conversation.js +32 -0
  220. package/build/utils/request/conversation.js.map +1 -0
  221. package/build/utils/request/messages.d.ts +15 -0
  222. package/build/utils/request/messages.d.ts.map +1 -0
  223. package/build/utils/request/messages.js +22 -0
  224. package/build/utils/request/messages.js.map +1 -0
  225. package/build/utils/theme.d.ts +1 -1
  226. package/build/utils/theme.d.ts.map +1 -1
  227. package/build/utils/theme.js +1 -1
  228. package/build/utils/theme.js.map +1 -1
  229. package/package.json +2 -2
  230. package/src/__tests__/utils/parse_simple_markdown.ts +93 -0
  231. package/src/__tests__/utils/pluralize.tsx +17 -0
  232. package/src/components/conversation/attachments/attachment_card.tsx +46 -0
  233. package/src/components/conversation/attachments/audio_attachment.tsx +102 -0
  234. package/src/components/conversation/attachments/constants.ts +5 -0
  235. package/src/components/conversation/attachments/download_attachment_button.tsx +46 -0
  236. package/src/components/conversation/attachments/expanded_link.tsx +54 -0
  237. package/src/components/conversation/attachments/generic_file_attachment.tsx +75 -0
  238. package/src/components/conversation/attachments/giphy_attachment.tsx +56 -0
  239. package/src/components/conversation/attachments/image_attachment.tsx +31 -0
  240. package/src/components/conversation/attachments/video_attachment.tsx +92 -0
  241. package/src/components/conversation/message.tsx +15 -6
  242. package/src/components/conversation/message_attachments.tsx +61 -0
  243. package/src/components/conversation/message_markdown.tsx +52 -0
  244. package/src/components/conversations/conversation_preview.tsx +84 -0
  245. package/src/components/conversations/conversations.tsx +79 -0
  246. package/src/components/conversations/unread_count_badge.tsx +38 -0
  247. package/src/components/display/badge.tsx +41 -10
  248. package/src/components/display/button.tsx +1 -1
  249. package/src/components/display/child_notice.tsx +2 -2
  250. package/src/components/display/icon.tsx +10 -8
  251. package/src/components/display/text.tsx +1 -7
  252. package/src/components/index.tsx +1 -1
  253. package/src/hooks/use_api.ts +11 -10
  254. package/src/hooks/use_api_client.ts +1 -1
  255. package/src/hooks/use_conversation.ts +51 -3
  256. package/src/hooks/use_conversation_jolt_events.ts +1 -1
  257. package/src/hooks/use_conversation_messages_jolt_events.ts +1 -1
  258. package/src/hooks/use_conversations.ts +3 -31
  259. package/src/hooks/use_groups.ts +31 -0
  260. package/src/hooks/use_groups_groups.ts +20 -0
  261. package/src/hooks/use_services_team.ts +30 -0
  262. package/src/hooks/use_suspense_api.ts +4 -4
  263. package/src/hooks/use_teams.ts +25 -0
  264. package/src/navigation/index.tsx +8 -0
  265. package/src/screens/conversation_details_screen.tsx +224 -111
  266. package/src/screens/conversation_filters/components/conversation_filters.tsx +233 -0
  267. package/src/screens/conversation_filters/components/rows.tsx +164 -0
  268. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +65 -0
  269. package/src/screens/conversation_filters/filter_types.ts +6 -0
  270. package/src/screens/conversation_filters/group_filters.tsx +31 -0
  271. package/src/screens/conversation_filters/hooks/filters.ts +68 -0
  272. package/src/screens/conversation_filters/screen_props.ts +15 -0
  273. package/src/screens/conversation_filters/team_filters.tsx +31 -0
  274. package/src/screens/conversation_filters_screen.tsx +152 -0
  275. package/src/screens/conversation_screen.tsx +16 -4
  276. package/src/screens/conversations_screen.tsx +210 -13
  277. package/src/screens/create/conversation_select_recipients_screen.tsx +3 -11
  278. package/src/types/resources/group_resource.ts +5 -2
  279. package/src/types/resources/index.ts +1 -0
  280. package/src/types/resources/member_ability.ts +1 -0
  281. package/src/types/resources/services/index.ts +1 -0
  282. package/src/types/resources/services/team_resource.ts +60 -0
  283. package/src/utils/client/types.d.ts +2 -1
  284. package/src/utils/index.ts +1 -0
  285. package/src/utils/native_adapters/audio.ts +15 -0
  286. package/src/utils/native_adapters/configuration.ts +24 -1
  287. package/src/utils/native_adapters/index.ts +2 -0
  288. package/src/utils/native_adapters/video.ts +30 -0
  289. package/src/utils/parse_simple_markdown.ts +40 -0
  290. package/src/utils/pluralize.ts +11 -0
  291. package/src/utils/request/conversation.ts +46 -0
  292. package/src/utils/request/messages.ts +21 -0
  293. package/src/utils/theme.ts +2 -2
  294. package/build/components/conversations.d.ts +0 -3
  295. package/build/components/conversations.d.ts.map +0 -1
  296. package/build/components/conversations.js +0 -100
  297. package/build/components/conversations.js.map +0 -1
  298. package/src/components/conversations.tsx +0 -144
@@ -0,0 +1,93 @@
1
+ import { parseSimpleMarkdown } from '../../utils/parse_simple_markdown'
2
+
3
+ describe('parseSimpleMarkdown', () => {
4
+ it('parses bold text correctly', () => {
5
+ const input = 'This is **bold** text'
6
+ const result = parseSimpleMarkdown(input)
7
+ expect(result).toEqual([
8
+ { type: 'text', content: 'This is ' },
9
+ { type: 'bold', content: 'bold' },
10
+ { type: 'text', content: ' text' },
11
+ ])
12
+ })
13
+
14
+ it('parses italic text correctly', () => {
15
+ const input = 'This is *italic* text'
16
+ const result = parseSimpleMarkdown(input)
17
+ expect(result).toEqual([
18
+ { type: 'text', content: 'This is ' },
19
+ { type: 'italic', content: 'italic' },
20
+ { type: 'text', content: ' text' },
21
+ ])
22
+ })
23
+
24
+ it('parses links correctly', () => {
25
+ const input = 'Visit https://example.com/page?query=info&number=1#hash. for more info'
26
+ const result = parseSimpleMarkdown(input)
27
+ expect(result).toEqual([
28
+ { type: 'text', content: 'Visit ' },
29
+ { type: 'link', content: 'https://example.com/page?query=info&number=1#hash' },
30
+ { type: 'text', content: '.' },
31
+ { type: 'text', content: ' for more info' },
32
+ ])
33
+ })
34
+
35
+ it('parses mixed formatting correctly', () => {
36
+ const input = 'This is **bold** and *italic* and a link: https://example.com'
37
+ const result = parseSimpleMarkdown(input)
38
+ expect(result).toEqual([
39
+ { type: 'text', content: 'This is ' },
40
+ { type: 'bold', content: 'bold' },
41
+ { type: 'text', content: ' and ' },
42
+ { type: 'italic', content: 'italic' },
43
+ { type: 'text', content: ' and a link: ' },
44
+ { type: 'link', content: 'https://example.com' },
45
+ ])
46
+ })
47
+
48
+ it('parses unordered lists correctly', () => {
49
+ const input = '* Item 1\n* Item 2\n* Item 3'
50
+ const result = parseSimpleMarkdown(input)
51
+ expect(result).toEqual([{ type: 'text', content: '* Item 1\n* Item 2\n* Item 3' }])
52
+ })
53
+
54
+ it('parses ordered lists correctly', () => {
55
+ const input = '1. Item 1\n2. Item 2\n3. Item 3'
56
+ const result = parseSimpleMarkdown(input)
57
+ expect(result).toEqual([{ type: 'text', content: '1. Item 1\n2. Item 2\n3. Item 3' }])
58
+ })
59
+
60
+ it('handles plain text without formatting', () => {
61
+ const input = 'Just plain text'
62
+ const result = parseSimpleMarkdown(input)
63
+ expect(result).toEqual([{ type: 'text', content: 'Just plain text' }])
64
+ })
65
+
66
+ it('handles empty input', () => {
67
+ const input = ''
68
+ const result = parseSimpleMarkdown(input)
69
+ expect(result).toEqual([])
70
+ })
71
+
72
+ it('handles multiple consecutive formatting', () => {
73
+ const input = '**bold** *italic* https://example.com'
74
+ const result = parseSimpleMarkdown(input)
75
+ expect(result).toEqual([
76
+ { type: 'bold', content: 'bold' },
77
+ { type: 'text', content: ' ' },
78
+ { type: 'italic', content: 'italic' },
79
+ { type: 'text', content: ' ' },
80
+ { type: 'link', content: 'https://example.com' },
81
+ ])
82
+ })
83
+
84
+ it('handles unmatched formatting gracefully', () => {
85
+ const input = 'This is **bold and *italic'
86
+ const result = parseSimpleMarkdown(input)
87
+ expect(result).toEqual([
88
+ { type: 'text', content: 'This is ' },
89
+ { type: 'italic', content: '*bold and ' },
90
+ { type: 'text', content: 'italic' },
91
+ ])
92
+ })
93
+ })
@@ -0,0 +1,17 @@
1
+ import { pluralize } from '../../utils'
2
+
3
+ describe('pluralize', () => {
4
+ it('includes count and adds an s', () => {
5
+ expect(pluralize(0, 'kid')).toEqual('0 kids')
6
+ expect(pluralize(1, 'kid')).toEqual('1 kid')
7
+ expect(pluralize(2, 'kid')).toEqual('2 kids')
8
+ expect(pluralize(10, 'kid')).toEqual('10 kids')
9
+ })
10
+ it('can optionally not include count', () => {
11
+ expect(pluralize(1, 'kid', false)).toEqual('kid')
12
+ expect(pluralize(2, 'kid', false)).toEqual('kids')
13
+ })
14
+ it('can convert "person" to "people"', () => {
15
+ expect(pluralize(2, 'person', false)).toEqual('people')
16
+ })
17
+ })
@@ -0,0 +1,46 @@
1
+ import React, { ComponentProps, ReactNode } from 'react'
2
+ import { StyleSheet, View } from 'react-native'
3
+ import { MESSAGE_ATTACHMENT_WIDTH_SINGLE } from './constants'
4
+ import { Text } from '../../display'
5
+ import { tokens } from '../../../vendor/tapestry/tokens'
6
+ import { useTheme } from '../../../hooks'
7
+
8
+ type AttachmentCardProps = ComponentProps<typeof View>
9
+
10
+ export function AttachmentCard({ children, ...props }: AttachmentCardProps) {
11
+ const styles = useStyles()
12
+
13
+ return (
14
+ <View style={styles.card} {...props}>
15
+ {children}
16
+ </View>
17
+ )
18
+ }
19
+
20
+ export function AttachmentCardTitle({ children }: { children: ReactNode }) {
21
+ const styles = useStyles()
22
+
23
+ return (
24
+ <Text style={styles.title} numberOfLines={1} ellipsizeMode="tail" selectable={false}>
25
+ {children}
26
+ </Text>
27
+ )
28
+ }
29
+
30
+ const useStyles = () => {
31
+ const { colors } = useTheme()
32
+
33
+ return StyleSheet.create({
34
+ card: {
35
+ padding: 4,
36
+ backgroundColor: 'white',
37
+ borderRadius: 8,
38
+ minWidth: MESSAGE_ATTACHMENT_WIDTH_SINGLE,
39
+ minHeight: 48,
40
+ },
41
+ title: {
42
+ color: colors.textColorDefaultPrimary,
43
+ fontSize: tokens.fontSizeSm,
44
+ },
45
+ })
46
+ }
@@ -0,0 +1,102 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet } from 'react-native'
3
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
4
+ import { IconButton } from '../../display'
5
+ import { AttachmentCard, AttachmentCardTitle } from './attachment_card'
6
+ import { tokens } from '../../../vendor/tapestry/tokens'
7
+ import { useTheme } from '../../../hooks'
8
+ import { Audio } from '../../../utils/native_adapters'
9
+
10
+ export function AudioAttachment({
11
+ attachment,
12
+ }: {
13
+ attachment: DenormalizedMessageAttachmentResource
14
+ }) {
15
+ const styles = useStyles()
16
+ const { attributes } = attachment
17
+ const { url, filename } = attributes
18
+ const sound = Audio.useAudio(url)
19
+
20
+ function toggleAudio() {
21
+ if (!sound.isPlaying) {
22
+ sound.play()
23
+ } else {
24
+ sound.pause()
25
+ }
26
+ }
27
+
28
+ const progress = sound.duration > 0 ? sound.position / sound.duration : 0
29
+
30
+ function durationText() {
31
+ if (sound.duration > 0) {
32
+ return `${Math.floor(sound.position / 1000)}s / ${Math.floor(sound.duration / 1000)}s`
33
+ } else {
34
+ return 'Loading...'
35
+ }
36
+ }
37
+
38
+ return (
39
+ <AttachmentCard>
40
+ <View style={styles.container}>
41
+ <IconButton
42
+ name={sound.isPlaying ? 'services.pause' : 'services.play'}
43
+ size="md"
44
+ accessibilityLabel={sound.isPlaying ? 'Pause' : 'Play'}
45
+ onPress={toggleAudio}
46
+ disabled={!sound}
47
+ style={styles.button}
48
+ />
49
+ <View style={styles.details}>
50
+ <AttachmentCardTitle>{filename}</AttachmentCardTitle>
51
+ <View style={styles.progressContainer}>
52
+ <View style={[styles.progressBar, { width: `${progress * 100}%` }]} />
53
+ </View>
54
+ <Text style={styles.durationText}>{durationText()}</Text>
55
+ </View>
56
+ </View>
57
+ </AttachmentCard>
58
+ )
59
+ }
60
+
61
+ const useStyles = () => {
62
+ const { colors } = useTheme()
63
+
64
+ return StyleSheet.create({
65
+ container: {
66
+ flexDirection: 'row',
67
+ },
68
+ button: {
69
+ width: 42,
70
+ height: 42,
71
+ justifyContent: 'center',
72
+ alignItems: 'center',
73
+ borderRadius: 25,
74
+ backgroundColor: '#eee',
75
+ borderColor: '#aaa',
76
+ borderWidth: 1,
77
+ },
78
+ buttonText: {
79
+ fontSize: 32,
80
+ lineHeight: 32,
81
+ },
82
+ details: {
83
+ flex: 1,
84
+ marginLeft: 10,
85
+ gap: 5,
86
+ },
87
+ progressContainer: {
88
+ height: 5,
89
+ backgroundColor: '#ccc',
90
+ borderRadius: 2.5,
91
+ overflow: 'hidden',
92
+ },
93
+ progressBar: {
94
+ height: '100%',
95
+ backgroundColor: 'blue',
96
+ },
97
+ durationText: {
98
+ fontSize: tokens.fontSizeXs,
99
+ color: colors.textColorDefaultSecondary,
100
+ },
101
+ })
102
+ }
@@ -0,0 +1,5 @@
1
+ const MESSAGE_BUBBLE_ATTACHMENT_PADDING = 2
2
+
3
+ export const MESSAGE_BUBBLE_WIDTH_SINGLE = 232
4
+ export const MESSAGE_ATTACHMENT_WIDTH_SINGLE =
5
+ MESSAGE_BUBBLE_WIDTH_SINGLE - MESSAGE_BUBBLE_ATTACHMENT_PADDING * 2
@@ -0,0 +1,46 @@
1
+ import React from 'react'
2
+ import { IconButton } from '../../display'
3
+ import { useTheme } from '../../../hooks'
4
+ import { Linking, StyleSheet } from 'react-native'
5
+
6
+ interface DownloadAttachmentButtonProps {
7
+ to: string
8
+ filename: string
9
+ title: string
10
+ }
11
+
12
+ export function DownloadAttachmentButton({ to, title }: DownloadAttachmentButtonProps) {
13
+ const styles = useStyles()
14
+ async function handleDownload() {
15
+ if (to) {
16
+ // Open link in the default web browser
17
+ Linking.openURL(to)
18
+ }
19
+ }
20
+
21
+ return (
22
+ <IconButton
23
+ name="general.fromCloudArrow"
24
+ size="md"
25
+ accessibilityLabel={title}
26
+ onPress={handleDownload}
27
+ style={styles.iconButton}
28
+ />
29
+ )
30
+ }
31
+
32
+ const useStyles = () => {
33
+ const { colors } = useTheme()
34
+
35
+ return StyleSheet.create({
36
+ iconButton: {
37
+ color: colors.fillColorInteractionDefault,
38
+ backgroundColor: colors.fillColorNeutral070,
39
+ borderWidth: 1,
40
+ borderColor: colors.fillColorNeutral060,
41
+ borderRadius: 4,
42
+ width: 24,
43
+ height: 24,
44
+ },
45
+ })
46
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react'
2
+ import { Image, Linking, Pressable, StyleSheet, Text, View } from 'react-native'
3
+ import { useTheme } from '../../../hooks'
4
+ import { tokens } from '../../../vendor/tapestry/tokens'
5
+
6
+ export function ExpandedLink({ attachment }: { attachment: any }) {
7
+ const styles = useStyles()
8
+ const { attributes } = attachment
9
+ const { url, title, description, imageUrl, imageHeight, imageWidth } = attributes
10
+
11
+ const aspectRatio = imageWidth && imageHeight ? imageWidth / imageHeight : 1
12
+
13
+ async function openUrl() {
14
+ if (url) Linking.openURL(url)
15
+ }
16
+
17
+ return (
18
+ <Pressable style={styles.container} onPress={openUrl}>
19
+ {imageUrl && (
20
+ <Image source={{ uri: imageUrl }} style={[styles.image, { aspectRatio }]} alt={title} />
21
+ )}
22
+ {(title || description) && (
23
+ <View style={styles.textWrapper}>
24
+ {title && <Text style={styles.title}>{title}</Text>}
25
+ {description && <Text style={styles.description}>{description}</Text>}
26
+ </View>
27
+ )}
28
+ </Pressable>
29
+ )
30
+ }
31
+
32
+ const useStyles = () => {
33
+ const { colors } = useTheme()
34
+
35
+ return StyleSheet.create({
36
+ container: {},
37
+ image: {
38
+ borderRadius: 8,
39
+ },
40
+ textWrapper: {
41
+ gap: 4,
42
+ padding: 8,
43
+ },
44
+ title: {
45
+ fontSize: tokens.fontSizeSm,
46
+ fontWeight: tokens.fontWeightMedium,
47
+ color: colors.textColorDefaultPrimary,
48
+ },
49
+ description: {
50
+ fontSize: tokens.fontSizeXs,
51
+ color: colors.textColorDefaultPrimary,
52
+ },
53
+ })
54
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react'
2
+ import { Pressable, StyleSheet, View } from 'react-native'
3
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
4
+ import { AttachmentCard, AttachmentCardTitle } from './attachment_card'
5
+ import { Icon } from '../../display'
6
+ import { DownloadAttachmentButton } from './download_attachment_button'
7
+ import { useTheme } from '../../../hooks'
8
+
9
+ export function GenericFileAttachment({
10
+ attachment,
11
+ }: {
12
+ attachment: DenormalizedMessageAttachmentResource
13
+ }) {
14
+ const styles = useStyles()
15
+ const { url, filename, contentType } = attachment.attributes
16
+ const iconName = getAttachmentIconName(contentType)
17
+ const fileTypeLabel = contentType === 'application/pdf' ? 'PDF' : 'file'
18
+
19
+ return (
20
+ <Pressable>
21
+ <AttachmentCard>
22
+ <View style={styles.container}>
23
+ <View style={styles.stack}>
24
+ <Icon name={iconName} />
25
+ <AttachmentCardTitle>{filename}</AttachmentCardTitle>
26
+ </View>
27
+ <DownloadAttachmentButton
28
+ to={url}
29
+ filename={filename}
30
+ title={`Download ${fileTypeLabel}`}
31
+ />
32
+ </View>
33
+ </AttachmentCard>
34
+ </Pressable>
35
+ )
36
+ }
37
+
38
+ const useStyles = () => {
39
+ const { colors } = useTheme()
40
+
41
+ return StyleSheet.create({
42
+ container: {
43
+ padding: 8,
44
+ gap: 4,
45
+ flexDirection: 'row',
46
+ alignItems: 'center',
47
+ justifyContent: 'space-between',
48
+ minHeight: 48,
49
+ },
50
+ stack: {
51
+ gap: 4,
52
+ flexDirection: 'row',
53
+ alignItems: 'center',
54
+ maxWidth: '70%',
55
+ },
56
+ icon: {
57
+ color: colors.textColorDefaultPrimary,
58
+ width: 24,
59
+ height: 24,
60
+ },
61
+ })
62
+ }
63
+
64
+ export function getAttachmentIconName(type: string) {
65
+ const isImage = type.startsWith('image/')
66
+ const isVideo = type.startsWith('video/')
67
+ const isAudio = type.startsWith('audio/')
68
+ const isPdf = type === 'application/pdf'
69
+
70
+ if (isImage) return 'general.outlinedImageFile'
71
+ if (isVideo) return 'general.outlinedVideoFile'
72
+ if (isAudio) return 'general.outlinedMusicFile'
73
+ if (isPdf) return 'general.outlinedPdfFile'
74
+ return 'general.outlinedGenericFile'
75
+ }
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+ import { Text, Image, StyleSheet, Linking, Pressable } from 'react-native'
3
+ import { tokens } from '../../../vendor/tapestry/tokens'
4
+ import { useTheme } from '../../../hooks'
5
+ import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
+
7
+ export function GiphyAttachment({
8
+ attachment,
9
+ }: {
10
+ attachment: DenormalizedGiphyAttachmentResource
11
+ }) {
12
+ const styles = useStyles()
13
+ const { title, titleLink, giphy } = attachment
14
+ const { url, width, height } = giphy.fixedWidth
15
+
16
+ function handlePress() {
17
+ if (titleLink) {
18
+ // Open Giphy link in the default web browser
19
+ Linking.openURL(titleLink)
20
+ }
21
+ }
22
+
23
+ return (
24
+ <Pressable onPress={handlePress} style={styles.container}>
25
+ <Image
26
+ source={{ uri: url }}
27
+ style={[styles.image, { width, height }]}
28
+ accessibilityLabel={title}
29
+ />
30
+ <Text style={styles.link}>
31
+ <Text style={styles.tag}>/giphy</Text> {title}
32
+ </Text>
33
+ </Pressable>
34
+ )
35
+ }
36
+
37
+ const useStyles = () => {
38
+ const { colors } = useTheme()
39
+ return StyleSheet.create({
40
+ container: {
41
+ gap: 4,
42
+ },
43
+ image: {
44
+ borderRadius: 8,
45
+ },
46
+ link: {
47
+ fontSize: tokens.fontSizeSm,
48
+ color: colors.fillColorInteractionDefault,
49
+ paddingHorizontal: 8,
50
+ paddingVertical: 6,
51
+ },
52
+ tag: {
53
+ fontWeight: 'bold',
54
+ },
55
+ })
56
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import { View, Image, StyleSheet } from 'react-native'
3
+
4
+ export function ImageAttachment({ attachment }: { attachment: any }) {
5
+ const styles = useStyles()
6
+ const { attributes } = attachment
7
+ const { url, urlMedium, filename, metadata = {} } = attributes
8
+ const width = metadata.width || 100
9
+ const height = metadata.height || 100
10
+ return (
11
+ <View style={styles.container}>
12
+ <Image
13
+ source={{ uri: urlMedium || url }}
14
+ style={[styles.image, { aspectRatio: width / height }]}
15
+ accessibilityLabel={filename}
16
+ />
17
+ </View>
18
+ )
19
+ }
20
+
21
+ const useStyles = () => {
22
+ return StyleSheet.create({
23
+ container: {
24
+ maxWidth: '100%',
25
+ },
26
+ image: {
27
+ borderRadius: 8,
28
+ minWidth: 200,
29
+ },
30
+ })
31
+ }
@@ -0,0 +1,92 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import { View, StyleSheet, Pressable } from 'react-native'
3
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
4
+ import { MESSAGE_ATTACHMENT_WIDTH_SINGLE } from './constants'
5
+ import { IconButton } from '../../display'
6
+ import { Video, VideoPlayerHandle } from '../../../utils/native_adapters'
7
+
8
+ export function VideoAttachment({
9
+ attachment,
10
+ }: {
11
+ attachment: DenormalizedMessageAttachmentResource
12
+ }) {
13
+ const { attributes } = attachment
14
+ const { width = MESSAGE_ATTACHMENT_WIDTH_SINGLE, height = MESSAGE_ATTACHMENT_WIDTH_SINGLE } =
15
+ attributes.metadata || {}
16
+ const { url } = attributes
17
+
18
+ const videoRef = useRef<VideoPlayerHandle>(null)
19
+ const [isOpen, setIsOpen] = useState(false)
20
+
21
+ function openVideo() {
22
+ if (!isOpen && videoRef.current) {
23
+ videoRef.current.presentFullscreenPlayer()
24
+ videoRef.current.play()
25
+ setIsOpen(true)
26
+ }
27
+ }
28
+
29
+ function onFullscreenPlayerWillDismiss() {
30
+ videoRef.current?.pause()
31
+ setIsOpen(false)
32
+ }
33
+
34
+ const viewRef = useRef<View>(null)
35
+
36
+ return (
37
+ <View style={styles.container} ref={viewRef}>
38
+ <Pressable onPress={openVideo}>
39
+ <Video.Player
40
+ ref={videoRef}
41
+ source={{ uri: url }}
42
+ aspectRatio={width / height}
43
+ style={styles.video}
44
+ onFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
45
+ />
46
+ {!isOpen && (
47
+ <View style={styles.playButtonWrapper}>
48
+ <IconButton
49
+ name="services.play"
50
+ size="md"
51
+ accessibilityLabel="Play Video"
52
+ onPress={openVideo}
53
+ style={styles.button}
54
+ />
55
+ </View>
56
+ )}
57
+ </Pressable>
58
+ </View>
59
+ )
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ container: {
64
+ maxWidth: 320,
65
+ maxHeight: 320,
66
+ },
67
+ video: {
68
+ width: '100%',
69
+ height: 'auto',
70
+ backgroundColor: 'black',
71
+ borderRadius: 8,
72
+ },
73
+ playButtonWrapper: {
74
+ position: 'absolute',
75
+ top: 0,
76
+ left: 0,
77
+ right: 0,
78
+ bottom: 0,
79
+ justifyContent: 'center',
80
+ alignItems: 'center',
81
+ },
82
+ button: {
83
+ width: 42,
84
+ height: 42,
85
+ justifyContent: 'center',
86
+ alignItems: 'center',
87
+ borderRadius: 25,
88
+ backgroundColor: '#eee',
89
+ borderColor: '#aaa',
90
+ borderWidth: 1,
91
+ },
92
+ })
@@ -8,6 +8,9 @@ import { Avatar, Text } from '../../components/display'
8
8
  import { useTheme } from '../../hooks'
9
9
  import { MessageResource } from '../../types'
10
10
  import { ReactionCountResource } from '../../types/resources/reaction'
11
+ import { MessageAttachments } from './message_attachments'
12
+ import ErrorBoundary from '../page/error_boundary'
13
+ import { MessageMarkdown } from './message_markdown'
11
14
 
12
15
  /** Message
13
16
  * Component for display of a message within a conversation list
@@ -29,7 +32,6 @@ export function Message(props: MessageResource & { conversation_id: number }) {
29
32
  reaction_value: reaction.value,
30
33
  })
31
34
  }
32
- if (!text) return null
33
35
 
34
36
  return (
35
37
  <View style={styles.message}>
@@ -41,7 +43,14 @@ export function Message(props: MessageResource & { conversation_id: number }) {
41
43
  <View style={styles.messageContent}>
42
44
  {!props.mine && <Text variant="tertiary">{props.author.name}</Text>}
43
45
  <PlatformPressable style={styles.messageBubble} onLongPress={handleMessagePress}>
44
- <Text style={styles.messageText}>{text}</Text>
46
+ <ErrorBoundary>
47
+ <MessageAttachments attachments={props.attachments} />
48
+ </ErrorBoundary>
49
+ {text && (
50
+ <View style={styles.messageText}>
51
+ <MessageMarkdown text={text} />
52
+ </View>
53
+ )}
45
54
  </PlatformPressable>
46
55
  <View style={styles.messageReactions}>
47
56
  {reactionCounts.map(reaction => (
@@ -76,12 +85,12 @@ const useMessageStyles = ({ mine }: MessageResource) => {
76
85
  },
77
86
  messageBubble: {
78
87
  backgroundColor: mine ? activeColor : colors.fillColorNeutral070,
79
- borderRadius: 12,
80
- paddingVertical: 6,
81
- paddingHorizontal: 8,
88
+ borderRadius: 8,
89
+ maxWidth: 232,
82
90
  },
83
91
  messageText: {
84
- color: colors.textColorDefaultPrimary,
92
+ paddingVertical: 6,
93
+ paddingHorizontal: 8,
85
94
  },
86
95
  messageReactions: {
87
96
  flexDirection: 'row',