Skip to content

Commit

Permalink
Merged PR 35429: Download en sla de chat historie op
Browse files Browse the repository at this point in the history
- add and improve date formatting utils
- implement native iOS retrieveTranscript
- add file system dependencies
- save chat history pdf in react native

Related work items: #129562
  • Loading branch information
RikSchefferAmsterdam committed Nov 4, 2024
2 parents 84aa08e + ce8aa7d commit 86a84df
Show file tree
Hide file tree
Showing 20 changed files with 390 additions and 75 deletions.
2 changes: 2 additions & 0 deletions .config/jest-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({}))
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1900,6 +1905,7 @@ SPEC CHECKSUMS:
ExpoLocalAuthentication: 9e02a56a4cf9868f0052656a93d4c94101a42ed7
ExpoModulesCore: 734c1802786b23c9598f4d15273753a779969368
ExpoScreenOrientation: 5d6a977177dc2f904cc072b51a0d37d0983c0d6a
ExpoSharing: 8db05dd85081219f75989a3db2c92fe5e9741033
FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda
Expand Down
2 changes: 2 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -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'],
Expand Down
18 changes: 15 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -171,7 +173,9 @@
"plugins": [
"expo-local-authentication",
"expo-document-picker",
"expo-image-picker"
"expo-image-picker",
"expo-file-system",
"expo-sharing"
]
},
"volta": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -812,6 +813,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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,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];

Expand All @@ -570,6 +577,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;
Expand Down
3 changes: 3 additions & 0 deletions react-native-salesforce-messaging-in-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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 = (
Expand Down
1 change: 1 addition & 0 deletions react-native-salesforce-messaging-in-app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export type NativeSalesforceMessagingInApp = {
generateUUID: () => string
removeListeners: (count: number) => void
retrieveRemoteConfiguration: () => Promise<RemoteConfiguration>
retrieveTranscript: () => Promise<string>
sendImage: (imageBase64: string, fileName: string) => Promise<void>
sendMessage: (message: string) => Promise<void>
sendPDF: (filePath: string, fileName: string) => Promise<void>
Expand Down
16 changes: 13 additions & 3 deletions src/modules/chat/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
/>
Expand All @@ -102,7 +107,12 @@ export const ChatHeader = () => {
</Row>
</PressableWhenMinimized>
</Box>
{!!isChatMenuVisible && <ChatMenu headerHeight={height} />}
{!!isChatMenuVisible && (
<ChatMenu
close={hideChatMenu}
headerHeight={height}
/>
)}
</View>
)
}
Expand Down
Loading

0 comments on commit 86a84df

Please sign in to comment.