Merge pull request #3936 from nextcloud/contacts_activity

Contacts activity
This commit is contained in:
Marcel Hibbe 2024-07-23 13:18:58 +02:00 committed by GitHub
commit eb18abef43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1205 additions and 316 deletions

View file

@ -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

View file

@ -2,6 +2,42 @@
<profile version="1.0">
<option name="myName" value="ktlint" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
</component>

View file

@ -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) {

View file

@ -126,6 +126,9 @@
android:name=".account.WebViewLoginActivity"
android:theme="@style/AppTheme" />
<activity android:name=".contacts.ContactsActivityCompose"
android:theme="@style/AppTheme"/>
<activity
android:name=".account.AccountVerificationActivity"
android:theme="@style/AppTheme" />

View file

@ -1,220 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* 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<ContactItem.ContactItemViewHolder> implements
ISectionable<ContactItem.ContactItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
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);
}
}
}

View file

@ -0,0 +1,199 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <infoi@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* 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<ContactItemViewHolder?>(),
ISectionable<ContactItemViewHolder?, GenericTextHeaderItem?>,
IFilterable<String?> {
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<IFlexible<RecyclerView.ViewHolder>>?
): ContactItemViewHolder {
return ContactItemViewHolder(view, adapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
holder: ContactItemViewHolder?,
position: Int,
payloads: List<Any>?
) {
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!!)
}
}

View file

@ -1,89 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* 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<GenericTextHeaderItem.HeaderViewHolder> {
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<IFlexible> adapter, HeaderViewHolder holder, int position, List<Object> 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);
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* 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<GenericTextHeaderItem.HeaderViewHolder>() {
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<IFlexible<RecyclerView.ViewHolder>>?
): HeaderViewHolder {
return HeaderViewHolder(view, adapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>?>?,
holder: HeaderViewHolder,
position: Int,
payloads: List<Any>
) {
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"
}
}

View file

@ -0,0 +1,42 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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<String>?,
@QueryMap options: Map<String, Any>?
): 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<String, String>?
): RoomOverall
}

View file

@ -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 -> {

View file

@ -0,0 +1,335 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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<AutocompleteUser>, contactsViewModel: ContactsViewModel, context: Context) {
val groupedContacts: Map<String, List<AutocompleteUser>> = 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"
}
}

View file

@ -0,0 +1,16 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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<String>): AutocompleteOverall
suspend fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?): RoomOverall
}

View file

@ -0,0 +1,67 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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
import com.nextcloud.talk.utils.ContactUtils
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<String>): AutocompleteOverall {
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14(
currentUser.baseUrl!!,
searchQuery
)
val modifiedQueryMap: HashMap<String, Any> = HashMap(retrofitBucket.queryMap)
modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT
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
}
}

View file

@ -0,0 +1,110 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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>(ContactsUiState.None)
val contactsViewState: StateFlow<ContactsUiState> = _contactsViewState
private val _roomViewState = MutableStateFlow<RoomUiState>(RoomUiState.None)
val roomViewState: StateFlow<RoomUiState> = _roomViewState
private val _currentUser = userManager.currentUser.blockingGet()
val currentUser: User = _currentUser
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery
private val shareTypes: MutableList<String> = mutableListOf(ShareType.User.shareType)
val shareTypeList: List<String> = shareTypes
private val _searchState = MutableStateFlow(false)
val searchState: StateFlow<Boolean> = _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<AutocompleteUser>? = 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<AutocompleteUser>?) : 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()
}

View file

@ -0,0 +1,115 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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
)
)
}
}

View file

@ -0,0 +1,16 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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("")
}

View file

@ -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)
}

View file

@ -8,6 +8,7 @@ package com.nextcloud.talk.dagger.modules;
import dagger.Module;
import dagger.Provides;
import org.greenrobot.eventbus.EventBus;
import javax.inject.Singleton;

View file

@ -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)
}
}

View file

@ -14,6 +14,7 @@ import com.github.aurae.retrofit2.LoganSquareConverterFactory;
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.UnrecoverableKeyException;
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.internal.tls.OkHostnameVerifier;
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 @@ public class RestModule {
return retrofit.create(NcApi.class);
}
@Singleton
@Provides
NcApiCoroutines provideNcApiCoroutines(Retrofit retrofit) {
return retrofit.create(NcApiCoroutines.class);
}
@Singleton
@Provides
Proxy provideProxy(AppPreferences appPreferences) {

View file

@ -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
}

View file

@ -11,6 +11,8 @@ import android.provider.ContactsContract
object ContactUtils {
const val MAX_CONTACT_LIMIT = 50
fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? {
var displayName: String? = null
val whereName =

View file

@ -0,0 +1,18 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2018-2024 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/>
</vector>

View file

@ -240,6 +240,8 @@ How to translate with transifex:
<string name="nc_mark_as_unread">Mark as unread</string>
<string name="nc_add_to_favorites">Add to favorites</string>
<string name="nc_remove_from_favorites">Remove from favorites</string>
<string name="nc_create_new_conversation">Create a new conversation</string>
<string name="nc_join_open_conversations">Join open conversations</string>
<string name="added_to_favorites">Added conversation %1$s to favorites</string>
<string name="removed_from_favorites">Removed conversation %1$s from favorites</string>
@ -259,7 +261,10 @@ How to translate with transifex:
<string name="nc_select_participants">Select participants</string>
<string name="nc_add_participants">Add participants</string>
<string name="nc_contacts_done">Done</string>
<string name="user_avatar">User avatar</string>
<string name="back_button">Back button</string>
<string name="new_conversation_creation_icon">New Conversation Creation Icon</string>
<string name="join_open_conversations_icon">Join Open Conversations Icon</string>
<!-- Permissions -->
<string name="nc_permissions_rationale_dialog_title">Please allow permissions</string>
<string name="nc_permissions_denied">Some permissions were denied.</string>
@ -379,6 +384,7 @@ How to translate with transifex:
<string name="openConversations">Open conversations</string>
<string name="error_loading_chats">There was a problem loading your chats</string>
<string name="close">Close</string>
<string name="close_icon">Close Icon</string>
<string name="nc_refresh">Refresh</string>
<string name="nc_check_your_internet">Please check your internet connection</string>
@ -681,6 +687,7 @@ How to translate with transifex:
<string name="message_search_hint">Search …</string>
<string name="message_search_begin_typing">Start typing to search …</string>
<string name="message_search_begin_empty">No search results</string>
<string name="search_icon">Search Icon</string>
<!-- Polls -->
<string name="message_poll_tap_to_open">Tap to open poll</string>

View file

@ -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

View file

@ -8,6 +8,9 @@
<trust group="com.github.nextcloud-deps" name="android-talk-webrtc" version="110.5481.0" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
<trust file=".*-sources[.]jar" regex="true"/>
<trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
<trust group="com.google.dagger" />
<trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
<trust group="androidx.fragment"/>
</trusted-artifacts>
<ignored-keys>
<ignored-key id="23778689FBFBE047" reason="Key couldn't be downloaded from any key server"/>
@ -137,6 +140,7 @@
<trusting group="androidx.sqlite"/>
<trusting group="androidx.webkit"/>
<trusting group="androidx.work"/>
<trusting group="androidx.compose.foundation"/>
</trusted-key>
<trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1">
<trusting group="org.apache" name="apache"/>
@ -219,6 +223,7 @@
<trusted-key id="E4AC7874F3479A0F1F8ECF9960BB45F36B649F22" group="fr.dudie" name="nominatim-api" version="3.4"/>
<trusted-key id="E77417AC194160A3FABD04969A259C7EE636C5ED" group="^com[.]google($|([.].*))" regex="true"/>
<trusted-key id="E7DC75FC24FB3C8DFE8086AD3D5839A2262CBBFB" group="org.jetbrains.kotlinx"/>
<trusted-key id="64B9B09F164AA0BF88742EB61188B69F6D6259CA" group="com.google.accompanist"/>
<trusted-key id="E82D2EAF2E83830CE1F7F6BE571A5291E827E1C7" group="net.java" name="jvnet-parent" version="3"/>
<trusted-key id="E85AED155021AF8A6C6B7A4A7C7D8456294423BA" group="org.objenesis"/>
<trusted-key id="EAA526B91DD83BA3E1B9636FA730529CA355A63E" group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2.1"/>
@ -305,6 +310,50 @@
<sha256 value="9516c2ae44284ea0bd3d0eade0ee638879b708cbe31e3af92ba96c300604ebc3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.6">
<artifact name="exifinterface-1.3.6.aar">
<sha256 value="1804105e9e05fdd8f760413bad5de498c381aa329f4f9d94c851bc891ac654c6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="exifinterface-1.3.6.module">
<sha256 value="5e9fd84ca3fd3b7706f6856fa4383107de8676bf7c42b7d4b8108949414d6201" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.1.0">
<artifact name="core-1.1.0.pom">
<sha256 value="dae46132cdcd46b798425f7cb78fd65890869b6d26101ccdcd43461a4f51754c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.3.2">
<artifact name="core-1.3.2.pom">
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.5">
<artifact name="junit-1.1.5.aar">
<sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="junit-1.1.5.pom">
<sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.8.0">
<artifact name="core-ktx-1.8.0.module">
<sha256 value="a91bc3e02f209f643dd8275345a9e3003ce20d64fc0760eccf479c1709842f72" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
<artifact name="annotation-experimental-1.3.0.aar">
<sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="annotation-experimental-1.3.0.module">
<sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.5.0">
<artifact name="annotation-1.5.0.jar">
<sha256 value="261fb7c0210858500bab66d34354972a75166ab4182add283780b05513d6ec4a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -321,6 +370,14 @@
<sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.2">
<artifact name="exifinterface-1.3.2.aar">
<sha256 value="8770c180103e0b8c04a07eb4c59153af639b09eca25deae9bdcdaf869d1e5b6b" origin="Generated by Gradle"/>
</artifact>
<artifact name="exifinterface-1.3.2.module">
<sha256 value="10ba5b5cbea7f5c8758be4fdaec60a3545e891a1130d830a442b88cf5336a885" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.0.0">
<artifact name="annotation-experimental-1.0.0.pom">
<sha256 value="6b73ff6608f4b1d6cbab620b65708a382d0b39901cf4e6b0d16f84a1b04d7732" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -342,6 +399,37 @@
<sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose" name="compose-bom" version="2024.06.00">
<artifact name="compose-bom-2024.06.00.pom">
<sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.1.0">
<artifact name="appcompat-1.1.0.pom">
<sha256 value="340d617121f8ef8e02a6680c8f357aa3e542276d0c8a1cdcb6fd98984b2cb7b9" origin="Generated by Gradle" reason="Artifact is not signed"/>