ContactsController.kt 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Marcel Hibbe
  6. * @author Andy Scherzinger
  7. * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  8. * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  9. * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. */
  24. package com.nextcloud.talk.controllers
  25. import android.app.SearchManager
  26. import android.content.Context
  27. import android.graphics.PorterDuff
  28. import android.os.Build
  29. import android.os.Bundle
  30. import android.text.InputType
  31. import android.util.Log
  32. import android.view.Menu
  33. import android.view.MenuInflater
  34. import android.view.MenuItem
  35. import android.view.View
  36. import android.view.inputmethod.EditorInfo
  37. import androidx.appcompat.widget.SearchView
  38. import androidx.core.content.res.ResourcesCompat
  39. import androidx.core.view.MenuItemCompat
  40. import androidx.work.Data
  41. import androidx.work.OneTimeWorkRequest
  42. import androidx.work.WorkManager
  43. import autodagger.AutoInjector
  44. import com.bluelinelabs.logansquare.LoganSquare
  45. import com.nextcloud.talk.R
  46. import com.nextcloud.talk.adapters.items.ContactItem
  47. import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
  48. import com.nextcloud.talk.api.NcApi
  49. import com.nextcloud.talk.application.NextcloudTalkApplication
  50. import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
  51. import com.nextcloud.talk.controllers.base.NewBaseController
  52. import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum
  53. import com.nextcloud.talk.controllers.util.viewBinding
  54. import com.nextcloud.talk.data.user.model.User
  55. import com.nextcloud.talk.databinding.ControllerContactsRvBinding
  56. import com.nextcloud.talk.events.OpenConversationEvent
  57. import com.nextcloud.talk.jobs.AddParticipantsToConversation
  58. import com.nextcloud.talk.models.RetrofitBucket
  59. import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
  60. import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
  61. import com.nextcloud.talk.models.json.conversations.Conversation
  62. import com.nextcloud.talk.models.json.conversations.RoomOverall
  63. import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
  64. import com.nextcloud.talk.models.json.participants.Participant
  65. import com.nextcloud.talk.ui.dialog.ContactsBottomDialog
  66. import com.nextcloud.talk.users.UserManager
  67. import com.nextcloud.talk.utils.ApiUtils
  68. import com.nextcloud.talk.utils.ConductorRemapping
  69. import com.nextcloud.talk.utils.bundle.BundleKeys
  70. import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
  71. import eu.davidea.flexibleadapter.FlexibleAdapter
  72. import eu.davidea.flexibleadapter.SelectableAdapter
  73. import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
  74. import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
  75. import io.reactivex.Observer
  76. import io.reactivex.android.schedulers.AndroidSchedulers
  77. import io.reactivex.disposables.Disposable
  78. import io.reactivex.schedulers.Schedulers
  79. import okhttp3.ResponseBody
  80. import org.greenrobot.eventbus.EventBus
  81. import org.greenrobot.eventbus.Subscribe
  82. import org.greenrobot.eventbus.ThreadMode
  83. import org.parceler.Parcels
  84. import java.io.IOException
  85. import java.util.Collections
  86. import java.util.Locale
  87. import javax.inject.Inject
  88. @AutoInjector(NextcloudTalkApplication::class)
  89. class ContactsController(args: Bundle) :
  90. NewBaseController(R.layout.controller_contacts_rv),
  91. SearchView.OnQueryTextListener,
  92. FlexibleAdapter.OnItemClickListener {
  93. private val binding: ControllerContactsRvBinding by viewBinding(ControllerContactsRvBinding::bind)
  94. @Inject
  95. lateinit var userManager: UserManager
  96. @Inject
  97. lateinit var eventBus: EventBus
  98. @Inject
  99. lateinit var ncApi: NcApi
  100. private var credentials: String? = null
  101. private var currentUser: User? = null
  102. private var contactsQueryDisposable: Disposable? = null
  103. private var cacheQueryDisposable: Disposable? = null
  104. private var adapter: FlexibleAdapter<*>? = null
  105. private var contactItems: MutableList<AbstractFlexibleItem<*>>? = null
  106. private var layoutManager: SmoothScrollLinearLayoutManager? = null
  107. private var searchItem: MenuItem? = null
  108. private var searchView: SearchView? = null
  109. private var isNewConversationView = false
  110. private var isPublicCall = false
  111. private var userHeaderItems: HashMap<String, GenericTextHeaderItem> = HashMap<String, GenericTextHeaderItem>()
  112. private var alreadyFetching = false
  113. private var doneMenuItem: MenuItem? = null
  114. private var selectedUserIds: MutableSet<String> = HashSet()
  115. private var selectedGroupIds: MutableSet<String> = HashSet()
  116. private var selectedCircleIds: MutableSet<String> = HashSet()
  117. private var selectedEmails: MutableSet<String> = HashSet()
  118. private var existingParticipants: List<String>? = null
  119. private var isAddingParticipantsView = false
  120. private var conversationToken: String? = null
  121. private var contactsBottomDialog: ContactsBottomDialog? = null
  122. init {
  123. setHasOptionsMenu(true)
  124. sharedApplication!!.componentApplication.inject(this)
  125. if (args.containsKey(BundleKeys.KEY_NEW_CONVERSATION)) {
  126. isNewConversationView = true
  127. existingParticipants = ArrayList()
  128. } else if (args.containsKey(BundleKeys.KEY_ADD_PARTICIPANTS)) {
  129. isAddingParticipantsView = true
  130. conversationToken = args.getString(BundleKeys.KEY_TOKEN)
  131. existingParticipants = ArrayList()
  132. if (args.containsKey(BundleKeys.KEY_EXISTING_PARTICIPANTS)) {
  133. existingParticipants = args.getStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS)
  134. }
  135. }
  136. selectedUserIds = HashSet()
  137. selectedGroupIds = HashSet()
  138. selectedEmails = HashSet()
  139. selectedCircleIds = HashSet()
  140. }
  141. override fun onAttach(view: View) {
  142. super.onAttach(view)
  143. eventBus.register(this)
  144. if (isNewConversationView) {
  145. toggleConversationPrivacyLayout(!isPublicCall)
  146. }
  147. if (isAddingParticipantsView) {
  148. binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
  149. binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.GONE
  150. } else {
  151. binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.setOnClickListener {
  152. joinConversationViaLink()
  153. }
  154. binding.conversationPrivacyToggle.callHeaderLayout.setOnClickListener {
  155. toggleCallHeader()
  156. }
  157. }
  158. }
  159. override fun onViewBound(view: View) {
  160. super.onViewBound(view)
  161. currentUser = userManager.currentUser.blockingGet()
  162. if (currentUser != null) {
  163. credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
  164. }
  165. if (adapter == null) {
  166. contactItems = ArrayList<AbstractFlexibleItem<*>>()
  167. adapter = FlexibleAdapter(contactItems, activity, false)
  168. if (currentUser != null) {
  169. fetchData()
  170. }
  171. }
  172. setupAdapter()
  173. prepareViews()
  174. }
  175. private fun setupAdapter() {
  176. adapter?.setNotifyChangeOfUnfilteredItems(true)?.mode = SelectableAdapter.Mode.MULTI
  177. adapter?.setStickyHeaderElevation(HEADER_ELEVATION)
  178. ?.setUnlinkAllItemsOnRemoveHeaders(true)
  179. ?.setDisplayHeadersAtStartUp(true)
  180. ?.setStickyHeaders(true)
  181. adapter?.addListener(this)
  182. }
  183. private fun selectionDone() {
  184. if (!isAddingParticipantsView) {
  185. if (!isPublicCall && selectedCircleIds.size + selectedGroupIds.size + selectedUserIds.size == 1) {
  186. val userId: String
  187. var sourceType: String? = null
  188. var roomType = "1"
  189. when {
  190. selectedGroupIds.size == 1 -> {
  191. roomType = "2"
  192. userId = selectedGroupIds.iterator().next()
  193. }
  194. selectedCircleIds.size == 1 -> {
  195. roomType = "2"
  196. sourceType = "circles"
  197. userId = selectedCircleIds.iterator().next()
  198. }
  199. else -> {
  200. userId = selectedUserIds.iterator().next()
  201. }
  202. }
  203. createRoom(roomType, sourceType, userId)
  204. } else {
  205. val bundle = Bundle()
  206. val roomType: Conversation.ConversationType = if (isPublicCall) {
  207. Conversation.ConversationType.ROOM_PUBLIC_CALL
  208. } else {
  209. Conversation.ConversationType.ROOM_GROUP_CALL
  210. }
  211. val userIdsArray = ArrayList(selectedUserIds)
  212. val groupIdsArray = ArrayList(selectedGroupIds)
  213. val emailsArray = ArrayList(selectedEmails)
  214. val circleIdsArray = ArrayList(selectedCircleIds)
  215. bundle.putParcelable(BundleKeys.KEY_CONVERSATION_TYPE, Parcels.wrap(roomType))
  216. bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray)
  217. bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray)
  218. bundle.putStringArrayList(BundleKeys.KEY_INVITED_EMAIL, emailsArray)
  219. bundle.putStringArrayList(BundleKeys.KEY_INVITED_CIRCLE, circleIdsArray)
  220. bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_INVITE_USERS)
  221. prepareAndShowBottomSheetWithBundle(bundle)
  222. }
  223. } else {
  224. addParticipantsToConversation()
  225. }
  226. }
  227. private fun createRoom(roomType: String, sourceType: String?, userId: String) {
  228. val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
  229. val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  230. apiVersion,
  231. currentUser!!.baseUrl,
  232. roomType,
  233. sourceType,
  234. userId,
  235. null
  236. )
  237. ncApi.createRoom(
  238. credentials,
  239. retrofitBucket.url, retrofitBucket.queryMap
  240. )
  241. .subscribeOn(Schedulers.io())
  242. .observeOn(AndroidSchedulers.mainThread())
  243. .subscribe(object : Observer<RoomOverall> {
  244. override fun onSubscribe(d: Disposable) {
  245. // unused atm
  246. }
  247. override fun onNext(roomOverall: RoomOverall) {
  248. val bundle = Bundle()
  249. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
  250. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  251. bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  252. // FIXME once APIv2 or later is used only, the createRoom already returns all the data
  253. ncApi.getRoom(
  254. credentials,
  255. ApiUtils.getUrlForRoom(
  256. apiVersion, currentUser!!.baseUrl,
  257. roomOverall.ocs!!.data!!.token
  258. )
  259. )
  260. .subscribeOn(Schedulers.io())
  261. .observeOn(AndroidSchedulers.mainThread())
  262. .subscribe(object : Observer<RoomOverall> {
  263. override fun onSubscribe(d: Disposable) {
  264. // unused atm
  265. }
  266. override fun onNext(roomOverall: RoomOverall) {
  267. bundle.putParcelable(
  268. BundleKeys.KEY_ACTIVE_CONVERSATION,
  269. Parcels.wrap(roomOverall.ocs!!.data!!)
  270. )
  271. ConductorRemapping.remapChatController(
  272. router, currentUser!!.id!!,
  273. roomOverall.ocs!!.data!!.token!!, bundle, true
  274. )
  275. }
  276. override fun onError(e: Throwable) {
  277. // unused atm
  278. }
  279. override fun onComplete() {
  280. // unused atm
  281. }
  282. })
  283. }
  284. override fun onError(e: Throwable) {
  285. // unused atm
  286. }
  287. override fun onComplete() {
  288. // unused atm
  289. }
  290. })
  291. }
  292. private fun addParticipantsToConversation() {
  293. val userIdsArray: Array<String> = selectedUserIds.toTypedArray()
  294. val groupIdsArray: Array<String> = selectedGroupIds.toTypedArray()
  295. val emailsArray: Array<String> = selectedEmails.toTypedArray()
  296. val circleIdsArray: Array<String> = selectedCircleIds.toTypedArray()
  297. val data = Data.Builder()
  298. data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id!!)
  299. data.putString(BundleKeys.KEY_TOKEN, conversationToken)
  300. data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray)
  301. data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray)
  302. data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailsArray)
  303. data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray)
  304. val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
  305. AddParticipantsToConversation::class.java
  306. ).setInputData(data.build()).build()
  307. WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
  308. router.popCurrentController()
  309. }
  310. private fun initSearchView() {
  311. if (activity != null) {
  312. val searchManager: SearchManager? = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager?
  313. if (searchItem != null) {
  314. searchView = MenuItemCompat.getActionView(searchItem) as SearchView
  315. viewThemeUtils.themeSearchView(searchView!!)
  316. searchView!!.maxWidth = Int.MAX_VALUE
  317. searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
  318. var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
  319. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
  320. appPreferences?.isKeyboardIncognito == true
  321. ) {
  322. imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
  323. }
  324. searchView!!.imeOptions = imeOptions
  325. searchView!!.queryHint = resources!!.getString(R.string.nc_search)
  326. if (searchManager != null) {
  327. searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
  328. }
  329. searchView!!.setOnQueryTextListener(this)
  330. }
  331. }
  332. }
  333. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  334. return when (item.itemId) {
  335. R.id.home -> {
  336. router.popCurrentController()
  337. }
  338. R.id.contacts_selection_done -> {
  339. selectionDone()
  340. true
  341. }
  342. else -> {
  343. super.onOptionsItemSelected(item)
  344. }
  345. }
  346. }
  347. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  348. super.onCreateOptionsMenu(menu, inflater)
  349. inflater.inflate(R.menu.menu_contacts, menu)
  350. searchItem = menu.findItem(R.id.action_search)
  351. doneMenuItem = menu.findItem(R.id.contacts_selection_done)
  352. initSearchView()
  353. }
  354. override fun onPrepareOptionsMenu(menu: Menu) {
  355. super.onPrepareOptionsMenu(menu)
  356. if (searchItem != null) {
  357. viewThemeUtils.colorToolbarMenuIcon(
  358. binding.titleTextView.context,
  359. searchItem!!
  360. )
  361. }
  362. checkAndHandleDoneMenuItem()
  363. if (adapter?.hasFilter() == true) {
  364. searchItem!!.expandActionView()
  365. searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false)
  366. }
  367. }
  368. private fun fetchData() {
  369. dispose(null)
  370. alreadyFetching = true
  371. userHeaderItems = HashMap<String, GenericTextHeaderItem>()
  372. val query = adapter!!.getFilter(String::class.java) as String?
  373. val retrofitBucket: RetrofitBucket =
  374. ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query)
  375. val modifiedQueryMap: HashMap<String, Any?> = HashMap<String, Any?>(retrofitBucket.queryMap)
  376. modifiedQueryMap.put("limit", CONTACTS_BATCH_SIZE)
  377. if (isAddingParticipantsView) {
  378. modifiedQueryMap.put("itemId", conversationToken)
  379. }
  380. val shareTypesList: ArrayList<String> = ArrayList()
  381. // users
  382. shareTypesList.add("0")
  383. if (!isAddingParticipantsView) {
  384. // groups
  385. shareTypesList.add("1")
  386. } else if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
  387. // groups
  388. shareTypesList.add("1")
  389. // emails
  390. shareTypesList.add("4")
  391. }
  392. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "circles-support")) {
  393. // circles
  394. shareTypesList.add("7")
  395. }
  396. modifiedQueryMap.put("shareTypes[]", shareTypesList)
  397. ncApi.getContactsWithSearchParam(
  398. credentials,
  399. retrofitBucket.url, shareTypesList, modifiedQueryMap
  400. )
  401. .subscribeOn(Schedulers.io())
  402. .observeOn(AndroidSchedulers.mainThread())
  403. .retry(RETRIES)
  404. .subscribe(object : Observer<ResponseBody> {
  405. override fun onSubscribe(d: Disposable) {
  406. contactsQueryDisposable = d
  407. }
  408. override fun onNext(responseBody: ResponseBody) {
  409. val newUserItemList = processAutocompleteUserList(responseBody)
  410. userHeaderItems = HashMap<String, GenericTextHeaderItem>()
  411. contactItems!!.addAll(newUserItemList)
  412. sortUserItems(newUserItemList)
  413. if (newUserItemList.size > 0) {
  414. adapter?.updateDataSet(newUserItemList as List<Nothing>?)
  415. } else {
  416. adapter?.filterItems()
  417. }
  418. withNullableControllerViewBinding {
  419. binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
  420. }
  421. }
  422. override fun onError(e: Throwable) {
  423. withNullableControllerViewBinding {
  424. binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
  425. }
  426. dispose(contactsQueryDisposable)
  427. }
  428. override fun onComplete() {
  429. withNullableControllerViewBinding {
  430. binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
  431. }
  432. dispose(contactsQueryDisposable)
  433. alreadyFetching = false
  434. disengageProgressBar()
  435. }
  436. })
  437. }
  438. private fun processAutocompleteUserList(responseBody: ResponseBody): MutableList<AbstractFlexibleItem<*>> {
  439. try {
  440. val autocompleteOverall: AutocompleteOverall = LoganSquare.parse<AutocompleteOverall>(
  441. responseBody.string(),
  442. AutocompleteOverall::class.java
  443. )
  444. val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList<AutocompleteUser>()
  445. autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!)
  446. return processAutocompleteUserList(autocompleteUsersList)
  447. } catch (ioe: IOException) {
  448. Log.e(TAG, "Parsing response body failed while getting contacts", ioe)
  449. }
  450. return ArrayList<AbstractFlexibleItem<*>>()
  451. }
  452. private fun processAutocompleteUserList(
  453. autocompleteUsersList: ArrayList<AutocompleteUser>
  454. ): MutableList<AbstractFlexibleItem<*>> {
  455. var participant: Participant
  456. val actorTypeConverter = EnumActorTypeConverter()
  457. val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList<AbstractFlexibleItem<*>>()
  458. for (autocompleteUser in autocompleteUsersList) {
  459. if (autocompleteUser.id != currentUser!!.userId &&
  460. !existingParticipants!!.contains(autocompleteUser.id!!)
  461. ) {
  462. participant = createParticipant(autocompleteUser, actorTypeConverter)
  463. val headerTitle = getHeaderTitle(participant)
  464. var genericTextHeaderItem: GenericTextHeaderItem
  465. if (!userHeaderItems.containsKey(headerTitle)) {
  466. genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils)
  467. userHeaderItems.put(headerTitle, genericTextHeaderItem)
  468. }
  469. val newContactItem = ContactItem(
  470. participant,
  471. currentUser,
  472. userHeaderItems[headerTitle],
  473. viewThemeUtils
  474. )
  475. if (!contactItems!!.contains(newContactItem)) {
  476. newUserItemList.add(newContactItem)
  477. }
  478. }
  479. }
  480. return newUserItemList
  481. }
  482. private fun getHeaderTitle(participant: Participant): String {
  483. return when {
  484. participant.calculatedActorType == Participant.ActorType.GROUPS -> {
  485. resources!!.getString(R.string.nc_groups)
  486. }
  487. participant.calculatedActorType == Participant.ActorType.CIRCLES -> {
  488. resources!!.getString(R.string.nc_circles)
  489. }
  490. else -> {
  491. participant.displayName!!.substring(0, 1).toUpperCase(Locale.getDefault())
  492. }
  493. }
  494. }
  495. private fun createParticipant(
  496. autocompleteUser: AutocompleteUser,
  497. actorTypeConverter: EnumActorTypeConverter
  498. ): Participant {
  499. val participant = Participant()
  500. participant.actorId = autocompleteUser.id
  501. participant.actorType = actorTypeConverter.getFromString(autocompleteUser.source)
  502. participant.displayName = autocompleteUser.label
  503. participant.source = autocompleteUser.source
  504. return participant
  505. }
  506. private fun sortUserItems(newUserItemList: MutableList<AbstractFlexibleItem<*>>) {
  507. Collections.sort(
  508. newUserItemList,
  509. { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
  510. val firstName: String = if (o1 is ContactItem) {
  511. (o1 as ContactItem).model.displayName!!
  512. } else {
  513. (o1 as GenericTextHeaderItem).model
  514. }
  515. val secondName: String = if (o2 is ContactItem) {
  516. (o2 as ContactItem).model.displayName!!
  517. } else {
  518. (o2 as GenericTextHeaderItem).model
  519. }
  520. if (o1 is ContactItem && o2 is ContactItem) {
  521. val firstSource: String = (o1 as ContactItem).model.source!!
  522. val secondSource: String = (o2 as ContactItem).model.source!!
  523. if (firstSource == secondSource) {
  524. return@sort firstName.compareTo(secondName, ignoreCase = true)
  525. }
  526. // First users
  527. if ("users" == firstSource) {
  528. return@sort -1
  529. } else if ("users" == secondSource) {
  530. return@sort 1
  531. }
  532. // Then groups
  533. if ("groups" == firstSource) {
  534. return@sort -1
  535. } else if ("groups" == secondSource) {
  536. return@sort 1
  537. }
  538. // Then circles
  539. if ("circles" == firstSource) {
  540. return@sort -1
  541. } else if ("circles" == secondSource) {
  542. return@sort 1
  543. }
  544. // Otherwise fall back to name sorting
  545. return@sort firstName.compareTo(secondName, ignoreCase = true)
  546. }
  547. firstName.compareTo(secondName, ignoreCase = true)
  548. }
  549. )
  550. Collections.sort(
  551. contactItems
  552. ) { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
  553. val firstName: String = if (o1 is ContactItem) {
  554. (o1 as ContactItem).model.displayName!!
  555. } else {
  556. (o1 as GenericTextHeaderItem).model
  557. }
  558. val secondName: String = if (o2 is ContactItem) {
  559. (o2 as ContactItem).model.displayName!!
  560. } else {
  561. (o2 as GenericTextHeaderItem).model
  562. }
  563. if (o1 is ContactItem && o2 is ContactItem) {
  564. if ("groups" == (o1 as ContactItem).model.source &&
  565. "groups" == (o2 as ContactItem).model.source
  566. ) {
  567. return@sort firstName.compareTo(secondName, ignoreCase = true)
  568. } else if ("groups" == (o1 as ContactItem).model.source) {
  569. return@sort -1
  570. } else if ("groups" == (o2 as ContactItem).model.source) {
  571. return@sort 1
  572. }
  573. }
  574. firstName.compareTo(secondName, ignoreCase = true)
  575. }
  576. }
  577. private fun prepareViews() {
  578. layoutManager = SmoothScrollLinearLayoutManager(activity)
  579. binding.controllerGenericRv.recyclerView.layoutManager = layoutManager
  580. binding.controllerGenericRv.recyclerView.setHasFixedSize(true)
  581. binding.controllerGenericRv.recyclerView.adapter = adapter
  582. binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() }
  583. viewThemeUtils.themeSwipeRefreshLayout(binding.controllerGenericRv.swipeRefreshLayout)
  584. binding.joinConversationViaLink.joinConversationViaLinkImageView
  585. .background
  586. .setColorFilter(
  587. ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
  588. PorterDuff.Mode.SRC_IN
  589. )
  590. viewThemeUtils.colorImageViewButton(binding.conversationPrivacyToggle.publicCallLink)
  591. disengageProgressBar()
  592. }
  593. private fun disengageProgressBar() {
  594. if (!alreadyFetching) {
  595. binding.loadingContent.visibility = View.GONE
  596. binding.controllerGenericRv.root.visibility = View.VISIBLE
  597. if (isNewConversationView) {
  598. binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.VISIBLE
  599. binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE
  600. }
  601. }
  602. }
  603. private fun dispose(disposable: Disposable?) {
  604. if (disposable != null && !disposable.isDisposed) {
  605. disposable.dispose()
  606. } else if (disposable == null) {
  607. if (contactsQueryDisposable != null && !contactsQueryDisposable!!.isDisposed) {
  608. contactsQueryDisposable!!.dispose()
  609. contactsQueryDisposable = null
  610. }
  611. if (cacheQueryDisposable != null && !cacheQueryDisposable!!.isDisposed) {
  612. cacheQueryDisposable!!.dispose()
  613. cacheQueryDisposable = null
  614. }
  615. }
  616. }
  617. override fun onSaveViewState(view: View, outState: Bundle) {
  618. adapter?.onSaveInstanceState(outState)
  619. super.onSaveViewState(view, outState)
  620. }
  621. override fun onRestoreViewState(view: View, savedViewState: Bundle) {
  622. super.onRestoreViewState(view, savedViewState)
  623. if (adapter != null) {
  624. adapter?.onRestoreInstanceState(savedViewState)
  625. }
  626. }
  627. override fun onDestroy() {
  628. super.onDestroy()
  629. dispose(null)
  630. }
  631. override fun onQueryTextChange(newText: String): Boolean {
  632. if (newText != "" && adapter?.hasNewFilter(newText) == true) {
  633. adapter?.setFilter(newText)
  634. fetchData()
  635. } else if (newText == "") {
  636. adapter?.setFilter("")
  637. adapter?.updateDataSet(contactItems as List<Nothing>?)
  638. }
  639. withNullableControllerViewBinding {
  640. binding.controllerGenericRv.swipeRefreshLayout.isEnabled = !adapter!!.hasFilter()
  641. }
  642. return true
  643. }
  644. override fun onQueryTextSubmit(query: String): Boolean {
  645. return onQueryTextChange(query)
  646. }
  647. private fun checkAndHandleDoneMenuItem() {
  648. if (adapter != null && doneMenuItem != null) {
  649. doneMenuItem!!.isVisible =
  650. selectedCircleIds.size + selectedEmails.size + selectedGroupIds.size + selectedUserIds.size > 0 ||
  651. isPublicCall
  652. } else if (doneMenuItem != null) {
  653. doneMenuItem!!.isVisible = false
  654. }
  655. }
  656. override val title: String
  657. get() = when {
  658. isAddingParticipantsView -> {
  659. resources!!.getString(R.string.nc_add_participants)
  660. }
  661. isNewConversationView -> {
  662. resources!!.getString(R.string.nc_select_participants)
  663. }
  664. else -> {
  665. resources!!.getString(R.string.nc_app_product_name)
  666. }
  667. }
  668. private fun prepareAndShowBottomSheetWithBundle(bundle: Bundle) {
  669. // 11: create conversation-enter name for new conversation
  670. // 10: get&join room when enter link
  671. contactsBottomDialog = ContactsBottomDialog(activity!!, bundle)
  672. contactsBottomDialog?.show()
  673. }
  674. @Subscribe(threadMode = ThreadMode.MAIN)
  675. fun onMessageEvent(openConversationEvent: OpenConversationEvent) {
  676. ConductorRemapping.remapChatController(
  677. router, currentUser!!.id!!,
  678. openConversationEvent.conversation!!.token!!,
  679. openConversationEvent.bundle!!, true
  680. )
  681. contactsBottomDialog?.dismiss()
  682. }
  683. override fun onDetach(view: View) {
  684. super.onDetach(view)
  685. eventBus.unregister(this)
  686. }
  687. override fun onItemClick(view: View, position: Int): Boolean {
  688. if (adapter?.getItem(position) is ContactItem) {
  689. if (!isNewConversationView && !isAddingParticipantsView) {
  690. createRoom(adapter?.getItem(position) as ContactItem)
  691. } else {
  692. val participant: Participant = (adapter?.getItem(position) as ContactItem).model
  693. updateSelection((adapter?.getItem(position) as ContactItem))
  694. }
  695. }
  696. return true
  697. }
  698. private fun updateSelection(contactItem: ContactItem) {
  699. contactItem.model.selected = !contactItem.model.selected
  700. updateSelectionLists(contactItem.model)
  701. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "last-room-activity") &&
  702. !CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") &&
  703. isValidGroupSelection(contactItem, contactItem.model, adapter)
  704. ) {
  705. val currentItems: List<ContactItem> = adapter?.currentItems as List<ContactItem>
  706. var internalParticipant: Participant
  707. for (i in currentItems.indices) {
  708. internalParticipant = currentItems[i].model
  709. if (internalParticipant.calculatedActorId == contactItem.model.calculatedActorId &&
  710. internalParticipant.calculatedActorType == Participant.ActorType.GROUPS &&
  711. internalParticipant.selected
  712. ) {
  713. internalParticipant.selected = false
  714. selectedGroupIds.remove(internalParticipant.calculatedActorId!!)
  715. }
  716. }
  717. }
  718. adapter?.notifyDataSetChanged()
  719. checkAndHandleDoneMenuItem()
  720. }
  721. private fun createRoom(contactItem: ContactItem) {
  722. var roomType = "1"
  723. if ("groups" == contactItem.model.source) {
  724. roomType = "2"
  725. }
  726. val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
  727. val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  728. apiVersion,
  729. currentUser!!.baseUrl,
  730. roomType,
  731. null,
  732. contactItem.model.calculatedActorId,
  733. null
  734. )
  735. ncApi.createRoom(
  736. credentials,
  737. retrofitBucket.url, retrofitBucket.queryMap
  738. )
  739. .subscribeOn(Schedulers.io())
  740. .observeOn(AndroidSchedulers.mainThread())
  741. .subscribe(object : Observer<RoomOverall> {
  742. override fun onSubscribe(d: Disposable) {
  743. // unused atm
  744. }
  745. override fun onNext(roomOverall: RoomOverall) {
  746. if (activity != null) {
  747. val bundle = Bundle()
  748. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
  749. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  750. bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  751. bundle.putParcelable(
  752. BundleKeys.KEY_ACTIVE_CONVERSATION,
  753. Parcels.wrap(roomOverall.ocs!!.data!!)
  754. )
  755. ConductorRemapping.remapChatController(
  756. router,
  757. currentUser!!.id!!,
  758. roomOverall.ocs!!.data!!.token!!,
  759. bundle,
  760. true
  761. )
  762. }
  763. }
  764. override fun onError(e: Throwable) {
  765. // unused atm
  766. }
  767. override fun onComplete() {
  768. // unused atm
  769. }
  770. })
  771. }
  772. private fun updateSelectionLists(participant: Participant) {
  773. if ("groups" == participant.source) {
  774. if (participant.selected) {
  775. selectedGroupIds.add(participant.calculatedActorId!!)
  776. } else {
  777. selectedGroupIds.remove(participant.calculatedActorId!!)
  778. }
  779. } else if ("emails" == participant.source) {
  780. if (participant.selected) {
  781. selectedEmails.add(participant.calculatedActorId!!)
  782. } else {
  783. selectedEmails.remove(participant.calculatedActorId!!)
  784. }
  785. } else if ("circles" == participant.source) {
  786. if (participant.selected) {
  787. selectedCircleIds.add(participant.calculatedActorId!!)
  788. } else {
  789. selectedCircleIds.remove(participant.calculatedActorId!!)
  790. }
  791. } else {
  792. if (participant.selected) {
  793. selectedUserIds.add(participant.calculatedActorId!!)
  794. } else {
  795. selectedUserIds.remove(participant.calculatedActorId!!)
  796. }
  797. }
  798. }
  799. private fun isValidGroupSelection(
  800. contactItem: ContactItem,
  801. participant: Participant,
  802. adapter: FlexibleAdapter<*>?
  803. ): Boolean {
  804. return "groups" == contactItem.model.source && participant.selected && adapter?.selectedItemCount!! > 1
  805. }
  806. private fun joinConversationViaLink() {
  807. val bundle = Bundle()
  808. bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM)
  809. prepareAndShowBottomSheetWithBundle(bundle)
  810. }
  811. private fun toggleCallHeader() {
  812. toggleConversationPrivacyLayout(isPublicCall)
  813. isPublicCall = !isPublicCall
  814. toggleConversationViaLinkVisibility(isPublicCall)
  815. enableContactForNonPublicCall()
  816. checkAndHandleDoneMenuItem()
  817. adapter?.notifyDataSetChanged()
  818. }
  819. private fun updateGroupParticipantSelection() {
  820. val currentItems: List<AbstractFlexibleItem<*>> = adapter?.currentItems as
  821. List<AbstractFlexibleItem<*>>
  822. var internalParticipant: Participant
  823. for (i in currentItems.indices) {
  824. if (currentItems[i] is ContactItem) {
  825. internalParticipant = (currentItems[i] as ContactItem).model
  826. if (internalParticipant.calculatedActorType == Participant.ActorType.GROUPS &&
  827. internalParticipant.selected
  828. ) {
  829. internalParticipant.selected = false
  830. selectedGroupIds.remove(internalParticipant.calculatedActorId)
  831. }
  832. }
  833. }
  834. }
  835. private fun enableContactForNonPublicCall() {
  836. for (i in 0 until adapter!!.itemCount) {
  837. if (adapter?.getItem(i) is ContactItem) {
  838. val contactItem: ContactItem = adapter?.getItem(i) as ContactItem
  839. if ("groups" == contactItem.model.source) {
  840. contactItem.isEnabled = !isPublicCall
  841. }
  842. }
  843. }
  844. }
  845. private fun toggleConversationPrivacyLayout(showInitialLayout: Boolean) {
  846. withNullableControllerViewBinding {
  847. if (showInitialLayout) {
  848. binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.VISIBLE
  849. binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.GONE
  850. } else {
  851. binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.GONE
  852. binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.VISIBLE
  853. }
  854. }
  855. }
  856. private fun toggleConversationViaLinkVisibility(isPublicCall: Boolean) {
  857. withNullableControllerViewBinding {
  858. if (isPublicCall) {
  859. binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
  860. updateGroupParticipantSelection()
  861. } else {
  862. binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE
  863. }
  864. }
  865. }
  866. companion object {
  867. const val TAG = "ContactsController"
  868. const val RETRIES: Long = 3
  869. const val CONTACTS_BATCH_SIZE: Int = 50
  870. const val HEADER_ELEVATION: Int = 5
  871. }
  872. }