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

View File

@ -1,900 +1,83 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* 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.app.SearchManager
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
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 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.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R
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.chat.ChatActivity
import com.nextcloud.talk.conversation.CreateConversationDialogFragment
import com.nextcloud.talk.data.user.model.User
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.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.components.SetupSystemBars
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 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
@AutoInjector(NextcloudTalkApplication::class)
class ContactsActivity :
BaseActivity(),
SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener {
private lateinit var binding: ActivityContactsBinding
class ContactsActivity : BaseActivity() {
@Inject
lateinit var userManager: UserManager
@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
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)
binding = ActivityContactsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
if (savedInstanceState != null) {
if (adapter != null) {
adapter?.onRestoreInstanceState(savedInstanceState)
}
}
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!!
contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
setContent {
val isAddParticipants = intent.getBooleanExtra(BundleKeys.KEY_ADD_PARTICIPANTS, false)
val hideAlreadyAddedParticipants = intent.getBooleanExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, false)
contactsViewModel.updateIsAddParticipants(isAddParticipants)
contactsViewModel.hideAlreadyAddedParticipants(hideAlreadyAddedParticipants)
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()
checkAndHandleDoneMenuItem()
if (adapter?.hasFilter() == true) {
searchItem!!.expandActionView()
searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false)
}
return true
}
val selectedParticipants = remember {
intent?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList()
}.toSet().toMutableList()
contactsViewModel.updateSelectedParticipants(selectedParticipants)
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
)
)
finish()
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "failed AddParticipantsToConversation")
}
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
shareTypesList.add("1")
// emails
shareTypesList.add("4")
}
if (CapabilitiesUtil.hasSpreedFeatureCapability(
currentUser?.capabilities?.spreedCapability!!,
SpreedFeatures.CIRCLES_SUPPORT
)
) {
// 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)
userHeaderItems = HashMap()
// getting the contact list from the endpoints.
contactItems!!.addAll(newUserItemList)
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)
MaterialTheme(
colorScheme = colorScheme
) {
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
ContactsScreen(
contactsViewModel = contactsViewModel,
uiState = uiState.value
)
if (!contactItems!!.contains(newContactItem)) {
newUserItemList.add(newContactItem)
}
SetupSystemBars()
}
}
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
}
}
class CompanionClass {
companion object {
internal val TAG = ContactsActivity::class.simpleName
internal const val ROOM_TYPE_ONE_ONE = "1"
const val KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS: String = "KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS"
}
}

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

View File

@ -36,6 +36,8 @@ class ContactsViewModel @Inject constructor(
private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView
private var hideAlreadyAddedParticipants: Boolean = false
init {
getContactsFromSearchParams()
}
@ -69,6 +71,10 @@ class ContactsViewModel @Inject constructor(
_isAddParticipantsView.value = value
}
fun hideAlreadyAddedParticipants(value: Boolean) {
hideAlreadyAddedParticipants = value
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams() {
_contactsViewState.value = ContactsUiState.Loading
@ -78,7 +84,12 @@ class ContactsViewModel @Inject constructor(
searchQuery.value,
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)
} catch (exception: Exception) {
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "")

View File

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

View File

@ -8,7 +8,6 @@
package com.nextcloud.talk.contacts.components
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -23,75 +22,71 @@ import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.conversationcreation.ConversationCreationActivity
import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
@Composable
fun ConversationCreationOptions(context: Context, contactsViewModel: ContactsViewModel) {
val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsState()
if (!isAddParticipants) {
Column {
Row(
fun ConversationCreationOptions() {
val context = LocalContext.current
Column {
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
.clickable {
val intent = Intent(context, ConversationCreationActivity::class.java)
context.startActivity(intent)
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24),
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
.clickable {
val intent = Intent(context, ConversationCreationActivity::class.java)
context.startActivity(intent)
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24),
modifier = Modifier
.width(40.dp)
.height(40.dp)
.padding(8.dp),
contentDescription = null
)
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = stringResource(R.string.nc_create_new_conversation),
maxLines = 1,
fontSize = 16.sp
)
}
Row(
.width(40.dp)
.height(40.dp)
.padding(8.dp),
contentDescription = null
)
Text(
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
) {
Icon(
Icons.AutoMirrored.Filled.List,
modifier = Modifier
.width(40.dp)
.height(40.dp)
.padding(8.dp),
contentDescription = null
)
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = stringResource(R.string.nc_join_open_conversations),
fontSize = 16.sp
)
}
.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
) {
Icon(
Icons.AutoMirrored.Filled.List,
modifier = Modifier
.width(40.dp)
.height(40.dp)
.padding(8.dp),
contentDescription = null
)
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = stringResource(R.string.nc_join_open_conversations),
fontSize = 16.sp
)
}
}
}

View File

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

View File

@ -11,6 +11,7 @@
package com.nextcloud.talk.conversationinfo
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
@ -21,6 +22,8 @@ import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog
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.bottomsheet.items.BasicListItemWithImage
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.conversationinfo.viewmodel.ConversationInfoViewModel
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.DialogBanParticipantBinding
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.domain.ConversationModel
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.conversations.ConversationEnums
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
@ -126,12 +134,9 @@ class ConversationInfoActivity :
private lateinit var conversationUser: User
private var hasAvatarSpacing: Boolean = false
private lateinit var credentials: String
private var roomDisposable: Disposable? = null
private var participantsDisposable: Disposable? = null
private var databaseStorageModule: DatabaseStorageModule? = null
// private var conversation: Conversation? = null
private var conversation: ConversationModel? = null
private var adapter: FlexibleAdapter<ParticipantItem>? = null
@ -147,10 +152,21 @@ class ConversationInfoActivity :
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
return data.build()
}
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?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@ -190,7 +206,7 @@ class ConversationInfoActivity :
binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog() }
binding.leaveConversationAction.setOnClickListener { leaveConversation() }
binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() }
binding.addParticipantsAction.setOnClickListener { addParticipants() }
binding.addParticipantsAction.setOnClickListener { selectParticipantsToAdd() }
binding.listBansButton.setOnClickListener { listBans() }
viewModel.getRoom(conversationUser, conversationToken)
@ -237,9 +253,11 @@ class ConversationInfoActivity :
when (state) {
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> {
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> {
Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show()
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> {
}
}
@ -249,6 +267,7 @@ class ConversationInfoActivity :
when (uiState) {
is ConversationInfoViewModel.ClearChatHistoryViewState.None -> {
}
is ConversationInfoViewModel.ClearChatHistoryViewState.Success -> {
Snackbar.make(
binding.root,
@ -256,6 +275,7 @@ class ConversationInfoActivity :
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.ClearChatHistoryViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to clear chat history", uiState.exception)
@ -319,7 +339,7 @@ class ConversationInfoActivity :
fun showOptionsMenu() {
if (::optionsMenu.isInitialized) {
optionsMenu.clear()
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) {
menuInflater.inflate(R.menu.menu_conversation_info, optionsMenu)
}
}
@ -383,7 +403,7 @@ class ConversationInfoActivity :
}
private fun setupWebinaryView() {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
webinaryRoomType(conversation!!) &&
ConversationUtils.canModerate(conversation!!, spreedCapabilities)
) {
@ -652,23 +672,89 @@ class ConversationInfoActivity :
.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 existingParticipantsId = arrayListOf<String>()
val existingParticipants = ArrayList<AutocompleteUser>()
for (userItem in userItems) {
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.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)
val intent = Intent(this, ContactsActivity::class.java)
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() {
@ -693,6 +779,7 @@ class ConversationInfoActivity :
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
WorkInfo.State.FAILED -> {
val errorType = workInfo.outputData.getString("error_type")
if (errorType == LeaveConversationWorker.ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT) {
@ -709,6 +796,7 @@ class ConversationInfoActivity :
).show()
}
}
else -> {
}
}
@ -769,7 +857,7 @@ class ConversationInfoActivity :
setUpNotificationSettings(databaseStorageModule!!)
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) &&
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) &&
conversationCopy.remoteServer.isNullOrEmpty()
) {
binding.sharedItemsButton.setOnClickListener { showSharedItems() }
@ -779,7 +867,7 @@ class ConversationInfoActivity :
if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE
if (CapabilitiesUtil.hasSpreedFeatureCapability(
if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation
@ -1011,7 +1099,7 @@ class ConversationInfoActivity :
private fun initExpiringMessageOption() {
if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) &&
CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)
) {
databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration)
val value = databaseStorageModule!!.getString("conversation_settings_dropdown", "")
@ -1034,7 +1122,7 @@ class ConversationInfoActivity :
private fun adjustNotificationLevelUI() {
if (conversation != null) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) {
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f
@ -1069,7 +1157,7 @@ class ConversationInfoActivity :
private fun setProperNotificationValue(conversation: ConversationModel?) {
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(
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.chat.ChatActivity
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.ContactsViewModel
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_TEXT
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_SCROLL_TO_NOTIFICATION_CATEGORY
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
private fun showNewConversationsScreen() {
val intent = Intent(context, ContactsActivityCompose::class.java)
intent.putExtra(KEY_NEW_CONVERSATION, true)
val intent = Intent(context, ContactsActivity::class.java)
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="colorBackgroundDarker">#2C2C2C</color>
<!-- Chat window incoming message text & informational -->
<color name="bg_bottom_sheet">#121212</color>
<color name="bg_message_list_incoming_bubble">#2A2A2A</color>

View File

@ -53,7 +53,6 @@
<color name="call_incomingCallTextView">#E9FFFFFF</color>
<color name="grey950">#111111</color>
<color name="textColorMaxContrast">#767676</color>
<color name="colorBackgroundDarker">#DBDBDB</color>
<color name="fg_default">#666666</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_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_unread">Mark as unread</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>
<!-- Contacts -->
<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>
@ -291,8 +289,6 @@ How to translate with transifex:
<string name="nc_connecting_call">Connecting …</string>
<string name="nc_nick_guest">Guest</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_reconnecting">Reconnecting …</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_participants">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_moderator">Moderator</string>

View File

@ -1,2 +1,2 @@
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>