diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..afe75b717 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +widgetssdk/src/test/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/increment-project-version.yml b/.github/workflows/increment-project-version.yml index f18302cb2..db6d7ae82 100644 --- a/.github/workflows/increment-project-version.yml +++ b/.github/workflows/increment-project-version.yml @@ -18,4 +18,4 @@ jobs: - uses: actions/checkout@v3 - name: Runs the increment project version workflow in Bitrise run: | - curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"master","workflow_id":"authenticated_increment_project_version","environments":[{"mapped_to":"GITHUB_VERSION_INCREMENT_TYPE","value":"${{ github.event.inputs.type }}","is_expand":true}]},"triggered_by":"curl"}' + curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"development","workflow_id":"authenticated_increment_project_version","environments":[{"mapped_to":"GITHUB_VERSION_INCREMENT_TYPE","value":"${{ github.event.inputs.type }}","is_expand":true}]},"triggered_by":"curl"}' diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index c48777935..4cb5c9a90 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/checkout@v3 - name: Runs the post-release workflow in Bitrise run: | - curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"master","workflow_id":"post_release","environments":[{"mapped_to":"NEW_VERSION","value":"${{ github.event.inputs.version }}","is_expand":true}]},"triggered_by":"curl"}' + curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"development","workflow_id":"post_release","environments":[{"mapped_to":"NEW_VERSION","value":"${{ github.event.inputs.version }}","is_expand":true}]},"triggered_by":"curl"}' diff --git a/app/build.gradle b/app/build.gradle index f8f45ee8b..d84386de6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ android { p.each { name, value -> ext[name] = value } } + initEnvProperty('GLIA_REGION', "beta") initEnvProperty('GLIA_API_KEY_SECRET') initEnvProperty('GLIA_API_KEY_ID') initEnvProperty('GLIA_SITE_ID') @@ -47,11 +48,12 @@ android { initEnvProperty('GLIA_JWT', "") initEnvProperty('FIREBASE_PROJECT_ID') initEnvProperty('FIREBASE_API_KEY') - initEnvProperty('FIREBASE_APP_ID') - initEnvProperty('FIREBASE_APP_ID_DEBUG') + initEnvProperty('FIREBASE_APP_ID', "") + initEnvProperty('FIREBASE_APP_ID_DEBUG', "") buildTypes { all { + resValue("string", "environment", GLIA_REGION) resValue("string", "site_id", GLIA_SITE_ID) resValue("string", "glia_api_key_id", GLIA_API_KEY_ID) resValue("string", "glia_api_key_secret", GLIA_API_KEY_SECRET) @@ -63,12 +65,12 @@ android { debug { signingConfig signingConfigs.debug applicationIdSuffix '.debug' - resValue("string", "firebase_app_id", FIREBASE_APP_ID_DEBUG ?: "") + resValue("string", "firebase_app_id", FIREBASE_APP_ID_DEBUG) } release { initWith debug applicationIdSuffix '' - resValue("string", "firebase_app_id", FIREBASE_APP_ID ?: "") + resValue("string", "firebase_app_id", FIREBASE_APP_ID) } } lint { @@ -107,7 +109,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' +// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$testLibraryVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb8b58ab2..b2579795e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,54 +1,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/glia/exampleapp/Activity.java b/app/src/main/java/com/glia/exampleapp/Activity.java index c80d80ee3..30c2c14bb 100644 --- a/app/src/main/java/com/glia/exampleapp/Activity.java +++ b/app/src/main/java/com/glia/exampleapp/Activity.java @@ -1,9 +1,11 @@ package com.glia.exampleapp; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.Navigation; import com.glia.androidsdk.Glia; import com.glia.widgets.GliaWidgets; @@ -24,4 +26,10 @@ private void initGliaWidgetsWithDeepLink() { GliaWidgets.init(GliaWidgetsConfigManager.obtainConfigFromDeepLink(uri, getApplicationContext())); } } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Navigation.findNavController(this, R.id.nav_host_fragment).handleDeepLink(intent); + } } diff --git a/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt b/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt index f86b2c0f1..09568d34d 100644 --- a/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt +++ b/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt @@ -24,13 +24,13 @@ object GliaWidgetsConfigManager { private const val QUEUE_ID_KEY = "queue_id" private const val VISITOR_CONTEXT_ASSET_ID_KEY = "visitor_context_asset_id" private const val REGION_KEY = "environment" - private const val REGION_BETA = "beta" private const val REGION_ACCEPTANCE = "acceptance" private const val BASE_DOMAIN = "base_domain" private const val DEFAULT_BASE_DOMAIN = "at.samo.io" @JvmStatic fun obtainConfigFromDeepLink(data: Uri, applicationContext: Context): GliaWidgetsConfig { + saveRegionIfPresent(data, applicationContext) saveQueueIdToPrefs(data, applicationContext) saveVisitorContextAssetIdIfPresent(data, applicationContext) saveSiteIdToPrefs(data, applicationContext) @@ -53,6 +53,15 @@ object GliaWidgetsConfigManager { ) } + private fun saveRegionIfPresent(data: Uri, applicationContext: Context) { + val visitorContextAssetId = data.getQueryParameter(REGION_KEY) ?: return + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + sharedPreferences.edit().putString( + applicationContext.getString(R.string.pref_environment), + visitorContextAssetId + ).apply() + } + private fun saveVisitorContextAssetIdIfPresent(data: Uri, applicationContext: Context) { val visitorContextAssetId = data.getQueryParameter(VISITOR_CONTEXT_ASSET_ID_KEY) ?: return val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) @@ -95,10 +104,14 @@ object GliaWidgetsConfigManager { context: Context, uiJsonRemoteConfig: String? = null, runtimeConfig: UiTheme? = null, - region: String = REGION_BETA, + region: String? = null, baseDomain: String = DEFAULT_BASE_DOMAIN, preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) ): GliaWidgetsConfig { + val siteRegion = region ?: preferences.getString( + context.getString(R.string.pref_environment), + context.getString(R.string.environment) + ) val apiKeyId = preferences.getString( context.getString(R.string.pref_api_key_id), context.getString(R.string.glia_api_key_id) @@ -128,7 +141,7 @@ object GliaWidgetsConfigManager { return GliaWidgetsConfig.Builder() .setSiteApiKey(SiteApiKey(apiKeyId!!, apiKeySecret!!)) .setSiteId(siteId) - .setRegion(region) + .setRegion(siteRegion) .setBaseDomain(baseDomain) .setCompanyName(companyName) .setUseOverlay(useOverlay) diff --git a/app/src/main/java/com/glia/exampleapp/MainFragment.java b/app/src/main/java/com/glia/exampleapp/MainFragment.java deleted file mode 100644 index 40f1f4790..000000000 --- a/app/src/main/java/com/glia/exampleapp/MainFragment.java +++ /dev/null @@ -1,527 +0,0 @@ -package com.glia.exampleapp; - -import static com.glia.androidsdk.visitor.Authentication.Behavior.FORBIDDEN_DURING_ENGAGEMENT; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.text.InputType; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SwitchCompat; -import androidx.cardview.widget.CardView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.navigation.NavController; -import androidx.navigation.fragment.NavHostFragment; -import androidx.preference.PreferenceManager; - -import com.glia.androidsdk.Glia; -import com.glia.androidsdk.fcm.GliaPushMessage; -import com.glia.androidsdk.screensharing.ScreenSharing; -import com.glia.androidsdk.visitor.Authentication; -import com.glia.widgets.GliaWidgets; -import com.glia.widgets.UiTheme; -import com.glia.widgets.call.CallActivity; -import com.glia.widgets.call.Configuration; -import com.glia.widgets.chat.ChatActivity; -import com.glia.widgets.chat.ChatType; -import com.glia.widgets.core.callvisualizer.domain.CallVisualizer; -import com.glia.widgets.core.configuration.GliaSdkConfiguration; -import com.glia.widgets.messagecenter.MessageCenterActivity; -import com.glia.widgets.view.VisitorCodeView; - -public class MainFragment extends Fragment { - - - @Nullable - private ConstraintLayout containerView; - - private Authentication authentication; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.main_fragment, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - this.containerView = view.findViewById(R.id.constraint_layout); - NavController navController = NavHostFragment.findNavController(this); - setupAuthButtonsVisibility(); - view.findViewById(R.id.settings_button).setOnClickListener(view1 -> - navController.navigate(R.id.settings)); - view.findViewById(R.id.chat_activity_button).setOnClickListener(v -> - navigateToChat(ChatType.LIVE_CHAT)); - view.findViewById(R.id.audio_call_button).setOnClickListener(v -> - navigateToCall(GliaWidgets.MEDIA_TYPE_AUDIO)); - view.findViewById(R.id.video_call_button).setOnClickListener(v -> - navigateToCall(GliaWidgets.MEDIA_TYPE_VIDEO)); - view.findViewById(R.id.message_center_activity_button).setOnClickListener(v -> - navigateToMessageCenter()); - view.findViewById(R.id.end_engagement_button).setOnClickListener(v -> - GliaWidgets.endEngagement()); - view.findViewById(R.id.initGliaWidgetsButton).setOnClickListener(v -> - new Thread(this::initGliaWidgets).start() - ); - view.findViewById(R.id.authenticationButton).setOnClickListener(v -> - showAuthenticationDialog(null)); - view.findViewById(R.id.deauthenticationButton).setOnClickListener(v -> - deauthenticate()); - view.findViewById(R.id.clear_session_button).setOnClickListener(v -> - clearSession()); - view.findViewById(R.id.visitor_code_button).setOnClickListener(v -> { - if (((SwitchCompat) view.findViewById(R.id.visitor_code_switch)).isChecked()) { - showVisitorCodeInADedicatedView(); - } else { - showVisitorCode(); - } - } - ); - handleOpensFromPushNotification(); - } - - private void handleOpensFromPushNotification() { - FragmentActivity activity = getActivity(); - if (activity == null) { - return; - } - - GliaPushMessage push = Glia.getPushNotifications() - .handleOnMainActivityCreate(activity.getIntent().getExtras()); - - if (push == null) { - return; - } - - if (push.getType() == GliaPushMessage.PushType.QUEUED_MESSAGE) { - authenticate(() -> navigateToChat(ChatType.SECURE_MESSAGING)); - } else { - navigateToChat(ChatType.LIVE_CHAT); - } - } - - private void authenticate(OnAuthCallback callback) { - if (Glia.isInitialized() && authentication == null) { - prepareAuthentication(); - } - if (!Glia.isInitialized()) { - new Thread(() -> { - initGliaWidgets(); - if (getActivity() != null) { - getActivity().runOnUiThread(() -> showAuthenticationDialog(callback)); - } - }).start(); - } else if (authentication != null && authentication.isAuthenticated()) { - callback.onAuthenticated(); - } else { - showAuthenticationDialog(callback); - } - } - - @Override - public void onResume() { - super.onResume(); - - if (Glia.isInitialized() && authentication == null) { - prepareAuthentication(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - GliaWidgets.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - - private void setupAuthButtonsVisibility() { - if (getActivity() == null || containerView == null) return; - if (!Glia.isInitialized()) { - getActivity().runOnUiThread(() -> { - containerView.findViewById(R.id.initGliaWidgetsButton).setVisibility(View.VISIBLE); - containerView.findViewById(R.id.authenticationButton).setVisibility(View.GONE); - containerView.findViewById(R.id.deauthenticationButton).setVisibility(View.GONE); - containerView.findViewById(R.id.visitor_code_button).setVisibility(View.GONE); - containerView.findViewById(R.id.visitor_code_switch_container).setVisibility(View.GONE); - }); - return; - } - getActivity().runOnUiThread(() -> { - containerView.findViewById(R.id.visitor_code_button).setVisibility(View.VISIBLE); - containerView.findViewById(R.id.visitor_code_switch_container).setVisibility(View.VISIBLE); - }); - if (authentication == null) return; - - if (authentication.isAuthenticated()) { - getActivity().runOnUiThread(() -> { - containerView.findViewById(R.id.initGliaWidgetsButton).setVisibility(View.GONE); - containerView.findViewById(R.id.authenticationButton).setVisibility(View.GONE); - containerView.findViewById(R.id.deauthenticationButton).setVisibility(View.VISIBLE); - }); - } else { - getActivity().runOnUiThread(() -> { - containerView.findViewById(R.id.initGliaWidgetsButton).setVisibility(View.GONE); - containerView.findViewById(R.id.authenticationButton).setVisibility(View.VISIBLE); - containerView.findViewById(R.id.deauthenticationButton).setVisibility(View.GONE); - }); - } - } - - private void listenForCallVisualizerEngagements() { - // If Visitor Code is displayed as embedded view then it should be hidden on engagement start - GliaWidgets.getCallVisualizer().onEngagementStart(() -> { - FragmentActivity activity = getActivity(); - if (activity == null) return; - activity.runOnUiThread(this::removeVisitorCodeFromDedicatedView); - }); - } - - private void navigateToChat(ChatType chatType) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); - Intent intent = ChatActivity.getIntent( - getContext(), - getContextAssetIdFromPrefs(sharedPreferences), - getQueueIdFromPrefs(sharedPreferences), - chatType); - startActivity(intent); - } - - private void navigateToMessageCenter() { - Intent intent = new Intent(requireContext(), MessageCenterActivity.class); - setNavigationIntentData(intent); - startActivity(intent); - } - - private void navigateToCall( - @Nullable String mediaType - ) { - - Configuration.Builder configBuilder = Configuration.Builder - .builder() - .setWidgetsConfiguration(getConfiguration()); - - if (!TextUtils.isEmpty(mediaType)) { - configBuilder.setMediaType(mediaType); - } - - Intent intent = CallActivity.getIntent(requireContext(), configBuilder.build()); - - startActivity(intent); - } - - private GliaSdkConfiguration getConfiguration() { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - - return new GliaSdkConfiguration.Builder() - .companyName(getCompanyNameFromPrefs(sharedPreferences)) - .contextAssetId(getContextAssetIdFromPrefs(sharedPreferences)) - .queueId(getQueueIdFromPrefs(sharedPreferences)) - .runTimeTheme(getRuntimeThemeFromPrefs(sharedPreferences)) - .screenSharingMode(getScreenSharingModeFromPrefs(sharedPreferences)) - .useOverlay(getUseOverlay(sharedPreferences)) - .build(); - } - - private void setNavigationIntentData(Intent intent) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - - intent.putExtra( - GliaWidgets.COMPANY_NAME, - getCompanyNameFromPrefs(sharedPreferences) - ); - intent.putExtra( - GliaWidgets.QUEUE_ID, - getQueueIdFromPrefs(sharedPreferences) - ); - intent.putExtra( - GliaWidgets.CONTEXT_ASSET_ID, - getContextAssetIdFromPrefs(sharedPreferences) - ); - intent.putExtra( - GliaWidgets.UI_THEME, - getRuntimeThemeFromPrefs(sharedPreferences) - ); - intent.putExtra( - GliaWidgets.USE_OVERLAY, - getUseOverlay(sharedPreferences) - ); - intent.putExtra( - GliaWidgets.SCREEN_SHARING_MODE, - getScreenSharingModeFromPrefs(sharedPreferences) - ); - } - - private Boolean getUseOverlay(SharedPreferences sharedPreferences) { - return Utils.getUseOverlay(sharedPreferences, getResources()); - } - - private ScreenSharing.Mode getScreenSharingModeFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getScreenSharingModeFromPrefs(sharedPreferences, getResources()); - } - - private UiTheme getRuntimeThemeFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getRunTimeThemeByPrefs(sharedPreferences, getResources()); - } - - private String getQueueIdFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getStringFromPrefs( - R.string.pref_queue_id, - getString(R.string.glia_queue_id), - sharedPreferences, - getResources() - ); - } - - private String getContextAssetIdFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getStringFromPrefs( - R.string.pref_context_asset_id, - null, - sharedPreferences, - getResources() - ); - } - - private String getCompanyNameFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getStringFromPrefs( - R.string.pref_company_name, - "", - sharedPreferences, - getResources() - ); - } - - private String getAuthToken() { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - String authTokenFromPrefs = getAuthTokenFromPrefs(sharedPreferences); - if (!authTokenFromPrefs.isEmpty()) { - return authTokenFromPrefs; - } - return getString(R.string.glia_jwt); - } - - private void saveAuthToken(String jwt) { - if (!jwt.equals(getString(R.string.glia_jwt))) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - putAuthTokenToPrefs(sharedPreferences, jwt); - } - } - - private void clearAuthToken() { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - putAuthTokenToPrefs(sharedPreferences, null); - } - - private String getAuthTokenFromPrefs(SharedPreferences sharedPreferences) { - return Utils.getStringFromPrefs( - R.string.pref_auth_token, - "", - sharedPreferences, - getResources() - ); - } - - private void putAuthTokenToPrefs(SharedPreferences sharedPreferences, String jwt) { - Utils.putStringToPrefs( - R.string.pref_auth_token, - jwt, - sharedPreferences, - getResources() - ); - } - - private void showAuthenticationDialog(@Nullable OnAuthCallback callback) { - if (getContext() == null) return; - - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - final EditText jwtInput = prepareJwtInputViewEditText(builder); - final EditText externalTokenInput = prepareExternalTokenInputViewEditText(builder); - jwtInput.setText(getAuthToken()); - builder.setPositiveButton( - getString(R.string.authentication_dialog_authenticate_button), - (dialog, which) -> authenticate(jwtInput, externalTokenInput, callback)); - builder.setNeutralButton( - getString(R.string.authentication_dialog_clear_button), - null); - builder.setNegativeButton( - R.string.authentication_dialog_cancel_button, - (dialog, which) -> dialog.cancel()); - builder.setView(prepareDialogLayout(jwtInput, externalTokenInput)); - AlertDialog alertDialog = builder.create(); - alertDialog.setOnShowListener(dialogInterface -> { - Button button = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); - button.setOnClickListener(v -> { - jwtInput.setText(""); - externalTokenInput.setText(""); - clearAuthToken(); - }); - }); - alertDialog.show(); - } - - @NonNull - private LinearLayout prepareDialogLayout(EditText jwtInput, EditText externalTokenInput) { - LinearLayout container = new LinearLayout(getContext()); - container.setOrientation(LinearLayout.VERTICAL); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - int marginInDp = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()); - layoutParams.setMargins(marginInDp, 0, marginInDp, 0); - jwtInput.setLayoutParams(layoutParams); - jwtInput.setGravity(android.view.Gravity.TOP | Gravity.START); - externalTokenInput.setLayoutParams(layoutParams); - externalTokenInput.setGravity(android.view.Gravity.TOP | Gravity.START); - - container.addView(jwtInput, layoutParams); - container.addView(externalTokenInput, layoutParams); - return container; - } - - @NonNull - private EditText prepareJwtInputViewEditText(AlertDialog.Builder builder) { - final EditText input = new EditText(getContext()); - input.setHint(R.string.authentication_dialog_jwt_input_hint); - input.setSingleLine(); - input.setMaxLines(10); - input.setHorizontallyScrolling(false); - input.setInputType(InputType.TYPE_CLASS_TEXT); - builder.setTitle(R.string.authentication_dialog_title); - builder.setView(input); - return input; - } - - @NonNull - private EditText prepareExternalTokenInputViewEditText(AlertDialog.Builder builder) { - final EditText input = new EditText(getContext()); - input.setHint(R.string.authentication_dialog_external_token_input_hint); - input.setSingleLine(); - input.setMaxLines(10); - input.setHorizontallyScrolling(false); - input.setInputType(InputType.TYPE_CLASS_TEXT); - builder.setTitle(R.string.authentication_dialog_title); - builder.setView(input); - return input; - } - - private void initGliaWidgets() { - if (Glia.isInitialized()) { - setupAuthButtonsVisibility(); - listenForCallVisualizerEngagements(); - return; - } - - //To load local raw file -// String rawConfigs = UnifiedUiConfigurationLoader.fetchLocalConfiguration(requireContext(), R.raw.global_colors_remote_config); -// GliaWidgets.init(GliaWidgetsConfigManager.createDefaultConfig(requireContext().getApplicationContext(), rawConfigs)); - - //To load remote configs -// String remoteConfigs = UnifiedUiConfigurationLoader.fetchRemoteConfiguration(); -// GliaWidgets.init(GliaWidgetsConfigManager.createDefaultConfig(requireContext().getApplicationContext(), remoteConfigs)); - - GliaWidgets.init(GliaWidgetsConfigManager.createDefaultConfig(requireActivity().getApplicationContext())); - prepareAuthentication(); - listenForCallVisualizerEngagements(); - } - - private void prepareAuthentication() { - authentication = GliaWidgets.getAuthentication(FORBIDDEN_DURING_ENGAGEMENT); - setupAuthButtonsVisibility(); - } - - private void authenticate(EditText jwtInput, - EditText externalTokenInput, - @Nullable OnAuthCallback callback) { - if (getActivity() == null || containerView == null) return; - - String jwt = jwtInput.getText().toString(); - String externalAccessToken = externalTokenInput.getText().toString(); - if (externalAccessToken.isEmpty()) externalAccessToken = null; - authentication.authenticate(jwt, externalAccessToken, (response, exception) -> { - if (exception == null && authentication.isAuthenticated()) { - setupAuthButtonsVisibility(); - if (callback != null) { - callback.onAuthenticated(); - } - } else { - showToast("Error: " + exception); - } - }); - - saveAuthToken(jwt); - } - - private void deauthenticate() { - if (getActivity() == null || containerView == null) return; - - authentication.deauthenticate((response, exception) -> { - if (exception == null && !authentication.isAuthenticated()) { - setupAuthButtonsVisibility(); - } else { - showToast("Error: " + exception); - } - }); - } - - private void clearSession() { - GliaWidgets.clearVisitorSession(); - setupAuthButtonsVisibility(); - } - - private void showVisitorCode() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); - String visitorContext = getContextAssetIdFromPrefs(sharedPreferences); - CallVisualizer cv = GliaWidgets.getCallVisualizer(); - if (visitorContext != null && !visitorContext.trim().isEmpty()) { - cv.addVisitorContext(visitorContext); - } - cv.showVisitorCodeDialog(getContext()); - } - - // For testing the integrated Visitor Code solution - private void showVisitorCodeInADedicatedView() { - VisitorCodeView visitorCodeView = GliaWidgets.getCallVisualizer().createVisitorCodeView(getContext()); - CardView cv = containerView.findViewById(R.id.container); - cv.removeAllViews(); - cv.addView(visitorCodeView); - cv.setVisibility(View.VISIBLE); - } - - private void removeVisitorCodeFromDedicatedView() { - CardView cv = containerView.findViewById(R.id.container); - cv.removeAllViews(); - cv.setVisibility(View.GONE); - } - - private void showToast(String message) { - if (getActivity() == null) return; - - getActivity().runOnUiThread(() -> Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show()); - } - - private interface OnAuthCallback { - void onAuthenticated(); - } -} diff --git a/app/src/main/java/com/glia/exampleapp/MainFragment.kt b/app/src/main/java/com/glia/exampleapp/MainFragment.kt new file mode 100644 index 000000000..bf7ab3c91 --- /dev/null +++ b/app/src/main/java/com/glia/exampleapp/MainFragment.kt @@ -0,0 +1,474 @@ +package com.glia.exampleapp + +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.text.InputType +import android.text.TextUtils +import android.util.TypedValue +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SwitchCompat +import androidx.cardview.widget.CardView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment +import androidx.preference.PreferenceManager +import com.glia.androidsdk.Glia +import com.glia.androidsdk.GliaException +import com.glia.androidsdk.fcm.GliaPushMessage +import com.glia.androidsdk.screensharing.ScreenSharing +import com.glia.androidsdk.visitor.Authentication +import com.glia.exampleapp.GliaWidgetsConfigManager.createDefaultConfig +import com.glia.widgets.GliaWidgets +import com.glia.widgets.UiTheme +import com.glia.widgets.call.CallActivity +import com.glia.widgets.call.Configuration +import com.glia.widgets.chat.ChatActivity +import com.glia.widgets.chat.ChatType +import com.glia.widgets.core.configuration.GliaSdkConfiguration +import com.glia.widgets.messagecenter.MessageCenterActivity +import kotlin.concurrent.thread + +class MainFragment : Fragment() { + private var containerView: ConstraintLayout? = null + private var authentication: Authentication? = null + + private val configuration: GliaSdkConfiguration + get() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + return GliaSdkConfiguration.Builder() + .companyName(getCompanyNameFromPrefs(sharedPreferences)) + .contextAssetId(getContextAssetIdFromPrefs(sharedPreferences)) + .queueId(getQueueIdFromPrefs(sharedPreferences)) + .runTimeTheme(getRuntimeThemeFromPrefs(sharedPreferences)) + .screenSharingMode(getScreenSharingModeFromPrefs(sharedPreferences)) + .useOverlay(getUseOverlay(sharedPreferences)) + .build() + } + + + private val authToken: String + get() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val authTokenFromPrefs = getAuthTokenFromPrefs(sharedPreferences) + return authTokenFromPrefs.ifEmpty { getString(R.string.glia_jwt) } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.main_fragment, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + containerView = view.findViewById(R.id.constraint_layout) + val navController = NavHostFragment.findNavController(this) + + setupAuthButtonsVisibility() + + view.findViewById(R.id.settings_button) + .setOnClickListener { navController.navigate(R.id.settings) } + view.findViewById(R.id.chat_activity_button) + .setOnClickListener { navigateToChat(ChatType.LIVE_CHAT) } + view.findViewById(R.id.audio_call_button) + .setOnClickListener { navigateToCall(GliaWidgets.MEDIA_TYPE_AUDIO) } + view.findViewById(R.id.video_call_button) + .setOnClickListener { navigateToCall(GliaWidgets.MEDIA_TYPE_VIDEO) } + view.findViewById(R.id.message_center_activity_button) + .setOnClickListener { navigateToMessageCenter() } + view.findViewById(R.id.end_engagement_button) + .setOnClickListener { GliaWidgets.endEngagement() } + view.findViewById(R.id.initGliaWidgetsButton).setOnClickListener { + thread { initGliaWidgets() } + } + view.findViewById(R.id.authenticationButton) + .setOnClickListener { showAuthenticationDialog(null) } + view.findViewById(R.id.deauthenticationButton) + .setOnClickListener { deauthenticate() } + view.findViewById(R.id.clear_session_button) + .setOnClickListener { clearSession() } + view.findViewById(R.id.visitor_code_button).setOnClickListener { + if ((view.findViewById(R.id.visitor_code_switch) as SwitchCompat).isChecked) { + showVisitorCodeInADedicatedView() + } else { + showVisitorCode() + } + } + handleOpensFromPushNotification() + } + + private fun handleOpensFromPushNotification() { + val push = Glia.getPushNotifications() + .handleOnMainActivityCreate(requireActivity().intent.extras) ?: return + + if (push.type == GliaPushMessage.PushType.QUEUED_MESSAGE) { + authenticate { navigateToChat(ChatType.SECURE_MESSAGING) } + } else { + navigateToChat(ChatType.LIVE_CHAT) + } + } + + private fun authenticate(callback: OnAuthCallback) { + if (Glia.isInitialized() && authentication == null) { + prepareAuthentication() + } + if (!Glia.isInitialized()) { + thread { + initGliaWidgets() + requireActivity().runOnUiThread { showAuthenticationDialog(callback) } + } + } else if (authentication != null && authentication!!.isAuthenticated) { + callback.onAuthenticated() + } else { + showAuthenticationDialog(callback) + } + } + + override fun onResume() { + super.onResume() + if (Glia.isInitialized() && authentication == null) { + prepareAuthentication() + } + } + + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + GliaWidgets.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + private fun setupAuthButtonsVisibility() { + if (activity == null || containerView == null) return + if (!Glia.isInitialized()) { + requireActivity().runOnUiThread { + containerView!!.findViewById(R.id.initGliaWidgetsButton).visibility = View.VISIBLE + containerView!!.findViewById(R.id.authenticationButton).visibility = View.GONE + containerView!!.findViewById(R.id.deauthenticationButton).visibility = View.GONE + containerView!!.findViewById(R.id.visitor_code_button).visibility = View.GONE + containerView!!.findViewById(R.id.visitor_code_switch_container).visibility = View.GONE + } + return + } + requireActivity().runOnUiThread { + containerView!!.findViewById(R.id.visitor_code_button).visibility = View.VISIBLE + containerView!!.findViewById(R.id.visitor_code_switch_container).visibility = View.VISIBLE + } + if (authentication == null) return + if (authentication!!.isAuthenticated) { + requireActivity().runOnUiThread { + containerView!!.findViewById(R.id.initGliaWidgetsButton).visibility = View.GONE + containerView!!.findViewById(R.id.authenticationButton).visibility = View.GONE + containerView!!.findViewById(R.id.deauthenticationButton).visibility = View.VISIBLE + } + } else { + requireActivity().runOnUiThread { + containerView!!.findViewById(R.id.initGliaWidgetsButton).visibility = View.GONE + containerView!!.findViewById(R.id.authenticationButton).visibility = View.VISIBLE + containerView!!.findViewById(R.id.deauthenticationButton).visibility = View.GONE + } + } + } + + private fun listenForCallVisualizerEngagements() { + // If a Visitor Code is displayed as embedded view then it should be hidden on engagement start + GliaWidgets.getCallVisualizer().onEngagementStart { + activity?.runOnUiThread { removeVisitorCodeFromDedicatedView() } + } + } + + private fun navigateToChat(chatType: ChatType) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val intent = ChatActivity.getIntent( + context, + getContextAssetIdFromPrefs(sharedPreferences), + getQueueIdFromPrefs(sharedPreferences), + chatType + ) + startActivity(intent) + } + + private fun navigateToMessageCenter() { + val intent = Intent(requireContext(), MessageCenterActivity::class.java) + setNavigationIntentData(intent) + startActivity(intent) + } + + private fun navigateToCall(mediaType: String?) { + val configBuilder = Configuration.Builder.builder() + .setWidgetsConfiguration(configuration) + if (!TextUtils.isEmpty(mediaType)) { + configBuilder.setMediaType(mediaType) + } + val intent = CallActivity.getIntent(requireContext(), configBuilder.build()) + startActivity(intent) + } + + private fun setNavigationIntentData(intent: Intent) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + intent.putExtra(GliaWidgets.COMPANY_NAME, getCompanyNameFromPrefs(sharedPreferences)) + .putExtra(GliaWidgets.QUEUE_ID, getQueueIdFromPrefs(sharedPreferences)) + .putExtra(GliaWidgets.CONTEXT_ASSET_ID, getContextAssetIdFromPrefs(sharedPreferences)) + .putExtra(GliaWidgets.UI_THEME, getRuntimeThemeFromPrefs(sharedPreferences)) + .putExtra(GliaWidgets.USE_OVERLAY, getUseOverlay(sharedPreferences)) + .putExtra(GliaWidgets.SCREEN_SHARING_MODE, getScreenSharingModeFromPrefs(sharedPreferences)) + } + + private fun getUseOverlay(sharedPreferences: SharedPreferences): Boolean { + return Utils.getUseOverlay(sharedPreferences, resources) + } + + private fun getScreenSharingModeFromPrefs(sharedPreferences: SharedPreferences): ScreenSharing.Mode { + return Utils.getScreenSharingModeFromPrefs(sharedPreferences, resources) + } + + private fun getRuntimeThemeFromPrefs(sharedPreferences: SharedPreferences): UiTheme? { + return Utils.getRunTimeThemeByPrefs(sharedPreferences, resources) + } + + private fun getQueueIdFromPrefs(sharedPreferences: SharedPreferences): String { + return Utils.getStringFromPrefs( + R.string.pref_queue_id, + getString(R.string.glia_queue_id), + sharedPreferences, + resources + ) + } + + private fun getContextAssetIdFromPrefs(sharedPreferences: SharedPreferences): String? { + return Utils.getStringFromPrefs( + R.string.pref_context_asset_id, + null, + sharedPreferences, + resources + ) + } + + private fun getCompanyNameFromPrefs(sharedPreferences: SharedPreferences): String { + return Utils.getStringFromPrefs( + R.string.pref_company_name, + "", + sharedPreferences, + resources + ) + } + + private fun saveAuthToken(jwt: String) { + if (jwt != getString(R.string.glia_jwt)) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + putAuthTokenToPrefs(sharedPreferences, jwt) + } + } + + private fun clearAuthToken() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + putAuthTokenToPrefs(sharedPreferences, null) + } + + private fun getAuthTokenFromPrefs(sharedPreferences: SharedPreferences): String { + return Utils.getStringFromPrefs( + R.string.pref_auth_token, + "", + sharedPreferences, + resources + ) + } + + private fun putAuthTokenToPrefs(sharedPreferences: SharedPreferences, jwt: String?) { + Utils.putStringToPrefs( + R.string.pref_auth_token, + jwt, + sharedPreferences, + resources + ) + } + + private fun showAuthenticationDialog(callback: OnAuthCallback?) { + if (context == null) return + val builder = AlertDialog.Builder(requireContext()) + val jwtInput = prepareJwtInputViewEditText(builder) + val externalTokenInput = prepareExternalTokenInputViewEditText(builder) + jwtInput.setText(authToken) + builder.setPositiveButton( + getString(R.string.authentication_dialog_authenticate_button) + ) { _, _ -> + authenticate(jwtInput, externalTokenInput, callback) + } + builder.setNeutralButton(getString(R.string.authentication_dialog_clear_button), null) + builder.setNegativeButton( + R.string.authentication_dialog_cancel_button + ) { dialog: DialogInterface, _ -> dialog.cancel() } + builder.setView(prepareDialogLayout(jwtInput, externalTokenInput)) + val alertDialog = builder.create() + alertDialog.setOnShowListener { + val button = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) + button.setOnClickListener { + jwtInput.setText("") + externalTokenInput.setText("") + clearAuthToken() + } + } + alertDialog.show() + } + + private fun prepareDialogLayout( + jwtInput: EditText, + externalTokenInput: EditText + ): LinearLayout { + val container = LinearLayout(context) + container.orientation = LinearLayout.VERTICAL + val layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT + ) + val marginInDp = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics + ).toInt() + layoutParams.setMargins(marginInDp, 0, marginInDp, 0) + jwtInput.layoutParams = layoutParams + jwtInput.gravity = Gravity.TOP or Gravity.START + externalTokenInput.layoutParams = layoutParams + externalTokenInput.gravity = Gravity.TOP or Gravity.START + container.addView(jwtInput, layoutParams) + container.addView(externalTokenInput, layoutParams) + return container + } + + private fun prepareJwtInputViewEditText(builder: AlertDialog.Builder): EditText { + val input = EditText(context) + input.setHint(R.string.authentication_dialog_jwt_input_hint) + input.setSingleLine() + input.maxLines = 10 + input.setHorizontallyScrolling(false) + input.inputType = InputType.TYPE_CLASS_TEXT + builder.setTitle(R.string.authentication_dialog_title) + builder.setView(input) + return input + } + + private fun prepareExternalTokenInputViewEditText(builder: AlertDialog.Builder): EditText { + val input = EditText(context) + input.setHint(R.string.authentication_dialog_external_token_input_hint) + input.setSingleLine() + input.maxLines = 10 + input.setHorizontallyScrolling(false) + input.inputType = InputType.TYPE_CLASS_TEXT + builder.setTitle(R.string.authentication_dialog_title) + builder.setView(input) + return input + } + + private fun initGliaWidgets() { + if (Glia.isInitialized()) { + setupAuthButtonsVisibility() + listenForCallVisualizerEngagements() + return + } + + GliaWidgets.init( + createDefaultConfig( + context = requireActivity().applicationContext, +// uiJsonRemoteConfig = UnifiedUiConfigurationLoader.fetchLocalGlobalColors(requireContext()), +// runtimeConfig = createSampleRuntimeConfig(), +// region = "us" + ) + ) + prepareAuthentication() + listenForCallVisualizerEngagements() + } + + private fun createSampleRuntimeConfig(): UiTheme = UiTheme( + gvaQuickReplyTextColor = android.R.color.holo_green_dark, + gvaQuickReplyStrokeColor = android.R.color.holo_green_dark, + ) + + private fun prepareAuthentication() { + authentication = + GliaWidgets.getAuthentication(Authentication.Behavior.FORBIDDEN_DURING_ENGAGEMENT) + setupAuthButtonsVisibility() + } + + private fun authenticate( + jwtInput: EditText, + externalTokenInput: EditText, + callback: OnAuthCallback? + ) { + if (activity == null || containerView == null) return + val jwt = jwtInput.text.toString() + var externalAccessToken: String? = externalTokenInput.text.toString() + if (externalAccessToken!!.isEmpty()) externalAccessToken = null + authentication!!.authenticate( + jwt, + externalAccessToken + ) { _, exception: GliaException? -> + if (exception == null && authentication!!.isAuthenticated) { + setupAuthButtonsVisibility() + callback?.onAuthenticated() + } else { + showToast("Error: $exception") + } + } + saveAuthToken(jwt) + } + + private fun deauthenticate() { + if (activity == null || containerView == null) return + authentication!!.deauthenticate { _, exception: GliaException? -> + if (exception == null && !authentication!!.isAuthenticated) { + setupAuthButtonsVisibility() + } else { + showToast("Error: $exception") + } + } + } + + private fun clearSession() { + GliaWidgets.clearVisitorSession() + setupAuthButtonsVisibility() + } + + private fun showVisitorCode() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val visitorContext = getContextAssetIdFromPrefs(sharedPreferences) + val cv = GliaWidgets.getCallVisualizer() + if (!visitorContext?.trim { it <= ' ' }.isNullOrBlank()) { + cv.addVisitorContext(visitorContext) + } + cv.showVisitorCodeDialog(context) + } + + // For testing the integrated Visitor Code solution + private fun showVisitorCodeInADedicatedView() { + val visitorCodeView = GliaWidgets.getCallVisualizer().createVisitorCodeView(context) + val cv = containerView!!.findViewById(R.id.container) + cv.removeAllViews() + cv.addView(visitorCodeView) + cv.visibility = View.VISIBLE + } + + private fun removeVisitorCodeFromDedicatedView() { + val cv = containerView!!.findViewById(R.id.container) + cv.removeAllViews() + cv.visibility = View.GONE + } + + private fun showToast(message: String) { + if (activity == null) return + requireActivity().runOnUiThread { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } + } + + private fun interface OnAuthCallback { + fun onAuthenticated() + } +} diff --git a/app/src/main/java/com/glia/exampleapp/UnifiedUiConfigurationLoader.kt b/app/src/main/java/com/glia/exampleapp/UnifiedUiConfigurationLoader.kt index 04b74946d..3db1d9afd 100644 --- a/app/src/main/java/com/glia/exampleapp/UnifiedUiConfigurationLoader.kt +++ b/app/src/main/java/com/glia/exampleapp/UnifiedUiConfigurationLoader.kt @@ -51,4 +51,10 @@ object UnifiedUiConfigurationLoader { @JvmStatic fun fetchLocalConfiguration(context: Context, @RawRes resId: Int): String = context.rawRes(resId) + + @JvmStatic + fun fetchLocalGlobalColors(context: Context): String = fetchLocalConfiguration(context, R.raw.global_colors_unified_config) + + @JvmStatic + fun fetchLocalConfigSample(context: Context): String = fetchLocalConfiguration(context, R.raw.sample_unified_config) } diff --git a/app/src/main/java/com/glia/exampleapp/Utils.java b/app/src/main/java/com/glia/exampleapp/Utils.java index 0b780108e..eb3895162 100644 --- a/app/src/main/java/com/glia/exampleapp/Utils.java +++ b/app/src/main/java/com/glia/exampleapp/Utils.java @@ -67,6 +67,7 @@ private static String toHexColor(Integer intColor, Resources resources) { return String.format("#%08X", resources.getColor(intColor, null)); } + @Nullable public static UiTheme getRunTimeThemeByPrefs(SharedPreferences sharedPreferences, Resources resources) { boolean isRunTimeThemeEnabled = sharedPreferences.getBoolean(resources.getString(R.string.pref_runtime_theme), false); if (!isRunTimeThemeEnabled) { @@ -102,8 +103,8 @@ public static UiTheme getRunTimeThemeByPrefs(SharedPreferences sharedPreferences Boolean whiteLabel = sharedPreferences.getBoolean(resources.getString(R.string.pref_white_label), false); Boolean gliaAlertDialogButtonUseVerticalAlignment = sharedPreferences.getBoolean( - resources.getString(R.string.pref_use_alert_dialog_button_vertical_alignment), - false + resources.getString(R.string.pref_use_alert_dialog_button_vertical_alignment), + false ); UiTheme.UiThemeBuilder builder = new UiTheme.UiThemeBuilder(); @@ -141,10 +142,10 @@ public static UiTheme getRunTimeThemeByPrefs(SharedPreferences sharedPreferences builder.setNeutralButtonConfiguration(null); builder.setChatHeadConfiguration( - getChatHeadConfiguration( - brandPrimaryColor, - baseLightColor - ) + getChatHeadConfiguration( + brandPrimaryColor, + baseLightColor + ) ); builder.setSendMessageButtonTintColor(chatSendMessageButtonTintColor); diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 01ab29327..45cb5dc02 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -19,22 +19,22 @@ + + + - + android:name="com.glia.exampleapp.SdkBasicSettingsFragment" /> - + android:name="com.glia.exampleapp.RuntimeThemeSettingsFragment" /> - + android:name="com.glia.exampleapp.RemoteThemeSettingsFragment" /> auth_token queue_id context_asset_id + environment white_label_toggle use_overlay remote_theme_toggle diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2db43e50..7c325c2be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Site Api Key id Site Api Key Secret Site id (needs restart) + Region (needs restart) Background color Negative color Visitor message bg color diff --git a/app/src/main/res/xml/sdk_basic_prefs.xml b/app/src/main/res/xml/sdk_basic_prefs.xml index 85a2eeed4..daec1786f 100644 --- a/app/src/main/res/xml/sdk_basic_prefs.xml +++ b/app/src/main/res/xml/sdk_basic_prefs.xml @@ -21,6 +21,12 @@ app:title="@string/settings_site_id" app:useSimpleSummaryProvider="true" /> + + + + + + + + + + + + + + + + diff --git a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java index 07d45a457..b2ba91581 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java +++ b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java @@ -30,42 +30,35 @@ * This class is a starting point for integration with Glia Widgets SDK */ public class GliaWidgets { - private final static String TAG = "GliaWidgets"; - public static final String REMOTE_CONFIGURATION = "remote_configuration"; - /** * Use with {@link android.os.Bundle} to pass in a {@link UiTheme} as a navigation argument when * navigating to {@link com.glia.widgets.chat.ChatActivity} */ public static final String UI_THEME = "ui_theme"; - /** * Use with {@link android.os.Bundle} to pass in the name of your company as a navigation * argument when navigating to {@link com.glia.widgets.chat.ChatActivity} */ public static final String COMPANY_NAME = "company_name"; - /** * Use with {@link android.os.Bundle} to pass in the id of the queue you wish to enroll in * as a navigation argument when navigating to {@link com.glia.widgets.chat.ChatActivity} */ public static final String QUEUE_ID = "queue_id"; - /** * Use with {@link android.os.Bundle} to pass in a context url as a navigation * argument when navigating to {@link com.glia.widgets.chat.ChatActivity} + * * @deprecated Use {@link com.glia.widgets.GliaWidgets#CONTEXT_ASSET_ID} */ @Deprecated public static final String CONTEXT_URL = "context_url"; - /** * Use with {@link android.os.Bundle} to pass in a context asset ID as a navigation * argument when navigating to {@link com.glia.widgets.chat.ChatActivity} */ public static final String CONTEXT_ASSET_ID = "context_asset_id"; - /** * Use with {@link android.os.Bundle} to pass in a boolean which represents if you would like to * use the chat head bubble as an overlay as a navigation argument when @@ -78,7 +71,6 @@ public class GliaWidgets { * When this value is not passed then by default this value is true. */ public static final String USE_OVERLAY = "use_overlay"; - /** * Use with {@link android.os.Bundle} to pass an input parameter to the call activity to * tell it which type of engagement you would like to start. Can be one of: @@ -86,36 +78,30 @@ public class GliaWidgets { * If no parameter is passed then will default to {@link MEDIA_TYPE_AUDIO} */ public static final String MEDIA_TYPE = "media_type"; - /** * Pass this parameter as an input parameter with {@link MEDIA_TYPE} as its key to * {@link com.glia.widgets.call.CallActivity} to start an audio call media engagement. */ public static final String MEDIA_TYPE_AUDIO = "media_type_audio"; - /** * Pass this parameter as an input parameter with {@link MEDIA_TYPE} as its key to * {@link com.glia.widgets.call.CallActivity} to start a video call media engagement. */ public static final String MEDIA_TYPE_VIDEO = "media_type_video"; - /** * Pass this parameter to call activity to tell it that upgrade to audio/video call is ongoing * If no parameter is passed then will default to false */ public static final String IS_UPGRADE_TO_CALL = "upgrade_to_call"; - public static final String SURVEY = "survey"; - /** * Use with {@link android.os.Bundle} to pass in * {@link com.glia.androidsdk.screensharing.ScreenSharing.Mode} as a navigation * argument when navigating to {@link com.glia.widgets.chat.ChatActivity} */ public static final String SCREEN_SHARING_MODE = "screens_haring_mode"; - public static final String CHAT_TYPE = "chat_type"; - + private final static String TAG = "GliaWidgets"; @Nullable private static CustomCardAdapter customCardAdapter = new WebViewCardAdapter(); @@ -148,16 +134,16 @@ private static GliaConfig createGliaConfig(GliaWidgetsConfig gliaWidgetsConfig) GliaConfig.Builder builder = new GliaConfig.Builder(); setAuthorization(gliaWidgetsConfig, builder); return builder - .setSiteId(gliaWidgetsConfig.getSiteId()) - .setRegion(gliaWidgetsConfig.getRegion()) - .setBaseDomain(gliaWidgetsConfig.getBaseDomain()) - .setContext(gliaWidgetsConfig.getContext()) - .build(); + .setSiteId(gliaWidgetsConfig.getSiteId()) + .setRegion(gliaWidgetsConfig.getRegion()) + .setBaseDomain(gliaWidgetsConfig.getBaseDomain()) + .setContext(gliaWidgetsConfig.getContext()) + .build(); } private static void setAuthorization( - GliaWidgetsConfig widgetsConfig, - GliaConfig.Builder builder + GliaWidgetsConfig widgetsConfig, + GliaConfig.Builder builder ) { if (widgetsConfig.getSiteApiKey() != null) { builder.setSiteApiKey(widgetsConfig.getSiteApiKey()); @@ -238,14 +224,14 @@ public static void endEngagement() { @Deprecated public static void updateVisitorInfo(VisitorInfoUpdate visitorInfoUpdate, Consumer exceptionConsumer) { Dependencies.glia().updateVisitorInfo(new VisitorInfoUpdateRequest.Builder() - .setName(visitorInfoUpdate.getName()) - .setEmail(visitorInfoUpdate.getEmail()) - .setPhone(visitorInfoUpdate.getPhone()) - .setNote(visitorInfoUpdate.getNote()) - .setCustomAttributes(visitorInfoUpdate.getCustomAttributes()) - .setCustomAttrsUpdateMethod(visitorInfoUpdate.getCustomAttrsUpdateMethod()) - .setNoteUpdateMethod(visitorInfoUpdate.getNoteUpdateMethod()) - .build(), e -> { + .setName(visitorInfoUpdate.getName()) + .setEmail(visitorInfoUpdate.getEmail()) + .setPhone(visitorInfoUpdate.getPhone()) + .setNote(visitorInfoUpdate.getNote()) + .setCustomAttributes(visitorInfoUpdate.getCustomAttributes()) + .setCustomAttrsUpdateMethod(visitorInfoUpdate.getCustomAttrsUpdateMethod()) + .setNoteUpdateMethod(visitorInfoUpdate.getNoteUpdateMethod()) + .build(), e -> { if (e != null) { exceptionConsumer.accept(new GliaWidgetException(e.debugMessage, e.cause)); } else { @@ -273,6 +259,14 @@ public static void getVisitorInfo(Consumer visitorCallback, Con }); } + /** + * @return current instance of {@link CustomCardAdapter} + */ + @Nullable + public static CustomCardAdapter getCustomCardAdapter() { + return customCardAdapter; + } + /** * Allows configuring custom response cards based on metadata. *

@@ -288,14 +282,6 @@ public static void setCustomCardAdapter(@Nullable CustomCardAdapter customCardAd GliaWidgets.customCardAdapter = customCardAdapter; } - /** - * @return current instance of {@link CustomCardAdapter} - */ - @Nullable - public static CustomCardAdapter getCustomCardAdapter() { - return customCardAdapter; - } - /** * Creates `Authentication` instance for a given JWT token. * @@ -365,7 +351,7 @@ private static void setupRxErrorHandler() { private static void throwUncaughtException(Throwable e) { Thread.UncaughtExceptionHandler handler = - Thread.currentThread().getUncaughtExceptionHandler(); + Thread.currentThread().getUncaughtExceptionHandler(); if (handler != null) { handler.uncaughtException(Thread.currentThread(), e); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt index bebb02339..a1b24ed28 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt @@ -339,7 +339,17 @@ data class UiTheme( @get:Deprecated("Replaced by Unified Ui") val chatHeadConfiguration: ChatHeadConfiguration? = null, @get:Deprecated("Replaced by Unified Ui") - val surveyStyle: SurveyStyle? = null + val surveyStyle: SurveyStyle? = null, + + // GVA + @ColorRes + val gvaQuickReplyBackgroundColor: Int? = null, + + @ColorRes + val gvaQuickReplyStrokeColor: Int? = null, + + @ColorRes + val gvaQuickReplyTextColor: Int? = null ) : Parcelable { @@ -404,7 +414,10 @@ data class UiTheme( gliaChatStartedHeadingTextColor = builder.chatStartedHeadingTextColor, gliaChoiceCardContentTextConfiguration = builder.choiceCardContentTextConfiguration, chatHeadConfiguration = builder.chatHeadConfiguration, - surveyStyle = builder.surveyStyle + surveyStyle = builder.surveyStyle, + gvaQuickReplyBackgroundColor = builder.gvaQuickReplyBackgroundColor, + gvaQuickReplyStrokeColor = builder.gvaQuickReplyStrokeColor, + gvaQuickReplyTextColor = builder.gvaQuickReplyTextColor ) class UiThemeBuilder { @@ -783,6 +796,19 @@ data class UiTheme( var surveyStyle: SurveyStyle? = SurveyStyle.Builder().build() private set + // GVA + @ColorRes + var gvaQuickReplyBackgroundColor: Int? = null + private set + + @ColorRes + var gvaQuickReplyStrokeColor: Int? = null + private set + + @ColorRes + var gvaQuickReplyTextColor: Int? = null + private set + fun setAppBarTitle(appBarTitle: String?) { this.appBarTitle = appBarTitle } @@ -1023,6 +1049,19 @@ data class UiTheme( this.surveyStyle = surveyStyle } + + fun setGvaQuickReplyBackgroundColor(@ColorRes gvaQuickReplyBackgroundColor: Int?) { + this.gvaQuickReplyBackgroundColor = gvaQuickReplyBackgroundColor + } + + fun setGvaQuickReplyStrokeColor(@ColorRes gvaQuickReplyStrokeColor: Int?) { + this.gvaQuickReplyStrokeColor = gvaQuickReplyStrokeColor + } + + fun setGvaQuickReplyTextColor(@ColorRes gvaQuickReplyTextColor: Int?) { + this.gvaQuickReplyTextColor = gvaQuickReplyTextColor + } + fun setTheme(theme: UiTheme) { appBarTitle = theme.appBarTitle brandPrimaryColor = theme.brandPrimaryColor @@ -1080,6 +1119,9 @@ data class UiTheme( choiceCardContentTextConfiguration = theme.gliaChoiceCardContentTextConfiguration chatHeadConfiguration = theme.chatHeadConfiguration surveyStyle = theme.surveyStyle + gvaQuickReplyBackgroundColor = theme.gvaQuickReplyBackgroundColor + gvaQuickReplyStrokeColor = theme.gvaQuickReplyStrokeColor + gvaQuickReplyTextColor = theme.gvaQuickReplyTextColor } fun build(): UiTheme { diff --git a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java index 9f67c1172..a23668977 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java +++ b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java @@ -231,6 +231,9 @@ public void onHoldChanged(boolean isOnHold) { public void engagementEnded() { Logger.d(TAG, "engagementEndedByOperator"); stop(); + if (!isOngoingEngagementUseCase.invoke()) { + dialogController.dismissDialogs(); + } } @Override diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt new file mode 100644 index 000000000..2c958e05a --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -0,0 +1,368 @@ +package com.glia.widgets.chat + +import android.text.format.DateUtils +import androidx.annotation.VisibleForTesting +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.VisitorMessage +import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase +import com.glia.widgets.chat.domain.AppendHistoryChatMessageUseCase +import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase +import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase +import com.glia.widgets.chat.domain.GliaOnMessageUseCase +import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase +import com.glia.widgets.chat.domain.IsAuthenticatedUseCase +import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.CustomCardChatItem +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaQuickReplies +import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.NewMessagesDividerItem +import com.glia.widgets.chat.model.OperatorChatItem +import com.glia.widgets.chat.model.OperatorMessageItem +import com.glia.widgets.chat.model.OperatorStatusItem +import com.glia.widgets.chat.model.Unsent +import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.core.secureconversations.domain.MarkMessagesReadWithDelayUseCase +import com.glia.widgets.helper.Logger +import com.glia.widgets.helper.TAG +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.processors.BehaviorProcessor +import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.Schedulers + +internal class ChatManager constructor( + private val onMessageUseCase: GliaOnMessageUseCase, + private val loadHistoryUseCase: GliaLoadHistoryUseCase, + private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase, + private val markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase, + private val appendHistoryChatMessageUseCase: AppendHistoryChatMessageUseCase, + private val appendNewChatMessageUseCase: AppendNewChatMessageUseCase, + private val sendUnsentMessagesUseCase: SendUnsentMessagesUseCase, + private val handleCustomCardClickUseCase: HandleCustomCardClickUseCase, + private val isAuthenticatedUseCase: IsAuthenticatedUseCase, + private val compositeDisposable: CompositeDisposable = CompositeDisposable(), + private val state: BehaviorProcessor = BehaviorProcessor.create(), + private val quickReplies: BehaviorProcessor> = BehaviorProcessor.create(), + private val action: PublishProcessor = PublishProcessor.create() +) { + fun initialize( + onHistoryLoaded: (hasHistory: Boolean) -> Unit, + onQuickReplyReceived: (List) -> Unit, + onOperatorMessageReceived: (count: Int) -> Unit + ): Flowable> { + + subscribe(onHistoryLoaded, onOperatorMessageReceived, onQuickReplyReceived) + + return state.doOnNext(::updateQuickReplies).map(State::immutableChatItems).onBackpressureLatest().share() + } + + @VisibleForTesting + fun subscribe( + onHistoryLoaded: (hasHistory: Boolean) -> Unit, + onOperatorMessageReceived: (count: Int) -> Unit, + onQuickReplyReceived: (List) -> Unit + ) { + subscribeToState(onHistoryLoaded, onOperatorMessageReceived).also(compositeDisposable::add) + subscribeToQuickReplies(onQuickReplyReceived).also(compositeDisposable::add) + } + + fun reset() { + state.onNext(State()) + quickReplies.onNext(emptyList()) + compositeDisposable.clear() + } + + fun onChatAction(action: Action) { + this.action.onNext(action) + } + + @VisibleForTesting + fun subscribeToState(onHistoryLoaded: (hasHistory: Boolean) -> Unit, onOperatorMessageReceived: (count: Int) -> Unit): Disposable = state.run { + loadHistory(onHistoryLoaded) + .concatWith(subscribeToMessages(onOperatorMessageReceived)) + .doOnError { it.printStackTrace() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.computation()) + .subscribe(::onNext, ::onError) + } + + @VisibleForTesting + fun loadHistory(onHistoryLoaded: (hasHistory: Boolean) -> Unit): Flowable = loadHistoryUseCase() + .map { mapChatHistory(it) } + .doOnSuccess { onHistoryLoaded(it.chatItems.isNotEmpty()) } + .toFlowable() + + @VisibleForTesting + fun subscribeToMessages(onOperatorMessageReceived: (count: Int) -> Unit): Flowable = Flowable.merge(onMessage(), onAction()) + .doOnNext { onOperatorMessageReceived(it.addedMessagesCount) } + + @VisibleForTesting + fun updateQuickReplies(state: State) { + state.run { chatItems.lastOrNull() as? GvaQuickReplies } + ?.run { options } + .orEmpty() + .also(quickReplies::onNext) + } + + @VisibleForTesting + fun subscribeToQuickReplies(onQuickReplyReceived: (List) -> Unit): Disposable = quickReplies + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { onQuickReplyReceived(it) } + + @VisibleForTesting + fun onMessage(): Flowable = onMessageUseCase().toFlowable(BackpressureStrategy.BUFFER).withLatestFrom(state, ::mapNewMessage) + + @VisibleForTesting + fun onAction(): Flowable = action.withLatestFrom(state, ::mapAction) + + @VisibleForTesting + fun checkUnsentMessages(state: State) { + sendUnsentMessagesUseCase(state.unsentItems.firstOrNull() ?: return) { + onChatAction(Action.MessageSent(it)) + } + } + + @VisibleForTesting + fun mapChatHistory(historyResponse: ChatHistoryResponse, currentState: State? = null): State { + val state: State = currentState ?: State() + + if (historyResponse.items.isEmpty()) return state + + val chatItems: MutableList = mutableListOf() + + val rawItems = historyResponse.items + + + for (index in rawItems.indices.reversed()) { + + val rawMessage = rawItems[index] + + if (state.isNew(rawMessage)) { + appendHistoryChatMessageUseCase(chatItems, rawMessage, index == rawItems.lastIndex) + } + } + + chatItems.reverse() + + if (addNewMessagesDividerUseCase(chatItems, historyResponse.newMessagesCount)) { + markMessagesReadWithDelay() + } + + state.lastMessageWithVisibleOperatorImage = chatItems.lastOrNull() as? OperatorChatItem + state.chatItems.addAll(chatItems) + + return state + } + + @VisibleForTesting + fun mapNewMessage(chatMessage: ChatMessageInternal, messagesState: State): State { + if (messagesState.isNew(chatMessage)) { + appendNewChatMessageUseCase(messagesState, chatMessage) + if (chatMessage.chatMessage is VisitorMessage) { + checkUnsentMessages(messagesState) + } + } + + return messagesState + } + + @VisibleForTesting + fun mapAction(action: Action, state: State): State { + return when (action) { + is Action.QueuingStarted -> mapInQueue(action.companyName, state) + is Action.OperatorConnected -> mapOperatorConnected(action, state) + Action.Transferring -> mapTransferring(state) + is Action.OperatorJoined -> mapOperatorJoined(action, state) + is Action.UnsentMessageReceived -> addUnsentMessage(action.message, state) + is Action.ResponseCardClicked -> mapResponseCardClicked(action.responseCard, state) + is Action.OnMediaUpgradeStarted -> mapMediaUpgrade(action.isVideo, state) + Action.OnMediaUpgradeToVideo -> mapUpgradeMediaToVideo(state) + Action.OnMediaUpgradeCanceled -> mapMediaUpgradeCanceled(state) + is Action.OnMediaUpgradeTimerUpdated -> mapMediaUpgradeTimerUpdated(action.formattedValue, state) + is Action.CustomCardClicked -> mapCustomCardClicked(action, state) + Action.ChatRestored -> state + is Action.MessageSent -> mapMessageSent(action.message, state) + } + } + + @VisibleForTesting + fun mapMessageSent(message: VisitorMessage, state: State): State = mapNewMessage(ChatMessageInternal(message), state) + + @VisibleForTesting + fun mapCustomCardClicked(action: Action.CustomCardClicked, state: State): State = action.run { + handleCustomCardClickUseCase(customCard, attachment, state) + } + + @VisibleForTesting + fun mapMediaUpgradeTimerUpdated(formattedValue: String, state: State): State = state.apply { + val oldItem = state.mediaUpgradeTimerItem ?: return@apply + + if (oldItem.time == formattedValue) return@apply + + val newItem = oldItem.updateTime(formattedValue) + + mediaUpgradeTimerItem = newItem + + val index = chatItems.indexOf(oldItem) + + if (index == -1) { + chatItems += newItem + } else { + chatItems[index] = newItem + } + + } + + @VisibleForTesting + fun mapMediaUpgradeCanceled(state: State): State = state.apply { + val oldItem = mediaUpgradeTimerItem + mediaUpgradeTimerItem = null + chatItems -= oldItem ?: return@apply + } + + @VisibleForTesting + fun mapUpgradeMediaToVideo(state: State): State = state.apply { + val oldItem = this.mediaUpgradeTimerItem + val newItem = MediaUpgradeStartedTimerItem.Video(oldItem?.time ?: DateUtils.formatElapsedTime(0)) + mediaUpgradeTimerItem = newItem + chatItems += newItem + chatItems -= oldItem ?: return@apply + } + + @VisibleForTesting + fun mapMediaUpgrade(video: Boolean, state: State): State = state.apply { + val mediaUpgradeTimerItem = if (video) MediaUpgradeStartedTimerItem.Video() else MediaUpgradeStartedTimerItem.Audio() + this.mediaUpgradeTimerItem = mediaUpgradeTimerItem + chatItems += mediaUpgradeTimerItem + } + + @VisibleForTesting + fun mapOperatorJoined(action: Action.OperatorJoined, state: State): State = state.apply { + chatItems += action.run { + OperatorStatusItem.Joined(companyName, operatorFormattedName, operatorImageUrl) + } + } + + @VisibleForTesting + fun mapResponseCardClicked(responseCard: OperatorMessageItem.ResponseCard, state: State): State = state.apply { + val index = chatItems.indexOf(responseCard) + chatItems[index] = responseCard.asPlainText() + } + + @VisibleForTesting + fun addUnsentMessage(message: Unsent, state: State): State { + state.unsentItems += message + return state.apply { + val index = if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1 + chatItems.add(index, message.chatMessage) + } + } + + @VisibleForTesting + fun mapOperatorConnected(action: Action.OperatorConnected, state: State): State { + val operatorStatusItem = action.run { OperatorStatusItem.Connected(companyName, operatorFormattedName, operatorImageUrl) } + val oldOperatorStatusItem: OperatorStatusItem? = state.operatorStatusItem + state.operatorStatusItem = operatorStatusItem + + checkUnsentMessages(state) + + if (oldOperatorStatusItem != null) { + val index = state.chatItems.indexOf(oldOperatorStatusItem) + + if (index != -1) { + state.chatItems[index] = operatorStatusItem + return state + } + } + + state.chatItems += operatorStatusItem + + return state + } + + @VisibleForTesting + fun mapTransferring(state: State): State = state.apply { + operatorStatusItem?.also { chatItems -= it } + operatorStatusItem = OperatorStatusItem.Transferring.also { + chatItems += it + } + } + + @VisibleForTesting + fun mapInQueue(companyName: String, state: State): State = state.apply { + OperatorStatusItem.InQueue(companyName).also { + operatorStatusItem = it + chatItems += it + } + } + + @VisibleForTesting + fun markMessagesReadWithDelay() { + val disposable = markMessagesReadWithDelayUseCase() + .toSingleDefault(Unit) + .toFlowable() + .withLatestFrom(state) { _, messagesState: State -> removeNewMessagesDivider(messagesState) } + .subscribe(state::onNext) { it.printStackTrace() } + compositeDisposable.add(disposable) + } + + @VisibleForTesting + fun removeNewMessagesDivider(messagesState: State) = messagesState.apply { + chatItems.remove(NewMessagesDividerItem) + } + + fun reloadHistoryIfNeeded() { + if (isAuthenticatedUseCase()) return + + compositeDisposable.add( + loadHistoryUseCase().map { mapChatHistory(it, state.value) } + .subscribe({ state.onNext(it) }) { Logger.e(TAG, "Chat reload failed", it) } + ) + } + + internal data class State( + val chatItems: MutableList = mutableListOf(), + val chatItemIds: MutableSet = mutableSetOf(), + val unsentItems: MutableList = mutableListOf(), + var lastMessageWithVisibleOperatorImage: OperatorChatItem? = null, + var operatorStatusItem: OperatorStatusItem? = null, + var mediaUpgradeTimerItem: MediaUpgradeStartedTimerItem? = null, + var addedMessagesCount: Int = 0 + ) { + val immutableChatItems: List get() = chatItems.toList() + + fun isNew(chatMessageInternal: ChatMessageInternal): Boolean = chatItemIds.add(chatMessageInternal.chatMessage.id) + + fun isOperatorChanged(operatorChatItem: OperatorChatItem): Boolean = lastMessageWithVisibleOperatorImage.let { + lastMessageWithVisibleOperatorImage = operatorChatItem + it?.operatorId != operatorChatItem.operatorId + } + + fun resetOperator() { + lastMessageWithVisibleOperatorImage = null + } + } + + internal sealed interface Action { + data class QueuingStarted(val companyName: String) : Action + data class OperatorConnected(val companyName: String, val operatorFormattedName: String, val operatorImageUrl: String?) : Action + object Transferring : Action + data class OperatorJoined(val companyName: String, val operatorFormattedName: String, val operatorImageUrl: String?) : Action + data class UnsentMessageReceived(val message: Unsent) : Action + data class ResponseCardClicked(val responseCard: OperatorMessageItem.ResponseCard) : Action + data class OnMediaUpgradeStarted(val isVideo: Boolean) : Action + data class OnMediaUpgradeTimerUpdated(val formattedValue: String) : Action + object OnMediaUpgradeToVideo : Action + object OnMediaUpgradeCanceled : Action + data class CustomCardClicked(val customCard: CustomCardChatItem, val attachment: SingleChoiceAttachment) : Action + object ChatRestored : Action + data class MessageSent(val message: VisitorMessage) : Action + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index d68fad930..064cde9a0 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -17,6 +17,7 @@ import android.text.Editable import android.text.TextWatcher import android.util.AttributeSet import android.view.View +import android.view.accessibility.AccessibilityEvent import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog @@ -44,14 +45,14 @@ import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.chat.adapter.ChatAdapter.OnCustomCardResponse import com.glia.widgets.chat.adapter.ChatAdapter.OnFileItemClickListener import com.glia.widgets.chat.adapter.ChatAdapter.OnImageItemClickListener +import com.glia.widgets.chat.adapter.ChatItemHeightManager import com.glia.widgets.chat.adapter.UploadAttachmentAdapter -import com.glia.widgets.chat.adapter.holder.WebViewViewHolder import com.glia.widgets.chat.controller.ChatController +import com.glia.widgets.chat.model.AttachmentItem import com.glia.widgets.chat.model.ChatInputMode +import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.ChatState -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.OperatorAttachmentItem -import com.glia.widgets.chat.model.history.VisitorAttachmentItem +import com.glia.widgets.chat.model.CustomCardChatItem import com.glia.widgets.core.configuration.GliaSdkConfiguration import com.glia.widgets.core.dialog.Dialog import com.glia.widgets.core.dialog.DialogController @@ -82,7 +83,6 @@ import com.glia.widgets.helper.getFontCompat import com.glia.widgets.helper.getFullHybridTheme import com.glia.widgets.helper.hideKeyboard import com.glia.widgets.helper.insetsController -import com.glia.widgets.helper.isDownloaded import com.glia.widgets.helper.layoutInflater import com.glia.widgets.helper.mapUriToFileAttachment import com.glia.widgets.helper.requireActivity @@ -173,17 +173,18 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } } private val onCustomCardResponse = - OnCustomCardResponse { messageId: String, text: String, value: String -> - controller?.sendCustomCardResponse(messageId, text, value) + OnCustomCardResponse { customCard: CustomCardChatItem, text: String, value: String -> + controller?.sendCustomCardResponse(customCard, text, value) } private val dataObserver: AdapterDataObserver = object : AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) + val totalItemCount = adapter.itemCount val lastIndex = totalItemCount - 1 - if (isInBottom) { - val holder = binding.chatRecyclerView.findViewHolderForAdapterPosition(lastIndex) - if (holder is WebViewViewHolder) { + if (isInBottom && lastIndex != -1) { + val itemViewType = adapter.getItemViewType(lastIndex) + if (itemViewType == ChatAdapter.CUSTOM_CARD_TYPE) { // WebView needs time for calculating the height. // So to scroll to the bottom, we need to do it with delay. postDelayed( @@ -210,6 +211,10 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty ) } + private val onGvaButtonsClickListener = ChatAdapter.OnGvaButtonsClickListener { + controller?.onGvaButtonClicked(it) + } + init { initConfigurations() bindViews() @@ -388,8 +393,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty updateShowSendButton(chatState) updateChatEditText(chatState) updateAppBar(chatState) - binding.newMessagesIndicatorLayout.isVisible = - chatState.showMessagesUnseenIndicator() + binding.newMessagesIndicatorLayout.isVisible = chatState.showMessagesUnseenIndicator updateNewMessageOperatorStatusView(chatState.operatorProfileImgUrl) isInBottom = chatState.isChatInBottom binding.chatRecyclerView.setInBottom(isInBottom) @@ -402,6 +406,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty binding.operatorTypingAnimationView.isVisible = chatState.isOperatorTyping updateAttachmentButton(chatState) + updateQuickRepliesState(chatState) } } @@ -434,10 +439,12 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } override fun smoothScrollToBottom() { + if (adapter.itemCount < 1) return post { binding.chatRecyclerView.smoothScrollToPosition(adapter.itemCount - 1) } } override fun scrollToBottomImmediate() { + if (adapter.itemCount < 1) return post { binding.chatRecyclerView.scrollToPosition(adapter.itemCount - 1) } } @@ -478,9 +485,64 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty override fun fileIsNotReadyForPreview() { showToast(context.getString(R.string.glia_view_file_not_ready_for_preview)) } + + override fun showBroadcastNotSupportedToast() { + showToast(context.getString(R.string.gva_not_supported)) + } + + override fun requestOpenUri(uri: Uri) { + this@ChatView.requestOpenUri(uri) + } + + override fun requestOpenDialer(uri: Uri) { + this@ChatView.requestOpenDialer(uri) + } + + override fun requestOpenEmailClient(uri: Uri) { + this@ChatView.requestOpenEmailClient(uri) + } + } + } + + private fun requestOpenEmailClient(uri: Uri) { + val intent = Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse("mailto:")) //This step makes sure that only email apps handle this. + .putExtra(Intent.EXTRA_EMAIL, arrayOf(uri.schemeSpecificPart)) + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + Logger.e(TAG, "No email client, uri - $uri") + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) + } + } + + private fun requestOpenDialer(uri: Uri) { + val intent = Intent(Intent.ACTION_DIAL).setData(uri) + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + Logger.e(TAG, "No dialer uri - $uri") + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) } } + private fun requestOpenUri(uri: Uri) { + Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE).also { + if (it.resolveActivity(context.packageManager) != null) { + context.startActivity(it) + } else { + Logger.e(TAG, "No app to open url - $uri") + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) + } + } + } + + private fun updateQuickRepliesState(chatState: ChatState) { + binding.gvaQuickRepliesLayout.setButtons(chatState.gvaQuickReplies) + } + private fun updateNewMessageOperatorStatusView(operatorProfileImgUrl: String?) { binding.newMessagesIndicatorImage.apply { operatorProfileImgUrl?.also(::showProfileImage) ?: showPlaceholder() @@ -508,6 +570,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty Dialog.MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING -> post { showAllowScreenSharingNotificationsAndStartSharingDialog() } + Dialog.MODE_VISITOR_CODE -> { Logger.e(TAG, "DialogController callback in ChatView with MODE_VISITOR_CODE") } // Should never happen inside ChatView @@ -522,24 +585,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } private fun updateIsFileDownloaded(item: ChatItem): ChatItem = when (item) { - is OperatorAttachmentItem -> OperatorAttachmentItem( - item.id, - item.viewType, - item.showChatHead, - item.attachmentFile, - item.operatorProfileImgUrl, - item.attachmentFile.isDownloaded(context), - item.isDownloading, - item.operatorId, - item.messageId, - item.timestamp - ) - - is VisitorAttachmentItem -> VisitorAttachmentItem.editDownloadedStatus( - item, - item.attachmentFile.isDownloaded(context) - ) - + is AttachmentItem -> item.run { updateWith(isDownloaded(context), isDownloading) } else -> item } @@ -595,12 +641,12 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty }, negativeButtonClickListener = { dismissAlertDialog() - controller?.notificationsDialogDismissed() + controller?.notificationDialogDismissed() screenSharingController?.onScreenSharingDeclined() }, cancelListener = { it.dismiss() - controller?.notificationsDialogDismissed() + controller?.notificationDialogDismissed() screenSharingController?.onScreenSharingDeclined() } ) @@ -618,16 +664,16 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty negativeButtonText = resources.getString(R.string.glia_dialog_allow_notifications_no), positiveButtonClickListener = { dismissAlertDialog() - controller?.notificationsDialogDismissed() + controller?.notificationDialogDismissed() this.context.openNotificationChannelScreen() }, negativeButtonClickListener = { dismissAlertDialog() - controller?.notificationsDialogDismissed() + controller?.notificationDialogDismissed() }, cancelListener = { it.dismiss() - controller?.notificationsDialogDismissed() + controller?.notificationDialogDismissed() } ) } @@ -702,6 +748,8 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty this, this, onCustomCardResponse, + onGvaButtonsClickListener, + ChatItemHeightManager(theme, layoutInflater, resources), GliaWidgets.getCustomCardAdapter(), Dependencies.getUseCaseFactory().createGetImageFileFromCacheUseCase(), Dependencies.getUseCaseFactory().createGetImageFileFromDownloadsUseCase(), @@ -762,6 +810,8 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty binding.operatorTypingAnimationView.addColorFilter(color = it) } + binding.gvaQuickRepliesLayout.updateTheme(theme) + applyTheme(Dependencies.getGliaThemeManager().theme) } @@ -790,6 +840,15 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } binding.appBarView.setOnXClickedListener { controller?.onXButtonClicked() } binding.newMessagesIndicatorCard.setOnClickListener { controller?.newMessagesIndicatorClicked() } + binding.gvaQuickRepliesLayout.onItemClickedListener = GvaChipGroup.OnItemClickedListener { + controller?.onGvaButtonClicked(it) + + // move the focus back to the chat list + binding.chatRecyclerView.adapter?.itemCount?.let { size -> + val viewHolder = binding.chatRecyclerView.findViewHolderForAdapterPosition(size - 1) + viewHolder?.itemView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } } private fun setupAddAttachmentButton() { @@ -1101,30 +1160,11 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty currentChatItem: ChatItem, isDownloading: Boolean, isFileExists: Boolean - ): ChatItem { - if (currentChatItem is VisitorAttachmentItem) { - if (currentChatItem.attachmentFile.id == attachmentFile.id) { - return VisitorAttachmentItem.editFileStatuses( - currentChatItem, - isFileExists, - isDownloading - ) - } - } else if (currentChatItem is OperatorAttachmentItem && currentChatItem.attachmentFile.id == attachmentFile.id) { - return OperatorAttachmentItem( - currentChatItem.id, - currentChatItem.viewType, - currentChatItem.showChatHead, - currentChatItem.attachmentFile, - currentChatItem.operatorProfileImgUrl, - isFileExists, - isDownloading, - currentChatItem.operatorId, - currentChatItem.messageId, - currentChatItem.timestamp - ) - } - return currentChatItem + ): ChatItem = when { + currentChatItem is AttachmentItem && currentChatItem.attachmentId == attachmentFile.id -> + currentChatItem.updateWith(isFileExists, isDownloading) + + else -> currentChatItem } override fun onFileOpenClick(file: AttachmentFile) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java index feb922015..64b6d8e19 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java @@ -1,15 +1,18 @@ package com.glia.widgets.chat; +import android.net.Uri; import android.view.View; import androidx.annotation.NonNull; import com.glia.androidsdk.chat.AttachmentFile; import com.glia.androidsdk.engagement.Survey; +import com.glia.widgets.chat.model.ChatItem; import com.glia.widgets.chat.model.ChatState; -import com.glia.widgets.chat.model.history.ChatItem; import com.glia.widgets.core.fileupload.model.FileAttachment; +import org.jetbrains.annotations.NotNull; + import java.util.List; public interface ChatViewCallback { @@ -43,4 +46,12 @@ public interface ChatViewCallback { void navigateToPreview(AttachmentFile attachmentFile, View view); void fileIsNotReadyForPreview(); + + void showBroadcastNotSupportedToast(); + + void requestOpenUri(@NonNull Uri uri); + + void requestOpenDialer(@NotNull Uri uri); + + void requestOpenEmailClient(@NotNull Uri uri); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt new file mode 100644 index 000000000..914cc46cd --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt @@ -0,0 +1,123 @@ +package com.glia.widgets.chat + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.transition.TransitionManager +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.di.Dependencies +import com.glia.widgets.helper.applyShadow +import com.glia.widgets.helper.getColorCompat +import com.glia.widgets.helper.wrapWithMaterialThemeOverlay +import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.google.android.material.transition.MaterialFadeThrough + +class GvaChip @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.chipStyle +) : Chip(context.wrapWithMaterialThemeOverlay(attrs, defStyleAttr), attrs, defStyleAttr) { + + private val quickReplyTheme: ButtonTheme? by lazy { + Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.quickReplyTheme + } + + init { + applyQuickReplyTheme() + } + + private fun applyQuickReplyTheme() { + quickReplyTheme?.also { applyButtonTheme(it) } + } + + private fun applyButtonTheme(buttonTheme: ButtonTheme) { + applyTextTheme(buttonTheme.text, withAlignment = false) + applyBackgroundTheme(buttonTheme.background) + buttonTheme.elevation?.also { elevation = it } + buttonTheme.shadowColor?.also(::applyShadow) + } + + private fun applyBackgroundTheme(background: LayerTheme?) { + background?.apply { + fill?.also { chipBackgroundColor = it.primaryColorStateList } + stroke?.also { chipStrokeColor = ColorStateList.valueOf(it) } + borderWidth?.also { chipStrokeWidth = it } + cornerRadius?.also { shapeAppearanceModel = shapeAppearanceModel.withCornerSize(it) } + } + } + + internal fun applyUiTheme(uiTheme: UiTheme?) { + with(uiTheme ?: return) { + gvaQuickReplyBackgroundColor?.let { setChipBackgroundColorResource(it) } + gvaQuickReplyStrokeColor?.let { setChipStrokeColorResource(it) } + gvaQuickReplyTextColor?.let { getColorCompat(it) }?.let { setTextColor(it) } + applyQuickReplyTheme() + } + } + +} + +class GvaChipGroup @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle +) : ChipGroup(context.wrapWithMaterialThemeOverlay(attrs, defStyleAttr), attrs, defStyleAttr) { + + internal var onItemClickedListener: OnItemClickedListener? = null + private var theme: UiTheme? = null + + init { + isSelectionRequired = false + isSingleLine = false + isSingleSelection = false + } + + internal fun updateTheme(theme: UiTheme?) { + this.theme = theme + + children.forEach { (it as? GvaChip)?.applyUiTheme(theme) } + } + + internal fun setButtons(buttons: List) { + + val hasItems = buttons.isNotEmpty() + + if (hasItems) { + removeAllViews() + buttons.forEach { addButton(it, theme) } + + if (isVisible) return + + TransitionManager.beginDelayedTransition(parent as ViewGroup, MaterialFadeThrough()) + } + + isVisible = hasItems + } + + private fun addButton(gvaButton: GvaButton, uiTheme: UiTheme?) { + GvaChip(context).apply { + applyUiTheme(uiTheme) + text = gvaButton.text + setOnClickListener { + onItemClickedListener?.onItemClicked(gvaButton) + this@GvaChipGroup.isVisible = false + } + + addView(this) + } + } + + + internal fun interface OnItemClickedListener { + fun onItemClicked(gvaButton: GvaButton) + } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt index dc71c2990..d8ea77c94 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt @@ -9,6 +9,9 @@ import com.glia.androidsdk.chat.AttachmentFile import com.glia.widgets.R import com.glia.widgets.UiTheme import com.glia.widgets.chat.adapter.holder.CustomCardViewHolder +import com.glia.widgets.chat.adapter.holder.GvaGalleryViewHolder +import com.glia.widgets.chat.adapter.holder.GvaPersistentButtonsViewHolder +import com.glia.widgets.chat.adapter.holder.GvaResponseTextViewHolder import com.glia.widgets.chat.adapter.holder.MediaUpgradeStartedViewHolder import com.glia.widgets.chat.adapter.holder.NewMessagesDividerViewHolder import com.glia.widgets.chat.adapter.holder.OperatorMessageViewHolder @@ -20,15 +23,22 @@ import com.glia.widgets.chat.adapter.holder.fileattachment.VisitorFileAttachment import com.glia.widgets.chat.adapter.holder.imageattachment.ImageAttachmentViewHolder import com.glia.widgets.chat.adapter.holder.imageattachment.OperatorImageAttachmentViewHolder import com.glia.widgets.chat.adapter.holder.imageattachment.VisitorImageAttachmentViewHolder -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.CustomCardItem -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem -import com.glia.widgets.chat.model.history.OperatorAttachmentItem -import com.glia.widgets.chat.model.history.OperatorMessageItem -import com.glia.widgets.chat.model.history.OperatorStatusItem -import com.glia.widgets.chat.model.history.SystemChatItem -import com.glia.widgets.chat.model.history.VisitorAttachmentItem -import com.glia.widgets.chat.model.history.VisitorMessageItem +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.CustomCardChatItem +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.chat.model.GvaPersistentButtons +import com.glia.widgets.chat.model.GvaQuickReplies +import com.glia.widgets.chat.model.GvaResponseText +import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.OperatorAttachmentItem +import com.glia.widgets.chat.model.OperatorMessageItem +import com.glia.widgets.chat.model.OperatorStatusItem +import com.glia.widgets.chat.model.SystemChatItem +import com.glia.widgets.chat.model.VisitorAttachmentItem +import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.databinding.ChatGvaGalleryLayoutBinding +import com.glia.widgets.databinding.ChatGvaPersistentButtonsContentBinding import com.glia.widgets.databinding.ChatMediaUpgradeLayoutBinding import com.glia.widgets.databinding.ChatNewMessagesDividerLayoutBinding import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding @@ -47,12 +57,14 @@ internal class ChatAdapter( private val onFileItemClickListener: OnFileItemClickListener, private val onImageItemClickListener: OnImageItemClickListener, private val onCustomCardResponse: OnCustomCardResponse, + private val onGvaButtonsClickListener: OnGvaButtonsClickListener, + private val chatItemHeightManager: ChatItemHeightManager, private val customCardAdapter: CustomCardAdapter?, private val getImageFileFromCacheUseCase: GetImageFileFromCacheUseCase, private val getImageFileFromDownloadsUseCase: GetImageFileFromDownloadsUseCase, private val getImageFileFromNetworkUseCase: GetImageFileFromNetworkUseCase ) : RecyclerView.Adapter() { - private val differ = AsyncListDiffer(this, ChatAdapterDillCallback()) + private val differ = AsyncListDiffer(this, ChatAdapterDiffCallback()) override fun onCreateViewHolder( parent: ViewGroup, @@ -66,11 +78,13 @@ internal class ChatAdapter( uiTheme ) } + VISITOR_FILE_VIEW_TYPE -> { val view = inflater.inflate(R.layout.chat_attachment_visitor_file_layout, parent, false) VisitorFileAttachmentViewHolder(view, uiTheme) } + VISITOR_IMAGE_VIEW_TYPE -> { VisitorImageAttachmentViewHolder( inflater.inflate(R.layout.chat_attachment_visitor_image_layout, parent, false), @@ -80,12 +94,14 @@ internal class ChatAdapter( getImageFileFromNetworkUseCase ) } + VISITOR_MESSAGE_TYPE -> { VisitorMessageViewHolder( ChatVisitorMessageLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + OPERATOR_IMAGE_VIEW_TYPE -> { OperatorImageAttachmentViewHolder( inflater.inflate(R.layout.chat_attachment_operator_image_layout, parent, false), @@ -95,6 +111,7 @@ internal class ChatAdapter( getImageFileFromNetworkUseCase ) } + OPERATOR_FILE_VIEW_TYPE -> { OperatorFileAttachmentViewHolder( inflater.inflate( @@ -105,18 +122,21 @@ internal class ChatAdapter( uiTheme ) } + OPERATOR_MESSAGE_VIEW_TYPE -> { OperatorMessageViewHolder( ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + MEDIA_UPGRADE_ITEM_TYPE -> { MediaUpgradeStartedViewHolder( ChatMediaUpgradeLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + NEW_MESSAGES_DIVIDER_TYPE -> { NewMessagesDividerViewHolder( ChatNewMessagesDividerLayoutBinding.inflate( @@ -127,6 +147,7 @@ internal class ChatAdapter( uiTheme ) } + SYSTEM_MESSAGE_TYPE -> SystemMessageViewHolder( ChatReceiveMessageContentBinding.inflate( inflater, @@ -135,6 +156,47 @@ internal class ChatAdapter( ), uiTheme ) + + GVA_RESPONSE_TEXT_TYPE, GVA_QUICK_REPLIES_TYPE -> { + val operatorMessageBinding = ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false) + GvaResponseTextViewHolder( + operatorMessageBinding, + ChatReceiveMessageContentBinding.inflate( + inflater, + operatorMessageBinding.contentLayout, + true + ), + uiTheme + ) + } + + GVA_PERSISTENT_BUTTONS_TYPE -> { + val operatorMessageBinding = ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false) + GvaPersistentButtonsViewHolder( + operatorMessageBinding, + ChatGvaPersistentButtonsContentBinding.inflate( + inflater, + operatorMessageBinding.contentLayout, + true + ), + onGvaButtonsClickListener, + uiTheme + ) + } + + GVA_GALLERY_CARDS_TYPE -> { + GvaGalleryViewHolder( + ChatGvaGalleryLayoutBinding.inflate( + inflater, + parent, + false + ), + onGvaButtonsClickListener, + uiTheme + ) + } + + else -> { var customCardViewHolder: CustomCardViewHolder? = null if (customCardAdapter != null) { @@ -152,64 +214,70 @@ internal class ChatAdapter( } } - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - if (differ.currentList[position] is MediaUpgradeStartedTimerItem) { - val time = (payloads.firstOrNull() as? String) - - if (time != null) { - (holder as? MediaUpgradeStartedViewHolder)?.updateTime(time) - return - } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList) { + val isHandled: Boolean = when (holder) { + is MediaUpgradeStartedViewHolder -> updateMediaUpgradeTimer(payloads, holder) + is VisitorMessageViewHolder -> updateDeliveredState(payloads, holder) + is VisitorFileAttachmentViewHolder -> updateDeliveredState(payloads, holder) + is VisitorImageAttachmentViewHolder -> updateDeliveredState(payloads, holder) + else -> false + } + + if (!isHandled) { + super.onBindViewHolder(holder, position, payloads) } - super.onBindViewHolder(holder, position, payloads) } + private fun updateDeliveredState(payloads: MutableList, holder: VisitorMessageViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + + private fun updateDeliveredState(payloads: MutableList, holder: VisitorFileAttachmentViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + + private fun updateDeliveredState(payloads: MutableList, holder: VisitorImageAttachmentViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + + private fun updateMediaUpgradeTimer(payloads: MutableList, viewHolder: MediaUpgradeStartedViewHolder): Boolean = payloads.run { + firstOrNull() as? String + }?.let { + viewHolder.updateTime(it) + true + } ?: false + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val chatItem = differ.currentList[position]) { is OperatorStatusItem -> (holder as OperatorStatusViewHolder).bind(chatItem) is VisitorMessageItem -> (holder as VisitorMessageViewHolder).bind(chatItem) - is OperatorMessageItem -> (holder as OperatorMessageViewHolder).bind( - chatItem, - onOptionClickedListener - ) - is MediaUpgradeStartedTimerItem -> (holder as MediaUpgradeStartedViewHolder).bind( - chatItem - ) - is OperatorAttachmentItem -> { - if (chatItem.getViewType() == OPERATOR_FILE_VIEW_TYPE) { - (holder as OperatorFileAttachmentViewHolder).bind( - chatItem, - onFileItemClickListener - ) - } else { - (holder as OperatorImageAttachmentViewHolder).bind( - chatItem, - onImageItemClickListener - ) - } - } - is VisitorAttachmentItem -> { - if (chatItem.getViewType() == VISITOR_FILE_VIEW_TYPE) { - (holder as VisitorFileAttachmentViewHolder).bind( - chatItem, - onFileItemClickListener - ) - } else { - val viewHolder = holder as VisitorImageAttachmentViewHolder - viewHolder.bind(chatItem.attachmentFile, chatItem.showDelivered) - viewHolder.itemView.setOnClickListener { - onImageItemClickListener.onImageItemClick(chatItem.attachmentFile, it) - } + is OperatorMessageItem -> (holder as OperatorMessageViewHolder).bind(chatItem, onOptionClickedListener) + is MediaUpgradeStartedTimerItem -> (holder as MediaUpgradeStartedViewHolder).bind(chatItem) + is OperatorAttachmentItem.Image -> (holder as OperatorImageAttachmentViewHolder).bind(chatItem, onImageItemClickListener) + is OperatorAttachmentItem.File -> (holder as OperatorFileAttachmentViewHolder).bind(chatItem, onFileItemClickListener) + is VisitorAttachmentItem.File -> (holder as VisitorFileAttachmentViewHolder).bind(chatItem, onFileItemClickListener) + is VisitorAttachmentItem.Image -> { + val viewHolder = holder as VisitorImageAttachmentViewHolder + viewHolder.bind(chatItem.attachmentFile, chatItem.showDelivered) + viewHolder.itemView.setOnClickListener { + onImageItemClickListener.onImageItemClick(chatItem.attachmentFile, it) } } + is SystemChatItem -> (holder as SystemMessageViewHolder).bind(chatItem.message) - is CustomCardItem -> { + is GvaResponseText -> (holder as GvaResponseTextViewHolder).bind(chatItem) + is GvaQuickReplies -> (holder as GvaResponseTextViewHolder).bind(chatItem.asResponseText()) + is GvaPersistentButtons -> (holder as GvaPersistentButtonsViewHolder).bind(chatItem) + is GvaGalleryCards -> (holder as GvaGalleryViewHolder).bind(chatItem, chatItemHeightManager.getMeasuredHeight(chatItem)) + is CustomCardChatItem -> { (holder as CustomCardViewHolder).bind(chatItem.message) { text: String, value: String -> - onCustomCardResponse.onCustomCardResponse(chatItem.getId(), text, value) + onCustomCardResponse.onCustomCardResponse(chatItem, text, value) } } } @@ -229,6 +297,7 @@ internal class ChatAdapter( } fun submitList(items: List?) { + chatItemHeightManager.measureHeight(items) differ.submitList(items) } @@ -240,12 +309,16 @@ internal class ChatAdapter( fun onFileDownloadClick(file: AttachmentFile) } - interface OnImageItemClickListener { + fun interface OnImageItemClickListener { fun onImageItemClick(item: AttachmentFile, view: View) } fun interface OnCustomCardResponse { - fun onCustomCardResponse(messageId: String, text: String, value: String) + fun onCustomCardResponse(customCard: CustomCardChatItem, text: String, value: String) + } + + fun interface OnGvaButtonsClickListener { + fun onGvaButtonClicked(gvaButton: GvaButton) } companion object { @@ -259,7 +332,15 @@ internal class ChatAdapter( const val VISITOR_IMAGE_VIEW_TYPE = 7 const val NEW_MESSAGES_DIVIDER_TYPE = 8 const val SYSTEM_MESSAGE_TYPE = 9 - const val CUSTOM_CARD_TYPE = 10 // Should be the last type with the highest value + + //GVA Types + const val GVA_RESPONSE_TEXT_TYPE = 10 + const val GVA_QUICK_REPLIES_TYPE = 11 + const val GVA_PERSISTENT_BUTTONS_TYPE = 12 + const val GVA_GALLERY_CARDS_TYPE = 13 + + //Custom Card + const val CUSTOM_CARD_TYPE = 14 // Should be the last type with the highest value } @IntDef( @@ -273,7 +354,11 @@ internal class ChatAdapter( VISITOR_IMAGE_VIEW_TYPE, NEW_MESSAGES_DIVIDER_TYPE, SYSTEM_MESSAGE_TYPE, - CUSTOM_CARD_TYPE + CUSTOM_CARD_TYPE, + GVA_RESPONSE_TEXT_TYPE, + GVA_QUICK_REPLIES_TYPE, + GVA_PERSISTENT_BUTTONS_TYPE, + GVA_GALLERY_CARDS_TYPE ) @Retention(AnnotationRetention.SOURCE) annotation class Type diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt new file mode 100644 index 000000000..b9fd760ce --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt @@ -0,0 +1,18 @@ +package com.glia.widgets.chat.adapter + +import androidx.recyclerview.widget.DiffUtil +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.VisitorChatItem + +internal class ChatAdapterDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean = oldItem.areContentsTheSame(newItem) + + override fun getChangePayload(oldItem: ChatItem, newItem: ChatItem): Any? = when { + oldItem is MediaUpgradeStartedTimerItem.Audio && newItem is MediaUpgradeStartedTimerItem.Audio -> newItem.time + oldItem is MediaUpgradeStartedTimerItem.Video && newItem is MediaUpgradeStartedTimerItem.Video -> newItem.time + oldItem is VisitorChatItem && newItem is VisitorChatItem -> newItem.showDelivered + else -> null + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDillCallback.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDillCallback.kt deleted file mode 100644 index a406c7066..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDillCallback.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.glia.widgets.chat.adapter - -import androidx.recyclerview.widget.DiffUtil -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.CustomCardItem -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem -import com.glia.widgets.chat.model.history.OperatorAttachmentItem -import com.glia.widgets.chat.model.history.OperatorMessageItem -import com.glia.widgets.chat.model.history.OperatorStatusItem -import com.glia.widgets.chat.model.history.VisitorAttachmentItem -import com.glia.widgets.chat.model.history.VisitorMessageItem - -class ChatAdapterDillCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { - return when { - oldItem is OperatorStatusItem && newItem is OperatorStatusItem -> oldItem == newItem - oldItem is VisitorMessageItem && newItem is VisitorMessageItem -> oldItem == newItem - oldItem is OperatorMessageItem && newItem is OperatorMessageItem -> oldItem == newItem - oldItem is MediaUpgradeStartedTimerItem && newItem is MediaUpgradeStartedTimerItem -> oldItem == newItem - oldItem is OperatorAttachmentItem && newItem is OperatorAttachmentItem -> oldItem == newItem - oldItem is VisitorAttachmentItem && newItem is VisitorAttachmentItem -> oldItem == newItem - oldItem is CustomCardItem && newItem is CustomCardItem -> oldItem == newItem - else -> false - } - } - - override fun getChangePayload(oldItem: ChatItem, newItem: ChatItem): Any? { - if ( - oldItem is MediaUpgradeStartedTimerItem && - newItem is MediaUpgradeStartedTimerItem && - oldItem.type == newItem.type - ) { - return newItem.time - } - - return null - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt new file mode 100644 index 000000000..3ca797b76 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt @@ -0,0 +1,59 @@ +package com.glia.widgets.chat.adapter + +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import androidx.collection.ArrayMap +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.holder.GvaGalleryItemViewHolder +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.databinding.ChatGvaGalleryItemBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme + +internal class ChatItemHeightManager( + private val uiTheme: UiTheme, + private val layoutInflater: LayoutInflater, + private val resources: Resources, + private val unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) { + private val measuredHeightsMap = ArrayMap() + + private val gvaGalleryItemViewHolder: GvaGalleryItemViewHolder by lazy { + GvaGalleryItemViewHolder(ChatGvaGalleryItemBinding.inflate(layoutInflater), {}, uiTheme, unifiedTheme) + } + + private val gvaGalleryCardWidth: Int by lazy { + resources.getDimensionPixelOffset(R.dimen.glia_chat_gva_gallery_card_width) + } + + fun getMeasuredHeight(chatItem: ChatItem): Int? { + return measuredHeightsMap[chatItem] + } + + fun measureHeight(chatItems: List?) { + chatItems + ?.filterIsInstance() // Currently the HeightManager works only for GvaGalleryCards + ?.forEach { chatItem -> + if (!measuredHeightsMap.contains(chatItem)) { + measuredHeightsMap[chatItem] = measureHeight(chatItem) + } + } + } + + private fun measureHeight(gvaGalleryCards: GvaGalleryCards): Int { + return gvaGalleryCards.galleryCards.maxOf(::measureHeight) + } + + private fun measureHeight(gvaGalleryCard: GvaGalleryCard): Int { + gvaGalleryItemViewHolder.bindForMeasure(gvaGalleryCard) + gvaGalleryItemViewHolder.itemView.measure( + View.MeasureSpec.makeMeasureSpec(gvaGalleryCardWidth, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + return gvaGalleryItemViewHolder.itemView.measuredHeight + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt new file mode 100644 index 000000000..3173228d2 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt @@ -0,0 +1,59 @@ +package com.glia.widgets.chat.adapter + +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.view.ContextThemeWrapper +import androidx.appcompat.widget.LinearLayoutCompat +import androidx.recyclerview.widget.RecyclerView +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.helper.Utils +import com.glia.widgets.helper.getFontCompat +import com.glia.widgets.view.unifiedui.applyButtonTheme +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.google.android.material.button.MaterialButton + +internal class GvaButtonsAdapter( + private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + private val uiTheme: UiTheme, + private val buttonTheme: ButtonTheme? +) : RecyclerView.Adapter() { + private var options: List? = null + + fun setOptions(options: List) { + this.options = options + notifyDataSetChanged() + } + + class ButtonViewHolder( + private val buttonView: MaterialButton + ) : RecyclerView.ViewHolder(buttonView) { + fun bind(button: GvaButton, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener) { + buttonView.contentDescription = button.text + buttonView.text = button.text + buttonView.setOnClickListener { + buttonsClickListener.onGvaButtonClicked(button) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ButtonViewHolder { + val styleResId = Utils.getAttrResourceId(parent.context, R.attr.gvaOptionButtonStyle) + val button = MaterialButton(ContextThemeWrapper(parent.context, styleResId), null, 0).also { + it.id = View.generateViewId() + + uiTheme.fontRes?.let(parent::getFontCompat)?.also(it::setTypeface) + + buttonTheme?.also(it::applyButtonTheme) + } + button.layoutParams = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT) + return ButtonViewHolder(button) + } + + override fun getItemCount(): Int = options?.size ?: 0 + + override fun onBindViewHolder(holder: ButtonViewHolder, position: Int) { + options?.get(position)?.let { holder.bind(it, buttonsClickListener) } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt new file mode 100644 index 000000000..25d273557 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt @@ -0,0 +1,40 @@ +package com.glia.widgets.chat.adapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.holder.GvaGalleryItemViewHolder +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.databinding.ChatGvaGalleryItemBinding +import com.glia.widgets.helper.layoutInflater +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme + +internal class GvaGalleryAdapter( + private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + private val uiTheme: UiTheme, + private val unifiedTheme: UnifiedTheme? +) : RecyclerView.Adapter() { + private var galleryCards: List? = null + + fun setGalleryCards(galleryCards: List) { + this.galleryCards = galleryCards + notifyDataSetChanged() + } + + override fun getItemCount() = galleryCards?.count() ?: 0 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GvaGalleryItemViewHolder( + ChatGvaGalleryItemBinding.inflate(parent.layoutInflater, parent, false), + buttonsClickListener, + uiTheme, + unifiedTheme + ) + + override fun onBindViewHolder(holder: GvaGalleryItemViewHolder, position: Int) { + galleryCards?.apply { + getOrNull(position)?.let { + holder.bind(it, position, size) + } + } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt new file mode 100644 index 000000000..5c3567833 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt @@ -0,0 +1,145 @@ +package com.glia.widgets.chat.adapter.holder + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.ChatAdapter +import com.glia.widgets.chat.adapter.GvaButtonsAdapter +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.databinding.ChatGvaGalleryItemBinding +import com.glia.widgets.helper.fromHtml +import com.glia.widgets.helper.getColorCompat +import com.glia.widgets.helper.getColorStateListCompat +import com.glia.widgets.helper.getFontCompat +import com.glia.widgets.helper.load +import com.glia.widgets.view.unifiedui.applyLayerTheme +import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme +import kotlin.properties.Delegates + +internal class GvaGalleryItemViewHolder( + private val binding: ChatGvaGalleryItemBinding, + buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + private val uiTheme: UiTheme, + private val unifiedTheme: UnifiedTheme? +) : ViewHolder(binding.root) { + + private var adapter: GvaButtonsAdapter by Delegates.notNull() + + private val operatorTheme: MessageBalloonTheme? by lazy { + unifiedTheme?.chatTheme?.operatorMessage + } + + private val galleryCardTheme: GvaGalleryCardTheme? by lazy { + unifiedTheme?.chatTheme?.gva?.galleryCardTheme + } + + init { + ViewCompat.setAccessibilityDelegate( + binding.root, + object : AccessibilityDelegateCompat() { + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) { + // Sends an accessibility event of accessibility focus type. + host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) + } + return super.performAccessibilityAction(host, action, args) + } + } + ) + adapter = GvaButtonsAdapter(buttonsClickListener, uiTheme, galleryCardTheme?.button) + binding.buttonsRecyclerView.adapter = adapter + binding.item.apply { + uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also { + backgroundTintList = it + } + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyLayerTheme(galleryCardTheme?.background ?: operatorTheme?.background) + } + binding.title.apply { + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyTextTheme(galleryCardTheme?.title ?: operatorTheme?.text) + } + binding.subtitle.apply { + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyTextTheme(galleryCardTheme?.subtitle ?: operatorTheme?.text) + } + galleryCardTheme?.image?.also(binding.image::applyLayerTheme) + } + + fun bindForMeasure(card: GvaGalleryCard) { + bindTexts(card) + bindButtons(card) + } + + fun bind(card: GvaGalleryCard, position: Int, size: Int) { + bindTexts(card) + bindButtons(card) + bindImage(card) + updateContendDescription(card, position, size) + } + + private fun bindTexts(card: GvaGalleryCard) { + binding.title.text = card.title.fromHtml() + + card.subtitle?.let { + binding.subtitle.text = it.fromHtml() + binding.subtitle.isVisible = true + } ?: run { + binding.subtitle.isVisible = false + } + } + + private fun bindButtons(card: GvaGalleryCard) { + adapter.setOptions(card.options) + binding.buttonsRecyclerView.isVisible = card.options.isEmpty().not() + } + + private fun bindImage(card: GvaGalleryCard) { + card.imageUrl?.let { + binding.image.load(it) + binding.image.isVisible = true + } ?: run { + binding.image.isVisible = false + } + } + + private fun updateContendDescription(card: GvaGalleryCard, position: Int, size: Int) { + val cardContentDescription = listOf(card.title, card.subtitle) + .filter { it?.isNotEmpty() ?: false } + .joinToString(separator = ". ") + + itemView.contentDescription = itemView.resources.getString( + R.string.gva_gallery_card_message_content_description, + cardContentDescription, + position + 1, + size + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt new file mode 100644 index 000000000..86e26fae7 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt @@ -0,0 +1,53 @@ +package com.glia.widgets.chat.adapter.holder + +import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSnapHelper +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.ChatAdapter +import com.glia.widgets.chat.adapter.GvaGalleryAdapter +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.databinding.ChatGvaGalleryLayoutBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme + +internal class GvaGalleryViewHolder( + private val contentBinding: ChatGvaGalleryLayoutBinding, + buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(contentBinding.root, contentBinding.chatHeadView, uiTheme, unifiedTheme) { + private val adapter = GvaGalleryAdapter(buttonsClickListener, uiTheme, unifiedTheme) + + init { + contentBinding.cardRecyclerView.adapter = adapter + contentBinding.cardRecyclerView.layoutManager = LinearLayoutManager( + contentBinding.root.context, + LinearLayoutManager.HORIZONTAL, + false + ) + LinearSnapHelper().attachToRecyclerView(contentBinding.cardRecyclerView) + } + + fun bind(item: GvaGalleryCards, measuredHeight: Int?) { + updateOperatorStatusView(item) + + setupRecyclerViewHeight(measuredHeight) + setupItems(item.galleryCards) + } + + private fun setupItems(galleryCards: List) { + adapter.setGalleryCards(galleryCards) + contentBinding.cardRecyclerView.scrollToPosition(0) + } + + private fun setupRecyclerViewHeight(measuredHeight: Int?) { + if (measuredHeight != null) { + contentBinding.cardRecyclerView.layoutParams.height = measuredHeight + } else { + contentBinding.cardRecyclerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } + } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt new file mode 100644 index 000000000..13c6d4d4b --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt @@ -0,0 +1,69 @@ +package com.glia.widgets.chat.adapter.holder + +import android.view.View +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.ChatAdapter +import com.glia.widgets.chat.adapter.GvaButtonsAdapter +import com.glia.widgets.chat.model.GvaPersistentButtons +import com.glia.widgets.databinding.ChatGvaPersistentButtonsContentBinding +import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.helper.fromHtml +import com.glia.widgets.helper.getColorCompat +import com.glia.widgets.helper.getColorStateListCompat +import com.glia.widgets.helper.getFontCompat +import com.glia.widgets.view.unifiedui.applyLayerTheme +import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme +import kotlin.properties.Delegates + +internal class GvaPersistentButtonsViewHolder( + operatorMessageBinding: ChatOperatorMessageLayoutBinding, + private val contentBinding: ChatGvaPersistentButtonsContentBinding, + buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme, unifiedTheme) { + + private var adapter: GvaButtonsAdapter by Delegates.notNull() + + private val persistentButtonTheme: GvaPersistentButtonTheme? by lazy { + unifiedTheme?.chatTheme?.gva?.persistentButtonTheme + } + + init { + adapter = GvaButtonsAdapter(buttonsClickListener, uiTheme, persistentButtonTheme?.button) + contentBinding.buttonsRecyclerView.adapter = adapter + contentBinding.root.apply { + uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also { + backgroundTintList = it + } + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyLayerTheme(persistentButtonTheme?.background ?: operatorTheme?.background) + } + contentBinding.message.apply { + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyTextTheme(persistentButtonTheme?.title ?: operatorTheme?.text) + } + } + + fun bind(item: GvaPersistentButtons) { + updateOperatorStatusView(item) + updateItemContentDescription(item.operatorName, item.content) + + contentBinding.message.text = item.content.fromHtml() + + adapter.setOptions(item.options) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt new file mode 100644 index 000000000..469b9da79 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt @@ -0,0 +1,55 @@ +package com.glia.widgets.chat.adapter.holder + +import android.view.View +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaResponseText +import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding +import com.glia.widgets.databinding.ChatReceiveMessageContentBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.helper.fromHtml +import com.glia.widgets.helper.getColorCompat +import com.glia.widgets.helper.getColorStateListCompat +import com.glia.widgets.helper.getFontCompat +import com.glia.widgets.view.unifiedui.applyLayerTheme +import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme + +internal class GvaResponseTextViewHolder( + operatorMessageBinding: ChatOperatorMessageLayoutBinding, + private val messageContentBinding: ChatReceiveMessageContentBinding, + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme, unifiedTheme) { + + init { + setupMessageContentView() + } + + private fun setupMessageContentView() { + messageContentBinding.root.apply { + uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also { + backgroundTintList = it + } + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyLayerTheme(operatorTheme?.background) + applyTextTheme(operatorTheme?.text) + } + } + + fun bind(item: GvaResponseText) { + updateOperatorStatusView(item) + updateMessageContentView(item) + updateItemContentDescription(item.operatorName, item.content) + } + + private fun updateMessageContentView(item: GvaResponseText) { + messageContentBinding.root.text = item.content.fromHtml() + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/MediaUpgradeStartedViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/MediaUpgradeStartedViewHolder.kt index 241353a3a..08a4fff5d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/MediaUpgradeStartedViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/MediaUpgradeStartedViewHolder.kt @@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.UiTheme -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem import com.glia.widgets.databinding.ChatMediaUpgradeLayoutBinding import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.getColorCompat @@ -65,21 +65,26 @@ internal class MediaUpgradeStartedViewHolder( } fun bind(chatItem: MediaUpgradeStartedTimerItem) { - if (chatItem.type == MediaUpgradeStartedTimerItem.Type.AUDIO) { - upgradeAudioIcon?.also(binding.iconView::setImageResource) - binding.iconView.contentDescription = - itemView.resources.getString(R.string.glia_chat_audio_icon_content_description) - binding.titleView.text = - itemView.resources.getString(R.string.glia_chat_upgraded_to_audio_call) - setMediaUpgradeTheme(chatTheme?.audioUpgrade) - } else { - upgradeVideoIcon?.also(binding.iconView::setImageResource) - binding.iconView.contentDescription = - itemView.resources.getString(R.string.glia_chat_video_icon_content_description) - binding.titleView.text = - itemView.resources.getString(R.string.glia_chat_upgraded_to_video_call) - setMediaUpgradeTheme(chatTheme?.videoUpgrade) + when (chatItem) { + is MediaUpgradeStartedTimerItem.Audio -> { + upgradeAudioIcon?.also(binding.iconView::setImageResource) + binding.iconView.contentDescription = + itemView.resources.getString(R.string.glia_chat_audio_icon_content_description) + binding.titleView.text = + itemView.resources.getString(R.string.glia_chat_upgraded_to_audio_call) + setMediaUpgradeTheme(chatTheme?.audioUpgrade) + } + + is MediaUpgradeStartedTimerItem.Video -> { + upgradeVideoIcon?.also(binding.iconView::setImageResource) + binding.iconView.contentDescription = + itemView.resources.getString(R.string.glia_chat_video_icon_content_description) + binding.titleView.text = + itemView.resources.getString(R.string.glia_chat_upgraded_to_video_call) + setMediaUpgradeTheme(chatTheme?.videoUpgrade) + } } + binding.timerView.text = chatItem.time } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt new file mode 100644 index 000000000..e2d2c8fe6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt @@ -0,0 +1,66 @@ +package com.glia.widgets.chat.adapter.holder + +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.OperatorChatItem +import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.OperatorStatusView +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme + +internal open class OperatorBaseViewHolder( + itemView: View, + private val chatHeadView: OperatorStatusView, + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : RecyclerView.ViewHolder(itemView) { + + val operatorTheme: MessageBalloonTheme? by lazy { + unifiedTheme?.chatTheme?.operatorMessage + } + + init { + setupOperatorStatusView() + } + + fun updateOperatorStatusView(item: OperatorChatItem) { + chatHeadView.isVisible = item.showChatHead + if (item.operatorProfileImgUrl != null) { + chatHeadView.showProfileImage(item.operatorProfileImgUrl) + } else { + chatHeadView.showPlaceholder() + } + } + + fun updateItemContentDescription(operatorName: String?, message: String?) { + when { + operatorName.isNullOrEmpty() && message.isNullOrEmpty() -> { + itemView.contentDescription = null + } + + operatorName.isNullOrEmpty().not() -> { + itemView.contentDescription = itemView.resources.getString( + R.string.glia_chat_operator_name_message_content_description, + operatorName, + message + ) + } + + else -> { + itemView.contentDescription = itemView.resources.getString( + R.string.glia_chat_operator_message_content_description, + message + ) + } + } + } + + private fun setupOperatorStatusView() { + chatHeadView.setTheme(uiTheme) + chatHeadView.setShowRippleAnimation(false) + chatHeadView.applyUserImageTheme(operatorTheme?.userImage) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorMessageViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorMessageViewHolder.kt index 85107ca10..900be8934 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorMessageViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorMessageViewHolder.kt @@ -9,8 +9,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.UiTheme -import com.glia.widgets.chat.model.history.OperatorMessageItem -import com.glia.widgets.chat.model.history.ResponseCardItem +import com.glia.widgets.chat.model.OperatorMessageItem import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding import com.glia.widgets.databinding.ChatReceiveMessageContentBinding import com.glia.widgets.di.Dependencies @@ -69,16 +68,15 @@ internal class OperatorMessageViewHolder( onOptionClickedListener: OnOptionClickedListener ) { binding.contentLayout.removeAllViews() - if (item is ResponseCardItem) { - addSingleChoiceCardView(item, onOptionClickedListener) - } else { - addMessageTextView(item) + when (item) { + is OperatorMessageItem.PlainText -> addMessageTextView(item) + is OperatorMessageItem.ResponseCard -> addSingleChoiceCardView(item, onOptionClickedListener) } updateOperatorStatusView(item) } private fun addSingleChoiceCardView( - item: ResponseCardItem, + item: OperatorMessageItem.ResponseCard, onOptionClickedListener: OnOptionClickedListener ) { val singleChoiceCardView = SingleChoiceCardView(itemView.context) @@ -101,7 +99,7 @@ internal class OperatorMessageViewHolder( itemView.contentDescription = item.content } - private fun addMessageTextView(item: OperatorMessageItem) { + private fun addMessageTextView(item: OperatorMessageItem.PlainText) { messageContentView.text = item.content binding.contentLayout.addView(messageContentView) if (!TextUtils.isEmpty(item.operatorName)) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt index 53b4f731a..0414f0bef 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt @@ -6,7 +6,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.UiTheme -import com.glia.widgets.chat.model.history.OperatorStatusItem +import com.glia.widgets.chat.model.OperatorStatusItem import com.glia.widgets.databinding.ChatOperatorStatusLayoutBinding import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.getColorCompat @@ -72,15 +72,11 @@ internal class OperatorStatusViewHolder( fun bind(item: OperatorStatusItem) { chatStartingHeadingView.text = item.companyName - when (item.status) { - OperatorStatusItem.Status.IN_QUEUE -> applyInQueueState(item.companyName) - OperatorStatusItem.Status.OPERATOR_CONNECTED -> applyConnectedState( - item.operatorName, - item.profileImgUrl - ) - - OperatorStatusItem.Status.JOINED -> applyJoinedState(item.operatorName, item.profileImgUrl) - OperatorStatusItem.Status.TRANSFERRING -> applyTransferringState() + when (item) { + is OperatorStatusItem.Connected -> applyConnectedState(item.operatorName, item.profileImgUrl) + is OperatorStatusItem.InQueue -> applyInQueueState(item.companyName) + is OperatorStatusItem.Joined -> applyConnectedState(item.operatorName, item.profileImgUrl) + is OperatorStatusItem.Transferring -> applyTransferringState() } statusPictureView.isVisible = true statusPictureView.setShowRippleAnimation(isShowStatusViewRippleAnimation(item)) @@ -90,18 +86,16 @@ internal class OperatorStatusViewHolder( statusPictureView.showPlaceholder() applyChatStartingViewsVisibility() applyChatStartedViewsVisibility(false) - itemView.contentDescription = - itemView.resources.getString( - R.string.glia_chat_in_queue_message_content_description, - companyName ?: "" - ) + itemView.contentDescription = itemView.resources.getString( + R.string.glia_chat_in_queue_message_content_description, + companyName ?: "" + ) engagementStatesTheme?.queue.also(::applyEngagementState) } private fun applyConnectedState(operatorName: String, profileImgUrl: String?) { - profileImgUrl?.let { statusPictureView.showProfileImage(it) } - ?: statusPictureView.showPlaceholder() + profileImgUrl?.let { statusPictureView.showProfileImage(it) } ?: statusPictureView.showPlaceholder() applyChatStartingViewsVisibility(false) applyChatStartedViewsVisibility() @@ -117,23 +111,6 @@ internal class OperatorStatusViewHolder( engagementStatesTheme?.connected.also(::applyEngagementState) } - private fun applyJoinedState(operatorName: String, profileImgUrl: String?) { - profileImgUrl?.let { statusPictureView.showProfileImage(it) } - ?: statusPictureView.showPlaceholder() - chatStartedNameView.text = operatorName - chatStartedCaptionView.text = - itemView.resources.getString(R.string.glia_chat_operator_has_joined, operatorName) - itemView.contentDescription = itemView.resources.getString( - R.string.glia_chat_operator_has_joined_content_description, - operatorName - ) - - applyChatStartingViewsVisibility(false) - applyChatStartedViewsVisibility() - - engagementStatesTheme?.connecting.also(::applyEngagementState) - } - private fun applyTransferringState() { statusPictureView.showPlaceholder() applyChatStartingViewsVisibility() @@ -167,7 +144,8 @@ internal class OperatorStatusViewHolder( } } - private fun isShowStatusViewRippleAnimation(item: OperatorStatusItem) = item.status.let { - it == OperatorStatusItem.Status.IN_QUEUE || it == OperatorStatusItem.Status.TRANSFERRING + private fun isShowStatusViewRippleAnimation(item: OperatorStatusItem): Boolean = when (item) { + is OperatorStatusItem.InQueue, is OperatorStatusItem.Transferring -> true + else -> false } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt index 9dff64b48..2b1e81d1b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt @@ -4,7 +4,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.UiTheme -import com.glia.widgets.chat.model.history.VisitorMessageItem +import com.glia.widgets.chat.model.VisitorMessageItem import com.glia.widgets.databinding.ChatVisitorMessageLayoutBinding import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.getColorCompat @@ -46,9 +46,9 @@ internal class VisitorMessageViewHolder( fun bind(item: VisitorMessageItem) { binding.content.text = item.message - binding.deliveredView.isVisible = item.isShowDelivered + binding.deliveredView.isVisible = item.showDelivered val contentDescription = itemView.resources.getString( - if (item.isShowDelivered) { + if (item.showDelivered) { R.string.glia_chat_visitor_message_delivered_content_description } else { R.string.glia_chat_visitor_message_content_description @@ -57,4 +57,8 @@ internal class VisitorMessageViewHolder( ) itemView.contentDescription = contentDescription } + + fun updateDelivered(delivered: Boolean) { + binding.deliveredView.isVisible = delivered + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java index 94626e57e..040340ecd 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java @@ -11,7 +11,7 @@ import com.glia.widgets.R; import com.glia.widgets.UiTheme; import com.glia.widgets.chat.adapter.ChatAdapter; -import com.glia.widgets.chat.model.history.OperatorAttachmentItem; +import com.glia.widgets.chat.model.OperatorAttachmentItem; import com.glia.widgets.view.OperatorStatusView; public class OperatorFileAttachmentViewHolder extends FileAttachmentViewHolder { @@ -23,8 +23,8 @@ public OperatorFileAttachmentViewHolder(@NonNull View itemView, UiTheme uiTheme) setupOperatorStatusView(uiTheme); } - public void bind(OperatorAttachmentItem item, ChatAdapter.OnFileItemClickListener listener) { - super.setData(item.isFileExists, item.isDownloading, item.attachmentFile, listener); + public void bind(OperatorAttachmentItem.File item, ChatAdapter.OnFileItemClickListener listener) { + super.setData(item.isFileExists(), item.isDownloading(), item.getAttachmentFile(), listener); updateOperatorStatusView(item); } @@ -33,30 +33,30 @@ private void setupOperatorStatusView(UiTheme uiTheme) { operatorStatusView.setShowRippleAnimation(false); } - private void updateOperatorStatusView(OperatorAttachmentItem item) { - operatorStatusView.setVisibility(item.showChatHead ? View.VISIBLE : View.GONE); - if (item.operatorProfileImgUrl != null) { - operatorStatusView.showProfileImage(item.operatorProfileImgUrl); + private void updateOperatorStatusView(OperatorAttachmentItem.File item) { + operatorStatusView.setVisibility(item.getShowChatHead() ? View.VISIBLE : View.GONE); + if (item.getOperatorProfileImgUrl() != null) { + operatorStatusView.showProfileImage(item.getOperatorProfileImgUrl()); } else { operatorStatusView.showPlaceholder(); } - String name = item.attachmentFile.getName(); - String byteSize = Formatter.formatFileSize(itemView.getContext(), item.attachmentFile.getSize()); + String name = item.getAttachmentFile().getName(); + String byteSize = Formatter.formatFileSize(itemView.getContext(), item.getAttachmentFile().getSize()); itemView.setContentDescription(itemView.getResources().getString(R.string.glia_chat_operator_file_content_description, name, byteSize)); ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() { @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - String actionLabel = host.getResources().getString(item.isFileExists - ? R.string.glia_chat_attachment_open_button_label - : R.string.glia_chat_attachment_download_button_label); + String actionLabel = host.getResources().getString(item.isFileExists() + ? R.string.glia_chat_attachment_open_button_label + : R.string.glia_chat_attachment_download_button_label); AccessibilityNodeInfoCompat.AccessibilityActionCompat actionClick - = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); + = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); info.addAction(actionClick); } }); diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java index aa000f977..50dd6ab50 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java @@ -16,7 +16,7 @@ import com.glia.widgets.R; import com.glia.widgets.UiTheme; import com.glia.widgets.chat.adapter.ChatAdapter; -import com.glia.widgets.chat.model.history.VisitorAttachmentItem; +import com.glia.widgets.chat.model.VisitorAttachmentItem; public class VisitorFileAttachmentViewHolder extends FileAttachmentViewHolder { private final TextView deliveredView; @@ -27,8 +27,8 @@ public VisitorFileAttachmentViewHolder(@NonNull View itemView, UiTheme uiTheme) setupDeliveredView(itemView.getContext(), uiTheme); } - public void bind(VisitorAttachmentItem item, ChatAdapter.OnFileItemClickListener listener) { - super.setData(item.isFileExists, item.isDownloading, item.attachmentFile, listener); + public void bind(VisitorAttachmentItem.File item, ChatAdapter.OnFileItemClickListener listener) { + super.setData(item.isFileExists(), item.isDownloading(), item.getAttachmentFile(), listener); updateDeliveredView(item); } @@ -41,33 +41,37 @@ private void setupDeliveredView(Context context, UiTheme uiTheme) { } private void updateDeliveredView(VisitorAttachmentItem item) { - deliveredView.setVisibility(item.showDelivered ? View.VISIBLE : View.GONE); + deliveredView.setVisibility(item.getShowDelivered() ? View.VISIBLE : View.GONE); setAccessibilityLabels(item); } private void setAccessibilityLabels(VisitorAttachmentItem item) { - String name = item.attachmentFile.getName(); - String byteSize = Formatter.formatFileSize(itemView.getContext(), item.attachmentFile.getSize()); - itemView.setContentDescription(itemView.getResources().getString(item.showDelivered - ? R.string.glia_chat_visitor_file_delivered_content_description - : R.string.glia_chat_visitor_file_content_description, - name, byteSize)); + String name = item.getAttachmentFile().getName(); + String byteSize = Formatter.formatFileSize(itemView.getContext(), item.getAttachmentFile().getSize()); + itemView.setContentDescription(itemView.getResources().getString(item.getShowDelivered() + ? R.string.glia_chat_visitor_file_delivered_content_description + : R.string.glia_chat_visitor_file_content_description, + name, byteSize)); ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - String actionLabel = host.getResources().getString(item.isFileExists - ? R.string.glia_chat_attachment_open_button_label - : R.string.glia_chat_attachment_download_button_label); + String actionLabel = host.getResources().getString(item.isFileExists() + ? R.string.glia_chat_attachment_open_button_label + : R.string.glia_chat_attachment_download_button_label); AccessibilityNodeInfoCompat.AccessibilityActionCompat actionClick - = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); + = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); info.addAction(actionClick); } }); } + + public void updateDelivered(boolean delivered) { + deliveredView.setVisibility(delivered ? View.VISIBLE : View.GONE); + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java index 808988b07..b7ce9cb8d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java @@ -10,7 +10,7 @@ import com.glia.widgets.R; import com.glia.widgets.UiTheme; import com.glia.widgets.chat.adapter.ChatAdapter; -import com.glia.widgets.chat.model.history.OperatorAttachmentItem; +import com.glia.widgets.chat.model.OperatorAttachmentItem; import com.glia.widgets.filepreview.domain.usecase.GetImageFileFromCacheUseCase; import com.glia.widgets.filepreview.domain.usecase.GetImageFileFromDownloadsUseCase; import com.glia.widgets.filepreview.domain.usecase.GetImageFileFromNetworkUseCase; @@ -36,9 +36,9 @@ private void setupOperatorStatus(UiTheme uiTheme) { operatorStatusView.setShowRippleAnimation(false); } - public void bind(OperatorAttachmentItem item, ChatAdapter.OnImageItemClickListener onImageItemClickListener) { - super.bind(item.attachmentFile); - itemView.setOnClickListener(v -> onImageItemClickListener.onImageItemClick(item.attachmentFile, v)); + public void bind(OperatorAttachmentItem.Image item, ChatAdapter.OnImageItemClickListener onImageItemClickListener) { + super.bind(item.getAttachmentFile()); + itemView.setOnClickListener(v -> onImageItemClickListener.onImageItemClick(item.getAttachmentFile(), v)); updateOperatorStatus(item); setAccessibilityLabels(); @@ -63,9 +63,9 @@ public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull Acces } private void updateOperatorStatus(OperatorAttachmentItem item) { - operatorStatusView.setVisibility(item.showChatHead ? View.VISIBLE : View.GONE); - if (item.operatorProfileImgUrl != null) { - operatorStatusView.showProfileImage(item.operatorProfileImgUrl); + operatorStatusView.setVisibility(item.getShowChatHead() ? View.VISIBLE : View.GONE); + if (item.getOperatorProfileImgUrl() != null) { + operatorStatusView.showProfileImage(item.getOperatorProfileImgUrl()); } else { operatorStatusView.showPlaceholder(); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java index fe73bff6a..298d33c80 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java @@ -20,11 +20,11 @@ public class VisitorImageAttachmentViewHolder extends ImageAttachmentViewHolder private final TextView deliveredView; public VisitorImageAttachmentViewHolder( - @NonNull View itemView, - UiTheme uiTheme, - GetImageFileFromCacheUseCase getImageFileFromCacheUseCase, - GetImageFileFromDownloadsUseCase getImageFileFromDownloadsUseCase, - GetImageFileFromNetworkUseCase getImageFileFromNetworkUseCase + @NonNull View itemView, + UiTheme uiTheme, + GetImageFileFromCacheUseCase getImageFileFromCacheUseCase, + GetImageFileFromDownloadsUseCase getImageFileFromDownloadsUseCase, + GetImageFileFromNetworkUseCase getImageFileFromNetworkUseCase ) { super(itemView, getImageFileFromCacheUseCase, getImageFileFromDownloadsUseCase, getImageFileFromNetworkUseCase); deliveredView = itemView.findViewById(R.id.delivered_view); @@ -41,10 +41,10 @@ public void bind(AttachmentFile attachmentFile, boolean showDelivered) { private void setAccessibilityLabels(boolean showDelivered) { if (showDelivered) { itemView.setContentDescription(itemView.getResources().getString( - R.string.glia_chat_visitor_image_delivered_content_description)); + R.string.glia_chat_visitor_image_delivered_content_description)); } else { itemView.setContentDescription(itemView.getResources().getString( - R.string.glia_chat_visitor_image_content_description)); + R.string.glia_chat_visitor_image_content_description)); } } @@ -59,4 +59,8 @@ private void setupDeliveredView(Context context, UiTheme uiTheme) { } deliveredView.setTextColor(ContextCompat.getColor(context, uiTheme.getBaseNormalColor())); } + + public void updateDelivered(boolean delivered) { + deliveredView.setVisibility(delivered ? View.VISIBLE : View.GONE); + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 6af928ac8..4c1d9ab9b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -1,16 +1,10 @@ package com.glia.widgets.chat.controller import android.net.Uri -import android.text.format.DateUtils import android.view.View -import androidx.annotation.VisibleForTesting import com.glia.androidsdk.GliaException import com.glia.androidsdk.Operator import com.glia.androidsdk.chat.AttachmentFile -import com.glia.androidsdk.chat.Chat -import com.glia.androidsdk.chat.ChatMessage -import com.glia.androidsdk.chat.FilesAttachment -import com.glia.androidsdk.chat.MessageAttachment import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.chat.SingleChoiceOption import com.glia.androidsdk.chat.VisitorMessage @@ -23,41 +17,28 @@ import com.glia.androidsdk.omnicore.OmnicoreEngagement import com.glia.androidsdk.site.SiteInfo import com.glia.widgets.Constants import com.glia.widgets.GliaWidgets +import com.glia.widgets.chat.ChatManager import com.glia.widgets.chat.ChatType import com.glia.widgets.chat.ChatView import com.glia.widgets.chat.ChatViewCallback -import com.glia.widgets.chat.adapter.ChatAdapter -import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase -import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase -import com.glia.widgets.chat.domain.CustomCardShouldShowUseCase -import com.glia.widgets.chat.domain.CustomCardTypeUseCase -import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase -import com.glia.widgets.chat.domain.GliaOnMessageUseCase import com.glia.widgets.chat.domain.GliaOnOperatorTypingUseCase import com.glia.widgets.chat.domain.GliaSendMessagePreviewUseCase import com.glia.widgets.chat.domain.GliaSendMessageUseCase -import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase +import com.glia.widgets.chat.domain.IsAuthenticatedUseCase import com.glia.widgets.chat.domain.IsFromCallScreenUseCase import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase import com.glia.widgets.chat.domain.IsShowSendButtonUseCase -import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase -import com.glia.widgets.chat.model.ChatInputMode +import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase +import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.ChatState -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.CustomCardItem -import com.glia.widgets.chat.model.history.LinkedChatItem -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem -import com.glia.widgets.chat.model.history.NewMessagesItem -import com.glia.widgets.chat.model.history.OperatorAttachmentItem -import com.glia.widgets.chat.model.history.OperatorChatItem -import com.glia.widgets.chat.model.history.OperatorMessageItem -import com.glia.widgets.chat.model.history.OperatorStatusItem -import com.glia.widgets.chat.model.history.ResponseCardItem -import com.glia.widgets.chat.model.history.SystemChatItem -import com.glia.widgets.chat.model.history.VisitorAttachmentItem -import com.glia.widgets.chat.model.history.VisitorMessageItem +import com.glia.widgets.chat.model.CustomCardChatItem +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.OperatorMessageItem +import com.glia.widgets.chat.model.OperatorStatusItem +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase import com.glia.widgets.core.chathead.domain.HasPendingSurveyUseCase import com.glia.widgets.core.chathead.domain.SetPendingSurveyUsedUseCase @@ -70,8 +51,7 @@ import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase import com.glia.widgets.core.engagement.domain.IsQueueingEngagementUseCase import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase -import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse -import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.core.engagement.domain.UpdateOperatorDefaultImageUrlUseCase import com.glia.widgets.core.engagement.domain.model.EngagementStateEvent import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor.OperatorVisitor @@ -96,7 +76,6 @@ import com.glia.widgets.core.queue.domain.GliaQueueForChatEngagementUseCase import com.glia.widgets.core.queue.domain.QueueTicketStateChangeToUnstaffedUseCase import com.glia.widgets.core.queue.domain.exception.QueueingOngoingException import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase -import com.glia.widgets.core.secureconversations.domain.MarkMessagesReadWithDelayUseCase import com.glia.widgets.core.survey.OnSurveyListener import com.glia.widgets.core.survey.domain.GliaSurveyUseCase import com.glia.widgets.di.Dependencies @@ -115,7 +94,6 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import java.util.Observer -import java.util.UUID internal class ChatController( chatViewCallback: ChatViewCallback, @@ -125,11 +103,9 @@ internal class ChatController( private val dialogController: DialogController, private val messagesNotSeenHandler: MessagesNotSeenHandler, private val callNotificationUseCase: CallNotificationUseCase, - private val loadHistoryUseCase: GliaLoadHistoryUseCase, private val queueForChatEngagementUseCase: GliaQueueForChatEngagementUseCase, private val getEngagementUseCase: GliaOnEngagementUseCase, private val engagementEndUseCase: GliaOnEngagementEndUseCase, - private val onMessageUseCase: GliaOnMessageUseCase, private val onOperatorTypingUseCase: GliaOnOperatorTypingUseCase, private val sendMessagePreviewUseCase: GliaSendMessagePreviewUseCase, private val sendMessageUseCase: GliaSendMessageUseCase, @@ -145,15 +121,11 @@ internal class ChatController( private val isShowSendButtonUseCase: IsShowSendButtonUseCase, private val isShowOverlayPermissionRequestDialogUseCase: IsShowOverlayPermissionRequestDialogUseCase, private val downloadFileUseCase: DownloadFileUseCase, - private val isEnableChatEditTextUseCase: IsEnableChatEditTextUseCase, private val siteInfoUseCase: SiteInfoUseCase, private val surveyUseCase: GliaSurveyUseCase, private val getGliaEngagementStateFlowableUseCase: GetEngagementStateFlowableUseCase, private val isFromCallScreenUseCase: IsFromCallScreenUseCase, private val updateFromCallScreenUseCase: UpdateFromCallScreenUseCase, - private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase, - private val customCardTypeUseCase: CustomCardTypeUseCase, - private val customCardShouldShowUseCase: CustomCardShouldShowUseCase, private val ticketStateChangeToUnstaffedUseCase: QueueTicketStateChangeToUnstaffedUseCase, private val isQueueingEngagementUseCase: IsQueueingEngagementUseCase, private val addMediaUpgradeCallbackUseCase: AddMediaUpgradeOfferCallbackUseCase, @@ -162,21 +134,21 @@ internal class ChatController( private val isOngoingEngagementUseCase: IsOngoingEngagementUseCase, private val engagementConfigUseCase: SetEngagementConfigUseCase, private val isSecureEngagementAvailableUseCase: IsSecureConversationsChatAvailableUseCase, - private val markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase, private val hasPendingSurveyUseCase: HasPendingSurveyUseCase, private val setPendingSurveyUsedUseCase: SetPendingSurveyUsedUseCase, private val isCallVisualizerUseCase: IsCallVisualizerUseCase, - private val preEngagementMessageUseCase: PreEngagementMessageUseCase, - private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase, private val isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase, - private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase + private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase, + private val determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase, + private val isAuthenticatedUseCase: IsAuthenticatedUseCase, + private val updateOperatorDefaultImageUrlUseCase: UpdateOperatorDefaultImageUrlUseCase, + private val chatManager: ChatManager ) : GliaOnEngagementUseCase.Listener, GliaOnEngagementEndUseCase.Listener, OnSurveyListener { private var backClickedListener: ChatView.OnBackClickedListener? = null private var viewCallback: ChatViewCallback? = null private var mediaUpgradeOfferRepositoryCallback: MediaUpgradeOfferRepositoryCallback? = null private var timerStatusListener: FormattedTimerStatusListener? = null private var engagementStateEventDisposable: Disposable? = null - private var unengagementMessagesDisposable: Disposable? = null private val disposable = CompositeDisposable() private val operatorMediaStateListener = @@ -185,28 +157,22 @@ internal class ChatController( private val sendMessageCallback: GliaSendMessageUseCase.Listener = object : GliaSendMessageUseCase.Listener { override fun messageSent(message: VisitorMessage?) { - onMessageSent(message) - } - - override fun onCardMessageUpdated(message: ChatMessage) { - updateCustomCard(message) + Logger.d(TAG, "messageSent: $message, id: ${message?.id}") + message?.also { chatManager.onChatAction(ChatManager.Action.MessageSent(it)) } + scrollChatToBottom() } override fun onMessageValidated() { viewCallback?.clearMessageInput() - emitViewState { - chatState - .setLastTypedText(EMPTY_MESSAGE) - .setShowSendButton(isShowSendButtonUseCase(EMPTY_MESSAGE)) + chatState.setLastTypedText("").setShowSendButton(isShowSendButtonUseCase("")) } } - override fun errorOperatorNotOnline(message: String) { + override fun errorOperatorNotOnline(message: Unsent) { onSendMessageOperatorOffline(message) } - override fun errorMessageInvalid() {} override fun error(ex: GliaException) { onMessageSendError(ex) } @@ -224,8 +190,7 @@ internal class ChatController( viewCallback?.apply { emitUploadAttachments(getFileAttachmentsUseCase.execute()) emitViewState { - chatState - .setShowSendButton(isShowSendButtonUseCase(chatState.lastTypedText)) + chatState.setShowSendButton(isShowSendButtonUseCase(chatState.lastTypedText)) .setIsAttachmentButtonEnabled(supportedFileCountCheckUseCase.execute()) } } @@ -246,6 +211,7 @@ internal class ChatController( ) { val queueIds = if (queueId != null) arrayOf(queueId) else emptyArray() engagementConfigUseCase(chatType, queueIds) + updateOperatorDefaultImageUrlUseCase() if (!hasPendingSurveyUseCase.invoke()) { ensureSecureMessagingAvailable() @@ -254,16 +220,12 @@ internal class ChatController( if (isSecureEngagement) { emitViewState { chatState.setSecureMessagingState() } } + chatManager.onChatAction(ChatManager.Action.ChatRestored) return } - var initChatState = chatState.initChat(companyName, queueId, visitorContextAssetId) - if (isSecureEngagement) { - initChatState = initChatState.setSecureMessagingState() - } - prepareChatComponents() - emitViewState { initChatState } - loadChatHistory() + emitViewState { chatState.initChat(companyName, queueId, visitorContextAssetId) } + initChatManager() } } @@ -314,18 +276,6 @@ internal class ChatController( } } - @Synchronized - private fun emitChatItems(callback: () -> ChatState?) { - val state = callback() ?: return - - if (setState(state) && viewCallback != null) { - Logger.d(TAG, """Emit chat items: ${state.chatItems} (State): $state""".trimIndent()) - - viewCallback?.emitItems(state.chatItems) - viewCallback?.emitUploadAttachments(getFileAttachmentsUseCase.execute()) - } - } - fun onDestroy(retain: Boolean) { Logger.d(TAG, "onDestroy, retain:$retain") dialogController.dismissMessageCenterUnavailableDialog() @@ -344,10 +294,10 @@ internal class ChatController( minimizeHandler.clear() getEngagementUseCase.unregisterListener(this) engagementEndUseCase.unregisterListener(this) - onMessageUseCase.unregisterListener() onOperatorTypingUseCase.unregisterListener() removeFileAttachmentObserverUseCase.execute(fileAttachmentObserver) shouldHandleEndedEngagement = false + chatManager.reset() } } @@ -379,6 +329,7 @@ internal class ChatController( Logger.d(TAG, "Send MESSAGE: $message") clearMessagePreview() sendMessageUseCase.execute(message, sendMessageCallback) + addQuickReplyButtons(emptyList()) } private fun sendMessagePreview(message: String) { @@ -389,111 +340,7 @@ internal class ChatController( private fun clearMessagePreview() { // An empty string has to be sent to clear the message preview. - sendMessagePreview(EMPTY_MESSAGE) - } - - private fun subscribeToMessages() { - disposable.add( - onMessageUseCase.execute() - .doOnNext { onMessage(it) } - .subscribe() - ) - } - - private fun subscribeToPreEngagementMessage() { - val subscribe = preEngagementMessageUseCase.execute() - .doOnNext { onPreEngagementMessage(it) } - .subscribe() - disposable.add(subscribe) - unengagementMessagesDisposable = subscribe - } - - private fun onPreEngagementMessage(messageInternal: ChatMessageInternal) { - emitChatItems { - val message = messageInternal.chatMessage - if (message.senderType == Chat.Participant.VISITOR && message.attachment != null && - !isNewMessage(chatState.chatItems, message) - ) { - val items: MutableList = chatState.chatItems.toMutableList() - val currentMessage = - items.first { (it as? LinkedChatItem)?.messageId == message.id } - val currentMessageIndex = items.indexOf(currentMessage) - items.removeAll { (it as? VisitorAttachmentItem)?.messageId == message.id } - addVisitorAttachmentItemsToChatItems(items, message, currentMessageIndex + 1) - return@emitChatItems chatState.changeItems(items) - } else { - onMessage(messageInternal) - } - return@emitChatItems null - } - } - - private fun onMessage(messageInternal: ChatMessageInternal) { - emitChatItems { - val message = messageInternal.chatMessage - if (!isNewMessage(chatState.chatItems, message)) { - return@emitChatItems null - } - val isUnsentMessage = - chatState.unsentMessages.isNotEmpty() && chatState.unsentMessages[0].message == message.content - Logger.d( - TAG, - "onMessage: ${message.content}, id: ${message.id}, isUnsentMessage: $isUnsentMessage" - ) - if (isUnsentMessage) { - // emitting state because there is no need to change recyclerview items here - emitViewState { - val unsentMessages: MutableList = - chatState.unsentMessages.toMutableList() - val currentMessage = unsentMessages[0] - unsentMessages.remove(currentMessage) - val currentChatItems: MutableList = - chatState.chatItems.toMutableList() - val currentMessageIndex = currentChatItems.indexOf(currentMessage) - currentChatItems.remove(currentMessage) - currentChatItems.add( - currentMessageIndex, - VisitorMessageItem.asNewMessage(message) - ) - - return@emitViewState chatState.changeItems(currentChatItems) - .changeUnsentMessages(unsentMessages) - } - if (chatState.unsentMessages.isNotEmpty()) { - sendMessageUseCase.execute( - chatState.unsentMessages[0].message, - sendMessageCallback - ) - } - return@emitChatItems null - } - - val items: MutableList = chatState.chatItems.toMutableList() - appendMessageItem(items, messageInternal) - - return@emitChatItems chatState.changeItems(items) - } - } - - private fun onMessageSent(message: VisitorMessage?) { - if (message != null) { - Logger.d(TAG, "messageSent: $message, id: ${message.id}") - emitChatItems { - val currentChatItems: MutableList = chatState.chatItems.toMutableList() - if (isQueueingOrOngoingEngagement) { - changeDeliveredIndex(currentChatItems, message) - } else if (isSecureEngagementUseCase() && isNewMessage(currentChatItems, message)) { - appendSentMessage(currentChatItems, message) - } - - // chat input mode has to be set to enable after a message is sent - if (isEnableChatEditTextUseCase(currentChatItems)) { - emitViewState { chatState.chatInputModeChanged(ChatInputMode.ENABLED) } - } - - return@emitChatItems chatState.changeItems(currentChatItems) - } - } + sendMessagePreview("") } private fun onMessageSendError(exception: GliaException) { @@ -501,28 +348,17 @@ internal class ChatController( error(exception) } - private fun onSendMessageOperatorOffline(message: String) { + private fun onSendMessageOperatorOffline(message: Unsent) { appendUnsentMessage(message) if (!chatState.engagementRequested) { queueForEngagement() } } - private fun appendUnsentMessage(message: String) { + private fun appendUnsentMessage(message: Unsent) { Logger.d(TAG, "appendUnsentMessage: $message") - emitChatItems { - val unsentMessages: MutableList = - chatState.unsentMessages.toMutableList() - val unsentItem = VisitorMessageItem.asUnsentItem(message) - unsentMessages.add(unsentItem) - val currentChatItems: MutableList = chatState.chatItems.toMutableList() - currentChatItems.add(unsentItem) - emitViewState { chatState.changeUnsentMessages(unsentMessages) } - - updateQueueing(currentChatItems) - - return@emitChatItems chatState.changeItems(currentChatItems) - } + chatManager.onChatAction(ChatManager.Action.UnsentMessageReceived(message)) + scrollChatToBottom() } private fun onOperatorTyping(isOperatorTyping: Boolean) { @@ -551,7 +387,7 @@ internal class ChatController( viewCallback?.backToCall() } else { backClickedListener?.onBackClicked() - Dependencies.getControllerFactory().destroyChatController() + onDestroy(isQueueingOrOngoingEngagement || isAuthenticatedUseCase()) Dependencies.getControllerFactory().destroyCallController() } updateFromCallScreenUseCase.updateFromCallScreen(false) @@ -605,13 +441,10 @@ internal class ChatController( Logger.d(TAG, "setViewCallback") viewCallback = chatViewCallback viewCallback?.emitState(chatState) - viewCallback?.emitItems(chatState.chatItems) viewCallback?.emitUploadAttachments(getFileAttachmentsUseCase.execute()) // always start in bottom - emitViewState { - chatState.isInBottomChanged(true).changeVisibility(true) - } + emitViewState { chatState.isInBottomChanged(true).changeVisibility(true) } viewCallback?.scrollToBottomImmediate() chatState.pendingNavigationType?.also { viewCallback?.navigateToCall(it) } @@ -681,7 +514,16 @@ internal class ChatController( visitor.visit(engagementState) ) - EngagementStateEvent.Type.ENGAGEMENT_ENDED -> {} + EngagementStateEvent.Type.ENGAGEMENT_ENDED -> { + Logger.d(TAG, "Engagement Ended") + if (!isOngoingEngagementUseCase.invoke()) { + dialogController.dismissDialogs() + } + } + + EngagementStateEvent.Type.NO_ENGAGEMENT -> { + Logger.d(TAG, "NoEngagement") + } } } @@ -698,16 +540,8 @@ internal class ChatController( } private fun onTransferring() { - emitChatItems { - val items: MutableList = chatState.chatItems.toMutableList() - if (chatState.operatorStatusItem != null) { - items.remove(chatState.operatorStatusItem) - } - items.add(OperatorStatusItem.TransferringStatusItem()) - emitViewState { chatState.transferring() } - - return@emitChatItems chatState.changeItems(items) - } + emitViewState { chatState.transferring() } + chatManager.onChatAction(ChatManager.Action.Transferring) } fun overlayPermissionsDialogDismissed() { @@ -736,6 +570,10 @@ internal class ChatController( return true } + private fun error(error: Throwable?) { + error?.also { error(it.toString()) } + } + private fun error(error: String) { Logger.e(TAG, error) dialogController.showUnexpectedErrorDialog() @@ -745,16 +583,13 @@ internal class ChatController( private fun initMediaUpgradeCallback() { mediaUpgradeOfferRepositoryCallback = object : MediaUpgradeOfferRepositoryCallback { override fun newOffer(offer: MediaUpgradeOffer) { - if (isChatViewPaused) return when { + isChatViewPaused -> return offer.video == MediaDirection.NONE && offer.audio == MediaDirection.TWO_WAY -> { // audio call Logger.d(TAG, "audioUpgradeRequested") if (chatState.isOperatorOnline) { - dialogController.showUpgradeAudioDialog( - offer, - chatState.formattedOperatorName - ) + dialogController.showUpgradeAudioDialog(offer, chatState.formattedOperatorName) } } @@ -762,20 +597,14 @@ internal class ChatController( // video call Logger.d(TAG, "2 way videoUpgradeRequested") if (chatState.isOperatorOnline) { - dialogController.showUpgradeVideoDialog2Way( - offer, - chatState.formattedOperatorName - ) + dialogController.showUpgradeVideoDialog2Way(offer, chatState.formattedOperatorName) } } offer.video == MediaDirection.ONE_WAY -> { Logger.d(TAG, "1 way videoUpgradeRequested") if (chatState.isOperatorOnline) { - dialogController.showUpgradeVideoDialog1Way( - offer, - chatState.formattedOperatorName - ) + dialogController.showUpgradeVideoDialog1Way(offer, chatState.formattedOperatorName) } } } @@ -811,25 +640,14 @@ internal class ChatController( private fun viewInitQueueing() { Logger.d(TAG, "viewInitQueueing") - emitChatItems { - val items: MutableList = chatState.chatItems.toMutableList() - if (chatState.operatorStatusItem != null) { - items.remove(chatState.operatorStatusItem) - } - val operatorStatusItem = OperatorStatusItem.QueueingStatusItem(chatState.companyName) - items.add(operatorStatusItem) - emitViewState { chatState.queueingStarted(operatorStatusItem) } - - return@emitChatItems chatState.changeItems(items) - } + chatManager.onChatAction(ChatManager.Action.QueuingStarted(chatState.companyName.orEmpty())) + emitViewState { chatState.queueingStarted() } } private fun updateQueueing(items: MutableList) { - if (chatState.operatorStatusItem?.status == OperatorStatusItem.Status.IN_QUEUE) { - items.remove(chatState.operatorStatusItem) - items.add( - OperatorStatusItem.QueueingStatusItem(chatState.companyName) - ) + (chatState.operatorStatusItem as? OperatorStatusItem.InQueue)?.also { + items.remove(it) + items.add(OperatorStatusItem.InQueue(chatState.companyName)) } } @@ -845,59 +663,29 @@ internal class ChatController( } private fun operatorConnected(formattedOperatorName: String, profileImgUrl: String?) { - emitChatItems { - val items: MutableList = chatState.chatItems.toMutableList() - if (chatState.operatorStatusItem != null) { - // remove previous operator status item - val operatorStatusItemIndex = items.indexOf(chatState.operatorStatusItem) - Logger.d( - TAG, - "operatorStatusItemIndex: " + operatorStatusItemIndex + ", size: " + items.size - ) - items.remove(chatState.operatorStatusItem) - items.add( - operatorStatusItemIndex, - OperatorStatusItem.OperatorFoundStatusItem( - chatState.companyName, - formattedOperatorName, - profileImgUrl - ) - ) - } else { - items.add( - OperatorStatusItem.OperatorFoundStatusItem( - chatState.companyName, - formattedOperatorName, - profileImgUrl - ) - ) - } - emitViewState { - chatState - .operatorConnected(formattedOperatorName, profileImgUrl) - .setLiveChatState() - } - - return@emitChatItems chatState.changeItems(items) - } + chatManager.onChatAction( + ChatManager.Action.OperatorConnected( + chatState.companyName.orEmpty(), + formattedOperatorName, + profileImgUrl + ) + ) + emitViewState { chatState.operatorConnected(formattedOperatorName, profileImgUrl).setLiveChatState() } } private fun operatorChanged(formattedOperatorName: String, profileImgUrl: String?) { - emitChatItems { - val items: MutableList = chatState.chatItems.toMutableList() - val operatorStatusItem = OperatorStatusItem.OperatorJoinedStatusItem( - chatState.companyName, + chatManager.onChatAction( + ChatManager.Action.OperatorJoined( + chatState.companyName.orEmpty(), formattedOperatorName, profileImgUrl ) - items.add(operatorStatusItem) - - return@emitChatItems chatState.changeItems(items) - } + ) emitViewState { chatState.operatorConnected(formattedOperatorName, profileImgUrl) } } private fun stop() { + chatManager.reset() Logger.d(TAG, "Stop, engagement ended") disposable.add( cancelQueueTicketUseCase.execute() @@ -910,386 +698,13 @@ internal class ChatController( emitViewState { chatState.stop() } } - private fun appendHistoryChatItem( - currentChatItems: MutableList, - chatMessageInternal: ChatMessageInternal, - isLastItem: Boolean - ) { - val message = chatMessageInternal.chatMessage - when (message.senderType) { - Chat.Participant.VISITOR -> { - appendHistoryMessage(currentChatItems, message) - addVisitorAttachmentItemsToChatItems(currentChatItems, message) - } - - Chat.Participant.OPERATOR -> { - appendOperatorMessage(currentChatItems, chatMessageInternal, isLastItem) - } - - Chat.Participant.SYSTEM -> { - appendSystemMessage(currentChatItems, chatMessageInternal) - } - - Chat.Participant.UNKNOWN -> Logger.d(TAG, "Unknown type `chat item` received: $message") - } - } - - private fun appendHistoryMessage( - currentChatItems: MutableList, - message: ChatMessage - ) { - if (message.content.isNotEmpty()) { - currentChatItems.add(VisitorMessageItem.asHistoryItem(message)) - } - } - - private fun appendMessageItem( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal - ) { - val message = messageInternal.chatMessage - when (message.senderType) { - Chat.Participant.VISITOR -> { - appendSentMessage(currentChatItems, message) - addVisitorAttachmentItemsToChatItems(currentChatItems, message) - } - - Chat.Participant.OPERATOR -> { - onOperatorMessageReceived(currentChatItems, messageInternal) - } - - Chat.Participant.SYSTEM -> { - onSystemMessageReceived(currentChatItems, messageInternal) - } - - Chat.Participant.UNKNOWN -> Logger.d(TAG, "Unknown type `chat item` received: $message") - } - } - - private fun onOperatorMessageReceived( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal - ) { - appendOperatorMessage(currentChatItems, messageInternal, true) - appendMessagesNotSeen() - } - - private fun onSystemMessageReceived( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal - ) { - appendSystemMessage(currentChatItems, messageInternal) - appendMessagesNotSeen() - } - - private fun addVisitorAttachmentItemsToChatItems( - currentChatItems: MutableList, - chatMessage: ChatMessage, - index: Int? = null - ) { - val attachment = chatMessage.attachment - if (attachment is FilesAttachment) { - val visitorAttachmentItems = attachment.files.map { - VisitorAttachmentItem.fromAttachmentFile( - chatMessage.id, - chatMessage.timestamp, - it - ) - } - if (index != null) { - currentChatItems.addAll(index, visitorAttachmentItems) - } else { - currentChatItems.addAll(visitorAttachmentItems) - } - } - } - - private fun appendSentMessage(items: MutableList, message: ChatMessage) { - if (message.content.isNotEmpty()) { - items.add(VisitorMessageItem.asNewMessage(message)) - } - } - - private fun appendMessagesNotSeen() { - emitViewState { - chatState.messagesNotSeenChanged( - if (chatState.isChatInBottom) 0 else chatState.messagesNotSeen + 1 - ) - } - } - private fun initGliaEngagementObserving() { getEngagementUseCase.execute(this) engagementEndUseCase.execute(this) } - private fun changeDeliveredIndex( - currentChatItems: MutableList, - message: VisitorMessage - ) { - // "Delivered" status only applies to visitor messages - if (message.senderType != Chat.Participant.VISITOR) return - val messageId = message.id - var foundDelivered = false - for (i in currentChatItems.indices.reversed()) { - val currentChatItem = currentChatItems[i] - if (currentChatItem is VisitorMessageItem) { - val itemId = currentChatItem.id - when { - itemId == VisitorMessageItem.HISTORY_ID -> { - // we reached the history items no point in going searching further - break - } - - !foundDelivered && itemId == messageId -> { - foundDelivered = true - currentChatItems[i] = VisitorMessageItem.editDeliveredStatus( - currentChatItem, - true - ) - } - - currentChatItem.isShowDelivered -> { - currentChatItems[i] = VisitorMessageItem.editDeliveredStatus( - currentChatItem, - false - ) - } - } - } else if (currentChatItem is VisitorAttachmentItem) { - if (!foundDelivered && currentChatItem.id == messageId) { - foundDelivered = true - setDelivered(currentChatItems, i, currentChatItem, true) - } else if (currentChatItem.showDelivered) { - setDelivered(currentChatItems, i, currentChatItem, false) - } - } - } - } - - private fun setDelivered( - currentChatItems: MutableList, - i: Int, - item: VisitorAttachmentItem, - delivered: Boolean - ) { - currentChatItems[i] = VisitorAttachmentItem.editDeliveredStatus(item, delivered) - } - - private fun appendSystemMessage( - currentChatItems: MutableList, - chatMessageInternal: ChatMessageInternal - ) { - chatMessageInternal.chatMessage.apply { - currentChatItems += SystemChatItem(id, timestamp, content) - } - } - - private fun appendOperatorMessage( - currentChatItems: MutableList, - chatMessageInternal: ChatMessageInternal, - isLastItem: Boolean - ) { - setLastOperatorItemChatHeadVisibility( - currentChatItems, - isOperatorChanged(currentChatItems, chatMessageInternal) - ) - appendOperatorOrCustomCardItem(currentChatItems, chatMessageInternal, isLastItem) - appendOperatorAttachmentItems(currentChatItems, chatMessageInternal) - setLastOperatorItemChatHeadVisibility(currentChatItems, true) - } - - private fun isOperatorChanged( - currentChatItems: List, - chatMessageInternal: ChatMessageInternal - ): Boolean { - if (currentChatItems.isEmpty()) return false - val lastItem = currentChatItems.last() - if (lastItem is OperatorChatItem) { - return !chatMessageInternal - .operatorId - .filter { it == lastItem.operatorId } - .isPresent - } - return false - } - - private fun setLastOperatorItemChatHeadVisibility( - currentChatItems: MutableList, - showChatHead: Boolean - ) { - if (currentChatItems.isNotEmpty()) { - when (val lastItem = currentChatItems.last()) { - is ResponseCardItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - ResponseCardItem( - lastItem.id, - lastItem.operatorName, - lastItem.operatorProfileImgUrl, - showChatHead, - lastItem.content, - lastItem.operatorId, - lastItem.timestamp, - lastItem.singleChoiceOptions, - lastItem.choiceCardImageUrl - ) - ) - } - - is OperatorMessageItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - OperatorMessageItem( - lastItem.id, - lastItem.operatorName, - lastItem.operatorProfileImgUrl, - showChatHead, - lastItem.content, - lastItem.operatorId, - lastItem.timestamp - ) - ) - } - - is OperatorAttachmentItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - OperatorAttachmentItem( - lastItem.id, - lastItem.viewType, - showChatHead, - lastItem.attachmentFile, - lastItem.operatorProfileImgUrl, - false, - false, - lastItem.operatorId, - lastItem.messageId, - lastItem.timestamp - ) - ) - } - - is CustomCardItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - CustomCardItem( - lastItem.message, - lastItem.viewType - ) - ) - } - } - } - } - - private fun appendOperatorAttachmentItems( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal - ) { - val message = messageInternal.chatMessage - val attachment = message.attachment - if (attachment is FilesAttachment) { - val files = attachment.files - for (file in files) { - val viewType: Int = if (file.contentType.startsWith("image")) { - ChatAdapter.OPERATOR_IMAGE_VIEW_TYPE - } else { - ChatAdapter.OPERATOR_FILE_VIEW_TYPE - } - currentChatItems.add( - OperatorAttachmentItem( - message.id, - viewType, - false, - file, - messageInternal.operatorImageUrl.orElse(chatState.operatorProfileImgUrl), - false, - false, - messageInternal.operatorId.orElse(UUID.randomUUID().toString()), - message.id, - message.timestamp - ) - ) - } - } - } - - private fun appendOperatorOrCustomCardItem( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal, - isLastItem: Boolean - ) { - val message = messageInternal.chatMessage - if (message.content != EMPTY_MESSAGE) { - val viewType = customCardAdapterTypeUseCase.execute(message) - if (viewType != null) { - appendCustomCardItem(currentChatItems, message, viewType) - } else { - appendOperatorMessageItem(currentChatItems, messageInternal, isLastItem) - } - } - } - - private fun appendCustomCardItem( - currentChatItems: MutableList, - message: ChatMessage, - viewType: Int - ) { - val customCardType = customCardTypeUseCase.execute(viewType) ?: return - if (customCardShouldShowUseCase.execute(message, customCardType, true)) { - currentChatItems.add(CustomCardItem(message, viewType)) - } - val visitorCardResponseItem = VisitorMessageItem.asCardResponseItem(message) - if (!visitorCardResponseItem.message.isNullOrEmpty()) { - currentChatItems.add(visitorCardResponseItem) - } - } - - private fun appendOperatorMessageItem( - currentChatItems: MutableList, - messageInternal: ChatMessageInternal, - isLastItem: Boolean - ) { - val message = messageInternal.chatMessage - val messageAttachment = message.attachment - val singleChoiceAttachmentOptions = getSingleChoiceAttachmentOptions(messageAttachment) - val operatorName = messageInternal.operatorName.orElse(chatState.formattedOperatorName) - val operatorImage = messageInternal.operatorImageUrl.orElse(chatState.operatorProfileImgUrl) - val operatorId = messageInternal.operatorId.orElse(UUID.randomUUID().toString()) - - val item = if (singleChoiceAttachmentOptions.isNullOrEmpty() || !isLastItem) { - OperatorMessageItem( - message.id, - operatorName, - operatorImage, - false, - message.content, - operatorId, - message.timestamp - ) - } else { - ResponseCardItem( - message.id, - operatorName, - operatorImage, - false, - message.content, - operatorId, - message.timestamp, - singleChoiceAttachmentOptions, - getSingleChoiceAttachmentImgUrl(messageAttachment) - ) - } - - currentChatItems.add(item) - } - - private fun getSingleChoiceAttachmentImgUrl(attachment: MessageAttachment?): String? = - (attachment as? SingleChoiceAttachment)?.imageUrl?.orElse(null) - - private fun getSingleChoiceAttachmentOptions(attachment: MessageAttachment?): List? { - return (attachment as? SingleChoiceAttachment)?.options?.toList() + private fun addQuickReplyButtons(options: List) { + emitViewState { chatState.copy(gvaQuickReplies = options) } } private fun startTimer() { @@ -1297,148 +712,53 @@ internal class ChatController( callTimer.startNew(Constants.CALL_TIMER_DELAY, Constants.CALL_TIMER_INTERVAL_VALUE) } - private fun upgradeMediaItem() { + private fun upgradeMediaItemToVideo() { Logger.d(TAG, "upgradeMediaItem") - emitChatItems { - val newItems: MutableList = chatState.chatItems.toMutableList() - val mediaUpgradeStartedTimerItem = MediaUpgradeStartedTimerItem( - MediaUpgradeStartedTimerItem.Type.VIDEO, - chatState.mediaUpgradeStartedTimerItem.time - ) - newItems.remove(chatState.mediaUpgradeStartedTimerItem) - newItems.add(mediaUpgradeStartedTimerItem) - - return@emitChatItems chatState.changeTimerItem(newItems, mediaUpgradeStartedTimerItem) - } + emitViewState { chatState.upgradeMedia(true) } + chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeToVideo) } private fun createNewTimerCallback() { timerStatusListener?.also { callTimer.removeFormattedValueListener(it) } timerStatusListener = object : FormattedTimerStatusListener { override fun onNewFormattedTimerValue(formatedValue: String) { - emitChatItems { - if (chatState.isMediaUpgradeStarted) { - val index = - chatState.chatItems.indexOf(chatState.mediaUpgradeStartedTimerItem) - if (index != -1) { - val newItems: MutableList = - chatState.chatItems.toMutableList() - val type = chatState.mediaUpgradeStartedTimerItem.type - newItems.removeAt(index) - val mediaUpgradeStartedTimerItem = - MediaUpgradeStartedTimerItem(type, formatedValue) - newItems.add(index, mediaUpgradeStartedTimerItem) - - return@emitChatItems chatState.changeTimerItem( - newItems, - mediaUpgradeStartedTimerItem - ) - } - } - return@emitChatItems null + if (chatState.isMediaUpgradeStarted) { + chatManager.onChatAction( + ChatManager.Action.OnMediaUpgradeTimerUpdated( + formatedValue + ) + ) } } override fun onFormattedTimerCancelled() { - if (chatState.isMediaUpgradeStarted && - chatState.chatItems.contains(chatState.mediaUpgradeStartedTimerItem) - ) { - emitChatItems { - val newItems: MutableList = chatState.chatItems.toMutableList() - newItems.remove(chatState.mediaUpgradeStartedTimerItem) - - return@emitChatItems chatState.changeTimerItem(newItems, null) - } + if (chatState.isMediaUpgradeStarted) { + emitViewState { chatState.upgradeMedia(null) } + chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeCanceled) } } } } fun singleChoiceOptionClicked( - item: ResponseCardItem, + item: OperatorMessageItem.ResponseCard, selectedOption: SingleChoiceOption ) { Logger.d(TAG, "singleChoiceOptionClicked, id: ${item.id}") sendMessageUseCase.execute(selectedOption.asSingleChoiceResponse(), sendMessageCallback) - val choiceCardItemWithSelected = OperatorMessageItem( - item.id, - item.operatorName, - item.operatorProfileImgUrl, - item.showChatHead, - item.content, - item.operatorId, - item.timestamp - ) - emitChatItems { - val modifiedItems: MutableList = chatState.chatItems.toMutableList() - val indexInList = modifiedItems.indexOf(item) - modifiedItems.remove(item) - if (indexInList >= 0) { - modifiedItems.add(indexInList, choiceCardItemWithSelected) - } else { - Logger.e(TAG, "singleChoiceOptionClicked, ResponseCardItem is not in the list!") - } - - return@emitChatItems chatState.changeItems(modifiedItems) - } - } - - fun sendCustomCardResponse(messageId: String, text: String, value: String) { - emitChatItems { - chatState.chatItems - .firstOrNull { messageId == it.id } - ?.let { it as CustomCardItem } - ?.also { - sendMessageUseCase.execute(it.message, text, value, sendMessageCallback) - - val customCardType = customCardTypeUseCase.execute(it.viewType) ?: return@also - val currentMessage = it.message - val showCustomCard = customCardShouldShowUseCase.execute( - currentMessage, - customCardType, - false - ) - if (!showCustomCard) { - val chatItems: MutableList = chatState.chatItems.toMutableList() - - // If the card should be hidden after the response, we remove it from the item list. - chatItems.remove(it) - return@emitChatItems chatState.changeItems(chatItems) - } - return@emitChatItems null - } ?: run { - sendMessageUseCase.execute(null, text, value, sendMessageCallback) - } - return@emitChatItems null - } + chatManager.onChatAction(ChatManager.Action.ResponseCardClicked(item)) } - private fun updateCustomCard(message: ChatMessage) { - chatState.chatItems - .firstOrNull { message.id == it.id } - ?.let { it as CustomCardItem } - ?.also { - emitChatItems { - val chatItems: MutableList = chatState.chatItems.toMutableList() - updateCustomCardSelectedOption(it, message, chatItems) + fun sendCustomCardResponse(customCard: CustomCardChatItem, text: String, value: String) { + val attachment = SingleChoiceAttachment.from(value, text) + sendMessageUseCase.execute(attachment, sendMessageCallback) - return@emitChatItems chatState.changeItems(chatItems) - } - } + chatManager.onChatAction(ChatManager.Action.CustomCardClicked(customCard, attachment)) } - private fun updateCustomCardSelectedOption( - currentCustomCardItem: CustomCardItem, - updatedMessage: ChatMessage, - chatItems: MutableList - ) { - val updatedCustomCardItem = CustomCardItem( - updatedMessage, - currentCustomCardItem.viewType - ) - val indexInList = chatItems.indexOf(currentCustomCardItem) - chatItems.removeAt(indexInList) - chatItems.add(indexInList, updatedCustomCardItem) + private fun sendGvaResponse(singleChoiceAttachment: SingleChoiceAttachment) { + addQuickReplyButtons(emptyList()) + sendMessageUseCase.execute(singleChoiceAttachment, sendMessageCallback) } fun onRecyclerviewPositionChanged(isBottom: Boolean) { @@ -1456,139 +776,61 @@ internal class ChatController( viewCallback?.smoothScrollToBottom() } - private fun loadChatHistory() { - unengagementMessagesDisposable?.dispose() - val historyDisposable = loadHistoryUseCase() - .subscribe({ historyLoaded(it) }, { error(it) }) - disposable.add(historyDisposable) - } - - @Synchronized - private fun historyLoaded(historyResponse: ChatHistoryResponse) { - Logger.d(TAG, "historyLoaded") - val (messages, newMessagesCount) = historyResponse - val currentItems: MutableList = chatState.chatItems.toMutableList() - val newItems = removeDuplicates(currentItems, messages) - - when { - !newItems.isNullOrEmpty() -> submitHistoryItems( - newItems, - currentItems, - newMessagesCount - ) - - !chatState.engagementRequested && !isSecureEngagement -> queueForEngagement() - else -> Logger.d(TAG, "Opened empty Secure Conversations chat") - } - - initGliaEngagementObserving() - subscribeToPreEngagementMessage() - } + init { + Logger.d(TAG, "constructor") - private fun submitHistoryItems( - newItems: List, - currentItems: MutableList, - newMessagesCount: Int - ) { - newItems.forEachIndexed { index, message -> - appendHistoryChatItem(currentItems, message, index == newItems.lastIndex) - } + // viewCallback is accessed from multiple threads + // and must be protected from race condition + synchronized(this) { viewCallback = chatViewCallback } - if (isSecureEngagementUseCase() && !isQueueingOrOngoingEngagement) { - emitChatTranscriptItems(currentItems, newMessagesCount) - } else { - emitChatItems { chatState.historyLoaded(currentItems) } - } + chatState = ChatState() } - @VisibleForTesting - fun emitChatTranscriptItems( - items: MutableList, - newMessagesCount: Int - ) { - if (addNewMessagesDividerUseCase(items, newMessagesCount)) { - emitChatItems { chatState.changeItems(items) } - markMessagesReadWithDelay() - } else { - emitChatItems { chatState.changeItems(items) } - } + override fun newEngagementLoaded(engagement: OmnicoreEngagement) { + Logger.d(TAG, "newEngagementLoaded") + onOperatorTypingUseCase.execute { onOperatorTyping(it) } + addOperatorMediaStateListenerUseCase.execute(operatorMediaStateListener) + mediaUpgradeOfferRepository.startListening() + emitViewState { chatState.engagementStarted() } + chatManager.reloadHistoryIfNeeded() } - private fun markMessagesReadWithDelay() { - disposable.add( - markMessagesReadWithDelayUseCase().subscribe({ - removeNewMessagesDivider() - }, { - Logger.e(TAG, "Marking messages read failed", it) - }) - ) + private fun initChatManager() { + chatManager.initialize(::onHistoryLoaded, ::addQuickReplyButtons, ::updateUnSeenMessagesCount) + .subscribe(::emitItems, ::error) + .also(disposable::add) } - private fun removeNewMessagesDivider() { - emitChatItems { chatState.run { changeItems(chatItems - NewMessagesItem) } } + private fun updateUnSeenMessagesCount(count: Int) { + emitViewState { + val notSeenCount = chatState.messagesNotSeen + chatState.messagesNotSeenChanged(if (chatState.isChatInBottom) 0 else notSeenCount + count) + } } - init { - Logger.d(TAG, "constructor") + private fun onHistoryLoaded(hasHistory: Boolean) { + Logger.d(TAG, "historyLoaded") - // viewCallback is accessed from multiple threads - // and must be protected from race condition - synchronized(this) { viewCallback = chatViewCallback } + if (!hasHistory) { + if (!chatState.engagementRequested && !isSecureEngagement) { + queueForEngagement() + } else { + Logger.d(TAG, "Opened empty Secure Conversations chat") + } + } - chatState = ChatState.Builder() - .setFormattedOperatorName(null) - .setCompanyName(null) - .setQueueId(null) - .setVisitorContextAssetId(null) - .setIsVisible(false) - .setIntegratorChatStarted(false) - .setChatItems(ArrayList()) - .setLastTypedText(EMPTY_MESSAGE) - .setChatInputMode(ChatInputMode.ENABLED_NO_ENGAGEMENT) - .setIsAttachmentButtonNeeded(false) - .setIsAttachmentAllowed(true) - .setIsChatInBottom(true) - .setMessagesNotSeen(0) - .setPendingNavigationType(null) - .setUnsentMessages(ArrayList()) - .setIsOperatorTyping(false) - .createChatState() - } - - @VisibleForTesting - fun removeDuplicates( - oldHistory: List?, - newHistory: List? - ): List? { - return if (newHistory.isNullOrEmpty() || oldHistory.isNullOrEmpty()) { - newHistory + if (isSecureEngagement) { + emitViewState { chatState.setSecureMessagingState() } } else { - newHistory.filter { isNewMessage(oldHistory, it.chatMessage) } + emitViewState { chatState.historyLoaded() } } - } - @VisibleForTesting - fun isNewMessage(oldHistory: List?, newMessage: ChatMessage): Boolean = - oldHistory?.none { (it as? LinkedChatItem)?.messageId == newMessage.id } ?: true - - private fun error(error: Throwable?) { - error?.also { error(it.toString()) } + prepareChatComponents() + initGliaEngagementObserving() } - override fun newEngagementLoaded(engagement: OmnicoreEngagement) { - Logger.d(TAG, "newEngagementLoaded") - subscribeToMessages() - onOperatorTypingUseCase.execute { onOperatorTyping(it) } - addOperatorMediaStateListenerUseCase.execute(operatorMediaStateListener) - mediaUpgradeOfferRepository.startListening() - if (chatState.unsentMessages.isNotEmpty()) { - sendMessageUseCase.execute(chatState.unsentMessages[0].message, sendMessageCallback) - Logger.d(TAG, "unsentMessage sent!") - } - emitViewState { chatState.engagementStarted() } - // Loading chat history again on engagement start in case it was an-authenticated visitor that restored ongoing engagement - // Currently there is no direct way to know if Visitor is authenticated. - loadChatHistory() + private fun emitItems(items: List) { + viewCallback?.emitItems(items) } override fun engagementEnded() { @@ -1623,7 +865,7 @@ internal class ChatController( private fun onNewOperatorMediaState(operatorMediaState: OperatorMediaState?) { Logger.d(TAG, "newOperatorMediaState: $operatorMediaState") if (chatState.isAudioCallStarted && operatorMediaState?.video != null) { - upgradeMediaItem() + upgradeMediaItemToVideo() } else if (!chatState.isMediaUpgradeStarted) { addMediaUpgradeItemToChatItems(operatorMediaState) if (!callTimer.isRunning) { @@ -1635,25 +877,17 @@ internal class ChatController( } private fun addMediaUpgradeItemToChatItems(operatorMediaState: OperatorMediaState?) { - var type: MediaUpgradeStartedTimerItem.Type? = null - if (operatorMediaState?.video == null && operatorMediaState?.audio != null) { - Logger.d(TAG, "starting audio timer") - type = MediaUpgradeStartedTimerItem.Type.AUDIO - } else if (operatorMediaState?.video != null) { - Logger.d(TAG, "starting video timer") - type = MediaUpgradeStartedTimerItem.Type.VIDEO - } - emitChatItems { - val newItems: MutableList = chatState.chatItems.toMutableList() - val mediaUpgradeStartedTimerItem = - MediaUpgradeStartedTimerItem(type, DateUtils.formatElapsedTime(0)) - newItems.add(mediaUpgradeStartedTimerItem) + val isVideo = when { + operatorMediaState?.video == null && operatorMediaState?.audio != null -> false + operatorMediaState?.video != null -> true + else -> null + } ?: return - return@emitChatItems chatState.changeTimerItem(newItems, mediaUpgradeStartedTimerItem) - } + emitViewState { chatState.upgradeMedia(isVideo) } + chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeStarted(isVideo)) } - fun notificationsDialogDismissed() { + fun notificationDialogDismissed() { dialogController.dismissCurrentDialog() } @@ -1719,9 +953,7 @@ internal class ChatController( downloadFileUseCase(attachmentFile) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ fileDownloadSuccess(attachmentFile) }) { - fileDownloadError(attachmentFile, it) - } + .subscribe({ fileDownloadSuccess(attachmentFile) }) { fileDownloadError(attachmentFile, it) } ) } @@ -1734,24 +966,19 @@ internal class ChatController( } private fun updateAllowFileSendState() { - siteInfoUseCase.execute { siteInfo: SiteInfo?, _ -> - onSiteInfoReceived(siteInfo) - } + siteInfoUseCase.execute { siteInfo: SiteInfo?, _ -> onSiteInfoReceived(siteInfo) } } private fun onSiteInfoReceived(siteInfo: SiteInfo?) { emitViewState { - chatState.allowSendAttachmentStateChanged( - siteInfo == null || siteInfo.allowedFileSenders.isVisitorAllowed - ) + chatState.allowSendAttachmentStateChanged(siteInfo == null || siteInfo.allowedFileSenders.isVisitorAllowed) } } private fun observeQueueTicketState() { Logger.d(TAG, "observeQueueTicketState") disposable.add( - ticketStateChangeToUnstaffedUseCase - .execute() + ticketStateChangeToUnstaffedUseCase.execute() .subscribe({ dialogController.showNoMoreOperatorsAvailableDialog() }) { Logger.e(TAG, "Error happened while observing queue state : $it") } @@ -1762,7 +989,18 @@ internal class ChatController( return isCallVisualizerUseCase() } - companion object { - private const val EMPTY_MESSAGE = "" + fun onGvaButtonClicked(button: GvaButton) { + when (val buttonType: Gva.ButtonType = determineGvaButtonTypeUseCase(button)) { + Gva.ButtonType.BroadcastEvent -> viewCallback?.showBroadcastNotSupportedToast() + is Gva.ButtonType.Email -> viewCallback?.requestOpenEmailClient(buttonType.uri) + is Gva.ButtonType.Phone -> viewCallback?.requestOpenDialer(buttonType.uri) + is Gva.ButtonType.PostBack -> sendGvaResponse(buttonType.singleChoiceAttachment) + is Gva.ButtonType.Url -> viewCallback?.requestOpenUri(buttonType.uri) + } + } + + private fun scrollChatToBottom() { + emitViewState { chatState.copy(isChatInBottom = true) } + viewCallback?.smoothScrollToBottom() } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AddNewMessagesDividerUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AddNewMessagesDividerUseCase.kt index edca2c74c..fa260172a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AddNewMessagesDividerUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AddNewMessagesDividerUseCase.kt @@ -1,7 +1,7 @@ package com.glia.widgets.chat.domain -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.NewMessagesItem +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.NewMessagesDividerItem internal class AddNewMessagesDividerUseCase( private val findNewMessagesDividerIndexUseCase: FindNewMessagesDividerIndexUseCase @@ -10,7 +10,7 @@ internal class AddNewMessagesDividerUseCase( val index = findNewMessagesDividerIndexUseCase(messages, unreadMessagesCount) if (index != NOT_PROVIDED) { - messages.add(index, NewMessagesItem) + messages.add(index, NewMessagesDividerItem) return true } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt new file mode 100644 index 000000000..c4d5969b2 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt @@ -0,0 +1,177 @@ +package com.glia.widgets.chat.domain + +import androidx.annotation.VisibleForTesting +import com.glia.androidsdk.chat.FilesAttachment +import com.glia.androidsdk.chat.OperatorMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.SystemMessage +import com.glia.androidsdk.chat.VisitorMessage +import com.glia.widgets.chat.domain.gva.IsGvaUseCase +import com.glia.widgets.chat.domain.gva.MapGvaUseCase +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.CustomCardChatItem +import com.glia.widgets.chat.model.OperatorStatusItem +import com.glia.widgets.chat.model.SystemChatItem +import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.helper.Logger +import com.glia.widgets.helper.TAG +import com.glia.widgets.helper.asSingleChoice + + +internal class AppendHistoryChatMessageUseCase( + private val appendHistoryVisitorChatItemUseCase: AppendHistoryVisitorChatItemUseCase, + private val appendHistoryOperatorChatItemUseCase: AppendHistoryOperatorChatItemUseCase, + private val appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase +) { + @VisibleForTesting + var operatorId: String? = null + + @VisibleForTesting + fun resetOperatorId() { + operatorId = null + } + + @VisibleForTesting + fun shouldShowChatHead(chatMessageInternal: ChatMessageInternal): Boolean { + if (operatorId != chatMessageInternal.operatorId) { + operatorId = chatMessageInternal.operatorId + return true + } + + return false + } + + operator fun invoke(chatItems: MutableList, chatMessageInternal: ChatMessageInternal, isLatest: Boolean) { + when (val message = chatMessageInternal.chatMessage) { + is VisitorMessage -> { + resetOperatorId() + appendHistoryVisitorChatItemUseCase(chatItems, message) + } + + is OperatorMessage -> appendHistoryOperatorChatItemUseCase( + chatItems, + chatMessageInternal, + isLatest, + shouldShowChatHead(chatMessageInternal) + ) + + is SystemMessage -> { + resetOperatorId() + appendSystemMessageItemUseCase(chatItems, message) + } + + else -> Logger.d(TAG, "Unexpected type of message received -> $message") + } + } +} + +internal class AppendHistoryOperatorChatItemUseCase( + private val isGvaUseCase: IsGvaUseCase, + private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase, + private val appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase, + private val appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase, + private val appendHistoryResponseCardOrTextItemUseCase: AppendHistoryResponseCardOrTextItemUseCase +) { + operator fun invoke(chatItems: MutableList, chatMessageInternal: ChatMessageInternal, isLatest: Boolean, showChatHead: Boolean) { + val message: OperatorMessage = chatMessageInternal.chatMessage as OperatorMessage + when { + isGvaUseCase(message) -> appendGvaMessageItemUseCase(chatItems, chatMessageInternal, showChatHead) + customCardAdapterTypeUseCase(message) != null -> appendHistoryCustomCardItemUseCase( + chatItems, + message, + customCardAdapterTypeUseCase(message)!! + ) + + else -> appendHistoryResponseCardOrTextItemUseCase(chatItems, chatMessageInternal, isLatest, showChatHead) + } + } +} + +internal class AppendHistoryVisitorChatItemUseCase( + private val mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase +) { + operator fun invoke(chatItems: MutableList, message: VisitorMessage) { + message.apply { + (attachment as? FilesAttachment)?.files?.reversed()?.forEach { + chatItems += mapVisitorAttachmentUseCase(it, message) + } + + if (content.isNotBlank()) { + chatItems += VisitorMessageItem(content, id, timestamp) + } + } + + } +} + +internal class AppendSystemMessageItemUseCase { + operator fun invoke(chatItems: MutableList, message: SystemMessage) { + val index = if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1 + chatItems.add(index, message.run { SystemChatItem(content, id, timestamp) }) + } +} + +internal class AppendGvaMessageItemUseCase(private val mapGvaUseCase: MapGvaUseCase) { + operator fun invoke(chatItems: MutableList, message: ChatMessageInternal, showChatHead: Boolean = true) { + chatItems += mapGvaUseCase(message, showChatHead) + } +} + +internal class AppendHistoryCustomCardItemUseCase( + private val customCardTypeUseCase: CustomCardTypeUseCase, + private val customCardShouldShowUseCase: CustomCardShouldShowUseCase, +) { + operator fun invoke(chatItems: MutableList, message: OperatorMessage, viewType: Int) { + val customCardType = customCardTypeUseCase.execute(viewType) ?: return + if (customCardShouldShowUseCase.execute(message, customCardType, true)) { + chatItems.add(message.run { CustomCardChatItem(message, viewType) }) + } + + message.attachment?.asSingleChoice()?.selectedOptionText?.takeIf { + it.isNotBlank() + }?.let { + VisitorMessageItem(it, message.id, message.timestamp) + }?.also { + chatItems.add(it) + } + } +} + +internal class AppendHistoryResponseCardOrTextItemUseCase( + private val mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase, + private val mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase, + private val mapResponseCardUseCase: MapResponseCardUseCase +) { + operator fun invoke(chatItems: MutableList, message: ChatMessageInternal, isLatest: Boolean, showChatHead: Boolean) { + val chatMessage = message.chatMessage + chatMessage.attachment?.asSingleChoice()?.takeIf { + it.options.isNotEmpty() && isLatest + }?.let { addResponseCard(chatItems, it, message, showChatHead) } ?: addPlainTextAndAttachments(chatItems, message, showChatHead) + } + + @VisibleForTesting + fun addPlainTextAndAttachments(chatItems: MutableList, message: ChatMessageInternal, showChatHead: Boolean) { + val filesAttachment = message.chatMessage.attachment as? FilesAttachment + + filesAttachment?.files?.apply { + for (index in indices.reversed()) { + chatItems += mapOperatorAttachmentUseCase(get(index), message, showChatHead && index == lastIndex) + } + } + + if (message.chatMessage.content.isNotBlank()) { + chatItems += mapOperatorPlainTextUseCase(message, showChatHead && filesAttachment == null) + } + } + + @VisibleForTesting + fun addResponseCard( + chatItems: MutableList, + attachment: SingleChoiceAttachment, + message: ChatMessageInternal, + showChatHead: Boolean + ) { + chatItems += mapResponseCardUseCase(attachment, message, showChatHead) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt new file mode 100644 index 000000000..809122cfa --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt @@ -0,0 +1,187 @@ +package com.glia.widgets.chat.domain + +import androidx.annotation.VisibleForTesting +import com.glia.androidsdk.chat.FilesAttachment +import com.glia.androidsdk.chat.OperatorMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.SystemMessage +import com.glia.androidsdk.chat.VisitorMessage +import com.glia.widgets.chat.ChatManager +import com.glia.widgets.chat.domain.gva.IsGvaUseCase +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.OperatorChatItem +import com.glia.widgets.chat.model.VisitorChatItem +import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.helper.Logger +import com.glia.widgets.helper.TAG +import com.glia.widgets.helper.asSingleChoice + + +internal class AppendNewChatMessageUseCase( + private val appendNewOperatorMessageUseCase: AppendNewOperatorMessageUseCase, + private val appendNewVisitorMessageUseCase: AppendNewVisitorMessageUseCase, + private val appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase +) { + operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) { + when (val message = chatMessageInternal.chatMessage) { + is VisitorMessage -> { + appendNewVisitorMessageUseCase(state, chatMessageInternal) + state.resetOperator() + } + + is OperatorMessage -> appendNewOperatorMessageUseCase(state, chatMessageInternal) + + is SystemMessage -> { + appendSystemMessageItemUseCase(state.chatItems, message) + state.resetOperator() + } + + else -> Logger.d(TAG, "Unexpected type of message received -> $message") + } + } +} + +internal class AppendNewOperatorMessageUseCase( + private val isGvaUseCase: IsGvaUseCase, + private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase, + private val appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase, + private val appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase, + private val appendNewResponseCardOrTextItemUseCase: AppendNewResponseCardOrTextItemUseCase +) { + operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) { + val itemsCount = state.chatItems.count() + val message: OperatorMessage = chatMessageInternal.chatMessage as OperatorMessage + when { + isGvaUseCase(message) -> appendGvaMessageItemUseCase( + state.chatItems, + chatMessageInternal + ) + + customCardAdapterTypeUseCase(message) != null -> appendHistoryCustomCardItemUseCase( + state.chatItems, + message, + customCardAdapterTypeUseCase(message)!! + ) + + else -> appendNewResponseCardOrTextItemUseCase(state.chatItems, chatMessageInternal) + } + + state.apply { addedMessagesCount = chatItems.count() - itemsCount } + + + val lastMessageWithVisibleOperatorImage = state.lastMessageWithVisibleOperatorImage + + + val lastItem = state.chatItems.lastOrNull() + + if (lastItem !is OperatorChatItem) { + state.resetOperator() + return + } + + if (state.isOperatorChanged(lastItem) || lastMessageWithVisibleOperatorImage == null) { + return + } + + val index = state.chatItems.indexOf(lastMessageWithVisibleOperatorImage) + if (index == -1) return + state.chatItems[index] = lastMessageWithVisibleOperatorImage.withShowChatHead(false) + } +} + +internal class AppendNewResponseCardOrTextItemUseCase( + private val mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase, + private val mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase, + private val mapResponseCardUseCase: MapResponseCardUseCase +) { + operator fun invoke(chatItems: MutableList, message: ChatMessageInternal) { + val chatMessage = message.chatMessage + chatMessage.attachment?.asSingleChoice()?.takeIf { + it.options.isNotEmpty() + }?.let { addResponseCard(chatItems, it, message) } ?: addPlainTextAndAttachments(chatItems, message) + } + + @VisibleForTesting + fun addPlainTextAndAttachments(chatItems: MutableList, message: ChatMessageInternal) { + val filesAttachment = message.chatMessage.attachment as? FilesAttachment + + if (message.chatMessage.content.isNotBlank()) { + chatItems += mapOperatorPlainTextUseCase(message, filesAttachment?.files.isNullOrEmpty()) + } + + filesAttachment?.files?.apply { + for (index in indices) { + chatItems += mapOperatorAttachmentUseCase(get(index), message, index == lastIndex) + } + } + } + + @VisibleForTesting + fun addResponseCard( + chatItems: MutableList, + attachment: SingleChoiceAttachment, + message: ChatMessageInternal + ) { + chatItems += mapResponseCardUseCase(attachment, message, true) + } +} + +internal class AppendNewVisitorMessageUseCase( + private val mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase +) { + + @VisibleForTesting + var lastDeliveredItem: VisitorChatItem? = null + + @VisibleForTesting + fun addUnsentItem(state: ChatManager.State, message: VisitorMessage): Boolean { + if (state.unsentItems.isEmpty()) return false + + val unsentMessage = state.unsentItems.firstOrNull { it.content == message.content } ?: return false + state.unsentItems.remove(unsentMessage) + + val index = state.chatItems.indexOf(unsentMessage.chatMessage) + if (index != -1) { + if (lastDeliveredItem != null) { + val lastDeliveredIndex = state.chatItems.indexOf(lastDeliveredItem!!) + state.chatItems[lastDeliveredIndex] = lastDeliveredItem!!.withDeliveredStatus(false) + } + + state.chatItems[index] = message.run { + lastDeliveredItem = VisitorMessageItem(content, id, timestamp, true) + lastDeliveredItem!! + } + + return true + } + + return false + } + + operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) { + val message = chatMessageInternal.chatMessage as VisitorMessage + + if (!addUnsentItem(state, message)) { + message.apply { + val files = (attachment as? FilesAttachment)?.files + val hasFiles = !files.isNullOrEmpty() + + if (content.isNotBlank()) { + state.chatItems += VisitorMessageItem(content, id, timestamp, !hasFiles) + } + + files?.forEachIndexed { index, attachmentFile -> + state.chatItems += mapVisitorAttachmentUseCase(attachmentFile, message, index == files.lastIndex) + } + + if (lastDeliveredItem != null) { + val index = state.chatItems.indexOf(lastDeliveredItem!!) + state.chatItems[index] = lastDeliveredItem!!.withDeliveredStatus(false) + } + + lastDeliveredItem = state.chatItems.last() as? VisitorChatItem + } + } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java deleted file mode 100644 index 18d3afe2a..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.glia.widgets.chat.domain; - -import androidx.annotation.Nullable; - -import com.glia.androidsdk.chat.ChatMessage; -import com.glia.widgets.chat.adapter.CustomCardAdapter; - -public class CustomCardAdapterTypeUseCase { - @Nullable - private final CustomCardAdapter adapter; - - public CustomCardAdapterTypeUseCase(@Nullable CustomCardAdapter adapter) { - this.adapter = adapter; - } - - @Nullable - public Integer execute(ChatMessage message) { - if (adapter == null || message.getMetadata() == null || message.getMetadata().length() == 0) { - return null; - } - return adapter.getChatAdapterViewType(message); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt new file mode 100644 index 000000000..196bd6c42 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt @@ -0,0 +1,11 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.chat.adapter.CustomCardAdapter + +class CustomCardAdapterTypeUseCase(private val adapter: CustomCardAdapter?) { + operator fun invoke(message: ChatMessage): Int? = when { + adapter == null || message.metadata == null || message.metadata!!.length() == 0 -> null + else -> adapter.getChatAdapterViewType(message) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/FindNewMessagesDividerIndexUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/FindNewMessagesDividerIndexUseCase.kt index d7be5c087..78bc8c225 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/FindNewMessagesDividerIndexUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/FindNewMessagesDividerIndexUseCase.kt @@ -1,9 +1,10 @@ package com.glia.widgets.chat.domain -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.ServerChatItem +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.ServerChatItem internal const val NOT_PROVIDED = -1 + internal class FindNewMessagesDividerIndexUseCase { /** diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt index bb576679c..20c9c755f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt @@ -3,6 +3,7 @@ package com.glia.widgets.chat.domain import com.glia.widgets.chat.data.GliaChatRepository import com.glia.widgets.core.engagement.domain.MapOperatorUseCase import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.domain.GetUnreadMessagesCountWithTimeoutUseCase import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase @@ -30,9 +31,9 @@ internal class GliaLoadHistoryUseCase( getUnreadMessagesCountUseCase() ) { messages, count -> ChatHistoryResponse(messages, count) } - private fun loadHistoryAndMapOperator() = loadHistory() + private fun loadHistoryAndMapOperator(): Single> = loadHistory() .flatMapPublisher { Flowable.fromArray(*it) } - .concatMapSingle { mapOperatorUseCase(it) } + .concatMapSingle { mapOperatorUseCase(chatMessage = it) } .toSortedList(Comparator.comparingLong { it.chatMessage.timestamp }) private fun loadHistory() = Single.create { emitter -> diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java deleted file mode 100644 index 2419c8eee..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.glia.widgets.chat.domain; - -import com.glia.androidsdk.chat.ChatMessage; -import com.glia.androidsdk.omnicore.OmnicoreEngagement; -import com.glia.widgets.chat.data.GliaChatRepository; -import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase; -import com.glia.widgets.core.engagement.domain.MapOperatorUseCase; -import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; - -public class GliaOnMessageUseCase implements - GliaOnEngagementUseCase.Listener, - GliaChatRepository.MessageListener { - - private final GliaOnEngagementUseCase onEngagementUseCase; - private final GliaChatRepository messageRepository; - private final MapOperatorUseCase mapOperatorUseCase; - private final PublishSubject publishSubject; - - public GliaOnMessageUseCase( - GliaChatRepository messageRepository, - GliaOnEngagementUseCase gliaOnEngagementUseCase, - MapOperatorUseCase mapOperatorUseCase) { - this.onEngagementUseCase = gliaOnEngagementUseCase; - this.messageRepository = messageRepository; - this.mapOperatorUseCase = mapOperatorUseCase; - publishSubject = PublishSubject.create(); - } - - public Observable execute() { - this.onEngagementUseCase.execute(this); - return publishSubject - .flatMapSingle(mapOperatorUseCase::invoke) - .doOnError(Throwable::printStackTrace) - .share(); - } - - public void unregisterListener() { - messageRepository.unregisterEngagementMessageListener(this); - onEngagementUseCase.unregisterListener(this); - } - - @Override - public void newEngagementLoaded(OmnicoreEngagement engagement) { - messageRepository.listenForEngagementMessages(this, engagement); - } - - @Override - public void onMessage(ChatMessage chatMessage) { - publishSubject.onNext(chatMessage); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt new file mode 100644 index 000000000..e85ee7c28 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt @@ -0,0 +1,27 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.core.engagement.domain.MapOperatorUseCase +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import io.reactivex.Observable +import java.util.function.Consumer + +internal class GliaOnMessageUseCase( + private val messageRepository: GliaChatRepository, + private val mapOperatorUseCase: MapOperatorUseCase +) { + + private val observable = Observable.create { observer -> + val messageListener = Consumer { observer.onNext(it) } + + messageRepository.listenForAllMessages(messageListener) + + observer.setCancellable { messageRepository.unregisterAllMessageListener(messageListener) } + } + .flatMapSingle { mapOperatorUseCase(it) } + .doOnError { it.printStackTrace() } + .share() + + operator fun invoke(): Observable = observable +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt index 45a3e551e..a5c1f8b5f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt @@ -1,12 +1,11 @@ package com.glia.widgets.chat.domain import com.glia.androidsdk.GliaException -import com.glia.androidsdk.chat.ChatMessage import com.glia.androidsdk.chat.FilesAttachment -import com.glia.androidsdk.chat.OperatorMessage import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.chat.VisitorMessage import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.engagement.GliaEngagementConfigRepository import com.glia.widgets.core.engagement.GliaEngagementStateRepository import com.glia.widgets.core.fileupload.FileAttachmentRepository @@ -14,7 +13,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase -class GliaSendMessageUseCase( +internal class GliaSendMessageUseCase( private val chatRepository: GliaChatRepository, private val fileAttachmentRepository: FileAttachmentRepository, private val engagementStateRepository: GliaEngagementStateRepository, @@ -24,11 +23,13 @@ class GliaSendMessageUseCase( ) { interface Listener { fun messageSent(message: VisitorMessage?) - fun onCardMessageUpdated(message: ChatMessage) fun onMessageValidated() - fun errorOperatorNotOnline(message: String) - fun errorMessageInvalid() + fun errorOperatorNotOnline(message: Unsent) fun error(ex: GliaException) + + fun errorMessageInvalid() { + // Currently, no need for this method, but have to keep it because it describes case in else branch + } } private val isSecureEngagement: Boolean @@ -79,44 +80,22 @@ class GliaSendMessageUseCase( sendMessage(message, listener) } } else { - listener.errorOperatorNotOnline(message) + listener.errorOperatorNotOnline(Unsent.Message(message)) } } else { listener.errorMessageInvalid() } } - fun execute(singleChoiceAttachment: SingleChoiceAttachment?, listener: Listener?) { - chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) - } + fun execute(singleChoiceAttachment: SingleChoiceAttachment, listener: Listener) { + when { + isSecureEngagement -> singleChoiceAttachment.apply { + secureConversationsRepository.send(selectedOptionText, engagementConfigRepository.queueIds, singleChoiceAttachment, listener) + } - fun execute(chatMessage: ChatMessage?, text: String, value: String, listener: Listener?) { - val attachment = SingleChoiceAttachment.from(value, text) - chatRepository.sendResponse(attachment) { result: VisitorMessage?, ex: GliaException? -> - listener?.let { - if (ex != null) { - listener.error(ex) - } - if (result != null) { - listener.messageSent(result) + isOperatorOnline -> chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) - chatMessage?.let { - it as? OperatorMessage - }?.let { - listener.onCardMessageUpdated( - ChatMessage( - it.id, - it.content, - it.timestamp, - ChatMessage.Sender(it.senderType, it.operatorHref, it.operatorId), - it.deliveredAt, - attachment, - it.metadata - ) - ) - } - } - } + else -> listener.errorOperatorNotOnline(Unsent.Attachment(singleChoiceAttachment)) } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt new file mode 100644 index 000000000..6bd9bd7bc --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt @@ -0,0 +1,48 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.androidsdk.chat.OperatorMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.widgets.chat.ChatManager +import com.glia.widgets.chat.model.CustomCardChatItem + +internal class HandleCustomCardClickUseCase( + private val customCardTypeUseCase: CustomCardTypeUseCase, + private val customCardShouldShowUseCase: CustomCardShouldShowUseCase +) { + operator fun invoke( + customCard: CustomCardChatItem, + attachment: SingleChoiceAttachment, + state: ChatManager.State + ): ChatManager.State { + val customCardType = customCardTypeUseCase.execute(customCard.viewType) ?: return state + val currentMessage = customCard.message + val showCustomCard = customCardShouldShowUseCase.execute( + currentMessage, + customCardType, + false + ) + if (!showCustomCard) { + state.chatItems.remove(customCard) + } else { + val index = state.chatItems.indexOf(customCard) + val message = (currentMessage as? OperatorMessage) + if (index != -1 && message != null) { + val newMessage = message.let { + ChatMessage( + it.id, + it.content, + it.timestamp, + ChatMessage.Sender(it.senderType, it.operatorHref, it.operatorId), + it.deliveredAt, + attachment, + it.metadata + ) + } + + state.chatItems[index] = CustomCardChatItem(newMessage, customCard.viewType) + } + } + return state + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt deleted file mode 100644 index 4373a4737..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.glia.widgets.chat.domain - -import com.glia.widgets.chat.adapter.ChatAdapter -import com.glia.widgets.chat.model.history.ChatItem -import com.glia.widgets.chat.model.history.ResponseCardItem - -class IsEnableChatEditTextUseCase { - - // should enable only if there is no unselected choice-card last - operator fun invoke(items: List?): Boolean = items?.lastOrNull()?.let { - it.viewType != ChatAdapter.OPERATOR_MESSAGE_VIEW_TYPE || it !is ResponseCardItem - } ?: true -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt index 4004c7283..159c813d8 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt @@ -17,7 +17,7 @@ class IsShowSendButtonUseCase( } private fun hasText(message: String?): Boolean { - return message != null && message.isNotEmpty() + return !message.isNullOrEmpty() } private fun hadReadyToSendUnsentAttachments(): Boolean { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt new file mode 100644 index 000000000..6d2082835 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt @@ -0,0 +1,77 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.AttachmentFile +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.VisitorMessage +import com.glia.widgets.chat.model.OperatorAttachmentItem +import com.glia.widgets.chat.model.OperatorMessageItem +import com.glia.widgets.chat.model.VisitorAttachmentItem +import com.glia.widgets.chat.model.VisitorChatItem +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.helper.isImage +import kotlin.jvm.optionals.getOrNull + +internal class MapOperatorAttachmentUseCase { + operator fun invoke(attachment: AttachmentFile, chatMessageInternal: ChatMessageInternal, showChatHead: Boolean) = chatMessageInternal.run { + if (attachment.isImage) { + OperatorAttachmentItem.Image( + attachmentFile = attachment, + id = chatMessage.id, + timestamp = chatMessage.timestamp, + showChatHead = showChatHead, + operatorProfileImgUrl = operatorImageUrl, + operatorId = operatorId + ) + } else { + OperatorAttachmentItem.File( + attachmentFile = attachment, + id = chatMessage.id, + timestamp = chatMessage.timestamp, + showChatHead = showChatHead, + operatorProfileImgUrl = operatorImageUrl, + operatorId = operatorId + ) + } + } +} + +internal class MapVisitorAttachmentUseCase { + operator fun invoke(attachmentFile: AttachmentFile, message: VisitorMessage, showDelivered: Boolean = false): VisitorChatItem = message.run { + if (attachmentFile.isImage) { + VisitorAttachmentItem.Image(id, timestamp, attachmentFile, showDelivered = showDelivered) + } else { + VisitorAttachmentItem.File(id, timestamp, attachmentFile, showDelivered = showDelivered) + } + } +} + +internal class MapOperatorPlainTextUseCase { + operator fun invoke(chatMessageInternal: ChatMessageInternal, showChatHead: Boolean): OperatorMessageItem = chatMessageInternal.run { + OperatorMessageItem.PlainText( + chatMessage.id, + chatMessage.timestamp, + showChatHead, + operatorImageUrl, + operatorId, + operatorName, + chatMessage.content + ) + } +} + +internal class MapResponseCardUseCase { + operator fun invoke(attachment: SingleChoiceAttachment, message: ChatMessageInternal, showChatHead: Boolean): OperatorMessageItem.ResponseCard = + message.run { + OperatorMessageItem.ResponseCard( + chatMessage.id, + chatMessage.timestamp, + showChatHead, + operatorImageUrl, + operatorId, + operatorName, + chatMessage.content, + attachment.options.asList(), + attachment.imageUrl.getOrNull() + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt deleted file mode 100644 index add4d49d6..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.glia.widgets.chat.domain - -import com.glia.androidsdk.chat.ChatMessage -import com.glia.widgets.chat.data.GliaChatRepository -import com.glia.widgets.core.engagement.GliaEngagementRepository -import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase -import com.glia.widgets.core.engagement.domain.MapOperatorUseCase -import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal -import io.reactivex.Observable -import java.util.function.Consumer - -internal class PreEngagementMessageUseCase( - private val messageRepository: GliaChatRepository, - private val engagementRepository: GliaEngagementRepository, - private val onEngagementUseCase: GliaOnEngagementUseCase, - private val mapOperatorUseCase: MapOperatorUseCase -) { - - fun execute(): Observable { - if (engagementRepository.hasOngoingEngagement()) { - return Observable.empty() - } - return Observable.create { observer -> - val messageListener = Consumer { chatMessage -> - observer.onNext(chatMessage) - } - - val engagementListener = GliaOnEngagementUseCase.Listener { - observer.onComplete() - } - - messageRepository.listenForAllMessages(messageListener) - onEngagementUseCase.execute(engagementListener) - - observer.setCancellable { - messageRepository.unregisterAllMessageListener(messageListener) - onEngagementUseCase.unregisterListener(engagementListener) - } - } - .flatMapSingle { chatMessage: ChatMessage -> mapOperatorUseCase(chatMessage) } - .doOnError { obj: Throwable -> obj.printStackTrace() } - .share() - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt new file mode 100644 index 000000000..ad159c6f6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt @@ -0,0 +1,32 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.VisitorMessage +import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.chat.model.Unsent + + +internal class SendUnsentMessagesUseCase(private val chatRepository: GliaChatRepository) { + operator fun invoke(message: Unsent, onSuccess: (VisitorMessage) -> Unit) { + when (message) { + is Unsent.Attachment -> sendAttachment(message.attachment, onSuccess) + is Unsent.Message -> sendMessage(message.message, onSuccess) + } + } + + private fun sendAttachment(attachment: SingleChoiceAttachment, onSuccess: (VisitorMessage) -> Unit) { + chatRepository.sendResponse(attachment) { response, exception -> + if (exception == null && response != null) { + onSuccess(response) + } + } + } + + private fun sendMessage(message: String, onSuccess: (VisitorMessage) -> Unit) { + chatRepository.sendMessage(message) { response, exception -> + if (exception == null && response != null) { + onSuccess(response) + } + } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt new file mode 100644 index 000000000..5807161c6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt @@ -0,0 +1,16 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton + +internal const val PHONE_SCHEME = "tel" +internal const val EMAIL_SCHEME = "mailto" + +internal class DetermineGvaButtonTypeUseCase(private val determineGvaUrlTypeUseCase: DetermineGvaUrlTypeUseCase) { + + operator fun invoke(button: GvaButton): Gva.ButtonType = when { + !button.destinationPbBroadcastEvent.isNullOrBlank() -> Gva.ButtonType.BroadcastEvent + button.url.isNullOrBlank() -> Gva.ButtonType.PostBack(button.toResponse()) + else -> determineGvaUrlTypeUseCase(button.url) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt new file mode 100644 index 000000000..28da2474f --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt @@ -0,0 +1,16 @@ +package com.glia.widgets.chat.domain.gva + +import android.net.Uri +import com.glia.widgets.chat.model.Gva + +internal class DetermineGvaUrlTypeUseCase { + + operator fun invoke(url: String): Gva.ButtonType { + val uri = Uri.parse(url) + return when (uri.scheme) { + PHONE_SCHEME -> Gva.ButtonType.Phone(uri) + EMAIL_SCHEME -> Gva.ButtonType.Email(uri) + else -> Gva.ButtonType.Url(uri) + } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt new file mode 100644 index 000000000..d6705f91f --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt @@ -0,0 +1,10 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import org.json.JSONObject + +internal class GetGvaTypeUseCase { + + operator fun invoke(metadata: JSONObject): Gva.Type? = Gva.Type.values().firstOrNull { it.value == metadata.optString(Gva.Keys.TYPE) } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt new file mode 100644 index 000000000..7b83cb927 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt @@ -0,0 +1,12 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.androidsdk.chat.ChatMessage + +internal class IsGvaUseCase( + private val getGvaTypeUseCase: GetGvaTypeUseCase +) { + + operator fun invoke(chatMessage: ChatMessage): Boolean = chatMessage.metadata?.let { + getGvaTypeUseCase(it) + } != null +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt new file mode 100644 index 000000000..d7393d2a1 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt @@ -0,0 +1,23 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaGvaGalleryCardsUseCase( + private val parseGvaGalleryCardsUseCase: ParseGvaGalleryCardsUseCase +) { + operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaGalleryCards { + val message = chatMessage.chatMessage + val metadata = message.metadata + + return GvaGalleryCards( + id = message.id, + galleryCards = parseGvaGalleryCardsUseCase(metadata), + showChatHead = showChatHead, + operatorId = chatMessage.operatorId, + timestamp = message.timestamp, + operatorProfileImgUrl = chatMessage.operatorImageUrl, + operatorName = chatMessage.operatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt new file mode 100644 index 000000000..7420574e3 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt @@ -0,0 +1,25 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaPersistentButtons +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaPersistentButtonsUseCase( + private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase +) { + operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaPersistentButtons { + val message = chatMessage.chatMessage + val metadata = message.metadata + + return GvaPersistentButtons( + id = message.id, + content = metadata?.optString(Gva.Keys.CONTENT).orEmpty(), + options = parseGvaButtonsUseCase(metadata), + showChatHead = showChatHead, + operatorId = chatMessage.operatorId, + timestamp = message.timestamp, + operatorProfileImgUrl = chatMessage.operatorImageUrl, + operatorName = chatMessage.operatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt new file mode 100644 index 000000000..3fb4deb5e --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt @@ -0,0 +1,21 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaQuickReplies +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaQuickRepliesUseCase(private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase) { + operator fun invoke(message: ChatMessageInternal, showChatHead: Boolean): GvaQuickReplies = + message.run { + GvaQuickReplies( + id = chatMessage.id, + content = chatMessage.metadata?.optString(Gva.Keys.CONTENT).orEmpty(), + showChatHead = showChatHead, + operatorId = operatorId, + timestamp = chatMessage.timestamp, + operatorProfileImgUrl = operatorImageUrl, + operatorName = operatorName, + options = parseGvaButtonsUseCase(chatMessage.metadata) + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt new file mode 100644 index 000000000..026e56662 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt @@ -0,0 +1,21 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaResponseText +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaResponseTextUseCase { + operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaResponseText { + val message = chatMessage.chatMessage + + return GvaResponseText( + id = message.id, + content = message.metadata?.optString(Gva.Keys.CONTENT).orEmpty(), + showChatHead = showChatHead, + operatorId = chatMessage.operatorId, + timestamp = message.timestamp, + operatorProfileImgUrl = chatMessage.operatorImageUrl, + operatorName = chatMessage.operatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt new file mode 100644 index 000000000..c3686039c --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt @@ -0,0 +1,22 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.OperatorChatItem +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaUseCase( + private val getGvaTypeUseCase: GetGvaTypeUseCase, + private val mapGvaResponseTextUseCase: MapGvaResponseTextUseCase, + private val mapGvaPersistentButtonsUseCase: MapGvaPersistentButtonsUseCase, + private val mapGvaQuickRepliesUseCase: MapGvaQuickRepliesUseCase, + private val mapGvaGvaGalleryCardsUseCase: MapGvaGvaGalleryCardsUseCase +) { + operator fun invoke(chatMessageInternal: ChatMessageInternal, showChatHead: Boolean = true): OperatorChatItem = + when (getGvaTypeUseCase(chatMessageInternal.chatMessage.metadata!!)) { + Gva.Type.PLAIN_TEXT -> mapGvaResponseTextUseCase(chatMessageInternal, showChatHead) + Gva.Type.PERSISTENT_BUTTONS -> mapGvaPersistentButtonsUseCase(chatMessageInternal, showChatHead) + Gva.Type.QUICK_REPLIES -> mapGvaQuickRepliesUseCase(chatMessageInternal, showChatHead) + Gva.Type.GALLERY_CARDS -> mapGvaGvaGalleryCardsUseCase(chatMessageInternal, showChatHead) + else -> throw IllegalArgumentException("metadata should contain on of the [${Gva.Type.values().joinToString { it.value }}] types") + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt new file mode 100644 index 000000000..3fa7327af --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.json.JSONObject + +internal class ParseGvaButtonsUseCase(private val gson: Gson) { + operator fun invoke(metadata: JSONObject?): List = metadata?.optString(Gva.Keys.OPTIONS)?.let { + gson.fromJson(it, object : TypeToken>() {}.type) + } ?: emptyList() +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt new file mode 100644 index 000000000..82a680874 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaGalleryCard +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.json.JSONObject + +internal class ParseGvaGalleryCardsUseCase(private val gson: Gson) { + operator fun invoke(metadata: JSONObject?): List = metadata?.optString(Gva.Keys.GALLERY_CARDS)?.let { + gson.fromJson(it, object : TypeToken>() {}.type) + } ?: emptyList() +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt new file mode 100644 index 000000000..b87b21a87 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt @@ -0,0 +1,253 @@ +package com.glia.widgets.chat.model + +import android.content.Context +import android.text.format.DateUtils +import com.glia.androidsdk.chat.AttachmentFile +import com.glia.androidsdk.chat.ChatMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.SingleChoiceOption +import com.glia.widgets.chat.adapter.ChatAdapter +import com.glia.widgets.helper.isDownloaded +import java.util.UUID + +internal abstract class ChatItem(@ChatAdapter.Type val viewType: Int) { + abstract val id: String + abstract val timestamp: Long + + open fun areContentsTheSame(newItem: ChatItem): Boolean = this == newItem +} + +/** + * This is the same as [ChatItem], but should be used for non-local chat items + */ +internal abstract class ServerChatItem(@ChatAdapter.Type viewType: Int) : ChatItem(viewType) + +internal abstract class OperatorChatItem(@ChatAdapter.Type viewType: Int) : ServerChatItem(viewType) { + abstract val showChatHead: Boolean + abstract val operatorProfileImgUrl: String? + abstract val operatorId: String? + + abstract fun withShowChatHead(showChatHead: Boolean): OperatorChatItem +} + +internal interface AttachmentItem { + val attachmentFile: AttachmentFile + val isFileExists: Boolean + val isDownloading: Boolean + + val attachmentId: String get() = attachmentFile.id + + fun isDownloaded(context: Context): Boolean = attachmentFile.isDownloaded(context) + + fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem +} + +internal data class CustomCardChatItem( + val message: ChatMessage, private val customCardViewType: Int +) : ServerChatItem(customCardViewType) { + override val id: String = message.id + override val timestamp: Long = message.timestamp +} + +internal class SystemChatItem( + val message: String, + override val id: String, + override val timestamp: Long +) : ServerChatItem(ChatAdapter.SYSTEM_MESSAGE_TYPE) + +internal sealed class OperatorAttachmentItem(@ChatAdapter.Type viewType: Int) : OperatorChatItem(viewType), AttachmentItem { + + data class Image( + override val isFileExists: Boolean = false, + override val isDownloading: Boolean = false, + override val attachmentFile: AttachmentFile, + override val id: String, + override val timestamp: Long, + override val showChatHead: Boolean = false, + override val operatorProfileImgUrl: String? = null, + override val operatorId: String? = null + ) : OperatorAttachmentItem(ChatAdapter.OPERATOR_IMAGE_VIEW_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) + + override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem = + copy(isFileExists = isFileExists, isDownloading = isDownloading) + } + + data class File( + override val isFileExists: Boolean = false, + override val isDownloading: Boolean = false, + override val attachmentFile: AttachmentFile, + override val id: String, + override val timestamp: Long, + override val showChatHead: Boolean = false, + override val operatorProfileImgUrl: String? = null, + override val operatorId: String? = null + ) : OperatorAttachmentItem(ChatAdapter.OPERATOR_FILE_VIEW_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) + override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem = + copy(isFileExists = isFileExists, isDownloading = isDownloading) + } +} + +internal sealed class OperatorMessageItem : OperatorChatItem(ChatAdapter.OPERATOR_MESSAGE_VIEW_TYPE) { + abstract val operatorName: String? + abstract val content: String? + + data class PlainText( + override val id: String, + override val timestamp: Long, + override val showChatHead: Boolean, + override val operatorProfileImgUrl: String?, + override val operatorId: String?, + override val operatorName: String?, + override val content: String? + ) : OperatorMessageItem() { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) + + fun asPlainText(): PlainText = PlainText( + id = id, + timestamp = timestamp, + showChatHead = showChatHead, + operatorProfileImgUrl = operatorProfileImgUrl, + operatorId = operatorId, + operatorName = operatorName, + content = content + ) + } + + data class ResponseCard( + override val id: String, + override val timestamp: Long, + override val showChatHead: Boolean, + override val operatorProfileImgUrl: String?, + override val operatorId: String?, + override val operatorName: String?, + override val content: String?, + val singleChoiceOptions: List, + val choiceCardImageUrl: String? + ) : OperatorMessageItem() { + + init { + require(singleChoiceOptions.isNotEmpty()) { "Response card should have at least one `SingleChoiceOption`" } + } + + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) + + fun asPlainText() = PlainText( + id = id, + timestamp = timestamp, + showChatHead = showChatHead, + operatorProfileImgUrl = operatorProfileImgUrl, + operatorId = operatorId, + operatorName = operatorName, + content = content + ) + } +} + +// Local + +internal sealed class MediaUpgradeStartedTimerItem : ChatItem(ChatAdapter.MEDIA_UPGRADE_ITEM_TYPE) { + override val id: String = "media_upgrade_item" + override val timestamp: Long = -1 + abstract val time: String + + abstract fun updateTime(time: String): MediaUpgradeStartedTimerItem + + data class Audio(override val time: String = DateUtils.formatElapsedTime(0)) : MediaUpgradeStartedTimerItem() { + override fun updateTime(time: String) = copy(time = time) + } + + data class Video(override val time: String = DateUtils.formatElapsedTime(0)) : MediaUpgradeStartedTimerItem() { + override fun updateTime(time: String) = copy(time = time) + } +} + +internal object NewMessagesDividerItem : ChatItem(ChatAdapter.NEW_MESSAGES_DIVIDER_TYPE) { + override val id: String = "new_messages_item" + override val timestamp: Long = -1 +} + +internal sealed class OperatorStatusItem : ChatItem(ChatAdapter.OPERATOR_STATUS_VIEW_TYPE) { + override val id: String = "operator_status_item" + override val timestamp: Long = -1 + + abstract val companyName: String? + + data class InQueue(override val companyName: String?) : OperatorStatusItem() + + data class Connected( + override val companyName: String?, + val operatorName: String, + val profileImgUrl: String? + ) : OperatorStatusItem() + + data class Joined( + override val companyName: String?, + val operatorName: String, + val profileImgUrl: String? + ) : OperatorStatusItem() + + object Transferring : OperatorStatusItem() { + override val companyName: String? = null + } + +} + +// Visitor + +internal abstract class VisitorChatItem(@ChatAdapter.Type viewType: Int) : ChatItem(viewType) { + abstract val showDelivered: Boolean + abstract fun withDeliveredStatus(delivered: Boolean): VisitorChatItem +} + +internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : VisitorChatItem(viewType), AttachmentItem { + + data class Image( + override val id: String, + override val timestamp: Long, + override val attachmentFile: AttachmentFile, + override val isFileExists: Boolean = false, + override val isDownloading: Boolean = false, + override val showDelivered: Boolean = false + ) : VisitorAttachmentItem(ChatAdapter.VISITOR_IMAGE_VIEW_TYPE) { + override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem = copy(showDelivered = delivered) + + override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem = + copy(isFileExists = isFileExists, isDownloading = isDownloading) + } + + data class File( + override val id: String, + override val timestamp: Long, + override val attachmentFile: AttachmentFile, + override val isFileExists: Boolean = false, + override val isDownloading: Boolean = false, + override val showDelivered: Boolean = false + ) : VisitorAttachmentItem(ChatAdapter.VISITOR_FILE_VIEW_TYPE) { + override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem = copy(showDelivered = delivered) + + override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem = + copy(isFileExists = isFileExists, isDownloading = isDownloading) + } +} + +internal data class VisitorMessageItem( + val message: String, + override val id: String = UUID.randomUUID().toString(), + override val timestamp: Long = System.currentTimeMillis(), + override val showDelivered: Boolean = false +) : VisitorChatItem(ChatAdapter.VISITOR_MESSAGE_TYPE) { + + override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem { + check(!delivered) { "The method should be called only with false value, to hide delivered status" } + return copy(showDelivered = delivered) + } +} + +internal sealed class Unsent(val content: String) { + val chatMessage: VisitorMessageItem = VisitorMessageItem(message = content) + + data class Message(val message: String) : Unsent(message) + data class Attachment(val attachment: SingleChoiceAttachment) : Unsent(attachment.selectedOptionText) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java deleted file mode 100644 index b245be4de..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java +++ /dev/null @@ -1,525 +0,0 @@ -package com.glia.widgets.chat.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.glia.widgets.chat.model.history.ChatItem; -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem; -import com.glia.widgets.chat.model.history.OperatorStatusItem; -import com.glia.widgets.chat.model.history.VisitorMessageItem; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ChatState { - public final boolean integratorChatStarted; - public final boolean isVisible; - public final boolean isChatInBottom; - public final Integer messagesNotSeen; - private final String formattedOperatorName; - public final String operatorProfileImgUrl; - public final String companyName; - public final String queueId; - public final String visitorContextAssetId; - public final MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem; - public final List chatItems; - public final ChatInputMode chatInputMode; - public final String lastTypedText; - public final boolean engagementRequested; - public final String pendingNavigationType; - public final List unsentMessages; - public final OperatorStatusItem operatorStatusItem; - public final boolean showSendButton; - public final boolean isAttachmentButtonEnabled; - public final boolean isAttachmentButtonNeeded; - public final boolean isOperatorTyping; - public final boolean isAttachmentAllowed; - public final boolean isSecureMessaging; - - private ChatState(Builder builder) { - this.formattedOperatorName = builder.formattedOperatorName; - this.operatorProfileImgUrl = builder.operatorProfileImgUrl; - this.companyName = builder.companyName; - this.queueId = builder.queueId; - this.visitorContextAssetId = builder.visitorContextAssetId; - this.isVisible = builder.isVisible; - this.integratorChatStarted = builder.integratorChatStarted; - this.mediaUpgradeStartedTimerItem = builder.mediaUpgradeStartedTimerItem; - this.chatItems = Collections.unmodifiableList(builder.chatItems); - this.chatInputMode = builder.chatInputMode; - this.lastTypedText = builder.lastTypedText; - this.isChatInBottom = builder.isChatInBottom; - this.messagesNotSeen = builder.messagesNotSeen; - this.engagementRequested = builder.engagementRequested; - this.pendingNavigationType = builder.pendingNavigationType; - this.unsentMessages = builder.unsentMessages; - this.operatorStatusItem = builder.operatorStatusItem; - this.showSendButton = builder.showSendButton; - this.isOperatorTyping = builder.isOperatorTyping; - this.isAttachmentButtonEnabled = builder.isAttachmentButtonEnabled; - this.isAttachmentButtonNeeded = builder.isAttachmentButtonNeeded; - this.isAttachmentAllowed = builder.isAttachmentAllowed; - this.isSecureMessaging = builder.isSecureMessaging; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ChatState chatState = (ChatState) o; - return integratorChatStarted == chatState.integratorChatStarted && - isVisible == chatState.isVisible && - Objects.equals(formattedOperatorName, chatState.formattedOperatorName) && - Objects.equals(operatorProfileImgUrl, chatState.operatorProfileImgUrl) && - Objects.equals(companyName, chatState.companyName) && - Objects.equals(queueId, chatState.queueId) && - Objects.equals(visitorContextAssetId, chatState.visitorContextAssetId) && - Objects.equals(mediaUpgradeStartedTimerItem, chatState.mediaUpgradeStartedTimerItem) && - Objects.equals(chatInputMode, chatState.chatInputMode) && - Objects.equals(lastTypedText, chatState.lastTypedText) && - isChatInBottom == chatState.isChatInBottom && - engagementRequested == chatState.engagementRequested && - Objects.equals(pendingNavigationType, chatState.pendingNavigationType) && - Objects.equals(messagesNotSeen, chatState.messagesNotSeen) && - Objects.equals(operatorStatusItem, chatState.operatorStatusItem) && - Objects.equals(unsentMessages, chatState.unsentMessages) && - Objects.equals(chatItems, chatState.chatItems) && - showSendButton == chatState.showSendButton && - isOperatorTyping == chatState.isOperatorTyping && - isAttachmentButtonEnabled == chatState.isAttachmentButtonEnabled && - isAttachmentButtonNeeded == chatState.isAttachmentButtonNeeded && - isAttachmentAllowed == chatState.isAttachmentAllowed && - isSecureMessaging == chatState.isSecureMessaging; - } - - @Override - public int hashCode() { - return Objects.hash(integratorChatStarted, isVisible, isChatInBottom, formattedOperatorName, operatorProfileImgUrl, companyName, queueId, visitorContextAssetId, mediaUpgradeStartedTimerItem, chatItems, chatInputMode, lastTypedText, messagesNotSeen, engagementRequested, pendingNavigationType, unsentMessages, showSendButton, isOperatorTyping, isAttachmentButtonEnabled, isAttachmentButtonNeeded); - } - - @NonNull - @Override - public String toString() { - return "ChatState{" + - "integratorChatStarted=" + integratorChatStarted + - ", isVisible=" + isVisible + - ", operatorName='" + formattedOperatorName + '\'' + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", companyName='" + companyName + '\'' + - ", queueId='" + queueId + '\'' + - ", visitorContextAssetId='" + visitorContextAssetId + '\'' + - ", mediaUpgradeStartedTimerItem=" + mediaUpgradeStartedTimerItem + - ", chatInputMode=" + chatInputMode + - ", lastTypedText: " + lastTypedText + - ", messagesNotSeen: " + messagesNotSeen + - ", isChatInBottom: " + isChatInBottom + - ", engagementRequested: " + engagementRequested + - ", pendingNavigationType: " + pendingNavigationType + - ", operatorStatusItem: " + operatorStatusItem + - ", unsentMessages: " + unsentMessages + - ", chatItems=" + chatItems + - ", showSendButton=" + showSendButton + - ", isOperatorTyping=" + isOperatorTyping + - ", isAttachmentButtonEnabled=" + isAttachmentButtonEnabled + - ", isAttachmentButtonEnabled=" + isAttachmentButtonEnabled + - ", isAttachmentAllowed=" + isAttachmentAllowed + - ", isSecureMessaging=" + isSecureMessaging + - '}'; - } - - public boolean isOperatorOnline() { - return formattedOperatorName != null; - } - - public boolean isMediaUpgradeStarted() { - return mediaUpgradeStartedTimerItem != null; - } - - public boolean isAudioCallStarted() { - return isMediaUpgradeStarted() && - mediaUpgradeStartedTimerItem.type == MediaUpgradeStartedTimerItem.Type.AUDIO; - } - - public boolean showMessagesUnseenIndicator() { - return !isChatInBottom && messagesNotSeen != null && messagesNotSeen > 0; - } - - public boolean isAttachmentButtonVisible() { - return isAttachmentButtonNeeded && isAttachmentAllowed; - } - - public ChatState initChat(String companyName, - String queueId, - String visitorContextAssetId) { - return new Builder() - .copyFrom(this) - .setIntegratorChatStarted(true) - .setCompanyName(companyName) - .setQueueId(queueId) - .setVisitorContextAssetId(visitorContextAssetId) - .setIsVisible(true) - .setShowSendButton(false) - .setIsAttachmentButtonEnabled(true) - .setIsAttachmentAllowed(true) - .createChatState(); - } - - public String getFormattedOperatorName() { - return formattedOperatorName; - } - - public ChatState queueingStarted(OperatorStatusItem operatorStatusItem) { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setChatInputMode(ChatInputMode.ENABLED) - .setEngagementRequested(true) - .setOperatorStatusItem(operatorStatusItem) - .createChatState(); - } - - public ChatState setSecureMessagingState() { - return new Builder() - .copyFrom(this) - .setSecureMessaging(true) - .enableChatPanel() - .createChatState(); - } - - public ChatState setLiveChatState() { - return new Builder() - .copyFrom(this) - .setSecureMessaging(false) - .createChatState(); - } - - public ChatState allowSendAttachmentStateChanged(boolean isAttachmentAllowed) { - return new Builder() - .copyFrom(this) - .setIsAttachmentAllowed(isAttachmentAllowed) - .createChatState(); - } - - public ChatState engagementStarted() { - return new Builder() - .copyFrom(this) - .enableChatPanel() - .setEngagementRequested(true) - .createChatState(); - } - - public ChatState transferring() { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setEngagementRequested(true) - .setOperatorStatusItem(OperatorStatusItem.TransferringStatusItem()) - .disableChatPanel() - .createChatState(); - } - - public ChatState operatorConnected(String formattedOperatorName, @Nullable String operatorProfileImgUrl) { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(formattedOperatorName) - .setOperatorProfileImgUrl(operatorProfileImgUrl) - .enableChatPanel() - .createChatState(); - } - - public ChatState historyLoaded(List chatItems) { - return new Builder() - .copyFrom(this) - .setChatInputMode(ChatInputMode.ENABLED_NO_ENGAGEMENT) - .setIsAttachmentButtonNeeded(false) - .setChatItems(chatItems) - .createChatState(); - } - - public ChatState changeItems(List newItems) { - return new Builder() - .copyFrom(this) - .setChatItems(newItems) - .createChatState(); - } - - public ChatState changeTimerItem( - List newItems, - MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem - ) { - return new Builder() - .copyFrom(changeItems(newItems)) - .setMediaUpgradeStartedItem(mediaUpgradeStartedTimerItem) - .createChatState(); - } - - public ChatState changeVisibility(boolean isVisible) { - return new Builder() - .copyFrom(this) - .setIsVisible(isVisible) - .createChatState(); - } - - public ChatState setLastTypedText(String text) { - return new Builder() - .copyFrom(this) - .setLastTypedText(text) - .createChatState(); - } - - public ChatState chatInputModeChanged(ChatInputMode chatInputMode) { - return new Builder() - .copyFrom(this) - .setChatInputMode(chatInputMode) - .setIsAttachmentButtonNeeded(chatInputMode == ChatInputMode.ENABLED) - .createChatState(); - } - - public ChatState isInBottomChanged(boolean isChatInBottom) { - return new Builder() - .copyFrom(this) - .setIsChatInBottom(isChatInBottom) - .createChatState(); - } - - public ChatState messagesNotSeenChanged(int messagesNotSeen) { - return new Builder() - .copyFrom(this) - .setMessagesNotSeen(messagesNotSeen) - .createChatState(); - } - - public ChatState setPendingNavigationType(String pendingNavigationType) { - return new Builder() - .copyFrom(this) - .setPendingNavigationType(pendingNavigationType) - .createChatState(); - } - - public ChatState changeUnsentMessages(List unsentMessages) { - return new Builder() - .copyFrom(this) - .setUnsentMessages(Collections.unmodifiableList(unsentMessages)) - .createChatState(); - } - - public ChatState setShowSendButton(boolean isShow) { - return new Builder() - .copyFrom(this) - .setShowSendButton(isShow) - .createChatState(); - } - - public ChatState setIsOperatorTyping(boolean isOperatorTyping) { - return new Builder() - .copyFrom(this) - .setIsOperatorTyping(isOperatorTyping) - .createChatState(); - } - - public ChatState setIsAttachmentButtonEnabled(boolean isAttachmentButtonEnabled) { - return new Builder() - .copyFrom(this) - .setIsAttachmentButtonEnabled(isAttachmentButtonEnabled) - .createChatState(); - } - - public ChatState stop() { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setIsVisible(false) - .setIntegratorChatStarted(false) - .setIsAttachmentButtonNeeded(false) - .createChatState(); - } - - public static class Builder { - public boolean isOperatorTyping; - public boolean isAttachmentButtonEnabled; - public boolean isAttachmentButtonNeeded; - private boolean isChatInBottom; - private String formattedOperatorName; - private String operatorProfileImgUrl; - private String companyName; - private String queueId; - private boolean isVisible; - private boolean integratorChatStarted; - private MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem; - private List chatItems; - private ChatInputMode chatInputMode; - private String lastTypedText; - private Integer messagesNotSeen; - private boolean engagementRequested; - private String pendingNavigationType; - private List unsentMessages; - private OperatorStatusItem operatorStatusItem; - private boolean showSendButton; - private boolean isAttachmentAllowed; - private String visitorContextAssetId; - private boolean isSecureMessaging; - - public Builder copyFrom(ChatState chatState) { - isChatInBottom = chatState.isChatInBottom; - formattedOperatorName = chatState.formattedOperatorName; - operatorProfileImgUrl = chatState.operatorProfileImgUrl; - companyName = chatState.companyName; - queueId = chatState.queueId; - visitorContextAssetId = chatState.visitorContextAssetId; - isVisible = chatState.isVisible; - integratorChatStarted = chatState.integratorChatStarted; - mediaUpgradeStartedTimerItem = chatState.mediaUpgradeStartedTimerItem; - chatItems = chatState.chatItems; - chatInputMode = chatState.chatInputMode; - lastTypedText = chatState.lastTypedText; - messagesNotSeen = chatState.messagesNotSeen; - engagementRequested = chatState.engagementRequested; - pendingNavigationType = chatState.pendingNavigationType; - unsentMessages = chatState.unsentMessages; - operatorStatusItem = chatState.operatorStatusItem; - showSendButton = chatState.showSendButton; - isOperatorTyping = chatState.isOperatorTyping; - isAttachmentButtonEnabled = chatState.isAttachmentButtonEnabled; - isAttachmentButtonNeeded = chatState.isAttachmentButtonNeeded; - isAttachmentAllowed = chatState.isAttachmentAllowed; - isSecureMessaging = chatState.isSecureMessaging; - return this; - } - - public Builder setFormattedOperatorName(String formattedOperatorName) { - this.formattedOperatorName = formattedOperatorName; - return this; - } - - public Builder setOperatorProfileImgUrl(String operatorProfileImgUrl) { - this.operatorProfileImgUrl = operatorProfileImgUrl; - return this; - } - - public Builder setCompanyName(String companyName) { - this.companyName = companyName; - return this; - } - - public Builder setQueueId(String queueId) { - this.queueId = queueId; - return this; - } - - public Builder setIsVisible(boolean isVisible) { - this.isVisible = isVisible; - return this; - } - - public Builder setIntegratorChatStarted(boolean integratorChatStarted) { - this.integratorChatStarted = integratorChatStarted; - return this; - } - - public Builder setMediaUpgradeStartedItem(MediaUpgradeStartedTimerItem mediaUpgradeStartedItem) { - this.mediaUpgradeStartedTimerItem = mediaUpgradeStartedItem; - return this; - } - - public Builder setChatItems(List chatItems) { - this.chatItems = chatItems; - return this; - } - - public Builder setChatInputMode(ChatInputMode chatInputMode) { - this.chatInputMode = chatInputMode; - return this; - } - - public Builder setLastTypedText(String lastTypedText) { - this.lastTypedText = lastTypedText; - return this; - } - - public Builder setIsChatInBottom(boolean isChatInBottom) { - this.isChatInBottom = isChatInBottom; - return this; - } - - public Builder setMessagesNotSeen(Integer messagesNotSeen) { - this.messagesNotSeen = messagesNotSeen; - return this; - } - - public Builder setEngagementRequested(boolean engagementRequested) { - this.engagementRequested = engagementRequested; - return this; - } - - public Builder setPendingNavigationType(String pendingNavigationType) { - this.pendingNavigationType = pendingNavigationType; - return this; - } - - public Builder setUnsentMessages(List unsentMessages) { - this.unsentMessages = unsentMessages; - return this; - } - - public Builder setOperatorStatusItem(OperatorStatusItem operatorStatusItem) { - this.operatorStatusItem = operatorStatusItem; - return this; - } - - public Builder setShowSendButton(boolean isShow) { - this.showSendButton = isShow; - return this; - } - - public Builder setIsOperatorTyping(boolean isOperatorTyping) { - this.isOperatorTyping = isOperatorTyping; - return this; - } - - public Builder setIsAttachmentButtonEnabled(boolean isAttachmentButtonEnabled) { - this.isAttachmentButtonEnabled = isAttachmentButtonEnabled; - return this; - } - - public Builder setIsAttachmentButtonNeeded(boolean isAttachmentButtonNeeded) { - this.isAttachmentButtonNeeded = isAttachmentButtonNeeded; - return this; - } - - public Builder setIsAttachmentAllowed(boolean isAttachmentAllowed) { - this.isAttachmentAllowed = isAttachmentAllowed; - return this; - } - - public ChatState createChatState() { - return new ChatState(this); - } - - public Builder setVisitorContextAssetId(String visitorContextAssetId) { - this.visitorContextAssetId = visitorContextAssetId; - return this; - } - - public Builder setSecureMessaging(boolean secureMessaging) { - this.isSecureMessaging = secureMessaging; - return this; - } - - public Builder enableChatPanel() { - setChatInputMode(ChatInputMode.ENABLED); - setIsAttachmentButtonNeeded(true); - return this; - } - - public Builder disableChatPanel() { - setChatInputMode(ChatInputMode.DISABLED); - setShowSendButton(false); - setIsAttachmentButtonNeeded(false); - return this; - } - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt new file mode 100644 index 000000000..8e7835839 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt @@ -0,0 +1,124 @@ +package com.glia.widgets.chat.model + +internal data class ChatState( + val integratorChatStarted: Boolean = false, + val isVisible: Boolean = false, + val isChatInBottom: Boolean = true, + val messagesNotSeen: Int = 0, + val formattedOperatorName: String? = null, + val operatorProfileImgUrl: String? = null, + val companyName: String? = null, + val queueId: String? = null, + val visitorContextAssetId: String? = null, + val isMediaUpgradeVide: Boolean? = null, + val chatInputMode: ChatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT, + val lastTypedText: String = "", + val engagementRequested: Boolean = false, + val pendingNavigationType: String? = null, + val operatorStatusItem: OperatorStatusItem? = null, + val showSendButton: Boolean = false, + val isAttachmentButtonEnabled: Boolean = false, + val isAttachmentButtonNeeded: Boolean = false, + val isOperatorTyping: Boolean = false, + val isAttachmentAllowed: Boolean = true, + val isSecureMessaging: Boolean = false, + val gvaQuickReplies: List = emptyList() +) { + + val isOperatorOnline: Boolean get() = formattedOperatorName != null + + val isMediaUpgradeStarted: Boolean get() = isMediaUpgradeVide != null + + val isAudioCallStarted: Boolean + get() = isMediaUpgradeVide != true + + val showMessagesUnseenIndicator: Boolean get() = !isChatInBottom && messagesNotSeen > 0 + + val isAttachmentButtonVisible: Boolean get() = isAttachmentButtonNeeded && isAttachmentAllowed + + fun initChat(companyName: String?, queueId: String?, visitorContextAssetId: String?): ChatState = copy( + integratorChatStarted = true, + companyName = companyName, + queueId = queueId, + visitorContextAssetId = visitorContextAssetId, + isVisible = true, + showSendButton = false, + isAttachmentButtonEnabled = true, + isAttachmentAllowed = true + ) + + fun queueingStarted(): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + chatInputMode = ChatInputMode.ENABLED, + engagementRequested = true, + ) + + fun setSecureMessagingState(): ChatState = copy( + isSecureMessaging = true, + chatInputMode = ChatInputMode.ENABLED, + isAttachmentButtonNeeded = true + ) + + fun setLiveChatState(): ChatState = copy(isSecureMessaging = false) + + fun allowSendAttachmentStateChanged(isAttachmentAllowed: Boolean): ChatState = copy(isAttachmentAllowed = isAttachmentAllowed) + + fun engagementStarted(): ChatState = copy( + chatInputMode = ChatInputMode.ENABLED, + isAttachmentButtonNeeded = true, + engagementRequested = true + ) + + fun transferring(): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + engagementRequested = false, + operatorStatusItem = OperatorStatusItem.Transferring, + chatInputMode = ChatInputMode.DISABLED, + showSendButton = false, + isAttachmentButtonNeeded = false + ) + + fun operatorConnected( + formattedOperatorName: String?, + operatorProfileImgUrl: String? + ): ChatState = copy( + formattedOperatorName = formattedOperatorName, + operatorProfileImgUrl = operatorProfileImgUrl, + chatInputMode = ChatInputMode.ENABLED, + isAttachmentButtonNeeded = true + ) + + fun historyLoaded(): ChatState = copy( + chatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT, + isAttachmentButtonNeeded = false, + ) + + fun upgradeMedia(isVideo: Boolean?): ChatState = copy(isMediaUpgradeVide = isVideo) + + fun changeVisibility(isVisible: Boolean): ChatState = copy(isVisible = isVisible) + + fun setLastTypedText(text: String): ChatState = copy(lastTypedText = text) + + fun isInBottomChanged(isChatInBottom: Boolean): ChatState = copy(isChatInBottom = isChatInBottom) + + fun messagesNotSeenChanged(messagesNotSeen: Int): ChatState = copy(messagesNotSeen = messagesNotSeen) + + fun setPendingNavigationType(pendingNavigationType: String?): ChatState = copy(pendingNavigationType = pendingNavigationType) + + fun setShowSendButton(isShow: Boolean): ChatState = copy(showSendButton = isShow) + + fun setIsOperatorTyping(isOperatorTyping: Boolean): ChatState = copy(isOperatorTyping = isOperatorTyping) + + fun setIsAttachmentButtonEnabled(isAttachmentButtonEnabled: Boolean): ChatState = copy(isAttachmentButtonEnabled = isAttachmentButtonEnabled) + + fun stop(): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + isVisible = false, + integratorChatStarted = false, + isAttachmentButtonNeeded = false, + isMediaUpgradeVide = null + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt new file mode 100644 index 000000000..06c7b2cdb --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt @@ -0,0 +1,74 @@ +package com.glia.widgets.chat.model + +import android.net.Uri +import androidx.annotation.StringDef +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.google.gson.annotations.SerializedName + +internal object Gva { + + object Keys { + const val TYPE = "type" + const val CONTENT = "content" + const val OPTIONS = "options" + const val GALLERY_CARDS = "galleryCards" + } + + enum class Type(val value: String) { + PLAIN_TEXT("plainText"), + PERSISTENT_BUTTONS("persistentButtons"), + QUICK_REPLIES("quickReplies"), + GALLERY_CARDS("galleryCards") + } + + @StringDef( + UrlTarget.MODAL, + UrlTarget.SELF, + UrlTarget.BLANK + ) + @Retention(AnnotationRetention.SOURCE) + annotation class UrlTarget { + companion object { + const val MODAL = "modal" + const val SELF = "self" + const val BLANK = "blank" + } + } + + sealed interface ButtonType { + object BroadcastEvent : ButtonType + data class PostBack(val singleChoiceAttachment: SingleChoiceAttachment) : ButtonType + data class Phone(val uri: Uri) : ButtonType + data class Email(val uri: Uri) : ButtonType + data class Url(val uri: Uri) : ButtonType + } +} + +internal data class GvaButton( + @SerializedName("text") + val text: String = "", + @SerializedName("value") + val value: String = "", + @SerializedName("url") + val url: String? = null, + @Gva.UrlTarget + @SerializedName("urlTarget") + val urlTarget: String? = null, + @SerializedName("destinationPbBroadcastEvent") + val destinationPbBroadcastEvent: String? = null, + @SerializedName("transferPhoneNumber") + val transferPhoneNumber: String? = null +) { + fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(value, text) +} + +internal data class GvaGalleryCard( + @SerializedName("title") + val title: String = "", + @SerializedName("subtitle") + val subtitle: String? = null, + @SerializedName("imageUrl") + val imageUrl: String? = null, + @SerializedName("options") + val options: List = listOf() +) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt new file mode 100644 index 000000000..b00360a80 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt @@ -0,0 +1,61 @@ +package com.glia.widgets.chat.model + +import com.glia.widgets.chat.adapter.ChatAdapter + +internal abstract class GvaOperatorChatItem(@ChatAdapter.Type viewType: Int) : OperatorChatItem(viewType) { + abstract val operatorName: String? +} + +internal data class GvaResponseText( + override val id: String = "", + val content: String = "", + override val showChatHead: Boolean = false, + override val operatorId: String? = "", + override val timestamp: Long = -1, + override val operatorProfileImgUrl: String? = null, + override val operatorName: String? = null +) : GvaOperatorChatItem(ChatAdapter.GVA_RESPONSE_TEXT_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) +} + +internal data class GvaPersistentButtons( + override val id: String = "", + val content: String = "", + val options: List = listOf(), + override val showChatHead: Boolean = false, + override val operatorId: String? = "", + override val timestamp: Long = -1, + override val operatorProfileImgUrl: String? = null, + override val operatorName: String? = null +) : GvaOperatorChatItem(ChatAdapter.GVA_PERSISTENT_BUTTONS_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) +} + +internal data class GvaGalleryCards( + override val id: String = "", + val galleryCards: List, + override val showChatHead: Boolean = false, + override val operatorId: String? = "", + override val timestamp: Long = -1, + override val operatorProfileImgUrl: String? = null, + override val operatorName: String? = null +) : GvaOperatorChatItem(ChatAdapter.GVA_GALLERY_CARDS_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) +} + +internal data class GvaQuickReplies( + override val id: String = "", + val content: String = "", + override val showChatHead: Boolean = false, + override val operatorId: String? = "", + override val timestamp: Long = -1, + override val operatorProfileImgUrl: String? = null, + override val operatorName: String? = null, + val options: List = listOf(), +) : GvaOperatorChatItem(ChatAdapter.GVA_QUICK_REPLIES_TYPE) { + override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead) + + fun asResponseText(): GvaResponseText = GvaResponseText( + id, content, showChatHead, operatorId, timestamp, operatorProfileImgUrl, operatorName + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ChatItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ChatItem.java deleted file mode 100644 index 0dffcdc3f..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ChatItem.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class ChatItem { - @ChatAdapter.Type - private final int viewType; - private final String id; - - protected ChatItem(String id, @ChatAdapter.Type int viewType) { - this.id = id; - this.viewType = viewType; - } - - @ChatAdapter.Type - public int getViewType() { - return viewType; - } - - public String getId() { - return id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ChatItem chatItem = (ChatItem) o; - return viewType == chatItem.viewType && - id.equals(chatItem.id); - } - - @Override - public int hashCode() { - return Objects.hash(viewType, id); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/CustomCardItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/CustomCardItem.java deleted file mode 100644 index aef055a68..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/CustomCardItem.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.ChatMessage; - -import java.util.Objects; - -public class CustomCardItem extends LinkedChatItem implements ServerChatItem { - private final ChatMessage message; - - public CustomCardItem(ChatMessage message, int viewType) { - super(message.getId(), viewType, message.getId(), message.getTimestamp()); - - this.message = message; - } - - public ChatMessage getMessage() { - return message; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - CustomCardItem that = (CustomCardItem) o; - - if (message.getTimestamp() != that.message.getTimestamp()) return false; - if (!message.getId().equals(that.message.getId())) return false; - if (!message.getContent().equals(that.message.getContent())) return false; - if (message.getSenderType() != that.message.getSenderType()) return false; - if (!Objects.equals(message.getAttachment(), that.message.getAttachment())) return false; - return Objects.equals(message.getMetadata(), that.message.getMetadata()); - } - - @Override - public int hashCode() { - int result = (int) (message.getTimestamp() ^ (message.getTimestamp() >>> 32)); - result = 31 * result + message.getId().hashCode(); - result = 31 * result + message.getContent().hashCode(); - result = 31 * result + message.getSenderType().hashCode(); - result = 31 * result + (message.getAttachment() != null ? message.getAttachment().hashCode() : 0); - result = 31 * result + (message.getMetadata() != null ? message.getMetadata().hashCode() : 0); - return result; - } - - @NonNull - @Override - public String toString() { - return "CustomCardItem{" + - "message={" + - "timestamp=" + message.getTimestamp() + - ", id='" + message.getId() + '\'' + - ", content='" + message.getContent() + '\'' + - ", sender=" + message.getSenderType() + - ", attachment=" + message.getAttachment() + - ", metadata=" + message.getMetadata() + - "}" + - "}"; - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/LinkedChatItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/LinkedChatItem.java deleted file mode 100644 index eec4814db..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/LinkedChatItem.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import com.glia.widgets.chat.adapter.ChatAdapter; - -public class LinkedChatItem extends ChatItem { - - private final String messageId; - private final long timestamp; - - public LinkedChatItem(String id, @ChatAdapter.Type int viewType, String messageId, long timestamp) { - super(id, viewType); - this.messageId = messageId; - this.timestamp = timestamp; - } - - public String getMessageId() { - return messageId; - } - - public long getTimestamp() { - return timestamp; - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java deleted file mode 100644 index 013e754c4..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class MediaUpgradeStartedTimerItem extends ChatItem { - public final static String ID = "media_upgrade_item"; - public final MediaUpgradeStartedTimerItem.Type type; - public final String time; - - public MediaUpgradeStartedTimerItem(MediaUpgradeStartedTimerItem.Type type, String time) { - super(ID, ChatAdapter.MEDIA_UPGRADE_ITEM_TYPE); - this.type = type; - this.time = time; - } - - public enum Type { - AUDIO, VIDEO - } - - @Override - public String toString() { - return "MediaUpgradeStartedTimerItem{" + - "type=" + type + - ", time='" + time + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - MediaUpgradeStartedTimerItem that = (MediaUpgradeStartedTimerItem) o; - return type == that.type && - Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), type, time); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/NewMessagesItem.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/NewMessagesItem.kt deleted file mode 100644 index 33ab7060d..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/NewMessagesItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.glia.widgets.chat.model.history - -import com.glia.widgets.chat.adapter.ChatAdapter -import java.util.* - -object NewMessagesItem : ChatItem(UUID.randomUUID().toString(), ChatAdapter.NEW_MESSAGES_DIVIDER_TYPE) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java deleted file mode 100644 index 1c13473e8..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.AttachmentFile; -import com.glia.widgets.helper.Utils; - -import java.util.Objects; - -public class OperatorAttachmentItem extends OperatorChatItem { - - public final AttachmentFile attachmentFile; - public final boolean isFileExists; - public final boolean isDownloading; - - public OperatorAttachmentItem( - String chatItemId, - int viewType, - boolean showChatHead, - AttachmentFile attachmentFile, - String operatorProfileImgUrl, - boolean isFileExists, - boolean isDownloading, - String operatorId, - String messageId, - long timestamp - ) { - super(chatItemId, viewType, showChatHead, operatorProfileImgUrl, operatorId, messageId, timestamp); - this.attachmentFile = attachmentFile; - this.isFileExists = isFileExists; - this.isDownloading = isDownloading; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof OperatorAttachmentItem)) return false; - if (!super.equals(o)) return false; - OperatorAttachmentItem that = (OperatorAttachmentItem) o; - return isFileExists == that.isFileExists && isDownloading == that.isDownloading && Objects.equals(attachmentFile, that.attachmentFile); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), attachmentFile, isFileExists, isDownloading); - } - - @Override - public String toString() { - return "OperatorAttachmentItem{" + - "attachmentFile=" + attachmentFile + - ", isFileExists=" + isFileExists + - ", isDownloading=" + isDownloading + - ", showChatHead=" + showChatHead + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", operatorId='" + operatorId + '\'' + - "} " + super.toString(); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java deleted file mode 100644 index 3d17cbc1c..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import java.util.Objects; - -public abstract class OperatorChatItem extends LinkedChatItem implements ServerChatItem { - - public final boolean showChatHead; - public final String operatorProfileImgUrl; - public final String operatorId; - - protected OperatorChatItem(String id, int viewType, boolean showChatHead, String operatorProfileImgUrl, String operatorId, String messageId, long timestamp) { - super(id, viewType, messageId, timestamp); - this.showChatHead = showChatHead; - this.operatorProfileImgUrl = operatorProfileImgUrl; - this.operatorId = operatorId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof OperatorChatItem)) return false; - if (!super.equals(o)) return false; - OperatorChatItem that = (OperatorChatItem) o; - return showChatHead == that.showChatHead - && Objects.equals(operatorProfileImgUrl, that.operatorProfileImgUrl) - && Objects.equals(operatorId, that.operatorId); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), showChatHead, operatorProfileImgUrl, operatorId); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java deleted file mode 100644 index 3efe2a5ca..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class OperatorMessageItem extends OperatorChatItem { - public final String operatorName; - public final String content; - - public OperatorMessageItem( - String id, - String operatorName, - String operatorProfileImgUrl, - boolean showChatHead, - String content, - String operatorId, - long timestamp - ) { - super(id, ChatAdapter.OPERATOR_MESSAGE_VIEW_TYPE, showChatHead, operatorProfileImgUrl, operatorId, id, timestamp); - this.operatorName = operatorName; - this.content = content; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof OperatorMessageItem)) return false; - if (!super.equals(o)) return false; - OperatorMessageItem that = (OperatorMessageItem) o; - return Objects.equals(operatorName, that.operatorName) - && Objects.equals(content, that.content); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), operatorName, content); - } - - @NonNull - @Override - public String toString() { - return "OperatorMessageItem{" + - "showChatHead=" + showChatHead + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", operatorId='" + operatorId + '\'' + - ", operatorName='" + operatorName + '\'' + - ", content='" + content + '\'' + - "} " + super.toString(); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorStatusItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorStatusItem.java deleted file mode 100644 index 1f6fac7e0..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorStatusItem.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class OperatorStatusItem extends ChatItem { - public static final String ID = "operator_status_item"; - private final String companyName; - private final Status status; - private final String operatorName; - private final String profileImgUrl; - - public OperatorStatusItem(Status status, String companyName, String operatorName, String profileImgUrl) { - super(ID, ChatAdapter.OPERATOR_STATUS_VIEW_TYPE); - this.companyName = companyName; - this.status = status; - this.operatorName = operatorName; - this.profileImgUrl = profileImgUrl; - } - - public String getCompanyName() { - return companyName; - } - - @NonNull - public Status getStatus() { - return status; - } - - public String getOperatorName() { - return operatorName; - } - - public String getProfileImgUrl() { - return profileImgUrl; - } - - public enum Status { - IN_QUEUE, - OPERATOR_CONNECTED, - JOINED, - TRANSFERRING - } - - public static OperatorStatusItem QueueingStatusItem(String companyName) { - return new OperatorStatusItem(OperatorStatusItem.Status.IN_QUEUE, companyName, null, null); - } - - public static OperatorStatusItem OperatorFoundStatusItem(String companyName, String operatorName, String profileImgUrl) { - return new OperatorStatusItem(Status.OPERATOR_CONNECTED, companyName, operatorName, profileImgUrl); - } - - public static OperatorStatusItem OperatorJoinedStatusItem(String companyName, String operatorName, String profileImgUrl) { - return new OperatorStatusItem(Status.JOINED, companyName, operatorName, profileImgUrl); - } - - public static OperatorStatusItem TransferringStatusItem() { - return new OperatorStatusItem(Status.TRANSFERRING, null, null, null); - } - - @Override - public String toString() { - return "OperatorStatusItem{" + - "companyName='" + companyName + '\'' + - ", status=" + status + - ", operatorName='" + operatorName + '\'' + - ", profileImgUrl='" + profileImgUrl + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - OperatorStatusItem that = (OperatorStatusItem) o; - return Objects.equals(companyName, that.companyName) && - status == that.status && - Objects.equals(operatorName, that.operatorName) && - Objects.equals(profileImgUrl, that.profileImgUrl); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), companyName, status, operatorName, profileImgUrl); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java deleted file mode 100644 index d4985ae8f..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.SingleChoiceOption; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ResponseCardItem extends OperatorMessageItem { - @NonNull - public final List singleChoiceOptions; - public final String choiceCardImageUrl; - - public ResponseCardItem( - @NonNull String id, - String operatorName, - String operatorProfileImgUrl, - boolean showChatHead, - String content, - String operatorId, - long timestamp, - //Must not be empty, otherwise it is OperatorMessageItem - @NonNull List singleChoiceOptions, - String choiceCardImageUrl - ) { - super(id, operatorName, operatorProfileImgUrl, showChatHead, content, operatorId, timestamp); - - assert !singleChoiceOptions.isEmpty(); - this.singleChoiceOptions = Collections.unmodifiableList(singleChoiceOptions); - this.choiceCardImageUrl = choiceCardImageUrl; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ResponseCardItem)) return false; - if (!super.equals(o)) return false; - ResponseCardItem that = (ResponseCardItem) o; - return singleChoiceOptions.equals(that.singleChoiceOptions) && Objects.equals(choiceCardImageUrl, that.choiceCardImageUrl); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), singleChoiceOptions, choiceCardImageUrl); - } - - @NonNull - @Override - public String toString() { - return "ResponseCardItem{" + - "singleChoiceOptions=" + singleChoiceOptions + - ", choiceCardImageUrl='" + choiceCardImageUrl + '\'' + - "} " + super.toString(); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ServerChatItem.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ServerChatItem.kt deleted file mode 100644 index 883a058ba..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ServerChatItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.glia.widgets.chat.model.history - -/** - * Indicates that the [ChatItem] was sent by server - */ -internal interface ServerChatItem diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/SystemChatItem.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/SystemChatItem.kt deleted file mode 100644 index dd346d054..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/SystemChatItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.glia.widgets.chat.model.history - -import com.glia.widgets.chat.adapter.ChatAdapter - -class SystemChatItem(id: String, timestamp: Long, val message: String) : - LinkedChatItem(id, ChatAdapter.SYSTEM_MESSAGE_TYPE, id, timestamp), ServerChatItem diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorAttachmentItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorAttachmentItem.java deleted file mode 100644 index 1bff7203f..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorAttachmentItem.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.AttachmentFile; -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class VisitorAttachmentItem extends LinkedChatItem { - - public final AttachmentFile attachmentFile; - public final boolean isFileExists; - public final boolean isDownloading; - public final boolean showDelivered; - - private VisitorAttachmentItem(String chatItemId, int viewType, AttachmentFile attachmentFile, - boolean isFileExists, boolean isDownloading, boolean showDelivered, long timestamp) { - super(chatItemId, viewType, chatItemId, timestamp); - this.attachmentFile = attachmentFile; - this.isFileExists = isFileExists; - this.isDownloading = isDownloading; - this.showDelivered = showDelivered; - } - - public static VisitorAttachmentItem editDeliveredStatus(VisitorAttachmentItem source, boolean isDelivered) { - return new VisitorAttachmentItem( - source.getId(), - source.getViewType(), - source.attachmentFile, - source.isFileExists, - source.isDownloading, - isDelivered, - source.getTimestamp() - ); - } - - public static VisitorAttachmentItem editDownloadedStatus(VisitorAttachmentItem source, boolean isDownloaded) { - return new VisitorAttachmentItem( - source.getId(), - source.getViewType(), - source.attachmentFile, - isDownloaded, - source.isDownloading, - source.showDelivered, - source.getTimestamp() - ); - } - - public static VisitorAttachmentItem editFileStatuses(VisitorAttachmentItem source, boolean doesFileExists, boolean isDownloading) { - return new VisitorAttachmentItem( - source.getId(), - source.getViewType(), - source.attachmentFile, - doesFileExists, - isDownloading, - source.showDelivered, - source.getTimestamp() - ); - } - - public static VisitorAttachmentItem fromAttachmentFile(String messageId, long messageTimestamp, AttachmentFile file) { - int type; - if (file.getContentType().startsWith("image")) { - type = ChatAdapter.VISITOR_IMAGE_VIEW_TYPE; - } else { - type = ChatAdapter.VISITOR_FILE_VIEW_TYPE; - } - return new VisitorAttachmentItem(messageId, type, file, false, false, false, messageTimestamp); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - VisitorAttachmentItem that = (VisitorAttachmentItem) o; - return isFileExists == that.isFileExists && isDownloading == that.isDownloading && - showDelivered == that.showDelivered && Objects.equals(attachmentFile, that.attachmentFile); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), attachmentFile, isFileExists, isDownloading, showDelivered); - } - - @NonNull - @Override - public String toString() { - return "VisitorAttachmentItem{" + - "attachmentFile=" + attachmentFile + - ", chatItemId=" + getId() + - ", isFileExists=" + isFileExists + - ", isDownloading=" + isDownloading + - ", showDelivered=" + showDelivered + - '}'; - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorMessageItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorMessageItem.java deleted file mode 100644 index 98c7fd862..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/VisitorMessageItem.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.ChatMessage; -import com.glia.androidsdk.chat.SingleChoiceAttachment; -import com.glia.widgets.chat.adapter.ChatAdapter; - -import java.util.Objects; - -public class VisitorMessageItem extends LinkedChatItem { - public final static String HISTORY_ID = "history_id"; - public final static String CARD_RESPONSE_ID = "card_response_id"; - public final static String UNSENT_MESSAGE_ID = "unsent_message_id"; - private final boolean showDelivered; - private final String message; - - private VisitorMessageItem(String id, String messageId, String message, long timestamp, boolean showDelivered) { - super(id, ChatAdapter.VISITOR_MESSAGE_TYPE, messageId, timestamp); - this.showDelivered = showDelivered; - this.message = message; - } - - public static VisitorMessageItem asNewMessage(ChatMessage message) { - return new VisitorMessageItem(message.getId(), message.getId(), message.getContent(), message.getTimestamp(), false); - } - - public static VisitorMessageItem asUnsentItem(String unsentMessageText) { - return new VisitorMessageItem(UNSENT_MESSAGE_ID, null, unsentMessageText, System.currentTimeMillis(), false); - } - - public static VisitorMessageItem asHistoryItem(ChatMessage message) { - return new VisitorMessageItem(HISTORY_ID, message.getId(), message.getContent(), message.getTimestamp(), false); - } - - public static VisitorMessageItem asCardResponseItem(ChatMessage message) { - String selectedOptionText = null; - if (message.getAttachment() != null && message.getAttachment() instanceof SingleChoiceAttachment) { - SingleChoiceAttachment singleChoiceAttachment = (SingleChoiceAttachment) message.getAttachment(); - selectedOptionText = singleChoiceAttachment.getSelectedOptionText(); - } - return new VisitorMessageItem(CARD_RESPONSE_ID, message.getId(), selectedOptionText, message.getTimestamp(), false); - } - - public static VisitorMessageItem asUnsentCardResponse(String unsentResponse) { - return new VisitorMessageItem(CARD_RESPONSE_ID, null, unsentResponse, System.currentTimeMillis(), false); - } - - public static VisitorMessageItem editDeliveredStatus(VisitorMessageItem source, boolean showDelivered) { - return new VisitorMessageItem(source.getId(), source.getMessageId(), source.getMessage(), source.getTimestamp(), showDelivered); - } - - public String getMessage() { - return message; - } - - public boolean isShowDelivered() { - return showDelivered; - } - - @NonNull - @Override - public String toString() { - return "VisitorMessageItem{" + - "chatItemId='" + getId() + '\'' + - ", showDelivered=" + showDelivered + - ", message='" + message + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - VisitorMessageItem that = (VisitorMessageItem) o; - return showDelivered == that.showDelivered && - getId().equals(that.getId()) && - Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), getId(), showDelivered, message); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java index f6df6976f..00bc0b691 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java @@ -1,6 +1,7 @@ package com.glia.widgets.core.engagement; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.glia.androidsdk.Engagement; import com.glia.androidsdk.Operator; @@ -17,23 +18,22 @@ public class GliaEngagementStateRepository { private final EngagementStateEventVisitor visitor = new EngagementStateEventVisitor.OperatorVisitor(); private final BehaviorProcessor> operatorProcessor = - BehaviorProcessor.createDefault(Optional.empty()); + BehaviorProcessor.createDefault(Optional.empty()); private final Flowable operatorFlowable = - operatorProcessor - .filter(Optional::isPresent) - .map(Optional::get) - .onBackpressureLatest(); + operatorProcessor + .filter(Optional::isPresent) + .map(Optional::get) + .onBackpressureLatest(); private final BehaviorProcessor> engagementStateProcessor = BehaviorProcessor.createDefault(Optional.empty()); private final BehaviorProcessor engagementStateEventProcessor = BehaviorProcessor.createDefault( - new EngagementStateEvent.EngagementEndedEvent() + new EngagementStateEvent.NoEngagementEvent() ); private final Flowable engagementStateEventFlowable = engagementStateEventProcessor.onBackpressureLatest(); - - private CompositeDisposable disposable = new CompositeDisposable(); - private final GliaOperatorRepository operatorRepository; + private CompositeDisposable disposable = new CompositeDisposable(); + private boolean isOngoingEngagement = false; public GliaEngagementStateRepository(GliaOperatorRepository operatorRepository) { this.operatorRepository = operatorRepository; @@ -42,14 +42,16 @@ public GliaEngagementStateRepository(GliaOperatorRepository operatorRepository) public void onEngagementStarted(Engagement engagement) { disposable = new CompositeDisposable(); disposable.add( - engagementStateProcessor - .onBackpressureLatest() - .map(state -> mapToEngagementStateChangeEvent(state.orElse(null), getOperator())) - .doOnNext(this::notifyEngagementStateEventUpdate) - .doOnNext(this::updateOperatorOnEngagementStateChanged) - .subscribe() + engagementStateProcessor + .onBackpressureLatest() + .map(state -> mapToEngagementStateChangeEvent(state.orElse(null), getOperator())) + .doOnNext(this::notifyEngagementStateEventUpdate) + .doOnNext(this::updateOperatorOnEngagementStateChanged) + .subscribe() ); - engagement.on(Engagement.Events.STATE_UPDATE, this::notifyEngagementStateUpdate); + engagement.on(Engagement.Events.STATE_UPDATE, engagementState -> { + notifyEngagementStateUpdate(engagementState); + }); engagement.on(Engagement.Events.END, () -> onEngagementEnded(engagement)); } @@ -75,7 +77,7 @@ public boolean isOperatorPresent() { private void notifyOperatorUpdate(Operator operator) { if (operator != null) { - operatorRepository.addOrUpdateOperator(operator); + operatorRepository.emit(operator); } operatorProcessor.onNext(Optional.ofNullable(operator)); } @@ -91,20 +93,25 @@ private void notifyEngagementStateEventUpdate(EngagementStateEvent engagementSta private @Nullable Operator getOperator() { return operatorProcessor - .getValue() - .orElse(null); + .getValue() + .orElse(null); } - private EngagementStateEvent mapToEngagementStateChangeEvent( + @VisibleForTesting + protected EngagementStateEvent mapToEngagementStateChangeEvent( EngagementState engagementState, @Nullable Operator operator ) { - if (engagementState == null) { + if (engagementState == null && isOngoingEngagement) { + isOngoingEngagement = false; return new EngagementStateEvent.EngagementEndedEvent(); + } else if (engagementState == null) { + return new EngagementStateEvent.NoEngagementEvent(); } else if (engagementState.getVisitorStatus() == EngagementState.VisitorStatus.TRANSFERRING) { return new EngagementStateEvent.EngagementTransferringEvent(); } else { if (operator == null) { + isOngoingEngagement = true; return new EngagementStateEvent.EngagementOperatorConnectedEvent(engagementState.getOperator()); } else { if (!engagementState.getOperator().getId().equals(operator.getId())) { @@ -124,6 +131,7 @@ private void updateOperatorOnEngagementStateChanged(EngagementStateEvent engagem notifyOperatorUpdate(visitor.visit(engagementStateEvent)); break; case ENGAGEMENT_TRANSFERRING: + case NO_ENGAGEMENT: case ENGAGEMENT_ENDED: notifyOperatorUpdate(null); break; diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java deleted file mode 100644 index c58b81f8f..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.glia.widgets.core.engagement; - -import androidx.annotation.NonNull; -import androidx.collection.SimpleArrayMap; -import androidx.core.util.Consumer; - -import com.glia.androidsdk.Operator; -import com.glia.widgets.di.GliaCore; - -public class GliaOperatorRepository { - private final GliaCore gliaCore; - - private final SimpleArrayMap cachedOperators = new SimpleArrayMap<>(); - - public GliaOperatorRepository(GliaCore gliaCore) { - this.gliaCore = gliaCore; - } - - public void getOperatorById(@NonNull String operatorId, @NonNull Consumer callback) { - Operator cachedOperator = cachedOperators.get(operatorId); - if (cachedOperator != null) { - callback.accept(cachedOperator); - return; - } - - gliaCore.getOperator(operatorId, (operator, error) -> { - if (operator != null) { - addOrUpdateOperator(operator); - } - callback.accept(operator); - }); - } - - public void addOrUpdateOperator(@NonNull Operator operator) { - cachedOperators.put(operator.getId(), operator); - } - -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt new file mode 100644 index 000000000..c4d0abb32 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt @@ -0,0 +1,42 @@ +package com.glia.widgets.core.engagement + +import androidx.annotation.VisibleForTesting +import androidx.collection.SimpleArrayMap +import androidx.core.util.Consumer +import com.glia.androidsdk.Operator +import com.glia.widgets.core.engagement.data.LocalOperator +import com.glia.widgets.di.GliaCore +import com.glia.widgets.helper.imageUrl + +internal class GliaOperatorRepository(private val gliaCore: GliaCore) { + private val cachedOperators = SimpleArrayMap() + + @VisibleForTesting + var operatorDefaultImageUrl: String? = null + + fun getOperatorById(operatorId: String, callback: Consumer) { + val cachedOperator = cachedOperators[operatorId] + if (cachedOperator != null) { + callback.accept(cachedOperator) + return + } + gliaCore.getOperator(operatorId) { operator: Operator?, _ -> + operator?.let { mapOperator(it) }?.also { putOperator(it) }.also { callback.accept(it) } + } + } + + fun emit(operator: Operator) = putOperator(mapOperator(operator)) + + @VisibleForTesting + fun mapOperator(operator: Operator): LocalOperator = operator.run { LocalOperator(id, name, imageUrl ?: operatorDefaultImageUrl) } + + @VisibleForTesting + fun putOperator(operator: LocalOperator) { + operator.apply { cachedOperators.put(id, this) } + } + + fun updateOperatorDefaultImageUrl(imageUrl: String?) { + operatorDefaultImageUrl = imageUrl + } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt new file mode 100644 index 000000000..d5e515762 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt @@ -0,0 +1,3 @@ +package com.glia.widgets.core.engagement.data + +internal data class LocalOperator(val id: String, val name: String, val imageUrl: String?) diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java index 9b3076069..dc63d30a5 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java @@ -2,8 +2,8 @@ import androidx.annotation.NonNull; -import com.glia.androidsdk.Operator; import com.glia.widgets.core.engagement.GliaOperatorRepository; +import com.glia.widgets.core.engagement.data.LocalOperator; import java.util.Optional; @@ -16,11 +16,11 @@ public GetOperatorUseCase(GliaOperatorRepository gliaOperatorRepository) { this.gliaOperatorRepository = gliaOperatorRepository; } - public Single> execute(@NonNull String operatorId) { + public Single> execute(@NonNull String operatorId) { return Single.create(emitter -> - gliaOperatorRepository.getOperatorById(operatorId, operator -> - emitter.onSuccess(Optional.ofNullable(operator)) - ) + gliaOperatorRepository.getOperatorById(operatorId, operator -> + emitter.onSuccess(Optional.ofNullable(operator)) + ) ); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java index 82bc59b57..8be154da2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java @@ -3,6 +3,7 @@ import com.glia.androidsdk.omnicore.OmnicoreEngagement; import com.glia.widgets.core.engagement.GliaEngagementRepository; import com.glia.widgets.core.engagement.GliaEngagementStateRepository; +import com.glia.widgets.core.engagement.GliaOperatorRepository; import com.glia.widgets.core.operator.GliaOperatorMediaRepository; import com.glia.widgets.core.queue.GliaQueueRepository; import com.glia.widgets.core.visitor.GliaVisitorMediaRepository; @@ -11,29 +12,28 @@ public class GliaOnEngagementUseCase implements Consumer { - public interface Listener { - void newEngagementLoaded(OmnicoreEngagement engagement); - } - private final GliaEngagementRepository gliaRepository; private final GliaOperatorMediaRepository operatorMediaRepository; private final GliaQueueRepository gliaQueueRepository; private final GliaVisitorMediaRepository gliaVisitorMediaRepository; private final GliaEngagementStateRepository gliaEngagementStateRepository; + private final GliaOperatorRepository operatorRepository; private Listener listener; public GliaOnEngagementUseCase( - GliaEngagementRepository gliaRepository, - GliaOperatorMediaRepository operatorMediaRepository, - GliaQueueRepository gliaQueueRepository, - GliaVisitorMediaRepository gliaVisitorMediaRepository, - GliaEngagementStateRepository gliaEngagementStateRepository + GliaEngagementRepository gliaRepository, + GliaOperatorMediaRepository operatorMediaRepository, + GliaQueueRepository gliaQueueRepository, + GliaVisitorMediaRepository gliaVisitorMediaRepository, + GliaEngagementStateRepository gliaEngagementStateRepository, + GliaOperatorRepository operatorRepository ) { this.gliaRepository = gliaRepository; this.operatorMediaRepository = operatorMediaRepository; this.gliaQueueRepository = gliaQueueRepository; this.gliaVisitorMediaRepository = gliaVisitorMediaRepository; this.gliaEngagementStateRepository = gliaEngagementStateRepository; + this.operatorRepository = operatorRepository; } public void execute(Listener listener) { @@ -47,6 +47,7 @@ public void execute(Listener listener) { @Override public void accept(OmnicoreEngagement engagement) { + operatorRepository.emit(engagement.getState().getOperator()); operatorMediaRepository.onEngagementStarted(engagement); gliaVisitorMediaRepository.onEngagementStarted(engagement); gliaEngagementStateRepository.onEngagementStarted(engagement); @@ -62,4 +63,8 @@ public void unregisterListener(Listener listener) { this.listener = null; } } + + public interface Listener { + void newEngagementLoaded(OmnicoreEngagement engagement); + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt index 651c7b335..555784202 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt @@ -1,28 +1,24 @@ package com.glia.widgets.core.engagement.domain -import com.glia.androidsdk.Operator import com.glia.androidsdk.chat.Chat import com.glia.androidsdk.chat.ChatMessage import com.glia.androidsdk.chat.OperatorMessage import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.helper.toChatMessageInternal import io.reactivex.Single +import kotlin.jvm.optionals.getOrNull internal class MapOperatorUseCase(private val getOperatorUseCase: GetOperatorUseCase) { operator fun invoke(chatMessage: ChatMessage): Single = when (chatMessage.senderType) { - Chat.Participant.OPERATOR -> Single.just(chatMessage) - .cast(OperatorMessage::class.java) - .flatMap { mapOperator(it) } - - else -> Single.just(chatMessage).map { ChatMessageInternal(it) } + Chat.Participant.OPERATOR -> processOperatorMessage(chatMessage as OperatorMessage) + else -> processVisitorMessage(chatMessage) } - private fun mapOperator(operatorMessage: OperatorMessage): Single { - return getOperatorUseCase.execute(operatorMessage.operatorId!!) - .map { map(operatorMessage, it.orElse(null)) } - } + private fun processOperatorMessage(chatMessage: OperatorMessage): Single = chatMessage + .takeIf { it.operatorImageUrl != null }?.toChatMessageInternal()?.let { Single.just(it) } + ?: getOperatorUseCase.execute(chatMessage.operatorId!!).map { ChatMessageInternal(chatMessage, it.getOrNull()) } + + private fun processVisitorMessage(chatMessage: ChatMessage): Single = Single.just(ChatMessageInternal(chatMessage)) - private fun map(operatorMessage: OperatorMessage, operator: Operator?): ChatMessageInternal { - return ChatMessageInternal(operatorMessage, operator) - } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt new file mode 100644 index 000000000..9e57381e6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt @@ -0,0 +1,14 @@ +package com.glia.widgets.core.engagement.domain + +import com.glia.widgets.chat.domain.SiteInfoUseCase +import com.glia.widgets.core.engagement.GliaOperatorRepository +import kotlin.jvm.optionals.getOrNull + +internal class UpdateOperatorDefaultImageUrlUseCase( + private val operatorRepository: GliaOperatorRepository, + private val siteInfoUseCase: SiteInfoUseCase +) { + operator fun invoke() = siteInfoUseCase.execute { response, _ -> + response.defaultOperatorPicture?.url?.getOrNull()?.also(operatorRepository::updateOperatorDefaultImageUrl) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java deleted file mode 100644 index 34ea8cf19..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.glia.widgets.core.engagement.domain.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.glia.androidsdk.Operator; -import com.glia.androidsdk.chat.Chat; -import com.glia.androidsdk.chat.ChatMessage; - -import java.util.Optional; - -public class ChatMessageInternal { - @NonNull - private final ChatMessage chatMessage; - @Nullable - private final Operator operator; - - public ChatMessageInternal(@NonNull ChatMessage chatMessage, @Nullable Operator operator) { - this.chatMessage = chatMessage; - this.operator = operator; - } - - public ChatMessageInternal(ChatMessage chatMessage) { - this(chatMessage, null); - } - - @NonNull - public ChatMessage getChatMessage() { - return chatMessage; - } - - public Optional getOperator() { - return Optional.ofNullable(operator); - } - - public Optional getOperatorId() { - return getOperator().map(Operator::getId); - } - - public Optional getOperatorName() { - return getOperator().map(Operator::getName); - } - - public Optional getOperatorImageUrl() { - return getOperator().map(Operator::getPicture).flatMap(Operator.Picture::getURL); - } - - public boolean isNotVisitor() { - return getChatMessage().getSenderType() != Chat.Participant.VISITOR; - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt new file mode 100644 index 000000000..55aa0e97b --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt @@ -0,0 +1,12 @@ +package com.glia.widgets.core.engagement.domain.model + +import com.glia.androidsdk.chat.Chat +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.core.engagement.data.LocalOperator + +internal data class ChatMessageInternal(val chatMessage: ChatMessage, val operator: LocalOperator? = null) { + val operatorId: String? get() = operator?.id + val operatorName: String? get() = operator?.name + val operatorImageUrl: String? get() = operator?.imageUrl + val isNotVisitor: Boolean get() = chatMessage.senderType != Chat.Participant.VISITOR +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java index 768973b0b..26a272398 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java @@ -9,6 +9,7 @@ enum Type { ENGAGEMENT_ONGOING, ENGAGEMENT_OPERATOR_CONNECTED, ENGAGEMENT_OPERATOR_CHANGED, + NO_ENGAGEMENT } Type getType(); @@ -94,6 +95,7 @@ public T accept(EngagementStateEventVisitor visitor) { } class EngagementEndedEvent implements EngagementStateEvent { + @Override public Type getType() { return Type.ENGAGEMENT_ENDED; @@ -104,6 +106,19 @@ public T accept(EngagementStateEventVisitor visitor) { return visitor.visit(this); } } + + class NoEngagementEvent implements EngagementStateEvent { + + @Override + public Type getType() { + return Type.NO_ENGAGEMENT; + } + + @Override + public T accept(EngagementStateEventVisitor visitor) { + return visitor.visit(this); + } + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt index 38640f4bb..802bed81a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt @@ -11,7 +11,9 @@ import com.glia.widgets.core.engagement.exception.EngagementMissingException import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.di.GliaCore -import java.util.* +import java.util.Observable +import java.util.Observer +import kotlin.jvm.optionals.getOrNull class FileAttachmentRepository( private val gliaCore: GliaCore, @@ -45,7 +47,7 @@ class FileAttachmentRepository( } fun uploadFile(file: FileAttachment, listener: AddFileToAttachmentAndUploadUseCase.Listener) { - val engagement = gliaCore.currentEngagement.orElse(null) + val engagement = gliaCore.currentEngagement.getOrNull() if (engagement != null) { engagement.uploadFile(file.uri, handleFileUpload(file, listener)) } else if (engagementConfigRepository.chatType == ChatType.SECURE_MESSAGING) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt index 52ed196a1..571c8735c 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt @@ -10,6 +10,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.di.GliaCore import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject +import kotlin.jvm.optionals.getOrNull class SecureFileAttachmentRepository(private val gliaCore: GliaCore) { private val secureConversations: SecureConversations by lazy { @@ -43,7 +44,7 @@ class SecureFileAttachmentRepository(private val gliaCore: GliaCore) { } fun uploadFile(file: FileAttachment, listener: AddFileToAttachmentAndUploadUseCase.Listener) { - val engagement = gliaCore.currentEngagement.orElse(null) + val engagement = gliaCore.currentEngagement.getOrNull() if (engagement != null) { engagement.uploadFile(file.uri, handleFileUpload(file, listener)) } else { diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt index 3af8947c8..b7b7e3625 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt @@ -11,7 +11,7 @@ import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject -class SecureConversationsRepository(private val secureConversations: SecureConversations) { +internal class SecureConversationsRepository(private val secureConversations: SecureConversations) { private val _messageSendingObservable: Subject = BehaviorSubject.createDefault(false) val messageSendingObservable: Observable = _messageSendingObservable diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt index d6324446e..c26c8cdc0 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt @@ -5,7 +5,7 @@ import com.glia.androidsdk.queuing.Queue import com.glia.androidsdk.queuing.QueueState import com.glia.widgets.core.queue.GliaQueueRepository import com.glia.widgets.helper.rx.Schedulers -import com.glia.widgets.helper.supportsMessaging +import com.glia.widgets.helper.supportMessaging import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject @@ -36,5 +36,5 @@ internal class IsMessagingAvailableUseCase( .filter { queueIds.contains(it.id) } .filterNot { it.state.status == QueueState.Status.CLOSED } .filterNot { it.state.status == QueueState.Status.UNKNOWN } - .any { it.supportsMessaging() } + .any { it.supportMessaging() } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt index 901b2df52..d2486a86b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt @@ -7,7 +7,7 @@ import com.glia.widgets.helper.rx.Schedulers import com.glia.widgets.messagecenter.MessageCenterState import io.reactivex.Observable -class SendMessageButtonStateUseCase( +internal class SendMessageButtonStateUseCase( private val sendMessageRepository: SendMessageRepository, private val fileAttachmentRepository: SecureFileAttachmentRepository, private val secureConversationsRepository: SecureConversationsRepository, diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt index 1d66dba94..b4e8477d7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt @@ -10,7 +10,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.SendMessageRepository -class SendSecureMessageUseCase( +internal class SendSecureMessageUseCase( private val queueId: String, private val sendMessageRepository: SendMessageRepository, private val secureConversationsRepository: SecureConversationsRepository, diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index 2f40bf5fa..fc483182a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -47,6 +47,7 @@ public class ControllerFactory { private final GliaSdkConfigurationManager sdkConfigurationManager; private final FilePreviewController filePreviewController; private final ChatHeadPosition chatHeadPosition; + private final ManagerFactory managerFactory; private ChatController retainedChatController; private CallController retainedCallController; private ScreenSharingController retainedScreenSharingController; @@ -56,87 +57,84 @@ public class ControllerFactory { private ActivityWatcherForChatHeadController activityWatcherForChatHeadController; public ControllerFactory( - RepositoryFactory repositoryFactory, - UseCaseFactory useCaseFactory, - GliaSdkConfigurationManager sdkConfigurationManager + RepositoryFactory repositoryFactory, + UseCaseFactory useCaseFactory, + GliaSdkConfigurationManager sdkConfigurationManager, + ManagerFactory managerFactory ) { this.repositoryFactory = repositoryFactory; messagesNotSeenHandler = new MessagesNotSeenHandler( - useCaseFactory.createGliaOnMessageUseCase(), - useCaseFactory.createOnEngagementEndUseCase() + useCaseFactory.createGliaOnMessageUseCase(), + useCaseFactory.createOnEngagementEndUseCase() ); this.useCaseFactory = useCaseFactory; this.dialogController = new DialogController( - useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), - useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() + useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), + useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() ); this.filePreviewController = new FilePreviewController( - useCaseFactory.createGetImageFileFromDownloadsUseCase(), - useCaseFactory.createGetImageFileFromCacheUseCase(), - useCaseFactory.createPutImageFileToDownloadsUseCase(), - useCaseFactory.createOnEngagementEndUseCase() + useCaseFactory.createGetImageFileFromDownloadsUseCase(), + useCaseFactory.createGetImageFileFromCacheUseCase(), + useCaseFactory.createPutImageFileToDownloadsUseCase(), + useCaseFactory.createOnEngagementEndUseCase() ); this.chatHeadPosition = ChatHeadPosition.getInstance(); this.sdkConfigurationManager = sdkConfigurationManager; + this.managerFactory = managerFactory; } public ChatController getChatController(ChatViewCallback chatViewCallback) { if (retainedChatController == null) { Logger.d(TAG, "new for chat activity"); retainedChatController = new ChatController( - chatViewCallback, - repositoryFactory.getMediaUpgradeOfferRepository(), - sharedTimer, - minimizeHandler, - dialogController, - messagesNotSeenHandler, - useCaseFactory.createCallNotificationUseCase(), - useCaseFactory.createGliaLoadHistoryUseCase(), - useCaseFactory.createQueueForChatEngagementUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createGliaOnMessageUseCase(), - useCaseFactory.createGliaOnOperatorTypingUseCase(), - useCaseFactory.createGliaSendMessagePreviewUseCase(), - useCaseFactory.createGliaSendMessageUseCase(), - useCaseFactory.createAddOperatorMediaStateListenerUseCase(), - useCaseFactory.createCancelQueueTicketUseCase(), - useCaseFactory.createEndEngagementUseCase(), - useCaseFactory.createAddFileToAttachmentAndUploadUseCase(), - useCaseFactory.createAddFileAttachmentsObserverUseCase(), - useCaseFactory.createRemoveFileAttachmentObserverUseCase(), - useCaseFactory.createGetFileAttachmentsUseCase(), - useCaseFactory.createRemoveFileAttachmentUseCase(), - useCaseFactory.createSupportedFileCountCheckUseCase(), - useCaseFactory.createIsShowSendButtonUseCase(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), - useCaseFactory.createDownloadFileUseCase(), - useCaseFactory.createIsEnableChatEditTextUseCase(), - useCaseFactory.createSiteInfoUseCase(), - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), - useCaseFactory.createIsFromCallScreenUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase(), - useCaseFactory.createCustomCardAdapterTypeUseCase(), - useCaseFactory.createCustomCardTypeUseCase(), - useCaseFactory.createCustomCardShouldShowUseCase(), - useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), - useCaseFactory.createIsQueueingEngagementUseCase(), - useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createIsSecureEngagementUseCase(), - useCaseFactory.createIsOngoingEngagementUseCase(), - useCaseFactory.createSetEngagementConfigUseCase(), - useCaseFactory.createIsSecureConversationsChatAvailableUseCase(), - useCaseFactory.createMarkMessagesReadUseCase(), - useCaseFactory.createHasPendingSurveyUseCase(), - useCaseFactory.createSetPendingSurveyUsed(), - useCaseFactory.createIsCallVisualizerUseCase(), - useCaseFactory.createPreEngagementMessageUseCase(), - useCaseFactory.createAddNewMessagesDividerUseCase(), - useCaseFactory.createIsFileReadyForPreviewUseCase(), - useCaseFactory.createAcceptMediaUpgradeOfferUseCase() + chatViewCallback, + repositoryFactory.getMediaUpgradeOfferRepository(), + sharedTimer, + minimizeHandler, + dialogController, + messagesNotSeenHandler, + useCaseFactory.createCallNotificationUseCase(), + useCaseFactory.createQueueForChatEngagementUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createGliaOnOperatorTypingUseCase(), + useCaseFactory.createGliaSendMessagePreviewUseCase(), + useCaseFactory.createGliaSendMessageUseCase(), + useCaseFactory.createAddOperatorMediaStateListenerUseCase(), + useCaseFactory.createCancelQueueTicketUseCase(), + useCaseFactory.createEndEngagementUseCase(), + useCaseFactory.createAddFileToAttachmentAndUploadUseCase(), + useCaseFactory.createAddFileAttachmentsObserverUseCase(), + useCaseFactory.createRemoveFileAttachmentObserverUseCase(), + useCaseFactory.createGetFileAttachmentsUseCase(), + useCaseFactory.createRemoveFileAttachmentUseCase(), + useCaseFactory.createSupportedFileCountCheckUseCase(), + useCaseFactory.createIsShowSendButtonUseCase(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), + useCaseFactory.createDownloadFileUseCase(), + useCaseFactory.createSiteInfoUseCase(), + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), + useCaseFactory.createIsFromCallScreenUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase(), + useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), + useCaseFactory.createIsQueueingEngagementUseCase(), + useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createIsSecureEngagementUseCase(), + useCaseFactory.createIsOngoingEngagementUseCase(), + useCaseFactory.createSetEngagementConfigUseCase(), + useCaseFactory.createIsSecureConversationsChatAvailableUseCase(), + useCaseFactory.createHasPendingSurveyUseCase(), + useCaseFactory.createSetPendingSurveyUsed(), + useCaseFactory.createIsCallVisualizerUseCase(), + useCaseFactory.createIsFileReadyForPreviewUseCase(), + useCaseFactory.createAcceptMediaUpgradeOfferUseCase(), + useCaseFactory.createDetermineGvaButtonTypeUseCase(), + useCaseFactory.createIsAuthenticatedUseCase(), + useCaseFactory.createUpdateOperatorDefaultImageUrlUseCase(), + managerFactory.getChatManager() ); } else { Logger.d(TAG, "retained chat controller"); @@ -149,42 +147,42 @@ public CallController getCallController(CallViewCallback callViewCallback) { if (retainedCallController == null) { Logger.d(TAG, "new call controller"); retainedCallController = new CallController( - sdkConfigurationManager, - repositoryFactory.getMediaUpgradeOfferRepository(), - sharedTimer, - callViewCallback, - new TimeCounter(), - new TimeCounter(), - minimizeHandler, - dialogController, - messagesNotSeenHandler, - useCaseFactory.createCallNotificationUseCase(), - useCaseFactory.createQueueForMediaEngagementUseCase(), - useCaseFactory.createCancelQueueTicketUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createAddOperatorMediaStateListenerUseCase(), - useCaseFactory.createRemoveOperatorMediaStateListenerUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createEndEngagementUseCase(), - useCaseFactory.createShouldShowMediaEngagementViewUseCase(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), - useCaseFactory.createHasCallNotificationChannelEnabledUseCase(), - useCaseFactory.createIsShowEnableCallNotificationChannelDialogUseCase(), - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createToggleVisitorAudioMediaMuteUseCase(), - useCaseFactory.createToggleVisitorVideoUseCase(), - useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase(), - useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), - useCaseFactory.createIsCallVisualizerUseCase(), - useCaseFactory.createIsOngoingEngagementUseCase(), - useCaseFactory.createSetPendingSurveyUsed(), - useCaseFactory.createTurnSpeakerphoneUseCase(), - useCaseFactory.createHandleCallPermissionsUseCase()); + sdkConfigurationManager, + repositoryFactory.getMediaUpgradeOfferRepository(), + sharedTimer, + callViewCallback, + new TimeCounter(), + new TimeCounter(), + minimizeHandler, + dialogController, + messagesNotSeenHandler, + useCaseFactory.createCallNotificationUseCase(), + useCaseFactory.createQueueForMediaEngagementUseCase(), + useCaseFactory.createCancelQueueTicketUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createAddOperatorMediaStateListenerUseCase(), + useCaseFactory.createRemoveOperatorMediaStateListenerUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createEndEngagementUseCase(), + useCaseFactory.createShouldShowMediaEngagementViewUseCase(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), + useCaseFactory.createHasCallNotificationChannelEnabledUseCase(), + useCaseFactory.createIsShowEnableCallNotificationChannelDialogUseCase(), + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createToggleVisitorAudioMediaMuteUseCase(), + useCaseFactory.createToggleVisitorVideoUseCase(), + useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase(), + useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), + useCaseFactory.createIsCallVisualizerUseCase(), + useCaseFactory.createIsOngoingEngagementUseCase(), + useCaseFactory.createSetPendingSurveyUsed(), + useCaseFactory.createTurnSpeakerphoneUseCase(), + useCaseFactory.createHandleCallPermissionsUseCase()); } else { Logger.d(TAG, "retained call controller"); retainedCallController.setViewCallback(callViewCallback); @@ -196,12 +194,12 @@ public ScreenSharingController getScreenSharingController() { if (retainedScreenSharingController == null) { Logger.d(TAG, "new screen sharing controller"); retainedScreenSharingController = new ScreenSharingController( - repositoryFactory.getGliaScreenSharingRepository(), - dialogController, - useCaseFactory.createShowScreenSharingNotificationUseCase(), - useCaseFactory.createRemoveScreenSharingNotificationUseCase(), - useCaseFactory.createHasScreenSharingNotificationChannelEnabledUseCase(), - sdkConfigurationManager + repositoryFactory.getGliaScreenSharingRepository(), + dialogController, + useCaseFactory.createShowScreenSharingNotificationUseCase(), + useCaseFactory.createRemoveScreenSharingNotificationUseCase(), + useCaseFactory.createHasScreenSharingNotificationChannelEnabledUseCase(), + sdkConfigurationManager ); } return retainedScreenSharingController; @@ -237,8 +235,8 @@ public DialogController getDialogController() { public DialogController createDialogController() { return new DialogController( - useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), - useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() + useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), + useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() ); } @@ -256,19 +254,19 @@ public FilePreviewController getImagePreviewController() { public ServiceChatHeadController getChatHeadController() { if (serviceChatHeadController == null) { serviceChatHeadController = new ServiceChatHeadController( - useCaseFactory.getToggleChatHeadServiceUseCase(), - useCaseFactory.getResolveChatHeadNavigationUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - messagesNotSeenHandler, - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - chatHeadPosition, - useCaseFactory.createGetOperatorFlowableUseCase(), - useCaseFactory.createSetPendingSurveyUseCase(), - useCaseFactory.createIsCallVisualizerScreenSharingUseCase() + useCaseFactory.getToggleChatHeadServiceUseCase(), + useCaseFactory.getResolveChatHeadNavigationUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + messagesNotSeenHandler, + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + chatHeadPosition, + useCaseFactory.createGetOperatorFlowableUseCase(), + useCaseFactory.createSetPendingSurveyUseCase(), + useCaseFactory.createIsCallVisualizerScreenSharingUseCase() ); } return serviceChatHeadController; @@ -277,18 +275,18 @@ public ServiceChatHeadController getChatHeadController() { public ApplicationChatHeadLayoutController getChatHeadLayoutController() { if (applicationChatHeadController == null) { applicationChatHeadController = new ApplicationChatHeadLayoutController( - useCaseFactory.getIsDisplayApplicationChatHeadUseCase(), - useCaseFactory.getResolveChatHeadNavigationUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - messagesNotSeenHandler, - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createGetOperatorFlowableUseCase(), - useCaseFactory.createSetPendingSurveyUseCase(), - useCaseFactory.createIsCallVisualizerScreenSharingUseCase() + useCaseFactory.getIsDisplayApplicationChatHeadUseCase(), + useCaseFactory.getResolveChatHeadNavigationUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + messagesNotSeenHandler, + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createGetOperatorFlowableUseCase(), + useCaseFactory.createSetPendingSurveyUseCase(), + useCaseFactory.createIsCallVisualizerScreenSharingUseCase() ); } return applicationChatHeadController; @@ -297,7 +295,7 @@ public ApplicationChatHeadLayoutController getChatHeadLayoutController() { public SurveyContract.Controller getSurveyController() { if (surveyController == null) { surveyController = new SurveyController( - useCaseFactory.getSurveyAnswerUseCase() + useCaseFactory.getSurveyAnswerUseCase() ); } return surveyController; @@ -306,12 +304,12 @@ public SurveyContract.Controller getSurveyController() { public CallVisualizerController getCallVisualizerController() { if (callVisualizerController == null) { callVisualizerController = new CallVisualizerController( - repositoryFactory.getCallVisualizerRepository(), - dialogController, - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - useCaseFactory.createIsCallOrChatScreenActiveUseCase() + repositoryFactory.getCallVisualizerRepository(), + dialogController, + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + useCaseFactory.createIsCallOrChatScreenActiveUseCase() ); } return callVisualizerController; @@ -319,29 +317,29 @@ public CallVisualizerController getCallVisualizerController() { public FloatingVisitorVideoContract.Controller getFloatingVisitorVideoController() { return new FloatingVisitorVideoController( - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createIsShowVideoUseCase(), - useCaseFactory.createIsShowOnHoldUseCase() + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createIsShowVideoUseCase(), + useCaseFactory.createIsShowOnHoldUseCase() ); } public MessageCenterContract.Controller getMessageCenterController(String queueId) { return new MessageCenterController( - serviceChatHeadController, - useCaseFactory.createSendSecureMessageUseCase(queueId), - useCaseFactory.createIsMessageCenterAvailableUseCase(queueId), - useCaseFactory.createAddSecureFileAttachmentsObserverUseCase(), - useCaseFactory.createAddSecureFileToAttachmentAndUploadUseCase(), - useCaseFactory.createGetSecureFileAttachmentsUseCase(), - useCaseFactory.createRemoveSecureFileAttachmentUseCase(), - useCaseFactory.createIsAuthenticatedUseCase(), - useCaseFactory.createSiteInfoUseCase(), - useCaseFactory.createOnNextMessageUseCase(), - useCaseFactory.createEnableSendMessageButtonUseCase(), - useCaseFactory.createShowMessageLimitErrorUseCase(), - useCaseFactory.createResetMessageCenterUseCase(), - createDialogController() + serviceChatHeadController, + useCaseFactory.createSendSecureMessageUseCase(queueId), + useCaseFactory.createIsMessageCenterAvailableUseCase(queueId), + useCaseFactory.createAddSecureFileAttachmentsObserverUseCase(), + useCaseFactory.createAddSecureFileToAttachmentAndUploadUseCase(), + useCaseFactory.createGetSecureFileAttachmentsUseCase(), + useCaseFactory.createRemoveSecureFileAttachmentUseCase(), + useCaseFactory.createIsAuthenticatedUseCase(), + useCaseFactory.createSiteInfoUseCase(), + useCaseFactory.createOnNextMessageUseCase(), + useCaseFactory.createEnableSendMessageButtonUseCase(), + useCaseFactory.createShowMessageLimitErrorUseCase(), + useCaseFactory.createResetMessageCenterUseCase(), + createDialogController() ); } @@ -351,17 +349,17 @@ public EndScreenSharingContract.Controller getEndScreenSharingController() { public VisitorCodeContract.Controller getVisitorCodeController() { return new VisitorCodeController( - dialogController, - repositoryFactory.getVisitorCodeRepository(), - repositoryFactory.getGliaEngagementRepository()); + dialogController, + repositoryFactory.getVisitorCodeRepository(), + repositoryFactory.getGliaEngagementRepository()); } public ActivityWatcherForCallVisualizerContract.Controller getActivityWatcherForCallVisualizerController() { if (activityWatcherforCallVisualizerController == null) { activityWatcherforCallVisualizerController = new ActivityWatcherForCallVisualizerController( - getCallVisualizerController(), - getScreenSharingController(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase()); + getCallVisualizerController(), + getScreenSharingController(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase()); } return activityWatcherforCallVisualizerController; } @@ -369,19 +367,19 @@ public ActivityWatcherForCallVisualizerContract.Controller getActivityWatcherFor public ActivityWatcherForChatHeadContract.Controller getActivityWatcherForChatHeadController() { if (activityWatcherForChatHeadController == null) { activityWatcherForChatHeadController = new ActivityWatcherForChatHeadController( - serviceChatHeadController, - getChatHeadLayoutController(), - getScreenSharingController(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createIsFromCallScreenUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase()); + serviceChatHeadController, + getChatHeadLayoutController(), + getScreenSharingController(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createIsFromCallScreenUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase()); } return activityWatcherForChatHeadController; } public PermissionsRequestContract.Controller getPermissionsController() { return new PermissionsRequestController( - repositoryFactory.getPermissionsRequestRepository() + repositoryFactory.getPermissionsRequestRepository() ); } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java b/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java index dffbdcdd8..ea95f13d2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java @@ -1,7 +1,6 @@ package com.glia.widgets.di; import android.app.Application; -import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; @@ -19,40 +18,35 @@ import com.glia.widgets.core.dialog.PermissionDialogManager; import com.glia.widgets.core.notification.device.INotificationManager; import com.glia.widgets.core.notification.device.NotificationManager; -import com.glia.widgets.permissions.ActivityWatcherForPermissionsRequest; import com.glia.widgets.core.permissions.PermissionManager; import com.glia.widgets.filepreview.data.source.local.DownloadsFolderDataSource; import com.glia.widgets.helper.ApplicationLifecycleManager; import com.glia.widgets.helper.Logger; import com.glia.widgets.helper.ResourceProvider; import com.glia.widgets.helper.rx.GliaWidgetsSchedulers; +import com.glia.widgets.permissions.ActivityWatcherForPermissionsRequest; import com.glia.widgets.view.head.ActivityWatcherForChatHead; import com.glia.widgets.view.head.controller.ServiceChatHeadController; import com.glia.widgets.view.unifiedui.theme.UnifiedThemeManager; -import kotlin.jvm.functions.Function2; - public class Dependencies { private final static String TAG = "Dependencies"; - + private static final UnifiedThemeManager UNIFIED_THEME_MANAGER = new UnifiedThemeManager(); private static ControllerFactory controllerFactory; private static INotificationManager notificationManager; private static CallVisualizerManager callVisualizerManager; private static GliaSdkConfigurationManager sdkConfigurationManager = new GliaSdkConfigurationManager(); private static UseCaseFactory useCaseFactory; + private static ManagerFactory managerFactory; private static GliaCore gliaCore = new GliaCoreImpl(); private static ResourceProvider resourceProvider; - private static final UnifiedThemeManager UNIFIED_THEME_MANAGER = new UnifiedThemeManager(); public static void onAppCreate(Application application) { notificationManager = new NotificationManager(application); DownloadsFolderDataSource downloadsFolderDataSource = new DownloadsFolderDataSource(application); - RepositoryFactory repositoryFactory = new RepositoryFactory( - gliaCore, - downloadsFolderDataSource - ); + RepositoryFactory repositoryFactory = new RepositoryFactory(gliaCore, downloadsFolderDataSource); PermissionManager permissionManager = new PermissionManager( application, @@ -72,20 +66,12 @@ public static void onAppCreate(Application application) { new GliaWidgetsSchedulers(), gliaCore ); - initAudioControlManager( - audioControlManager, - useCaseFactory.createOnAudioStartedUseCase() - ); + initAudioControlManager(audioControlManager, useCaseFactory.createOnAudioStartedUseCase()); - controllerFactory = new ControllerFactory( - repositoryFactory, - useCaseFactory, - sdkConfigurationManager - ); - initApplicationLifecycleObserver( - new ApplicationLifecycleManager(), - controllerFactory.getChatHeadController() - ); + managerFactory = new ManagerFactory(useCaseFactory); + + controllerFactory = new ControllerFactory(repositoryFactory, useCaseFactory, sdkConfigurationManager, managerFactory); + initApplicationLifecycleObserver(new ApplicationLifecycleManager(), controllerFactory.getChatHeadController()); ActivityWatcherForCallVisualizer activityWatcherForCallVisualizer = new ActivityWatcherForCallVisualizer( @@ -96,8 +82,7 @@ public static void onAppCreate(Application application) { ActivityWatcherForChatHead activityWatcherForChatHead = new ActivityWatcherForChatHead( - getControllerFactory().getActivityWatcherForChatHeadController() - ); + getControllerFactory().getActivityWatcherForChatHeadController()); application.registerActivityLifecycleCallbacks(activityWatcherForChatHead); ActivityWatcherForPermissionsRequest activityWatcherForPermissionsRequest = @@ -123,6 +108,11 @@ public static GliaSdkConfigurationManager getSdkConfigurationManager() { return sdkConfigurationManager; } + @VisibleForTesting + public static void setSdkConfigurationManager(@NonNull GliaSdkConfigurationManager sdkConfigurationManager) { + Dependencies.sdkConfigurationManager = sdkConfigurationManager; + } + @NonNull public static UnifiedThemeManager getGliaThemeManager() { return UNIFIED_THEME_MANAGER; @@ -141,6 +131,11 @@ public static ControllerFactory getControllerFactory() { return controllerFactory; } + @VisibleForTesting + public static void setControllerFactory(ControllerFactory controllerFactory) { + Dependencies.controllerFactory = controllerFactory; + } + public static GliaCore glia() { return gliaCore; } @@ -158,16 +153,6 @@ public static ResourceProvider getResourceProvider() { return resourceProvider; } - @VisibleForTesting - public static void setControllerFactory(ControllerFactory controllerFactory) { - Dependencies.controllerFactory = controllerFactory; - } - - @VisibleForTesting - public static void setSdkConfigurationManager(@NonNull GliaSdkConfigurationManager sdkConfigurationManager) { - Dependencies.sdkConfigurationManager = sdkConfigurationManager; - } - @VisibleForTesting public static void setResourceProvider(ResourceProvider resourceProvider) { Dependencies.resourceProvider = resourceProvider; diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt new file mode 100644 index 000000000..808d8e161 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt @@ -0,0 +1,20 @@ +package com.glia.widgets.di + +import com.glia.widgets.chat.ChatManager + +internal class ManagerFactory(private val useCaseFactory: UseCaseFactory) { + val chatManager: ChatManager + get() = useCaseFactory.run { + ChatManager( + onMessageUseCase = createGliaOnMessageUseCase(), + loadHistoryUseCase = createGliaLoadHistoryUseCase(), + addNewMessagesDividerUseCase = createAddNewMessagesDividerUseCase(), + markMessagesReadWithDelayUseCase = createMarkMessagesReadUseCase(), + appendHistoryChatMessageUseCase = createAppendHistoryChatMessageUseCase(), + appendNewChatMessageUseCase = createAppendNewChatMessageUseCase(), + sendUnsentMessagesUseCase = createSendUnsentMessagesUseCase(), + handleCustomCardClickUseCase = createHandleCustomCardClickUseCase(), + isAuthenticatedUseCase = createIsAuthenticatedUseCase() + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index 43ea3c552..431f58719 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -9,6 +9,17 @@ import com.glia.widgets.call.domain.ToggleVisitorVideoUseCase; import com.glia.widgets.callvisualizer.domain.IsCallOrChatScreenActiveUseCase; import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase; +import com.glia.widgets.chat.domain.AppendGvaMessageItemUseCase; +import com.glia.widgets.chat.domain.AppendHistoryChatMessageUseCase; +import com.glia.widgets.chat.domain.AppendHistoryCustomCardItemUseCase; +import com.glia.widgets.chat.domain.AppendHistoryOperatorChatItemUseCase; +import com.glia.widgets.chat.domain.AppendHistoryResponseCardOrTextItemUseCase; +import com.glia.widgets.chat.domain.AppendHistoryVisitorChatItemUseCase; +import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase; +import com.glia.widgets.chat.domain.AppendNewOperatorMessageUseCase; +import com.glia.widgets.chat.domain.AppendNewResponseCardOrTextItemUseCase; +import com.glia.widgets.chat.domain.AppendNewVisitorMessageUseCase; +import com.glia.widgets.chat.domain.AppendSystemMessageItemUseCase; import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase; import com.glia.widgets.chat.domain.CustomCardShouldShowUseCase; import com.glia.widgets.chat.domain.CustomCardTypeUseCase; @@ -19,14 +30,29 @@ import com.glia.widgets.chat.domain.GliaOnOperatorTypingUseCase; import com.glia.widgets.chat.domain.GliaSendMessagePreviewUseCase; import com.glia.widgets.chat.domain.GliaSendMessageUseCase; +import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase; import com.glia.widgets.chat.domain.IsAuthenticatedUseCase; -import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase; import com.glia.widgets.chat.domain.IsFromCallScreenUseCase; import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase; import com.glia.widgets.chat.domain.IsShowSendButtonUseCase; -import com.glia.widgets.chat.domain.PreEngagementMessageUseCase; +import com.glia.widgets.chat.domain.MapOperatorAttachmentUseCase; +import com.glia.widgets.chat.domain.MapOperatorPlainTextUseCase; +import com.glia.widgets.chat.domain.MapResponseCardUseCase; +import com.glia.widgets.chat.domain.MapVisitorAttachmentUseCase; +import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase; import com.glia.widgets.chat.domain.SiteInfoUseCase; import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase; +import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase; +import com.glia.widgets.chat.domain.gva.DetermineGvaUrlTypeUseCase; +import com.glia.widgets.chat.domain.gva.GetGvaTypeUseCase; +import com.glia.widgets.chat.domain.gva.IsGvaUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaGvaGalleryCardsUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaPersistentButtonsUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaQuickRepliesUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaResponseTextUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaUseCase; +import com.glia.widgets.chat.domain.gva.ParseGvaButtonsUseCase; +import com.glia.widgets.chat.domain.gva.ParseGvaGalleryCardsUseCase; import com.glia.widgets.core.audio.AudioControlManager; import com.glia.widgets.core.audio.domain.OnAudioStartedUseCase; import com.glia.widgets.core.audio.domain.TurnSpeakerphoneUseCase; @@ -61,6 +87,7 @@ import com.glia.widgets.core.engagement.domain.ResetSurveyUseCase; import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase; import com.glia.widgets.core.engagement.domain.ShouldShowMediaEngagementViewUseCase; +import com.glia.widgets.core.engagement.domain.UpdateOperatorDefaultImageUrlUseCase; import com.glia.widgets.core.fileupload.domain.AddFileAttachmentsObserverUseCase; import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase; import com.glia.widgets.core.fileupload.domain.GetFileAttachmentsUseCase; @@ -110,8 +137,11 @@ import com.glia.widgets.helper.rx.Schedulers; import com.glia.widgets.view.floatingvisitorvideoview.domain.IsShowOnHoldUseCase; import com.glia.widgets.view.floatingvisitorvideoview.domain.IsShowVideoUseCase; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; public class UseCaseFactory { + private static final SurveyStateManager surveyStateManager = new SurveyStateManager(); private static CallNotificationUseCase callNotificationUseCase; private static ShowScreenSharingNotificationUseCase showScreenSharingNotificationUseCase; private static RemoveScreenSharingNotificationUseCase removeScreenSharingNotificationUseCase; @@ -119,20 +149,20 @@ public class UseCaseFactory { private static IsDisplayApplicationChatHeadUseCase isDisplayApplicationChatHeadUseCase; private static ResolveChatHeadNavigationUseCase resolveChatHeadNavigationUseCase; private static VisitorCodeViewBuilderUseCase visitorCodeViewBuilderUseCase; - private static GliaQueueForChatEngagementUseCase gliaQueueForChatEngagementUseCase; private static GliaQueueForMediaEngagementUseCase gliaQueueForMediaEngagementUseCase; private final RepositoryFactory repositoryFactory; private final PermissionManager permissionManager; private final PermissionDialogManager permissionDialogManager; private final GliaSdkConfigurationManager gliaSdkConfigurationManager; - private static final SurveyStateManager surveyStateManager = new SurveyStateManager(); private final INotificationManager notificationManager; private final ChatHeadManager chatHeadManager; private final AudioControlManager audioControlManager; private final Schedulers schedulers; private final GliaCore gliaCore; + private Gson gvaGson; + public UseCaseFactory(RepositoryFactory repositoryFactory, PermissionManager permissionManager, PermissionDialogManager permissionDialogManager, @@ -153,17 +183,41 @@ public UseCaseFactory(RepositoryFactory repositoryFactory, this.gliaCore = gliaCore; } + @NonNull + private AppendHistoryResponseCardOrTextItemUseCase createAppendHistoryResponseCardOrTextItemUseCase() { + return new AppendHistoryResponseCardOrTextItemUseCase( + createMapOperatorAttachmentUseCase(), + createMapOperatorPlainTextUseCase(), + createMapResponseCardUseCase() + ); + } + + @NonNull + public MapResponseCardUseCase createMapResponseCardUseCase() { + return new MapResponseCardUseCase(); + } + + @NonNull + public MapOperatorAttachmentUseCase createMapOperatorAttachmentUseCase() { + return new MapOperatorAttachmentUseCase(); + } + + @NonNull + public MapOperatorPlainTextUseCase createMapOperatorPlainTextUseCase() { + return new MapOperatorPlainTextUseCase(); + } + @NonNull public ToggleChatHeadServiceUseCase getToggleChatHeadServiceUseCase() { if (toggleChatHeadServiceUseCase == null) { toggleChatHeadServiceUseCase = new ToggleChatHeadServiceUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaScreenSharingRepository(), - chatHeadManager, - permissionManager, - gliaSdkConfigurationManager, - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaScreenSharingRepository(), + chatHeadManager, + permissionManager, + gliaSdkConfigurationManager, + repositoryFactory.getGliaEngagementTypeRepository() ); } return toggleChatHeadServiceUseCase; @@ -173,12 +227,12 @@ public ToggleChatHeadServiceUseCase getToggleChatHeadServiceUseCase() { public IsDisplayApplicationChatHeadUseCase getIsDisplayApplicationChatHeadUseCase() { if (isDisplayApplicationChatHeadUseCase == null) { isDisplayApplicationChatHeadUseCase = new IsDisplayApplicationChatHeadUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaScreenSharingRepository(), - permissionManager, - gliaSdkConfigurationManager, - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaScreenSharingRepository(), + permissionManager, + gliaSdkConfigurationManager, + repositoryFactory.getGliaEngagementTypeRepository() ); } return isDisplayApplicationChatHeadUseCase; @@ -188,10 +242,10 @@ public IsDisplayApplicationChatHeadUseCase getIsDisplayApplicationChatHeadUseCas public ResolveChatHeadNavigationUseCase getResolveChatHeadNavigationUseCase() { if (resolveChatHeadNavigationUseCase == null) { resolveChatHeadNavigationUseCase = new ResolveChatHeadNavigationUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementTypeRepository(), - createIsCallVisualizerScreenSharingUseCase() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementTypeRepository(), + createIsCallVisualizerScreenSharingUseCase() ); } return resolveChatHeadNavigationUseCase; @@ -229,11 +283,11 @@ public RemoveScreenSharingNotificationUseCase createRemoveScreenSharingNotificat @NonNull public GliaLoadHistoryUseCase createGliaLoadHistoryUseCase() { return new GliaLoadHistoryUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getSecureConversationsRepository(), - createIsSecureEngagementUseCase(), - getMapOperatorUseCase(), - createSubscribeToUnreadMessagesCountUseCase() + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getSecureConversationsRepository(), + createIsSecureEngagementUseCase(), + getMapOperatorUseCase(), + createSubscribeToUnreadMessagesCountUseCase() ); } @@ -246,9 +300,9 @@ public MapOperatorUseCase getMapOperatorUseCase() { public GliaQueueForChatEngagementUseCase createQueueForChatEngagementUseCase() { if (gliaQueueForChatEngagementUseCase == null) { gliaQueueForChatEngagementUseCase = new GliaQueueForChatEngagementUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementRepository() + schedulers, + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementRepository() ); } return gliaQueueForChatEngagementUseCase; @@ -258,9 +312,9 @@ public GliaQueueForChatEngagementUseCase createQueueForChatEngagementUseCase() { public GliaQueueForMediaEngagementUseCase createQueueForMediaEngagementUseCase() { if (gliaQueueForMediaEngagementUseCase == null) { gliaQueueForMediaEngagementUseCase = new GliaQueueForMediaEngagementUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementRepository() + schedulers, + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementRepository() ); } return gliaQueueForMediaEngagementUseCase; @@ -269,8 +323,8 @@ public GliaQueueForMediaEngagementUseCase createQueueForMediaEngagementUseCase() @NonNull public GliaCancelQueueTicketUseCase createCancelQueueTicketUseCase() { return new GliaCancelQueueTicketUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository() + schedulers, + repositoryFactory.getGliaQueueRepository() ); } @@ -282,26 +336,27 @@ public GliaEndEngagementUseCase createEndEngagementUseCase() { @NonNull public GliaOnEngagementUseCase createOnEngagementUseCase() { return new GliaOnEngagementUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getGliaEngagementStateRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getGliaEngagementStateRepository(), + repositoryFactory.getOperatorRepository() ); } @NonNull public GliaOnEngagementEndUseCase createOnEngagementEndUseCase() { return new GliaOnEngagementEndUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - createOnEngagementUseCase(), - createCallNotificationUseCase(), - createRemoveScreenSharingNotificationUseCase(), - repositoryFactory.getGliaSurveyRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getEngagementConfigRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + createOnEngagementUseCase(), + createCallNotificationUseCase(), + createRemoveScreenSharingNotificationUseCase(), + repositoryFactory.getGliaSurveyRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getEngagementConfigRepository() ); } @@ -320,29 +375,19 @@ public SetPendingSurveyUsedUseCase createSetPendingSurveyUsed() { return new SetPendingSurveyUsedUseCase(surveyStateManager); } - @NonNull - public PreEngagementMessageUseCase createPreEngagementMessageUseCase() { - return new PreEngagementMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaEngagementRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase() - ); - } - @NonNull public GliaOnMessageUseCase createGliaOnMessageUseCase() { return new GliaOnMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase()); + repositoryFactory.getGliaMessageRepository(), + getMapOperatorUseCase() + ); } @NonNull public GliaOnOperatorTypingUseCase createGliaOnOperatorTypingUseCase() { return new GliaOnOperatorTypingUseCase( - repositoryFactory.getGliaMessageRepository(), - createOnEngagementUseCase() + repositoryFactory.getGliaMessageRepository(), + createOnEngagementUseCase() ); } @@ -354,35 +399,35 @@ public GliaSendMessagePreviewUseCase createGliaSendMessagePreviewUseCase() { @NonNull public GliaSendMessageUseCase createGliaSendMessageUseCase() { return new GliaSendMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - repositoryFactory.getGliaEngagementStateRepository(), - repositoryFactory.getEngagementConfigRepository(), - repositoryFactory.getSecureConversationsRepository(), - createIsSecureEngagementUseCase() + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + repositoryFactory.getGliaEngagementStateRepository(), + repositoryFactory.getEngagementConfigRepository(), + repositoryFactory.getSecureConversationsRepository(), + createIsSecureEngagementUseCase() ); } @NonNull public AddOperatorMediaStateListenerUseCase createAddOperatorMediaStateListenerUseCase() { return new AddOperatorMediaStateListenerUseCase( - repositoryFactory.getGliaOperatorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository() ); } @NonNull public RemoveOperatorMediaStateListenerUseCase createRemoveOperatorMediaStateListenerUseCase() { return new RemoveOperatorMediaStateListenerUseCase( - repositoryFactory.getGliaOperatorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository() ); } @NonNull public ShouldShowMediaEngagementViewUseCase createShouldShowMediaEngagementViewUseCase() { return new ShouldShowMediaEngagementViewUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementTypeRepository() ); } @@ -394,9 +439,9 @@ public AddFileAttachmentsObserverUseCase createAddFileAttachmentsObserverUseCase @NonNull public AddFileToAttachmentAndUploadUseCase createAddFileToAttachmentAndUploadUseCase() { return new AddFileToAttachmentAndUploadUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - repositoryFactory.getEngagementConfigRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + repositoryFactory.getEngagementConfigRepository() ); } @@ -423,9 +468,9 @@ public SupportedFileCountCheckUseCase createSupportedFileCountCheckUseCase() { @NonNull public IsShowSendButtonUseCase createIsShowSendButtonUseCase() { return new IsShowSendButtonUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - createIsSecureEngagementUseCase() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + createIsSecureEngagementUseCase() ); } @@ -472,8 +517,8 @@ public GetImageFileFromCacheUseCase createGetImageFileFromCacheUseCase() { @NonNull public GetImageFileFromNetworkUseCase createGetImageFileFromNetworkUseCase() { return new GetImageFileFromNetworkUseCase( - repositoryFactory.getGliaFileRepository(), - createDecodeSampledBitmapFromInputStreamUseCase() + repositoryFactory.getGliaFileRepository(), + createDecodeSampledBitmapFromInputStreamUseCase() ); } @@ -487,11 +532,6 @@ public DownloadFileUseCase createDownloadFileUseCase() { return new DownloadFileUseCase(repositoryFactory.getGliaFileRepository()); } - @NonNull - public IsEnableChatEditTextUseCase createIsEnableChatEditTextUseCase() { - return new IsEnableChatEditTextUseCase(); - } - @NonNull public OnNextMessageUseCase createOnNextMessageUseCase() { return new OnNextMessageUseCase(repositoryFactory.getSendMessageRepository()); @@ -500,27 +540,27 @@ public OnNextMessageUseCase createOnNextMessageUseCase() { @NonNull public SendMessageButtonStateUseCase createEnableSendMessageButtonUseCase() { return new SendMessageButtonStateUseCase( - repositoryFactory.getSendMessageRepository(), - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getSecureConversationsRepository(), - createShowMessageLimitErrorUseCase(), - schedulers + repositoryFactory.getSendMessageRepository(), + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getSecureConversationsRepository(), + createShowMessageLimitErrorUseCase(), + schedulers ); } @NonNull public ShowMessageLimitErrorUseCase createShowMessageLimitErrorUseCase() { return new ShowMessageLimitErrorUseCase( - repositoryFactory.getSendMessageRepository(), - schedulers + repositoryFactory.getSendMessageRepository(), + schedulers ); } @NonNull public ResetMessageCenterUseCase createResetMessageCenterUseCase() { return new ResetMessageCenterUseCase( - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getSendMessageRepository() + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getSendMessageRepository() ); } @@ -552,16 +592,16 @@ public RemoveVisitorMediaStateListenerUseCase createRemoveVisitorMediaStateListe @NonNull public ToggleVisitorAudioMediaMuteUseCase createToggleVisitorAudioMediaMuteUseCase() { return new ToggleVisitorAudioMediaMuteUseCase( - schedulers, - repositoryFactory.getGliaVisitorMediaRepository() + schedulers, + repositoryFactory.getGliaVisitorMediaRepository() ); } @NonNull public ToggleVisitorVideoUseCase createToggleVisitorVideoUseCase() { return new ToggleVisitorVideoUseCase( - schedulers, - repositoryFactory.getGliaVisitorMediaRepository() + schedulers, + repositoryFactory.getGliaVisitorMediaRepository() ); } @@ -648,12 +688,12 @@ public RemoveMediaUpgradeOfferCallbackUseCase createRemoveMediaUpgradeOfferCallb @NonNull public SendSecureMessageUseCase createSendSecureMessageUseCase(String queueId) { return new SendSecureMessageUseCase( - queueId, - repositoryFactory.getSendMessageRepository(), - repositoryFactory.getSecureConversationsRepository(), - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaEngagementRepository() + queueId, + repositoryFactory.getSendMessageRepository(), + repositoryFactory.getSecureConversationsRepository(), + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getGliaEngagementRepository() ); } @@ -670,8 +710,8 @@ public AddSecureFileToAttachmentAndUploadUseCase createAddSecureFileToAttachment @NonNull public AddSecureFileAttachmentsObserverUseCase createAddSecureFileAttachmentsObserverUseCase() { return new AddSecureFileAttachmentsObserverUseCase( - repositoryFactory.getSecureFileAttachmentRepository(), - schedulers + repositoryFactory.getSecureFileAttachmentRepository(), + schedulers ); } @@ -688,9 +728,9 @@ public RemoveSecureFileAttachmentUseCase createRemoveSecureFileAttachmentUseCase @NonNull public IsSecureEngagementUseCase createIsSecureEngagementUseCase() { return new IsSecureEngagementUseCase( - repositoryFactory.getEngagementConfigRepository(), - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository() + repositoryFactory.getEngagementConfigRepository(), + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository() ); } @@ -702,8 +742,8 @@ public IsAuthenticatedUseCase createIsAuthenticatedUseCase() { @NonNull public SetEngagementConfigUseCase createSetEngagementConfigUseCase() { return new SetEngagementConfigUseCase( - repositoryFactory.getEngagementConfigRepository(), - createResetSurveyUseCase() + repositoryFactory.getEngagementConfigRepository(), + createResetSurveyUseCase() ); } @@ -725,8 +765,8 @@ public IsMessagingAvailableUseCase createIsMessagingAvailableUseCase() { @NonNull public IsSecureConversationsChatAvailableUseCase createIsSecureConversationsChatAvailableUseCase() { return new IsSecureConversationsChatAvailableUseCase( - repositoryFactory.getEngagementConfigRepository(), - createIsMessagingAvailableUseCase() + repositoryFactory.getEngagementConfigRepository(), + createIsMessagingAvailableUseCase() ); } @@ -743,24 +783,24 @@ public MarkMessagesReadWithDelayUseCase createMarkMessagesReadUseCase() { @NonNull public GliaOnCallVisualizerUseCase createOnCallVisualizerUseCase() { return new GliaOnCallVisualizerUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getGliaEngagementStateRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getGliaEngagementStateRepository() ); } @NonNull public GliaOnCallVisualizerEndUseCase createOnCallVisualizerEndUseCase() { return new GliaOnCallVisualizerEndUseCase( - repositoryFactory.getCallVisualizerRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - createOnCallVisualizerUseCase(), - callNotificationUseCase, - removeScreenSharingNotificationUseCase, - repositoryFactory.getGliaSurveyRepository(), - repositoryFactory.getGliaVisitorMediaRepository() + repositoryFactory.getCallVisualizerRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + createOnCallVisualizerUseCase(), + callNotificationUseCase, + removeScreenSharingNotificationUseCase, + repositoryFactory.getGliaSurveyRepository(), + repositoryFactory.getGliaVisitorMediaRepository() ); } @@ -792,34 +832,209 @@ public ResetSurveyUseCase createResetSurveyUseCase() { @NonNull public OnAudioStartedUseCase createOnAudioStartedUseCase() { return new OnAudioStartedUseCase( - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaVisitorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaVisitorMediaRepository() ); } @NonNull public TurnSpeakerphoneUseCase createTurnSpeakerphoneUseCase() { return new TurnSpeakerphoneUseCase( - audioControlManager + audioControlManager ); } @NonNull public AcceptMediaUpgradeOfferUseCase createAcceptMediaUpgradeOfferUseCase() { return new AcceptMediaUpgradeOfferUseCase( - repositoryFactory.getMediaUpgradeOfferRepository(), - permissionManager + repositoryFactory.getMediaUpgradeOfferRepository(), + permissionManager ); } @NonNull public HandleCallPermissionsUseCase createHandleCallPermissionsUseCase() { return new HandleCallPermissionsUseCase( - createIsCallVisualizerUseCase(), - permissionManager + createIsCallVisualizerUseCase(), + permissionManager + ); + } + + @NonNull + private Gson getGvaGson() { + if (gvaGson == null) { + gvaGson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + } + + return gvaGson; + } + + @NonNull + public ParseGvaButtonsUseCase createParseGvaButtonsUseCase() { + return new ParseGvaButtonsUseCase(getGvaGson()); + } + + @NonNull + public ParseGvaGalleryCardsUseCase createParseGvaGalleryCardsUseCase() { + return new ParseGvaGalleryCardsUseCase(getGvaGson()); + } + + @NonNull + public GetGvaTypeUseCase createGetGvaTypeUseCase() { + return new GetGvaTypeUseCase(); + } + + @NonNull + public IsGvaUseCase createIsGvaUseCase() { + return new IsGvaUseCase(createGetGvaTypeUseCase()); + } + + @NonNull + public MapGvaResponseTextUseCase createMapGvaResponseTextUseCase() { + return new MapGvaResponseTextUseCase(); + } + + @NonNull + public MapGvaPersistentButtonsUseCase createMapGvaPersistentButtonsUseCase() { + return new MapGvaPersistentButtonsUseCase(createParseGvaButtonsUseCase()); + } + + @NonNull + public MapGvaQuickRepliesUseCase createMapGvaGvaQuickRepliesUseCase() { + return new MapGvaQuickRepliesUseCase(createParseGvaButtonsUseCase()); + } + + @NonNull + public MapGvaGvaGalleryCardsUseCase createMapGvaGvaGalleryCardsUseCase() { + return new MapGvaGvaGalleryCardsUseCase(createParseGvaGalleryCardsUseCase()); + } + + @NonNull + public MapGvaUseCase createMapGvaUseCase() { + return new MapGvaUseCase( + createGetGvaTypeUseCase(), + createMapGvaResponseTextUseCase(), + createMapGvaPersistentButtonsUseCase(), + createMapGvaGvaQuickRepliesUseCase(), + createMapGvaGvaGalleryCardsUseCase() ); } + @NonNull + public DetermineGvaUrlTypeUseCase createDetermineGvaUrlTypeUseCase() { + return new DetermineGvaUrlTypeUseCase(); + } + + @NonNull + public DetermineGvaButtonTypeUseCase createDetermineGvaButtonTypeUseCase() { + return new DetermineGvaButtonTypeUseCase(createDetermineGvaUrlTypeUseCase()); + } + + @NonNull + public HandleCustomCardClickUseCase createHandleCustomCardClickUseCase() { + return new HandleCustomCardClickUseCase( + createCustomCardTypeUseCase(), + createCustomCardShouldShowUseCase() + ); + } + + @NonNull + public AppendHistoryChatMessageUseCase createAppendHistoryChatMessageUseCase() { + return new AppendHistoryChatMessageUseCase( + createAppendHistoryVisitorChatItemUseCase(), + createAppendHistoryOperatorChatItemUseCase(), + createAppendSystemMessageItemUseCase() + ); + } + + @NonNull + public AppendSystemMessageItemUseCase createAppendSystemMessageItemUseCase() { + return new AppendSystemMessageItemUseCase(); + } + + @NonNull + public AppendHistoryVisitorChatItemUseCase createAppendHistoryVisitorChatItemUseCase() { + return new AppendHistoryVisitorChatItemUseCase(createMapVisitorAttachmentUseCase()); + } + + @NonNull + public MapVisitorAttachmentUseCase createMapVisitorAttachmentUseCase() { + return new MapVisitorAttachmentUseCase(); + } + + @NonNull + public AppendHistoryOperatorChatItemUseCase createAppendHistoryOperatorChatItemUseCase() { + return new AppendHistoryOperatorChatItemUseCase( + createIsGvaUseCase(), + createCustomCardAdapterTypeUseCase(), + createAppendHistoryGvaMessageItemUseCase(), + createAppendHistoryCustomCardItemUseCase(), + createAppendHistoryResponseCardOrTextItemUseCase() + ); + } + + @NonNull + public AppendHistoryCustomCardItemUseCase createAppendHistoryCustomCardItemUseCase() { + return new AppendHistoryCustomCardItemUseCase( + createCustomCardTypeUseCase(), + createCustomCardShouldShowUseCase() + ); + } + + @NonNull + public AppendGvaMessageItemUseCase createAppendHistoryGvaMessageItemUseCase() { + return new AppendGvaMessageItemUseCase(createMapGvaUseCase()); + } + + @NonNull + public AppendNewVisitorMessageUseCase createAppendNewVisitorMessageUseCase() { + return new AppendNewVisitorMessageUseCase(createMapVisitorAttachmentUseCase()); + } + + @NonNull + public AppendNewOperatorMessageUseCase createAppendNewOperatorMessageUseCase() { + return new AppendNewOperatorMessageUseCase( + createIsGvaUseCase(), + createCustomCardAdapterTypeUseCase(), + createAppendGvaMessageItemUseCase(), + createAppendHistoryCustomCardItemUseCase(), + createAppendNewResponseCardOrTextItemUseCase() + ); + } + + @NonNull + private AppendNewResponseCardOrTextItemUseCase createAppendNewResponseCardOrTextItemUseCase() { + return new AppendNewResponseCardOrTextItemUseCase( + createMapOperatorAttachmentUseCase(), + createMapOperatorPlainTextUseCase(), + createMapResponseCardUseCase() + ); + } + + @NonNull + public AppendGvaMessageItemUseCase createAppendGvaMessageItemUseCase() { + return new AppendGvaMessageItemUseCase(createMapGvaUseCase()); + } + + @NonNull + public AppendNewChatMessageUseCase createAppendNewChatMessageUseCase() { + return new AppendNewChatMessageUseCase( + createAppendNewOperatorMessageUseCase(), + createAppendNewVisitorMessageUseCase(), + createAppendSystemMessageItemUseCase() + ); + } + + @NonNull + public SendUnsentMessagesUseCase createSendUnsentMessagesUseCase() { + return new SendUnsentMessagesUseCase(repositoryFactory.getGliaMessageRepository()); + } + + @NonNull + public UpdateOperatorDefaultImageUrlUseCase createUpdateOperatorDefaultImageUrlUseCase() { + return new UpdateOperatorDefaultImageUrlUseCase(repositoryFactory.getOperatorRepository(), createSiteInfoUseCase()); + } + public void resetState() { createResetSurveyUseCase().invoke(); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt b/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt index cc2fff4ed..94f786cb0 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt @@ -14,6 +14,7 @@ import com.glia.widgets.helper.fileName import io.reactivex.Completable import io.reactivex.Maybe import java.io.InputStream +import kotlin.jvm.optionals.getOrNull internal class GliaFileRepositoryImpl( private val bitmapCache: InAppBitmapCache, @@ -67,7 +68,7 @@ internal class GliaFileRepositoryImpl( } private fun fetchFile(attachmentFile: AttachmentFile, callback: RequestCallback) { - val engagement = gliaCore.currentEngagement.orElse(null) + val engagement = gliaCore.currentEngagement.getOrNull() if (engagement == null && engagementConfigRepository.chatType == ChatType.SECURE_MESSAGING) { secureConversations.fetchFile(attachmentFile.id, callback) } else { diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt index 866616af9..40cbf3cbe 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt @@ -2,38 +2,52 @@ package com.glia.widgets.helper import android.content.res.ColorStateList import android.graphics.drawable.Drawable +import android.text.Html +import android.text.Spanned import android.text.format.DateUtils import androidx.annotation.ColorInt import androidx.core.graphics.drawable.DrawableCompat import com.glia.androidsdk.Engagement import com.glia.androidsdk.Operator +import com.glia.androidsdk.chat.AttachmentFile +import com.glia.androidsdk.chat.MessageAttachment +import com.glia.androidsdk.chat.OperatorMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.queuing.Queue import com.glia.widgets.UiTheme +import com.glia.widgets.core.engagement.data.LocalOperator +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import com.glia.widgets.view.unifiedui.deepMerge +import kotlin.jvm.optionals.getOrNull internal fun Drawable.setTintCompat(@ColorInt color: Int) = DrawableCompat.setTint(this, color) @ColorInt -internal fun ColorStateList?.colorForState(state: IntArray): Int? = - this?.getColorForState(state, defaultColor) +internal fun ColorStateList?.colorForState(state: IntArray): Int? = this?.getColorForState(state, defaultColor) // Common -internal fun String.separateStringWithSymbol(symbol: String): String = - asSequence().joinToString(symbol) +internal fun String.separateStringWithSymbol(symbol: String): String = asSequence().joinToString(symbol) -internal fun Queue.supportsMessaging() = state.medias.contains(Engagement.MediaType.MESSAGING) +internal fun Queue.supportMessaging() = state.medias.contains(Engagement.MediaType.MESSAGING) -internal val Operator.imageUrl: String? - get() = picture?.url?.orElse(null) +internal fun formatElapsedTime(elapsedMilliseconds: Long) = DateUtils.formatElapsedTime(elapsedMilliseconds / DateUtils.SECOND_IN_MILLIS) -internal fun formatElapsedTime(elapsedMilliseconds: Long) = - DateUtils.formatElapsedTime(elapsedMilliseconds / DateUtils.SECOND_IN_MILLIS) +internal val Operator.formattedName: String get() = name.substringBefore(' ') -internal val Operator.formattedName: String - get() = name.substringBefore(' ') +internal val Operator.imageUrl: String? get() = picture?.url?.getOrNull() -internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = - this?.gliaAlertDialogButtonUseVerticalAlignment ?: false +internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = this?.gliaAlertDialogButtonUseVerticalAlignment ?: false -internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = - deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() +internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() + +/** + * Returns styled text from the provided HTML string. Replaces \n to
regardless of the operating system where the string was created. + */ +internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(replace("(\r\n|\n)".toRegex(), "
"), flags) + +internal val AttachmentFile.isImage: Boolean get() = contentType.startsWith("image") + +internal fun MessageAttachment.asSingleChoice(): SingleChoiceAttachment? = this as? SingleChoiceAttachment + +internal fun OperatorMessage.toChatMessageInternal(): ChatMessageInternal = + ChatMessageInternal(this, LocalOperator(operatorId.orEmpty(), operatorName.orEmpty(), operatorImageUrl)) diff --git a/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt index a22180880..b38b67510 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt @@ -21,6 +21,7 @@ import com.glia.widgets.view.configuration.survey.SurveyStyle import com.glia.widgets.view.unifiedui.applyTextTheme import com.glia.widgets.view.unifiedui.theme.survey.SurveySingleQuestionTheme import java.util.Optional +import kotlin.jvm.optionals.getOrNull class SingleQuestionViewHolder( private val binding: SurveySingleQuestionItemBinding, @@ -50,7 +51,7 @@ class SingleQuestionViewHolder( private fun singleChoice(item: QuestionItem) { val selectedId = Optional.ofNullable(item.answer) .map { answer: Survey.Answer -> answer.getResponse() as String } - .orElse(null) + .getOrNull() val options = item.question.options ?: return radioGroup.removeAllViews() for (i in options.indices) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt index 64a771a8b..8f80010a1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt @@ -181,7 +181,7 @@ object Dialogs { text = negativeButtonText setOnClickListener(negativeButtonClickListener) applyButtonTheme( - backgroundColor = systemNegativeColor, + backgroundColor = if (isButtonsColorsReversed) { brandPrimaryColor } else { systemNegativeColor }, textColor = baseLightColor, textFont = fontFamily ) @@ -191,7 +191,7 @@ object Dialogs { text = positiveButtonText setOnClickListener(positiveButtonClickListener) applyButtonTheme( - backgroundColor = brandPrimaryColor, + backgroundColor = if (isButtonsColorsReversed) { systemNegativeColor } else { brandPrimaryColor }, textColor = baseLightColor, textFont = fontFamily ) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java index f80da8526..4076539da 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java +++ b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java @@ -27,7 +27,7 @@ public MessagesNotSeenHandler( public void init() { Logger.d(TAG, "init"); - gliaOnMessageUseCase.execute().doOnNext(this::onMessage).subscribe(); + gliaOnMessageUseCase.invoke().doOnNext(this::onMessage).subscribe(); } public void chatOnBackClicked() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/SingleChoiceCardView.kt b/widgetssdk/src/main/java/com/glia/widgets/view/SingleChoiceCardView.kt index 549c5969f..cee0d8e02 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/SingleChoiceCardView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/SingleChoiceCardView.kt @@ -14,7 +14,7 @@ import androidx.core.view.updatePadding import com.glia.androidsdk.chat.SingleChoiceOption import com.glia.widgets.R import com.glia.widgets.UiTheme -import com.glia.widgets.chat.model.history.ResponseCardItem +import com.glia.widgets.chat.model.OperatorMessageItem import com.glia.widgets.databinding.SingleChoiceCardViewBinding import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.Utils @@ -64,22 +64,22 @@ class SingleChoiceCardView @JvmOverloads constructor( bgDrawable.setStroke(strokeSize, color) } - fun setData( - item: ResponseCardItem, + internal fun setData( + item: OperatorMessageItem.ResponseCard, theme: UiTheme ) { setupCardView(theme) setupImage(item.choiceCardImageUrl) - setupText(item.content, theme) + setupText(item.content.orEmpty(), theme) setupButtons(item, theme) } - fun setOnOptionClickedListener(onOptionClickedListener: OnOptionClickedListener?) { + internal fun setOnOptionClickedListener(onOptionClickedListener: OnOptionClickedListener?) { this.onOptionClickedListener = onOptionClickedListener } - fun interface OnOptionClickedListener { - fun onClicked(item: ResponseCardItem, selectedOption: SingleChoiceOption) + internal fun interface OnOptionClickedListener { + fun onClicked(item: OperatorMessageItem.ResponseCard, selectedOption: SingleChoiceOption) } private fun setupCardView(theme: UiTheme) { @@ -131,7 +131,7 @@ class SingleChoiceCardView @JvmOverloads constructor( } private fun setupButtons( - item: ResponseCardItem, + item: OperatorMessageItem.ResponseCard, theme: UiTheme ) { val horizontalMargin = resources.getDimensionPixelOffset(R.dimen.glia_large) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt b/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt index da4ece5c6..e881d35e1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt @@ -187,14 +187,11 @@ internal class ApplicationChatHeadLayoutController( state = State.ENGAGEMENT engagementDisposables.add( getOperatorFlowableUseCase.execute() - .subscribe( - { operator: Operator -> operatorDataLoaded(operator) } - ) { throwable: Throwable -> - Logger.e(TAG, "getOperatorFlowableUseCase error: " + throwable.message) - } + .subscribe({ operatorDataLoaded(it) }) { Logger.e(TAG, "getOperatorFlowableUseCase error: " + it.message) } ) updateChatHeadView() } + override fun newEngagementLoaded(engagement: OmnibrowseEngagement) { onNewEngagementLoaded() } diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt index 98a9fef81..944edaa99 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt @@ -26,7 +26,7 @@ import kotlin.reflect.full.primaryConstructor * val c = a deepMerge b */ internal inline infix fun T.deepMerge(other: T): T { - if (!T::class.isData) throw UnsupportedOperationException("Merge supports only data classes") + if (!T::class.isData) throw UnsupportedOperationException("Merge supports only data classes, ${T::class.simpleName} is not a data class") return unsafeMerge(other) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt index 36bded1e6..223d738b5 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt @@ -25,7 +25,9 @@ import com.glia.widgets.view.unifiedui.theme.survey.OptionButtonTheme import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.progressindicator.CircularProgressIndicator +import com.google.android.material.shape.CornerFamily internal fun View.applyColorTheme(color: ColorTheme?) { background = createBackgroundFromTheme(color ?: return) @@ -74,6 +76,25 @@ internal fun View.applyLayerTheme(layer: LayerTheme?) { background = drawable } +internal fun ShapeableImageView.applyLayerTheme(layer: LayerTheme?) { + layer?.fill?.also { + val drawable = (background as? GradientDrawable) ?: GradientDrawable() + if (it.isGradient) { + drawable.colors = it.valuesArray + } else { + drawable.setColor(it.primaryColor) + } + background = drawable + } + + layer?.stroke?.also { strokeColor = ColorStateList.valueOf(it) } + layer?.borderWidth?.also(::setStrokeWidth) + + layer?.cornerRadius?.also { + shapeAppearanceModel = shapeAppearanceModel.toBuilder().setAllCorners(CornerFamily.ROUNDED, it).build() + } +} + internal fun MaterialCardView.applyCardLayerTheme(layer: LayerTheme?) { layer?.fill?.primaryColor?.also { /* diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/chat/ChatRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/chat/ChatRemoteConfig.kt index b192ffadf..5eb49170e 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/chat/ChatRemoteConfig.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/chat/ChatRemoteConfig.kt @@ -5,6 +5,7 @@ import com.glia.widgets.view.unifiedui.config.base.HeaderRemoteConfig import com.glia.widgets.view.unifiedui.config.base.LayerRemoteConfig import com.glia.widgets.view.unifiedui.config.base.TextRemoteConfig import com.glia.widgets.view.unifiedui.config.bubble.BubbleRemoteConfig +import com.glia.widgets.view.unifiedui.config.gva.GvaRemoteConfig import com.glia.widgets.view.unifiedui.theme.chat.ChatTheme import com.google.gson.annotations.SerializedName @@ -52,7 +53,10 @@ internal data class ChatRemoteConfig( val newMessagesDividerColorRemoteConfig: ColorLayerRemoteConfig?, @SerializedName("newMessagesDividerText") - val newMessagesDividerTextRemoteConfig: TextRemoteConfig? + val newMessagesDividerTextRemoteConfig: TextRemoteConfig?, + + @SerializedName("gva") + val gvaRemoteConfig: GvaRemoteConfig? ) { fun toChatTheme(): ChatTheme = ChatTheme( background = background?.toLayerTheme(), @@ -69,6 +73,7 @@ internal data class ChatRemoteConfig( unreadIndicator = unreadIndicator?.toUnreadIndicatorTheme(), typingIndicator = typingIndicator?.toColorTheme(), newMessagesDividerColorTheme = newMessagesDividerColorRemoteConfig?.toColorTheme(), - newMessagesDividerTextTheme = newMessagesDividerTextRemoteConfig?.toTextTheme() + newMessagesDividerTextTheme = newMessagesDividerTextRemoteConfig?.toTextTheme(), + gva = gvaRemoteConfig?.toGvaTheme() ) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt new file mode 100644 index 000000000..b4297a113 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt @@ -0,0 +1,32 @@ +package com.glia.widgets.view.unifiedui.config.gva + +import com.glia.widgets.view.unifiedui.config.base.ButtonRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.LayerRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.TextRemoteConfig +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme +import com.google.gson.annotations.SerializedName + +internal data class GvaGalleryCardRemoteConfig( + @SerializedName("title") + val titleRemoteConfig: TextRemoteConfig?, + + @SerializedName("subtitle") + val subtitleRemoteConfig: TextRemoteConfig?, + + @SerializedName("image") + val imageRemoteConfig: LayerRemoteConfig?, + + @SerializedName("button") + val buttonRemoteConfig: ButtonRemoteConfig?, + + @SerializedName("background") + val backgroundRemoteConfig: LayerRemoteConfig? +) { + fun toGvaGalleryCardTheme(): GvaGalleryCardTheme = GvaGalleryCardTheme( + title = titleRemoteConfig?.toTextTheme(), + subtitle = subtitleRemoteConfig?.toTextTheme(), + image = imageRemoteConfig?.toLayerTheme(), + button = buttonRemoteConfig?.toButtonTheme(), + background = backgroundRemoteConfig?.toLayerTheme() + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaPersistentButtonRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaPersistentButtonRemoteConfig.kt new file mode 100644 index 000000000..0e01d541e --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaPersistentButtonRemoteConfig.kt @@ -0,0 +1,24 @@ +package com.glia.widgets.view.unifiedui.config.gva + +import com.glia.widgets.view.unifiedui.config.base.ButtonRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.LayerRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.TextRemoteConfig +import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme +import com.google.gson.annotations.SerializedName + +internal data class GvaPersistentButtonRemoteConfig( + @SerializedName("title") + val titleRemoteConfig: TextRemoteConfig?, + + @SerializedName("background") + val backgroundRemoteConfig: LayerRemoteConfig?, + + @SerializedName("button") + val buttonRemoteConfig: ButtonRemoteConfig? +) { + fun toGvaPersistentButtonTheme(): GvaPersistentButtonTheme = GvaPersistentButtonTheme( + title = titleRemoteConfig?.toTextTheme(), + background = backgroundRemoteConfig?.toLayerTheme(), + button = buttonRemoteConfig?.toButtonTheme() + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt new file mode 100644 index 000000000..7db26dd9b --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt @@ -0,0 +1,22 @@ +package com.glia.widgets.view.unifiedui.config.gva + +import com.glia.widgets.view.unifiedui.config.base.ButtonRemoteConfig +import com.glia.widgets.view.unifiedui.theme.gva.GvaTheme +import com.google.gson.annotations.SerializedName + +internal data class GvaRemoteConfig( + @SerializedName("quickReplyButton") + val quickReplyRemoteConfig: ButtonRemoteConfig?, + + @SerializedName("persistentButton") + val persistentButtonRemoteConfig: GvaPersistentButtonRemoteConfig?, + + @SerializedName("galleryCard") + val galleryCardRemoteConfig: GvaGalleryCardRemoteConfig? +) { + fun toGvaTheme(): GvaTheme = GvaTheme( + quickReplyTheme = quickReplyRemoteConfig?.toButtonTheme(), + persistentButtonTheme = persistentButtonRemoteConfig?.toGvaPersistentButtonTheme(), + galleryCardTheme = galleryCardRemoteConfig?.toGvaGalleryCardTheme() + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt index 07a5161a0..49f483676 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt @@ -1,6 +1,7 @@ package com.glia.widgets.view.unifiedui.parse import com.glia.widgets.di.Dependencies +import com.glia.widgets.helper.ResourceProvider import com.glia.widgets.view.unifiedui.config.RemoteConfiguration import com.glia.widgets.view.unifiedui.config.alert.AxisRemoteConfig import com.glia.widgets.view.unifiedui.config.base.AlignmentTypeRemoteConfig @@ -13,7 +14,9 @@ import com.glia.widgets.view.unifiedui.config.chat.AttachmentSourceTypeRemoteCon import com.google.gson.Gson import com.google.gson.GsonBuilder -internal class RemoteConfigurationParser { +internal class RemoteConfigurationParser( + resourceProvider: ResourceProvider = Dependencies.getResourceProvider() +) { /** * @return [Gson] instance with applied deserializers to parse remote config. */ @@ -23,7 +26,7 @@ internal class RemoteConfigurationParser { .registerTypeAdapter(ColorLayerRemoteConfig::class.java, ColorLayerDeserializer()) .registerTypeAdapter( SizeDpRemoteConfig::class.java, - DpDeserializer(Dependencies.getResourceProvider()) + DpDeserializer(resourceProvider) ) .registerTypeAdapter(SizeSpRemoteConfig::class.java, SpDeserializer()) .registerTypeAdapter(TextStyleRemoteConfig::class.java, TextStyleDeserializer()) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/base/LayerTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/base/LayerTheme.kt index 5126b1f9c..cfe36b42b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/base/LayerTheme.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/base/LayerTheme.kt @@ -8,7 +8,7 @@ internal data class LayerTheme( val fill: ColorTheme? = null, @ColorInt val stroke: Int? = null, - // Currently it is not possible to draw gradient stroke(change to ThemeColor in case of migrating to Jetpack Compose) + // Currently, it is impossible to draw a gradient stroke(change to ThemeColor in case of migrating to Jetpack Compose) @Px val borderWidth: Float? = null, // width in pixels @Px diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/chat/ChatTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/chat/ChatTheme.kt index cb99e79fa..afbd1e812 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/chat/ChatTheme.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/chat/ChatTheme.kt @@ -5,6 +5,7 @@ import com.glia.widgets.view.unifiedui.theme.base.HeaderTheme import com.glia.widgets.view.unifiedui.theme.base.LayerTheme import com.glia.widgets.view.unifiedui.theme.base.TextTheme import com.glia.widgets.view.unifiedui.theme.bubble.BubbleTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaTheme internal data class ChatTheme( val background: LayerTheme? = null, @@ -21,5 +22,6 @@ internal data class ChatTheme( val unreadIndicator: UnreadIndicatorTheme? = null, val typingIndicator: ColorTheme? = null, val newMessagesDividerColorTheme: ColorTheme? = null, - val newMessagesDividerTextTheme: TextTheme? = null + val newMessagesDividerTextTheme: TextTheme? = null, + val gva: GvaTheme? = null ) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt index 912551efe..708332c8d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt @@ -29,6 +29,29 @@ internal fun NegativeDefaultButtonTheme(pallet: ColorPallet) = pallet.run { DefaultButtonTheme(text = baseLightColorTheme, background = systemNegativeColorTheme) } +/** + * Default theme for Outlined Button + */ +internal fun OutlinedButtonTheme( + text: ColorTheme?, + stroke: ColorTheme? +): ButtonTheme? = composeIfAtLeastOneNotNull(text, stroke) { + ButtonTheme( + text = TextTheme(textColor = text), + background = LayerTheme(stroke = stroke?.primaryColor), + iconColor = null, + elevation = null, + shadowColor = null + ) +} + +/** + * Default theme for GVA Button + */ +internal fun GvaDefaultButtonTheme(pallet: ColorPallet) = pallet.run { + DefaultButtonTheme(text = baseDarkColorTheme, background = backgroundColorTheme) +} + private fun DefaultButtonTheme( text: ColorTheme? = null, background: ColorTheme? = null, diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Chat.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Chat.kt index 5fc9bb893..500eb0427 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Chat.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Chat.kt @@ -34,7 +34,8 @@ internal fun ChatTheme(pallet: ColorPallet): ChatTheme = unreadIndicator = ChatUnreadIndicatorTheme(pallet), typingIndicator = pallet.primaryColorTheme, newMessagesDividerColorTheme = pallet.primaryColorTheme, - newMessagesDividerTextTheme = TextTheme(textColor = pallet.primaryColorTheme) + newMessagesDividerTextTheme = TextTheme(textColor = pallet.primaryColorTheme), + gva = GvaTheme(pallet) ) /** diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt new file mode 100644 index 000000000..30093c2ed --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt @@ -0,0 +1,48 @@ +@file:Suppress("FunctionName") + +package com.glia.widgets.view.unifiedui.theme.defaulttheme + +import com.glia.widgets.view.unifiedui.composeIfAtLeastOneNotNull +import com.glia.widgets.view.unifiedui.theme.ColorPallet +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaTheme + +/** + * Default Theme for Gva + */ +internal fun GvaTheme(pallet: ColorPallet): GvaTheme = GvaTheme( + quickReplyTheme = pallet.primaryColorTheme?.let { OutlinedButtonTheme(it, it) }, + persistentButtonTheme = GvaPersistentButtonTheme(pallet), + galleryCardTheme = GvaGalleryCardTheme(pallet) +) + +private fun GvaPersistentButtonTheme( + pallet: ColorPallet +): GvaPersistentButtonTheme? = pallet.run { + composeIfAtLeastOneNotNull(baseLightColorTheme, baseDarkColorTheme, backgroundColorTheme) { + GvaPersistentButtonTheme( + title = BaseDarkColorTextTheme(this), + background = LayerTheme( + fill = baseLightColorTheme + ), + button = GvaDefaultButtonTheme(this) + ) + } +} + +private fun GvaGalleryCardTheme( + pallet: ColorPallet +): GvaGalleryCardTheme? = pallet.run { + composeIfAtLeastOneNotNull(baseLightColorTheme, baseDarkColorTheme, backgroundColorTheme) { + GvaGalleryCardTheme( + title = BaseDarkColorTextTheme(this), + subtitle = BaseDarkColorTextTheme(this), + background = LayerTheme( + fill = baseLightColorTheme + ), + button = GvaDefaultButtonTheme(this) + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt new file mode 100644 index 000000000..ab00918be --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.view.unifiedui.theme.gva + +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.glia.widgets.view.unifiedui.theme.base.TextTheme + +internal data class GvaGalleryCardTheme( + val title: TextTheme? = null, + val subtitle: TextTheme? = null, + val image: LayerTheme? = null, + val button: ButtonTheme? = null, + val background: LayerTheme? = null +) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaPersistentButtonTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaPersistentButtonTheme.kt new file mode 100644 index 000000000..7095182a7 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaPersistentButtonTheme.kt @@ -0,0 +1,11 @@ +package com.glia.widgets.view.unifiedui.theme.gva + +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.glia.widgets.view.unifiedui.theme.base.TextTheme + +internal data class GvaPersistentButtonTheme( + val title: TextTheme? = null, + val background: LayerTheme? = null, + val button: ButtonTheme? = null +) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt new file mode 100644 index 000000000..4fb076216 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt @@ -0,0 +1,9 @@ +package com.glia.widgets.view.unifiedui.theme.gva + +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme + +internal data class GvaTheme( + val quickReplyTheme: ButtonTheme? = null, + val persistentButtonTheme: GvaPersistentButtonTheme? = null, + val galleryCardTheme: GvaGalleryCardTheme? = null +) diff --git a/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml new file mode 100644 index 000000000..5ed6a14c4 --- /dev/null +++ b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml b/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml new file mode 100644 index 000000000..98545d8cb --- /dev/null +++ b/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ No newline at end of file diff --git a/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml b/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml new file mode 100644 index 000000000..75a740547 --- /dev/null +++ b/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/widgetssdk/src/main/res/layout/chat_view.xml b/widgetssdk/src/main/res/layout/chat_view.xml index bc064bdfa..5368fcd3e 100644 --- a/widgetssdk/src/main/res/layout/chat_view.xml +++ b/widgetssdk/src/main/res/layout/chat_view.xml @@ -17,12 +17,22 @@ android:id="@+id/chat_recycler_view" android:layout_width="match_parent" android:layout_height="0dp" - android:paddingBottom="@dimen/glia_medium" android:clipToPadding="false" - app:layout_constraintBottom_toTopOf="@+id/operator_typing_animation_view" + android:paddingBottom="@dimen/glia_medium" + app:layout_constraintBottom_toTopOf="@+id/gva_quick_replies_layout" app:layout_constraintTop_toBottomOf="@+id/app_bar_view" tools:listitem="@layout/chat_visitor_message_layout" /> + + + @@ -115,13 +125,13 @@ android:layout_weight="1" android:hint="@string/glia_chat_enter_message" android:importantForAutofill="no" - android:textCursorDrawable="@null" + android:maxLength="10000" + android:maxLines="4" android:minHeight="@dimen/glia_chat_edit_text_min_height" android:paddingStart="@dimen/glia_large" android:textColor="?attr/gliaBaseDarkColor" android:textColorHint="?attr/gliaBaseNormalColor" - android:maxLines="4" - android:maxLength="10000" + android:textCursorDrawable="@null" tools:ignore="RtlSymmetry" tools:textColor="@color/glia_black_color" /> @@ -153,6 +163,6 @@ android:id="@+id/group_chat_controls" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="operator_typing_animation_view, divider_view, add_attachment_queue, chat_message_layout"/> + app:constraint_referenced_ids="operator_typing_animation_view, divider_view, add_attachment_queue, chat_message_layout" /> diff --git a/widgetssdk/src/main/res/values/attrs.xml b/widgetssdk/src/main/res/values/attrs.xml index 08667623a..2c0b4f032 100644 --- a/widgetssdk/src/main/res/values/attrs.xml +++ b/widgetssdk/src/main/res/values/attrs.xml @@ -84,9 +84,9 @@ - + - + @@ -174,9 +174,9 @@ - + - + @@ -208,6 +208,13 @@ + + + + + + + diff --git a/widgetssdk/src/main/res/values/dimens.xml b/widgetssdk/src/main/res/values/dimens.xml index 70384fbc6..527a7a5d6 100644 --- a/widgetssdk/src/main/res/values/dimens.xml +++ b/widgetssdk/src/main/res/values/dimens.xml @@ -58,6 +58,8 @@ 18sp 6dp + 234dp + 44dp 375dp 64dp diff --git a/widgetssdk/src/main/res/values/strings.xml b/widgetssdk/src/main/res/values/strings.xml index d7dce1e18..47abf28af 100644 --- a/widgetssdk/src/main/res/values/strings.xml +++ b/widgetssdk/src/main/res/values/strings.xml @@ -258,4 +258,8 @@ Could not load the visitor code. Please try refreshing. Refresh + + This action is not currently supported on mobile. + %1$s \n Card: %2$d of %3$d. + diff --git a/widgetssdk/src/main/res/values/styles.xml b/widgetssdk/src/main/res/values/styles.xml index db2402dd4..6b85d7515 100644 --- a/widgetssdk/src/main/res/values/styles.xml +++ b/widgetssdk/src/main/res/values/styles.xml @@ -28,6 +28,11 @@ @dimen/glia_x_large + + + + + + + + + +