@planningcenter/chat-react-native 4.4.1-qa-staging.13 → 4.4.1-qa-staging.15

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 (108) hide show
  1. package/build/components/conversation/message.d.ts +2 -1
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +190 -161
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_list.d.ts.map +1 -1
  6. package/build/components/conversation/message_list.js +35 -31
  7. package/build/components/conversation/message_list.js.map +1 -1
  8. package/build/components/conversation/message_reaction.d.ts +2 -1
  9. package/build/components/conversation/message_reaction.d.ts.map +1 -1
  10. package/build/components/conversation/message_reaction.js +67 -60
  11. package/build/components/conversation/message_reaction.js.map +1 -1
  12. package/build/components/conversation/messages_disabled_banners.d.ts +7 -5
  13. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  14. package/build/components/conversation/messages_disabled_banners.js +15 -14
  15. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  16. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  17. package/build/components/conversation/reply_shadow_message.js +84 -83
  18. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  19. package/build/components/conversation/system_message.d.ts +2 -1
  20. package/build/components/conversation/system_message.d.ts.map +1 -1
  21. package/build/components/conversation/system_message.js +87 -65
  22. package/build/components/conversation/system_message.js.map +1 -1
  23. package/build/components/display/filter_dropdown.d.ts +13 -0
  24. package/build/components/display/filter_dropdown.d.ts.map +1 -0
  25. package/build/components/display/filter_dropdown.js +188 -0
  26. package/build/components/display/filter_dropdown.js.map +1 -0
  27. package/build/components/display/index.d.ts +1 -0
  28. package/build/components/display/index.d.ts.map +1 -1
  29. package/build/components/display/index.js +1 -0
  30. package/build/components/display/index.js.map +1 -1
  31. package/build/hooks/use_conversation.d.ts +73 -1
  32. package/build/hooks/use_conversation.d.ts.map +1 -1
  33. package/build/hooks/use_conversation.js +122 -3
  34. package/build/hooks/use_conversation.js.map +1 -1
  35. package/build/hooks/use_unarchive_conversation_action.d.ts +7 -0
  36. package/build/hooks/use_unarchive_conversation_action.d.ts.map +1 -0
  37. package/build/hooks/use_unarchive_conversation_action.js +52 -0
  38. package/build/hooks/use_unarchive_conversation_action.js.map +1 -0
  39. package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
  40. package/build/screens/attachment_actions/attachment_actions_screen.js +84 -61
  41. package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
  42. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  43. package/build/screens/conversation_details_screen.js +487 -393
  44. package/build/screens/conversation_details_screen.js.map +1 -1
  45. package/build/screens/conversation_screen.d.ts.map +1 -1
  46. package/build/screens/conversation_screen.js +12 -9
  47. package/build/screens/conversation_screen.js.map +1 -1
  48. package/build/screens/design_system_screen.d.ts.map +1 -1
  49. package/build/screens/design_system_screen.js +275 -215
  50. package/build/screens/design_system_screen.js.map +1 -1
  51. package/build/screens/message_actions_screen.d.ts.map +1 -1
  52. package/build/screens/message_actions_screen.js +112 -92
  53. package/build/screens/message_actions_screen.js.map +1 -1
  54. package/build/types/resources/member_ability.d.ts +2 -0
  55. package/build/types/resources/member_ability.d.ts.map +1 -1
  56. package/build/types/resources/member_ability.js.map +1 -1
  57. package/build/utils/archived_conversation.d.ts +5 -0
  58. package/build/utils/archived_conversation.d.ts.map +1 -0
  59. package/build/utils/archived_conversation.js +5 -0
  60. package/build/utils/archived_conversation.js.map +1 -0
  61. package/build/utils/index.d.ts +1 -0
  62. package/build/utils/index.d.ts.map +1 -1
  63. package/build/utils/index.js +1 -0
  64. package/build/utils/index.js.map +1 -1
  65. package/build/utils/native_adapters/configuration.d.ts +3 -0
  66. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  67. package/build/utils/native_adapters/configuration.js +3 -0
  68. package/build/utils/native_adapters/configuration.js.map +1 -1
  69. package/build/utils/native_adapters/index.d.ts +1 -0
  70. package/build/utils/native_adapters/index.d.ts.map +1 -1
  71. package/build/utils/native_adapters/index.js +1 -0
  72. package/build/utils/native_adapters/index.js.map +1 -1
  73. package/build/utils/native_adapters/menu.d.ts +40 -0
  74. package/build/utils/native_adapters/menu.d.ts.map +1 -0
  75. package/build/utils/native_adapters/menu.js +55 -0
  76. package/build/utils/native_adapters/menu.js.map +1 -0
  77. package/package.json +3 -3
  78. package/src/__tests__/screens/message_actions_screen.test.tsx +65 -2
  79. package/src/components/conversation/__tests__/message_list.test.tsx +68 -2
  80. package/src/components/conversation/__tests__/system_message.test.tsx +3 -1
  81. package/src/components/conversation/message.tsx +11 -14
  82. package/src/components/conversation/message_list.tsx +9 -1
  83. package/src/components/conversation/message_reaction.tsx +7 -2
  84. package/src/components/conversation/messages_disabled_banners.tsx +17 -18
  85. package/src/components/conversation/reply_shadow_message.tsx +9 -8
  86. package/src/components/conversation/system_message.tsx +24 -10
  87. package/src/components/display/__tests__/filter_dropdown.test.tsx +106 -0
  88. package/src/components/display/filter_dropdown.tsx +123 -0
  89. package/src/components/display/index.ts +1 -0
  90. package/src/hooks/__tests__/use_conversation_archive.test.tsx +130 -0
  91. package/src/hooks/__tests__/use_conversation_jolt_events.test.tsx +74 -0
  92. package/src/hooks/__tests__/use_unarchive_conversation_action.test.tsx +67 -0
  93. package/src/hooks/use_conversation.ts +41 -3
  94. package/src/hooks/use_unarchive_conversation_action.ts +24 -0
  95. package/src/screens/__tests__/conversation_details_screen.test.tsx +289 -0
  96. package/src/screens/attachment_actions/__tests__/attachment_actions_screen.test.tsx +72 -0
  97. package/src/screens/attachment_actions/attachment_actions_screen.tsx +3 -1
  98. package/src/screens/conversation_details_screen.tsx +78 -14
  99. package/src/screens/conversation_screen.tsx +3 -1
  100. package/src/screens/design_system_screen.tsx +26 -0
  101. package/src/screens/message_actions_screen.tsx +29 -20
  102. package/src/types/resources/member_ability.ts +2 -0
  103. package/src/utils/archived_conversation.ts +13 -0
  104. package/src/utils/index.ts +1 -0
  105. package/src/utils/native_adapters/__tests__/menu.test.tsx +42 -0
  106. package/src/utils/native_adapters/configuration.ts +5 -0
  107. package/src/utils/native_adapters/index.ts +1 -0
  108. package/src/utils/native_adapters/menu.tsx +68 -0
@@ -1 +1 @@
1
- {"version":3,"file":"configuration.js","names":["AsyncStorage","Linking","RNLinking","AudioAdapter","ClipboardAdapter","DocumentPickerAdapter","HapticAdapter","ImagePickerAdapter","LinkingAdapter","LogAdapter","StorageAdapter","VideoAdapter","ChatAdapters","configure","configurations","Clipboard","clipboard","Audio","audio","Video","video","ImagePicker","imagePicker","DocumentPicker","documentPicker","Log","log","linking","Haptic","haptic","methodMissing","console","warn","getStringAsync","setStringAsync","_","useAudio","Player","Object","assign","$$typeof","Symbol","for","openCameraAsync","canceled","assets","openImageLibraryAsync","Storage","getItem","setItem","removeItem"],"sources":["../../../src/utils/native_adapters/configuration.ts"],"sourcesContent":["import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { Linking as RNLinking } from 'react-native'\nimport { AudioAdapter } from './audio'\nimport { ClipboardAdapter } from './clipboard'\nimport { DocumentPickerAdapter } from './document_picker'\nimport { HapticAdapter } from './haptic'\nimport { ImagePickerAdapter } from './image_picker'\nimport { LinkingAdapter } from './linking'\nimport { LogAdapter } from './log'\nimport { StorageAdapter } from './storage_adapter'\nimport { VideoAdapter } from './video'\n\ntype ChatConfigurations = {\n clipboard: ClipboardAdapter\n audio: AudioAdapter\n video: VideoAdapter\n imagePicker: ImagePickerAdapter\n documentPicker?: DocumentPickerAdapter\n log?: LogAdapter\n linking?: LinkingAdapter\n haptic?: HapticAdapter\n}\n\nexport class ChatAdapters {\n static configure(configurations: ChatConfigurations) {\n Clipboard = configurations.clipboard\n Audio = configurations.audio\n Video = configurations.video\n ImagePicker = configurations.imagePicker\n DocumentPicker = configurations.documentPicker || new DocumentPickerAdapter()\n Log = configurations.log || new LogAdapter()\n Linking = configurations.linking || new LinkingAdapter(RNLinking)\n Haptic = configurations.haptic || new HapticAdapter()\n }\n}\n\nconst methodMissing = () => {\n console.warn('ChatAdapters.configure() must be called before using any adapters')\n}\n\nexport let Clipboard: ClipboardAdapter = new ClipboardAdapter({\n getStringAsync: async () => {\n methodMissing()\n return ''\n },\n setStringAsync: async (_: string) => methodMissing(),\n})\n\nexport let Audio: AudioAdapter = new AudioAdapter({\n useAudio: (_: string) => {\n methodMissing()\n return {} as any\n },\n})\n\nexport let Video: VideoAdapter = new VideoAdapter({\n Player: Object.assign(\n () => {\n methodMissing()\n return null\n },\n { $$typeof: Symbol.for('react.forward_ref') }\n ),\n})\n\nexport let ImagePicker: ImagePickerAdapter = new ImagePickerAdapter({\n openCameraAsync: async () => {\n methodMissing()\n return { canceled: true, assets: null }\n },\n openImageLibraryAsync: async () => {\n methodMissing()\n return { canceled: true, assets: null }\n },\n})\n\nexport let DocumentPicker: DocumentPickerAdapter = new DocumentPickerAdapter()\n\nexport let Log: LogAdapter = new LogAdapter()\n\nexport let Linking: LinkingAdapter = new LinkingAdapter(RNLinking)\n\nexport let Haptic: HapticAdapter = new HapticAdapter()\n\nexport let Storage = new StorageAdapter({\n getItem: AsyncStorage.getItem,\n setItem: AsyncStorage.setItem,\n removeItem: AsyncStorage.removeItem,\n})\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"configuration.js","names":["AsyncStorage","Linking","RNLinking","AudioAdapter","ClipboardAdapter","DocumentPickerAdapter","HapticAdapter","ImagePickerAdapter","LinkingAdapter","LogAdapter","MenuAdapter","StorageAdapter","VideoAdapter","ChatAdapters","configure","configurations","Clipboard","clipboard","Audio","audio","Video","video","ImagePicker","imagePicker","DocumentPicker","documentPicker","Log","log","linking","Haptic","haptic","Menu","menu","methodMissing","console","warn","getStringAsync","setStringAsync","_","useAudio","Player","Object","assign","$$typeof","Symbol","for","openCameraAsync","canceled","assets","openImageLibraryAsync","Storage","getItem","setItem","removeItem"],"sources":["../../../src/utils/native_adapters/configuration.ts"],"sourcesContent":["import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { Linking as RNLinking } from 'react-native'\nimport { AudioAdapter } from './audio'\nimport { ClipboardAdapter } from './clipboard'\nimport { DocumentPickerAdapter } from './document_picker'\nimport { HapticAdapter } from './haptic'\nimport { ImagePickerAdapter } from './image_picker'\nimport { LinkingAdapter } from './linking'\nimport { LogAdapter } from './log'\nimport { MenuAdapter } from './menu'\nimport { StorageAdapter } from './storage_adapter'\nimport { VideoAdapter } from './video'\n\ntype ChatConfigurations = {\n clipboard: ClipboardAdapter\n audio: AudioAdapter\n video: VideoAdapter\n imagePicker: ImagePickerAdapter\n documentPicker?: DocumentPickerAdapter\n log?: LogAdapter\n linking?: LinkingAdapter\n haptic?: HapticAdapter\n menu?: MenuAdapter\n}\n\nexport class ChatAdapters {\n static configure(configurations: ChatConfigurations) {\n Clipboard = configurations.clipboard\n Audio = configurations.audio\n Video = configurations.video\n ImagePicker = configurations.imagePicker\n DocumentPicker = configurations.documentPicker || new DocumentPickerAdapter()\n Log = configurations.log || new LogAdapter()\n Linking = configurations.linking || new LinkingAdapter(RNLinking)\n Haptic = configurations.haptic || new HapticAdapter()\n Menu = configurations.menu || new MenuAdapter()\n }\n}\n\nconst methodMissing = () => {\n console.warn('ChatAdapters.configure() must be called before using any adapters')\n}\n\nexport let Clipboard: ClipboardAdapter = new ClipboardAdapter({\n getStringAsync: async () => {\n methodMissing()\n return ''\n },\n setStringAsync: async (_: string) => methodMissing(),\n})\n\nexport let Audio: AudioAdapter = new AudioAdapter({\n useAudio: (_: string) => {\n methodMissing()\n return {} as any\n },\n})\n\nexport let Video: VideoAdapter = new VideoAdapter({\n Player: Object.assign(\n () => {\n methodMissing()\n return null\n },\n { $$typeof: Symbol.for('react.forward_ref') }\n ),\n})\n\nexport let ImagePicker: ImagePickerAdapter = new ImagePickerAdapter({\n openCameraAsync: async () => {\n methodMissing()\n return { canceled: true, assets: null }\n },\n openImageLibraryAsync: async () => {\n methodMissing()\n return { canceled: true, assets: null }\n },\n})\n\nexport let DocumentPicker: DocumentPickerAdapter = new DocumentPickerAdapter()\n\nexport let Log: LogAdapter = new LogAdapter()\n\nexport let Linking: LinkingAdapter = new LinkingAdapter(RNLinking)\n\nexport let Haptic: HapticAdapter = new HapticAdapter()\n\nexport let Menu: MenuAdapter = new MenuAdapter()\n\nexport let Storage = new StorageAdapter({\n getItem: AsyncStorage.getItem,\n setItem: AsyncStorage.setItem,\n removeItem: AsyncStorage.removeItem,\n})\n"],"mappings":"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","ignoreList":[]}
@@ -5,6 +5,7 @@ export * from './document_picker';
5
5
  export * from './image_picker';
6
6
  export * from './linking';
7
7
  export * from './log';
8
+ export * from './menu';
8
9
  export * from './video';
9
10
  export * from './haptic';
10
11
  export * from './storage_adapter';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,QAAQ,CAAA;AACtB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,mBAAmB,CAAA"}
@@ -5,6 +5,7 @@ export * from './document_picker';
5
5
  export * from './image_picker';
6
6
  export * from './linking';
7
7
  export * from './log';
8
+ export * from './menu';
8
9
  export * from './video';
9
10
  export * from './haptic';
10
11
  export * from './storage_adapter';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/utils/native_adapters/index.ts"],"sourcesContent":["export * from './audio'\nexport * from './clipboard'\nexport * from './configuration'\nexport * from './document_picker'\nexport * from './image_picker'\nexport * from './linking'\nexport * from './log'\nexport * from './video'\nexport * from './haptic'\nexport * from './storage_adapter'\n"],"mappings":"AAAA,cAAc,SAAS;AACvB,cAAc,aAAa;AAC3B,cAAc,iBAAiB;AAC/B,cAAc,mBAAmB;AACjC,cAAc,gBAAgB;AAC9B,cAAc,WAAW;AACzB,cAAc,OAAO;AACrB,cAAc,SAAS;AACvB,cAAc,UAAU;AACxB,cAAc,mBAAmB","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/utils/native_adapters/index.ts"],"sourcesContent":["export * from './audio'\nexport * from './clipboard'\nexport * from './configuration'\nexport * from './document_picker'\nexport * from './image_picker'\nexport * from './linking'\nexport * from './log'\nexport * from './menu'\nexport * from './video'\nexport * from './haptic'\nexport * from './storage_adapter'\n"],"mappings":"AAAA,cAAc,SAAS;AACvB,cAAc,aAAa;AAC3B,cAAc,iBAAiB;AAC/B,cAAc,mBAAmB;AACjC,cAAc,gBAAgB;AAC9B,cAAc,WAAW;AACzB,cAAc,OAAO;AACrB,cAAc,QAAQ;AACtB,cAAc,SAAS;AACvB,cAAc,UAAU;AACxB,cAAc,mBAAmB","ignoreList":[]}
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export type MenuActionState = 'on' | 'off';
4
+ export interface MenuActionAttributes {
5
+ destructive?: boolean;
6
+ disabled?: boolean;
7
+ hidden?: boolean;
8
+ }
9
+ export interface MenuAction {
10
+ id?: string;
11
+ title: string;
12
+ state?: MenuActionState;
13
+ attributes?: MenuActionAttributes;
14
+ subactions?: MenuAction[];
15
+ displayInline?: boolean;
16
+ }
17
+ export interface MenuActionEvent {
18
+ nativeEvent: {
19
+ event: string;
20
+ };
21
+ }
22
+ export interface MenuViewProps {
23
+ actions: MenuAction[];
24
+ onPressAction?: (event: MenuActionEvent) => void;
25
+ onOpenMenu?: () => void;
26
+ onCloseMenu?: () => void;
27
+ shouldOpenOnLongPress?: boolean;
28
+ title?: string;
29
+ style?: StyleProp<ViewStyle>;
30
+ testID?: string;
31
+ children?: React.ReactNode;
32
+ }
33
+ export type MenuView = React.ComponentType<MenuViewProps>;
34
+ export declare class MenuAdapter {
35
+ View: MenuView;
36
+ constructor(methods?: {
37
+ View: MenuView;
38
+ });
39
+ }
40
+ //# sourceMappingURL=menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/menu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAA;AACxC,OAAO,EAAE,SAAS,EAAQ,SAAS,EAAE,MAAM,cAAc,CAAA;AASzD,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,KAAK,CAAA;AAE1C,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,UAAU,CAAC,EAAE,oBAAoB,CAAA;IACjC,UAAU,CAAC,EAAE,UAAU,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED,MAAM,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,CAAA;AAkBzD,qBAAa,WAAW;IACtB,IAAI,EAAE,QAAQ,CAAA;gBAEF,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE;CAGzC"}
@@ -0,0 +1,55 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import React, { useEffect } from 'react';
3
+ import { View } from 'react-native';
4
+
5
+ // =================================
6
+ // ====== Types ====================
7
+ // =================================
8
+
9
+ // Mirrors the props shared by `@expo/ui/community/menu` and
10
+ // `@react-native-menu/menu`, so a host can pass either `MenuView` straight in.
11
+ import { jsx as _jsx } from "react/jsx-runtime";
12
+ // =================================
13
+ // ====== Adapter ==================
14
+ // =================================
15
+
16
+ function UnconfiguredMenuView(t0) {
17
+ const $ = _c(5);
18
+ const {
19
+ children,
20
+ style,
21
+ testID
22
+ } = t0;
23
+ let t1;
24
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
+ t1 = [];
26
+ $[0] = t1;
27
+ } else {
28
+ t1 = $[0];
29
+ }
30
+ useEffect(_temp, t1);
31
+ let t2;
32
+ if ($[1] !== children || $[2] !== style || $[3] !== testID) {
33
+ t2 = /*#__PURE__*/_jsx(View, {
34
+ style: style,
35
+ testID: testID,
36
+ children: children
37
+ });
38
+ $[1] = children;
39
+ $[2] = style;
40
+ $[3] = testID;
41
+ $[4] = t2;
42
+ } else {
43
+ t2 = $[4];
44
+ }
45
+ return t2;
46
+ }
47
+ function _temp() {
48
+ console.warn("ChatAdapters.configure() must include a menu adapter before rendering a menu");
49
+ }
50
+ export class MenuAdapter {
51
+ constructor(methods) {
52
+ this.View = methods?.View ?? UnconfiguredMenuView;
53
+ }
54
+ }
55
+ //# sourceMappingURL=menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.js","names":["React","useEffect","View","jsx","_jsx","UnconfiguredMenuView","t0","$","_c","children","style","testID","t1","Symbol","for","_temp","t2","console","warn","MenuAdapter","constructor","methods"],"sources":["../../../src/utils/native_adapters/menu.tsx"],"sourcesContent":["import React, { useEffect } from 'react'\nimport { StyleProp, View, ViewStyle } from 'react-native'\n\n// =================================\n// ====== Types ====================\n// =================================\n\n// Mirrors the props shared by `@expo/ui/community/menu` and\n// `@react-native-menu/menu`, so a host can pass either `MenuView` straight in.\n\nexport type MenuActionState = 'on' | 'off'\n\nexport interface MenuActionAttributes {\n destructive?: boolean\n disabled?: boolean\n hidden?: boolean\n}\n\nexport interface MenuAction {\n id?: string\n title: string\n state?: MenuActionState\n attributes?: MenuActionAttributes\n subactions?: MenuAction[]\n displayInline?: boolean\n}\n\nexport interface MenuActionEvent {\n nativeEvent: { event: string }\n}\n\nexport interface MenuViewProps {\n actions: MenuAction[]\n onPressAction?: (event: MenuActionEvent) => void\n onOpenMenu?: () => void\n onCloseMenu?: () => void\n shouldOpenOnLongPress?: boolean\n title?: string\n style?: StyleProp<ViewStyle>\n testID?: string\n children?: React.ReactNode\n}\n\nexport type MenuView = React.ComponentType<MenuViewProps>\n\n// =================================\n// ====== Adapter ==================\n// =================================\n\nfunction UnconfiguredMenuView({ children, style, testID }: MenuViewProps) {\n useEffect(() => {\n console.warn('ChatAdapters.configure() must include a menu adapter before rendering a menu')\n }, [])\n\n return (\n <View style={style} testID={testID}>\n {children}\n </View>\n )\n}\n\nexport class MenuAdapter {\n View: MenuView\n\n constructor(methods?: { View: MenuView }) {\n this.View = methods?.View ?? UnconfiguredMenuView\n }\n}\n"],"mappings":";AAAA,OAAOA,KAAK,IAAIC,SAAS,QAAQ,OAAO;AACxC,SAAoBC,IAAI,QAAmB,cAAc;;AAEzD;AACA;AACA;;AAEA;AACA;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAqCA;AACA;AACA;;AAEA,SAAAC,qBAAAC,EAAA;EAAA,MAAAC,CAAA,GAAAC,EAAA;EAA8B;IAAAC,QAAA;IAAAC,KAAA;IAAAC;EAAA,IAAAL,EAA0C;EAAA,IAAAM,EAAA;EAAA,IAAAL,CAAA,QAAAM,MAAA,CAAAC,GAAA;IAGnEF,EAAA,KAAE;IAAAL,CAAA,MAAAK,EAAA;EAAA;IAAAA,EAAA,GAAAL,CAAA;EAAA;EAFLN,SAAS,CAACc,KAET,EAAEH,EAAE,CAAC;EAAA,IAAAI,EAAA;EAAA,IAAAT,CAAA,QAAAE,QAAA,IAAAF,CAAA,QAAAG,KAAA,IAAAH,CAAA,QAAAI,MAAA;IAGJK,EAAA,gBAAAZ,IAAA,CAACF,IAAI;MAAQQ,KAAK,EAALA,KAAK;MAAUC,MAAM,EAANA,MAAM;MAAAF,QAAA,EAC/BA;IAAQ,CACL,CAAC;IAAAF,CAAA,MAAAE,QAAA;IAAAF,CAAA,MAAAG,KAAA;IAAAH,CAAA,MAAAI,MAAA;IAAAJ,CAAA,MAAAS,EAAA;EAAA;IAAAA,EAAA,GAAAT,CAAA;EAAA;EAAA,OAFPS,EAEO;AAAA;AARX,SAAAD,MAAA;EAEIE,OAAO,CAAAC,IAAK,CAAC,8EAA8E,CAAC;AAAA;AAUhG,OAAO,MAAMC,WAAW,CAAC;EAGvBC,WAAWA,CAACC,OAA4B,EAAE;IACxC,IAAI,CAACnB,IAAI,GAAGmB,OAAO,EAAEnB,IAAI,IAAIG,oBAAoB;EACnD;AACF","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "4.4.1-qa-staging.13",
3
+ "version": "4.4.1-qa-staging.15",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
35
35
  "@fortawesome/react-native-fontawesome": "^0.3.2",
36
- "@planningcenter/emoji-keyboard": "4.4.1-qa-staging.13",
36
+ "@planningcenter/emoji-keyboard": "4.4.1-qa-staging.15",
37
37
  "lodash-inflection": "^1.5.0"
38
38
  },
39
39
  "peerDependencies": {
@@ -84,5 +84,5 @@
84
84
  "sf-symbols-typescript": "^2.2.0",
85
85
  "typescript": "~6.0.3"
86
86
  },
87
- "gitHead": "b6fa6a1c2ed0ab85c232050525ec3253be762dff"
87
+ "gitHead": "810a097ac7d4e1d4eeb6ea64a0516e62dd8e0a2c"
88
88
  }
@@ -9,7 +9,7 @@ import {
9
9
  MessageActionsScreen,
10
10
  MessageActionsScreenProps,
11
11
  } from '../../screens/message_actions_screen'
12
- import { MessageResource } from '../../types'
12
+ import { ConversationResource, MessageResource } from '../../types'
13
13
  import {
14
14
  AudioAdapter,
15
15
  ChatAdapters,
@@ -36,7 +36,15 @@ const mockMessage: MessageResource = {
36
36
  messageType: 'user',
37
37
  }
38
38
 
39
+ const myMessageWithLinkPreview = {
40
+ ...mockMessage,
41
+ mine: true,
42
+ attachments: [{ type: 'ExpandedLink', id: '9' }],
43
+ } as MessageResource
44
+
39
45
  let mockCurrentMessage = mockMessage
46
+ let mockConversation = { id: 1, disabled: false } as ConversationResource
47
+ let mockEnabledFeatures: string[] = []
40
48
 
41
49
  const mockGoBack = jest.fn()
42
50
  jest.mock('@react-navigation/native', () => ({
@@ -44,9 +52,15 @@ jest.mock('@react-navigation/native', () => ({
44
52
  useNavigation: () => ({ navigate: jest.fn(), dispatch: jest.fn(), goBack: mockGoBack }),
45
53
  }))
46
54
  jest.mock('../../hooks/use_api_client', () => ({ useApiClient: () => ({}) }))
55
+ jest.mock('../../hooks/use_conversation', () => ({
56
+ useConversation: () => ({ data: mockConversation }),
57
+ }))
47
58
  jest.mock('../../hooks/use_features', () => ({
48
59
  ...jest.requireActual('../../hooks/use_features'),
49
- useFeatures: () => ({ features: [], featureEnabled: () => false }),
60
+ useFeatures: () => ({
61
+ features: [],
62
+ featureEnabled: (name: string) => mockEnabledFeatures.includes(name),
63
+ }),
50
64
  }))
51
65
  jest.mock('../../hooks/use_message_reaction_toggle', () => ({
52
66
  useMessageReactionToggle: () => ({ handleReactionToggle: jest.fn(), isPending: false }),
@@ -108,6 +122,55 @@ describe('MessageActionsScreen', () => {
108
122
  beforeEach(() => {
109
123
  jest.clearAllMocks()
110
124
  mockCurrentMessage = mockMessage
125
+ mockConversation = { id: 1, disabled: false } as ConversationResource
126
+ mockEnabledFeatures = []
127
+ })
128
+
129
+ it('offers Delete on my own message', () => {
130
+ mockCurrentMessage = { ...mockMessage, mine: true }
131
+ const { getByText } = renderScreen()
132
+
133
+ expect(getByText('Delete message')).toBeTruthy()
134
+ })
135
+
136
+ it('hides Delete when the conversation is disabled', () => {
137
+ mockCurrentMessage = { ...mockMessage, mine: true }
138
+ mockConversation = { id: 1, disabled: true, disabledReason: 'locked' } as ConversationResource
139
+ const { queryByText, getByText } = renderScreen()
140
+
141
+ expect(queryByText('Delete message')).toBeNull()
142
+ expect(getByText('Copy text')).toBeTruthy()
143
+ })
144
+
145
+ it('offers reactions, reply, edit, and link preview removal on my message', () => {
146
+ mockCurrentMessage = myMessageWithLinkPreview
147
+ const { getByText } = renderScreen()
148
+
149
+ expect(getByText('👍')).toBeTruthy()
150
+ expect(getByText('Reply to message')).toBeTruthy()
151
+ expect(getByText('Edit message')).toBeTruthy()
152
+ expect(getByText('Remove link preview')).toBeTruthy()
153
+ })
154
+
155
+ it('hides reactions, reply, edit, and link preview removal when the conversation is disabled', () => {
156
+ mockCurrentMessage = myMessageWithLinkPreview
157
+ mockConversation = { id: 1, disabled: true, disabledReason: 'archived' } as ConversationResource
158
+ const { getByText, queryByText } = renderScreen()
159
+
160
+ expect(queryByText('👍')).toBeNull()
161
+ expect(queryByText('Reply to message')).toBeNull()
162
+ expect(queryByText('Edit message')).toBeNull()
163
+ expect(queryByText('Remove link preview')).toBeNull()
164
+ expect(getByText('Copy text')).toBeTruthy()
165
+ expect(getByText('View read receipts')).toBeTruthy()
166
+ })
167
+
168
+ it('keeps Report when the conversation is disabled', () => {
169
+ mockEnabledFeatures = ['ROLLOUT_MOBILE_message_reporting']
170
+ mockConversation = { id: 1, disabled: true, disabledReason: 'archived' } as ConversationResource
171
+ const { getByText } = renderScreen()
172
+
173
+ expect(getByText('Report message')).toBeTruthy()
111
174
  })
112
175
 
113
176
  it('copies the message text and gives success haptic feedback before closing', async () => {
@@ -5,7 +5,7 @@ import {
5
5
  QueryClient,
6
6
  QueryClientProvider,
7
7
  } from '@tanstack/react-query'
8
- import { act, render, screen } from '@testing-library/react-native'
8
+ import { act, fireEvent, render, screen } from '@testing-library/react-native'
9
9
  import React from 'react'
10
10
  import { FlatList } from 'react-native'
11
11
  import { buildTestQueryClient } from '../../../__utils__/query_client'
@@ -37,9 +37,18 @@ jest.mock('../../../hooks/use_animated_message_background_color', () => ({
37
37
  handleMessagePressOut: jest.fn(),
38
38
  }),
39
39
  }))
40
+ const mockNavigate = jest.fn()
40
41
  jest.mock('@react-navigation/native', () => ({
41
42
  ...jest.requireActual('@react-navigation/native'),
42
- useNavigation: () => ({ navigate: jest.fn() }),
43
+ useNavigation: () => ({ navigate: mockNavigate }),
44
+ }))
45
+ const mockToggleReaction = jest.fn()
46
+ jest.mock('../../../hooks/use_message_reaction_toggle', () => ({
47
+ useMessageReactionToggle: () => ({ handleReactionToggle: mockToggleReaction, isPending: false }),
48
+ }))
49
+ jest.mock('../../../utils/native_adapters', () => ({
50
+ ...jest.requireActual('../../../utils/native_adapters'),
51
+ Haptic: { impactLight: jest.fn() },
43
52
  }))
44
53
  // Every row calls this hook on each render, so its calls count row renders.
45
54
  jest.mock('../../../hooks/use_live_relative_time', () => {
@@ -293,6 +302,63 @@ describe('MessageList', () => {
293
302
  expect(renderedRowIds()).toEqual([])
294
303
  })
295
304
 
305
+ describe('in a disabled conversation', () => {
306
+ const reacted = buildMessage(1, {
307
+ reactionCounts: [
308
+ {
309
+ type: 'ReactionCount',
310
+ id: 'heart',
311
+ messageId: messageId(1),
312
+ value: 'heart',
313
+ count: 1,
314
+ mine: 0,
315
+ authorIds: [OTHER],
316
+ },
317
+ ],
318
+ })
319
+ const archived = buildMessage(2, {
320
+ text: 'This conversation was archived by Ann',
321
+ systemTextFormat: 'actor',
322
+ })
323
+ const disable = () => act(() => setHarnessConversation({ ...conversation, disabled: true }))
324
+
325
+ it('toggles a reaction pill while the conversation is active', () => {
326
+ renderList([reacted])
327
+
328
+ fireEvent.press(screen.getByTestId(`reaction-${reacted.id}-heart`))
329
+
330
+ expect(mockToggleReaction).toHaveBeenCalledWith({ value: 'heart', mine: false })
331
+ })
332
+
333
+ it('keeps reaction pills read-only but still shows who reacted', () => {
334
+ renderList([reacted])
335
+ disable()
336
+ const pill = screen.getByTestId(`reaction-${reacted.id}-heart`)
337
+
338
+ fireEvent.press(pill)
339
+ fireEvent(pill, 'longPress')
340
+
341
+ expect(mockToggleReaction).not.toHaveBeenCalled()
342
+ expect(mockNavigate).toHaveBeenCalledWith('Reactions', expect.anything())
343
+ })
344
+
345
+ it('opens message actions on a system message only while the conversation is active', () => {
346
+ queryClient.setQueryData<MessagesQueryData>(messagesQueryKey, {
347
+ pages: [{ data: [archived], links: {}, meta: { count: 1, totalCount: 1 } }],
348
+ pageParams: [{}],
349
+ })
350
+ render(<Harness />, { wrapper: Wrapper })
351
+ fireEvent(screen.getByText(/archived by Ann/), 'longPress')
352
+ expect(mockNavigate).toHaveBeenCalledWith('MessageActions', expect.anything())
353
+
354
+ mockNavigate.mockClear()
355
+ disable()
356
+ fireEvent(screen.getByText(/archived by Ann/), 'longPress')
357
+
358
+ expect(mockNavigate).not.toHaveBeenCalled()
359
+ })
360
+ })
361
+
296
362
  describe('opened at an anchor message', () => {
297
363
  const anchor = messages[4]
298
364
 
@@ -65,7 +65,9 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
65
65
  )
66
66
 
67
67
  const renderSystemMessage = (message: SystemMessageResource) =>
68
- render(<SystemMessage message={message} conversationId={1} />, { wrapper: Wrapper })
68
+ render(<SystemMessage message={message} conversationId={1} conversationDisabled={false} />, {
69
+ wrapper: Wrapper,
70
+ })
69
71
 
70
72
  beforeEach(() => {
71
73
  jest.useFakeTimers({ now: new Date(2026, 5, 15, 12, 0) })
@@ -44,6 +44,7 @@ import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from
44
44
  */
45
45
  interface MessageProps extends MessageResource {
46
46
  canDeleteNonAuthoredMessages: boolean
47
+ conversationDisabled: boolean
47
48
  conversation_id: number
48
49
  readByOthers?: boolean
49
50
  inReplyScreen?: boolean
@@ -53,6 +54,7 @@ interface MessageProps extends MessageResource {
53
54
 
54
55
  export const Message = memo(function MessageRow({
55
56
  canDeleteNonAuthoredMessages,
57
+ conversationDisabled,
56
58
  conversation_id,
57
59
  readByOthers,
58
60
  inReplyScreen,
@@ -108,6 +110,7 @@ export const Message = memo(function MessageRow({
108
110
  : hasReactions || showMessageMeta || showReplyCountButton
109
111
  ? 8
110
112
  : 4
113
+ const spaceBelowPressable = messageBottomMargin - MESSAGE_BACKGROUND_OUTSET
111
114
 
112
115
  const messageIsReplyLabel = replyToReplyRootMessage ? 'Reply' : ''
113
116
  const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
@@ -214,6 +217,7 @@ export const Message = memo(function MessageRow({
214
217
  return (
215
218
  <Pressable
216
219
  testID={`message-${message.id}`}
220
+ style={[styles.pressable, { marginBottom: spaceBelowPressable }]}
217
221
  onLongPress={isDeleting ? undefined : handleMessageLongPress}
218
222
  onPress={isDeleting ? undefined : handleMessagePress}
219
223
  onPressIn={isDeleting ? undefined : handleMessagePressIn}
@@ -226,14 +230,8 @@ export const Message = memo(function MessageRow({
226
230
  onAccessibilityAction={isReplyRootMessage ? handleAccessibilityAction : undefined}
227
231
  disabled={isDeletedReplyRootMessage || isDeleting}
228
232
  >
229
- <View style={styles.message}>
230
- <Animated.View
231
- style={[
232
- styles.background,
233
- { bottom: messageBottomMargin - MESSAGE_BACKGROUND_OUTSET },
234
- animatedBackgroundColor,
235
- ]}
236
- />
233
+ <Animated.View style={[StyleSheet.absoluteFill, animatedBackgroundColor]} />
234
+ <View style={[styles.message, { marginBottom: -spaceBelowPressable }]}>
237
235
  {!message.mine && (
238
236
  <View>
239
237
  {renderAuthor ? (
@@ -304,6 +302,7 @@ export const Message = memo(function MessageRow({
304
302
  onLongPress={handleReactionLongPress}
305
303
  message={message}
306
304
  conversationId={conversation_id}
305
+ disabled={conversationDisabled}
307
306
  />
308
307
  ))}
309
308
  </View>
@@ -370,18 +369,16 @@ const useMessageStyles = (mine: boolean, isDeleting: boolean) => {
370
369
  const messageBubbleBackgroundColor = mine ? myMessageBackgroundColor : colors.fillColorNeutral070
371
370
 
372
371
  return StyleSheet.create({
372
+ pressable: {
373
+ marginTop: -MESSAGE_BACKGROUND_OUTSET,
374
+ paddingTop: MESSAGE_BACKGROUND_OUTSET,
375
+ },
373
376
  message: {
374
377
  gap: 8,
375
378
  flexDirection: mine ? 'row-reverse' : 'row',
376
379
  paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
377
380
  opacity: isDeleting ? 0.5 : 1,
378
381
  },
379
- background: {
380
- position: 'absolute',
381
- left: 0,
382
- right: 0,
383
- top: -MESSAGE_BACKGROUND_OUTSET,
384
- },
385
382
  messageContent: {
386
383
  flex: 1,
387
384
  gap: 4,
@@ -72,6 +72,7 @@ export function MessageList({
72
72
  const inReplyScreen = !!replyRootId
73
73
  const canDeleteNonAuthoredMessages =
74
74
  conversation.memberAbility?.canDeleteNonAuthoredMessages ?? false
75
+ const conversationDisabled = conversation.disabled ?? false
75
76
  const latestRead = conversation.latestReadMessageSortKey
76
77
  const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen })
77
78
  const anchorIndex = anchorMessageId
@@ -193,7 +194,13 @@ export function MessageList({
193
194
  }
194
195
 
195
196
  if (isSystemMessage(item)) {
196
- return <SystemMessage message={item} conversationId={conversationId} />
197
+ return (
198
+ <SystemMessage
199
+ message={item}
200
+ conversationId={conversationId}
201
+ conversationDisabled={conversationDisabled}
202
+ />
203
+ )
197
204
  }
198
205
 
199
206
  const readByOthers = item.mine && !!latestRead && item.id <= latestRead
@@ -201,6 +208,7 @@ export function MessageList({
201
208
  <Message
202
209
  {...item}
203
210
  canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
211
+ conversationDisabled={conversationDisabled}
204
212
  conversation_id={conversationId}
205
213
  readByOthers={readByOthers}
206
214
  inReplyScreen={inReplyScreen}
@@ -16,11 +16,13 @@ export function MessageReaction({
16
16
  onLongPress,
17
17
  message,
18
18
  conversationId,
19
+ disabled = false,
19
20
  }: {
20
21
  reaction: ReactionCountResource
21
22
  onLongPress: (_reaction: ReactionCountResource) => void
22
23
  message: MessageResource
23
24
  conversationId: number
25
+ disabled?: boolean
24
26
  }) {
25
27
  const styles = useReactionStyles(reaction)
26
28
  const { handleReactionToggle } = useMessageReactionToggle({
@@ -46,8 +48,11 @@ export function MessageReaction({
46
48
  testID={`reaction-${message.id}-${reaction.value}`}
47
49
  style={styles.reaction}
48
50
  onLongPress={() => onLongPress(reaction)}
49
- onPress={handlePress}
50
- android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }}
51
+ onPress={disabled ? undefined : handlePress}
52
+ pressOpacity={disabled ? 1 : undefined}
53
+ android_ripple={
54
+ disabled ? undefined : { color: androidRippleColor, borderless: false, foreground: true }
55
+ }
51
56
  >
52
57
  <Text style={styles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
53
58
  <Text style={styles.reactionText}>{reaction.count}</Text>
@@ -22,38 +22,36 @@ export const MemberMessagesDisabledBanner = () => {
22
22
 
23
23
  const DEFAULT_DISABLED_TEXT = 'This conversation has been disabled.'
24
24
 
25
+ interface BannerAction {
26
+ label: string
27
+ onPress: () => void
28
+ loading?: boolean
29
+ }
30
+
25
31
  interface ConversationDisabledBannerProps {
26
32
  message: string | null
27
- messageLink?: { label: string; onPress: () => void }
33
+ action?: BannerAction
28
34
  }
29
35
 
30
36
  export const ConversationDisabledBanner = ({
31
37
  message,
32
- messageLink,
38
+ action,
33
39
  }: ConversationDisabledBannerProps) => {
34
40
  const styles = useStyles()
35
41
  const description = message ?? DEFAULT_DISABLED_TEXT
36
42
 
37
43
  return (
38
- <MessagesDisabledBanner
39
- description={description}
40
- style={styles.memberBanner}
41
- messageLink={messageLink}
42
- />
44
+ <MessagesDisabledBanner description={description} style={styles.memberBanner} action={action} />
43
45
  )
44
46
  }
45
47
 
46
48
  interface MessagesDisabledBannerProps {
47
49
  description: string
48
50
  style?: ViewStyle
49
- messageLink?: { label: string; onPress: () => void }
51
+ action?: BannerAction
50
52
  }
51
53
 
52
- const MessagesDisabledBanner = ({
53
- description,
54
- style,
55
- messageLink,
56
- }: MessagesDisabledBannerProps) => {
54
+ const MessagesDisabledBanner = ({ description, style, action }: MessagesDisabledBannerProps) => {
57
55
  const styles = useStyles()
58
56
 
59
57
  return (
@@ -61,13 +59,14 @@ const MessagesDisabledBanner = ({
61
59
  <Text style={styles.text} variant="tertiary">
62
60
  {description}
63
61
  </Text>
64
- {messageLink && (
62
+ {action && (
65
63
  <Button
66
64
  variant="fill"
67
65
  size="lg"
68
- title={messageLink.label}
69
- onPress={messageLink.onPress}
70
- style={styles.messageButton}
66
+ title={action.label}
67
+ onPress={action.onPress}
68
+ loading={action.loading}
69
+ style={styles.actionButton}
71
70
  />
72
71
  )}
73
72
  </View>
@@ -94,7 +93,7 @@ const useStyles = () => {
94
93
  paddingBottom: 16 + bottom,
95
94
  marginBottom: -bottom,
96
95
  },
97
- messageButton: {
96
+ actionButton: {
98
97
  marginTop: 12,
99
98
  },
100
99
  })