Merge pull request #4786 from nextcloud/removeOldContactsActivity

remove old ContactsActivity
This commit is contained in:
Julius Linus 2025-03-18 12:01:13 -05:00 committed by GitHub
commit 0cde5aae3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 279 additions and 1420 deletions

View File

@ -126,7 +126,7 @@
android:name=".account.WebViewLoginActivity" android:name=".account.WebViewLoginActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity android:name=".contacts.ContactsActivityCompose" <activity android:name=".contacts.ContactsActivity"
android:theme="@style/AppTheme"/> android:theme="@style/AppTheme"/>
<activity android:name=".conversationcreation.ConversationCreationActivity" <activity android:name=".conversationcreation.ConversationCreationActivity"
@ -246,10 +246,6 @@
android:name=".conversationinfoedit.ConversationInfoEditActivity" android:name=".conversationinfoedit.ConversationInfoEditActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".contacts.ContactsActivity"
android:theme="@style/AppTheme" />
<activity <activity
android:name=".openconversations.ListOpenConversationsActivity" android:name=".openconversations.ListOpenConversationsActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />

View File

@ -1,900 +1,83 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.contacts package com.nextcloud.talk.contacts
import android.app.SearchManager import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.InputType import androidx.activity.compose.setContent
import android.util.Log import androidx.compose.material3.MaterialTheme
import android.view.Menu import androidx.compose.runtime.remember
import android.view.MenuItem import androidx.lifecycle.ViewModelProvider
import android.view.View import androidx.lifecycle.compose.collectAsStateWithLifecycle
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuItemCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.adapters.items.ContactItem
import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.conversation.CreateConversationDialogFragment import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.databinding.ActivityContactsBinding
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.events.OpenConversationEvent
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.UserIdUtils.getIdForUser
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.ResponseBody
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.parceler.Parcels
import java.io.IOException
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class ContactsActivity : class ContactsActivity : BaseActivity() {
BaseActivity(),
SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener {
private lateinit var binding: ActivityContactsBinding
@Inject @Inject
lateinit var userManager: UserManager lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var contactsViewModel: ContactsViewModel
@Inject
lateinit var ncApi: NcApi
private var credentials: String? = null
private var currentUser: User? = null
private var contactsQueryDisposable: Disposable? = null
private var cacheQueryDisposable: Disposable? = null
private var adapter: FlexibleAdapter<*>? = null
private var contactItems: MutableList<AbstractFlexibleItem<*>>? = null
private var layoutManager: SmoothScrollLinearLayoutManager? = null
private var searchItem: MenuItem? = null
private var searchView: SearchView? = null
private var isNewConversationView = false
private var isPublicCall = false
private var userHeaderItems: HashMap<String, GenericTextHeaderItem> = HashMap<String, GenericTextHeaderItem>()
private var alreadyFetching = false
private var doneMenuItem: MenuItem? = null
private var selectedUserIds: MutableSet<String> = HashSet()
private var selectedGroupIds: MutableSet<String> = HashSet()
private var selectedCircleIds: MutableSet<String> = HashSet()
private var selectedEmails: MutableSet<String> = HashSet()
private var existingParticipants: List<String>? = null
private var isAddingParticipantsView = false
private var conversationToken: String? = null
@SuppressLint("UnrememberedMutableState")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
binding = ActivityContactsBinding.inflate(layoutInflater) setContent {
setupActionBar() val isAddParticipants = intent.getBooleanExtra(BundleKeys.KEY_ADD_PARTICIPANTS, false)
setContentView(binding.root) val hideAlreadyAddedParticipants = intent.getBooleanExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, false)
setupSystemColors() contactsViewModel.updateIsAddParticipants(isAddParticipants)
contactsViewModel.hideAlreadyAddedParticipants(hideAlreadyAddedParticipants)
if (savedInstanceState != null) { if (isAddParticipants) {
if (adapter != null) { contactsViewModel.updateShareTypes(
adapter?.onRestoreInstanceState(savedInstanceState) listOf(
} ShareType.Group.shareType,
} ShareType.Email.shareType,
ShareType.Circle.shareType
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)) {
existingParticipants = intent.getStringArrayListExtra(BundleKeys.KEY_EXISTING_PARTICIPANTS)
}
}
selectedUserIds = HashSet()
selectedGroupIds = HashSet()
selectedEmails = HashSet()
selectedCircleIds = HashSet()
}
override fun onResume() {
super.onResume()
if (isNewConversationView) {
toggleConversationPrivacyLayout(!isPublicCall)
}
if (isAddingParticipantsView) {
binding.callHeaderLayout.visibility = View.GONE
binding.listOpenConversations.visibility = View.GONE
} else {
binding.listOpenConversations.setOnClickListener {
listOpenConversations()
}
binding.callHeaderLayout.setOnClickListener {
toggleCallHeader()
}
}
currentUser = currentUserProvider.currentUser.blockingGet()
if (currentUser != null) {
credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
}
if (adapter == null) {
contactItems = ArrayList<AbstractFlexibleItem<*>>()
adapter = FlexibleAdapter(contactItems, this, false)
if (currentUser != null) {
fetchData()
}
}
setupAdapter()
prepareViews()
}
private fun setupActionBar() {
setSupportActionBar(binding.contactsToolbar)
binding.contactsToolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent, null)))
supportActionBar?.title = when {
isAddingParticipantsView -> {
resources!!.getString(R.string.nc_add_participants)
}
isNewConversationView -> {
resources!!.getString(R.string.nc_select_participants)
}
else -> {
resources!!.getString(R.string.nc_app_product_name)
}
}
viewThemeUtils.material.themeToolbar(binding.contactsToolbar)
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
adapter?.onSaveInstanceState(bundle)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_contacts, menu)
searchItem = menu.findItem(R.id.action_search)
doneMenuItem = menu.findItem(R.id.contacts_selection_done)
initSearchView()
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
super.onPrepareOptionsMenu(menu)
if (searchItem != null) {
binding.titleTextView.let {
viewThemeUtils.platform.colorToolbarMenuIcon(
it.context,
searchItem!!
)
}
}
checkAndHandleDoneMenuItem()
if (adapter?.hasFilter() == true) {
searchItem!!.expandActionView()
searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.home -> {
finish()
true
}
R.id.contacts_selection_done -> {
selectionDone()
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}
private fun setupAdapter() {
adapter?.setNotifyChangeOfUnfilteredItems(true)?.mode = SelectableAdapter.Mode.MULTI
adapter?.setStickyHeaderElevation(HEADER_ELEVATION)
?.setUnlinkAllItemsOnRemoveHeaders(true)
?.setDisplayHeadersAtStartUp(true)
?.setStickyHeaders(true)
adapter?.addListener(this)
}
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')
if (!isPublicCall && selectedCircleIds.size + selectedGroupIds.size + selectedUserIds.size == 1) {
val userId: String
var sourceType: String? = null
var roomType = "1"
when {
selectedGroupIds.size == 1 -> {
roomType = "2"
userId = selectedGroupIds.iterator().next()
}
selectedCircleIds.size == 1 -> {
roomType = "2"
sourceType = "circles"
userId = selectedCircleIds.iterator().next()
}
else -> {
userId = selectedUserIds.iterator().next()
}
}
createRoom(roomType, sourceType, userId)
// if there are more participants to add, ask for roomName and add them one after another
} else {
val roomType: ConversationEnums.ConversationType = if (isPublicCall) {
ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
} else {
ConversationEnums.ConversationType.ROOM_GROUP_CALL
}
val userIdsArray = ArrayList(selectedUserIds)
val groupIdsArray = ArrayList(selectedGroupIds)
val emailsArray = ArrayList(selectedEmails)
val circleIdsArray = ArrayList(selectedCircleIds)
val createConversationDialog = CreateConversationDialogFragment.newInstance(
userIdsArray,
groupIdsArray,
emailsArray,
circleIdsArray,
Parcels.wrap(roomType)
)
createConversationDialog.show(
supportFragmentManager,
TAG
)
}
}
}
private fun createRoom(roomType: String, sourceType: String?, userId: String) {
val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser!!, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl!!,
roomType,
sourceType,
userId,
null
)
ncApi.createRoom(
credentials,
retrofitBucket.url,
retrofitBucket.queryMap
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
// bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
private fun addParticipantsToConversation() {
val userIdsArray: Array<String> = selectedUserIds.toTypedArray()
val groupIdsArray: Array<String> = selectedGroupIds.toTypedArray()
val emailsArray: Array<String> = selectedEmails.toTypedArray()
val circleIdsArray: Array<String> = selectedCircleIds.toTypedArray()
val data = Data.Builder()
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id!!)
data.putString(BundleKeys.KEY_TOKEN, conversationToken)
data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray)
data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray)
data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailsArray)
data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray)
val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
AddParticipantsToConversation::class.java
).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(addParticipantsToConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.RUNNING -> {
Log.d(TAG, "running AddParticipantsToConversation")
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "success AddParticipantsToConversation")
eventBus.post(
EventStatus(
getIdForUser(currentUser),
EventStatus.EventType.PARTICIPANTS_UPDATE,
true
) )
) )
contactsViewModel.getContactsFromSearchParams()
}
val colorScheme = viewThemeUtils.getColorScheme(this)
val uiState = contactsViewModel.contactsViewState.collectAsStateWithLifecycle()
finish() val selectedParticipants = remember {
} intent?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList()
}.toSet().toMutableList()
contactsViewModel.updateSelectedParticipants(selectedParticipants)
WorkInfo.State.FAILED -> { MaterialTheme(
Log.d(TAG, "failed AddParticipantsToConversation") colorScheme = colorScheme
}
else -> {
}
}
}
}
}
private fun initSearchView() {
val searchManager: SearchManager? = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
viewThemeUtils.talk.themeSearchView(searchView!!)
searchView!!.maxWidth = Int.MAX_VALUE
searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (appPreferences.isKeyboardIncognito == true) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView!!.imeOptions = imeOptions
searchView!!.queryHint = resources!!.getString(R.string.nc_search)
if (searchManager != null) {
searchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName))
}
searchView!!.setOnQueryTextListener(this)
}
}
private fun fetchData() {
dispose(null)
alreadyFetching = true
userHeaderItems = HashMap()
val query = adapter!!.getFilter(String::class.java)
val retrofitBucket: RetrofitBucket =
ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl!!, query)
val modifiedQueryMap: HashMap<String, Any?> = HashMap(retrofitBucket.queryMap)
modifiedQueryMap["limit"] = CONTACTS_BATCH_SIZE
if (isAddingParticipantsView) {
modifiedQueryMap["itemId"] = conversationToken
}
val shareTypesList: ArrayList<String> = ArrayList()
// users
shareTypesList.add("0")
if (!isAddingParticipantsView) {
// groups
shareTypesList.add("1")
} else if (CapabilitiesUtil.hasSpreedFeatureCapability(
currentUser?.capabilities?.spreedCapability!!,
SpreedFeatures.INVITE_GROUPS_AND_MAILS
)
) { ) {
// groups ContactsScreen(
shareTypesList.add("1") contactsViewModel = contactsViewModel,
// emails uiState = uiState.value
shareTypesList.add("4")
}
if (CapabilitiesUtil.hasSpreedFeatureCapability(
currentUser?.capabilities?.spreedCapability!!,
SpreedFeatures.CIRCLES_SUPPORT
) )
) { SetupSystemBars()
// circles
shareTypesList.add("7")
} }
modifiedQueryMap["shareTypes[]"] = shareTypesList
ncApi.getContactsWithSearchParam(
credentials,
retrofitBucket.url,
shareTypesList,
modifiedQueryMap
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(RETRIES)
.subscribe(object : Observer<ResponseBody> {
override fun onSubscribe(d: Disposable) {
contactsQueryDisposable = d
} }
}
override fun onNext(responseBody: ResponseBody) { }
// getting contacts
val newUserItemList = processAutocompleteUserList(responseBody) class CompanionClass {
companion object {
userHeaderItems = HashMap() internal val TAG = ContactsActivity::class.simpleName
// getting the contact list from the endpoints. internal const val ROOM_TYPE_ONE_ONE = "1"
contactItems!!.addAll(newUserItemList) const val KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS: String = "KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS"
sortUserItems(newUserItemList)
if (newUserItemList.size > 0) {
adapter?.updateDataSet(newUserItemList as List<Nothing>?)
} else {
adapter?.filterItems()
}
}
override fun onError(e: Throwable) {
dispose(contactsQueryDisposable)
}
override fun onComplete() {
dispose(contactsQueryDisposable)
alreadyFetching = false
disengageProgressBar()
}
})
}
private fun processAutocompleteUserList(responseBody: ResponseBody): MutableList<AbstractFlexibleItem<*>> {
try {
val autocompleteOverall: AutocompleteOverall = LoganSquare.parse(
responseBody.string(),
AutocompleteOverall::class.java
)
val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList()
autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!)
return processAutocompleteUserList(autocompleteUsersList)
} catch (ioe: IOException) {
Log.e(TAG, "Parsing response body failed while getting contacts", ioe)
}
return ArrayList()
}
private fun processAutocompleteUserList(
autocompleteUsersList: ArrayList<AutocompleteUser>
): MutableList<AbstractFlexibleItem<*>> {
var participant: Participant
val actorTypeConverter = EnumActorTypeConverter()
val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList()
for (autocompleteUser in autocompleteUsersList) {
if (autocompleteUser.id != null &&
autocompleteUser.id != currentUser!!.userId &&
!existingParticipants!!.contains(autocompleteUser.id)
) {
participant = createParticipant(autocompleteUser, actorTypeConverter)
val headerTitle = getHeaderTitle(participant)
var genericTextHeaderItem: GenericTextHeaderItem
if (!userHeaderItems.containsKey(headerTitle)) {
genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils)
userHeaderItems.put(headerTitle, genericTextHeaderItem)
}
val newContactItem = ContactItem(
participant,
currentUser!!,
userHeaderItems[headerTitle],
viewThemeUtils
)
if (!contactItems!!.contains(newContactItem)) {
newUserItemList.add(newContactItem)
}
}
}
return newUserItemList
}
// this function displays the title of the contacts activity
private fun getHeaderTitle(participant: Participant): String {
return when {
participant.calculatedActorType == Participant.ActorType.GROUPS -> {
resources!!.getString(R.string.nc_groups)
}
participant.calculatedActorType == Participant.ActorType.CIRCLES -> {
resources!!.getString(R.string.nc_teams)
}
else -> {
participant.displayName!!.substring(0, 1).uppercase(Locale.getDefault())
}
}
}
private fun createParticipant(
autocompleteUser: AutocompleteUser,
actorTypeConverter: EnumActorTypeConverter
): Participant {
val participant = Participant()
participant.actorId = autocompleteUser.id
participant.actorType = actorTypeConverter.getFromString(autocompleteUser.source)
participant.displayName = autocompleteUser.label
return participant
}
@Suppress("LongMethod")
private fun sortUserItems(newUserItemList: MutableList<AbstractFlexibleItem<*>>) {
newUserItemList.sortWith sort@{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
o1.model.displayName!!
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
o2.model.displayName!!
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
val firstSource: Participant.ActorType = o1.model.actorType!!
val secondSource: Participant.ActorType = o2.model.actorType!!
if (firstSource == secondSource) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
// First users
if (Participant.ActorType.USERS == firstSource) {
return@sort -1
} else if (Participant.ActorType.USERS == secondSource) {
return@sort 1
}
// Then groups
if (Participant.ActorType.GROUPS == firstSource) {
return@sort -1
} else if (Participant.ActorType.GROUPS == secondSource) {
return@sort 1
}
// Then circles
if (Participant.ActorType.CIRCLES == firstSource) {
return@sort -1
} else if (Participant.ActorType.CIRCLES == secondSource) {
return@sort 1
}
// Otherwise fall back to name sorting
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
firstName.compareTo(secondName, ignoreCase = true)
}
contactItems?.sortWith sort@{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
o1.model.displayName!!
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
o2.model.displayName!!
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
if (Participant.ActorType.GROUPS == o1.model.actorType &&
Participant.ActorType.GROUPS == o2.model.actorType
) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
} else if (Participant.ActorType.GROUPS == o1.model.actorType) {
return@sort -1
} else if (Participant.ActorType.GROUPS == o2.model.actorType) {
return@sort 1
}
}
firstName.compareTo(secondName, ignoreCase = true)
}
}
private fun prepareViews() {
layoutManager = SmoothScrollLinearLayoutManager(this)
binding.contactsRv.layoutManager = layoutManager
binding.contactsRv.setHasFixedSize(true)
binding.contactsRv.adapter = adapter
binding.listOpenConversationsImage.background?.setColorFilter(
ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
PorterDuff.Mode.SRC_IN
)
binding.let {
viewThemeUtils.platform.colorImageViewBackgroundAndIcon(it.publicCallLink)
}
disengageProgressBar()
}
private fun disengageProgressBar() {
if (!alreadyFetching) {
binding.contactsRv.visibility = View.VISIBLE
binding.loadingContent.visibility = View.GONE
binding.root.visibility = View.VISIBLE
if (isNewConversationView) {
binding.callHeaderLayout.visibility = View.VISIBLE
}
}
}
private fun dispose(disposable: Disposable?) {
if (disposable != null && !disposable.isDisposed) {
disposable.dispose()
} else if (disposable == null) {
if (contactsQueryDisposable != null && !contactsQueryDisposable!!.isDisposed) {
contactsQueryDisposable!!.dispose()
contactsQueryDisposable = null
}
if (cacheQueryDisposable != null && !cacheQueryDisposable!!.isDisposed) {
cacheQueryDisposable!!.dispose()
cacheQueryDisposable = null
}
}
}
override fun onDestroy() {
super.onDestroy()
dispose(null)
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText != "" && adapter?.hasNewFilter(newText) == true) {
adapter?.setFilter(newText)
fetchData()
} else if (newText == "") {
adapter?.setFilter("")
adapter?.updateDataSet(contactItems as List<Nothing>?)
}
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
return onQueryTextChange(query)
}
private fun checkAndHandleDoneMenuItem() {
if (adapter != null && doneMenuItem != null) {
doneMenuItem!!.isVisible =
selectedCircleIds.size + selectedEmails.size + selectedGroupIds.size + selectedUserIds.size > 0 ||
isPublicCall
} else if (doneMenuItem != null) {
doneMenuItem!!.isVisible = false
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(openConversationEvent: OpenConversationEvent) {
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(openConversationEvent.bundle!!)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
override fun onItemClick(view: View, position: Int): Boolean {
if (adapter?.getItem(position) is ContactItem) {
if (!isNewConversationView && !isAddingParticipantsView) {
createRoom(adapter?.getItem(position) as ContactItem)
} else {
updateSelection((adapter?.getItem(position) as ContactItem))
}
}
return true
}
private fun updateSelection(contactItem: ContactItem) {
contactItem.model.selected = !contactItem.model.selected
updateSelectionLists(contactItem.model)
if (CapabilitiesUtil.hasSpreedFeatureCapability(
currentUser?.capabilities?.spreedCapability!!, SpreedFeatures.LAST_ROOM_ACTIVITY
) &&
!CapabilitiesUtil.hasSpreedFeatureCapability(
currentUser?.capabilities?.spreedCapability!!, SpreedFeatures.INVITE_GROUPS_AND_MAILS
) &&
isValidGroupSelection(contactItem, contactItem.model, adapter)
) {
val currentItems: List<ContactItem> = adapter?.currentItems as List<ContactItem>
var internalParticipant: Participant
for (i in currentItems.indices) {
internalParticipant = currentItems[i].model
if (internalParticipant.calculatedActorId == contactItem.model.calculatedActorId &&
internalParticipant.calculatedActorType == Participant.ActorType.GROUPS &&
internalParticipant.selected
) {
internalParticipant.selected = false
selectedGroupIds.remove(internalParticipant.calculatedActorId!!)
}
}
}
adapter?.notifyDataSetChanged()
checkAndHandleDoneMenuItem()
}
private fun createRoom(contactItem: ContactItem) {
var roomType = "1"
if (Participant.ActorType.GROUPS == contactItem.model.actorType) {
roomType = "2"
}
val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser!!, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl!!,
roomType,
null,
contactItem.model.calculatedActorId,
null
)
ncApi.createRoom(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
// bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
private fun updateSelectionLists(participant: Participant) {
if (Participant.ActorType.GROUPS == participant.actorType) {
if (participant.selected) {
selectedGroupIds.add(participant.calculatedActorId!!)
} else {
selectedGroupIds.remove(participant.calculatedActorId!!)
}
} else if (Participant.ActorType.EMAILS == participant.actorType) {
if (participant.selected) {
selectedEmails.add(participant.calculatedActorId!!)
} else {
selectedEmails.remove(participant.calculatedActorId!!)
}
} else if (Participant.ActorType.CIRCLES == participant.actorType) {
if (participant.selected) {
selectedCircleIds.add(participant.calculatedActorId!!)
} else {
selectedCircleIds.remove(participant.calculatedActorId!!)
}
} else {
if (participant.selected) {
selectedUserIds.add(participant.calculatedActorId!!)
} else {
selectedUserIds.remove(participant.calculatedActorId!!)
}
}
}
private fun isValidGroupSelection(
contactItem: ContactItem,
participant: Participant,
adapter: FlexibleAdapter<*>?
): Boolean {
return Participant.ActorType.GROUPS == contactItem.model.actorType &&
participant.selected && adapter?.selectedItemCount!! > 1
}
private fun listOpenConversations() {
val intent = Intent(this, ListOpenConversationsActivity::class.java)
startActivity(intent)
}
private fun toggleCallHeader() {
toggleConversationPrivacyLayout(isPublicCall)
isPublicCall = !isPublicCall
enableContactForNonPublicCall()
checkAndHandleDoneMenuItem()
adapter?.notifyDataSetChanged()
}
private fun enableContactForNonPublicCall() {
for (i in 0 until adapter!!.itemCount) {
if (adapter?.getItem(i) is ContactItem) {
val contactItem: ContactItem = adapter?.getItem(i) as ContactItem
if (Participant.ActorType.GROUPS == contactItem.model.actorType) {
contactItem.isEnabled = !isPublicCall
}
}
}
}
private fun toggleConversationPrivacyLayout(showInitialLayout: Boolean) {
if (showInitialLayout) {
binding.publicConversationCreate.visibility = View.VISIBLE
binding.publicConversationInfo.visibility = View.GONE
} else {
binding.publicConversationCreate.visibility = View.GONE
binding.publicConversationInfo.visibility = View.VISIBLE
binding.listOpenConversations.visibility = View.GONE
}
}
companion object {
private val TAG = ContactsActivity::class.simpleName
const val RETRIES: Long = 3
const val CONTACTS_BATCH_SIZE: Int = 50
const val HEADER_ELEVATION: Int = 5
} }
} }

View File

@ -1,82 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactsActivityCompose : BaseActivity() {
@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 {
val isAddParticipants = intent.getBooleanExtra("isAddParticipants", false)
contactsViewModel.updateIsAddParticipants(isAddParticipants)
if (isAddParticipants) {
contactsViewModel.updateShareTypes(
listOf(
ShareType.Group.shareType,
ShareType.Email.shareType,
ShareType.Circle.shareType
)
)
contactsViewModel.getContactsFromSearchParams()
}
val colorScheme = viewThemeUtils.getColorScheme(this)
val uiState = contactsViewModel.contactsViewState.collectAsStateWithLifecycle()
val selectedParticipants = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra("selectedParticipants", AutocompleteUser::class.java)
?: emptyList()
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra("selectedParticipants") ?: emptyList()
}
}.toSet().toMutableList()
contactsViewModel.updateSelectedParticipants(selectedParticipants)
MaterialTheme(
colorScheme = colorScheme
) {
ContactsScreen(
contactsViewModel = contactsViewModel,
uiState = uiState.value
)
SetupSystemBars()
}
}
}
}
class CompanionClass {
companion object {
internal val TAG = ContactsActivityCompose::class.simpleName
internal const val ROOM_TYPE_ONE_ONE = "1"
}
}

View File

@ -15,7 +15,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -26,8 +25,6 @@ import com.nextcloud.talk.contacts.components.ConversationCreationOptions
@Composable @Composable
fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiState) { fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiState) {
val context = LocalContext.current
val searchQuery by contactsViewModel.searchQuery.collectAsStateWithLifecycle() val searchQuery by contactsViewModel.searchQuery.collectAsStateWithLifecycle()
val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle() val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle()
val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle() val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle()
@ -57,17 +54,17 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiStat
}, },
content = { content = {
Column( Column(
Modifier.padding(it) Modifier
.padding(it)
.background(colorResource(id = R.color.bg_default)) .background(colorResource(id = R.color.bg_default))
) { ) {
ConversationCreationOptions( if (!isAddParticipants) {
context = context, ConversationCreationOptions()
contactsViewModel = contactsViewModel }
)
ContactsList( ContactsList(
contactsUiState = uiState, contactsUiState = uiState,
contactsViewModel = contactsViewModel, contactsViewModel = contactsViewModel
context = context
) )
} }
} }

View File

@ -36,6 +36,8 @@ class ContactsViewModel @Inject constructor(
private val _isAddParticipantsView = MutableStateFlow(false) private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView
private var hideAlreadyAddedParticipants: Boolean = false
init { init {
getContactsFromSearchParams() getContactsFromSearchParams()
} }
@ -69,6 +71,10 @@ class ContactsViewModel @Inject constructor(
_isAddParticipantsView.value = value _isAddParticipantsView.value = value
} }
fun hideAlreadyAddedParticipants(value: Boolean) {
hideAlreadyAddedParticipants = value
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams() { fun getContactsFromSearchParams() {
_contactsViewState.value = ContactsUiState.Loading _contactsViewState.value = ContactsUiState.Loading
@ -78,7 +84,12 @@ class ContactsViewModel @Inject constructor(
searchQuery.value, searchQuery.value,
shareTypeList shareTypeList
) )
val contactsList: List<AutocompleteUser>? = contacts.ocs!!.data val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList()
if (hideAlreadyAddedParticipants) {
contactsList?.removeAll(selectedParticipants.value)
}
_contactsViewState.value = ContactsUiState.Success(contactsList) _contactsViewState.value = ContactsUiState.Success(contactsList)
} catch (exception: Exception) { } catch (exception: Exception) {
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "") _contactsViewState.value = ContactsUiState.Error(exception.message ?: "")

View File

@ -8,7 +8,6 @@
package com.nextcloud.talk.contacts.components package com.nextcloud.talk.contacts.components
import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -18,15 +17,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.nextcloud.talk.contacts.CompanionClass import com.nextcloud.talk.contacts.CompanionClass
import com.nextcloud.talk.contacts.ContactsUiState import com.nextcloud.talk.contacts.ContactsUiState
import com.nextcloud.talk.contacts.ContactsViewModel import com.nextcloud.talk.contacts.ContactsViewModel
@Composable @Composable
fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel, context: Context) { fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel) {
val context = LocalContext.current
when (contactsUiState) { when (contactsUiState) {
is ContactsUiState.None -> { is ContactsUiState.None -> {
} }
is ContactsUiState.Loading -> { is ContactsUiState.Loading -> {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -35,6 +37,7 @@ fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsVi
CircularProgressIndicator() CircularProgressIndicator()
} }
} }
is ContactsUiState.Success -> { is ContactsUiState.Success -> {
val contacts = contactsUiState.contacts val contacts = contactsUiState.contacts
Log.d(CompanionClass.TAG, "Contacts:$contacts") Log.d(CompanionClass.TAG, "Contacts:$contacts")
@ -42,6 +45,7 @@ fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsVi
ContactsItem(contacts, contactsViewModel, context) ContactsItem(contacts, contactsViewModel, context)
} }
} }
is ContactsUiState.Error -> { is ContactsUiState.Error -> {
val errorMessage = contactsUiState.message val errorMessage = contactsUiState.message
Box( Box(

View File

@ -8,7 +8,6 @@
package com.nextcloud.talk.contacts.components package com.nextcloud.talk.contacts.components
import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -23,23 +22,20 @@ import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.conversationcreation.ConversationCreationActivity import com.nextcloud.talk.conversationcreation.ConversationCreationActivity
import com.nextcloud.talk.openconversations.ListOpenConversationsActivity import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
@Composable @Composable
fun ConversationCreationOptions(context: Context, contactsViewModel: ContactsViewModel) { fun ConversationCreationOptions() {
val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsState() val context = LocalContext.current
if (!isAddParticipants) {
Column { Column {
Row( Row(
modifier = Modifier modifier = Modifier
@ -93,5 +89,4 @@ fun ConversationCreationOptions(context: Context, contactsViewModel: ContactsVie
) )
} }
} }
}
} }

View File

@ -84,9 +84,10 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.components.SetupSystemBars import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.contacts.ContactsActivityCompose
import com.nextcloud.talk.contacts.loadImage import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.PickImage import com.nextcloud.talk.utils.PickImage
@ -163,7 +164,7 @@ fun ConversationCreationScreen(
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val data = result.data val data = result.data
val selectedParticipants = val selectedParticipants =
data?.getParcelableArrayListExtra<AutocompleteUser>("selectedParticipants") data?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList() ?: emptyList()
val participants = selectedParticipants.toMutableList() val participants = selectedParticipants.toMutableList()
conversationCreationViewModel.updateSelectedParticipants(participants) conversationCreationViewModel.updateSelectedParticipants(participants)
@ -378,12 +379,12 @@ fun AddParticipants(
modifier = Modifier modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp) .padding(start = 16.dp, bottom = 16.dp)
.clickable { .clickable {
val intent = Intent(context, ContactsActivityCompose::class.java) val intent = Intent(context, ContactsActivity::class.java)
intent.putParcelableArrayListExtra( intent.putParcelableArrayListExtra(
"selectedParticipants", "selectedParticipants",
participants as ArrayList<AutocompleteUser> participants as ArrayList<AutocompleteUser>
) )
intent.putExtra("isAddParticipants", true) intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
intent.putExtra("isAddParticipantsEdit", true) intent.putExtra("isAddParticipantsEdit", true)
launcher.launch(intent) launcher.launch(intent)
}, },
@ -416,8 +417,8 @@ fun AddParticipants(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
val intent = Intent(context, ContactsActivityCompose::class.java) val intent = Intent(context, ContactsActivity::class.java)
intent.putExtra("isAddParticipants", true) intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
launcher.launch(intent) launcher.launch(intent)
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -608,7 +609,8 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally

View File

@ -11,6 +11,7 @@
package com.nextcloud.talk.conversationinfo package com.nextcloud.talk.conversationinfo
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
@ -21,6 +22,8 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -49,6 +52,8 @@ import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage
import com.nextcloud.talk.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.bottomsheet.items.listItemsWithImage
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity
@ -56,14 +61,17 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityConversationInfoBinding import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
import com.nextcloud.talk.databinding.DialogBanParticipantBinding import com.nextcloud.talk.databinding.DialogBanParticipantBinding
import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.LeaveConversationWorker import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
@ -126,12 +134,9 @@ class ConversationInfoActivity :
private lateinit var conversationUser: User private lateinit var conversationUser: User
private var hasAvatarSpacing: Boolean = false private var hasAvatarSpacing: Boolean = false
private lateinit var credentials: String private lateinit var credentials: String
private var roomDisposable: Disposable? = null
private var participantsDisposable: Disposable? = null private var participantsDisposable: Disposable? = null
private var databaseStorageModule: DatabaseStorageModule? = null private var databaseStorageModule: DatabaseStorageModule? = null
// private var conversation: Conversation? = null
private var conversation: ConversationModel? = null private var conversation: ConversationModel? = null
private var adapter: FlexibleAdapter<ParticipantItem>? = null private var adapter: FlexibleAdapter<ParticipantItem>? = null
@ -147,10 +152,21 @@ class ConversationInfoActivity :
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!) data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
return data.build() return data.build()
} }
return null return null
} }
private val addParticipantsResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
executeIfResultOk(it) { intent ->
val selectedParticipants =
intent?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList()
val participants = selectedParticipants.toMutableList()
addParticipantsToConversation(participants)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@ -190,7 +206,7 @@ class ConversationInfoActivity :
binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog() } binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog() }
binding.leaveConversationAction.setOnClickListener { leaveConversation() } binding.leaveConversationAction.setOnClickListener { leaveConversation() }
binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() } binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() }
binding.addParticipantsAction.setOnClickListener { addParticipants() } binding.addParticipantsAction.setOnClickListener { selectParticipantsToAdd() }
binding.listBansButton.setOnClickListener { listBans() } binding.listBansButton.setOnClickListener { listBans() }
viewModel.getRoom(conversationUser, conversationToken) viewModel.getRoom(conversationUser, conversationToken)
@ -237,9 +253,11 @@ class ConversationInfoActivity :
when (state) { when (state) {
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> { is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> {
} }
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> { is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> {
Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show()
} }
is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> { is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> {
} }
} }
@ -249,6 +267,7 @@ class ConversationInfoActivity :
when (uiState) { when (uiState) {
is ConversationInfoViewModel.ClearChatHistoryViewState.None -> { is ConversationInfoViewModel.ClearChatHistoryViewState.None -> {
} }
is ConversationInfoViewModel.ClearChatHistoryViewState.Success -> { is ConversationInfoViewModel.ClearChatHistoryViewState.Success -> {
Snackbar.make( Snackbar.make(
binding.root, binding.root,
@ -256,6 +275,7 @@ class ConversationInfoActivity :
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).show()
} }
is ConversationInfoViewModel.ClearChatHistoryViewState.Error -> { is ConversationInfoViewModel.ClearChatHistoryViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to clear chat history", uiState.exception) Log.e(TAG, "failed to clear chat history", uiState.exception)
@ -319,7 +339,7 @@ class ConversationInfoActivity :
fun showOptionsMenu() { fun showOptionsMenu() {
if (::optionsMenu.isInitialized) { if (::optionsMenu.isInitialized) {
optionsMenu.clear() optionsMenu.clear()
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) {
menuInflater.inflate(R.menu.menu_conversation_info, optionsMenu) menuInflater.inflate(R.menu.menu_conversation_info, optionsMenu)
} }
} }
@ -383,7 +403,7 @@ class ConversationInfoActivity :
} }
private fun setupWebinaryView() { private fun setupWebinaryView() {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
webinaryRoomType(conversation!!) && webinaryRoomType(conversation!!) &&
ConversationUtils.canModerate(conversation!!, spreedCapabilities) ConversationUtils.canModerate(conversation!!, spreedCapabilities)
) { ) {
@ -652,23 +672,89 @@ class ConversationInfoActivity :
.commit() .commit()
} }
private fun addParticipants() { private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
if (result.resultCode == Activity.RESULT_OK) {
onResult(result.data)
} else {
Log.e(ChatActivity.TAG, "resultCode for received intent was != ok")
}
}
private fun selectParticipantsToAdd() {
val bundle = Bundle() val bundle = Bundle()
val existingParticipantsId = arrayListOf<String>() val existingParticipants = ArrayList<AutocompleteUser>()
for (userItem in userItems) { for (userItem in userItems) {
if (userItem.model.calculatedActorType == USERS) { if (userItem.model.calculatedActorType == USERS) {
existingParticipantsId.add(userItem.model.calculatedActorId!!) val user = AutocompleteUser(
userItem.model.calculatedActorId!!,
userItem.model.displayName,
userItem.model.calculatedActorType.name.lowercase()
)
existingParticipants.add(user)
} }
} }
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true) bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId) bundle.putParcelableArrayList("selectedParticipants", existingParticipants)
bundle.putBoolean(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, true)
bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token) bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token)
val intent = Intent(this, ContactsActivity::class.java) val intent = Intent(this, ContactsActivity::class.java)
intent.putExtras(bundle) intent.putExtras(bundle)
startActivity(intent)
addParticipantsResult.launch(intent)
}
private fun addParticipantsToConversation(participants: List<AutocompleteUser>) {
val groupIdsArray: MutableSet<String> = HashSet()
val emailIdsArray: MutableSet<String> = HashSet()
val circleIdsArray: MutableSet<String> = HashSet()
val userIdsArray: MutableSet<String> = HashSet()
participants.forEach { participant ->
when (participant.source) {
Participant.ActorType.GROUPS.name.lowercase() -> groupIdsArray.add(participant.id!!)
Participant.ActorType.EMAILS.name.lowercase() -> emailIdsArray.add(participant.id!!)
Participant.ActorType.CIRCLES.name.lowercase() -> circleIdsArray.add(participant.id!!)
else -> userIdsArray.add(participant.id!!)
}
}
val data = Data.Builder()
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
data.putString(BundleKeys.KEY_TOKEN, conversationToken)
data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailIdsArray.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray.toTypedArray())
val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
AddParticipantsToConversation::class.java
).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(addParticipantsToConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.RUNNING -> {
Log.d(TAG, "running AddParticipantsToConversation")
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "success AddParticipantsToConversation")
getListOfParticipants()
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "failed AddParticipantsToConversation")
}
else -> {
}
}
}
}
} }
private fun leaveConversation() { private fun leaveConversation() {
@ -693,6 +779,7 @@ class ConversationInfoActivity :
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent) startActivity(intent)
} }
WorkInfo.State.FAILED -> { WorkInfo.State.FAILED -> {
val errorType = workInfo.outputData.getString("error_type") val errorType = workInfo.outputData.getString("error_type")
if (errorType == LeaveConversationWorker.ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT) { if (errorType == LeaveConversationWorker.ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT) {
@ -709,6 +796,7 @@ class ConversationInfoActivity :
).show() ).show()
} }
} }
else -> { else -> {
} }
} }
@ -769,7 +857,7 @@ class ConversationInfoActivity :
setUpNotificationSettings(databaseStorageModule!!) setUpNotificationSettings(databaseStorageModule!!)
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) && if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) &&
conversationCopy.remoteServer.isNullOrEmpty() conversationCopy.remoteServer.isNullOrEmpty()
) { ) {
binding.sharedItemsButton.setOnClickListener { showSharedItems() } binding.sharedItemsButton.setOnClickListener { showSharedItems() }
@ -779,7 +867,7 @@ class ConversationInfoActivity :
if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) { if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE binding.addParticipantsAction.visibility = VISIBLE
if (CapabilitiesUtil.hasSpreedFeatureCapability( if (hasSpreedFeatureCapability(
spreedCapabilities, spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation ) && conversationCopy.canDeleteConversation
@ -1011,7 +1099,7 @@ class ConversationInfoActivity :
private fun initExpiringMessageOption() { private fun initExpiringMessageOption() {
if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) && if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) &&
CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION) hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)
) { ) {
databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration) databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration)
val value = databaseStorageModule!!.getString("conversation_settings_dropdown", "") val value = databaseStorageModule!!.getString("conversation_settings_dropdown", "")
@ -1034,7 +1122,7 @@ class ConversationInfoActivity :
private fun adjustNotificationLevelUI() { private fun adjustNotificationLevelUI() {
if (conversation != null) { if (conversation != null) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) {
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f
@ -1069,7 +1157,7 @@ class ConversationInfoActivity :
private fun setProperNotificationValue(conversation: ConversationModel?) { private fun setProperNotificationValue(conversation: ConversationModel?) {
if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) {
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText(
resources.getString(R.string.nc_notify_me_always) resources.getString(R.string.nc_notify_me_always)
) )

View File

@ -82,7 +82,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.contacts.ContactsActivityCompose import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.contacts.ContactsUiState import com.nextcloud.talk.contacts.ContactsUiState
import com.nextcloud.talk.contacts.ContactsViewModel import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.contacts.RoomUiState import com.nextcloud.talk.contacts.RoomUiState
@ -131,7 +131,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_CONVERSATION
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SCROLL_TO_NOTIFICATION_CATEGORY import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SCROLL_TO_NOTIFICATION_CATEGORY
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT
@ -1253,8 +1252,7 @@ class ConversationsListActivity :
conversation.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL conversation.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
private fun showNewConversationsScreen() { private fun showNewConversationsScreen() {
val intent = Intent(context, ContactsActivityCompose::class.java) val intent = Intent(context, ContactsActivity::class.java)
intent.putExtra(KEY_NEW_CONVERSATION, true)
startActivity(intent) startActivity(intent)
} }

View File

@ -0,0 +1,32 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.extensions
import android.content.Intent
import android.os.Build
import android.os.Parcelable
@Suppress("DEPRECATION")
inline fun <reified T : Parcelable> Intent.getParcelableExtraProvider(identifierParameter: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.getParcelableExtra(identifierParameter, T::class.java)
} else {
this.getParcelableExtra(identifierParameter)
}
}
@Suppress("DEPRECATION")
inline fun <reified T : Parcelable> Intent.getParcelableArrayListExtraProvider(
identifierParameter: String
): java.util.ArrayList<T>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.getParcelableArrayListExtra(identifierParameter, T::class.java)
} else {
this.getParcelableArrayListExtra(identifierParameter)
}
}

View File

@ -1,16 +0,0 @@
<!--
~ 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:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: Tobias Kaminsky <tobias@kaminsky.me>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ededed" />
<corners android:radius="24dp" />
</shape>

View File

@ -1,196 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
~ SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
~ SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/contacts_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/contacts_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/nc_app_product_name" />
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
android:id="@+id/call_header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contacts_appbar">
<RelativeLayout
android:id="@+id/public_conversation_create"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/public_call_link"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/standard_margin"
android:background="@drawable/round_bgnd"
android:contentDescription="@null"
android:padding="@dimen/standard_half_padding"
android:src="@drawable/ic_add_white_24px"
app:tint="@color/white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/public_call_link"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/nc_public_call"
android:textAlignment="viewStart"
android:textAppearance="@style/ListItem"
tools:text="@string/nc_public_call" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/public_conversation_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:minHeight="@dimen/small_item_height"
android:visibility="gone"
tools:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/nc_public_call_explanation"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="@string/nc_public_call_explanation" />
</RelativeLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/list_open_conversations"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/call_header_layout">
<ImageView
android:id="@+id/list_open_conversations_image"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/standard_margin"
android:background="@drawable/round_bgnd"
android:contentDescription="@null"
android:padding="@dimen/standard_half_padding"
android:src="@drawable/baseline_format_list_bulleted_24"
app:tint="@color/white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/list_open_conversations_image"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/nc_list_open_conversations"
android:textAlignment="viewStart"
android:textAppearance="@style/ListItem" />
</RelativeLayout>
<LinearLayout
android:id="@+id/loading_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_marginTop="@dimen/standard_half_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_open_conversations"
tools:visibility="gone">
<include layout="@layout/rv_item_contact_shimmer" />
<include layout="@layout/rv_item_contact_shimmer" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:id="@+id/title_text_view"
android:layout_width="16dp"
android:layout_height="32dp"
android:layout_marginStart="72dp"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
app:custom_color="@color/nc_shimmer_default_color" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?android:attr/listDivider" />
</LinearLayout>
<include layout="@layout/rv_item_contact_shimmer" />
<include layout="@layout/rv_item_contact_shimmer" />
<include layout="@layout/rv_item_contact_shimmer" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_open_conversations">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/fast_scroller_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:alpha="0.5"
android:contentDescription="@null"
android:paddingStart="6dp"
android:paddingEnd="0dp"
android:src="@drawable/fast_scroller_handle" />
<TextView
android:id="@+id/fast_scroller_bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="0dp"
android:layout_toStartOf="@+id/fast_scroller_handle"
android:background="@drawable/fast_scroller_bubble"
android:gravity="start|center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="38sp"
android:visibility="gone"
tools:text="A"
tools:visibility="visible" />
</RelativeLayout>
<View
android:id="@+id/fast_scroller_bar"
android:layout_width="7dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@color/transparent" />
</merge>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:foregroundTint="@color/colorPrimary"
android:layout_margin="8dp"
android:layout_height="24dp"
android:layout_centerInParent="true" />
</RelativeLayout>

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:orientation="vertical">
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:id="@+id/loader_text_view"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/standard_margin"
app:corners="100"
app:custom_color="@color/nc_shimmer_default_color" />
<com.elyeproj.loaderviewlibrary.LoaderTextView
android:id="@+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/loader_text_view"
app:custom_color="@color/nc_shimmer_default_color" />
</RelativeLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:animateLayoutChanges="true"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/nc_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/contacts_selection_done"
android:title="@string/nc_contacts_done"
android:transitionName="userAvatar.transitionTag"
android:visible="false"
app:showAsAction="always" />
</menu>

View File

@ -39,8 +39,6 @@
<color name="chat_separator">#484848</color> <color name="chat_separator">#484848</color>
<color name="colorBackgroundDarker">#2C2C2C</color>
<!-- Chat window incoming message text & informational --> <!-- Chat window incoming message text & informational -->
<color name="bg_bottom_sheet">#121212</color> <color name="bg_bottom_sheet">#121212</color>
<color name="bg_message_list_incoming_bubble">#2A2A2A</color> <color name="bg_message_list_incoming_bubble">#2A2A2A</color>

View File

@ -53,7 +53,6 @@
<color name="call_incomingCallTextView">#E9FFFFFF</color> <color name="call_incomingCallTextView">#E9FFFFFF</color>
<color name="grey950">#111111</color> <color name="grey950">#111111</color>
<color name="textColorMaxContrast">#767676</color> <color name="textColorMaxContrast">#767676</color>
<color name="colorBackgroundDarker">#DBDBDB</color>
<color name="fg_default">#666666</color> <color name="fg_default">#666666</color>
<color name="fg_inverse">#FFFFFF</color> <color name="fg_inverse">#FFFFFF</color>

View File

@ -241,7 +241,6 @@ How to translate with transifex:
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string> <string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_new_conversation">New conversation</string> <string name="nc_new_conversation">New conversation</string>
<string name="nc_list_open_conversations">List open conversations</string>
<string name="nc_mark_as_read">Mark as read</string> <string name="nc_mark_as_read">Mark as read</string>
<string name="nc_mark_as_unread">Mark as unread</string> <string name="nc_mark_as_unread">Mark as unread</string>
<string name="nc_add_to_favorites">Add to favorites</string> <string name="nc_add_to_favorites">Add to favorites</string>
@ -267,7 +266,6 @@ How to translate with transifex:
<string name="nc_no_open_conversations_text">No open conversations that you can join.\nEither there are no open conversations or you already joined all of them.</string> <string name="nc_no_open_conversations_text">No open conversations that you can join.\nEither there are no open conversations or you already joined all of them.</string>
<!-- Contacts --> <!-- Contacts -->
<string name="nc_select_participants">Select participants</string>
<string name="nc_add_participants">Add participants</string> <string name="nc_add_participants">Add participants</string>
<string name="nc_contacts_done">Done</string> <string name="nc_contacts_done">Done</string>
<string name="user_avatar">User avatar</string> <string name="user_avatar">User avatar</string>
@ -291,8 +289,6 @@ How to translate with transifex:
<string name="nc_connecting_call">Connecting …</string> <string name="nc_connecting_call">Connecting …</string>
<string name="nc_nick_guest">Guest</string> <string name="nc_nick_guest">Guest</string>
<string name="nc_public_call_status">Public conversation</string> <string name="nc_public_call_status">Public conversation</string>
<string name="nc_public_call">New public conversation</string>
<string name="nc_public_call_explanation">Public conversations let you invite people from outside through a specially crafted link.</string>
<string name="nc_call_timeout">No response in 45 seconds, tap to try again</string> <string name="nc_call_timeout">No response in 45 seconds, tap to try again</string>
<string name="nc_call_reconnecting">Reconnecting …</string> <string name="nc_call_reconnecting">Reconnecting …</string>
<string name="nc_offline">Currently offline, please check your connectivity</string> <string name="nc_offline">Currently offline, please check your connectivity</string>
@ -481,6 +477,7 @@ How to translate with transifex:
<string name="nc_teams">Teams</string> <string name="nc_teams">Teams</string>
<string name="nc_participants">Participants</string> <string name="nc_participants">Participants</string>
<string name="nc_participants_add">Add participants</string> <string name="nc_participants_add">Add participants</string>
<string name="nc_start_group_chat">Start group chat</string>
<string name="nc_owner">Owner</string> <string name="nc_owner">Owner</string>
<string name="nc_moderator">Moderator</string> <string name="nc_moderator">Moderator</string>

View File

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 9 errors and 100 warnings</span> <span class="mdl-layout-title">Lint Report: 9 errors and 98 warnings</span>