@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.
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +4 -0
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +23 -3
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/display/toggle_button.d.ts +5 -1
- package/build/components/display/toggle_button.d.ts.map +1 -1
- package/build/components/display/toggle_button.js +8 -2
- package/build/components/display/toggle_button.js.map +1 -1
- package/build/hooks/use_attachment_uploader.d.ts +2 -1
- package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
- package/build/hooks/use_attachment_uploader.js +6 -2
- package/build/hooks/use_attachment_uploader.js.map +1 -1
- package/build/hooks/use_message_draft.d.ts +13 -0
- package/build/hooks/use_message_draft.d.ts.map +1 -0
- package/build/hooks/use_message_draft.js +83 -0
- package/build/hooks/use_message_draft.js.map +1 -0
- package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +12 -8
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +6 -1
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js +10 -6
- package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js.map +1 -1
- package/build/screens/design_system_screen.d.ts.map +1 -1
- package/build/screens/design_system_screen.js +54 -2
- package/build/screens/design_system_screen.js.map +1 -1
- package/build/screens/message_actions_screen.d.ts.map +1 -1
- package/build/screens/message_actions_screen.js +2 -1
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/types/resources/denormalized_attachment_resource_for_create.d.ts +1 -0
- package/build/types/resources/denormalized_attachment_resource_for_create.d.ts.map +1 -1
- package/build/types/resources/denormalized_attachment_resource_for_create.js.map +1 -1
- package/build/utils/native_adapters/configuration.d.ts +3 -0
- package/build/utils/native_adapters/configuration.d.ts.map +1 -1
- package/build/utils/native_adapters/configuration.js +3 -0
- package/build/utils/native_adapters/configuration.js.map +1 -1
- package/build/utils/native_adapters/haptic.d.ts +23 -0
- package/build/utils/native_adapters/haptic.d.ts.map +1 -0
- package/build/utils/native_adapters/haptic.js +22 -0
- package/build/utils/native_adapters/haptic.js.map +1 -0
- package/build/utils/native_adapters/index.d.ts +1 -0
- package/build/utils/native_adapters/index.d.ts.map +1 -1
- package/build/utils/native_adapters/index.js +1 -0
- package/build/utils/native_adapters/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/utils/native_adapters/configuration.ts +46 -1
- package/src/components/conversation/message.tsx +4 -0
- package/src/components/conversation/message_form.tsx +25 -3
- package/src/components/display/toggle_button.tsx +15 -1
- package/src/hooks/use_attachment_uploader.ts +12 -2
- package/src/hooks/use_message_draft.ts +108 -0
- package/src/screens/conversation_new/components/services_form.tsx +4 -2
- package/src/screens/conversation_screen.tsx +7 -0
- package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx +4 -2
- package/src/screens/design_system_screen.tsx +66 -1
- package/src/screens/message_actions_screen.tsx +2 -1
- package/src/types/resources/denormalized_attachment_resource_for_create.ts +1 -0
- package/src/utils/native_adapters/configuration.ts +5 -0
- package/src/utils/native_adapters/haptic.ts +34 -0
- 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;
|
|
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"]}
|
|
@@ -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"}
|
|
@@ -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.
|
|
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": "
|
|
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
|
|
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({
|
|
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 />
|
package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx
CHANGED
|
@@ -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"
|