@planningcenter/chat-react-native 3.13.2-rc.3 → 3.14.0-rc.1

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 (64) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +4 -0
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts.map +1 -1
  5. package/build/components/conversation/message_form.js +23 -3
  6. package/build/components/conversation/message_form.js.map +1 -1
  7. package/build/components/display/toggle_button.d.ts +5 -1
  8. package/build/components/display/toggle_button.d.ts.map +1 -1
  9. package/build/components/display/toggle_button.js +8 -2
  10. package/build/components/display/toggle_button.js.map +1 -1
  11. package/build/hooks/use_attachment_uploader.d.ts +2 -1
  12. package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
  13. package/build/hooks/use_attachment_uploader.js +6 -2
  14. package/build/hooks/use_attachment_uploader.js.map +1 -1
  15. package/build/hooks/use_message_draft.d.ts +13 -0
  16. package/build/hooks/use_message_draft.d.ts.map +1 -0
  17. package/build/hooks/use_message_draft.js +83 -0
  18. package/build/hooks/use_message_draft.js.map +1 -0
  19. package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
  20. package/build/screens/conversation_new/components/services_form.js +12 -8
  21. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  22. package/build/screens/conversation_screen.d.ts.map +1 -1
  23. package/build/screens/conversation_screen.js +6 -1
  24. package/build/screens/conversation_screen.js.map +1 -1
  25. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts.map +1 -1
  26. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js +10 -6
  27. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js.map +1 -1
  28. package/build/screens/design_system_screen.d.ts.map +1 -1
  29. package/build/screens/design_system_screen.js +54 -2
  30. package/build/screens/design_system_screen.js.map +1 -1
  31. package/build/screens/message_actions_screen.d.ts.map +1 -1
  32. package/build/screens/message_actions_screen.js +2 -1
  33. package/build/screens/message_actions_screen.js.map +1 -1
  34. package/build/types/resources/denormalized_attachment_resource_for_create.d.ts +1 -0
  35. package/build/types/resources/denormalized_attachment_resource_for_create.d.ts.map +1 -1
  36. package/build/types/resources/denormalized_attachment_resource_for_create.js.map +1 -1
  37. package/build/utils/native_adapters/configuration.d.ts +3 -0
  38. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  39. package/build/utils/native_adapters/configuration.js +3 -0
  40. package/build/utils/native_adapters/configuration.js.map +1 -1
  41. package/build/utils/native_adapters/haptic.d.ts +23 -0
  42. package/build/utils/native_adapters/haptic.d.ts.map +1 -0
  43. package/build/utils/native_adapters/haptic.js +22 -0
  44. package/build/utils/native_adapters/haptic.js.map +1 -0
  45. package/build/utils/native_adapters/index.d.ts +1 -0
  46. package/build/utils/native_adapters/index.d.ts.map +1 -1
  47. package/build/utils/native_adapters/index.js +1 -0
  48. package/build/utils/native_adapters/index.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/__tests__/utils/native_adapters/configuration.ts +46 -1
  51. package/src/components/conversation/message.tsx +4 -0
  52. package/src/components/conversation/message_form.tsx +25 -3
  53. package/src/components/display/toggle_button.tsx +15 -1
  54. package/src/hooks/use_attachment_uploader.ts +12 -2
  55. package/src/hooks/use_message_draft.ts +108 -0
  56. package/src/screens/conversation_new/components/services_form.tsx +4 -2
  57. package/src/screens/conversation_screen.tsx +7 -0
  58. package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx +4 -2
  59. package/src/screens/design_system_screen.tsx +66 -1
  60. package/src/screens/message_actions_screen.tsx +2 -1
  61. package/src/types/resources/denormalized_attachment_resource_for_create.ts +1 -0
  62. package/src/utils/native_adapters/configuration.ts +5 -0
  63. package/src/utils/native_adapters/haptic.ts +34 -0
  64. package/src/utils/native_adapters/index.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"configuration.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAW1C,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,SAAS,CAAC,cAAkC;QACjD,SAAS,GAAG,cAAc,CAAC,SAAS,CAAA;QACpC,KAAK,GAAG,cAAc,CAAC,KAAK,CAAA;QAC5B,KAAK,GAAG,cAAc,CAAC,KAAK,CAAA;QAC5B,WAAW,GAAG,cAAc,CAAC,WAAW,CAAA;QACxC,GAAG,GAAG,cAAc,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAA;QAC5C,OAAO,GAAG,cAAc,CAAC,OAAO,IAAI,IAAI,cAAc,CAAC,SAAS,CAAC,CAAA;IACnE,CAAC;CACF;AAED,MAAM,aAAa,GAAG,GAAG,EAAE;IACzB,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAA;AACnF,CAAC,CAAA;AAED,MAAM,CAAC,IAAI,SAAS,GAAqB,IAAI,gBAAgB,CAAC;IAC5D,cAAc,EAAE,KAAK,IAAI,EAAE;QACzB,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,CAAA;IACX,CAAC;IACD,cAAc,EAAE,KAAK,EAAE,CAAS,EAAE,EAAE,CAAC,aAAa,EAAE;CACrD,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,KAAK,GAAiB,IAAI,YAAY,CAAC;IAChD,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE;QACtB,aAAa,EAAE,CAAA;QACf,OAAO,EAAS,CAAA;IAClB,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,KAAK,GAAiB,IAAI,YAAY,CAAC;IAChD,MAAM,EAAE,MAAM,CAAC,MAAM,CACnB,GAAG,EAAE;QACH,aAAa,EAAE,CAAA;QACf,OAAO,IAAI,CAAA;IACb,CAAC,EACD,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,CAC9C;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,WAAW,GAAuB,IAAI,kBAAkB,CAAC;IAClE,eAAe,EAAE,KAAK,IAAI,EAAE;QAC1B,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IACzC,CAAC;IACD,qBAAqB,EAAE,KAAK,IAAI,EAAE;QAChC,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IACzC,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,GAAG,GAAe,IAAI,UAAU,EAAE,CAAA;AAE7C,MAAM,CAAC,IAAI,OAAO,GAAmB,IAAI,cAAc,CAAC,SAAS,CAAC,CAAA","sourcesContent":["import { LogAdapter } from './log'\nimport { AudioAdapter } from './audio'\nimport { ClipboardAdapter } from './clipboard'\nimport { ImagePickerAdapter } from './image_picker'\nimport { VideoAdapter } from './video'\nimport { Linking as RNLinking } from 'react-native'\nimport { LinkingAdapter } from './linking'\n\ntype ChatConfigurations = {\n clipboard: ClipboardAdapter\n audio: AudioAdapter\n video: VideoAdapter\n imagePicker: ImagePickerAdapter\n log?: LogAdapter\n linking?: LinkingAdapter\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 Log = configurations.log || new LogAdapter()\n Linking = configurations.linking || new LinkingAdapter(RNLinking)\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 Log: LogAdapter = new LogAdapter()\n\nexport let Linking: LinkingAdapter = new LinkingAdapter(RNLinking)\n"]}
1
+ {"version":3,"file":"configuration.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAYxC,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,SAAS,CAAC,cAAkC;QACjD,SAAS,GAAG,cAAc,CAAC,SAAS,CAAA;QACpC,KAAK,GAAG,cAAc,CAAC,KAAK,CAAA;QAC5B,KAAK,GAAG,cAAc,CAAC,KAAK,CAAA;QAC5B,WAAW,GAAG,cAAc,CAAC,WAAW,CAAA;QACxC,GAAG,GAAG,cAAc,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAA;QAC5C,OAAO,GAAG,cAAc,CAAC,OAAO,IAAI,IAAI,cAAc,CAAC,SAAS,CAAC,CAAA;QACjE,MAAM,GAAG,cAAc,CAAC,MAAM,IAAI,IAAI,aAAa,EAAE,CAAA;IACvD,CAAC;CACF;AAED,MAAM,aAAa,GAAG,GAAG,EAAE;IACzB,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAA;AACnF,CAAC,CAAA;AAED,MAAM,CAAC,IAAI,SAAS,GAAqB,IAAI,gBAAgB,CAAC;IAC5D,cAAc,EAAE,KAAK,IAAI,EAAE;QACzB,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,CAAA;IACX,CAAC;IACD,cAAc,EAAE,KAAK,EAAE,CAAS,EAAE,EAAE,CAAC,aAAa,EAAE;CACrD,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,KAAK,GAAiB,IAAI,YAAY,CAAC;IAChD,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE;QACtB,aAAa,EAAE,CAAA;QACf,OAAO,EAAS,CAAA;IAClB,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,KAAK,GAAiB,IAAI,YAAY,CAAC;IAChD,MAAM,EAAE,MAAM,CAAC,MAAM,CACnB,GAAG,EAAE;QACH,aAAa,EAAE,CAAA;QACf,OAAO,IAAI,CAAA;IACb,CAAC,EACD,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,CAC9C;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,WAAW,GAAuB,IAAI,kBAAkB,CAAC;IAClE,eAAe,EAAE,KAAK,IAAI,EAAE;QAC1B,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IACzC,CAAC;IACD,qBAAqB,EAAE,KAAK,IAAI,EAAE;QAChC,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IACzC,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,IAAI,GAAG,GAAe,IAAI,UAAU,EAAE,CAAA;AAE7C,MAAM,CAAC,IAAI,OAAO,GAAmB,IAAI,cAAc,CAAC,SAAS,CAAC,CAAA;AAElE,MAAM,CAAC,IAAI,MAAM,GAAkB,IAAI,aAAa,EAAE,CAAA","sourcesContent":["import { LogAdapter } from './log'\nimport { AudioAdapter } from './audio'\nimport { ClipboardAdapter } from './clipboard'\nimport { ImagePickerAdapter } from './image_picker'\nimport { VideoAdapter } from './video'\nimport { Linking as RNLinking } from 'react-native'\nimport { LinkingAdapter } from './linking'\nimport { HapticAdapter } from './haptic'\n\ntype ChatConfigurations = {\n clipboard: ClipboardAdapter\n audio: AudioAdapter\n video: VideoAdapter\n imagePicker: ImagePickerAdapter\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 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 Log: LogAdapter = new LogAdapter()\n\nexport let Linking: LinkingAdapter = new LinkingAdapter(RNLinking)\n\nexport let Haptic: HapticAdapter = new HapticAdapter()\n"]}
@@ -0,0 +1,23 @@
1
+ interface Haptic {
2
+ impactLight: () => void;
3
+ impactMedium: () => void;
4
+ impactHeavy: () => void;
5
+ rigid: () => void;
6
+ soft: () => void;
7
+ notificationSuccess: () => void;
8
+ notificationWarning: () => void;
9
+ notificationError: () => void;
10
+ }
11
+ export declare class HapticAdapter {
12
+ impactLight: () => void;
13
+ impactMedium: () => void;
14
+ impactHeavy: () => void;
15
+ rigid: () => void;
16
+ soft: () => void;
17
+ notificationSuccess: () => void;
18
+ notificationWarning: () => void;
19
+ notificationError: () => void;
20
+ constructor(methods?: Haptic);
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=haptic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptic.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/haptic.ts"],"names":[],"mappings":"AAEA,UAAU,MAAM;IACd,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,mBAAmB,EAAE,MAAM,IAAI,CAAA;IAC/B,mBAAmB,EAAE,MAAM,IAAI,CAAA;IAC/B,iBAAiB,EAAE,MAAM,IAAI,CAAA;CAC9B;AAED,qBAAa,aAAa;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,mBAAmB,EAAE,MAAM,IAAI,CAAA;IAC/B,mBAAmB,EAAE,MAAM,IAAI,CAAA;IAC/B,iBAAiB,EAAE,MAAM,IAAI,CAAA;gBAEjB,OAAO,CAAC,EAAE,MAAM;CAU7B"}
@@ -0,0 +1,22 @@
1
+ import { noop } from 'lodash';
2
+ export class HapticAdapter {
3
+ impactLight;
4
+ impactMedium;
5
+ impactHeavy;
6
+ rigid;
7
+ soft;
8
+ notificationSuccess;
9
+ notificationWarning;
10
+ notificationError;
11
+ constructor(methods) {
12
+ this.impactLight = methods?.impactLight ?? noop;
13
+ this.impactMedium = methods?.impactMedium ?? noop;
14
+ this.impactHeavy = methods?.impactHeavy ?? noop;
15
+ this.rigid = methods?.rigid ?? noop;
16
+ this.soft = methods?.soft ?? noop;
17
+ this.notificationSuccess = methods?.notificationSuccess ?? noop;
18
+ this.notificationWarning = methods?.notificationWarning ?? noop;
19
+ this.notificationError = methods?.notificationError ?? noop;
20
+ }
21
+ }
22
+ //# sourceMappingURL=haptic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptic.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/haptic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAa7B,MAAM,OAAO,aAAa;IACxB,WAAW,CAAY;IACvB,YAAY,CAAY;IACxB,WAAW,CAAY;IACvB,KAAK,CAAY;IACjB,IAAI,CAAY;IAChB,mBAAmB,CAAY;IAC/B,mBAAmB,CAAY;IAC/B,iBAAiB,CAAY;IAE7B,YAAY,OAAgB;QAC1B,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,IAAI,CAAA;QAC/C,IAAI,CAAC,YAAY,GAAG,OAAO,EAAE,YAAY,IAAI,IAAI,CAAA;QACjD,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,IAAI,CAAA;QAC/C,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,IAAI,CAAA;QACnC,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,IAAI,CAAA;QACjC,IAAI,CAAC,mBAAmB,GAAG,OAAO,EAAE,mBAAmB,IAAI,IAAI,CAAA;QAC/D,IAAI,CAAC,mBAAmB,GAAG,OAAO,EAAE,mBAAmB,IAAI,IAAI,CAAA;QAC/D,IAAI,CAAC,iBAAiB,GAAG,OAAO,EAAE,iBAAiB,IAAI,IAAI,CAAA;IAC7D,CAAC;CACF","sourcesContent":["import { noop } from 'lodash'\n\ninterface Haptic {\n impactLight: () => void\n impactMedium: () => void\n impactHeavy: () => void\n rigid: () => void\n soft: () => void\n notificationSuccess: () => void\n notificationWarning: () => void\n notificationError: () => void\n}\n\nexport class HapticAdapter {\n impactLight: () => void\n impactMedium: () => void\n impactHeavy: () => void\n rigid: () => void\n soft: () => void\n notificationSuccess: () => void\n notificationWarning: () => void\n notificationError: () => void\n\n constructor(methods?: Haptic) {\n this.impactLight = methods?.impactLight ?? noop\n this.impactMedium = methods?.impactMedium ?? noop\n this.impactHeavy = methods?.impactHeavy ?? noop\n this.rigid = methods?.rigid ?? noop\n this.soft = methods?.soft ?? noop\n this.notificationSuccess = methods?.notificationSuccess ?? noop\n this.notificationWarning = methods?.notificationWarning ?? noop\n this.notificationError = methods?.notificationError ?? noop\n }\n}\n"]}
@@ -5,4 +5,5 @@ export * from './image_picker';
5
5
  export * from './linking';
6
6
  export * from './log';
7
7
  export * from './video';
8
+ export * from './haptic';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -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,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,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,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA"}
@@ -5,4 +5,5 @@ export * from './image_picker';
5
5
  export * from './linking';
6
6
  export * from './log';
7
7
  export * from './video';
8
+ export * from './haptic';
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","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,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA","sourcesContent":["export * from './audio'\nexport * from './clipboard'\nexport * from './configuration'\nexport * from './image_picker'\nexport * from './linking'\nexport * from './log'\nexport * from './video'\n"]}
1
+ {"version":3,"file":"index.js","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,gBAAgB,CAAA;AAC9B,cAAc,WAAW,CAAA;AACzB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA","sourcesContent":["export * from './audio'\nexport * from './clipboard'\nexport * from './configuration'\nexport * from './image_picker'\nexport * from './linking'\nexport * from './log'\nexport * from './video'\nexport * from './haptic'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.13.2-rc.3",
3
+ "version": "3.14.0-rc.1",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "react-native-url-polyfill": "^2.0.0",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "580f76cfbf66e9da665162c5af8b75fe09c07c26"
58
+ "gitHead": "a17321118033ef2c7ff8f69198214342f878f308"
59
59
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react'
2
2
  import { Linking as RNLinking } from 'react-native'
3
- import { ChatAdapters, Linking } from '../../../utils/native_adapters/configuration'
3
+ import { ChatAdapters, Linking, Haptic } from '../../../utils/native_adapters/configuration'
4
4
  import {
5
5
  AudioAdapter,
6
6
  Clipboard,
@@ -10,6 +10,7 @@ import {
10
10
  VideoAdapter,
11
11
  } from '../../../utils/native_adapters'
12
12
  import { VideoPlayerHandle, VideoPlayerProps } from '../../../utils/native_adapters/video'
13
+ import { HapticAdapter } from '../../../utils/native_adapters/haptic'
13
14
 
14
15
  describe('ChatAdapters', () => {
15
16
  const getStringAsync = jest.fn(async () => '')
@@ -36,6 +37,16 @@ describe('ChatAdapters', () => {
36
37
  openCameraAsync: async () => ({ canceled: true, assets: null }),
37
38
  openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
38
39
  })
40
+ const haptic = new HapticAdapter({
41
+ impactLight: jest.fn(),
42
+ impactMedium: jest.fn(),
43
+ impactHeavy: jest.fn(),
44
+ rigid: jest.fn(),
45
+ soft: jest.fn(),
46
+ notificationSuccess: jest.fn(),
47
+ notificationWarning: jest.fn(),
48
+ notificationError: jest.fn(),
49
+ })
39
50
 
40
51
  it('should be defined', () => {
41
52
  expect(ChatAdapters).toBeDefined()
@@ -47,6 +58,7 @@ describe('ChatAdapters', () => {
47
58
  audio,
48
59
  video,
49
60
  imagePicker,
61
+ haptic,
50
62
  })
51
63
 
52
64
  expect(Clipboard).toEqual(clipboard)
@@ -91,4 +103,37 @@ describe('ChatAdapters', () => {
91
103
  linking.canOpenURL('https://www.google.com')
92
104
  })
93
105
  })
106
+
107
+ describe('haptic adapter', () => {
108
+ it('should configure the haptic adapter', () => {
109
+ ChatAdapters.configure({
110
+ clipboard,
111
+ audio,
112
+ video,
113
+ imagePicker,
114
+ haptic,
115
+ })
116
+
117
+ expect(Haptic).toEqual(haptic)
118
+ })
119
+
120
+ it('should be configured with defaults', () => {
121
+ ChatAdapters.configure({
122
+ clipboard,
123
+ audio,
124
+ video,
125
+ imagePicker,
126
+ })
127
+
128
+ expect(Haptic).toBeDefined()
129
+ expect(Haptic.impactLight).toBeDefined()
130
+ expect(Haptic.impactMedium).toBeDefined()
131
+ expect(Haptic.impactHeavy).toBeDefined()
132
+ expect(Haptic.rigid).toBeDefined()
133
+ expect(Haptic.soft).toBeDefined()
134
+ expect(Haptic.notificationSuccess).toBeDefined()
135
+ expect(Haptic.notificationWarning).toBeDefined()
136
+ expect(Haptic.notificationError).toBeDefined()
137
+ })
138
+ })
94
139
  })
@@ -24,6 +24,7 @@ import Animated, {
24
24
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
25
25
  import { MessageReadReceipts } from './message_read_receipts'
26
26
  import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
27
+ import { Haptic } from '../../utils/native_adapters'
27
28
 
28
29
  /** Message
29
30
  * Component for display of a message within a conversation list
@@ -104,6 +105,7 @@ export function Message({
104
105
  const handleMessageLongPress = () => {
105
106
  if (!isPersisted) return
106
107
 
108
+ Haptic.impactLight()
107
109
  navigation.navigate('MessageActions', {
108
110
  message_id: message.id,
109
111
  conversation_id,
@@ -111,6 +113,7 @@ export function Message({
111
113
  })
112
114
  }
113
115
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
116
+ Haptic.impactLight()
114
117
  navigation.navigate('Reactions', {
115
118
  message_id: message.id,
116
119
  conversation_id,
@@ -119,6 +122,7 @@ export function Message({
119
122
  }
120
123
 
121
124
  const handleMessageAttachmentLongPress = (attachment: DenormalizedMessageAttachmentResource) => {
125
+ Haptic.impactLight()
122
126
  navigation.navigate('AttachmentActions', {
123
127
  attachmentId: attachment?.id,
124
128
  attachmentContentType: attachment?.attributes.contentType,
@@ -26,9 +26,10 @@ import { ConversationResource, MessageResource } from '../../types'
26
26
  import { ConversationScreenProps } from '../../screens/conversation_screen'
27
27
 
28
28
  import { ChatContext } from '../../contexts/chat_context'
29
- import { ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
29
+ import { Haptic, ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
30
30
  import { platformFontWeightMedium, platformPressedOpacityStyle } from '../../utils'
31
31
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
32
+ import { useMessageDraft } from '../../hooks/use_message_draft'
32
33
  import {
33
34
  DenormalizedAttachmentResourceForCreate,
34
35
  DenormalizedMessageAttachmentResourceForCreate,
@@ -88,7 +89,11 @@ function MessageFormRoot({
88
89
  const { giphyApiKey } = useContext(ChatContext)
89
90
  const canGiphy = !!giphyApiKey && !currentlyEditingMessage
90
91
  const styles = useMessageFormStyles()
91
- const [text, setText] = React.useState('')
92
+ const { draft, saveDraft, clearDraft } = useMessageDraft(conversation.id)
93
+ const [text, setText] = React.useState(() => {
94
+ // Initialize from draft only when not editing a message
95
+ return currentlyEditingMessage ? '' : draft?.text || ''
96
+ })
92
97
  const [usingGiphy, setUsingGiphy] = useState(false)
93
98
  const navigation = useNavigation()
94
99
  const route = useRoute() as ConversationScreenProps['route']
@@ -103,6 +108,7 @@ function MessageFormRoot({
103
108
  })
104
109
  const attachmentUploader = useAttachmentUploader({
105
110
  conversationId: conversation.id,
111
+ draftAttachments: currentlyEditingMessage ? undefined : draft?.attachments,
106
112
  })
107
113
  const resetAttachmentUploader = attachmentUploader.reset
108
114
 
@@ -111,10 +117,13 @@ function MessageFormRoot({
111
117
  resetMutation()
112
118
  setText('')
113
119
  setUsingGiphy(false)
120
+ if (!currentlyEditingMessage) {
121
+ clearDraft()
122
+ }
114
123
  navigation.setParams({
115
124
  editing_message_id: null,
116
125
  })
117
- }, [resetAttachmentUploader, resetMutation, navigation])
126
+ }, [resetAttachmentUploader, resetMutation, navigation, currentlyEditingMessage, clearDraft])
118
127
 
119
128
  useEffect(() => {
120
129
  if (canGiphy && !usingGiphy && text.startsWith('/giphy ')) {
@@ -138,6 +147,18 @@ function MessageFormRoot({
138
147
  }
139
148
  }, [reset, navigation, route.params])
140
149
 
150
+ // Save draft when text or attachments change (debounced)
151
+ useEffect(() => {
152
+ // Don't save drafts when editing a message
153
+ if (currentlyEditingMessage) return
154
+
155
+ const timeoutId = setTimeout(() => {
156
+ saveDraft(text, attachmentUploader.attachments)
157
+ }, 500)
158
+
159
+ return () => clearTimeout(timeoutId)
160
+ }, [text, attachmentUploader.attachments, saveDraft, currentlyEditingMessage])
161
+
141
162
  const canSubmit = (() => {
142
163
  if (isPending) return false
143
164
  if (attachmentUploader?.pendingUploads) return false
@@ -173,6 +194,7 @@ function MessageFormRoot({
173
194
 
174
195
  if (canGiphy && usingGiphy) {
175
196
  TextInput.State.blurTextInput(TextInput.State.currentlyFocusedInput())
197
+ Haptic.impactLight()
176
198
  navigateToSendGiphyAfterKeyboardHides()
177
199
  } else {
178
200
  let attachmentsForSubmit: DenormalizedAttachmentResourceForCreate[] =
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Pressable, StyleSheet } from 'react-native'
3
- import type { PressableProps } from 'react-native'
3
+ import type { PressableProps, GestureResponderEvent } from 'react-native'
4
4
  import { Text } from './text'
5
5
  import {
6
6
  useTheme,
@@ -11,6 +11,7 @@ import {
11
11
  import { platformFontWeightBold, platformPressedOpacityStyle } from '../../utils'
12
12
  import { Icon, IconString } from './icon'
13
13
  import { tokens } from '../../vendor/tapestry/tokens'
14
+ import { Haptic } from '../../utils/native_adapters/configuration'
14
15
 
15
16
  // =================================
16
17
  // ====== Component ================
@@ -53,6 +54,10 @@ export interface ToggleButtonProps extends PressableProps {
53
54
  * Pressable container styles
54
55
  */
55
56
  style?: PressableProps['style']
57
+ /**
58
+ * Specifies whether haptic feedback should be enabled.
59
+ */
60
+ hapticFeedback?: boolean
56
61
  }
57
62
 
58
63
  export function ToggleButton({
@@ -65,6 +70,8 @@ export function ToggleButton({
65
70
  minimumFontScale,
66
71
  title,
67
72
  style,
73
+ hapticFeedback = true,
74
+ onPress,
68
75
  ...props
69
76
  }: ToggleButtonProps) {
70
77
  const styles = useStyles({ active, maxFontSizeMultiplier })
@@ -74,6 +81,12 @@ export function ToggleButton({
74
81
  const baseRippleColor = active ? colors.interaction : colors.fillColorNeutral050Base
75
82
  const androidRippleColor = useCreateAndroidRippleColor({ color: baseRippleColor })
76
83
 
84
+ const handlePress = (event: GestureResponderEvent) => {
85
+ if (hapticFeedback) Haptic.impactLight()
86
+
87
+ onPress?.(event)
88
+ }
89
+
77
90
  return (
78
91
  <Pressable
79
92
  style={({ pressed }) => ({
@@ -84,6 +97,7 @@ export function ToggleButton({
84
97
  accessibilityRole="togglebutton"
85
98
  accessibilityState={{ checked: active }}
86
99
  android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }}
100
+ onPress={handlePress}
87
101
  {...props}
88
102
  >
89
103
  {iconNameLeft && (
@@ -18,10 +18,16 @@ const MAX_FILE_SIZE_IN_MB = 50
18
18
  const MAX_FILE_SIZE_IN_BYTES = MAX_FILE_SIZE_IN_MB * 1024 * 1024
19
19
  const MAX_NUMBER_OF_ATTACHMENTS = 10
20
20
 
21
- export function useAttachmentUploader({ conversationId }: { conversationId: number }) {
21
+ export function useAttachmentUploader({
22
+ conversationId,
23
+ draftAttachments,
24
+ }: {
25
+ conversationId: number
26
+ draftAttachments?: FileAttachment[]
27
+ }) {
22
28
  const apiClient = useApiClient()
23
29
  const uploadApi = useUploadClient()
24
- const [attachments, setAttachments] = useState<FileAttachment[]>([])
30
+ const [attachments, setAttachments] = useState<FileAttachment[]>(() => draftAttachments || [])
25
31
  const uploadState = useRef<FileUploadState>({})
26
32
  const [lastUploadId, setLastUploadId] = useState<string>()
27
33
  const numberOfAttachments = attachments.length
@@ -67,6 +73,7 @@ export function useAttachmentUploader({ conversationId }: { conversationId: numb
67
73
  const newAttachments: FileAttachment[] = validFiles.map(file => ({
68
74
  file,
69
75
  status: 'uploading',
76
+ uploadedAt: Date.now(),
70
77
  }))
71
78
 
72
79
  if (newAttachments && newAttachments.length > 0) {
@@ -111,6 +118,9 @@ export function useAttachmentUploader({ conversationId }: { conversationId: numb
111
118
  setLastUploadId(undefined)
112
119
  setAttachments(
113
120
  attachments.map(attachment => {
121
+ // Don't risk overwriting ids already set
122
+ if (attachment.id) return attachment
123
+
114
124
  const state = uploadState.current[attachment.file.name]
115
125
  if (state) {
116
126
  return { ...attachment, id: state.id, status: state.status }
@@ -0,0 +1,108 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { useAsyncStorage } from './use_async_storage'
3
+ import type { FileAttachment } from '../types/resources/denormalized_attachment_resource_for_create'
4
+
5
+ interface MessageDraft {
6
+ text: string
7
+ attachments: FileAttachment[]
8
+ lastUpdated: number
9
+ }
10
+
11
+ const DRAFT_STORAGE_KEY = 'chat_message_drafts'
12
+ const DRAFT_EXPIRY_HOURS = 720 // 30 days
13
+ const DRAFT_EXPIRY_MS = DRAFT_EXPIRY_HOURS * 60 * 60 * 1000
14
+ const ATTACHMENT_EXPIRY_HOURS = 48 // 2 days
15
+ const ATTACHMENT_EXPIRY_MS = ATTACHMENT_EXPIRY_HOURS * 60 * 60 * 1000
16
+
17
+ export function useMessageDraft(conversationId: number) {
18
+ const conversationKey = conversationId.toString()
19
+ const [allDrafts, setAllDrafts] = useAsyncStorage<Record<string, MessageDraft>>(
20
+ DRAFT_STORAGE_KEY,
21
+ {}
22
+ )
23
+
24
+ const [draft, setDraft] = useState<MessageDraft | null>(() => {
25
+ const storedDraft = allDrafts[conversationKey] || null
26
+
27
+ // Filter expired attachments
28
+ if (storedDraft?.attachments) {
29
+ const now = Date.now()
30
+ storedDraft.attachments = storedDraft.attachments.filter(attachment => {
31
+ if (!attachment.uploadedAt) return false
32
+ return now - attachment.uploadedAt < ATTACHMENT_EXPIRY_MS
33
+ })
34
+ }
35
+
36
+ return storedDraft
37
+ })
38
+
39
+ // Clean up expired drafts on mount
40
+ useEffect(() => {
41
+ const now = Date.now()
42
+ const validDrafts: Record<string, MessageDraft> = {}
43
+
44
+ Object.entries(allDrafts).forEach(([id, draftData]) => {
45
+ if (draftData?.lastUpdated && now - draftData.lastUpdated < DRAFT_EXPIRY_MS) {
46
+ validDrafts[id] = draftData
47
+ }
48
+ })
49
+
50
+ // If we filtered out any drafts, update storage
51
+ if (Object.keys(validDrafts).length !== Object.keys(allDrafts).length) {
52
+ setAllDrafts(validDrafts)
53
+ }
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, []) // I only want this to run on mount
56
+
57
+ const saveDraft = useCallback(
58
+ (text: string, attachments: FileAttachment[]) => {
59
+ const hasContent = text.trim().length > 0 || attachments.length > 0
60
+ const updatedDrafts = { ...allDrafts }
61
+
62
+ if (hasContent) {
63
+ // Convert attachments to serializable format (similar to web implementation)
64
+ const normalizedAttachments = attachments
65
+ .filter(att => att.id && att.status === 'success')
66
+ .map(att => ({
67
+ ...att,
68
+ file: {
69
+ uri: att.file.uri,
70
+ name: att.file.name,
71
+ type: att.file.type,
72
+ size: att.file.size,
73
+ width: att.file.width,
74
+ height: att.file.height,
75
+ },
76
+ }))
77
+
78
+ const newDraft: MessageDraft = {
79
+ text,
80
+ attachments: normalizedAttachments,
81
+ lastUpdated: Date.now(),
82
+ }
83
+
84
+ updatedDrafts[conversationKey] = newDraft
85
+ setDraft(newDraft)
86
+ } else {
87
+ delete updatedDrafts[conversationKey]
88
+ setDraft(null)
89
+ }
90
+
91
+ setAllDrafts(updatedDrafts)
92
+ },
93
+ [conversationKey, allDrafts, setAllDrafts]
94
+ )
95
+
96
+ const clearDraft = useCallback(() => {
97
+ const updatedDrafts = { ...allDrafts }
98
+ delete updatedDrafts[conversationKey]
99
+ setAllDrafts(updatedDrafts)
100
+ setDraft(null)
101
+ }, [conversationKey, allDrafts, setAllDrafts])
102
+
103
+ return {
104
+ draft,
105
+ saveDraft,
106
+ clearDraft,
107
+ }
108
+ }
@@ -13,6 +13,7 @@ import { ConversationResource, MemberResource } from '../../../types'
13
13
  import { tokens } from '../../../vendor/tapestry/tokens'
14
14
  import { TeamFilterTypes } from '../../conversation_filter_recipients/types'
15
15
  import { useServicesTeams } from '../../../hooks/services/use_services_team'
16
+ import { Haptic } from '../../../utils/native_adapters'
16
17
 
17
18
  type ServicesFormProps = {
18
19
  initialTeamIds?: number[]
@@ -153,7 +154,8 @@ function FormContent({
153
154
  <TextButton
154
155
  accessibilityLabel="Select teams"
155
156
  textStyle={styles.teamCountHeader}
156
- onPress={() =>
157
+ onPress={() => {
158
+ Haptic.impactLight()
157
159
  navigation.navigate('New', {
158
160
  screen: 'ConversationFilterRecipients',
159
161
  params: {
@@ -162,7 +164,7 @@ function FormContent({
162
164
  team_filter_type: teamFilterType,
163
165
  },
164
166
  })
165
- }
167
+ }}
166
168
  >
167
169
  {teamCountHeader}
168
170
  </TextButton>
@@ -149,6 +149,13 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
149
149
  <MessageForm.Root
150
150
  conversation={conversation}
151
151
  currentlyEditingMessage={currentlyEditingMessage}
152
+ // We use a separate key so that it remounts component when switching between new
153
+ // and edit message. This simplifies internal state handling.
154
+ key={
155
+ currentlyEditingMessage
156
+ ? `edit-message-form-${currentlyEditingMessage.id}`
157
+ : 'new-message-form'
158
+ }
152
159
  >
153
160
  <MessageForm.AttachmentPicker />
154
161
  <MessageForm.Commands />
@@ -14,6 +14,7 @@ import { TeamRecipientRow } from './components/team_recipient_row'
14
14
  import { GroupsRecipientRow } from './components/groups_recipient_row'
15
15
  import { DefaultLoading } from '../../components/page/loading'
16
16
  import { useAppGrants } from '../../hooks'
17
+ import { Haptic } from '../../utils/native_adapters'
17
18
 
18
19
  const MAX_VISIBLE_RECIPIENTS = 5
19
20
 
@@ -128,14 +129,15 @@ export const ConversationSelectRecipientsScreen = ({
128
129
  {hasServiceTypes && (
129
130
  <Button
130
131
  style={styles.selectTeamsButton}
131
- onPress={() =>
132
+ onPress={() => {
133
+ Haptic.impactLight()
132
134
  navigation.navigate('New', {
133
135
  screen: 'ConversationFilterRecipients',
134
136
  params: {
135
137
  source_app_name: 'Services',
136
138
  },
137
139
  })
138
- }
140
+ }}
139
141
  title="Select teams"
140
142
  variant="outline"
141
143
  iconNameLeft="general.search"