diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f45fe3..78fc514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,46 @@ # Changelog +## 3.10.0 - Released 30 Jul 2024 + +- [Source] Parley now uses Kotlin at certain parts. Make sure to configure Kotlin in your project in case it doesn't use Kotlin yet. +- [Send Media] Fixed an issue that could cause media to be send twice when using Android 14 or higher. +- [Send Media] Added support for sending PDF files when using clientApi version 1.6 or higher. +- [Chat Message] Added support for PDF documents within the chat. +- [Styling] *Addition*: Added `parley_compose_media_icon` to `ParleyComposeView`. By default this is a `+` icon to send media within the chat (camera/gallery/document). +- [Styling] *Addition*: Added `parley_compose_media_icon_tint` to `ParleyComposeView`. Since this now reflects what it is referring to. +- [Styling] *DELETION*: Removed `parley_compose_camera_tint` from `ParleyComposeView`. Use `parley_compose_media_icon_tint` instead. +- [Styling] *DELETION*: Removed `parley_compose_camera_icon` from `ParleyComposeView`. Use `parley_compose_media_icon` instead. +- [Styling] *DEPRECATION*: Replace `parley_images_enabled` with `parley_media_enabled`. +- [Styling] *REPLACED*: Replaced `parley_images_enabled` style attribute with `parley_media_enabled`. +- [Styling] *REPLACED*: Replaced `parley_ic_camera` icon from the drawables with `parley_ic_add`. +- [Styling] *REPLACED*: Replaced `parley_action_divider_margin_*` with `parley_divider_margin_*`. +- [Styling] *REPLACED*: Replaced `parley_action_divider_color` with `parley_divider_color`. +- [Styling] *REPLACED*: Replaced `parley_agent_action_divider_margin_*` with `parley_agent_divider_margin_*`. +- [Styling] *Addition*: Added `parley_user_divider_margin_*` (also as `parley_divider_margin_*` for the style `ParleyMessageUserStyle`). +- [Styling] *Addition*: Added `parley_user_divider_color` (also as `parley_divider_color` for the style `ParleyMessageUserStyle`). +- [Styling] *Addition*: Added `parley_file_name_font_family` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_name_font_style` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_name_text_size` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_name_text_color` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_action_font_family` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_action_font_style` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_action_text_size` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Styling] *Addition*: Added `parley_file_action_text_color` (for the styles `ParleyMessageUserStyle` and `ParleyMessageAgentStyle`). +- [Strings] *Addition*: Added `parley_message_file_downloading`. +- [Strings] *Addition*: Added `parley_media_select`. +- [Strings] *Addition*: Added `parley_media_camera`. +- [Strings] *Addition*: Added `parley_media_gallery`. +- [Strings] *Addition*: Added `parley_media_document`. +- [Strings] *Addition*: Added `parley_general_open`. +- [Strings] *DELETION*: Removed `parley_photo`. +- [Strings] *DELETION*: Removed `parley_select_photo`. +- [Strings] *DELETION*: Removed `parley_take_photo`. +- [Api Version] *DELETION*: Removed support for clientApi version 1.0 and 1.1. + ## 3.9.7 - 16 Jul 2024 -- Fixed an issue that could cause images to be send twice when using Android 14 or higher. -- Updated TrustKit to version 1.1.5. +- [Send Media] Fixed an issue that could cause media to be send twice when using Android 14 or higher. +- [Dependency] Updated TrustKit to version 1.1.5. ## 3.9.6 - 14 Jun 2024 diff --git a/README.md b/README.md index d16169b..57d3e20 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ Empty | Conversation ## Requirements -- Java 11 (Example project uses Java 17) +- Java 17 +- Kotlin 1.9.24 - Android 5+ (API 21+) - Android target API 34 (Android 14) - Using AndroidX artifacts @@ -56,7 +57,7 @@ allprojects { To integrate Parley, specify the following in your `app/build.gradle` file: ```groovy -implementation 'com.github.parley-messaging:android-library:3.9.7' +implementation 'com.github.parley-messaging:android-library:3.10.0' ``` ### Upgrading @@ -381,6 +382,23 @@ public void `onRequestPermissionsResult`(int requestCode, @NonNull String[] perm > Note: Now that since the Fragment is handling these results now, the Activity no longer needs to forward the `onActivityResult` and `onRequestPermissionsResult` methods to Parley. So these can be removed from the Activity. +### Handling Downloads + +By default Parley uses the `DefaultParleyDownloadCallback` which downloads files by using the native `DownloadManager` and stores them in the internal storage of the app. After downloading, it will offer the user to open the downloaded file by using the `ParleyLaunchCallback`. + +To change this default behavior, the download callback can be overridden by a custom implementation: + +```java +parleyView.setDownloadCallback(new ParleyDownloadCallback() { + @Override + public void launchParleyDownload(String url, Map headers) { + // ... + } +}); +``` + +> Note: Make sure to apply the headers when downloading the file from the given url. Otherwise the file download will not succeed. + ## Customize ### Callbacks diff --git a/app/build.gradle b/app/build.gradle index ed5c35b..a07b682 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' android { namespace "nu.parley" @@ -8,7 +9,7 @@ android { minSdkVersion 21 targetSdk 34 versionCode 1 - versionName "3.9.6" + versionName "3.10.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -24,10 +25,14 @@ android { buildFeatures { buildConfig true } + kotlinOptions { + jvmTarget = '17' + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.core:core-ktx:1.13.1' // AndroidX implementation 'androidx.appcompat:appcompat:1.6.1' @@ -37,7 +42,7 @@ dependencies { implementation "com.google.firebase:firebase-messaging:23.4.1" // Library -// implementation 'com.github.parley-messaging:android-library:3.9.7' // Remote +// implementation 'com.github.parley-messaging:android-library:3.10.0' // Remote implementation project(':parley') // Local // Tests @@ -46,4 +51,4 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } -apply plugin: 'com.google.gms.google-services' \ No newline at end of file +apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/java/nu/parley/ui/ChatActivity.java b/app/src/main/java/nu/parley/ui/ChatActivity.java index c9466b3..84a9384 100644 --- a/app/src/main/java/nu/parley/ui/ChatActivity.java +++ b/app/src/main/java/nu/parley/ui/ChatActivity.java @@ -34,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { * All of these settings are optional. */ private void setParleyViewSettings() { -// parleyView.setImagesEnabled(false); // Optional, default `true` +// parleyView.setMediaEnabled(false); // Optional, default `true` // parleyView.setNotificationsPosition(ParleyPosition.Vertical.BOTTOM); // Optional, default `TOP` parleyView.setListener(() -> Log.d("ChatActivity", "The user did sent a message")); diff --git a/build.gradle b/build.gradle index 7b0cf7e..44aa6f7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,7 @@ buildscript { + ext { + kotlin_version = '1.9.24' + } repositories { google() jcenter() @@ -6,6 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.2' classpath 'com.google.gms:google-services:4.4.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/fastlane/test/message/Current/AgentMessage-Document.png b/fastlane/test/message/Current/AgentMessage-Document.png new file mode 100644 index 0000000..a6d6626 Binary files /dev/null and b/fastlane/test/message/Current/AgentMessage-Document.png differ diff --git a/fastlane/test/message/Current/AgentMessage-FullMessageWithActions.png b/fastlane/test/message/Current/AgentMessage-FullMessageWithActions.png index dcc8ba6..eacb693 100644 Binary files a/fastlane/test/message/Current/AgentMessage-FullMessageWithActions.png and b/fastlane/test/message/Current/AgentMessage-FullMessageWithActions.png differ diff --git a/fastlane/test/message/Current/AgentMessage-FullMessageWithTitle.png b/fastlane/test/message/Current/AgentMessage-FullMessageWithTitle.png index 48e7029..e222763 100644 Binary files a/fastlane/test/message/Current/AgentMessage-FullMessageWithTitle.png and b/fastlane/test/message/Current/AgentMessage-FullMessageWithTitle.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageFailure.png b/fastlane/test/message/Current/AgentMessage-ImageFailure.png index ef600b9..b7f8d3a 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageFailure.png and b/fastlane/test/message/Current/AgentMessage-ImageFailure.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageRemoteDark.png b/fastlane/test/message/Current/AgentMessage-ImageRemoteDark.png index f492b24..c04f13a 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageRemoteDark.png and b/fastlane/test/message/Current/AgentMessage-ImageRemoteDark.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageRemoteGifWithoutName.png b/fastlane/test/message/Current/AgentMessage-ImageRemoteGifWithoutName.png index d2b0143..e806680 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageRemoteGifWithoutName.png and b/fastlane/test/message/Current/AgentMessage-ImageRemoteGifWithoutName.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageRemoteLight.png b/fastlane/test/message/Current/AgentMessage-ImageRemoteLight.png index 82ddba8..cbb30f4 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageRemoteLight.png and b/fastlane/test/message/Current/AgentMessage-ImageRemoteLight.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageRemoteTransparency.png b/fastlane/test/message/Current/AgentMessage-ImageRemoteTransparency.png index a048758..35a8daa 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageRemoteTransparency.png and b/fastlane/test/message/Current/AgentMessage-ImageRemoteTransparency.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ImageWithActions.png b/fastlane/test/message/Current/AgentMessage-ImageWithActions.png index e7a42ff..aaceefc 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ImageWithActions.png and b/fastlane/test/message/Current/AgentMessage-ImageWithActions.png differ diff --git a/fastlane/test/message/Current/AgentMessage-Long.png b/fastlane/test/message/Current/AgentMessage-Long.png index ae5619b..5d16cce 100644 Binary files a/fastlane/test/message/Current/AgentMessage-Long.png and b/fastlane/test/message/Current/AgentMessage-Long.png differ diff --git a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselFull.png b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselFull.png index 110c16a..ddbabcb 100644 Binary files a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselFull.png and b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselFull.png differ diff --git a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselImages.png b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselImages.png index d7681f6..9443a31 100644 Binary files a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselImages.png and b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselImages.png differ diff --git a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselOnly.png b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselOnly.png index 1b04b0f..ac44896 100644 Binary files a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselOnly.png and b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselOnly.png differ diff --git a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselSmall.png b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselSmall.png index 5f15b01..02930ab 100644 Binary files a/fastlane/test/message/Current/AgentMessage-MessageWithCarouselSmall.png and b/fastlane/test/message/Current/AgentMessage-MessageWithCarouselSmall.png differ diff --git a/fastlane/test/message/Current/AgentMessage-OnlyActions.png b/fastlane/test/message/Current/AgentMessage-OnlyActions.png index e139d6f..57aa671 100644 Binary files a/fastlane/test/message/Current/AgentMessage-OnlyActions.png and b/fastlane/test/message/Current/AgentMessage-OnlyActions.png differ diff --git a/fastlane/test/message/Current/AgentMessage-OnlyActionsWithName.png b/fastlane/test/message/Current/AgentMessage-OnlyActionsWithName.png index 0348cab..30bfede 100644 Binary files a/fastlane/test/message/Current/AgentMessage-OnlyActionsWithName.png and b/fastlane/test/message/Current/AgentMessage-OnlyActionsWithName.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ShortWithName.png b/fastlane/test/message/Current/AgentMessage-ShortWithName.png index 30c17e0..3908a12 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ShortWithName.png and b/fastlane/test/message/Current/AgentMessage-ShortWithName.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ShortWithTitle.png b/fastlane/test/message/Current/AgentMessage-ShortWithTitle.png index 0b5ddef..45c813c 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ShortWithTitle.png and b/fastlane/test/message/Current/AgentMessage-ShortWithTitle.png differ diff --git a/fastlane/test/message/Current/AgentMessage-ShortWithoutName.png b/fastlane/test/message/Current/AgentMessage-ShortWithoutName.png index 93a9c0c..b2104b0 100644 Binary files a/fastlane/test/message/Current/AgentMessage-ShortWithoutName.png and b/fastlane/test/message/Current/AgentMessage-ShortWithoutName.png differ diff --git a/fastlane/test/message/Current/AgentMessage-TextAndDocument.png b/fastlane/test/message/Current/AgentMessage-TextAndDocument.png new file mode 100644 index 0000000..81dd6b9 Binary files /dev/null and b/fastlane/test/message/Current/AgentMessage-TextAndDocument.png differ diff --git a/fastlane/test/message/Current/AgentMessage-TextAndDocumentAndActions.png b/fastlane/test/message/Current/AgentMessage-TextAndDocumentAndActions.png new file mode 100644 index 0000000..393437d Binary files /dev/null and b/fastlane/test/message/Current/AgentMessage-TextAndDocumentAndActions.png differ diff --git a/fastlane/test/message/Current/AgentMessage-TextAndImage.png b/fastlane/test/message/Current/AgentMessage-TextAndImage.png index 6bfde9c..aeaf2db 100644 Binary files a/fastlane/test/message/Current/AgentMessage-TextAndImage.png and b/fastlane/test/message/Current/AgentMessage-TextAndImage.png differ diff --git a/fastlane/test/message/Current/AgentMessage-TextWithActions.png b/fastlane/test/message/Current/AgentMessage-TextWithActions.png index c16f338..3c06afd 100644 Binary files a/fastlane/test/message/Current/AgentMessage-TextWithActions.png and b/fastlane/test/message/Current/AgentMessage-TextWithActions.png differ diff --git a/fastlane/test/message/Current/RenderOrder-ImageTextImage.png b/fastlane/test/message/Current/RenderOrder-ImageTextImage.png index ebca7b7..ff05e50 100644 Binary files a/fastlane/test/message/Current/RenderOrder-ImageTextImage.png and b/fastlane/test/message/Current/RenderOrder-ImageTextImage.png differ diff --git a/fastlane/test/message/Current/UserMessage-Document.png b/fastlane/test/message/Current/UserMessage-Document.png new file mode 100644 index 0000000..a4f602c Binary files /dev/null and b/fastlane/test/message/Current/UserMessage-Document.png differ diff --git a/fastlane/test/message/Current/UserMessage-DocumentPending.png b/fastlane/test/message/Current/UserMessage-DocumentPending.png new file mode 100644 index 0000000..16430e9 Binary files /dev/null and b/fastlane/test/message/Current/UserMessage-DocumentPending.png differ diff --git a/fastlane/test/message/Current/UserMessage-ImageFailure.png b/fastlane/test/message/Current/UserMessage-ImageFailure.png index 4f827b1..758e49d 100644 Binary files a/fastlane/test/message/Current/UserMessage-ImageFailure.png and b/fastlane/test/message/Current/UserMessage-ImageFailure.png differ diff --git a/fastlane/test/message/Current/UserMessage-ImageRemoteDark.png b/fastlane/test/message/Current/UserMessage-ImageRemoteDark.png index ce7db7b..ec7c9c6 100644 Binary files a/fastlane/test/message/Current/UserMessage-ImageRemoteDark.png and b/fastlane/test/message/Current/UserMessage-ImageRemoteDark.png differ diff --git a/fastlane/test/message/Current/UserMessage-ImageRemoteGif.png b/fastlane/test/message/Current/UserMessage-ImageRemoteGif.png index e321372..46d0864 100644 Binary files a/fastlane/test/message/Current/UserMessage-ImageRemoteGif.png and b/fastlane/test/message/Current/UserMessage-ImageRemoteGif.png differ diff --git a/fastlane/test/message/Current/UserMessage-ImageRemoteLight.png b/fastlane/test/message/Current/UserMessage-ImageRemoteLight.png index 4df212b..97629c8 100644 Binary files a/fastlane/test/message/Current/UserMessage-ImageRemoteLight.png and b/fastlane/test/message/Current/UserMessage-ImageRemoteLight.png differ diff --git a/fastlane/test/message/Current/UserMessage-ImageRemoteTransparency.png b/fastlane/test/message/Current/UserMessage-ImageRemoteTransparency.png index f07c697..10c7384 100644 Binary files a/fastlane/test/message/Current/UserMessage-ImageRemoteTransparency.png and b/fastlane/test/message/Current/UserMessage-ImageRemoteTransparency.png differ diff --git a/fastlane/test/message/Current/UserMessage-LongSuccess.png b/fastlane/test/message/Current/UserMessage-LongSuccess.png index d67391f..536cb6b 100644 Binary files a/fastlane/test/message/Current/UserMessage-LongSuccess.png and b/fastlane/test/message/Current/UserMessage-LongSuccess.png differ diff --git a/fastlane/test/message/Current/UserMessage-LongSuccessWithMarkdown.png b/fastlane/test/message/Current/UserMessage-LongSuccessWithMarkdown.png index 2ddd2e8..ebba13e 100644 Binary files a/fastlane/test/message/Current/UserMessage-LongSuccessWithMarkdown.png and b/fastlane/test/message/Current/UserMessage-LongSuccessWithMarkdown.png differ diff --git a/fastlane/test/message/Current/UserMessage-ShortFailed.png b/fastlane/test/message/Current/UserMessage-ShortFailed.png index f7b94b6..71002b6 100644 Binary files a/fastlane/test/message/Current/UserMessage-ShortFailed.png and b/fastlane/test/message/Current/UserMessage-ShortFailed.png differ diff --git a/fastlane/test/message/Current/UserMessage-ShortPending.png b/fastlane/test/message/Current/UserMessage-ShortPending.png index 1b10be1..d764616 100644 Binary files a/fastlane/test/message/Current/UserMessage-ShortPending.png and b/fastlane/test/message/Current/UserMessage-ShortPending.png differ diff --git a/fastlane/test/message/Current/UserMessage-TextAndDocument.png b/fastlane/test/message/Current/UserMessage-TextAndDocument.png new file mode 100644 index 0000000..1cad914 Binary files /dev/null and b/fastlane/test/message/Current/UserMessage-TextAndDocument.png differ diff --git a/fastlane/test/message/Current/UserMessage-TextAndImage.png b/fastlane/test/message/Current/UserMessage-TextAndImage.png index 36fdd66..38a8fda 100644 Binary files a/fastlane/test/message/Current/UserMessage-TextAndImage.png and b/fastlane/test/message/Current/UserMessage-TextAndImage.png differ diff --git a/fastlane/test/message/diff.md b/fastlane/test/message/diff.md index f5f49f2..3743a4c 100644 --- a/fastlane/test/message/diff.md +++ b/fastlane/test/message/diff.md @@ -1,33 +1,39 @@ -Current | Updated --- | -- -![Current](Current/UserMessage-LongSuccess.png) | ![Updated](Update/UserMessage-LongSuccess.png) -![Current](Current/AgentMessage-ImageRemoteGifWithoutName.png) | ![Updated](Update/AgentMessage-ImageRemoteGifWithoutName.png) -![Current](Current/AgentMessage-TextAndImage.png) | ![Updated](Update/AgentMessage-TextAndImage.png) -![Current](Current/AgentMessage-ImageRemoteDark.png) | ![Updated](Update/AgentMessage-ImageRemoteDark.png) -![Current](Current/AgentMessage-Long.png) | ![Updated](Update/AgentMessage-Long.png) -![Current](Current/UserMessage-ImageFailure.png) | ![Updated](Update/UserMessage-ImageFailure.png) -![Current](Current/AgentMessage-ImageWithActions.png) | ![Updated](Update/AgentMessage-ImageWithActions.png) -![Current](Current/UserMessage-ImageRemoteLight.png) | ![Updated](Update/UserMessage-ImageRemoteLight.png) -![Current](Current/UserMessage-LongSuccessWithMarkdown.png) | ![Updated](Update/UserMessage-LongSuccessWithMarkdown.png) -![Current](Current/UserMessage-ImageRemoteDark.png) | ![Updated](Update/UserMessage-ImageRemoteDark.png) -![Current](Current/RenderOrder-ImageTextImage.png) | ![Updated](Update/RenderOrder-ImageTextImage.png) -![Current](Current/AgentMessage-ImageFailure.png) | ![Updated](Update/AgentMessage-ImageFailure.png) -![Current](Current/UserMessage-ImageRemoteTransparency.png) | ![Updated](Update/UserMessage-ImageRemoteTransparency.png) -![Current](Current/AgentMessage-FullMessageWithActions.png) | ![Updated](Update/AgentMessage-FullMessageWithActions.png) -![Current](Current/UserMessage-ShortPending.png) | ![Updated](Update/UserMessage-ShortPending.png) -![Current](Current/AgentMessage-MessageWithCarouselSmall.png) | ![Updated](Update/AgentMessage-MessageWithCarouselSmall.png) -![Current](Current/UserMessage-ImageRemoteGif.png) | ![Updated](Update/UserMessage-ImageRemoteGif.png) -![Current](Current/AgentMessage-ShortWithTitle.png) | ![Updated](Update/AgentMessage-ShortWithTitle.png) -![Current](Current/AgentMessage-TextWithActions.png) | ![Updated](Update/AgentMessage-TextWithActions.png) -![Current](Current/AgentMessage-FullMessageWithTitle.png) | ![Updated](Update/AgentMessage-FullMessageWithTitle.png) -![Current](Current/AgentMessage-MessageWithCarouselFull.png) | ![Updated](Update/AgentMessage-MessageWithCarouselFull.png) -![Current](Current/AgentMessage-ShortWithName.png) | ![Updated](Update/AgentMessage-ShortWithName.png) -![Current](Current/AgentMessage-ImageRemoteLight.png) | ![Updated](Update/AgentMessage-ImageRemoteLight.png) -![Current](Current/AgentMessage-MessageWithCarouselImages.png) | ![Updated](Update/AgentMessage-MessageWithCarouselImages.png) -![Current](Current/AgentMessage-ImageRemoteTransparency.png) | ![Updated](Update/AgentMessage-ImageRemoteTransparency.png) -![Current](Current/AgentMessage-ShortWithoutName.png) | ![Updated](Update/AgentMessage-ShortWithoutName.png) -![Current](Current/UserMessage-ShortFailed.png) | ![Updated](Update/UserMessage-ShortFailed.png) -![Current](Current/UserMessage-TextAndImage.png) | ![Updated](Update/UserMessage-TextAndImage.png) -![Current](Current/AgentMessage-MessageWithCarouselOnly.png) | ![Updated](Update/AgentMessage-MessageWithCarouselOnly.png) -![Current](Current/AgentMessage-OnlyActions.png) | ![Updated](Update/AgentMessage-OnlyActions.png) -![Current](Current/AgentMessage-OnlyActionsWithName.png) | ![Updated](Update/AgentMessage-OnlyActionsWithName.png) + Current | Updated +----------------------------------------------------------------|--------------------------------------------------------------- + ![Current](Current/UserMessage-LongSuccess.png) | ![Updated](Update/UserMessage-LongSuccess.png) + ![Current](Current/AgentMessage-ImageRemoteGifWithoutName.png) | ![Updated](Update/AgentMessage-ImageRemoteGifWithoutName.png) + ![Current](Current/AgentMessage-TextAndImage.png) | ![Updated](Update/AgentMessage-TextAndImage.png) + ![Current](Current/UserMessage-DocumentPending.png) | ![Updated](Update/UserMessage-DocumentPending.png) + ![Current](Current/AgentMessage-ImageRemoteDark.png) | ![Updated](Update/AgentMessage-ImageRemoteDark.png) + ![Current](Current/AgentMessage-Long.png) | ![Updated](Update/AgentMessage-Long.png) + ![Current](Current/UserMessage-ImageFailure.png) | ![Updated](Update/UserMessage-ImageFailure.png) + ![Current](Current/AgentMessage-ImageWithActions.png) | ![Updated](Update/AgentMessage-ImageWithActions.png) + ![Current](Current/AgentMessage-TextAndDocument.png) | ![Updated](Update/AgentMessage-TextAndDocument.png) + ![Current](Current/AgentMessage-TextAndDocumentAndActions.png) | ![Updated](Update/AgentMessage-TextAndDocumentAndActions.png) + ![Current](Current/RenderOrder-ImageTextImage.png) | ![Updated](Update/RenderOrder-ImageTextImage.png) + ![Current](Current/AgentMessage-ImageFailure.png) | ![Updated](Update/AgentMessage-ImageFailure.png) + ![Current](Current/UserMessage-ImageRemoteTransparency.png) | ![Updated](Update/UserMessage-ImageRemoteTransparency.png) + ![Current](Current/UserMessage-ImageRemoteLight.png) | ![Updated](Update/UserMessage-ImageRemoteLight.png) + ![Current](Current/AgentMessage-FullMessageWithActions.png) | ![Updated](Update/AgentMessage-FullMessageWithActions.png) + ![Current](Current/AgentMessage-OnlyActions.png) | ![Updated](Update/AgentMessage-OnlyActions.png) + ![Current](Current/UserMessage-ShortPending.png) | ![Updated](Update/UserMessage-ShortPending.png) + ![Current](Current/AgentMessage-MessageWithCarouselSmall.png) | ![Updated](Update/AgentMessage-MessageWithCarouselSmall.png) + ![Current](Current/UserMessage-ImageRemoteDark.png) | ![Updated](Update/UserMessage-ImageRemoteDark.png) + ![Current](Current/UserMessage-ImageRemoteGif.png) | ![Updated](Update/UserMessage-ImageRemoteGif.png) + ![Current](Current/AgentMessage-ShortWithTitle.png) | ![Updated](Update/AgentMessage-ShortWithTitle.png) + ![Current](Current/UserMessage-Document.png) | ![Updated](Update/UserMessage-Document.png) + ![Current](Current/UserMessage-TextAndDocument.png) | ![Updated](Update/UserMessage-TextAndDocument.png) + ![Current](Current/AgentMessage-TextWithActions.png) | ![Updated](Update/AgentMessage-TextWithActions.png) + ![Current](Current/UserMessage-LongSuccessWithMarkdown.png) | ![Updated](Update/UserMessage-LongSuccessWithMarkdown.png) + ![Current](Current/AgentMessage-FullMessageWithTitle.png) | ![Updated](Update/AgentMessage-FullMessageWithTitle.png) + ![Current](Current/AgentMessage-MessageWithCarouselFull.png) | ![Updated](Update/AgentMessage-MessageWithCarouselFull.png) + ![Current](Current/AgentMessage-MessageWithCarouselOnly.png) | ![Updated](Update/AgentMessage-MessageWithCarouselOnly.png) + ![Current](Current/AgentMessage-ShortWithName.png) | ![Updated](Update/AgentMessage-ShortWithName.png) + ![Current](Current/AgentMessage-ImageRemoteLight.png) | ![Updated](Update/AgentMessage-ImageRemoteLight.png) + ![Current](Current/AgentMessage-MessageWithCarouselImages.png) | ![Updated](Update/AgentMessage-MessageWithCarouselImages.png) + ![Current](Current/AgentMessage-ImageRemoteTransparency.png) | ![Updated](Update/AgentMessage-ImageRemoteTransparency.png) + ![Current](Current/AgentMessage-ShortWithoutName.png) | ![Updated](Update/AgentMessage-ShortWithoutName.png) + ![Current](Current/UserMessage-ShortFailed.png) | ![Updated](Update/UserMessage-ShortFailed.png) + ![Current](Current/AgentMessage-Document.png) | ![Updated](Update/AgentMessage-Document.png) + ![Current](Current/AgentMessage-OnlyActionsWithName.png) | ![Updated](Update/AgentMessage-OnlyActionsWithName.png) + ![Current](Current/UserMessage-TextAndImage.png) | ![Updated](Update/UserMessage-TextAndImage.png) \ No newline at end of file diff --git a/fastlane/test/suggestion/Current/Suggestions-DifferentSizes.png b/fastlane/test/suggestion/Current/Suggestions-DifferentSizes.png index 3984323..33f3b16 100644 Binary files a/fastlane/test/suggestion/Current/Suggestions-DifferentSizes.png and b/fastlane/test/suggestion/Current/Suggestions-DifferentSizes.png differ diff --git a/fastlane/test/suggestion/Current/Suggestions-Full.png b/fastlane/test/suggestion/Current/Suggestions-Full.png index bf56c61..366f9e6 100644 Binary files a/fastlane/test/suggestion/Current/Suggestions-Full.png and b/fastlane/test/suggestion/Current/Suggestions-Full.png differ diff --git a/fastlane/test/suggestion/Current/Suggestions-Multiline.png b/fastlane/test/suggestion/Current/Suggestions-Multiline.png index b1989e6..ccc50d6 100644 Binary files a/fastlane/test/suggestion/Current/Suggestions-Multiline.png and b/fastlane/test/suggestion/Current/Suggestions-Multiline.png differ diff --git a/fastlane/test/suggestion/Current/Suggestions-Small.png b/fastlane/test/suggestion/Current/Suggestions-Small.png index 95fd6b3..2d56d50 100644 Binary files a/fastlane/test/suggestion/Current/Suggestions-Small.png and b/fastlane/test/suggestion/Current/Suggestions-Small.png differ diff --git a/parley/build.gradle b/parley/build.gradle index 9464737..30d754e 100644 --- a/parley/build.gradle +++ b/parley/build.gradle @@ -1,8 +1,9 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' +apply plugin: 'org.jetbrains.kotlin.android' group = 'com.github.parley-messaging' -version = '3.9.7' +version = '3.10.0' android { namespace 'nu.parley.android' @@ -24,6 +25,13 @@ android { withSourcesJar() } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } } afterEvaluate { @@ -57,6 +65,7 @@ dependencies { // Glide implementation "com.github.bumptech.glide:glide:4.15.1" + implementation 'androidx.core:core-ktx:1.13.1' annotationProcessor "com.github.bumptech.glide:compiler:4.15.1" // TrustKit @@ -72,9 +81,9 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'com.novoda:espresso-support:1.0.0' + androidTestImplementation 'androidx.test:runner:1.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation('tools.fastlane:screengrab:2.1.0') } diff --git a/parley/src/androidTest/AndroidManifest.xml b/parley/src/androidTest/AndroidManifest.xml index ba989b5..927bcc4 100644 --- a/parley/src/androidTest/AndroidManifest.xml +++ b/parley/src/androidTest/AndroidManifest.xml @@ -13,5 +13,15 @@ tools:ignore="ProtectedPermissions" /> + android:theme="@style/Theme.AppCompat"> + + + + + + + + + \ No newline at end of file diff --git a/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.java b/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.java deleted file mode 100644 index 319d94c..0000000 --- a/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.java +++ /dev/null @@ -1,332 +0,0 @@ -package nu.parley.android; - - -import android.util.Log; -import android.widget.LinearLayout; - -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; - -import com.novoda.espresso.ViewTestRule; - -import org.junit.AfterClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -import nu.parley.android.data.model.Action; -import nu.parley.android.data.model.Message; -import nu.parley.android.data.model.MockAction; -import nu.parley.android.data.model.MockAgent; -import nu.parley.android.data.model.MockMessage; -import nu.parley.android.view.chat.MessageViewHolderFactory; -import nu.parley.android.view.chat.holder.ParleyBaseViewHolder; - -@RunWith(AndroidJUnit4ClassRunner.class) -public class ParleyChatTestSuit extends ParleyScreenBaseTest { - - private static final String URL_PARLEY_INVALID = "https://parley"; - private static final String URL_PARLEY_LOGO_COLORED = "https://www.tracebuzz.com/assets/images/parley-blog.jpg"; - private static final String URL_PARLEY_LOGO_LIGHT = "https://media.licdn.com/dms/image/C560BAQGaitUb5D_v9Q/company-logo_200_200/0?e=2159024400&v=beta&t=zQCzNT4cnFEiEfzKkzBaBaGfK5rapGGXNKLFjZYFcH4"; - private static final String URL_PARLEY_IMAGE_TRANSPARENCY = "https://www.parley.nu/images/tab2.png"; - private static final String URL_PARLEY_IMAGE_GIF = "https://media0.giphy.com/media/kEKcOWl8RMLde/giphy.gif"; - private static final String URL_PARLEY_IMAGE_PLATFORMS = "https://parley.nu/images/tab6.png"; - private static final String URL_PARLEY_IMAGE_WEB = "https://www.parley.nu/images/tab1_mobile.png"; - private static final String URL_PARLEY_IMAGE_PLATFORMS_TB = "http://www.socialmediatoolvergelijken.nl/tools/tracebuzz/img/tracebuzz_1.png"; - - @Rule - public ViewTestRule rule = new ViewTestRule<>(R.layout.item_message); - - @Test - public void userMessage_shortPending() { - renderMessage(MockMessage.textOfUser("Hello \uD83D\uDC4B", Message.SEND_STATUS_PENDING)); - makeScreenshot("UserMessage-ShortPending"); - } - - @Test - public void userMessage_shortFailed() { - renderMessage(MockMessage.textOfUser("I have a question", Message.SEND_STATUS_FAILED)); - makeScreenshot("UserMessage-ShortFailed"); - } - - @Test - public void userMessage_longSuccess() { - renderMessage(MockMessage.textOfUser("Is it possible to change the styling? We want the chat to have the same styling as our app.\n\nAlso, does it support emoji like ✨✔️?", Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-LongSuccess"); - } - - @Test - public void userMessage_longSuccessWithMarkdown() { - renderMessage(MockMessage.textOfUser("And does it support *Markdown* inside the **chat**? ***Checking it***", Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-LongSuccessWithMarkdown"); - } - -// @Test -// public void userMessage_imageLocal() { -// // TODO: Implement -// makeScreenshot("UserMessage-ImageLocal"); -// } - - @Test - public void userMessage_imageFailure() { - renderMessage(MockMessage.imageOfUser(URL_PARLEY_INVALID, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-ImageFailure"); - } - - @Test - public void userMessage_imageRemoteDark() { - renderMessage(MockMessage.imageOfUser(URL_PARLEY_LOGO_COLORED, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-ImageRemoteDark"); - } - - @Test - public void userMessage_imageRemoteLight() { - renderMessage(MockMessage.imageOfUser(URL_PARLEY_LOGO_LIGHT, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-ImageRemoteLight"); - } - - @Test - public void userMessage_imageRemoteTransparency() { - renderMessage(MockMessage.imageOfUser(URL_PARLEY_IMAGE_TRANSPARENCY, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-ImageRemoteTransparency"); - } - - @Test - public void userMessage_imageRemoteGif() { - renderMessage(MockMessage.imageOfUser(URL_PARLEY_IMAGE_GIF, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-ImageRemoteGif"); - } - - @Test - public void userMessage_textAndImage() { - renderMessage(MockMessage.messageOfUser("Look at this image", URL_PARLEY_LOGO_COLORED, Message.SEND_STATUS_SUCCESS)); - makeScreenshot("UserMessage-TextAndImage"); - } - - @Test - public void agentMessage_shortWithoutName() { - renderMessage(MockMessage.textOfAgent(null, "We will respond shortly")); - makeScreenshot("AgentMessage-ShortWithoutName"); - } - - @Test - public void agentMessage_shortWithName() { - renderMessage(MockMessage.textOfAgent(MockAgent.Webuildapps, "Hello John")); - makeScreenshot("AgentMessage-ShortWithName"); - } - - @Test - public void agentMessage_shortWithTitle() { - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, "Hello", "Welcome to *Parley* John", null)); - makeScreenshot("AgentMessage-ShortWithTitle"); - } - - @Test - public void agentMessage_long() { - renderMessage(MockMessage.textOfAgent(MockAgent.Webuildapps, "Yes, it is possible to **fully** change the styling. The *Parley* library provides a default style to get you started \uD83D\uDE80\n\nAs you already recognized, *Parley* supports *Markdown* in the chat \uD83D\uDC4D\n\nYou can also check out our [website](https://parley.nu)")); - makeScreenshot("AgentMessage-Long"); - } - - @Test - public void agentMessage_imageFailure() { - renderMessage(MockMessage.imageOfAgent(MockAgent.Webuildapps, URL_PARLEY_INVALID)); - makeScreenshot("AgentMessage-ImageFailure"); - } - - @Test - public void agentMessage_imageRemoteDark() { - renderMessage(MockMessage.imageOfAgent(MockAgent.Webuildapps, URL_PARLEY_LOGO_COLORED)); - makeScreenshot("AgentMessage-ImageRemoteDark"); - } - - @Test - public void agentMessage_imageRemoteLight() { - renderMessage(MockMessage.imageOfAgent(MockAgent.Webuildapps, URL_PARLEY_LOGO_LIGHT)); - makeScreenshot("AgentMessage-ImageRemoteLight"); - } - - @Test - public void agentMessage_imageRemoteTransparency() { - renderMessage(MockMessage.imageOfAgent(MockAgent.Webuildapps, URL_PARLEY_IMAGE_TRANSPARENCY)); - makeScreenshot("AgentMessage-ImageRemoteTransparency"); - } - - @Test - public void agentMessage_imageRemoteGifWithoutName() { - renderMessage(MockMessage.imageOfAgent(null, URL_PARLEY_IMAGE_GIF)); - makeScreenshot("AgentMessage-ImageRemoteGifWithoutName"); - } - - @Test - public void agentMessage_textAndImage() { - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, "This is an image of Parley", URL_PARLEY_LOGO_COLORED)); - makeScreenshot("AgentMessage-TextAndImage"); - } - - @Test - public void agentMessage_fullMessageWithTitle() { - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, "Welcome", "This is an image of Parley", URL_PARLEY_IMAGE_PLATFORMS)); - makeScreenshot("AgentMessage-FullMessageWithTitle"); - } - - @Test - public void agentMessage_textWithActions() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Open app", "open-app://parley.nu")); - actions.add(MockAction.create("Call us", "call://+31362022080")); - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, "Here are some quick actions for more information about *Parley*", null, actions, null)); - makeScreenshot("AgentMessage-TextWithActions"); - } - - @Test - public void agentMessage_imageWithActions() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Web documentation", "https://developers.parley.nu/docs/introduction")); - actions.add(MockAction.create("Android documentation", "https://developers.parley.nu/docs/introduction-1")); - actions.add(MockAction.create("iOS documentation", "https://developers.parley.nu/docs/introduction-2")); - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, null, URL_PARLEY_IMAGE_PLATFORMS, actions, null)); - makeScreenshot("AgentMessage-ImageWithActions"); - } - - @Test - public void agentMessage_fullMessageWithActions() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Open app", "open-app://parley.nu")); - actions.add(MockAction.create("Call us", "call://+31362022080")); - actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")); - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, "Welcome", "Here are some quick actions for more information about *Parley*", URL_PARLEY_LOGO_COLORED, actions, null)); - makeScreenshot("AgentMessage-FullMessageWithActions"); - } - - @Test - public void agentMessage_onlyActions() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Open app", "open-app://parley.nu")); - actions.add(MockAction.create("Call us", "call://+31362022080")); - actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")); - renderMessage(MockMessage.messageOfAgent(null, null, null, null, actions, null)); - makeScreenshot("AgentMessage-OnlyActions"); - } - - @Test - public void agentMessage_onlyActionsWithName() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Open app", "open-app://parley.nu")); - actions.add(MockAction.create("Call us", "call://+31362022080")); - actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")); - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, null, null, actions, null)); - makeScreenshot("AgentMessage-OnlyActionsWithName"); - } - - @Test - public void agentMessage_messageWithCarouselFull() { - List carousel = new ArrayList<>(); - List actionsItem1 = new ArrayList<>(); - actionsItem1.add(MockAction.create("Parley secure messaging", "https://www.parley.nu/en/secure-messaging")); - actionsItem1.add(MockAction.create("Developer documentation", "https://developers.parley.nu")); - List actionsItem2 = new ArrayList<>(); - actionsItem2.add(MockAction.create("Parley", "https://parley.nu")); - actionsItem2.add(MockAction.create("Webuildapps", "https://webuildapps.com")); - actionsItem2.add(MockAction.create("Tracebuzz", "https://tracebuzz.com")); - carousel.add(MockMessage.ofCarousel("Parley - Redefining Customer Happiness", "Fast and secure messaging with *Parley*. The Android and iOS library are available which enables you to fully customise the chat.", URL_PARLEY_LOGO_COLORED, actionsItem1)); - carousel.add(MockMessage.ofCarousel("More information", "Check out the following links", URL_PARLEY_IMAGE_PLATFORMS_TB, actionsItem2)); - - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, "Here are some quick actions for more information about *Parley*", null, null, carousel)); - makeScreenshot("AgentMessage-MessageWithCarouselFull"); - } - - @Test - public void agentMessage_messageWithCarouselSmall() { - List actionsMessage = new ArrayList<>(); - actionsMessage.add(MockAction.create("Home page", "https://www.parley.nu/")); - - List carousel = new ArrayList<>(); - List actionsItem1 = new ArrayList<>(); - actionsItem1.add(MockAction.create("Android SDK", "https://github.com/parley-messaging/android-library")); - actionsItem1.add(MockAction.create("iOS SDK", "https://github.com/parley-messaging/ios-library")); - List actionsItem2 = new ArrayList<>(); - actionsItem2.add(MockAction.create("Web documentation", "https://developers.parley.nu/docs/introduction")); - actionsItem2.add(MockAction.create("Android documentation", "https://developers.parley.nu/docs/introduction-1")); - actionsItem2.add(MockAction.create("iOS documentation", "https://developers.parley.nu/docs/introduction-2")); - carousel.add(MockMessage.ofCarousel("Parley libraries", "Parley provides open source SDK's for the Web, Android and iOS to easily integrate it with any platform.\n\nThe chat is fully customisable.", null, actionsItem1)); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_WEB, actionsItem2)); - - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, "Here are some quick actions for more information about *Parley*", URL_PARLEY_LOGO_COLORED, actionsMessage, carousel)); - makeScreenshot("AgentMessage-MessageWithCarouselSmall"); - } - - @Test - public void agentMessage_messageWithCarouselOnly() { - List carousel = new ArrayList<>(); - List actionsItem1 = new ArrayList<>(); - actionsItem1.add(MockAction.create("Android SDK", "https://github.com/parley-messaging/android-library")); - actionsItem1.add(MockAction.create("iOS SDK", "https://github.com/parley-messaging/ios-library")); - List actionsItem2 = new ArrayList<>(); - actionsItem2.add(MockAction.create("Web documentation", "https://developers.parley.nu/docs/introduction")); - actionsItem2.add(MockAction.create("Android documentation", "https://developers.parley.nu/docs/introduction-1")); - actionsItem2.add(MockAction.create("iOS documentation", "https://developers.parley.nu/docs/introduction-2")); - carousel.add(MockMessage.ofCarousel("Parley libraries", "Parley provides open source SDK's for the Web, Android and iOS to easily integrate it with any platform.\n\nThe chat is fully customisable.", null, actionsItem1)); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_WEB, actionsItem2)); - - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, null, null, null, carousel)); - makeScreenshot("AgentMessage-MessageWithCarouselOnly"); - } - - @Test - public void agentMessage_messageWithCarouselImages() { - List actions = new ArrayList<>(); - actions.add(MockAction.create("Home page", "https://www.parley.nu/")); - - List carousel = new ArrayList<>(); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_TRANSPARENCY, null)); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_WEB, null)); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_PLATFORMS, null)); - carousel.add(MockMessage.ofCarousel(null, null, URL_PARLEY_IMAGE_PLATFORMS_TB, null)); - - renderMessage(MockMessage.messageOfAgent(MockAgent.Webuildapps, null, null, URL_PARLEY_IMAGE_PLATFORMS, actions, carousel)); - makeScreenshot("AgentMessage-MessageWithCarouselImages"); - } - - // Rendering order - @Test - public void userMessage_renderOrder_imageTextImage() { - takeScreenshots = false; - userMessage_imageRemoteLight(); - userMessage_longSuccessWithMarkdown(); - userMessage_imageRemoteDark(); - takeScreenshots = true; - makeScreenshot("RenderOrder-ImageTextImage"); - } - - @AfterClass - public static void tearDown() { - // Make a report doc - StringBuilder markdownText = new StringBuilder("\n" + - "Current | Updated\n" + - "-- | --\n"); - for (String screenshot : screenshots) { - markdownText.append("![Current](Current/") - .append(screenshot) - .append(".png) | ![Updated](Update/") - .append(screenshot) - .append(".png)\n"); - } - Log.d("diff", markdownText.toString()); - } - - private void renderMessage(final Message message) { - rule.runOnMainSynchronously(new ViewTestRule.Runner() { - @Override - public void run(LinearLayout view) { - final ParleyBaseViewHolder viewHolder = MessageViewHolderFactory.getViewHolder(message.getTypeId(), rule.getView(), null); - viewHolder.show(message); - } - }); - sleepForVisual(0); -// sleepForVisual(1000); -// sleepForVisual(5000); -// sleepForVisual(60000); - } -} \ No newline at end of file diff --git a/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.kt b/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.kt new file mode 100644 index 0000000..c7df03c --- /dev/null +++ b/parley/src/androidTest/java/nu/parley/android/ParleyChatTestSuit.kt @@ -0,0 +1,635 @@ +package nu.parley.android + +import android.app.Activity +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import nu.parley.android.base.ParleyBaseViewTest +import nu.parley.android.data.model.Action +import nu.parley.android.data.model.Message +import nu.parley.android.data.model.MimeType +import nu.parley.android.util.mock.MockAction +import nu.parley.android.util.mock.MockAgent +import nu.parley.android.util.mock.MockMedia +import nu.parley.android.util.mock.MockMessage.agentCarousel +import nu.parley.android.util.mock.MockMessage.agentFull +import nu.parley.android.util.mock.MockMessage.agentFullImage +import nu.parley.android.util.mock.MockMessage.agentImage +import nu.parley.android.util.mock.MockMessage.agentMedia +import nu.parley.android.util.mock.MockMessage.agentMessageAndImage +import nu.parley.android.util.mock.MockMessage.agentMessageAndMedia +import nu.parley.android.util.mock.MockMessage.agentText +import nu.parley.android.util.mock.MockMessage.userImage +import nu.parley.android.util.mock.MockMessage.userMedia +import nu.parley.android.util.mock.MockMessage.userText +import nu.parley.android.util.mock.MockMessage.userTextAndImage +import nu.parley.android.util.mock.MockMessage.userTextAndMedia +import nu.parley.android.view.chat.MessageViewHolderFactory +import org.junit.AfterClass +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4ClassRunner::class) +class ParleyChatTestSuit : ParleyBaseViewTest() { + + companion object { + private const val UrlParleyInvalid = "https://parley.jpg" + private const val UrlParleyLogoColored = + "https://www.tracebuzz.com/assets/images/parley-blog.jpg" + private const val UrlParleyLogoLight = + "https://media.licdn.com/dms/image/C560BAQGaitUb5D_v9Q/company-logo_200_200/0?e=2159024400&v=beta&t=zQCzNT4cnFEiEfzKkzBaBaGfK5rapGGXNKLFjZYFcH4&ext=.png" + private const val UrlParleyImageTransparency = + "https://images.prismic.io/endeavour-parley/48f62e1a-4450-469b-abb6-fa0e00acb68a_Chat.png?auto=compress,format&h=500&ext=.png" + private const val UrlParleyImageGif = + "https://media0.giphy.com/media/kEKcOWl8RMLde/giphy.gif" + private const val UrlParleyImagePerson = + "https://images.prismic.io/endeavour-parley/05e079ec-2a7a-4069-9107-6518acb2879e_Scherm%C2%ADafbeelding+2022-12-16+om+11.14.43.png?auto=compress,format&ext=.png" + private const val UrlParleyImageWeb = + "https://www.tracebuzz.com/assets/images/parley_tab.png" + private const val UrlParleyImageSocials = + "https://images.prismic.io/endeavour-parley/72be8647-8b35-4b7e-ba6e-9e2c527d78f7_PARLEY_STILLS+-+SCENE_3_MESSAGING.jpg?auto=compress,format&h=500&ext=.png" + + @JvmStatic + @AfterClass + fun tearDown() { + // Make a report doc + val markdownText = StringBuilder( + """ + + Current | Updated + -- | -- + + """.trimIndent() + ) + for (screenshot in screenshots) { + markdownText.append("![Current](Current/") + .append(screenshot) + .append(".png) | ![Updated](Update/") + .append(screenshot) + .append(".png)\n") + } + Log.d("diff", markdownText.toString()) + } + } + + override fun createView(activity: Activity): View { + return LayoutInflater.from(activity).inflate(R.layout.item_message, null) + } + + @Test + fun userMessage_shortPending() { + renderMessage(userText("Hello \uD83D\uDC4B", Message.SEND_STATUS_PENDING)) + makeScreenshot("UserMessage-ShortPending") + } + + @Test + fun userMessage_shortFailed() { + renderMessage(userText("I have a question", Message.SEND_STATUS_FAILED)) + makeScreenshot("UserMessage-ShortFailed") + } + + @Test + fun userMessage_longSuccess() { + renderMessage( + userText( + "Is it possible to change the styling? We want the chat to have the same styling as our app.\n\nAlso, does it support emoji like ✨✔️?", + Message.SEND_STATUS_SUCCESS + ) + ) + makeScreenshot("UserMessage-LongSuccess") + } + + @Test + fun userMessage_longSuccessWithMarkdown() { + renderMessage( + userText( + "And does it support *Markdown* inside the **chat**? ***Checking it***", + Message.SEND_STATUS_SUCCESS + ) + ) + makeScreenshot("UserMessage-LongSuccessWithMarkdown") + } + + // @Test + // public void userMessage_imageLocal() { + // // TODO: Implement + // makeScreenshot("UserMessage-ImageLocal"); + // } + @Test + fun userMessage_imageFailure() { + renderMessage( + userImage( + UrlParleyInvalid, + Message.SEND_STATUS_SUCCESS + ) + ) + sleepForVisual(500) + makeScreenshot("UserMessage-ImageFailure") + } + + @Test + fun userMessage_imageRemoteDark() { + renderMessage(userImage(UrlParleyLogoColored, Message.SEND_STATUS_SUCCESS)) + sleepForVisual(500) + makeScreenshot("UserMessage-ImageRemoteDark") + } + + @Test + fun userMessage_imageRemoteLight() { + renderMessage(userImage(UrlParleyLogoLight, Message.SEND_STATUS_SUCCESS)) + sleepForVisual(500) + makeScreenshot("UserMessage-ImageRemoteLight") + } + + @Test + fun userMessage_imageRemoteTransparency() { + renderMessage(userImage(UrlParleyImageTransparency, Message.SEND_STATUS_SUCCESS)) + sleepForVisual(500) + makeScreenshot("UserMessage-ImageRemoteTransparency") + } + + @Test + fun userMessage_imageRemoteGif() { + renderMessage(userImage(UrlParleyImageGif, Message.SEND_STATUS_SUCCESS)) + sleepForVisual(500) + makeScreenshot("UserMessage-ImageRemoteGif") + } + + @Test + fun userMessage_textAndImage() { + renderMessage( + userTextAndImage( + "Look at this image", + UrlParleyLogoColored, + Message.SEND_STATUS_SUCCESS + ) + ) + sleepForVisual(500) + makeScreenshot("UserMessage-TextAndImage") + } + + @Test + fun userMessage_documentPending() { + renderMessage(userImage("/sample.pdf", Message.SEND_STATUS_SUCCESS)) + makeScreenshot("UserMessage-DocumentPending") + } + + @Test + fun userMessage_document() { + val media = MockMedia.document("document.pdf", MimeType.ApplicationPdf) + renderMessage(userMedia(media, Message.SEND_STATUS_SUCCESS)) + makeScreenshot("UserMessage-Document") + } + + @Test + fun userMessage_textAndDocument() { + renderMessage( + userTextAndMedia( + "Look at this document", + MockMedia.document("document.pdf", MimeType.ApplicationPdf), + Message.SEND_STATUS_SUCCESS, + ) + ) + makeScreenshot("UserMessage-TextAndDocument") + } + + @Test + fun agentMessage_shortWithoutName() { + renderMessage(agentText(null, "We will respond shortly")) + makeScreenshot("AgentMessage-ShortWithoutName") + } + + @Test + fun agentMessage_shortWithName() { + renderMessage(agentText(MockAgent.Webuildapps, "Hello John")) + makeScreenshot("AgentMessage-ShortWithName") + } + + @Test + fun agentMessage_shortWithTitle() { + renderMessage( + agentMessageAndMedia( + MockAgent.Webuildapps, + "Hello", + "Welcome to *Parley* John", + null + ) + ) + makeScreenshot("AgentMessage-ShortWithTitle") + } + + @Test + fun agentMessage_long() { + renderMessage( + agentText( + MockAgent.Webuildapps, + "Yes, it is possible to **fully** change the styling. The *Parley* library provides a default style to get you started \uD83D\uDE80\n\nAs you already recognized, *Parley* supports *Markdown* in the chat \uD83D\uDC4D\n\nYou can also check out our [website](https://parley.nu)" + ) + ) + makeScreenshot("AgentMessage-Long") + } + + @Test + fun agentMessage_imageFailure() { + renderMessage(agentImage(MockAgent.Webuildapps, UrlParleyInvalid)) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageFailure") + } + + @Test + fun agentMessage_imageRemoteDark() { + renderMessage(agentImage(MockAgent.Webuildapps, UrlParleyLogoColored)) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageRemoteDark") + } + + @Test + fun agentMessage_imageRemoteLight() { + renderMessage(agentImage(MockAgent.Webuildapps, UrlParleyLogoLight)) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageRemoteLight") + } + + @Test + fun agentMessage_imageRemoteTransparency() { + renderMessage(agentImage(MockAgent.Webuildapps, UrlParleyImageTransparency)) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageRemoteTransparency") + } + + @Test + fun agentMessage_imageRemoteGifWithoutName() { + renderMessage(agentImage(null, UrlParleyImageGif)) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageRemoteGifWithoutName") + } + + @Test + fun agentMessage_textAndImage() { + renderMessage( + agentMessageAndImage( + MockAgent.Webuildapps, + null, + "This is an image of Parley", + UrlParleyLogoColored + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-TextAndImage") + } + + @Test + fun agentMessage_document() { + val media = MockMedia.document("document.pdf", MimeType.ApplicationPdf) + renderMessage(agentMedia(MockAgent.Webuildapps, media)) + makeScreenshot("AgentMessage-Document") + } + + @Test + fun agentMessage_textAndDocument() { + renderMessage( + agentMessageAndMedia( + MockAgent.Webuildapps, + null, + "Look at this document", + MockMedia.document("document.pdf", MimeType.ApplicationPdf), + ) + ) + makeScreenshot("AgentMessage-TextAndDocument") + } + + @Test + fun agentMessage_textAndDocumentAndActions() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Open app", "open-app://parley.nu")) + actions.add(MockAction.create("Call us", "call://+31362022080")) + renderMessage( + agentFull( + MockAgent.Webuildapps, + null, + "Here are some quick actions for more information about *Parley*", + MockMedia.document("document.pdf", MimeType.ApplicationPdf), + actions, + null + ) + ) + makeScreenshot("AgentMessage-TextAndDocumentAndActions") + } + + @Test + fun agentMessage_fullMessageWithTitle() { + renderMessage( + agentMessageAndImage( + MockAgent.Webuildapps, + "Welcome", + "This is an image of Parley", + UrlParleyImagePerson + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-FullMessageWithTitle") + } + + @Test + fun agentMessage_textWithActions() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Open app", "open-app://parley.nu")) + actions.add(MockAction.create("Call us", "call://+31362022080")) + renderMessage( + agentFull( + MockAgent.Webuildapps, + null, + "Here are some quick actions for more information about *Parley*", + null, + actions, + null + ) + ) + makeScreenshot("AgentMessage-TextWithActions") + } + + @Test + fun agentMessage_imageWithActions() { + val actions: MutableList = ArrayList() + actions.add( + MockAction.create( + "Web documentation", + "https://developers.parley.nu/docs/introduction" + ) + ) + actions.add( + MockAction.create( + "Android documentation", + "https://developers.parley.nu/docs/introduction-1" + ) + ) + actions.add( + MockAction.create( + "iOS documentation", + "https://developers.parley.nu/docs/introduction-2" + ) + ) + renderMessage( + agentFullImage( + MockAgent.Webuildapps, + null, + null, + UrlParleyImagePerson, + actions, + null + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-ImageWithActions") + } + + @Test + fun agentMessage_fullMessageWithActions() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Open app", "open-app://parley.nu")) + actions.add(MockAction.create("Call us", "call://+31362022080")) + actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")) + renderMessage( + agentFullImage( + MockAgent.Webuildapps, + "Welcome", + "Here are some quick actions for more information about *Parley*", + UrlParleyLogoColored, + actions, + null + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-FullMessageWithActions") + } + + @Test + fun agentMessage_onlyActions() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Open app", "open-app://parley.nu")) + actions.add(MockAction.create("Call us", "call://+31362022080")) + actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")) + renderMessage(agentFull(null, null, null, null, actions, null)) + makeScreenshot("AgentMessage-OnlyActions") + } + + @Test + fun agentMessage_onlyActionsWithName() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Open app", "open-app://parley.nu")) + actions.add(MockAction.create("Call us", "call://+31362022080")) + actions.add(MockAction.create("Webuildapps", "https://webuildapps.com")) + renderMessage(agentFull(MockAgent.Webuildapps, null, null, null, actions, null)) + makeScreenshot("AgentMessage-OnlyActionsWithName") + } + + @Test + fun agentMessage_messageWithCarouselFull() { + val carousel: MutableList = ArrayList() + val actionsItem1: MutableList = ArrayList() + actionsItem1.add( + MockAction.create( + "Parley secure messaging", + "https://www.parley.nu/en/secure-messaging" + ) + ) + actionsItem1.add( + MockAction.create( + "Developer documentation", + "https://developers.parley.nu" + ) + ) + val actionsItem2: MutableList = ArrayList() + actionsItem2.add(MockAction.create("Parley", "https://parley.nu")) + actionsItem2.add(MockAction.create("Webuildapps", "https://webuildapps.com")) + actionsItem2.add(MockAction.create("Tracebuzz", "https://tracebuzz.com")) + carousel.add( + agentCarousel( + "Parley - Redefining Customer Happiness", + "Fast and secure messaging with *Parley*. The Android and iOS library are available which enables you to fully customise the chat.", + UrlParleyLogoColored, + actionsItem1 + ) + ) + carousel.add( + agentCarousel( + "More information", + "Check out the following links", + UrlParleyImageSocials, + actionsItem2 + ) + ) + + renderMessage( + agentFull( + MockAgent.Webuildapps, + null, + "Here are some quick actions for more information about *Parley*", + null, + null, + carousel + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-MessageWithCarouselFull") + } + + @Test + fun agentMessage_messageWithCarouselSmall() { + val actionsMessage: MutableList = ArrayList() + actionsMessage.add(MockAction.create("Home page", "https://www.parley.nu/")) + + val carousel: MutableList = ArrayList() + val actionsItem1: MutableList = ArrayList() + actionsItem1.add( + MockAction.create( + "Android SDK", + "https://github.com/parley-messaging/android-library" + ) + ) + actionsItem1.add( + MockAction.create( + "iOS SDK", + "https://github.com/parley-messaging/ios-library" + ) + ) + val actionsItem2: MutableList = ArrayList() + actionsItem2.add( + MockAction.create( + "Web documentation", + "https://developers.parley.nu/docs/introduction" + ) + ) + actionsItem2.add( + MockAction.create( + "Android documentation", + "https://developers.parley.nu/docs/introduction-1" + ) + ) + actionsItem2.add( + MockAction.create( + "iOS documentation", + "https://developers.parley.nu/docs/introduction-2" + ) + ) + carousel.add( + agentCarousel( + "Parley libraries", + "Parley provides open source SDK's for the Web, Android and iOS to easily integrate it with any platform.\n\nThe chat is fully customisable.", + null, + actionsItem1 + ) + ) + carousel.add(agentCarousel(null, null, UrlParleyImageWeb, actionsItem2)) + + renderMessage( + agentFullImage( + MockAgent.Webuildapps, + null, + "Here are some quick actions for more information about *Parley*", + UrlParleyLogoColored, + actionsMessage, + carousel + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-MessageWithCarouselSmall") + } + + @Test + fun agentMessage_messageWithCarouselOnly() { + val carousel: MutableList = ArrayList() + val actionsItem1: MutableList = ArrayList() + actionsItem1.add( + MockAction.create( + "Android SDK", + "https://github.com/parley-messaging/android-library" + ) + ) + actionsItem1.add( + MockAction.create( + "iOS SDK", + "https://github.com/parley-messaging/ios-library" + ) + ) + val actionsItem2: MutableList = ArrayList() + actionsItem2.add( + MockAction.create( + "Web documentation", + "https://developers.parley.nu/docs/introduction" + ) + ) + actionsItem2.add( + MockAction.create( + "Android documentation", + "https://developers.parley.nu/docs/introduction-1" + ) + ) + actionsItem2.add( + MockAction.create( + "iOS documentation", + "https://developers.parley.nu/docs/introduction-2" + ) + ) + carousel.add( + agentCarousel( + "Parley libraries", + "Parley provides open source SDK's for the Web, Android and iOS to easily integrate it with any platform.\n\nThe chat is fully customisable.", + null, + actionsItem1 + ) + ) + carousel.add(agentCarousel(null, null, UrlParleyImageWeb, actionsItem2)) + + renderMessage(agentFull(MockAgent.Webuildapps, null, null, null, null, carousel)) + sleepForVisual(500) + makeScreenshot("AgentMessage-MessageWithCarouselOnly") + } + + @Test + fun agentMessage_messageWithCarouselImages() { + val actions: MutableList = ArrayList() + actions.add(MockAction.create("Home page", "https://www.parley.nu/")) + + val carousel: MutableList = ArrayList() + carousel.add(agentCarousel(null, null, UrlParleyImageTransparency, null)) + carousel.add(agentCarousel(null, null, UrlParleyImageWeb, null)) + carousel.add(agentCarousel(null, null, UrlParleyImagePerson, null)) + carousel.add(agentCarousel(null, null, UrlParleyImageSocials, null)) + + renderMessage( + agentFullImage( + MockAgent.Webuildapps, + null, + null, + UrlParleyImagePerson, + actions, + carousel + ) + ) + sleepForVisual(500) + makeScreenshot("AgentMessage-MessageWithCarouselImages") + } + + // Rendering order + @Test + fun userMessage_renderOrder_imageTextImage() { + takeScreenshots = false + userMessage_imageRemoteLight() + userMessage_longSuccessWithMarkdown() + userMessage_imageRemoteDark() + takeScreenshots = true + makeScreenshot("RenderOrder-ImageTextImage") + } + + private fun renderMessage(message: Message) { + getView { view: View? -> + val viewHolder = MessageViewHolderFactory.getViewHolder( + message.typeId!!, view, null + ) + viewHolder.show(message) + } + } +} \ No newline at end of file diff --git a/parley/src/androidTest/java/nu/parley/android/ParleyScreenBaseTest.java b/parley/src/androidTest/java/nu/parley/android/ParleyScreenBaseTest.java deleted file mode 100644 index 85ffbb0..0000000 --- a/parley/src/androidTest/java/nu/parley/android/ParleyScreenBaseTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package nu.parley.android; - -import org.junit.Before; -import org.junit.BeforeClass; - -import java.util.ArrayList; -import java.util.List; - -import tools.fastlane.screengrab.Screengrab; -import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy; - -import static java.lang.Thread.sleep; - -public abstract class ParleyScreenBaseTest { - - static List screenshots; - static boolean takeScreenshots = true; - - @BeforeClass - public static void setup() { - Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy()); - screenshots = new ArrayList<>(); - } - - @Before - public void setInitialState() { - takeScreenshots = true; - } - - void sleepForVisual(int duration) { - try { - sleep(duration); - } catch (InterruptedException e) { - throw new RuntimeException("Interrupted"); - } - } - - void makeScreenshot(final String name) { - if (!takeScreenshots) { - return; - } - - sleepForVisual(600); - Screengrab.screenshot(name); - if (screenshots.contains(name)) { - throw new IllegalArgumentException("This screenshot name is already taken!"); - } - screenshots.add(name); - } -} diff --git a/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.java b/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.java deleted file mode 100644 index 37543ed..0000000 --- a/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package nu.parley.android; - -import android.util.Log; -import android.widget.FrameLayout; - -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; - -import com.novoda.espresso.ViewTestRule; - -import org.junit.AfterClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -import nu.parley.android.view.compose.suggestion.SuggestionView; - -@RunWith(AndroidJUnit4ClassRunner.class) -public class ParleySuggestionViewTest extends ParleyScreenBaseTest { - - @Rule - public ViewTestRule rule = new ViewTestRule<>(nu.parley.android.test.R.layout.view_suggestions_test); - - @Test - public void suggestions_small_shouldAlignRight() { - List suggestions = new ArrayList<>(); - suggestions.add("Yes"); - suggestions.add("No"); - - renderSuggestions(suggestions); - makeScreenshot("Suggestions-Small"); - } - - @Test - public void suggestions_big_shouldScroll() { - List suggestions = new ArrayList<>(); - suggestions.add("Accept"); - suggestions.add("Decline"); - suggestions.add("Maybe later"); - suggestions.add("Cancel"); - suggestions.add("More information"); - - renderSuggestions(suggestions); - makeScreenshot("Suggestions-Full"); - } - - @Test - public void suggestions_multiline() { - List suggestions = new ArrayList<>(); - suggestions.add("What meassures are taken for secure messaging?"); - suggestions.add("What are the possible styling options when integrating Parley?"); - - renderSuggestions(suggestions); - makeScreenshot("Suggestions-Multiline"); - } - - @Test - public void suggestions_differentSizes() { - List suggestions = new ArrayList<>(); - suggestions.add("I would like more information before making a choice"); - suggestions.add("Yes"); - suggestions.add("No"); - - renderSuggestions(suggestions); - makeScreenshot("Suggestions-DifferentSizes"); - } - - private void renderSuggestions(final List suggestions) { - rule.runOnMainSynchronously(new ViewTestRule.Runner() { - @Override - public void run(FrameLayout view) { - SuggestionView suggestionView = rule.getView().findViewById(R.id.suggestion_view); - suggestionView.setSuggestions(suggestions); - } - }); -// sleepForVisual(0); -// sleepForVisual(1000); -// sleepForVisual(5000); -// sleepForVisual(60000); - } - - @AfterClass - public static void tearDown() { - // Make a report doc - StringBuilder markdownText = new StringBuilder("\n" + - "Current | Updated\n" + - "-- | --\n"); - for (String screenshot : screenshots) { - markdownText.append("![Current](Current/") - .append(screenshot) - .append(".png) | ![Updated](Update/") - .append(screenshot) - .append(".png)\n"); - } - Log.d("diff", markdownText.toString()); - } -} diff --git a/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.kt b/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.kt new file mode 100644 index 0000000..e70f612 --- /dev/null +++ b/parley/src/androidTest/java/nu/parley/android/ParleySuggestionViewTest.kt @@ -0,0 +1,89 @@ +package nu.parley.android + +import android.app.Activity +import android.util.Log +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import nu.parley.android.base.ParleyBaseViewTest +import nu.parley.android.view.compose.suggestion.SuggestionView +import org.junit.AfterClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4ClassRunner::class) +class ParleySuggestionViewTest : ParleyBaseViewTest() { + override fun createView(activity: Activity): SuggestionView { + return SuggestionView(activity) + } + + @Test + fun suggestions_small_shouldAlignRight() { + val suggestions: MutableList = ArrayList() + suggestions.add("Yes") + suggestions.add("No") + + renderSuggestions(suggestions) + makeScreenshot("Suggestions-Small") + } + + @Test + fun suggestions_big_shouldScroll() { + val suggestions: MutableList = ArrayList() + suggestions.add("Accept") + suggestions.add("Decline") + suggestions.add("Maybe later") + suggestions.add("Cancel") + suggestions.add("More information") + + renderSuggestions(suggestions) + makeScreenshot("Suggestions-Full") + } + + @Test + fun suggestions_multiline() { + val suggestions: MutableList = ArrayList() + suggestions.add("What meassures are taken for secure messaging?") + suggestions.add("What are the possible styling options when integrating Parley?") + + renderSuggestions(suggestions) + makeScreenshot("Suggestions-Multiline") + } + + @Test + fun suggestions_differentSizes() { + val suggestions: MutableList = ArrayList() + suggestions.add("I would like more information before making a choice") + suggestions.add("Yes") + suggestions.add("No") + + renderSuggestions(suggestions) + makeScreenshot("Suggestions-DifferentSizes") + } + + private fun renderSuggestions(suggestions: List) { + getView { view -> view.setSuggestions(suggestions) } + } + + companion object { + @JvmStatic + @AfterClass + fun tearDown() { + // Make a report doc + val markdownText = StringBuilder( + """ + + Current | Updated + -- | -- + + """.trimIndent() + ) + for (screenshot in screenshots) { + markdownText.append("![Current](Current/") + .append(screenshot) + .append(".png) | ![Updated](Update/") + .append(screenshot) + .append(".png)\n") + } + Log.d("diff", markdownText.toString()) + } + } +} diff --git a/parley/src/androidTest/java/nu/parley/android/base/ParleyBaseViewTest.kt b/parley/src/androidTest/java/nu/parley/android/base/ParleyBaseViewTest.kt new file mode 100644 index 0000000..780bb2e --- /dev/null +++ b/parley/src/androidTest/java/nu/parley/android/base/ParleyBaseViewTest.kt @@ -0,0 +1,75 @@ +package nu.parley.android.base + +import android.app.Activity +import android.view.View +import androidx.test.ext.junit.rules.ActivityScenarioRule +import nu.parley.android.util.TestActivity +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy + +abstract class ParleyBaseViewTest { + @JvmField + @Rule + var activityScenarioRule: ActivityScenarioRule = ActivityScenarioRule( + TestActivity::class.java + ) + + private var view: T? = null + + abstract fun createView(activity: Activity): T + + protected fun getView(callback: (T) -> Unit) { + if (view == null) { + activityScenarioRule.scenario.onActivity { activity: TestActivity -> + view = createView(activity) + activity.setContentView(view) + view!!.post { + callback(requireNotNull(view)) + } + } + } else { + view!!.post { + callback(requireNotNull(view)) + } + } + } + + @Before + fun setInitialState() { + takeScreenshots = true + } + + fun sleepForVisual(duration: Int) { + try { + Thread.sleep(duration.toLong()) + } catch (e: InterruptedException) { + throw RuntimeException("Interrupted") + } + } + + fun makeScreenshot(name: String) { + if (!takeScreenshots) { + return + } + + sleepForVisual(600) + Screengrab.screenshot(name) + require(!screenshots!!.contains(name)) { "This screenshot name is already taken!" } + screenshots!!.add(name) + } + + companion object { + var screenshots = mutableListOf() + var takeScreenshots = true + + @JvmStatic + @BeforeClass + fun setup() { + Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy()) + screenshots = ArrayList() + } + } +} diff --git a/parley/src/androidTest/java/nu/parley/android/data/model/MockAction.java b/parley/src/androidTest/java/nu/parley/android/data/model/MockAction.java deleted file mode 100644 index cdef766..0000000 --- a/parley/src/androidTest/java/nu/parley/android/data/model/MockAction.java +++ /dev/null @@ -1,8 +0,0 @@ -package nu.parley.android.data.model; - -public final class MockAction { - - public static Action create(String title, String payload) { - return new Action(title, payload); - } -} diff --git a/parley/src/androidTest/java/nu/parley/android/data/model/MockAgent.java b/parley/src/androidTest/java/nu/parley/android/data/model/MockAgent.java deleted file mode 100644 index 1d96612..0000000 --- a/parley/src/androidTest/java/nu/parley/android/data/model/MockAgent.java +++ /dev/null @@ -1,16 +0,0 @@ -package nu.parley.android.data.model; - -import java.util.Random; - -public final class MockAgent { - - private static int generateRandomId() { - return new Random().nextInt(); - } - - private static Agent create(String name) { - return new Agent(name); - } - - public static Agent Webuildapps = create("Webuildapps"); -} diff --git a/parley/src/androidTest/java/nu/parley/android/data/model/MockMessage.java b/parley/src/androidTest/java/nu/parley/android/data/model/MockMessage.java deleted file mode 100644 index 1413738..0000000 --- a/parley/src/androidTest/java/nu/parley/android/data/model/MockMessage.java +++ /dev/null @@ -1,56 +0,0 @@ -package nu.parley.android.data.model; - -import androidx.annotation.Nullable; - -import java.util.Date; -import java.util.List; -import java.util.Random; - -import nu.parley.android.view.chat.MessageViewHolderFactory; - -public final class MockMessage { - - private static int generateRandomId() { - return new Random().nextInt(); - } - - private static long getCurrentTimeStamp() { - return new Date().getTime() / 1000; - } - - public static Message textOfUser(String text, int sendStatus) { - return messageOfUser(text, null, sendStatus); - } - - public static Message imageOfUser(String url, int sendStatus) { - return messageOfUser(null, url, sendStatus); - } - - public static Message textOfAgent(Agent agent, String text) { - return messageOfAgent(agent, null, text, null); - } - - public static Message imageOfAgent(Agent agent, String url) { - return messageOfAgent(agent, null, null, url); - } - - public static Message messageOfUser(String text, String imageUrl, int sendStatus) { - return new Message(generateRandomId(), getCurrentTimeStamp(), null, text, imageUrl, MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_OWN, null, sendStatus, null, null); - } - - public static Message messageOfAgent(Agent agent, @Nullable String title, String text, String imageUrl) { - return new Message(generateRandomId(), getCurrentTimeStamp(), title, text, imageUrl, MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, agent, Message.SEND_STATUS_SUCCESS, null, null); - } - - public static Message messageOfAgent(Agent agent, @Nullable String title, String text, String imageUrl, List actions, List carousel) { - return new Message(generateRandomId(), getCurrentTimeStamp(), title, text, imageUrl, MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, agent, Message.SEND_STATUS_SUCCESS, actions, carousel); - } - - public static Message create(@Nullable Integer id, @Nullable Long timeStamp, @Nullable String title, String message, String imageUrl, @Nullable Integer typeId, Agent agent, int sendStatus, List carousel) { - return new Message(id, timeStamp, title, message, imageUrl, typeId, agent, sendStatus, null, carousel); - } - - public static Message ofCarousel(String title, String text, String url, List actions) { - return new Message(null, null, title, text, url, null, null, Message.SEND_STATUS_SUCCESS, actions, null); - } -} diff --git a/parley/src/androidTest/java/nu/parley/android/util/TestActivity.java b/parley/src/androidTest/java/nu/parley/android/util/TestActivity.java new file mode 100644 index 0000000..d2fca65 --- /dev/null +++ b/parley/src/androidTest/java/nu/parley/android/util/TestActivity.java @@ -0,0 +1,7 @@ +package nu.parley.android.util; + +import androidx.appcompat.app.AppCompatActivity; + +public class TestActivity extends AppCompatActivity { + +} diff --git a/parley/src/main/java/nu/parley/android/DefaultParleyDownloadCallback.java b/parley/src/main/java/nu/parley/android/DefaultParleyDownloadCallback.java new file mode 100644 index 0000000..548f68b --- /dev/null +++ b/parley/src/main/java/nu/parley/android/DefaultParleyDownloadCallback.java @@ -0,0 +1,71 @@ +package nu.parley.android; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import java.util.Map; + +public class DefaultParleyDownloadCallback implements ParleyDownloadCallback { + + public interface Listener { + void onComplete(Uri uri); + void onFailed(); + } + + private final Context context; + private final Listener listener; + + public DefaultParleyDownloadCallback(@NonNull Context context, @NonNull Listener listener) { + this.context = context; + this.listener = listener; + } + + private DownloadManager getDownloadManager() { + return (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + } + + @Override + public void launchParleyDownload(String url, Map headers) { + DownloadManager downloadManager = getDownloadManager(); + Uri downloadUri = Uri.parse(url); + DownloadManager.Request request = new DownloadManager.Request(downloadUri); + for (String key : headers.keySet()) { + String value = headers.get(key); + request.addRequestHeader(key, value); + } + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + long downloadId = downloadManager.enqueue(request); + register(downloadId); + } + + private void register(long downloadId) { + BroadcastReceiver onComplete = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + if (downloadId == id) { + context.unregisterReceiver(this); + + Uri uri = getDownloadManager().getUriForDownloadedFile(id); + if (uri == null) { + listener.onFailed(); + } else { + listener.onComplete(uri); + } + } + } + } + }; + + ContextCompat.registerReceiver(context, onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), ContextCompat.RECEIVER_EXPORTED); + } +} diff --git a/parley/src/main/java/nu/parley/android/Parley.java b/parley/src/main/java/nu/parley/android/Parley.java index 903dc69..6f94480 100644 --- a/parley/src/main/java/nu/parley/android/Parley.java +++ b/parley/src/main/java/nu/parley/android/Parley.java @@ -384,7 +384,7 @@ public static void send(String message, boolean silent) { * @return `true` if Parley handled this request, `false` otherwise */ public static boolean onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { - if (requestCode == ParleyView.REQUEST_SELECT_IMAGE || requestCode == ParleyView.REQUEST_TAKE_PHOTO) { + if (requestCode == ParleyView.REQUEST_SELECT_MEDIA || requestCode == ParleyView.REQUEST_TAKE_PHOTO) { if (getInstance().listener == null) { // We will handle it when the listener is attached new Handler().postDelayed(new Runnable() { @@ -792,24 +792,24 @@ private void resendMessage(Message message, @Nullable ChainListener chainListene this.submitMessage(message, false, false, chainListener); } - public void sendImageMessage(final File imageFile) { + public void sendMediaMessage(final File mediaFile) { if (refreshingMessages) { // Wait for it new Handler().postDelayed(new Runnable() { @Override public void run() { - sendImageMessage(imageFile); + sendMediaMessage(mediaFile); } }, 100); return; } - final Message message = Message.ofTypeOwnImage(imageFile.getAbsolutePath()); + final Message message = Message.ofTypeOwnPendingMedia(mediaFile); this.submitMessage(message, true); } @SuppressWarnings("SameParameterValue") private void submitMessage(final Message message, final boolean isNewMessage) { - boolean triggerSentMessage = isNewMessage && getNetwork().apiVersion.isUsingMedia() && message.getLegacyImageUrl() == null; + boolean triggerSentMessage = isNewMessage && getNetwork().apiVersion.isUsingMedia() && message.getLocalUrl() == null; this.submitMessage(message, isNewMessage, triggerSentMessage, null); } @@ -822,10 +822,10 @@ private void submitMessage(final Message message, final boolean showNewMessage, new EventRepository().fire(EVENT_STOP_TYPING); } - boolean uploadMediaFirst = getNetwork().apiVersion.isUsingMedia() && message.getLegacyImageUrl() != null; - if (uploadMediaFirst) { + String uploadMedia = message.getLocalUrl(); + if (getNetwork().apiVersion.isUsingMedia() && uploadMedia != null) { // Upload image first, then update the message (remove `image`, add `media`) and after that submit the actual message with the right media - new MessageRepository().sendMedia(message, new RepositoryCallback() { + new MessageRepository().sendMedia(message, uploadMedia, new RepositoryCallback() { @Override public void onSuccess(Message updatedMessage) { uploading.remove(uuid); @@ -864,7 +864,7 @@ public void onFailed(Integer code, String errorMessage) { }); } else { // Just submit the message (and upload images like before V1.6) - new MessageRepository().send(message, new RepositoryCallback() { + new MessageRepository().send(message, uploadMedia, new RepositoryCallback() { @Override public void onSuccess(final Message updatedMessage) { uploading.remove(uuid); diff --git a/parley/src/main/java/nu/parley/android/ParleyDownloadCallback.java b/parley/src/main/java/nu/parley/android/ParleyDownloadCallback.java new file mode 100644 index 0000000..87a6cb1 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/ParleyDownloadCallback.java @@ -0,0 +1,8 @@ +package nu.parley.android; + +import java.util.Map; + +public interface ParleyDownloadCallback { + + void launchParleyDownload(String url, Map headers); +} diff --git a/parley/src/main/java/nu/parley/android/data/model/Action.java b/parley/src/main/java/nu/parley/android/data/model/Action.java index b551840..2e4f241 100644 --- a/parley/src/main/java/nu/parley/android/data/model/Action.java +++ b/parley/src/main/java/nu/parley/android/data/model/Action.java @@ -15,7 +15,7 @@ public final class Action { @SerializedName("type") private ButtonType type; - Action(String title, String payload) { + public Action(String title, String payload) { this.title = title; this.payload = payload; } diff --git a/parley/src/main/java/nu/parley/android/data/model/Agent.java b/parley/src/main/java/nu/parley/android/data/model/Agent.java index 6ba92a9..0a16582 100644 --- a/parley/src/main/java/nu/parley/android/data/model/Agent.java +++ b/parley/src/main/java/nu/parley/android/data/model/Agent.java @@ -34,7 +34,7 @@ public Date getIsTyping() { return new Date(isTyping * 1000); } - Agent(String name) { + public Agent(String name) { this(name, null); } diff --git a/parley/src/main/java/nu/parley/android/data/model/ApiVersion.java b/parley/src/main/java/nu/parley/android/data/model/ApiVersion.java index d1e2c52..01f129e 100644 --- a/parley/src/main/java/nu/parley/android/data/model/ApiVersion.java +++ b/parley/src/main/java/nu/parley/android/data/model/ApiVersion.java @@ -5,14 +5,6 @@ * https://developers.parley.nu/docs/version-lifetime */ public enum ApiVersion { - /** - * @deprecated A newer version is available to support the latest functionality of Parley. - */ - @Deprecated V1_0, - /** - * @deprecated A newer version is available to support the latest functionality of Parley. - */ - @Deprecated V1_1, /** * @deprecated A newer version is available to support the latest functionality of Parley. */ @@ -36,12 +28,25 @@ public enum ApiVersion { /** * This is the latest supported version by the library. */ - V1_7; + V1_7, + ; public boolean isUsingMedia() { switch (this) { - case V1_0: - case V1_1: + case V1_2: + case V1_3: + case V1_4: + case V1_5: + return false; + case V1_6: + case V1_7: + return true; + } + throw new IllegalStateException("Unhandled API version"); + } + + public boolean isSupportingPdf() { + switch (this) { case V1_2: case V1_3: case V1_4: diff --git a/parley/src/main/java/nu/parley/android/data/model/Media.java b/parley/src/main/java/nu/parley/android/data/model/Media.java deleted file mode 100644 index 55f7855..0000000 --- a/parley/src/main/java/nu/parley/android/data/model/Media.java +++ /dev/null @@ -1,39 +0,0 @@ -package nu.parley.android.data.model; - -import android.text.TextUtils; - -import com.google.gson.annotations.SerializedName; - -import java.util.ArrayList; -import java.util.Arrays; - -public final class Media { - - @SerializedName("id") - private String id; - - @SerializedName("description") - private String description; - - public Media(String id) { - this.id = id; - } - - public String getId() { - return id; - } - - public String getDescription() { - return description; - } - - /** - * @return The id that is needed for the Media url - */ - public String getIdForUrl() { - ArrayList splits = new ArrayList<>(Arrays.asList(id.split("/"))); - splits.remove(0); // Remove `img` - splits.remove(0); // Remove account id - return TextUtils.join("/", splits); - } -} \ No newline at end of file diff --git a/parley/src/main/java/nu/parley/android/data/model/Media.kt b/parley/src/main/java/nu/parley/android/data/model/Media.kt new file mode 100644 index 0000000..cc8d407 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/data/model/Media.kt @@ -0,0 +1,37 @@ +package nu.parley.android.data.model + +import com.google.gson.annotations.SerializedName +import nu.parley.android.data.net.Connectivity +import java.io.File + +data class Media( + @SerializedName("id") private val id: String, + /* `filename` since clientApi 1.8 */ + @SerializedName("filename") private val fileName: String?, + @SerializedName("mimeType") private val mimeType: String, +) { + companion object { + + fun fromFile(file: File): Media { + val fileName = file.name + val extension = file.extension + val mimeType = MimeType.fromExtension(extension) + return Media(fileName, fileName, mimeType.value) + } + } + + fun getFileName(): String { + return fileName ?: id.split("/").last() + } + + fun getMimeType() = MimeType.fromValue(mimeType) + + fun getIdForUrl(): String { + return id + .split("/") + .drop(2) + .joinToString(separator = "/") + } + + fun getUrl(): String = Connectivity.toMediaUrl(getIdForUrl()) +} diff --git a/parley/src/main/java/nu/parley/android/data/model/Message.java b/parley/src/main/java/nu/parley/android/data/model/Message.java index df5e119..de12084 100644 --- a/parley/src/main/java/nu/parley/android/data/model/Message.java +++ b/parley/src/main/java/nu/parley/android/data/model/Message.java @@ -2,14 +2,13 @@ import androidx.annotation.Nullable; -import com.bumptech.glide.load.model.GlideUrl; import com.google.gson.annotations.SerializedName; +import java.io.File; import java.util.Date; import java.util.List; import java.util.UUID; -import nu.parley.android.data.net.Connectivity; import nu.parley.android.util.CompareUtil; import nu.parley.android.view.chat.MessageViewHolderFactory; @@ -53,7 +52,11 @@ public final class Message { @SerializedName("image") @Nullable @Deprecated - private String imageUrl; + private String image; + + @SerializedName("mediaLocal") + @Nullable + private String localUrl; @SerializedName("media") @Nullable @@ -86,24 +89,26 @@ private Message() { // Hide constructor } - private Message(UUID uuid, @Nullable Integer id, long timeStamp, @Nullable String message, @Nullable String imageUrl, @Nullable Media media, int typeId, @Nullable Agent agent, int sendStatus) { + private Message(UUID uuid, @Nullable Integer id, long timeStamp, @Nullable String message, @Nullable String localUrl, @Nullable Media media, int typeId, @Nullable Agent agent, int sendStatus) { this.uuid = uuid; this.id = id; this.timeStamp = timeStamp; this.message = message; - this.imageUrl = imageUrl; + this.localUrl = localUrl; this.media = media; this.typeId = typeId; this.agent = agent; this.sendStatus = sendStatus; } - Message(@Nullable Integer id, Long timeStamp, @Nullable String title, @Nullable String message, @Nullable String imageUrl, Integer typeId, @Nullable Agent agent, int sendStatus, @Nullable List actions, @Nullable List carousel) { + public Message(@Nullable Integer id, Long timeStamp, @Nullable String title, @Nullable String message, @Nullable String localUrl, @Nullable Media media, Integer typeId, @Nullable Agent agent, int sendStatus, @Nullable List actions, @Nullable List carousel) { this.id = id; this.timeStamp = timeStamp; this.title = title; + this.localUrl = localUrl; + this.image = localUrl; this.message = message; - this.imageUrl = imageUrl; + this.media = media; this.typeId = typeId; this.agent = agent; this.sendStatus = sendStatus; @@ -146,9 +151,9 @@ public static Message ofTypeOwnMessage(String text, boolean silent) { return message; } - public static Message ofTypeOwnImage(String imageUrl) { + public static Message ofTypeOwnPendingMedia(File mediaFile) { Message message = Message.ofTypeOwnMessage(false); - message.imageUrl = imageUrl; + message.localUrl = mediaFile.getAbsolutePath(); return message; } @@ -171,15 +176,15 @@ public static Message from(PushMessage pushMessage) { } public static Message withIdAndStatus(Message sourceMessage, Integer id, int status) { - return new Message(sourceMessage.uuid, id, sourceMessage.timeStamp, sourceMessage.message, sourceMessage.imageUrl, sourceMessage.media, sourceMessage.typeId, sourceMessage.agent, status); + return new Message(sourceMessage.uuid, id, sourceMessage.timeStamp, sourceMessage.message, sourceMessage.localUrl, sourceMessage.media, sourceMessage.typeId, sourceMessage.agent, status); } - public static Message withMedia(Message sourceMessage, String mediaId) { - return new Message(sourceMessage.uuid, sourceMessage.id, sourceMessage.timeStamp, sourceMessage.message, null, new Media(mediaId), sourceMessage.typeId, sourceMessage.agent, sourceMessage.sendStatus); + public static Message withMedia(Message sourceMessage, Media media) { + return new Message(sourceMessage.uuid, sourceMessage.id, sourceMessage.timeStamp, sourceMessage.message, null, media, sourceMessage.typeId, sourceMessage.agent, sourceMessage.sendStatus); } public static Message withMessageAndDate(Message sourceMessage, String message, Date date) { - return new Message(sourceMessage.uuid, sourceMessage.id, date.getTime() / 1000, message, sourceMessage.imageUrl, sourceMessage.media, sourceMessage.typeId, sourceMessage.agent, sourceMessage.sendStatus); + return new Message(sourceMessage.uuid, sourceMessage.id, date.getTime() / 1000, message, sourceMessage.localUrl, sourceMessage.media, sourceMessage.typeId, sourceMessage.agent, sourceMessage.sendStatus); } /** @@ -234,43 +239,35 @@ public Agent getAgent() { return agent; } - public String getLegacyImageUrl() { - return imageUrl; - } - @Nullable - private GlideUrl getImageUrl() { - if (imageUrl == null) { - return null; - } - if (id == null) { - return null; - } else { - return Connectivity.toGlideUrl(id); - } + public String getLocalUrl() { + return localUrl; } @Nullable public Media getMedia() { - return media; + if (localUrl == null) { + return media; + } else { + return Media.Companion.fromFile(new File(localUrl)); + } } - /** - * Helper method to get the image as either the GlideUrl or String. - * - * @return The image url as GlideUrl if possible, otherwise as String. `null` if it has no image. - */ @Nullable - public Object getImage() { - if (media != null && media.getId() != null) { - String mediaId = media.getIdForUrl(); - return Connectivity.toGlideUrlMedia(mediaId); + public String getImageUrl() { + if (image != null) { + // Legacy: Messages from clientApi 1.5 and lower + return image; } - if (getLegacyImageUrl() != null) { - return getLegacyImageUrl(); - } - if (getImageUrl() != null) { - return getImageUrl(); + + if (getMedia() != null && getMedia().getMimeType().isImage()) { + if (localUrl == null) { + // Messages from clientApi 1.6 and higher + return getMedia().getUrl(); + } else { + // Pending upload + return localUrl; + } } return null; @@ -295,7 +292,7 @@ public int getSendStatus() { * @return `true` if the message only consists of an image, `false` otherwise. */ public boolean isImageOnly() { - return isImageContentOnly() && + return isContentImageOnly() && (actions == null || actions.isEmpty()); } @@ -305,14 +302,34 @@ public boolean isImageOnly() { * * @return `true` if the content of the message only consists of an image, `false` otherwise. */ - public boolean isImageContentOnly() { + public boolean isContentImageOnly() { return hasImageContent() && !hasTextContent() && !hasActionsContent(); } + /** + * Determine whether this message consists of only a file as content. However, the message + * might still have actions or other content attached. + * + * @return `true` if the content of the message only consists of a file, `false` otherwise. + */ + public boolean isContentFileOnly() { + return hasFileContent() && + !hasTextContent() && + !hasActionsContent(); + } + + public boolean hasName() { + return getAgent() != null && !agent.getName().trim().isEmpty(); + } + public boolean hasImageContent() { - return getLegacyImageUrl() != null || media != null; + return getImageUrl() != null; + } + + public boolean hasFileContent() { + return getMedia() != null && getMedia().getMimeType().isFile(); } public boolean hasTextContent() { @@ -331,8 +348,8 @@ public boolean hasCarouselContent() { public boolean hasContent() { return hasTextContent() || hasImageContent() || - hasActionsContent() || - hasCarouselContent(); + hasFileContent() || + hasActionsContent(); } public boolean isEqualVisually(Message other) { @@ -341,7 +358,8 @@ public boolean isEqualVisually(Message other) { return CompareUtil.equals(message, other.message) && CompareUtil.equals(typeId, other.typeId) && CompareUtil.equals(agent, other.agent) && - CompareUtil.equals(imageUrl, other.imageUrl) && + CompareUtil.equals(localUrl, other.localUrl) && + CompareUtil.equals(image, other.image) && CompareUtil.equals(media, other.media) && CompareUtil.equals(timeStamp, other.timeStamp) && CompareUtil.equals(sendStatus, other.sendStatus) && diff --git a/parley/src/main/java/nu/parley/android/data/model/MimeType.kt b/parley/src/main/java/nu/parley/android/data/model/MimeType.kt new file mode 100644 index 0000000..d96002e --- /dev/null +++ b/parley/src/main/java/nu/parley/android/data/model/MimeType.kt @@ -0,0 +1,53 @@ +package nu.parley.android.data.model + +import java.io.File + +enum class MimeType( + val value: String, + val extensions: List, +) { + ImageJpeg("image/jpeg", listOf("jpg", "jpeg")), + ImagePng("image/png", listOf("png")), + ImageGif("image/gif", listOf("gif")), + ApplicationPdf("application/pdf", listOf("pdf")), + Other("*/*", listOf()); + + fun isImage() = when (this) { + ImageJpeg, + ImagePng, + ImageGif -> true + + ApplicationPdf, + Other -> false + } + + fun isDocument() = when (this) { + ImageJpeg, + ImagePng, + ImageGif -> false + + ApplicationPdf -> true + Other -> false + } + + fun isFile() = when (this) { + ImageJpeg, + ImagePng, + ImageGif -> false + + ApplicationPdf, + Other -> true + } + + fun getExtension() = extensions.first() + + companion object { + fun fromUrl(url: String) = fromExtension(File(url).extension) + fun fromValue(value: String) = entries.firstOrNull { it.value == value } ?: Other + fun fromExtension(extension: String) = + entries.firstOrNull { it.extensions.contains(extension) } ?: Other + + val images = entries.filter { it.isImage() } + val documents = entries.filter { it.isDocument() } + } +} diff --git a/parley/src/main/java/nu/parley/android/data/net/Connectivity.java b/parley/src/main/java/nu/parley/android/data/net/Connectivity.java index 4dd4158..e399f18 100644 --- a/parley/src/main/java/nu/parley/android/data/net/Connectivity.java +++ b/parley/src/main/java/nu/parley/android/data/net/Connectivity.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -58,8 +59,12 @@ public Response intercept(Chain chain) throws IOException { Request.Builder requestBuilder = original.newBuilder() .method(original.method(), original.body()); - addAdditionalHttpHeaders(requestBuilder); - addParleyHttpHeaders(requestBuilder); + for (Map.Entry entry : getAdditionalHeaders().entrySet()) { + requestBuilder.addHeader(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : getParleyHeaders().entrySet()) { + requestBuilder.addHeader(entry.getKey(), entry.getValue()); + } Request request = requestBuilder.build(); return chain.proceed(request); @@ -91,32 +96,17 @@ private static void applySslPinning(OkHttpClient.Builder okHttpClientBuilder) { } } - /** - * Adds the additional http headers that were provided to the Parley instance to the request builder. - * - * @param requestBuilder The request builder on which the headers should be added. - */ - private static void addAdditionalHttpHeaders(Request.Builder requestBuilder) { - for (Map.Entry entry : Parley.getInstance().getNetwork().headers.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - - requestBuilder.addHeader(name, value); - } + public static Map getAdditionalHeaders() { + return Parley.getInstance().getNetwork().headers; } - /** - * Adds the additional http headers that were provided to the Parley instance to the request builder. - * - * @param requestBuilder The request builder on which the headers should be added. - */ - private static void addAdditionalHttpHeaders(LazyHeaders.Builder requestBuilder) { - for (Map.Entry entry : Parley.getInstance().getNetwork().headers.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - - requestBuilder.addHeader(name, value); + public static Map getParleyHeaders() { + Map headers = new HashMap<>(); + headers.put(HEADER_PARLEY_IDENTIFICATION, Parley.getInstance().getSecret() + ":" + Parley.getInstance().getUniqueDeviceIdentifier()); + if (Parley.getInstance().getUserAuthorization() != null) { + headers.put(HEADER_PARLEY_AUTHORIZATION, Parley.getInstance().getUserAuthorization()); } + return headers; } /** @@ -132,38 +122,18 @@ private static OkHttpClient.Builder addInterceptor(OkHttpClient.Builder builder) return builder; } - /** - * Adds the http headers that are specific to Parley to the request builder. - * - * @param requestBuilder The request builder on which the headers should be added. - */ - private static void addParleyHttpHeaders(Request.Builder requestBuilder) { - requestBuilder.addHeader(HEADER_PARLEY_IDENTIFICATION, Parley.getInstance().getSecret() + ":" + Parley.getInstance().getUniqueDeviceIdentifier()); - - if (Parley.getInstance().getUserAuthorization() != null) { - requestBuilder.addHeader(HEADER_PARLEY_AUTHORIZATION, Parley.getInstance().getUserAuthorization()); - } - } - - public static GlideUrl toGlideUrl(int messageId) { - return toGlideUrl(Parley.getInstance().getNetwork().getBaseUrl() + "images/" + messageId); - } - - public static GlideUrl toGlideUrlMedia(String mediaId) { - return toGlideUrl(Parley.getInstance().getNetwork().getBaseUrl() + "media/" + mediaId); + public static String toMediaUrl(String mediaId) { + return Parley.getInstance().getNetwork().getBaseUrl() + "media/" + mediaId; } - private static GlideUrl toGlideUrl(String url) { + public static GlideUrl toGlideUrl(String url) { LazyHeaders.Builder lazyHeadersBuilder = new LazyHeaders.Builder(); - - addAdditionalHttpHeaders(lazyHeadersBuilder); - - lazyHeadersBuilder.addHeader(HEADER_PARLEY_IDENTIFICATION, Parley.getInstance().getSecret() + ":" + Parley.getInstance().getUniqueDeviceIdentifier()); - - if (Parley.getInstance().getUserAuthorization() != null) { - lazyHeadersBuilder.addHeader(HEADER_PARLEY_AUTHORIZATION, Parley.getInstance().getUserAuthorization()); + for (Map.Entry entry : getAdditionalHeaders().entrySet()) { + lazyHeadersBuilder.addHeader(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : getParleyHeaders().entrySet()) { + lazyHeadersBuilder.addHeader(entry.getKey(), entry.getValue()); } - return new GlideUrl(url, lazyHeadersBuilder.build()); } diff --git a/parley/src/main/java/nu/parley/android/data/net/response/ParleyResponsePostMedia.java b/parley/src/main/java/nu/parley/android/data/net/response/ParleyResponsePostMedia.java index 99147d8..c9962e3 100644 --- a/parley/src/main/java/nu/parley/android/data/net/response/ParleyResponsePostMedia.java +++ b/parley/src/main/java/nu/parley/android/data/net/response/ParleyResponsePostMedia.java @@ -8,9 +8,9 @@ public final class ParleyResponsePostMedia { @SerializedName("media") @Nullable - public final String media; + public final String mediaId; - public ParleyResponsePostMedia(@Nullable String media) { - this.media = media; + public ParleyResponsePostMedia(@Nullable String mediaId) { + this.mediaId = mediaId; } } diff --git a/parley/src/main/java/nu/parley/android/data/repository/MessageRepository.java b/parley/src/main/java/nu/parley/android/data/repository/MessageRepository.java index a9f299a..c8ae744 100644 --- a/parley/src/main/java/nu/parley/android/data/repository/MessageRepository.java +++ b/parley/src/main/java/nu/parley/android/data/repository/MessageRepository.java @@ -3,7 +3,9 @@ import java.io.File; import java.util.List; +import nu.parley.android.data.model.Media; import nu.parley.android.data.model.Message; +import nu.parley.android.data.model.MimeType; import nu.parley.android.data.net.Connectivity; import nu.parley.android.data.net.RepositoryCallback; import nu.parley.android.data.net.response.ParleyResponsePostMedia; @@ -21,9 +23,9 @@ import static nu.parley.android.data.model.Message.SEND_STATUS_SUCCESS; -public final class MessageRepository { +import androidx.annotation.Nullable; - private final static String MIME_TYPE_IMAGE_FALLBACK = "image/*"; +public final class MessageRepository { public void findAll(final RepositoryCallback>> callback) { Call>> messagesCall = Connectivity.getRetrofit().create(MessageService.class).findAll(); @@ -67,20 +69,17 @@ public void onFailure(Call>> call, Throwable t) { }); } - public void send(final Message message, final RepositoryCallback callback) { + public void send(final Message message, @Nullable final String media, final RepositoryCallback callback) { Call> messagesCall; - if (message.getLegacyImageUrl() == null) { + if (media == null) { // Text or media message messagesCall = Connectivity.getRetrofit().create(MessageService.class).post(message); } else { // Image message API V1.2: Uploading it together when sending the message - String url = message.getLegacyImageUrl(); - File file = new File(url); - String mediaType = FileUtil.getMimeType(url); - if (mediaType == null) { - mediaType = MIME_TYPE_IMAGE_FALLBACK; - } - RequestBody requestBody = RequestBody.create(MediaType.parse(mediaType), file); + File file = new File(media); + String mediaType = FileUtil.getMimeType(file.getAbsolutePath()); + MimeType mimeType = MimeType.Companion.fromValue(mediaType); + RequestBody requestBody = RequestBody.create(MediaType.parse(mimeType.getValue()), media); MultipartBody.Part filePart = MultipartBody.Part.createFormData("image", file.getName(), requestBody); messagesCall = Connectivity.getRetrofit().create(MessageService.class).postImage(filePart); @@ -105,15 +104,12 @@ public void onFailure(Call> call, Thro }); } - public void sendMedia(final Message message, final RepositoryCallback callback) { + public void sendMedia(final Message message, final String media, final RepositoryCallback callback) { // API V1.6+: Uploading media - String imagePath = message.getImage().toString(); - File file = new File(imagePath); - String mediaType = FileUtil.getMimeType(imagePath); - if (mediaType == null) { - mediaType = MIME_TYPE_IMAGE_FALLBACK; - } - RequestBody requestBody = RequestBody.create(MediaType.parse(mediaType), file); + File file = new File(media); + String mediaType = FileUtil.getMimeType(media); + MimeType mimeType = MimeType.Companion.fromValue(mediaType); + RequestBody requestBody = RequestBody.create(MediaType.parse(mimeType.getValue()), file); MultipartBody.Part filePart = MultipartBody.Part.createFormData("media", file.getName(), requestBody); @@ -122,7 +118,8 @@ public void sendMedia(final Message message, final RepositoryCallback c @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful()) { - Message updatedMessage = Message.withMedia(message, response.body().getData().media); + Media media = new Media(response.body().getData().mediaId, file.getName(), mimeType.getValue()); + Message updatedMessage = Message.withMedia(message, media); callback.onSuccess(updatedMessage); } else { callback.onFailed(response.code(), Connectivity.getFormattedError(response)); diff --git a/parley/src/main/java/nu/parley/android/util/AccessibilityUtil.java b/parley/src/main/java/nu/parley/android/util/AccessibilityUtil.java index b0b7387..e4d39c7 100644 --- a/parley/src/main/java/nu/parley/android/util/AccessibilityUtil.java +++ b/parley/src/main/java/nu/parley/android/util/AccessibilityUtil.java @@ -136,7 +136,7 @@ private static String getContentDescriptionBody(Context context, Message message appendDotIfNeeded(builder); } // Media - if (message.hasImageContent()) { + if (message.hasImageContent() || message.hasFileContent()) { builder.append(context.getString(R.string.parley_accessibility_message_media_attached)); } // Actions diff --git a/parley/src/main/java/nu/parley/android/util/FileUtil.java b/parley/src/main/java/nu/parley/android/util/FileUtil.java index fcb3198..a269bed 100644 --- a/parley/src/main/java/nu/parley/android/util/FileUtil.java +++ b/parley/src/main/java/nu/parley/android/util/FileUtil.java @@ -14,21 +14,21 @@ import java.text.SimpleDateFormat; import java.util.Date; -public final class FileUtil { +import nu.parley.android.data.model.MimeType; - public static final String MIME_TYPE_IMAGE = "image/*"; +public final class FileUtil { - private static String getUniqueImageFileName() { + private static String getUniqueFileName() { @SuppressLint("SimpleDateFormat") String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - return "image_" + timeStamp + "_"; + return timeStamp; } public static File createImageFile(Context context) throws IOException { // Create an image file name File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); File image = File.createTempFile( - getUniqueImageFileName(), + getUniqueFileName(), ".jpg", storageDir ); @@ -37,11 +37,19 @@ public static File createImageFile(Context context) throws IOException { public static File getFileFromContentUri(Context context, Uri uri) { try { + String type = context.getContentResolver().getType(uri); + MimeType mimeType = MimeType.Companion.fromValue(type); InputStream inputStream = context.getContentResolver().openInputStream(uri); - File file = new File(new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getPath() + "/" + FileUtil.getUniqueImageFileName()).getAbsolutePath()); - copyInputStreamToFile(inputStream, file); + String directory; + if (mimeType.isImage()) { + directory = Environment.DIRECTORY_PICTURES; + } else { + directory = Environment.DIRECTORY_DOCUMENTS; + } + File destination = new File(new File(context.getExternalFilesDir(directory).getPath() + "/" + FileUtil.getUniqueFileName() + "." + mimeType.getExtension()).getAbsolutePath()); + copyInputStreamToFile(inputStream, destination); - return file; + return destination; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { diff --git a/parley/src/main/java/nu/parley/android/util/IntentUtil.java b/parley/src/main/java/nu/parley/android/util/IntentUtil.java new file mode 100644 index 0000000..575fd7c --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/IntentUtil.java @@ -0,0 +1,28 @@ +package nu.parley.android.util; + +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.Nullable; + +import java.net.URISyntaxException; + +public class IntentUtil { + + @Nullable + public static Intent fromUrl(String url) { + try { + Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + if (intent == null) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + browserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return browserIntent; + } else { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return intent; + } + } catch (URISyntaxException e) { + return null; + } + } +} diff --git a/parley/src/main/java/nu/parley/android/util/mock/Mock.kt b/parley/src/main/java/nu/parley/android/util/mock/Mock.kt new file mode 100644 index 0000000..f65b1c0 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/mock/Mock.kt @@ -0,0 +1,8 @@ +package nu.parley.android.util.mock + +import java.util.UUID + +internal object Mock { + + fun uuid() = UUID.randomUUID().toString() +} \ No newline at end of file diff --git a/parley/src/main/java/nu/parley/android/util/mock/MockAction.kt b/parley/src/main/java/nu/parley/android/util/mock/MockAction.kt new file mode 100644 index 0000000..bd1d0d9 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/mock/MockAction.kt @@ -0,0 +1,9 @@ +package nu.parley.android.util.mock + +import nu.parley.android.data.model.Action + +object MockAction { + fun create(title: String?, payload: String?): Action { + return Action(title, payload) + } +} diff --git a/parley/src/main/java/nu/parley/android/util/mock/MockAgent.kt b/parley/src/main/java/nu/parley/android/util/mock/MockAgent.kt new file mode 100644 index 0000000..49a0223 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/mock/MockAgent.kt @@ -0,0 +1,14 @@ +package nu.parley.android.util.mock + +import nu.parley.android.data.model.Agent +import java.util.Random + +internal object MockAgent { + private fun generateRandomId() = Random().nextInt() + + private fun create(name: String): Agent { + return Agent(name) + } + + var Webuildapps: Agent = create("Webuildapps") +} diff --git a/parley/src/main/java/nu/parley/android/util/mock/MockMedia.kt b/parley/src/main/java/nu/parley/android/util/mock/MockMedia.kt new file mode 100644 index 0000000..b871b9f --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/mock/MockMedia.kt @@ -0,0 +1,14 @@ +package nu.parley.android.util.mock + +import nu.parley.android.data.model.Media +import nu.parley.android.data.model.MimeType + +internal object MockMedia { + fun image(fileName: String, mimeType: MimeType): Media { + return Media(Mock.uuid(), fileName, mimeType.value) + } + + fun document(fileName: String, mimeType: MimeType): Media { + return Media(Mock.uuid(), fileName, mimeType.value) + } +} diff --git a/parley/src/main/java/nu/parley/android/util/mock/MockMessage.kt b/parley/src/main/java/nu/parley/android/util/mock/MockMessage.kt new file mode 100644 index 0000000..ed01532 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/util/mock/MockMessage.kt @@ -0,0 +1,220 @@ +package nu.parley.android.util.mock + +import nu.parley.android.data.model.Action +import nu.parley.android.data.model.Agent +import nu.parley.android.data.model.Media +import nu.parley.android.data.model.Message +import nu.parley.android.view.chat.MessageViewHolderFactory +import java.util.Date +import java.util.Random + +internal object MockMessage { + private fun generateRandomId() = Random().nextInt() + + private fun generateTimeStamp() = Date().time / 1000 + + fun userText(text: String?, sendStatus: Int): Message { + return userTextAndMedia(text, null, sendStatus) + } + + fun userImage(url: String, sendStatus: Int): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + null, + null, + url, + null, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_OWN, + null, + sendStatus, + null, + null + ) + } + + fun userMedia(media: Media?, sendStatus: Int): Message { + return userTextAndMedia(null, media, sendStatus) + } + + fun agentText(agent: Agent?, text: String?): Message { + return agentMessageAndMedia(agent, null, text, null) + } + + fun agentImage(agent: Agent?, image: String?): Message { + return return Message( + generateRandomId(), + generateTimeStamp(), + null, + null, + image, + null, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, + agent, + Message.SEND_STATUS_SUCCESS, + null, + null + ) + } + + fun agentMedia(agent: Agent?, media: Media?): Message { + return agentMessageAndMedia(agent, null, null, media) + } + + fun userTextAndImage(text: String?, image: String?, sendStatus: Int): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + null, + text, + image, + null, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_OWN, + null, + sendStatus, + null, + null + ) + } + + fun userTextAndMedia(text: String?, media: Media?, sendStatus: Int): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + null, + text, + null, + media, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_OWN, + null, + sendStatus, + null, + null + ) + } + + fun agentMessageAndImage(agent: Agent?, title: String?, text: String?, image: String?): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + title, + text, + image, + null, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, + agent, + Message.SEND_STATUS_SUCCESS, + null, + null + ) + } + + fun agentMessageAndMedia(agent: Agent?, title: String?, text: String?, media: Media?): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + title, + text, + null, + media, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, + agent, + Message.SEND_STATUS_SUCCESS, + null, + null + ) + } + + fun agentFullImage( + agent: Agent?, + title: String?, + text: String?, + image: String?, + actions: List?, + carousel: List? + ): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + title, + text, + image, + null, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, + agent, + Message.SEND_STATUS_SUCCESS, + actions, + carousel + ) + } + + fun agentFull( + agent: Agent?, + title: String?, + text: String?, + media: Media?, + actions: List?, + carousel: List? + ): Message { + return Message( + generateRandomId(), + generateTimeStamp(), + title, + text, + null, + media, + MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_AGENT, + agent, + Message.SEND_STATUS_SUCCESS, + actions, + carousel + ) + } + + fun create( + id: Int?, + timeStamp: Long?, + title: String?, + message: String?, + media: Media?, + typeId: Int?, + agent: Agent?, + sendStatus: Int, + carousel: List? + ): Message { + return Message( + id, + timeStamp, + title, + message, + null, + media, + typeId, + agent, + sendStatus, + null, + carousel + ) + } + + fun agentCarousel( + title: String?, + text: String?, + image: String?, + actions: List? + ): Message { + return Message( + null, + null, + title, + text, + image, + null, + null, + null, + Message.SEND_STATUS_SUCCESS, + actions, + null + ) + } +} diff --git a/parley/src/main/java/nu/parley/android/view/BalloonView.java b/parley/src/main/java/nu/parley/android/view/BalloonView.java index 553ea68..4fcad80 100644 --- a/parley/src/main/java/nu/parley/android/view/BalloonView.java +++ b/parley/src/main/java/nu/parley/android/view/BalloonView.java @@ -24,6 +24,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; import androidx.appcompat.widget.AppCompatImageView; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityViewCommand; @@ -41,15 +42,19 @@ import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; +import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import nu.parley.android.R; import nu.parley.android.data.model.Action; +import nu.parley.android.data.model.Media; +import nu.parley.android.data.net.Connectivity; import nu.parley.android.data.net.response.ParleyNotificationResponseType; import nu.parley.android.util.MarkdownUtil; import nu.parley.android.util.StyleUtil; +import nu.parley.android.view.balloon.BalloonFileView; import nu.parley.android.view.chat.action.MessageAdditionAdapter; public final class BalloonView extends FrameLayout { @@ -71,6 +76,7 @@ public final class BalloonView extends FrameLayout { private ImageView contentImageView; private AppCompatImageView contentImagePlaceholderView; private ProgressBar imageLoader; + private BalloonFileView fileView; private ViewGroup metaLayout; private TextView timeTextView; @@ -120,6 +126,7 @@ private void init() { contentImageView = findViewById(R.id.image_view); contentImagePlaceholderView = findViewById(R.id.image_placeholder_view); imageLoader = findViewById(R.id.image_loader); + fileView = findViewById(R.id.file_view); metaLayout = findViewById(R.id.meta_layout); timeTextView = findViewById(R.id.time_text_view); @@ -133,6 +140,10 @@ private void init() { messageTextView.setMovementMethod(LinkMovementMethod.getInstance()); } + public void style(@StyleRes int messageStyle) { + fileView.style(messageStyle); + } + public void setName(@Nullable String text, boolean hasImage, boolean useBottomMargin) { nameTextView.setText(text); nameSpaceView.setVisibility(useBottomMargin ? View.VISIBLE : View.GONE); @@ -182,7 +193,7 @@ public void setHasTextContent(boolean hasTextContent) { messageLayout.setVisibility(hasTextContent ? View.VISIBLE : View.GONE); } - public void setImage(@Nullable Object imageUrl, boolean applyBottomCornerRadius) { + public void setImage(@Nullable String imageUrl, boolean applyBottomCornerRadius) { Glide.with(this).clear(contentImageView); if (imageUrl == null) { @@ -190,20 +201,24 @@ public void setImage(@Nullable Object imageUrl, boolean applyBottomCornerRadius) return; } - if (!(imageUrl instanceof GlideUrl) && !(imageUrl instanceof String)) { - Log.d(getClass().toString(), "setImage :: Detected invalid image url"); - imageUrl = null; - } - - imageLayout.setVisibility(imageUrl == null ? View.GONE : View.VISIBLE); - imageLoader.setVisibility(imageUrl == null ? View.GONE : View.VISIBLE); + imageLayout.setVisibility(View.VISIBLE); + imageLoader.setVisibility(View.VISIBLE); boolean isNameEmpty = nameTextView.getText().toString().isEmpty(); renderImageShadows(isNameEmpty, applyBottomCornerRadius); contentImageView.setVisibility(View.VISIBLE); + + Object url; + if (imageUrl.startsWith("http")) { + // Remote + url = Connectivity.toGlideUrl(imageUrl); + } else { + // Local + url = imageUrl; + } Glide.with(this) - .load(imageUrl) + .load(url) .transform(getImageTransformations(applyBottomCornerRadius)) .listener(new RequestListener() { @Override @@ -223,6 +238,10 @@ public boolean onResourceReady(final Drawable resource, Object model, Target getImageTransformations(boolean applyBottomCornerRadius) { List> transformations = new ArrayList<>(); transformations.add(new CenterCrop()); // Always CenterCrop @@ -304,9 +323,7 @@ public void setAddition(@Nullable final MessageAdditionAdapter adapter) { actionsRecyclerView.setAdapter(adapter); boolean hasAdditions = adapter != null && adapter.getItemCount() > 0; - messageMetaSpace.setVisibility(hasAdditions ? View.GONE : View.VISIBLE); actionsRecyclerView.setVisibility(hasAdditions ? View.VISIBLE : View.GONE); - actionsMetaSpace.setVisibility(hasAdditions ? View.VISIBLE : View.GONE); if (adapter != null) { for (final Action action : adapter.getActions()) { @@ -321,9 +338,19 @@ public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) } } + public void setTextMetaSpace(boolean visible) { + messageMetaSpace.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void setBottomMetaSpace(boolean visible) { + actionsMetaSpace.setVisibility(visible ? View.VISIBLE : View.GONE); + } + public void setOnContentClickListener(@Nullable View.OnClickListener clickListener) { contentLayout.setOnClickListener(clickListener); contentLayout.setClickable(clickListener != null); + fileView.setOnClickListener(clickListener); + fileView.setClickable(clickListener != null); } // Styling diff --git a/parley/src/main/java/nu/parley/android/view/ParleyView.java b/parley/src/main/java/nu/parley/android/view/ParleyView.java index f930b0e..cd87195 100644 --- a/parley/src/main/java/nu/parley/android/view/ParleyView.java +++ b/parley/src/main/java/nu/parley/android/view/ParleyView.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.TypedArray; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.util.AttributeSet; @@ -29,8 +30,10 @@ import java.util.List; +import nu.parley.android.DefaultParleyDownloadCallback; import nu.parley.android.DefaultParleyLaunchCallback; import nu.parley.android.Parley; +import nu.parley.android.ParleyDownloadCallback; import nu.parley.android.ParleyLaunchCallback; import nu.parley.android.ParleyListener; import nu.parley.android.R; @@ -41,6 +44,7 @@ import nu.parley.android.util.AccessibilityMonitor; import nu.parley.android.util.AccessibilityUtil; import nu.parley.android.util.ConnectivityMonitor; +import nu.parley.android.util.IntentUtil; import nu.parley.android.util.ParleyPermissionUtil; import nu.parley.android.util.StyleUtil; import nu.parley.android.view.chat.MessageAdapter; @@ -52,7 +56,7 @@ public final class ParleyView extends FrameLayout implements ParleyListener, ConnectivityMonitor.Listener, AccessibilityMonitor.Listener { - public static final int REQUEST_SELECT_IMAGE = 1661; + public static final int REQUEST_SELECT_MEDIA = 1661; public static final int REQUEST_TAKE_PHOTO = 1662; public static final int REQUEST_PERMISSION_ACCESS_CAMERA = 1663; public static final int REQUEST_PERMISSION_NOTIFICATIONS = 1664; @@ -81,8 +85,9 @@ public final class ParleyView extends FrameLayout implements ParleyListener, Con // Is typing private Handler isTypingAgentHandler = new Handler(); private Runnable isTypingAgentRunnable = null; - // Launching + // Callbacks private ParleyLaunchCallback launchCallback; + private ParleyDownloadCallback downloadCallback; public ParleyView(Context context) { super(context); @@ -106,27 +111,38 @@ public ParleyView(Context context, AttributeSet attrs, int defStyle) { * Allows setting a {@link ParleyLaunchCallback} which allows client apps to change how Parley * "starts an Activity for result" and requests permissions. */ - public void setLaunchCallback(@Nullable ParleyLaunchCallback launchCallback) { - if (launchCallback == null) { - this.launchCallback = new DefaultParleyLaunchCallback(getContext()); - } else { - this.launchCallback = launchCallback; - } + public void setLaunchCallback(@NonNull ParleyLaunchCallback launchCallback) { + this.launchCallback = launchCallback; parleyMessageListener.setLaunchCallback(this.launchCallback); composeView.setLaunchCallback(this.launchCallback); } + /** + * Allows setting a {@link ParleyDownloadCallback} which allows client apps to change how Parley + * downloads files from the chat. + */ + public void setDownloadCallback(@NonNull ParleyDownloadCallback downloadCallback) { + this.downloadCallback = downloadCallback; + parleyMessageListener.setDownloadCallback(this.downloadCallback); + } + public void setListener(@Nullable Listener listener) { this.listener = listener; } + + @Deprecated(since = "3.10.0, replace with `setMediaEnabled`,", forRemoval = true) + public void setImagesEnabled(boolean enabled) { + setMediaEnabled(enabled); + } + /** * Sets whether the user can upload images in this chat. * * @param enabled */ - public void setImagesEnabled(boolean enabled) { - composeView.setImagesEnabled(enabled); + public void setMediaEnabled(boolean enabled) { + composeView.setMediaEnabled(enabled); } /** @@ -160,8 +176,18 @@ private void init() { composeView = findViewById(R.id.compose_view); // Configure - launchCallback = new DefaultParleyLaunchCallback(getContext()); - setLaunchCallback(launchCallback); + setLaunchCallback(new DefaultParleyLaunchCallback(getContext())); + setDownloadCallback(new DefaultParleyDownloadCallback(getContext(), new DefaultParleyDownloadCallback.Listener() { + @Override + public void onFailed() { + Snackbar.make(ParleyView.this, getContext().getString(R.string.parley_message_file_download_failed), Snackbar.LENGTH_LONG).show(); + } + + @Override + public void onComplete(Uri uri) { + launchCallback.launchParleyActivity(IntentUtil.fromUrl(uri.toString())); + } + })); notificationsView.checkNotifications(); recyclerView.setAdapter(adapter); @@ -277,7 +303,7 @@ private void applyStyle(@Nullable AttributeSet attrs) { StyleUtil.Helper.applyLoaderTint(statusLoader, loaderColor); } - setImagesEnabled(StyleUtil.getBoolean(ta, R.styleable.ParleyView_parley_images_enabled, true)); + setMediaEnabled(StyleUtil.getBoolean(ta, R.styleable.ParleyView_parley_media_enabled, true)); ParleyPosition.Vertical notificationsPosition = StyleUtil.getPositionVertical(ta, R.styleable.ParleyView_parley_notifications_position); setNotificationsPosition(notificationsPosition); @@ -530,9 +556,9 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { return true; } - if (requestCode == REQUEST_SELECT_IMAGE) { + if (requestCode == REQUEST_SELECT_MEDIA) { if (resultCode == RESULT_OK) { - composeView.submitSelectedImage(data); + composeView.submitSelectedMedia(data); } return true; } @@ -567,7 +593,8 @@ public interface Listener { // Accessibility - @SuppressLint("NotifyDataSetChanged") // TalkBack updates visuals only, which is why we need to render the chat messages again + @SuppressLint("NotifyDataSetChanged") + // TalkBack updates visuals only, which is why we need to render the chat messages again @Override public void onTalkbackChanged(boolean enabled) { adapter.notifyDataSetChanged(); diff --git a/parley/src/main/java/nu/parley/android/view/balloon/BalloonFileView.java b/parley/src/main/java/nu/parley/android/view/balloon/BalloonFileView.java new file mode 100644 index 0000000..c73a841 --- /dev/null +++ b/parley/src/main/java/nu/parley/android/view/balloon/BalloonFileView.java @@ -0,0 +1,142 @@ +package nu.parley.android.view.balloon; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.widget.AppCompatImageView; + +import nu.parley.android.R; +import nu.parley.android.data.model.Media; +import nu.parley.android.data.model.MimeType; +import nu.parley.android.util.StyleUtil; + +public final class BalloonFileView extends FrameLayout { + + private LinearLayout rootLayout; + private FrameLayout dividerTopLayout; + private View dividerTop; + private AppCompatImageView icon; + private LinearLayout contentLayout; + private TextView name; + private TextView action; + private FrameLayout dividerBottomLayout; + private View dividerBottom; + + public BalloonFileView(Context context) { + super(context); + init(context, null); + } + + public BalloonFileView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public BalloonFileView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + private void init(Context context, @Nullable AttributeSet attrs) { + inflate(getContext(), R.layout.view_balloon_file, this); + + rootLayout = findViewById(R.id.root); + dividerTopLayout = findViewById(R.id.divider_top_layout); + dividerTop = findViewById(R.id.divider_top); + dividerBottomLayout = findViewById(R.id.divider_bottom_layout); + dividerBottom = findViewById(R.id.divider_bottom); + contentLayout = findViewById(R.id.content_layout); + icon = findViewById(R.id.icon); + name = findViewById(R.id.name); + action = findViewById(R.id.action); + } + + public void style(@StyleRes int messageStyle) { + TypedArray ta = getContext().obtainStyledAttributes(messageStyle, R.styleable.ParleyMessageBase); + + StyleUtil.StyleSpacing dividerSpacing = StyleUtil.getSpacingData(ta, R.styleable.ParleyMessageBase_parley_divider_margin, R.styleable.ParleyMessageBase_parley_divider_margin_top, R.styleable.ParleyMessageBase_parley_divider_margin_right, R.styleable.ParleyMessageBase_parley_divider_margin_bottom, R.styleable.ParleyMessageBase_parley_divider_margin_left); + dividerTopLayout.setPadding(dividerSpacing.left, dividerSpacing.top, dividerSpacing.right, dividerSpacing.bottom); + dividerBottomLayout.setPadding(dividerSpacing.left, dividerSpacing.top, dividerSpacing.right, dividerSpacing.bottom); + setContentPadding(StyleUtil.getSpacingData(ta, R.styleable.ParleyMessageBase_parley_file_content_padding, R.styleable.ParleyMessageBase_parley_file_content_padding_top, R.styleable.ParleyMessageBase_parley_file_content_padding_right, R.styleable.ParleyMessageBase_parley_file_content_padding_bottom, R.styleable.ParleyMessageBase_parley_file_content_padding_left)); + @ColorInt @Nullable Integer dividerColor = StyleUtil.getColor(ta, R.styleable.ParleyMessageBase_parley_divider_color); + if (dividerColor != null) { + setDividerColor(dividerColor); + } + setIconTint(StyleUtil.getColorStateList(ta, R.styleable.ParleyMessageBase_parley_file_icon_tint_color)); + setNameFont(StyleUtil.getFont(getContext(), ta, R.styleable.ParleyMessageBase_parley_file_name_font_family), StyleUtil.getFontStyle(ta, R.styleable.ParleyMessageBase_parley_file_name_font_style)); + setNameSize(TypedValue.COMPLEX_UNIT_PX, StyleUtil.getDimension(ta, R.styleable.ParleyMessageBase_parley_file_name_text_size)); + setNameColor(StyleUtil.getColorStateList(ta, R.styleable.ParleyMessageBase_parley_file_name_text_color)); + setActionFont(StyleUtil.getFont(getContext(), ta, R.styleable.ParleyMessageBase_parley_file_action_font_family), StyleUtil.getFontStyle(ta, R.styleable.ParleyMessageBase_parley_file_action_font_style)); + setActionSize(TypedValue.COMPLEX_UNIT_PX, StyleUtil.getDimension(ta, R.styleable.ParleyMessageBase_parley_file_action_text_size)); + setActionColor(StyleUtil.getColorStateList(ta, R.styleable.ParleyMessageBase_parley_file_action_text_color)); + + ta.recycle(); + } + + public void setContentPadding(StyleUtil.StyleSpacing data) { + contentLayout.setPadding(data.left, data.top, data.right, data.bottom); + } + + public void setDividerColor(@ColorInt int color) { + dividerTop.setBackgroundColor(color); + dividerBottom.setBackgroundColor(color); + } + + public void setIconTint(ColorStateList color) { + icon.setImageTintList(color); + } + + public void setNameFont(Typeface font, int style) { + name.setTypeface(font, style); + } + + public void setNameSize(int complexUnit, int dimension) { + name.setTextSize(complexUnit, dimension); + } + + public void setNameColor(ColorStateList color) { + name.setTextColor(color); + } + + public void setActionFont(Typeface font, int style) { + action.setTypeface(font, style); + } + + public void setActionSize(int complexUnit, int dimension) { + action.setTextSize(complexUnit, dimension); + } + + public void setActionColor(ColorStateList color) { + action.setTextColor(color); + } + + public void render(@Nullable Media media, boolean showDividerTop, boolean showDividerBottom) { + boolean visible = media != null && media.getMimeType().isFile(); + rootLayout.setVisibility(visible ? View.VISIBLE : View.GONE); + dividerTop.setVisibility(showDividerTop ? View.VISIBLE : View.GONE); + dividerBottom.setVisibility(showDividerBottom ? View.VISIBLE : View.GONE); + if (media != null) { + MimeType mimeType = media.getMimeType(); + switch (mimeType) { + case ApplicationPdf: + icon.setImageResource(R.drawable.parley_ic_file_pdf); + break; + default: + icon.setImageResource(R.drawable.parley_ic_file_unknown); + break; + } + name.setText(media.getFileName()); + } + } +} diff --git a/parley/src/main/java/nu/parley/android/view/chat/MessageListener.java b/parley/src/main/java/nu/parley/android/view/chat/MessageListener.java index 11e72bf..c22e68c 100644 --- a/parley/src/main/java/nu/parley/android/view/chat/MessageListener.java +++ b/parley/src/main/java/nu/parley/android/view/chat/MessageListener.java @@ -1,6 +1,5 @@ package nu.parley.android.view.chat; -import android.content.Context; import android.view.View; import nu.parley.android.data.model.Action; @@ -10,7 +9,7 @@ public interface MessageListener { void onRetryMessageClicked(Message message); - void onImageClicked(Context context, Message message); + void onMediaClicked(View view, Message message); void onActionClicked(View view, Action action); } \ No newline at end of file diff --git a/parley/src/main/java/nu/parley/android/view/chat/ParleyMessageListener.java b/parley/src/main/java/nu/parley/android/view/chat/ParleyMessageListener.java index 0898142..fb831ce 100644 --- a/parley/src/main/java/nu/parley/android/view/chat/ParleyMessageListener.java +++ b/parley/src/main/java/nu/parley/android/view/chat/ParleyMessageListener.java @@ -6,28 +6,34 @@ import android.net.Uri; import android.view.View; import android.widget.ImageView; -import android.widget.Toast; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.model.GlideUrl; import com.google.android.material.snackbar.Snackbar; -import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import nu.parley.android.Parley; +import nu.parley.android.ParleyDownloadCallback; import nu.parley.android.ParleyLaunchCallback; import nu.parley.android.R; import nu.parley.android.data.model.Action; +import nu.parley.android.data.model.Media; import nu.parley.android.data.model.Message; +import nu.parley.android.data.net.Connectivity; import nu.parley.android.imageviewer.ImageViewer; import nu.parley.android.imageviewer.ImageViewerLoader; +import nu.parley.android.util.IntentUtil; public final class ParleyMessageListener implements MessageListener { private ParleyLaunchCallback launchCallback; + private ParleyDownloadCallback downloadCallback; @Override public void onRetryMessageClicked(Message message) { @@ -35,15 +41,34 @@ public void onRetryMessageClicked(Message message) { } @Override - public void onImageClicked(final Context context, final Message message) { - List list = new ArrayList<>(); - list.add(message); + public void onMediaClicked(final View view, final Message message) { + Media media = message.getMedia(); + if (media == null) { + return; + } + + switch (media.getMimeType()) { + case ImageJpeg: + case ImagePng: + case ImageGif: + List list = new ArrayList<>(); + list.add(message); + openImages(view.getContext(), list); + break; + case ApplicationPdf: + case Other: + openDownload(view, media.getIdForUrl()); + break; + } + } + private static void openImages(Context context, List list) { ImageViewerLoader loader = new ImageViewerLoader() { @Override public void loadImage(final ImageView imageView, final Message image) { + GlideUrl url = Connectivity.toGlideUrl(image.getImageUrl()); Glide.with(imageView.getContext()) - .load(image.getImage()) + .load(url) .placeholder(android.R.color.transparent) .fitCenter() .into(imageView); @@ -68,16 +93,27 @@ public void onActionClicked(View view, Action action) { } } + private void openDownload(View view, String url) { + Uri downloadUri = Uri.parse(Parley.getInstance().getNetwork().getBaseUrl() + "media/" + url); + Map headers = new HashMap<>(); + headers.putAll(Connectivity.getAdditionalHeaders()); + headers.putAll(Connectivity.getParleyHeaders()); + Snackbar.make(view, R.string.parley_message_file_downloading, Snackbar.LENGTH_LONG).show(); + downloadCallback.launchParleyDownload( + downloadUri.toString(), + headers + ); + } + private void openUrl(View view, String url) { try { - Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); - if (intent != null) { - launchCallback.launchParleyActivity(intent); + Intent intent = IntentUtil.fromUrl(url); + if (intent == null) { + Snackbar.make(view, R.string.parley_error_action_open, Snackbar.LENGTH_LONG).show(); } else { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - launchCallback.launchParleyActivity(browserIntent); + launchCallback.launchParleyActivity(intent); } - } catch (URISyntaxException | ActivityNotFoundException e) { + } catch (ActivityNotFoundException e) { e.printStackTrace(); Snackbar.make(view, R.string.parley_error_action_open, Snackbar.LENGTH_LONG).show(); } @@ -86,4 +122,8 @@ private void openUrl(View view, String url) { public void setLaunchCallback(@NonNull ParleyLaunchCallback launchCallback) { this.launchCallback = launchCallback; } + + public void setDownloadCallback(@NonNull ParleyDownloadCallback downloadCallback) { + this.downloadCallback = downloadCallback; + } } diff --git a/parley/src/main/java/nu/parley/android/view/chat/action/MessageAdditionViewHolder.java b/parley/src/main/java/nu/parley/android/view/chat/action/MessageAdditionViewHolder.java index 053f667..7fb61a8 100644 --- a/parley/src/main/java/nu/parley/android/view/chat/action/MessageAdditionViewHolder.java +++ b/parley/src/main/java/nu/parley/android/view/chat/action/MessageAdditionViewHolder.java @@ -49,12 +49,13 @@ public void show(Action action, boolean showFirstDivider) { } private void applyStyle() { + TypedArray taBase = getContext().obtainStyledAttributes(currentStyle, R.styleable.ParleyMessageBase); TypedArray ta = getContext().obtainStyledAttributes(currentStyle, R.styleable.ParleyMessageAction); - StyleUtil.StyleSpacing dividerSpaceData = StyleUtil.getSpacingData(ta, R.styleable.ParleyMessageAction_parley_action_divider_margin, R.styleable.ParleyMessageAction_parley_action_divider_margin_top, R.styleable.ParleyMessageAction_parley_action_divider_margin_right, R.styleable.ParleyMessageAction_parley_action_divider_margin_bottom, R.styleable.ParleyMessageAction_parley_action_divider_margin_left); + StyleUtil.StyleSpacing dividerSpaceData = StyleUtil.getSpacingData(taBase, R.styleable.ParleyMessageBase_parley_divider_margin, R.styleable.ParleyMessageBase_parley_divider_margin_top, R.styleable.ParleyMessageBase_parley_divider_margin_right, R.styleable.ParleyMessageBase_parley_divider_margin_bottom, R.styleable.ParleyMessageBase_parley_divider_margin_left); dividerTopLayout.setPadding(dividerSpaceData.left, dividerSpaceData.top, dividerSpaceData.right, dividerSpaceData.bottom); dividerBottomLayout.setPadding(dividerSpaceData.left, dividerSpaceData.top, dividerSpaceData.right, dividerSpaceData.bottom); - @ColorInt @Nullable Integer backgroundColor = StyleUtil.getColor(ta, R.styleable.ParleyMessageAction_parley_action_divider_color); + @ColorInt @Nullable Integer backgroundColor = StyleUtil.getColor(taBase, R.styleable.ParleyMessageBase_parley_divider_color); if (backgroundColor != null) { dividerTopView.setBackgroundColor(backgroundColor); dividerBottomView.setBackgroundColor(backgroundColor); @@ -70,6 +71,7 @@ private void applyStyle() { int fontStyle = StyleUtil.getFontStyle(ta, R.styleable.ParleyMessageAction_parley_action_font_style); titleTextView.setTypeface(font, fontStyle); + taBase.recycle(); ta.recycle(); } } diff --git a/parley/src/main/java/nu/parley/android/view/chat/holder/MessageViewHolder.java b/parley/src/main/java/nu/parley/android/view/chat/holder/MessageViewHolder.java index 33882df..9e19287 100644 --- a/parley/src/main/java/nu/parley/android/view/chat/holder/MessageViewHolder.java +++ b/parley/src/main/java/nu/parley/android/view/chat/holder/MessageViewHolder.java @@ -14,6 +14,7 @@ import nu.parley.android.R; import nu.parley.android.data.model.Action; +import nu.parley.android.data.model.Media; import nu.parley.android.data.model.Message; import nu.parley.android.util.AccessibilityMonitor; import nu.parley.android.util.AccessibilityUtil; @@ -67,6 +68,8 @@ private void applyBaseStyle() { balloonView.setImagePlaceholderTintColor(StyleUtil.getColorStateList(ta, R.styleable.ParleyMessageBase_parley_image_placeholder_tint_color)); balloonView.setImageLoadingTintColor(StyleUtil.getColor(ta, R.styleable.ParleyMessageBase_parley_image_loader_tint_color)); + balloonView.style(getStyleTheme()); + balloonView.setTextFont(StyleUtil.getFont(getContext(), ta, R.styleable.ParleyMessageBase_parley_font_family), StyleUtil.getFontStyle(ta, R.styleable.ParleyMessageBase_parley_font_style)); balloonView.setTextSize(TypedValue.COMPLEX_UNIT_PX, StyleUtil.getDimension(ta, R.styleable.ParleyMessageBase_parley_text_size)); balloonView.setTextColor(StyleUtil.getColorStateList(ta, R.styleable.ParleyMessageBase_parley_text_color)); @@ -86,34 +89,37 @@ public void show(final Message message) { public void show(final Message message, final Date messageTime) { balloonView.setLayoutGravity(shouldAlignRight() ? Gravity.END : Gravity.START); - if (message.hasTextContent() || message.hasImageContent() || message.hasActionsContent()) { + if (message.hasContent()) { balloonLayout.setVisibility(View.VISIBLE); } else { balloonLayout.setVisibility(View.GONE); } - balloonView.refreshStyle(message.isImageContentOnly()); + balloonView.refreshStyle(message.isContentImageOnly()); // Agent name boolean showAgentName = shouldShowName() && message.getAgent() != null; - boolean hasImage = message.getImage() != null; + boolean hasImage = message.hasImageContent(); if (showAgentName) { balloonView.setName(message.getAgent().getName(), hasImage, !message.hasTextContent()); } else { balloonView.setName(null, hasImage, !message.hasTextContent()); } - // Content: A message has either an image or some text - balloonView.setImage(message.getImage(), message.isImageOnly()); + // Media: A message has either an image or a file, never both. + Media media = message.getMedia(); + if (media != null && media.getMimeType().isImage()) { + balloonView.setImage(message.getImageUrl(), message.isImageOnly()); + } else { + balloonView.setImage(null, message.isImageOnly()); + } + boolean showFileDividerTop = message.hasName() || message.hasTextContent(); + boolean showFileDividerBottom = !message.hasActionsContent(); + balloonView.setFile(message.getMedia(), showFileDividerTop, showFileDividerBottom); + balloonView.setHasTextContent(message.hasTextContent()); balloonView.setTitle(message.getTitle()); balloonView.setText(message.getMessage()); - // Meta - balloonView.setInfo(message.getResponseInfoType()); - balloonView.setTime(messageTime); - balloonView.setStatus(message.getSendStatus()); - balloonView.setStatusVisible(shouldShowStatus()); - // Additional data if (message.getActions() == null) { balloonView.setAddition(null); @@ -132,6 +138,14 @@ public void onActionClicked(View view, Action action) { balloonView.setAddition(messageAdditionAdapter); } + // Meta + balloonView.setTextMetaSpace(!(message.hasFileContent() || message.hasActionsContent())); + balloonView.setBottomMetaSpace(message.hasFileContent() || message.hasActionsContent()); + balloonView.setInfo(message.getResponseInfoType()); + balloonView.setTime(messageTime); + balloonView.setStatus(message.getSendStatus()); + balloonView.setStatusVisible(shouldShowStatus()); + balloonView.setOnContentClickListener(getContentClickListener(message)); handleCarousel(message); @@ -144,15 +158,16 @@ public void onActionClicked(View view, Action action) { private View.OnClickListener getContentClickListener(final Message message) { boolean talkback = AccessibilityMonitor.isTalkbackEnabled(itemView.getContext()); final boolean retry = message.getTypeId() != null && message.getTypeId() == MessageViewHolderFactory.MESSAGE_TYPE_MESSAGE_OWN && message.getSendStatus() == Message.SEND_STATUS_FAILED; - final boolean image = message.getImage() != null; - if (retry || (image && !talkback)) { + final boolean image = message.hasImageContent(); + final boolean file = message.hasFileContent(); + if (retry || (image && !talkback) || file) { return new View.OnClickListener() { @Override public void onClick(View v) { if (retry) { listener.onRetryMessageClicked(message); - } else if (image) { - listener.onImageClicked(itemView.getContext(), message); + } else if (image || file) { + listener.onMediaClicked(itemView, message); } } }; diff --git a/parley/src/main/java/nu/parley/android/view/compose/ComposeListener.java b/parley/src/main/java/nu/parley/android/view/compose/ComposeListener.java index 5c7baa6..65e7375 100644 --- a/parley/src/main/java/nu/parley/android/view/compose/ComposeListener.java +++ b/parley/src/main/java/nu/parley/android/view/compose/ComposeListener.java @@ -6,7 +6,7 @@ public interface ComposeListener { void onSendMessage(String message); - void onSendImage(File file); + void onSendMedia(File file); /** * Called when the user starts typing and when the user is still typing after the given startTypingTriggerInterval. diff --git a/parley/src/main/java/nu/parley/android/view/compose/ComposeImageInputView.java b/parley/src/main/java/nu/parley/android/view/compose/ComposeMediaInputView.java similarity index 54% rename from parley/src/main/java/nu/parley/android/view/compose/ComposeImageInputView.java rename to parley/src/main/java/nu/parley/android/view/compose/ComposeMediaInputView.java index d1f918d..cc44de1 100644 --- a/parley/src/main/java/nu/parley/android/view/compose/ComposeImageInputView.java +++ b/parley/src/main/java/nu/parley/android/view/compose/ComposeMediaInputView.java @@ -1,10 +1,9 @@ package nu.parley.android.view.compose; -import static nu.parley.android.view.ParleyView.REQUEST_SELECT_IMAGE; +import static nu.parley.android.view.ParleyView.REQUEST_SELECT_MEDIA; import static nu.parley.android.view.ParleyView.REQUEST_TAKE_PHOTO; import android.Manifest; -import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -13,7 +12,6 @@ import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.provider.MediaStore; import android.util.AttributeSet; import android.util.Log; @@ -23,45 +21,48 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.app.ActivityCompat; import androidx.core.widget.ImageViewCompat; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import nu.parley.android.Parley; import nu.parley.android.ParleyLaunchCallback; import nu.parley.android.R; +import nu.parley.android.data.model.MimeType; import nu.parley.android.util.FileUtil; import nu.parley.android.util.ParleyPermissionUtil; import nu.parley.android.util.TakePictureFileProvider; import nu.parley.android.view.ParleyView; -public final class ComposeImageInputView extends FrameLayout implements View.OnClickListener { +public final class ComposeMediaInputView extends FrameLayout implements View.OnClickListener { - private AppCompatImageView cameraImageView; + private AppCompatImageView addMediaView; private ParleyLaunchCallback launchCallback; private File currentPhotoPath; - public ComposeImageInputView(Context context) { + public ComposeMediaInputView(Context context) { super(context); init(); } - public ComposeImageInputView(Context context, AttributeSet attrs) { + public ComposeMediaInputView(Context context, AttributeSet attrs) { super(context, attrs); init(); } - public ComposeImageInputView(Context context, AttributeSet attrs, int defStyle) { + public ComposeMediaInputView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { - inflate(getContext(), R.layout.view_compose_input_image, this); + inflate(getContext(), R.layout.view_compose_media, this); - cameraImageView = findViewById(R.id.camera_image_view); + addMediaView = findViewById(R.id.add_media_view); setOnClickListener(this); } @@ -77,45 +78,68 @@ public void onClick(View v) { } private void openImageChooser() { + String optionCamera = getContext().getString(R.string.parley_media_camera); + String optionGallery = getContext().getString(R.string.parley_media_gallery); + String optionDocument = getContext().getString(R.string.parley_media_document); + + List options = new ArrayList<>(); if (isCameraAvailable()) { - // Show chooser - String[] options = new String[]{ - getContext().getString(R.string.parley_select_photo), - getContext().getString(R.string.parley_take_photo) - }; - new AlertDialog.Builder(getContext()) - .setTitle(R.string.parley_photo) - .setItems(options, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - selectImage(); - } else { - checkCameraAccess(); - } - } - }) - .setNegativeButton(R.string.parley_general_cancel, null) - .show(); - } else { - // Force select image - Log.d("ParleyComposeView", "Detected camera not available, disabling camera input."); - selectImage(); + options.add(optionCamera); } + options.add(optionGallery); + if (Parley.getInstance().getNetwork().apiVersion.isSupportingPdf()) { + options.add(optionDocument); + } + + // Show chooser + String[] optionsArray = new String[options.size()]; + options.toArray(optionsArray); + new AlertDialog.Builder(getContext()) + .setTitle(R.string.parley_media_select) + .setItems(optionsArray, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String selected = options.get(which); + if (optionDocument.equals(selected)) { + selectDocument(); + } else if (optionGallery.equals(selected)) { + selectImage(); + } else if (optionCamera.equals(selected)) { + checkCameraAccess(); + } else { + Log.d("ParleyComposeView", "Unhandled selection: " + selected); + } + } + }) + .setNegativeButton(R.string.parley_general_cancel, null) + .show(); } private boolean isCameraAvailable() { return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } + private void selectDocument() { + select(MimeType.Companion.getDocuments()); + } + private void selectImage() { - String intentFilter = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT; - Intent intent = new Intent(intentFilter); + select(MimeType.Companion.getImages()); + } + + private void select(List mimeTypes) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(FileUtil.MIME_TYPE_IMAGE); + intent.setType("*/*"); +// intent.setType(FileUtil.MIME_TYPE_IMAGE); + String[] mimeTypeStrings = new String[mimeTypes.size()]; + for (int i = 0; i < mimeTypes.size(); i++) { + mimeTypeStrings[i] = mimeTypes.get(i).getValue(); + } + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypeStrings); - String title = getContext().getString(R.string.parley_select_photo); - launchIntent(Intent.createChooser(intent, title), REQUEST_SELECT_IMAGE); + String title = ""; // getContext().getString(R.string.parley_media_gallery); + launchIntent(Intent.createChooser(intent, title), REQUEST_SELECT_MEDIA); } private void checkCameraAccess() { @@ -151,11 +175,11 @@ private void launchIntent(Intent intent, int requestCode) { } public void setImageDrawable(Drawable drawable) { - cameraImageView.setImageDrawable(drawable); + addMediaView.setImageDrawable(drawable); } public void setImageTintList(ColorStateList color) { - ImageViewCompat.setImageTintList(cameraImageView, color); + ImageViewCompat.setImageTintList(addMediaView, color); } public void setLaunchCallback(@NonNull ParleyLaunchCallback launchCallback) { diff --git a/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeListener.java b/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeListener.java index 6e613a9..a5bc4d6 100644 --- a/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeListener.java +++ b/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeListener.java @@ -16,8 +16,8 @@ public void onSendMessage(String message) { } @Override - public void onSendImage(File file) { - Parley.getInstance().sendImageMessage(file); + public void onSendMedia(File file) { + Parley.getInstance().sendMediaMessage(file); } @Override diff --git a/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeView.java b/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeView.java index 0220eb6..e272dc8 100644 --- a/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeView.java +++ b/parley/src/main/java/nu/parley/android/view/compose/ParleyComposeView.java @@ -2,7 +2,6 @@ import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Typeface; @@ -37,7 +36,7 @@ public final class ParleyComposeView extends FrameLayout implements View.OnClick private ViewGroup inputLayout; private EditText inputEditText; - private ComposeImageInputView imageInputView; + private ComposeMediaInputView imageInputView; private FloatingActionButton sendButton; private ComposeListener listener; @@ -108,8 +107,8 @@ private void applyStyle(Context context, AttributeSet attrs) { sendButton.setBackgroundTintList(StyleUtil.getColorStateList(ta, R.styleable.ParleyComposeView_parley_send_background_tint_color)); sendButton.setImageDrawable(StyleUtil.getDrawable(getContext(), ta, R.styleable.ParleyComposeView_parley_send_icon)); sendButton.setSupportImageTintList(StyleUtil.getColorStateList(ta, R.styleable.ParleyComposeView_parley_send_icon_tint_color)); - imageInputView.setImageDrawable(StyleUtil.getDrawable(getContext(), ta, R.styleable.ParleyComposeView_parley_camera_icon)); - imageInputView.setImageTintList(StyleUtil.getColorStateList(ta, R.styleable.ParleyComposeView_parley_camera_icon_tint_color)); + imageInputView.setImageDrawable(StyleUtil.getDrawable(getContext(), ta, R.styleable.ParleyComposeView_parley_media_icon)); + imageInputView.setImageTintList(StyleUtil.getColorStateList(ta, R.styleable.ParleyComposeView_parley_media_icon_tint_color)); ta.recycle(); } @@ -209,13 +208,13 @@ public void submitCreatedImage() { post(new Runnable() { @Override public void run() { - listener.onSendImage(photoPath); + listener.onSendMedia(photoPath); } }); } } - public void submitSelectedImage(Intent data) { + public void submitSelectedMedia(Intent data) { if (data != null && data.getData() != null) { Uri uri = data.getData(); @@ -228,7 +227,7 @@ public void submitSelectedImage(Intent data) { post(new Runnable() { @Override public void run() { - listener.onSendImage(file); + listener.onSendMedia(file); } }); } @@ -247,7 +246,7 @@ public void onCameraPermissionGranted() { imageInputView.openCamera(); } - public void setImagesEnabled(boolean enabled) { + public void setMediaEnabled(boolean enabled) { imageInputView.setVisibility(enabled ? View.VISIBLE : View.GONE); FrameLayout.LayoutParams imageInputLayoutParams = new FrameLayout.LayoutParams(imageInputView.getLayoutParams()); diff --git a/parley/src/main/res/drawable/parley_ic_add.xml b/parley/src/main/res/drawable/parley_ic_add.xml new file mode 100644 index 0000000..2a8d8e6 --- /dev/null +++ b/parley/src/main/res/drawable/parley_ic_add.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/parley/src/main/res/drawable/parley_ic_camera.xml b/parley/src/main/res/drawable/parley_ic_camera.xml deleted file mode 100644 index 63aeec8..0000000 --- a/parley/src/main/res/drawable/parley_ic_camera.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/parley/src/main/res/drawable/parley_ic_file_pdf.xml b/parley/src/main/res/drawable/parley_ic_file_pdf.xml new file mode 100644 index 0000000..3e375ac --- /dev/null +++ b/parley/src/main/res/drawable/parley_ic_file_pdf.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/parley/src/main/res/drawable/parley_ic_file_unknown.xml b/parley/src/main/res/drawable/parley_ic_file_unknown.xml new file mode 100644 index 0000000..3e375ac --- /dev/null +++ b/parley/src/main/res/drawable/parley_ic_file_unknown.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/parley/src/main/res/layout/item_action.xml b/parley/src/main/res/layout/item_action.xml index 0ac593e..b4ed2e2 100644 --- a/parley/src/main/res/layout/item_action.xml +++ b/parley/src/main/res/layout/item_action.xml @@ -14,7 +14,7 @@ android:id="@+id/divider_top_view" android:layout_width="match_parent" android:layout_height="1dp" - tools:background="@color/parley_agent_action_divider" /> + tools:background="@color/parley_agent_divider" /> + tools:background="@color/parley_agent_divider" /> \ No newline at end of file diff --git a/parley/src/main/res/layout/view_balloon.xml b/parley/src/main/res/layout/view_balloon.xml index 3b51734..ffb69ce 100644 --- a/parley/src/main/res/layout/view_balloon.xml +++ b/parley/src/main/res/layout/view_balloon.xml @@ -136,6 +136,11 @@ tools:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/parley/src/main/res/layout/view_compose.xml b/parley/src/main/res/layout/view_compose.xml index 48c91d0..cf32332 100644 --- a/parley/src/main/res/layout/view_compose.xml +++ b/parley/src/main/res/layout/view_compose.xml @@ -29,7 +29,7 @@ android:maxLines="4" android:minHeight="40dp" /> - + tools:src="@drawable/parley_ic_add" /> \ No newline at end of file diff --git a/parley/src/main/res/values-nl/strings.xml b/parley/src/main/res/values-nl/strings.xml index d3b02e7..071924c 100644 --- a/parley/src/main/res/values-nl/strings.xml +++ b/parley/src/main/res/values-nl/strings.xml @@ -2,17 +2,21 @@ Typ een bericht… Controleer uw internetverbinding Meldingen zijn uitgeschakeld - Selecteer een foto - Maak een foto + Afbeelding of document sturen + Camera + Gallerij + Document Chat berichten De chat is niet geconfigureerd. De chat kan op het moment niet weergegeven worden. Annuleren Oke - Foto + Open Deze link kon helaas niet geopend worden. Camerarechten zijn geweigerd, verleen toegang tot de camera in de app-rechten. + Bestand wordt gedownload… + Het bestand kon niet gedownload worden. Versturen is mislukt Foto is te groot diff --git a/parley/src/main/res/values/attrs.xml b/parley/src/main/res/values/attrs.xml index 0ac06e7..cd4e1e9 100644 --- a/parley/src/main/res/values/attrs.xml +++ b/parley/src/main/res/values/attrs.xml @@ -60,7 +60,7 @@ - + @@ -162,8 +162,8 @@ - - + + @@ -218,6 +218,13 @@ + + + + + + + @@ -228,6 +235,22 @@ + + + + + + + + + + + + + + + + @@ -276,22 +299,16 @@ - - - - - - - - - - - + + + + + @@ -365,6 +382,20 @@ + + + + + + + + + + + + + + @@ -388,4 +419,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/parley/src/main/res/values/parley_configuration.xml b/parley/src/main/res/values/parley_configuration.xml index 810aa0c..ef33b91 100644 --- a/parley/src/main/res/values/parley_configuration.xml +++ b/parley/src/main/res/values/parley_configuration.xml @@ -36,6 +36,12 @@ sans normal + + sans + normal + sans + bold + sans normal @@ -58,7 +64,8 @@ #EAEAEA @color/parley_primary_color - true + true + @bool/parley_images_enabled top @@ -127,8 +134,8 @@ @color/parley_primary_color @drawable/parley_ic_send #FFF - @drawable/parley_ic_camera - @color/parley_primary_color + @drawable/parley_ic_add + @color/parley_primary_color @null @@ -167,6 +174,12 @@ 8dp 8dp 100dp + @null + 0dp + 0dp + 1.5dp + 0dp + #EAEAEA @null 8dp 8dp @@ -181,6 +194,16 @@ @drawable/parley_ic_image_placeholder @color/parley_user_text @color/parley_loader_tint + @null + 16dp + 16dp + 16dp + 16dp + @color/parley_user_text_tint + 14sp + @color/parley_user_text + 14sp + @color/parley_user_text_tint @null 0dp @dimen/parley_user_message_content_padding_bottom @@ -188,7 +211,7 @@ 10dp 14sp #FFF - #147DFA + #9ACEFF 12sp #BBC4C8 #FFF @@ -203,6 +226,12 @@ 8dp 100dp 8dp + @null + 0dp + 0dp + 0dp + 1.5dp + #EAEAEA @null 8dp 8dp @@ -217,6 +246,16 @@ @drawable/parley_ic_image_placeholder @color/parley_agent_text @color/parley_loader_tint + @null + 16dp + 16dp + 16dp + 16dp + @color/parley_agent_text_tint + 14dp + @color/parley_agent_text + 14dp + @color/parley_agent_text_tint @null 0dp @dimen/parley_agent_message_content_padding_bottom @@ -238,12 +277,6 @@ @dimen/parley_agent_text_size @color/parley_agent_text @dimen/parley_agent_text_size - @null - 0dp - 0dp - 0dp - 1.5dp - #EAEAEA @null 8dp 8dp @@ -260,6 +293,12 @@ 8dp 8dp 8dp + @null + 0dp + 0dp + 0dp + 0dp + @color/parley_agent_divider @dimen/parley_agent_message_content_padding @dimen/parley_agent_message_content_padding_top @dimen/parley_agent_message_content_padding_bottom @@ -274,17 +313,16 @@ @drawable/parley_agent_image_placeholder @color/parley_agent_image_placeholder_tint @color/parley_agent_image_loader_tint + @color/parley_agent_file_icon_tint + @dimen/parley_agent_file_name_text_size + @color/parley_agent_file_name_text_color + @dimen/parley_agent_file_action_text_size + @color/parley_agent_file_action_text_color @dimen/parley_agent_text_size @color/parley_agent_text @color/parley_agent_text_tint @color/parley_agent_title @dimen/parley_agent_title_text_size - @null - 0dp - 0dp - 0dp - 0dp - @color/parley_agent_action_divider @dimen/parley_agent_action_padding @dimen/parley_agent_action_padding_top @dimen/parley_agent_action_padding_bottom diff --git a/parley/src/main/res/values/strings.xml b/parley/src/main/res/values/strings.xml index 9ff039c..f104653 100644 --- a/parley/src/main/res/values/strings.xml +++ b/parley/src/main/res/values/strings.xml @@ -4,17 +4,20 @@ Type a message… Check your internet connection Notifications are disabled - Select photo - Take photo + Send image or document + Camera + Gallery + Document Chat messages The chat is not configured. The chat cannot be displayed at this time. Cancel OK - Photo Unfortunately this link couldn\'t be opened. Camera permission is denied, please grant access to the camera in the app permissions. + File is downloading… + The file could not be downloaded. Sending failed Photo is too large @@ -47,5 +50,6 @@ Informational message received. Suggestions available. Agent is typing. + Open diff --git a/parley/src/main/res/values/styles.xml b/parley/src/main/res/values/styles.xml index 346a8ca..417e71f 100644 --- a/parley/src/main/res/values/styles.xml +++ b/parley/src/main/res/values/styles.xml @@ -26,7 +26,7 @@ @color/parley_background @color/parley_loader_tint - @bool/parley_images_enabled + @bool/parley_media_enabled @integer/parley_notifications_position @@ -121,8 +121,8 @@ @drawable/parley_compose_send_icon @color/parley_compose_send_icon_tint - @drawable/parley_compose_camera_icon - @color/parley_compose_camera_icon_tint + @drawable/parley_compose_media_icon + @color/parley_compose_media_icon_tint