Merge commit 'dc705ccf4027f61ebaefe625a99087cf5641d6a7'

This commit is contained in:
drone 2022-05-13 09:25:43 +00:00
commit 8bc3f80667
8 changed files with 1024 additions and 1057 deletions

View File

@ -0,0 +1,964 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* @author Andy Scherzinger
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers
import android.app.SearchManager
import android.content.Context
import android.graphics.PorterDuff
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
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.WorkManager
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R
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.Companion.sharedApplication
import com.nextcloud.talk.controllers.base.NewBaseController
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerContactsRvBinding
import com.nextcloud.talk.events.OpenConversationEvent
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.models.json.conversations.Conversation
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.ui.dialog.ContactsBottomDialog
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.UserUtils
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.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.parceler.Parcels
import java.io.IOException
import java.util.ArrayList
import java.util.Collections
import java.util.HashMap
import java.util.HashSet
import java.util.Locale
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactsController(args: Bundle) :
NewBaseController(R.layout.controller_contacts_rv),
SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener {
private val binding: ControllerContactsRvBinding by viewBinding(ControllerContactsRvBinding::bind)
@Inject
lateinit var userUtils: UserUtils
@Inject
lateinit var eventBus: EventBus
@Inject
lateinit var ncApi: NcApi
private var credentials: String? = null
private var currentUser: UserEntity? = 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
private var contactsBottomDialog: ContactsBottomDialog? = null
init {
setHasOptionsMenu(true)
sharedApplication!!.componentApplication.inject(this)
if (args.containsKey(BundleKeys.KEY_NEW_CONVERSATION)) {
isNewConversationView = true
existingParticipants = ArrayList()
} else if (args.containsKey(BundleKeys.KEY_ADD_PARTICIPANTS)) {
isAddingParticipantsView = true
conversationToken = args.getString(BundleKeys.KEY_TOKEN)
existingParticipants = ArrayList()
if (args.containsKey(BundleKeys.KEY_EXISTING_PARTICIPANTS)) {
existingParticipants = args.getStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS)
}
}
selectedUserIds = HashSet()
selectedGroupIds = HashSet()
selectedEmails = HashSet()
selectedCircleIds = HashSet()
}
override fun onAttach(view: View) {
super.onAttach(view)
eventBus.register(this)
if (isNewConversationView) {
toggleNewCallHeaderVisibility(!isPublicCall)
}
if (isAddingParticipantsView) {
binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.GONE
} else {
binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.setOnClickListener {
joinConversationViaLink()
}
binding.conversationPrivacyToggle.callHeaderLayout.setOnClickListener {
toggleCallHeader()
}
}
}
override fun onViewBound(view: View) {
super.onViewBound(view)
currentUser = userUtils.currentUser
if (currentUser != null) {
credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
}
if (adapter == null) {
contactItems = ArrayList<AbstractFlexibleItem<*>>()
adapter = FlexibleAdapter(contactItems, activity, false)
if (currentUser != null) {
fetchData()
}
}
setupAdapter()
prepareViews()
}
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) {
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)
} else {
val bundle = Bundle()
val roomType: Conversation.ConversationType = if (isPublicCall) {
Conversation.ConversationType.ROOM_PUBLIC_CALL
} else {
Conversation.ConversationType.ROOM_GROUP_CALL
}
val userIdsArray = ArrayList(selectedUserIds)
val groupIdsArray = ArrayList(selectedGroupIds)
val emailsArray = ArrayList(selectedEmails)
val circleIdsArray = ArrayList(selectedCircleIds)
bundle.putParcelable(BundleKeys.KEY_CONVERSATION_TYPE, Parcels.wrap(roomType))
bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_EMAIL, emailsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_CIRCLE, circleIdsArray)
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_INVITE_USERS)
prepareAndShowBottomSheetWithBundle(bundle)
}
} else {
addParticipantsToConversation()
}
}
private fun createRoom(roomType: String, sourceType: String?, userId: String) {
val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl,
roomType,
sourceType,
userId,
null
)
ncApi.createRoom(
credentials,
retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
)
.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.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
// FIXME once APIv2 or later is used only, the createRoom already returns all the data
ncApi.getRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion, currentUser!!.baseUrl,
roomOverall.getOcs().getData().getToken()
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
bundle.putParcelable(
BundleKeys.KEY_ACTIVE_CONVERSATION,
Parcels.wrap(roomOverall.getOcs().getData())
)
ConductorRemapping.remapChatController(
router, currentUser!!.id,
roomOverall.getOcs().getData().getToken(), bundle, true
)
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
private fun addParticipantsToConversation() {
val userIdsArray: Array<String> = selectedUserIds.toTypedArray<String>()
val groupIdsArray: Array<String> = selectedGroupIds.toTypedArray<String>()
val emailsArray: Array<String> = selectedEmails.toTypedArray<String>()
val circleIdsArray: Array<String> = selectedCircleIds.toTypedArray<String>()
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)
router.popCurrentController()
}
private fun initSearchView() {
if (activity != null) {
val searchManager: SearchManager? = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager?
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
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(activity?.componentName))
}
searchView!!.setOnQueryTextListener(this)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.home -> {
router.popCurrentController()
}
R.id.contacts_selection_done -> {
selectionDone()
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_contacts, menu)
searchItem = menu.findItem(R.id.action_search)
doneMenuItem = menu.findItem(R.id.contacts_selection_done)
initSearchView()
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
checkAndHandleDoneMenuItem()
if (adapter?.hasFilter() == true) {
searchItem!!.expandActionView()
searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false)
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun fetchData() {
dispose(null)
alreadyFetching = true
userHeaderItems = HashMap<String, GenericTextHeaderItem>()
val query = adapter!!.getFilter(String::class.java) as String?
val retrofitBucket: RetrofitBucket =
ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query)
val modifiedQueryMap: HashMap<String, Any?> = HashMap<String, Any?>(retrofitBucket.getQueryMap())
modifiedQueryMap.put("limit", CONTACTS_BATCH_SIZE)
if (isAddingParticipantsView) {
modifiedQueryMap.put("itemId", conversationToken)
}
val shareTypesList: ArrayList<String> = ArrayList()
// users
shareTypesList.add("0")
if (!isAddingParticipantsView) {
// groups
shareTypesList.add("1")
} else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
// groups
shareTypesList.add("1")
// emails
shareTypesList.add("4")
}
if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) {
// circles
shareTypesList.add("7")
}
modifiedQueryMap.put("shareTypes[]", shareTypesList)
ncApi.getContactsWithSearchParam(
credentials,
retrofitBucket.getUrl(), 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) {
val newUserItemList = processAutocompleteUserList(responseBody)
userHeaderItems = HashMap<String, GenericTextHeaderItem>()
contactItems!!.addAll(newUserItemList)
sortUserItems(newUserItemList)
if (newUserItemList.size > 0) {
adapter?.updateDataSet(newUserItemList as List<Nothing>?)
} else {
adapter?.filterItems()
}
try {
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
} catch (npe: NullPointerException) {
// view binding can be null
// since this is called asynchronously and UI might have been destroyed in the meantime
Log.i(TAG, "UI destroyed - view binding already gone")
}
}
override fun onError(e: Throwable) {
try {
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
} catch (npe: NullPointerException) {
// view binding can be null
// since this is called asynchronously and UI might have been destroyed in the meantime
Log.i(TAG, "UI destroyed - view binding already gone")
}
dispose(contactsQueryDisposable)
}
override fun onComplete() {
try {
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
} catch (npe: NullPointerException) {
// view binding can be null
// since this is called asynchronously and UI might have been destroyed in the meantime
Log.i(TAG, "UI destroyed - view binding already gone")
}
dispose(contactsQueryDisposable)
alreadyFetching = false
disengageProgressBar()
}
})
}
private fun processAutocompleteUserList(responseBody: ResponseBody): MutableList<AbstractFlexibleItem<*>> {
try {
val autocompleteOverall: AutocompleteOverall = LoganSquare.parse<AutocompleteOverall>(
responseBody.string(),
AutocompleteOverall::class.java
)
val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList<AutocompleteUser>()
autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!)
return processAutocompleteUserList(autocompleteUsersList)
} catch (ioe: IOException) {
Log.e(TAG, "Parsing response body failed while getting contacts", ioe)
}
return ArrayList<AbstractFlexibleItem<*>>()
}
private fun processAutocompleteUserList(
autocompleteUsersList: ArrayList<AutocompleteUser>
): MutableList<AbstractFlexibleItem<*>> {
var participant: Participant
val actorTypeConverter = EnumActorTypeConverter()
val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList<AbstractFlexibleItem<*>>()
for (autocompleteUser in autocompleteUsersList) {
if (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)
userHeaderItems.put(headerTitle, genericTextHeaderItem)
}
val newContactItem = ContactItem(
participant,
currentUser,
userHeaderItems[headerTitle]
)
if (!contactItems!!.contains(newContactItem)) {
newUserItemList.add(newContactItem)
}
}
}
return newUserItemList
}
private fun getHeaderTitle(participant: Participant): String {
return when {
participant.getActorType() == Participant.ActorType.GROUPS -> {
resources!!.getString(R.string.nc_groups)
}
participant.getActorType() == Participant.ActorType.CIRCLES -> {
resources!!.getString(R.string.nc_circles)
}
else -> {
participant.getDisplayName().substring(0, 1).toUpperCase(Locale.getDefault())
}
}
}
private fun createParticipant(
autocompleteUser: AutocompleteUser,
actorTypeConverter: EnumActorTypeConverter
): Participant {
val participant = Participant()
participant.setActorId(autocompleteUser.id)
participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.source))
participant.setDisplayName(autocompleteUser.label)
participant.setSource(autocompleteUser.source)
return participant
}
private fun sortUserItems(newUserItemList: MutableList<AbstractFlexibleItem<*>>) {
Collections.sort(
newUserItemList,
{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
(o1 as ContactItem).model.getDisplayName()
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
(o2 as ContactItem).model.getDisplayName()
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
val firstSource: String = (o1 as ContactItem).model.getSource()
val secondSource: String = (o2 as ContactItem).model.getSource()
if (firstSource == secondSource) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
// First users
if ("users" == firstSource) {
return@sort -1
} else if ("users" == secondSource) {
return@sort 1
}
// Then groups
if ("groups" == firstSource) {
return@sort -1
} else if ("groups" == secondSource) {
return@sort 1
}
// Then circles
if ("circles" == firstSource) {
return@sort -1
} else if ("circles" == secondSource) {
return@sort 1
}
// Otherwise fall back to name sorting
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
firstName.compareTo(secondName, ignoreCase = true)
}
)
Collections.sort(
contactItems
) { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
(o1 as ContactItem).model.getDisplayName()
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
(o2 as ContactItem).model.getDisplayName()
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
if ("groups" == (o1 as ContactItem).model.getSource() &&
"groups" == (o2 as ContactItem).model.getSource()
) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
} else if ("groups" == (o1 as ContactItem).model.getSource()) {
return@sort -1
} else if ("groups" == (o2 as ContactItem).model.getSource()) {
return@sort 1
}
}
firstName.compareTo(secondName, ignoreCase = true)
}
}
private fun prepareViews() {
layoutManager = SmoothScrollLinearLayoutManager(activity)
binding.controllerGenericRv.recyclerView.layoutManager = layoutManager
binding.controllerGenericRv.recyclerView.setHasFixedSize(true)
binding.controllerGenericRv.recyclerView.adapter = adapter
binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() }
binding.controllerGenericRv.swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary)
binding.controllerGenericRv.swipeRefreshLayout
.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
binding.joinConversationViaLink.joinConversationViaLinkImageView
.background
.setColorFilter(
ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
PorterDuff.Mode.SRC_IN
)
binding.conversationPrivacyToggle.publicCallLink
.background
.setColorFilter(
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null),
PorterDuff.Mode.SRC_IN
)
disengageProgressBar()
}
private fun disengageProgressBar() {
if (!alreadyFetching) {
binding.loadingContent.visibility = View.GONE
binding.controllerGenericRv.root.visibility = View.VISIBLE
if (isNewConversationView) {
binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.VISIBLE
binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.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 onSaveViewState(view: View, outState: Bundle) {
adapter?.onSaveInstanceState(outState)
super.onSaveViewState(view, outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
if (adapter != null) {
adapter?.onRestoreInstanceState(savedViewState)
}
}
override fun onDestroy() {
super.onDestroy()
dispose(null)
}
@Suppress("Detekt.TooGenericExceptionCaught")
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>?)
}
try {
binding.controllerGenericRv.swipeRefreshLayout.isEnabled = !adapter!!.hasFilter()
} catch (npe: NullPointerException) {
// view binding can be null
// since this is called asynchronously and UI might have been destroyed in the meantime
Log.i(TAG, "UI destroyed - view binding already gone")
}
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
}
}
override val title: String
get() = 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)
}
}
private fun prepareAndShowBottomSheetWithBundle(bundle: Bundle) {
// 11: create conversation-enter name for new conversation
// 10: get&join room when enter link
contactsBottomDialog = ContactsBottomDialog(activity!!, bundle)
contactsBottomDialog?.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(openConversationEvent: OpenConversationEvent) {
ConductorRemapping.remapChatController(
router, currentUser!!.id,
openConversationEvent.conversation!!.getToken(),
openConversationEvent.bundle!!, true
)
contactsBottomDialog?.dismiss()
}
override fun onDetach(view: View) {
super.onDetach(view)
eventBus.unregister(this)
}
override fun onItemClick(view: View, position: Int): Boolean {
if (adapter?.getItem(position) is ContactItem) {
if (!isNewConversationView && !isAddingParticipantsView) {
createRoom(adapter?.getItem(position) as ContactItem)
} else {
val participant: Participant = (adapter?.getItem(position) as ContactItem).model
updateSelection((adapter?.getItem(position) as ContactItem))
}
}
return true
}
private fun updateSelection(contactItem: ContactItem) {
contactItem.model.isSelected = !contactItem.model.isSelected
updateSelectionLists(contactItem.model)
if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity") &&
!CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "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.getActorId() == contactItem.model.getActorId() &&
internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
internalParticipant.isSelected
) {
internalParticipant.isSelected = false
selectedGroupIds.remove(internalParticipant.getActorId())
}
}
}
adapter?.notifyDataSetChanged()
checkAndHandleDoneMenuItem()
}
private fun createRoom(contactItem: ContactItem) {
var roomType = "1"
if ("groups" == contactItem.model.getSource()) {
roomType = "2"
}
val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl,
roomType,
null,
contactItem.model.getActorId(),
null
)
ncApi.createRoom(
credentials,
retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
if (activity != null) {
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
bundle.putParcelable(
BundleKeys.KEY_ACTIVE_CONVERSATION,
Parcels.wrap(roomOverall.getOcs().getData())
)
ConductorRemapping.remapChatController(
router,
currentUser!!.id,
roomOverall.getOcs().getData().getToken(),
bundle,
true
)
}
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
private fun updateSelectionLists(participant: Participant) {
if ("groups" == participant.getSource()) {
if (participant.isSelected) {
selectedGroupIds.add(participant.getActorId())
} else {
selectedGroupIds.remove(participant.getActorId())
}
} else if ("emails" == participant.getSource()) {
if (participant.isSelected) {
selectedEmails.add(participant.getActorId())
} else {
selectedEmails.remove(participant.getActorId())
}
} else if ("circles" == participant.getSource()) {
if (participant.isSelected) {
selectedCircleIds.add(participant.getActorId())
} else {
selectedCircleIds.remove(participant.getActorId())
}
} else {
if (participant.isSelected) {
selectedUserIds.add(participant.getActorId())
} else {
selectedUserIds.remove(participant.getActorId())
}
}
}
private fun isValidGroupSelection(
contactItem: ContactItem,
participant: Participant,
adapter: FlexibleAdapter<*>?
): Boolean {
return "groups" == contactItem.model.getSource() && participant.isSelected && adapter?.selectedItemCount!! > 1
}
private fun joinConversationViaLink() {
val bundle = Bundle()
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM)
prepareAndShowBottomSheetWithBundle(bundle)
}
private fun toggleCallHeader() {
toggleNewCallHeaderVisibility(isPublicCall)
isPublicCall = !isPublicCall
if (isPublicCall) {
binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
updateGroupParticipantSelection()
} else {
binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE
}
enableContactForNonPublicCall()
checkAndHandleDoneMenuItem()
adapter?.notifyDataSetChanged()
}
private fun updateGroupParticipantSelection() {
val currentItems: List<AbstractFlexibleItem<*>> = adapter?.currentItems as
List<AbstractFlexibleItem<*>>
var internalParticipant: Participant
for (i in currentItems.indices) {
if (currentItems[i] is ContactItem) {
internalParticipant = (currentItems[i] as ContactItem).model
if (internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
internalParticipant.isSelected
) {
internalParticipant.isSelected = false
selectedGroupIds.remove(internalParticipant.getActorId())
}
}
}
}
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 ("groups" == contactItem.model.getSource()) {
contactItem.isEnabled = !isPublicCall
}
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun toggleNewCallHeaderVisibility(showInitialLayout: Boolean) {
try {
if (showInitialLayout) {
binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.VISIBLE
binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.GONE
} else {
binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.GONE
binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.VISIBLE
}
} catch (npe: NullPointerException) {
// view binding can be null
// since this is called asynchronously and UI might have been destroyed in the meantime
Log.i(TAG, "UI destroyed - view binding already gone")
}
}
companion object {
const val TAG = "ContactsController"
const val RETRIES: Long = 3
const val CONTACTS_BATCH_SIZE: Int = 50
const val HEADER_ELEVATION: Int = 5
}
}

View File

@ -357,6 +357,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
}
}
@Suppress("Detekt.LongMethod")
private fun createUserInfoDetails(userInfo: UserProfileData?): List<UserInfoDetailsItem> {
val result: MutableList<UserInfoDetailsItem> = LinkedList()

View File

@ -32,7 +32,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS
import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
class EnumActorTypeConverter : StringBasedTypeConverter<Participant.ActorType>() {
override fun getFromString(string: String): Participant.ActorType {
override fun getFromString(string: String?): Participant.ActorType {
return when (string) {
"emails" -> EMAILS
"groups" -> GROUPS

View File

@ -121,53 +121,53 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_RE
*/
class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.SystemMessageType>() {
override fun getFromString(string: String): ChatMessage.SystemMessageType {
when (string) {
"conversation_created" -> return CONVERSATION_CREATED
"conversation_renamed" -> return CONVERSATION_RENAMED
"description_set" -> return DESCRIPTION_SET
"description_removed" -> return DESCRIPTION_REMOVED
"call_started" -> return CALL_STARTED
"call_joined" -> return CALL_JOINED
"call_left" -> return CALL_LEFT
"call_ended" -> return CALL_ENDED
"call_ended_everyone" -> return CALL_ENDED_EVERYONE
"call_missed" -> return CALL_MISSED
"call_tried" -> return CALL_TRIED
"read_only_off" -> return READ_ONLY_OFF
"read_only" -> return READ_ONLY
"listable_none" -> return LISTABLE_NONE
"listable_users" -> return LISTABLE_USERS
"listable_all" -> return LISTABLE_ALL
"lobby_none" -> return LOBBY_NONE
"lobby_non_moderators" -> return LOBBY_NON_MODERATORS
"lobby_timer_reached" -> return LOBBY_OPEN_TO_EVERYONE
"guests_allowed" -> return GUESTS_ALLOWED
"guests_disallowed" -> return GUESTS_DISALLOWED
"password_set" -> return PASSWORD_SET
"password_removed" -> return PASSWORD_REMOVED
"user_added" -> return USER_ADDED
"user_removed" -> return USER_REMOVED
"group_added" -> return GROUP_ADDED
"group_removed" -> return GROUP_REMOVED
"circle_added" -> return CIRCLE_ADDED
"circle_removed" -> return CIRCLE_REMOVED
"moderator_promoted" -> return MODERATOR_PROMOTED
"moderator_demoted" -> return MODERATOR_DEMOTED
"guest_moderator_promoted" -> return GUEST_MODERATOR_PROMOTED
"guest_moderator_demoted" -> return GUEST_MODERATOR_DEMOTED
"message_deleted" -> return MESSAGE_DELETED
"file_shared" -> return FILE_SHARED
"object_shared" -> return OBJECT_SHARED
"matterbridge_config_added" -> return MATTERBRIDGE_CONFIG_ADDED
"matterbridge_config_edited" -> return MATTERBRIDGE_CONFIG_EDITED
"matterbridge_config_removed" -> return MATTERBRIDGE_CONFIG_REMOVED
"matterbridge_config_enabled" -> return MATTERBRIDGE_CONFIG_ENABLED
"matterbridge_config_disabled" -> return MATTERBRIDGE_CONFIG_DISABLED
"history_cleared" -> return CLEARED_CHAT
"reaction" -> return REACTION
"reaction_deleted" -> return REACTION_DELETED
"reaction_revoked" -> return REACTION_REVOKED
else -> return DUMMY
return when (string) {
"conversation_created" -> CONVERSATION_CREATED
"conversation_renamed" -> CONVERSATION_RENAMED
"description_set" -> DESCRIPTION_SET
"description_removed" -> DESCRIPTION_REMOVED
"call_started" -> CALL_STARTED
"call_joined" -> CALL_JOINED
"call_left" -> CALL_LEFT
"call_ended" -> CALL_ENDED
"call_ended_everyone" -> CALL_ENDED_EVERYONE
"call_missed" -> CALL_MISSED
"call_tried" -> CALL_TRIED
"read_only_off" -> READ_ONLY_OFF
"read_only" -> READ_ONLY
"listable_none" -> LISTABLE_NONE
"listable_users" -> LISTABLE_USERS
"listable_all" -> LISTABLE_ALL
"lobby_none" -> LOBBY_NONE
"lobby_non_moderators" -> LOBBY_NON_MODERATORS
"lobby_timer_reached" -> LOBBY_OPEN_TO_EVERYONE
"guests_allowed" -> GUESTS_ALLOWED
"guests_disallowed" -> GUESTS_DISALLOWED
"password_set" -> PASSWORD_SET
"password_removed" -> PASSWORD_REMOVED
"user_added" -> USER_ADDED
"user_removed" -> USER_REMOVED
"group_added" -> GROUP_ADDED
"group_removed" -> GROUP_REMOVED
"circle_added" -> CIRCLE_ADDED
"circle_removed" -> CIRCLE_REMOVED
"moderator_promoted" -> MODERATOR_PROMOTED
"moderator_demoted" -> MODERATOR_DEMOTED
"guest_moderator_promoted" -> GUEST_MODERATOR_PROMOTED
"guest_moderator_demoted" -> GUEST_MODERATOR_DEMOTED
"message_deleted" -> MESSAGE_DELETED
"file_shared" -> FILE_SHARED
"object_shared" -> OBJECT_SHARED
"matterbridge_config_added" -> MATTERBRIDGE_CONFIG_ADDED
"matterbridge_config_edited" -> MATTERBRIDGE_CONFIG_EDITED
"matterbridge_config_removed" -> MATTERBRIDGE_CONFIG_REMOVED
"matterbridge_config_enabled" -> MATTERBRIDGE_CONFIG_ENABLED
"matterbridge_config_disabled" -> MATTERBRIDGE_CONFIG_DISABLED
"history_cleared" -> CLEARED_CHAT
"reaction" -> REACTION
"reaction_deleted" -> REACTION_DELETED
"reaction_revoked" -> REACTION_REVOKED
else -> DUMMY
}
}

View File

@ -162,10 +162,8 @@ class MessageActionsDialog(
}
private fun initEmojiBar(hasChatPermission: Boolean) {
if (hasChatPermission &&
CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions") &&
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY !=
currentConversation?.conversationReadOnlyState &&
if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions") &&
isPermitted(hasChatPermission) &&
isReactableMessageType(message)
) {
checkAndSetEmojiSelfReaction(dialogMessageActionsBinding.emojiThumbsUp)
@ -203,6 +201,11 @@ class MessageActionsDialog(
}
}
private fun isPermitted(hasChatPermission: Boolean): Boolean {
return hasChatPermission && Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY !=
currentConversation?.conversationReadOnlyState
}
private fun isReactableMessageType(message: ChatMessage): Boolean {
return !(message.isCommandMessage || message.isDeletedCommentMessage || message.isDeleted)
}

View File

@ -25,6 +25,7 @@ import java.util.HashMap
object DrawableUtils {
@Suppress("Detekt.LongMethod")
fun getDrawableResourceIdForMimeType(mimetype: String?): Int {
var localMimetype = mimetype
val drawableMap = HashMap<String, Int>()

View File

@ -62,14 +62,17 @@
</LinearLayout>
<include
android:id="@+id/conversation_privacy_toggle"
layout="@layout/conversation_privacy_toggle"
android:visibility="gone" />
<include
android:id="@+id/join_conversation_via_link"
layout="@layout/join_conversation_via_link"
android:visibility="gone" />
<include
android:id="@+id/controller_generic_rv"
layout="@layout/controller_generic_rv"
android:visibility="gone" />
</LinearLayout>