From bb46b6adadd4db271cd89c4ca18a7f6e9e118c11 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Fri, 19 Jul 2024 11:13:33 +0200 Subject: [PATCH 1/3] Contacts Activity Compose Signed-off-by: sowjanyakch --- .editorconfig | 1 + .idea/inspectionProfiles/ktlint.xml | 38 +- app/build.gradle | 29 ++ app/src/main/AndroidManifest.xml | 3 + .../talk/adapters/items/ContactItem.java | 220 ------------ .../talk/adapters/items/ContactItem.kt | 199 +++++++++++ .../adapters/items/GenericTextHeaderItem.java | 89 ----- .../adapters/items/GenericTextHeaderItem.kt | 78 ++++ .../com/nextcloud/talk/api/NcApiCoroutines.kt | 42 +++ .../talk/contacts/ContactsActivity.kt | 8 +- .../talk/contacts/ContactsActivityCompose.kt | 335 ++++++++++++++++++ .../talk/contacts/ContactsRepository.kt | 16 + .../talk/contacts/ContactsRepositoryImpl.kt | 65 ++++ .../talk/contacts/ContactsViewModel.kt | 110 ++++++ .../talk/contacts/SearchComponent.kt | 115 ++++++ .../com/nextcloud/talk/contacts/ShareType.kt | 16 + .../ConversationsListActivity.kt | 4 +- .../talk/dagger/modules/BusModule.java | 1 + .../talk/dagger/modules/RepositoryModule.kt | 9 + .../talk/dagger/modules/RestModule.java | 10 + .../talk/dagger/modules/ViewModelModule.kt | 6 + .../baseline_chat_bubble_outline_24.xml | 5 + app/src/main/res/values/strings.xml | 9 +- build.gradle | 8 +- gradle/verification-metadata.xml | 88 +++++ 25 files changed, 1188 insertions(+), 316 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt create mode 100644 app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml diff --git a/.editorconfig b/.editorconfig index 43461fa344..8232982133 100644 --- a/.editorconfig +++ b/.editorconfig @@ -51,3 +51,4 @@ ktlint_standard_import-ordering = disabled ktlint_standard_wrapping = enabled ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index 7d04a74be8..0938bfdc8f 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -2,6 +2,42 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index eb9da60592..29eb613c72 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'com.github.spotbugs' apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: "org.jlleitschuh.gradle.ktlint" apply plugin: 'kotlinx-serialization' +apply plugin: 'dagger.hilt.android.plugin' android { compileSdk 34 @@ -45,6 +46,7 @@ android { flavorDimensions "default" renderscriptTargetApi 19 renderscriptSupportModeEnabled true + javaCompileOptions.annotationProcessorOptions.arguments['dagger.hilt.disableModulesHaveInstallInCheck'] = 'true' productFlavors { // used for f-droid @@ -121,6 +123,11 @@ android { buildFeatures { viewBinding true buildConfig = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.13" } lint { @@ -130,6 +137,9 @@ android { htmlReport true } } +kapt { + correctErrorTypes = true +} ext { androidxCameraVersion = "1.3.4" @@ -259,6 +269,7 @@ dependencies { implementation "com.afollestad.material-dialogs:lifecycle:${materialDialogsVersion}" implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.squareup.retrofit2:converter-gson:2.11.0' implementation "androidx.media3:media3-exoplayer:$media3_version" implementation "androidx.media3:media3-ui:$media3_version" @@ -280,6 +291,19 @@ dependencies { implementation 'androidx.core:core-ktx:1.13.1' + //compose + implementation(platform("androidx.compose:compose-bom:2024.02.01")) + implementation("androidx.compose.ui:ui") + implementation 'androidx.compose.material3:material3' + implementation("androidx.compose.ui:ui-tooling-preview") + implementation 'androidx.activity:activity-compose:1.9.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.1' + debugImplementation("androidx.compose.ui:ui-tooling") + + //tests + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-test-manifest") + testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.12.0' androidTestImplementation 'org.mockito:mockito-android:5.12.0' @@ -307,6 +331,11 @@ dependencies { implementation 'com.github.nextcloud.android-common:ui:0.22.0' implementation 'com.github.nextcloud-deps:android-talk-webrtc:121.6167.0' + implementation(platform("androidx.compose:compose-bom:2024.06.00")) + implementation("io.coil-kt:coil-compose:2.6.0") + + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" } tasks.register('installGitHooks', Copy) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd0deb166f..a92209bbab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -126,6 +126,9 @@ android:name=".account.WebViewLoginActivity" android:theme="@style/AppTheme" /> + + diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java deleted file mode 100644 index 998243f993..0000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2022 Marcel Hibbe - * SPDX-FileCopyrightText: 2021 Andy Scherzinger - * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.adapters.items; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.text.TextUtils; -import android.view.View; - -import com.nextcloud.talk.R; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.databinding.RvItemContactBinding; -import com.nextcloud.talk.extensions.ImageViewExtensionsKt; -import com.nextcloud.talk.models.json.participants.Participant; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; - -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; - -import androidx.core.content.res.ResourcesCompat; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.flexibleadapter.items.IFilterable; -import eu.davidea.flexibleadapter.items.ISectionable; -import eu.davidea.viewholders.FlexibleViewHolder; - -public class ContactItem extends AbstractFlexibleItem implements - ISectionable, IFilterable { - - private final Participant participant; - private final User user; - private GenericTextHeaderItem header; - private final ViewThemeUtils viewThemeUtils; - public boolean isOnline = true; - - public ContactItem(Participant participant, - User user, - GenericTextHeaderItem genericTextHeaderItem, - ViewThemeUtils viewThemeUtils) { - this.participant = participant; - this.user = user; - this.header = genericTextHeaderItem; - this.viewThemeUtils = viewThemeUtils; - } - - @Override - public boolean equals(Object o) { - if (o instanceof ContactItem inItem) { - return participant.getCalculatedActorType() == inItem.getModel().getCalculatedActorType() && - participant.getCalculatedActorId().equals(inItem.getModel().getCalculatedActorId()); - } - return false; - } - - @Override - public int hashCode() { - return participant.hashCode(); - } - - /** - * @return the model object - */ - public Participant getModel() { - return participant; - } - - - @Override - public int getLayoutRes() { - return R.layout.rv_item_contact; - } - - @Override - public ContactItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new ContactItemViewHolder(view, adapter); - } - - @SuppressLint("SetTextI18n") - @Override - public void bindViewHolder(FlexibleAdapter adapter, ContactItemViewHolder holder, int position, List payloads) { - - if (participant.getSelected()) { - viewThemeUtils.platform.colorImageView(holder.binding.checkedImageView); - holder.binding.checkedImageView.setVisibility(View.VISIBLE); - } else { - holder.binding.checkedImageView.setVisibility(View.GONE); - } - - if (!isOnline) { - holder.binding.nameText.setTextColor(ResourcesCompat.getColor( - holder.binding.nameText.getContext().getResources(), - R.color.medium_emphasis_text, - null) - ); - holder.binding.avatarView.setAlpha(0.38f); - } else { - holder.binding.nameText.setTextColor(ResourcesCompat.getColor( - holder.binding.nameText.getContext().getResources(), - R.color.high_emphasis_text, - null) - ); - holder.binding.avatarView.setAlpha(1.0f); - } - - holder.binding.nameText.setText(participant.getDisplayName()); - - if (adapter.hasFilter()) { - viewThemeUtils.talk.themeAndHighlightText(holder.binding.nameText, - participant.getDisplayName(), - String.valueOf(adapter.getFilter(String.class))); - } - - if (TextUtils.isEmpty(participant.getDisplayName()) && - (participant.getType() == Participant.ParticipantType.GUEST || - participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) { - holder.binding.nameText.setText(NextcloudTalkApplication - .Companion - .getSharedApplication() - .getString(R.string.nc_guest)); - } - - if ( - participant.getCalculatedActorType() == Participant.ActorType.GROUPS || - participant.getCalculatedActorType() == Participant.ActorType.CIRCLES) { - - setGenericAvatar(holder, R.drawable.ic_avatar_group, R.drawable.ic_circular_group); - - } else if (participant.getCalculatedActorType() == Participant.ActorType.EMAILS) { - - setGenericAvatar(holder, R.drawable.ic_avatar_mail, R.drawable.ic_circular_mail); - - } else if ( - participant.getCalculatedActorType() == Participant.ActorType.GUESTS || - participant.getType() == Participant.ParticipantType.GUEST || - participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) { - - String displayName; - - if (!TextUtils.isEmpty(participant.getDisplayName())) { - displayName = participant.getDisplayName(); - } else { - displayName = Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication()) - .getResources().getString(R.string.nc_guest); - } - - // absolute fallback to prevent NPE deference - if (displayName == null) { - displayName = "Guest"; - } - - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, user, displayName, true, false); - } else if (participant.getCalculatedActorType() == Participant.ActorType.USERS) { - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, - user, - participant.getCalculatedActorId(), - true, - false); - } - } - - private void setGenericAvatar( - ContactItemViewHolder holder, - int roundPlaceholderDrawable, - int fallbackImageResource) { - Object avatar; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - avatar = viewThemeUtils.talk.themePlaceholderAvatar( - holder.binding.avatarView, - roundPlaceholderDrawable - ); - - } else { - avatar = fallbackImageResource; - } - - ImageViewExtensionsKt.loadUserAvatar(holder.binding.avatarView, avatar); - } - - @Override - public boolean filter(String constraint) { - return participant.getDisplayName() != null && - (Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(participant.getDisplayName().trim()) - .find() || - Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(participant.getCalculatedActorId().trim()) - .find()); - } - - @Override - public GenericTextHeaderItem getHeader() { - return header; - } - - @Override - public void setHeader(GenericTextHeaderItem header) { - this.header = header; - } - - static class ContactItemViewHolder extends FlexibleViewHolder { - - RvItemContactBinding binding; - - /** - * Default constructor. - */ - ContactItemViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - binding = RvItemContactBinding.bind(view); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt new file mode 100644 index 0000000000..e598c6b09d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt @@ -0,0 +1,199 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.os.Build +import android.text.TextUtils +import android.view.View +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ContactItem.ContactItemViewHolder +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemContactBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.Objects +import java.util.regex.Pattern + +class ContactItem( + /** + * @return the model object + */ + val model: Participant, + private val user: User, + private var header: GenericTextHeaderItem?, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), + ISectionable, + IFilterable { + var isOnline: Boolean = true + + override fun equals(o: Any?): Boolean { + if (o is ContactItem) { + return model.calculatedActorType == o.model.calculatedActorType && + model.calculatedActorId == o.model.calculatedActorId + } + return false + } + override fun hashCode(): Int { + return model.hashCode() + } + + override fun filter(constraint: String?): Boolean { + return model.displayName != null && + ( + Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName!!.trim { it <= ' ' }) + .find() || + Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.calculatedActorId!!.trim { it <= ' ' }) + .find() + ) + } + + override fun getLayoutRes(): Int { + return R.layout.rv_item_contact + } + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): ContactItemViewHolder { + return ContactItemViewHolder(view, adapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>?, + holder: ContactItemViewHolder?, + position: Int, + payloads: List? + ) { + if (model.selected) { + holder?.binding?.checkedImageView?.let { viewThemeUtils.platform.colorImageView(it) } + holder?.binding?.checkedImageView?.visibility = View.VISIBLE + } else { + holder?.binding?.checkedImageView?.visibility = View.GONE + } + + if (!isOnline) { + holder?.binding?.nameText?.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.medium_emphasis_text, + null + ) + ) + holder?.binding?.avatarView?.alpha = 0.38f + } else { + holder?.binding?.nameText?.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.high_emphasis_text, + null + ) + ) + holder?.binding?.avatarView?.alpha = 1.0f + } + + holder?.binding?.nameText?.text = model.displayName + + if (adapter != null) { + if (adapter.hasFilter()) { + holder?.binding?.let { + viewThemeUtils.talk.themeAndHighlightText( + it.nameText, + model.displayName, + adapter.getFilter(String::class.java).toString() + ) + } + } + } + + if (TextUtils.isEmpty(model.displayName) && + ( + model.type == Participant.ParticipantType.GUEST || + model.type == Participant.ParticipantType.USER_FOLLOWING_LINK + ) + ) { + holder?.binding?.nameText?.text = sharedApplication!!.getString(R.string.nc_guest) + } + + if (model.calculatedActorType == Participant.ActorType.GROUPS || + model.calculatedActorType == Participant.ActorType.CIRCLES + ) { + setGenericAvatar(holder!!, R.drawable.ic_avatar_group, R.drawable.ic_circular_group) + } else if (model.calculatedActorType == Participant.ActorType.EMAILS) { + setGenericAvatar(holder!!, R.drawable.ic_avatar_mail, R.drawable.ic_circular_mail) + } else if (model.calculatedActorType == Participant.ActorType.GUESTS || + model.type == Participant.ParticipantType.GUEST || model.type == Participant.ParticipantType.GUEST_MODERATOR + ) { + var displayName: String? + + displayName = if (!TextUtils.isEmpty(model.displayName)) { + model.displayName + } else { + Objects.requireNonNull(sharedApplication)!!.resources!!.getString(R.string.nc_guest) + } + + // absolute fallback to prevent NPE deference + if (displayName == null) { + displayName = "Guest" + } + + holder?.binding?.avatarView?.loadUserAvatar(user, displayName, true, false) + } else if (model.calculatedActorType == Participant.ActorType.USERS) { + holder?.binding?.avatarView + ?.loadUserAvatar( + user, + model.calculatedActorId!!, + true, + false + ) + } + } + + private fun setGenericAvatar( + holder: ContactItemViewHolder, + roundPlaceholderDrawable: Int, + fallbackImageResource: Int + ) { + val avatar = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + roundPlaceholderDrawable + ) + } else { + fallbackImageResource + } + + holder.binding.avatarView.loadUserAvatar(avatar) + } + + override fun getHeader(): GenericTextHeaderItem? { + return header + } + + override fun setHeader(p0: GenericTextHeaderItem?) { + this.header = header + } + + class ContactItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { + var binding: RvItemContactBinding = + RvItemContactBinding.bind(view!!) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java deleted file mode 100644 index 34ffc6e18e..0000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2022 Andy Scherzinger - * SPDX-FileCopyrightText: 2017-2018 Mario Danic - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.adapters.items; - -import android.util.Log; -import android.view.View; - -import com.nextcloud.talk.R; -import com.nextcloud.talk.databinding.RvItemTitleHeaderBinding; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; - -import java.util.List; -import java.util.Objects; - -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractHeaderItem; -import eu.davidea.flexibleadapter.items.IFlexible; -import eu.davidea.viewholders.FlexibleViewHolder; - -public class GenericTextHeaderItem extends AbstractHeaderItem { - private static final String TAG = "GenericTextHeaderItem"; - - private final String title; - private final ViewThemeUtils viewThemeUtils; - - public GenericTextHeaderItem(String title, ViewThemeUtils viewThemeUtils) { - super(); - setHidden(false); - setSelectable(false); - this.title = title; - this.viewThemeUtils = viewThemeUtils; - } - - public String getModel() { - return title; - } - - @Override - public boolean equals(Object o) { - if (o instanceof GenericTextHeaderItem) { - GenericTextHeaderItem inItem = (GenericTextHeaderItem) o; - return title.equals(inItem.getModel()); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(title); - } - - @Override - public int getLayoutRes() { - return R.layout.rv_item_title_header; - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, HeaderViewHolder holder, int position, List payloads) { - if (payloads.size() > 0) { - Log.d(TAG, "We have payloads, so ignoring!"); - } else { - holder.binding.titleTextView.setText(title); - viewThemeUtils.platform.colorPrimaryTextViewElement(holder.binding.titleTextView); - } - } - - @Override - public HeaderViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new HeaderViewHolder(view, adapter); - } - - static class HeaderViewHolder extends FlexibleViewHolder { - - RvItemTitleHeaderBinding binding; - - /** - * Default constructor. - */ - HeaderViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter, true); - binding = RvItemTitleHeaderBinding.bind(view); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt new file mode 100644 index 0000000000..d4d0550841 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.util.Log +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.databinding.RvItemTitleHeaderBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.Objects + +open class GenericTextHeaderItem(title: String, viewThemeUtils: ViewThemeUtils) : + AbstractHeaderItem() { + val model: String + private val viewThemeUtils: ViewThemeUtils + + init { + isHidden = false + isSelectable = false + this.model = title + this.viewThemeUtils = viewThemeUtils + } + + override fun equals(o: Any?): Boolean { + if (o is GenericTextHeaderItem) { + return model == o.model + } + return false + } + + override fun hashCode(): Int { + return Objects.hash(model) + } + + override fun getLayoutRes(): Int { + return R.layout.rv_item_title_header + } + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): HeaderViewHolder { + return HeaderViewHolder(view, adapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter?>?, + holder: HeaderViewHolder, + position: Int, + payloads: List + ) { + if (payloads.size > 0) { + Log.d(TAG, "We have payloads, so ignoring!") + } else { + holder.binding.titleTextView.text = model + viewThemeUtils.platform.colorPrimaryTextViewElement(holder.binding.titleTextView) + } + } + + class HeaderViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter, true) { + var binding: RvItemTitleHeaderBinding = + RvItemTitleHeaderBinding.bind(view!!) + } + + companion object { + private const val TAG = "GenericTextHeaderItem" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt new file mode 100644 index 0000000000..c330c33cb7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.api + +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query +import retrofit2.http.QueryMap +import retrofit2.http.Url + +interface NcApiCoroutines { + @GET + @JvmSuppressWildcards + suspend fun getContactsWithSearchParam( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Query("shareTypes[]") listOfShareTypes: List?, + @QueryMap options: Map? + ): AutocompleteOverall + + /* + QueryMap items are as follows: + - "roomType" : "" + - "invite" : "" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room + */ + @POST + suspend fun createRoom( + @Header("Authorization") authorization: String?, + @Url url: String?, + @QueryMap options: Map? + ): RoomOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index 191a837636..ffcb08c4f2 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -125,8 +125,10 @@ class ContactsActivity : existingParticipants = ArrayList() if (intent.hasExtra(BundleKeys.KEY_NEW_CONVERSATION)) { + // adding a new conversation, setting a flag. isNewConversationView = true } else if (intent.hasExtra(BundleKeys.KEY_ADD_PARTICIPANTS)) { + // adding the participants in the conversation also opens this activity, setting a flag for it. isAddingParticipantsView = true conversationToken = intent.getStringExtra(BundleKeys.KEY_TOKEN) if (intent.hasExtra(BundleKeys.KEY_EXISTING_PARTICIPANTS)) { @@ -258,6 +260,7 @@ class ContactsActivity : private fun selectionDone() { if (isAddingParticipantsView) { + // add participants in the view addParticipantsToConversation() } else { // if there is only 1 participant, directly add him while creating room (which can only add 'one') @@ -477,9 +480,11 @@ class ContactsActivity : } override fun onNext(responseBody: ResponseBody) { + // getting contacts val newUserItemList = processAutocompleteUserList(responseBody) userHeaderItems = HashMap() + // getting the contact list from the endpoints. contactItems!!.addAll(newUserItemList) sortUserItems(newUserItemList) @@ -539,7 +544,7 @@ class ContactsActivity : } val newContactItem = ContactItem( participant, - currentUser, + currentUser!!, userHeaderItems[headerTitle], viewThemeUtils ) @@ -551,6 +556,7 @@ class ContactsActivity : return newUserItemList } + // this function displays the title of the contacts activity private fun getHeaderTitle(participant: Participant): String { return when { participant.calculatedActorType == Participant.ActorType.GROUPS -> { diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt new file mode 100644 index 0000000000..0746d7a612 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt @@ -0,0 +1,335 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.openconversations.ListOpenConversationsActivity +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ContactsActivityCompose : ComponentActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var contactsViewModel: ContactsViewModel + + @SuppressLint("UnrememberedMutableState") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java] + + setContent { + MaterialTheme { + val context = LocalContext.current + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.nc_app_product_name), + context = context, + contactsViewModel = contactsViewModel + ) + }, + content = { + val uiState = contactsViewModel.contactsViewState.collectAsState() + Column(Modifier.padding(it)) { + ConversationCreationOptions(context = context) + ContactsList( + contactsUiState = uiState.value, + contactsViewModel = contactsViewModel, + context = context + ) + } + } + ) + } + } + } +} + +@Composable +fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel, context: Context) { + when (contactsUiState) { + is ContactsUiState.None -> { + } + is ContactsUiState.Loading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + is ContactsUiState.Success -> { + val contacts = contactsUiState.contacts + Log.d(CompanionClass.TAG, "Contacts:$contacts") + if (contacts != null) { + ContactsItem(contacts, contactsViewModel, context) + } + } + is ContactsUiState.Error -> { + val errorMessage = contactsUiState.message + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ContactsItem(contacts: List, contactsViewModel: ContactsViewModel, context: Context) { + val groupedContacts: Map> = contacts.groupBy { contact -> + ( + if (contact.source == "users") { + contact.label?.first()?.uppercase() + } else { + contact.source?.replaceFirstChar { actorType -> + actorType.uppercase() + } + } + ).toString() + } + LazyColumn( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + contentPadding = PaddingValues(all = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + groupedContacts.forEach { (initial, contactsForInitial) -> + stickyHeader { + Column { + Surface(Modifier.fillParentMaxWidth()) { + Header(initial) + } + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) + } + } + items(contactsForInitial) { contact -> + ContactItemRow(contact = contact, contactsViewModel = contactsViewModel, context = context) + } + } + } +} + +@Composable +fun Header(header: String) { + Text( + text = header, + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .padding(start = 60.dp), + color = Color.Blue, + fontWeight = FontWeight.Bold + ) +} + +@Composable +fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewModel, context: Context) { + val roomUiState by contactsViewModel.roomViewState.collectAsState() + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + contactsViewModel.createRoom( + CompanionClass.ROOM_TYPE_ONE_ONE, + contact.source!!, + contact.id!!, + null + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) } + val imageRequest = ImageRequest.Builder(context) + .data(imageUri) + .transformations(CircleCropTransformation()) + .error(R.drawable.account_circle_96dp) + .placeholder(R.drawable.account_circle_96dp) + .build() + + AsyncImage( + model = imageRequest, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier.size(width = 45.dp, height = 45.dp) + ) + Text(modifier = Modifier.padding(16.dp), text = contact.label!!) + } + when (roomUiState) { + is RoomUiState.Success -> { + val conversation = (roomUiState as RoomUiState.Success).conversation + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token) + bundle.putString(BundleKeys.KEY_ROOM_ID, conversation?.roomId) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + is RoomUiState.Error -> { + val errorMessage = (roomUiState as RoomUiState.Error).message + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + is RoomUiState.None -> {} + } +} + +@SuppressLint("UnrememberedMutableState") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppBar(title: String, context: Context, contactsViewModel: ContactsViewModel) { + val searchQuery by contactsViewModel.searchQuery.collectAsState() + val searchState = contactsViewModel.searchState.collectAsState() + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + IconButton(onClick = { + (context as? Activity)?.finish() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) + } + }, + actions = { + IconButton(onClick = { + contactsViewModel.updateSearchState(true) + }) { + Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon)) + } + } + ) + if (searchState.value) { + DisplaySearch( + text = searchQuery, + onTextChange = { searchQuery -> + contactsViewModel.updateSearchQuery(query = searchQuery) + contactsViewModel.getContactsFromSearchParams() + }, + contactsViewModel = contactsViewModel + ) + } +} + +@Composable +fun ConversationCreationOptions(context: Context) { + Column { + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + painter = painterResource(R.drawable.baseline_chat_bubble_outline_24), + contentDescription = stringResource(R.string.new_conversation_creation_icon) + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_create_new_conversation), + maxLines = 1, + fontSize = 16.sp + ) + } + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) + .clickable { + val intent = Intent(context, ListOpenConversationsActivity::class.java) + context.startActivity(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + painter = painterResource(R.drawable.baseline_format_list_bulleted_24), + contentDescription = stringResource(R.string.join_open_conversations_icon) + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_join_open_conversations), + fontSize = 16.sp + ) + } + } +} + +class CompanionClass { + companion object { + internal val TAG = ContactsActivityCompose::class.simpleName + internal const val ROOM_TYPE_ONE_ONE = "1" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt new file mode 100644 index 0000000000..1a33472519 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall + +interface ContactsRepository { + suspend fun getContacts(searchQuery: String?, shareTypes: List): AutocompleteOverall + suspend fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?): RoomOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt new file mode 100644 index 0000000000..1148932080 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils + +class ContactsRepositoryImpl( + private val ncApiCoroutines: NcApiCoroutines, + private val userManager: UserManager +) : ContactsRepository { + private val _currentUser = userManager.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) + val apiVersion = ApiUtils.getConversationApiVersion(_currentUser, intArrayOf(ApiUtils.API_V4, 1)) + + override suspend fun getContacts(searchQuery: String?, shareTypes: List): AutocompleteOverall { + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14( + currentUser.baseUrl!!, + searchQuery + ) + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) + modifiedQueryMap["limit"] = 50 + modifiedQueryMap["shareTypes[]"] = shareTypes + val response = ncApiCoroutines.getContactsWithSearchParam( + credentials, + retrofitBucket.url, + shareTypes, + modifiedQueryMap + ) + return response + } + + override suspend fun createRoom( + roomType: String, + sourceType: String, + userId: String, + conversationName: String? + ): RoomOverall { + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + _currentUser.baseUrl, + roomType, + sourceType, + userId, + conversationName + ) + val response = ncApiCoroutines.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + return response + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt new file mode 100644 index 0000000000..955e2da861 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt @@ -0,0 +1,110 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ContactsViewModel @Inject constructor( + private val repository: ContactsRepository, + private val userManager: UserManager +) : ViewModel() { + + private val _contactsViewState = MutableStateFlow(ContactsUiState.None) + val contactsViewState: StateFlow = _contactsViewState + private val _roomViewState = MutableStateFlow(RoomUiState.None) + val roomViewState: StateFlow = _roomViewState + private val _currentUser = userManager.currentUser.blockingGet() + val currentUser: User = _currentUser + private val _searchQuery = MutableStateFlow("") + val searchQuery: StateFlow = _searchQuery + private val shareTypes: MutableList = mutableListOf(ShareType.User.shareType) + val shareTypeList: List = shareTypes + private val _searchState = MutableStateFlow(false) + val searchState: StateFlow = _searchState + + init { + getContactsFromSearchParams() + } + + fun updateSearchQuery(query: String) { + _searchQuery.value = query + } + + fun updateSearchState(searchState: Boolean) { + _searchState.value = searchState + } + + fun updateShareTypes(value: String) { + shareTypes.add(value) + } + + fun getContactsFromSearchParams() { + _contactsViewState.value = ContactsUiState.Loading + viewModelScope.launch { + try { + val contacts = repository.getContacts( + searchQuery.value, + shareTypeList + ) + val contactsList: List? = contacts.ocs!!.data + _contactsViewState.value = ContactsUiState.Success(contactsList) + } catch (exception: Exception) { + _contactsViewState.value = ContactsUiState.Error(exception.message ?: "") + } + } + } + + fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?) { + viewModelScope.launch { + try { + val room = repository.createRoom( + roomType, + sourceType, + userId, + conversationName + ) + + val conversation: Conversation? = room.ocs?.data + _roomViewState.value = RoomUiState.Success(conversation) + } catch (exception: Exception) { + _roomViewState.value = RoomUiState.Error(exception.message ?: "") + } + } + } + + fun getImageUri(avatarId: String, requestBigSize: Boolean): String { + return ApiUtils.getUrlForAvatar( + _currentUser.baseUrl, + avatarId, + requestBigSize + ) + } +} + +sealed class ContactsUiState { + data object None : ContactsUiState() + data object Loading : ContactsUiState() + data class Success(val contacts: List?) : ContactsUiState() + data class Error(val message: String) : ContactsUiState() +} + +sealed class RoomUiState { + data object None : RoomUiState() + data class Success(val conversation: Conversation?) : RoomUiState() + data class Error(val message: String) : RoomUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt b/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt new file mode 100644 index 0000000000..7adaef8bf1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R + +@Composable +fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewModel: ContactsViewModel) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .background(Color.White) + ) { + val keyboardController = LocalSoftwareKeyboardController.current + TextField( + modifier = Modifier + .fillMaxWidth(), + value = text, + onValueChange = { onTextChange(it) }, + placeholder = { + Text( + text = stringResource(R.string.nc_search), + color = Color.DarkGray + ) + }, + + textStyle = TextStyle( + color = Color.Black, + fontSize = 16.sp + ), + singleLine = true, + leadingIcon = { + IconButton( + onClick = { + onTextChange("") + contactsViewModel.updateSearchState(false) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.back_button), + tint = Color.Black + ) + } + }, + + trailingIcon = { + if (text.isNotEmpty()) { + IconButton( + onClick = { + onTextChange("") + } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.close_icon), + tint = Color.Black + ) + } + } + }, + + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + + keyboardActions = KeyboardActions( + onSearch = { + if (text.trim().isNotEmpty()) { + keyboardController?.hide() + } else { + return@KeyboardActions + } + } + ), + maxLines = 1, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + disabledContainerColor = Color.White, + focusedTextColor = Color.Black, + cursorColor = Color.Black + ) + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt new file mode 100644 index 0000000000..fb8f2dc5b3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +enum class ShareType(val shareType: String) { + User("0"), + Group("1"), + Email(""), + Circle(""), + Federated("") +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 3f30b734ec..bda2f14ca6 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -78,7 +78,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.contacts.ContactsActivity +import com.nextcloud.talk.contacts.ContactsActivityCompose import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationsBinding @@ -1072,7 +1072,7 @@ class ConversationsListActivity : conversation.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL private fun showNewConversationsScreen() { - val intent = Intent(context, ContactsActivity::class.java) + val intent = Intent(context, ContactsActivityCompose::class.java) intent.putExtra(KEY_NEW_CONVERSATION, true) startActivity(intent) } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java index 8461856827..f3ee3e05ab 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java @@ -8,6 +8,7 @@ import dagger.Module; import dagger.Provides; + import org.greenrobot.eventbus.EventBus; import javax.inject.Singleton; diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 98eb7b4480..4c882dba2a 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -10,8 +10,11 @@ package com.nextcloud.talk.dagger.modules import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.chat.data.ChatRepository import com.nextcloud.talk.chat.data.network.NetworkChatRepositoryImpl +import com.nextcloud.talk.contacts.ContactsRepository +import com.nextcloud.talk.contacts.ContactsRepositoryImpl import com.nextcloud.talk.conversation.repository.ConversationRepository import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository @@ -45,6 +48,7 @@ import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl import com.nextcloud.talk.translate.repositories.TranslateRepository import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl +import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import dagger.Module @@ -150,4 +154,9 @@ class RepositoryModule { fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository { return InvitationsRepositoryImpl(ncApi) } + + @Provides + fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository { + return ContactsRepositoryImpl(ncApiCoroutines, userManager) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java index 550c2ea7a9..e28a1f175f 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java @@ -14,6 +14,7 @@ import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.R; import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.api.NcApiCoroutines; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.users.UserManager; import com.nextcloud.talk.utils.ApiUtils; @@ -35,6 +36,7 @@ import java.security.cert.CertificateException; import java.util.concurrent.TimeUnit; +import javax.inject.Named; import javax.inject.Singleton; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; @@ -58,6 +60,7 @@ import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; @Module(includes = DatabaseModule.class) public class RestModule { @@ -75,6 +78,13 @@ NcApi provideNcApi(Retrofit retrofit) { return retrofit.create(NcApi.class); } + @Singleton + @Provides + NcApiCoroutines provideNcApiCoroutines(Retrofit retrofit) { + return retrofit.create(NcApiCoroutines.class); + } + + @Singleton @Provides Proxy provideProxy(AppPreferences appPreferences) { diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index e800e90afb..511e6d7e23 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -10,6 +10,7 @@ package com.nextcloud.talk.dagger.modules import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.contacts.ContactsViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel @@ -150,4 +151,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(InvitationsViewModel::class) abstract fun invitationsViewModel(viewModel: InvitationsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ContactsViewModel::class) + abstract fun contactsViewModel(viewModel: ContactsViewModel): ViewModel } diff --git a/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml new file mode 100644 index 0000000000..2a2911fc49 --- /dev/null +++ b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eefd4fc690..ffa22fa1d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -240,6 +240,8 @@ How to translate with transifex: Mark as unread Add to favorites Remove from favorites + Create a new conversation + Join open conversations Added conversation %1$s to favorites Removed conversation %1$s from favorites @@ -259,7 +261,10 @@ How to translate with transifex: Select participants Add participants Done - + User avatar + Back button + New Conversation Creation Icon + Join Open Conversations Icon Please allow permissions Some permissions were denied. @@ -379,6 +384,7 @@ How to translate with transifex: Open conversations There was a problem loading your chats Close + Close Icon Refresh Please check your internet connection @@ -681,6 +687,7 @@ How to translate with transifex: Search … Start typing to search … No search results + Search Icon Tap to open poll diff --git a/build.gradle b/build.gradle index 7490187d1a..ba8116c7dd 100644 --- a/build.gradle +++ b/build.gradle @@ -10,9 +10,12 @@ buildscript { ext { - kotlinVersion = '2.0.0' + kotlinVersion = '1.9.23' + hilt_version = '2.44' + kotlinVersion = '2.0.0' } + repositories { google() gradlePluginPortal() @@ -25,7 +28,8 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}" classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.19' classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.6" - classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.1" + classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.0" + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5e4880e698..adc0221a32 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -8,6 +8,9 @@ + + + @@ -137,6 +140,7 @@ + @@ -219,6 +223,7 @@ + @@ -305,6 +310,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -321,6 +370,14 @@ + + + + + + + + @@ -342,6 +399,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From dc1125414987999313e944f5d644f5d39b3e8996 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Fri, 19 Jul 2024 13:16:03 +0200 Subject: [PATCH 2/3] Refactoring Signed-off-by: sowjanyakch --- .../com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt | 4 +++- app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt index 1148932080..dbe8be1c5f 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt @@ -14,6 +14,7 @@ import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ContactUtils class ContactsRepositoryImpl( private val ncApiCoroutines: NcApiCoroutines, @@ -29,8 +30,9 @@ class ContactsRepositoryImpl( currentUser.baseUrl!!, searchQuery ) + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) - modifiedQueryMap["limit"] = 50 + modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT modifiedQueryMap["shareTypes[]"] = shareTypes val response = ncApiCoroutines.getContactsWithSearchParam( credentials, diff --git a/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt index 879ca078ec..62a7cd5ba1 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt @@ -11,6 +11,8 @@ import android.provider.ContactsContract object ContactUtils { + val MAX_CONTACT_LIMIT = 50 + fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? { var displayName: String? = null val whereName = From 1255a0b5851e37ecbef054c29cb9b1c757ee045b Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Tue, 23 Jul 2024 12:27:30 +0200 Subject: [PATCH 3/3] Add copyright to the drawable Signed-off-by: sowjanyakch --- .../com/nextcloud/talk/utils/ContactUtils.kt | 2 +- .../baseline_chat_bubble_outline_24.xml | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt index 62a7cd5ba1..cf8434993e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt @@ -11,7 +11,7 @@ import android.provider.ContactsContract object ContactUtils { - val MAX_CONTACT_LIMIT = 50 + const val MAX_CONTACT_LIMIT = 50 fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? { var displayName: String? = null diff --git a/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml index 2a2911fc49..50892e8fb5 100644 --- a/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml +++ b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml @@ -1,5 +1,18 @@ - + + + - +