From 80f4a80498fdd14206d33b4abb79d0ee0f704f1c Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Thu, 31 Oct 2024 14:22:06 +0100 Subject: [PATCH 1/7] add and improve date formatting utils --- src/utils/cutAmountOfCharsFromString.ts | 2 + src/utils/datetime/formatDate.test.ts | 73 +++++++++++-------- src/utils/datetime/formatDate.ts | 6 +- src/utils/datetime/formatDateTime.test.ts | 51 +++++++++++++ src/utils/datetime/formatDateTime.ts | 12 +++ .../datetime/formatDateToDisplay.test.ts | 16 ++++ 6 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 src/utils/datetime/formatDateTime.test.ts create mode 100644 src/utils/datetime/formatDateTime.ts create mode 100644 src/utils/datetime/formatDateToDisplay.test.ts diff --git a/src/utils/cutAmountOfCharsFromString.ts b/src/utils/cutAmountOfCharsFromString.ts index 873db93bb..d01fee4fc 100644 --- a/src/utils/cutAmountOfCharsFromString.ts +++ b/src/utils/cutAmountOfCharsFromString.ts @@ -16,4 +16,6 @@ export const cutAmountOfCharsFromString = ({ if (position === 'end') { return text.slice(0, -amount) } + + return text } diff --git a/src/utils/datetime/formatDate.test.ts b/src/utils/datetime/formatDate.test.ts index 338ea903b..41f467f67 100644 --- a/src/utils/datetime/formatDate.test.ts +++ b/src/utils/datetime/formatDate.test.ts @@ -1,32 +1,45 @@ import {formatDate} from '@/utils/datetime/formatDate' -test('Test datum', () => - expect(formatDate('December 17, 1995 03:24:00')).toBe('17 december 1995')) - -test('ISO date year/month/day', () => - expect(formatDate('2023-03-01')).toBe('1 maart 2023')) - -test('ISO date year/month', () => - expect(formatDate('2021-01')).toBe('1 januari 2021')) - -test('ISO date year', () => expect(formatDate('2021')).toBe('1 januari 2021')) - -test('ISO date UTC', () => - expect(formatDate('2021-01-01T12:00:00Z')).toBe('1 januari 2021')) - -test('ISO date GMT+2', () => - expect(formatDate('2021-01-01T12:00:00+02:00')).toBe('1 januari 2021')) - -test('Date format with slashes', () => - expect(formatDate('01/01/2021')).toBe('1 januari 2021')) - -test('Date format MMM DD YYYY', () => - expect(formatDate('Jan 01 2021')).toBe('1 januari 2021')) - -test(`value of 'null'`, () => - //@ts-ignore - expect(formatDate(null)).toBe('')) - -test(`value of 'undefined'`, () => - //@ts-ignore - expect(formatDate(undefined)).toBe('')) +describe('formatDate', () => { + test('Test datum', () => { + expect(formatDate('December 17, 1995 03:24:00')).toBe('17 december 1995') + }) + + test('ISO date year/month/day', () => { + expect(formatDate('2023-03-01')).toBe('1 maart 2023') + }) + + test('ISO date year/month', () => { + expect(formatDate('2021-01')).toBe('1 januari 2021') + }) + + test('ISO date year', () => { + expect(formatDate('2021')).toBe('1 januari 2021') + }) + + test('ISO date UTC', () => { + expect(formatDate('2021-01-01T12:00:00Z')).toBe('1 januari 2021') + }) + + test('ISO date GMT+2', () => { + expect(formatDate('2021-01-01T12:00:00+02:00')).toBe('1 januari 2021') + }) + + test('Date format with slashes', () => { + expect(formatDate('01/01/2021')).toBe('1 januari 2021') + }) + + test('Date format MMM DD YYYY', () => { + expect(formatDate('Jan 01 2021')).toBe('1 januari 2021') + }) + + test(`value of 'null'`, () => { + //@ts-ignore + expect(formatDate(null)).toBe('') + }) + + test(`value of 'undefined'`, () => { + //@ts-ignore + expect(formatDate(undefined)).toBe('') + }) +}) diff --git a/src/utils/datetime/formatDate.ts b/src/utils/datetime/formatDate.ts index 639dfe3f4..01e20ec28 100644 --- a/src/utils/datetime/formatDate.ts +++ b/src/utils/datetime/formatDate.ts @@ -1,10 +1,10 @@ -import {dayjs} from '@/utils/datetime/dayjs' +import {type Dayjs, dayjs} from '@/utils/datetime/dayjs' /** * Converts string to date */ -export const formatDate = (date: string | number) => { - if (date === null || date === undefined) { +export const formatDate = (date: string | number | Dayjs) => { + if (date === null || date === undefined || date === '') { return '' } diff --git a/src/utils/datetime/formatDateTime.test.ts b/src/utils/datetime/formatDateTime.test.ts new file mode 100644 index 000000000..9e0ee8885 --- /dev/null +++ b/src/utils/datetime/formatDateTime.test.ts @@ -0,0 +1,51 @@ +import {formatDateTime} from '@/utils/datetime/formatDateTime' + +describe('formatDateTime', () => { + test('Test datum', () => { + expect(formatDateTime('December 17, 1995 03:24:00')).toBe( + '17 december 1995 03:24:00', + ) + }) + + test('ISO date year/month/day', () => { + expect(formatDateTime('2023-03-01')).toBe('1 maart 2023 00:00:00') + }) + + test('ISO date year/month', () => { + expect(formatDateTime('2021-01')).toBe('1 januari 2021 00:00:00') + }) + + test('ISO date year', () => { + expect(formatDateTime('2021')).toBe('1 januari 2021 00:00:00') + }) + + test('ISO date UTC', () => { + expect(formatDateTime('2021-01-01T12:00:00Z')).toBe( + '1 januari 2021 13:00:00', + ) + }) + + test('ISO date GMT+2', () => { + expect(formatDateTime('2021-01-01T12:00:00+02:00')).toBe( + '1 januari 2021 11:00:00', + ) + }) + + test('Date format with slashes', () => { + expect(formatDateTime('01/01/2021')).toBe('1 januari 2021 00:00:00') + }) + + test('Date format MMM DD YYYY', () => { + expect(formatDateTime('Jan 01 2021')).toBe('1 januari 2021 00:00:00') + }) + + test(`value of 'null'`, () => { + //@ts-ignore + expect(formatDateTime(null)).toBe('') + }) + + test(`value of 'undefined'`, () => { + //@ts-ignore + expect(formatDateTime(undefined)).toBe('') + }) +}) diff --git a/src/utils/datetime/formatDateTime.ts b/src/utils/datetime/formatDateTime.ts new file mode 100644 index 000000000..d3e1b74de --- /dev/null +++ b/src/utils/datetime/formatDateTime.ts @@ -0,0 +1,12 @@ +import {type Dayjs, dayjs} from '@/utils/datetime/dayjs' + +/** + * Converts string to date + */ +export const formatDateTime = (date: string | number | Dayjs) => { + if (date === null || date === undefined || date === '') { + return '' + } + + return dayjs(date).format('D MMMM YYYY HH:mm:ss') +} diff --git a/src/utils/datetime/formatDateToDisplay.test.ts b/src/utils/datetime/formatDateToDisplay.test.ts new file mode 100644 index 000000000..ed36ce82b --- /dev/null +++ b/src/utils/datetime/formatDateToDisplay.test.ts @@ -0,0 +1,16 @@ +import {formatDateToDisplay} from '@/utils/datetime/formatDateToDisplay' + +describe('formatDateToDisplay', () => { + it('should format and cut the date string correctly', () => { + expect(formatDateToDisplay('2023-01-01')).toBe('1 januari') + }) + + it('should return an empty string for an empty input', () => { + expect(formatDateToDisplay('')).toBe('') + }) + + it('should return null or undefined for an empty input', () => { + expect(formatDateToDisplay(null as unknown as string)).toBe('') + expect(formatDateToDisplay(undefined as unknown as string)).toBe('') + }) +}) From 6909e1a5f4ff38efe1e550967e00491b3bfb3a68 Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Thu, 31 Oct 2024 14:22:47 +0100 Subject: [PATCH 2/7] implement native iOS retrieveTranscript --- .../ios/SalesforceMessagingInApp.mm | 62 +++++++++++++++++++ .../src/index.ts | 3 + .../src/types.ts | 1 + 3 files changed, 66 insertions(+) diff --git a/react-native-salesforce-messaging-in-app/ios/SalesforceMessagingInApp.mm b/react-native-salesforce-messaging-in-app/ios/SalesforceMessagingInApp.mm index 2bae5ad92..9ff34ba03 100644 --- a/react-native-salesforce-messaging-in-app/ios/SalesforceMessagingInApp.mm +++ b/react-native-salesforce-messaging-in-app/ios/SalesforceMessagingInApp.mm @@ -518,6 +518,13 @@ @implementation SalesforceMessagingInApp reject:(RCTPromiseRejectBlock)reject) { @try { + if (conversationClient == nil) { + NSError *error = [NSError errorWithDomain:@"ConversationClient Not Initialized" + code:500 + userInfo:@{NSLocalizedDescriptionKey: @"ConversationClient is not initialized."}]; + reject(@"send_image_exception", @"ConversationClient is not initialized", error); + return; + } // Decode the Base64 string into NSData NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64Image options:0]; @@ -544,6 +551,61 @@ @implementation SalesforceMessagingInApp } } +// Method to send the Base64-encoded image +RCT_EXPORT_METHOD(retrieveTranscript:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + @try { + if (conversationClient == nil) { + NSError *error = [NSError errorWithDomain:@"ConversationClient Not Initialized" + code:500 + userInfo:@{NSLocalizedDescriptionKey: @"ConversationClient is not initialized."}]; + reject(@"retrieve_transcript_exception", @"ConversationClient is not initialized", error); + return; + } + // Call retrieveTranscript on the conversationClient + [conversationClient retrieveTranscript:^(PDFDocument * _Nullable pdfDocument, NSError * _Nullable error) { + if (error != nil) { + // Handle error by rejecting the promise + reject(@"retrieve_transcript_error", @"Failed to retrieve transcript", error); + return; + } + + if (pdfDocument != nil) { + // Convert PDFDocument to NSData + NSData *pdfData = [pdfDocument dataRepresentation]; + + if (pdfData == nil) { + // Handle error in case PDF data is nil + NSError *dataError = [NSError errorWithDomain:@"retrieveTranscript" + code:500 + userInfo:@{NSLocalizedDescriptionKey: @"Failed to retrieve PDF data"}]; + reject(@"retrieve_transcript_data_error", @"Failed to retrieve PDF data", dataError); + return; + } + + // Encode the PDF data to a Base64 string + NSString *base64PdfString = [pdfData base64EncodedStringWithOptions:0]; + + // Resolve the promise with the Base64-encoded PDF string + resolve(base64PdfString); + } else { + // Handle case where pdfDocument is nil, though no error was returned + NSError *noPdfError = [NSError errorWithDomain:@"retrieveTranscript" + code:500 + userInfo:@{NSLocalizedDescriptionKey: @"No PDF document returned"}]; + reject(@"retrieve_transcript_no_pdf", @"No PDF document returned", noPdfError); + } + }]; + } @catch (NSException *exception) { + // Catch and handle any exceptions by rejecting the promise + NSError *exceptionError = [NSError errorWithDomain:@"retrieveTranscriptException" + code:500 + userInfo:@{NSLocalizedDescriptionKey: [exception reason]}]; + reject(@"retrieve_transcript_exception", @"An exception occurred during retrieveTranscript", exceptionError); + } +} + RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(generateUUID) { NSUUID *uuid; diff --git a/react-native-salesforce-messaging-in-app/src/index.ts b/react-native-salesforce-messaging-in-app/src/index.ts index f850bf4e1..ac59b3b6a 100644 --- a/react-native-salesforce-messaging-in-app/src/index.ts +++ b/react-native-salesforce-messaging-in-app/src/index.ts @@ -81,6 +81,9 @@ export const sendImage = (imageBase64: string, fileName: string) => export const retrieveRemoteConfiguration = () => SalesforceMessagingInApp.retrieveRemoteConfiguration() +export const retrieveTranscript = () => + SalesforceMessagingInApp.retrieveTranscript() + export const generateUUID = () => SalesforceMessagingInApp.generateUUID() export const submitRemoteConfiguration = ( diff --git a/react-native-salesforce-messaging-in-app/src/types.ts b/react-native-salesforce-messaging-in-app/src/types.ts index 8832f3af2..e946a3bde 100644 --- a/react-native-salesforce-messaging-in-app/src/types.ts +++ b/react-native-salesforce-messaging-in-app/src/types.ts @@ -95,6 +95,7 @@ export type NativeSalesforceMessagingInApp = { generateUUID: () => string removeListeners: (count: number) => void retrieveRemoteConfiguration: () => Promise + retrieveTranscript: () => Promise sendImage: (imageBase64: string, fileName: string) => Promise sendMessage: (message: string) => Promise sendPDF: (filePath: string) => Promise From 1cbd4527cefcdda9a39b694868b20666f106fbb8 Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Thu, 31 Oct 2024 14:23:27 +0100 Subject: [PATCH 3/7] add file system dependencies --- .config/jest-init.js | 2 ++ ios/Podfile.lock | 6 ++++++ package-lock.json | 18 +++++++++++++++--- package.json | 12 ++++++++---- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.config/jest-init.js b/.config/jest-init.js index 4aeff3263..7e3a6a934 100644 --- a/.config/jest-init.js +++ b/.config/jest-init.js @@ -78,3 +78,5 @@ jest.mock('react-native-block-screenshot', () => ({})) jest.mock('react-native-salesforce-messaging-in-app', () => ({})) jest.mock('expo-document-picker', () => ({})) jest.mock('expo-image-picker', () => ({})) +jest.mock('expo-file-system', () => ({})) +jest.mock('expo-sharing', () => ({})) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index abd31bb1a..32a99c536 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -67,6 +67,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoSharing (12.0.1): + - ExpoModulesCore - FBLazyVector (0.74.3) - Firebase (10.29.0): - Firebase/Core (= 10.29.0) @@ -1583,6 +1585,7 @@ DEPENDENCIES: - ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoScreenOrientation (from `../node_modules/expo-screen-orientation/ios`) + - ExpoSharing (from `../node_modules/expo-sharing/ios`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - Firebase - FirebaseCore @@ -1723,6 +1726,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" ExpoScreenOrientation: :path: "../node_modules/expo-screen-orientation/ios" + ExpoSharing: + :path: "../node_modules/expo-sharing/ios" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" fmt: @@ -1900,6 +1905,7 @@ SPEC CHECKSUMS: ExpoLocalAuthentication: 9e02a56a4cf9868f0052656a93d4c94101a42ed7 ExpoModulesCore: 734c1802786b23c9598f4d15273753a779969368 ExpoScreenOrientation: 5d6a977177dc2f904cc072b51a0d37d0983c0d6a + ExpoSharing: 8db05dd85081219f75989a3db2c92fe5e9741033 FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2 Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda diff --git a/package-lock.json b/package-lock.json index 8b7066603..cc6d6f876 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,10 +31,12 @@ "dayjs": "^1.11.11", "expo": "^51.0.0", "expo-clipboard": "^6.0.3", - "expo-document-picker": "^12.0.2", - "expo-image-picker": "^15.0.7", + "expo-document-picker": "~12.0.2", + "expo-file-system": "~17.0.1", + "expo-image-picker": "~15.0.7", "expo-local-authentication": "~14.0.1", - "expo-screen-orientation": "^7.0.5", + "expo-screen-orientation": "~7.0.5", + "expo-sharing": "~12.0.1", "jwt-decode": "^4.0.0", "pascal-case": "^3.1.2", "picoquery": "^1.4.0", @@ -19563,6 +19565,7 @@ "version": "17.0.1", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz", "integrity": "sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==", + "license": "MIT", "peerDependencies": { "expo": "*" } @@ -19719,6 +19722,15 @@ "expo": "*" } }, + "node_modules/expo-sharing": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-12.0.1.tgz", + "integrity": "sha512-wBT+WeXwapj/9NWuLJO01vi9bdlchYu/Q/xD8slL/Ls4vVYku8CPqzkTtDFcjLrjtlJqyeHsdQXwKLvORmBIew==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/express": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", diff --git a/package.json b/package.json index 049e5275e..d83d0678e 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,12 @@ "dayjs": "^1.11.11", "expo": "^51.0.0", "expo-clipboard": "^6.0.3", - "expo-document-picker": "^12.0.2", - "expo-image-picker": "^15.0.7", + "expo-document-picker": "~12.0.2", + "expo-file-system": "~17.0.1", + "expo-image-picker": "~15.0.7", "expo-local-authentication": "~14.0.1", - "expo-screen-orientation": "^7.0.5", + "expo-screen-orientation": "~7.0.5", + "expo-sharing": "~12.0.1", "jwt-decode": "^4.0.0", "pascal-case": "^3.1.2", "picoquery": "^1.4.0", @@ -171,7 +173,9 @@ "plugins": [ "expo-local-authentication", "expo-document-picker", - "expo-image-picker" + "expo-image-picker", + "expo-file-system", + "expo-sharing" ] }, "volta": { From 976ab90644324b645756c084a4abc22287348603 Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Thu, 31 Oct 2024 14:24:04 +0100 Subject: [PATCH 4/7] save chat history pdf in react native --- src/modules/chat/components/ChatHeader.tsx | 16 ++++- src/modules/chat/components/ChatMenu.tsx | 71 ++++++++++++++++++++-- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/modules/chat/components/ChatHeader.tsx b/src/modules/chat/components/ChatHeader.tsx index 7aede9e7d..d42ccf4ef 100644 --- a/src/modules/chat/components/ChatHeader.tsx +++ b/src/modules/chat/components/ChatHeader.tsx @@ -8,6 +8,7 @@ import {Box} from '@/components/ui/containers/Box' import {Row} from '@/components/ui/layout/Row' import {Icon} from '@/components/ui/media/Icon' import {ScreenTitle} from '@/components/ui/text/ScreenTitle' +import {useToggle} from '@/hooks/useToggle' import {MeatballsMenu} from '@/modules/chat/assets/MeatballsMenu' import {ChatMenu} from '@/modules/chat/components/ChatMenu' import {useChat} from '@/modules/chat/slice' @@ -37,7 +38,11 @@ const PressableWhenMinimized = ({ export const ChatHeader = () => { const {isMaximized, toggleVisibility} = useChat() - const [isChatMenuVisible, setChatMenuVisible] = useState(false) + const { + value: isChatMenuVisible, + toggle: toggleIsChatMenuVisible, + disable: hideChatMenu, + } = useToggle(false) const [height, setHeight] = useState(0) const {color} = useTheme() @@ -79,7 +84,7 @@ export const ChatHeader = () => { color={color.pressable.secondary.default.icon} /> } - onPress={() => setChatMenuVisible(visibility => !visibility)} + onPress={toggleIsChatMenuVisible} pointerEvents={isMaximized ? 'auto' : 'none'} testID="ChatHeaderMeatballsMenuButton" /> @@ -102,7 +107,12 @@ export const ChatHeader = () => { - {!!isChatMenuVisible && } + {!!isChatMenuVisible && ( + + )} ) } diff --git a/src/modules/chat/components/ChatMenu.tsx b/src/modules/chat/components/ChatMenu.tsx index 1dfa9eb1a..9b8d59e5e 100644 --- a/src/modules/chat/components/ChatMenu.tsx +++ b/src/modules/chat/components/ChatMenu.tsx @@ -1,17 +1,29 @@ -import {StyleSheet} from 'react-native' +import { + StorageAccessFramework, + writeAsStringAsync, + EncodingType, + documentDirectory, +} from 'expo-file-system' +import {shareAsync} from 'expo-sharing' +import {Alert, Platform, StyleSheet} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {retrieveTranscript} from 'react-native-salesforce-messaging-in-app/src' import {Pressable} from '@/components/ui/buttons/Pressable' import {Box} from '@/components/ui/containers/Box' import {Column} from '@/components/ui/layout/Column' import {Phrase} from '@/components/ui/text/Phrase' +import {devLog} from '@/processes/development' import {Theme} from '@/themes/themes' import {useTheme} from '@/themes/useTheme' +import {dayjs} from '@/utils/datetime/dayjs' +import {formatDateTime} from '@/utils/datetime/formatDateTime' type Props = { + close: () => void headerHeight: number } -export const ChatMenu = ({headerHeight}: Props) => { +export const ChatMenu = ({headerHeight, close}: Props) => { const theme = useTheme() const sheetStyles = createStyles(theme, headerHeight) @@ -21,7 +33,54 @@ export const ChatMenu = ({headerHeight}: Props) => { exiting={FadeOut.duration(theme.duration.transition.short)} style={sheetStyles.container}> - + { + try { + close() + const result = await retrieveTranscript() + const fileName = `Chatgeschiedenis ${formatDateTime(dayjs())}.pdf` + const mimeType = 'application/pdf' + let uri: string | undefined + + if (Platform.OS === 'android') { + const permissions = + await StorageAccessFramework.requestDirectoryPermissionsAsync() + + if (permissions.granted) { + uri = await StorageAccessFramework.createFileAsync( + permissions.directoryUri, + fileName, + mimeType, + ).then( + async safUri => { + await writeAsStringAsync(safUri, result, { + encoding: EncodingType.Base64, + }) + + return safUri + }, + () => undefined, + ) + } + } + + if (!uri) { + uri = `${documentDirectory}${fileName}` + await writeAsStringAsync(uri, result, { + encoding: EncodingType.Base64, + }) + void shareAsync(uri, { + mimeType, + UTI: 'com.adobe.pdf', + }) + } + + devLog('saved to file', uri) + } catch (error) { + Alert.alert('Chat downloaden mislukt') + } + }} + testID="ChatMenuPressableDownloadChat"> @@ -32,7 +91,11 @@ export const ChatMenu = ({headerHeight}: Props) => { - + { + close() + }} + testID="ChatMenuPressableStopChat"> From a152e845aee74d0c6f54e82823bb149983b37bf5 Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Thu, 31 Oct 2024 15:06:20 +0100 Subject: [PATCH 5/7] fix timezone problems for tests --- jest.config.ts | 2 ++ src/utils/datetime/formatDateTime.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index b55254469..68fe24bfe 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,7 @@ import {Config} from 'jest' +process.env.TZ = 'UTC+1' + const config: Config = { preset: 'react-native', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], diff --git a/src/utils/datetime/formatDateTime.test.ts b/src/utils/datetime/formatDateTime.test.ts index 9e0ee8885..86fb94d96 100644 --- a/src/utils/datetime/formatDateTime.test.ts +++ b/src/utils/datetime/formatDateTime.test.ts @@ -21,13 +21,13 @@ describe('formatDateTime', () => { test('ISO date UTC', () => { expect(formatDateTime('2021-01-01T12:00:00Z')).toBe( - '1 januari 2021 13:00:00', + '1 januari 2021 11:00:00', ) }) test('ISO date GMT+2', () => { expect(formatDateTime('2021-01-01T12:00:00+02:00')).toBe( - '1 januari 2021 11:00:00', + '1 januari 2021 09:00:00', ) }) From a94744c900ea388f1eb19bcae63f49f93461af24 Mon Sep 17 00:00:00 2001 From: Frank Filius Date: Mon, 4 Nov 2024 13:46:34 +0100 Subject: [PATCH 6/7] Make download possible for Android --- .../SalesforceMessagingInAppModule.kt | 34 +++++++++++++++++++ .../oldarch/SalesforceMessagingInAppSpec.kt | 19 +++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/react-native-salesforce-messaging-in-app/android/src/main/java/com/salesforcemessaginginapp/SalesforceMessagingInAppModule.kt b/react-native-salesforce-messaging-in-app/android/src/main/java/com/salesforcemessaginginapp/SalesforceMessagingInAppModule.kt index 0164cf2bc..257449f43 100644 --- a/react-native-salesforce-messaging-in-app/android/src/main/java/com/salesforcemessaginginapp/SalesforceMessagingInAppModule.kt +++ b/react-native-salesforce-messaging-in-app/android/src/main/java/com/salesforcemessaginginapp/SalesforceMessagingInAppModule.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.net.URI @@ -800,6 +801,39 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat } } + @ReactMethod + override fun retrieveTranscript(promise: Promise) { + try { + if (conversationClient == null) { + promise.reject("Error", "conversationClient not created.") + return + } + scope.launch { + try { + val result = conversationClient?.retrieveTranscript() + if (result is Result.Success) { + val inputStream = result.data + val byteArrayOutputStream = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead) + } + val byteArray = byteArrayOutputStream.toByteArray() + val base64String = Base64.encodeToString(byteArray, Base64.DEFAULT) + promise.resolve(base64String) + } else { + promise.reject("Error", result.toString()) + } + } catch (e: Exception) { + promise.reject("Error", e.message, e) + } + } + } catch (e: Exception) { + promise.reject("Error", "An error occurred: ${e.message}", e) + } + } + private fun encodeImageAssetToMap(imageAsset: FileAsset.ImageAsset): ReadableMap { val file = imageAsset.file ?: throw IllegalArgumentException("File cannot be null") val bytes = file.readBytes() diff --git a/react-native-salesforce-messaging-in-app/android/src/oldarch/SalesforceMessagingInAppSpec.kt b/react-native-salesforce-messaging-in-app/android/src/oldarch/SalesforceMessagingInAppSpec.kt index 99dd39c81..98c64e95d 100644 --- a/react-native-salesforce-messaging-in-app/android/src/oldarch/SalesforceMessagingInAppSpec.kt +++ b/react-native-salesforce-messaging-in-app/android/src/oldarch/SalesforceMessagingInAppSpec.kt @@ -6,17 +6,23 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReadableMap abstract class SalesforceMessagingInAppSpec internal constructor(context: ReactApplicationContext) : - ReactContextBaseJavaModule(context) { + ReactContextBaseJavaModule(context) { abstract fun createCoreClient( - url: String, - organizationId: String, - developerName: String, - promise: Promise, + url: String, + organizationId: String, + developerName: String, + promise: Promise, ) + abstract fun generateUUID(): String abstract fun retrieveRemoteConfiguration(promise: Promise) - abstract fun submitRemoteConfiguration(remoteConfiguration: ReadableMap, createConversationOnSubmit: Boolean, promise: Promise) + abstract fun submitRemoteConfiguration( + remoteConfiguration: ReadableMap, + createConversationOnSubmit: Boolean, + promise: Promise, + ) + abstract fun createConversationClient(clientID: String?, promise: Promise) abstract fun sendMessage(message: String, promise: Promise) abstract fun sendReply(choice: ReadableMap, promise: Promise) @@ -26,4 +32,5 @@ abstract class SalesforceMessagingInAppSpec internal constructor(context: ReactA abstract fun checkIfInBusinessHours(promise: Promise) abstract fun addListener(eventName: String) abstract fun removeListeners(count: Int) + abstract fun retrieveTranscript(promise: Promise) } From ce8aa7dfc1cee1b7430cfc8acbdee83a8b2760f2 Mon Sep 17 00:00:00 2001 From: Rik Scheffer Date: Mon, 4 Nov 2024 14:49:00 +0100 Subject: [PATCH 7/7] Move Chat menu items to separate components --- src/modules/chat/components/ChatMenu.tsx | 100 +++---------------- src/modules/chat/components/ChatMenuItem.tsx | 27 +++++ src/modules/chat/utils/downloadChat.ts | 58 +++++++++++ 3 files changed, 100 insertions(+), 85 deletions(-) create mode 100644 src/modules/chat/components/ChatMenuItem.tsx create mode 100644 src/modules/chat/utils/downloadChat.ts diff --git a/src/modules/chat/components/ChatMenu.tsx b/src/modules/chat/components/ChatMenu.tsx index 9b8d59e5e..946df2c97 100644 --- a/src/modules/chat/components/ChatMenu.tsx +++ b/src/modules/chat/components/ChatMenu.tsx @@ -1,22 +1,10 @@ -import { - StorageAccessFramework, - writeAsStringAsync, - EncodingType, - documentDirectory, -} from 'expo-file-system' -import {shareAsync} from 'expo-sharing' -import {Alert, Platform, StyleSheet} from 'react-native' +import {StyleSheet} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' -import {retrieveTranscript} from 'react-native-salesforce-messaging-in-app/src' -import {Pressable} from '@/components/ui/buttons/Pressable' -import {Box} from '@/components/ui/containers/Box' import {Column} from '@/components/ui/layout/Column' -import {Phrase} from '@/components/ui/text/Phrase' -import {devLog} from '@/processes/development' +import {ChatMenuItem} from '@/modules/chat/components/ChatMenuItem' +import {downloadChat} from '@/modules/chat/utils/downloadChat' import {Theme} from '@/themes/themes' import {useTheme} from '@/themes/useTheme' -import {dayjs} from '@/utils/datetime/dayjs' -import {formatDateTime} from '@/utils/datetime/formatDateTime' type Props = { close: () => void @@ -33,79 +21,21 @@ export const ChatMenu = ({headerHeight, close}: Props) => { exiting={FadeOut.duration(theme.duration.transition.short)} style={sheetStyles.container}> - { - try { - close() - const result = await retrieveTranscript() - const fileName = `Chatgeschiedenis ${formatDateTime(dayjs())}.pdf` - const mimeType = 'application/pdf' - let uri: string | undefined - - if (Platform.OS === 'android') { - const permissions = - await StorageAccessFramework.requestDirectoryPermissionsAsync() - - if (permissions.granted) { - uri = await StorageAccessFramework.createFileAsync( - permissions.directoryUri, - fileName, - mimeType, - ).then( - async safUri => { - await writeAsStringAsync(safUri, result, { - encoding: EncodingType.Base64, - }) - - return safUri - }, - () => undefined, - ) - } - } - - if (!uri) { - uri = `${documentDirectory}${fileName}` - await writeAsStringAsync(uri, result, { - encoding: EncodingType.Base64, - }) - void shareAsync(uri, { - mimeType, - UTI: 'com.adobe.pdf', - }) - } - - devLog('saved to file', uri) - } catch (error) { - Alert.alert('Chat downloaden mislukt') - } - }} - testID="ChatMenuPressableDownloadChat"> - - - Chat downloaden - - - - { close() + void downloadChat() }} - testID="ChatMenuPressableStopChat"> - - - Chat stoppen - - - + testID="ChatMenuPressableDownloadChat" + /> + ) diff --git a/src/modules/chat/components/ChatMenuItem.tsx b/src/modules/chat/components/ChatMenuItem.tsx new file mode 100644 index 000000000..31029dc7d --- /dev/null +++ b/src/modules/chat/components/ChatMenuItem.tsx @@ -0,0 +1,27 @@ +import {Pressable} from '@/components/ui/buttons/Pressable' +import {Box} from '@/components/ui/containers/Box' +import {Phrase} from '@/components/ui/text/Phrase' +import {type TestProps} from '@/components/ui/types' +import {type Theme} from '@/themes/themes' + +type Props = { + color?: keyof Theme['color']['text'] + label: string + onPress: () => void +} & TestProps + +export const ChatMenuItem = ({onPress, testID, label, color}: Props) => ( + + + + {label} + + + +) diff --git a/src/modules/chat/utils/downloadChat.ts b/src/modules/chat/utils/downloadChat.ts new file mode 100644 index 000000000..dcd1da23d --- /dev/null +++ b/src/modules/chat/utils/downloadChat.ts @@ -0,0 +1,58 @@ +import { + StorageAccessFramework, + writeAsStringAsync, + EncodingType, + documentDirectory, +} from 'expo-file-system' +import {shareAsync} from 'expo-sharing' +import {Platform, Alert} from 'react-native' +import {retrieveTranscript} from 'react-native-salesforce-messaging-in-app/src' +import {devLog} from '@/processes/development' +import {dayjs} from '@/utils/datetime/dayjs' +import {formatDateTime} from '@/utils/datetime/formatDateTime' + +export const downloadChat = async () => { + try { + const result = await retrieveTranscript() + const fileName = `Chatgeschiedenis ${formatDateTime(dayjs()).replaceAll(':', ' ')}.pdf` + const mimeType = 'application/pdf' + let uri: string | undefined + + if (Platform.OS === 'android') { + const permissions = + await StorageAccessFramework.requestDirectoryPermissionsAsync() + + if (permissions.granted) { + uri = await StorageAccessFramework.createFileAsync( + permissions.directoryUri, + fileName, + mimeType, + ).then( + async safUri => { + await writeAsStringAsync(safUri, result, { + encoding: EncodingType.Base64, + }) + + return safUri + }, + () => undefined, + ) + } + } + + if (!uri) { + uri = `${documentDirectory}${fileName}` + await writeAsStringAsync(uri, result, { + encoding: EncodingType.Base64, + }) + void shareAsync(uri, { + mimeType, + UTI: 'com.adobe.pdf', + }) + } + + devLog('saved to file', uri) + } catch (error) { + Alert.alert('Chat downloaden mislukt') + } +}