ConversationInfoController.kt 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.talk.controllers
  21. import android.annotation.SuppressLint
  22. import android.graphics.drawable.Drawable
  23. import android.graphics.drawable.LayerDrawable
  24. import android.os.Bundle
  25. import android.text.TextUtils
  26. import android.util.Log
  27. import android.view.MenuItem
  28. import android.view.View
  29. import androidx.appcompat.widget.SwitchCompat
  30. import androidx.core.content.ContextCompat
  31. import androidx.work.Data
  32. import androidx.work.OneTimeWorkRequest
  33. import androidx.work.WorkManager
  34. import autodagger.AutoInjector
  35. import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT
  36. import com.afollestad.materialdialogs.MaterialDialog
  37. import com.afollestad.materialdialogs.bottomsheets.BottomSheet
  38. import com.afollestad.materialdialogs.datetime.dateTimePicker
  39. import com.bluelinelabs.conductor.RouterTransaction
  40. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  41. import com.facebook.drawee.backends.pipeline.Fresco
  42. import com.nextcloud.talk.R
  43. import com.nextcloud.talk.adapters.items.UserItem
  44. import com.nextcloud.talk.api.NcApi
  45. import com.nextcloud.talk.application.NextcloudTalkApplication
  46. import com.nextcloud.talk.controllers.base.NewBaseController
  47. import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage
  48. import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage
  49. import com.nextcloud.talk.controllers.util.viewBinding
  50. import com.nextcloud.talk.databinding.ControllerConversationInfoBinding
  51. import com.nextcloud.talk.events.EventStatus
  52. import com.nextcloud.talk.jobs.DeleteConversationWorker
  53. import com.nextcloud.talk.jobs.LeaveConversationWorker
  54. import com.nextcloud.talk.models.database.CapabilitiesUtil
  55. import com.nextcloud.talk.models.database.UserEntity
  56. import com.nextcloud.talk.models.json.conversations.Conversation
  57. import com.nextcloud.talk.models.json.conversations.RoomOverall
  58. import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
  59. import com.nextcloud.talk.models.json.generic.GenericOverall
  60. import com.nextcloud.talk.models.json.participants.Participant
  61. import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
  62. import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
  63. import com.nextcloud.talk.models.json.participants.ParticipantsOverall
  64. import com.nextcloud.talk.utils.ApiUtils
  65. import com.nextcloud.talk.utils.DateUtils
  66. import com.nextcloud.talk.utils.DisplayUtils
  67. import com.nextcloud.talk.utils.bundle.BundleKeys
  68. import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
  69. import com.yarolegovich.lovelydialog.LovelySaveStateHandler
  70. import com.yarolegovich.lovelydialog.LovelyStandardDialog
  71. import eu.davidea.flexibleadapter.FlexibleAdapter
  72. import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
  73. import io.reactivex.Observer
  74. import io.reactivex.android.schedulers.AndroidSchedulers
  75. import io.reactivex.disposables.Disposable
  76. import io.reactivex.schedulers.Schedulers
  77. import org.greenrobot.eventbus.EventBus
  78. import org.greenrobot.eventbus.Subscribe
  79. import org.greenrobot.eventbus.ThreadMode
  80. import java.util.Calendar
  81. import java.util.Collections
  82. import java.util.Comparator
  83. import java.util.Locale
  84. import javax.inject.Inject
  85. @AutoInjector(NextcloudTalkApplication::class)
  86. class ConversationInfoController(args: Bundle) : NewBaseController(R.layout.controller_conversation_info, args),
  87. FlexibleAdapter
  88. .OnItemClickListener {
  89. private val binding: ControllerConversationInfoBinding by viewBinding(ControllerConversationInfoBinding::bind)
  90. @Inject
  91. @JvmField
  92. var ncApi: NcApi? = null
  93. @Inject
  94. @JvmField
  95. var eventBus: EventBus? = null
  96. private val conversationToken: String?
  97. private val conversationUser: UserEntity?
  98. private val hasAvatarSpacing: Boolean
  99. private val credentials: String?
  100. private var roomDisposable: Disposable? = null
  101. private var participantsDisposable: Disposable? = null
  102. private var databaseStorageModule: DatabaseStorageModule? = null
  103. private var conversation: Conversation? = null
  104. private var adapter: FlexibleAdapter<UserItem>? = null
  105. private var recyclerViewItems: MutableList<UserItem> = ArrayList()
  106. private var saveStateHandler: LovelySaveStateHandler? = null
  107. private val workerData: Data?
  108. get() {
  109. if (!TextUtils.isEmpty(conversationToken) && conversationUser != null) {
  110. val data = Data.Builder()
  111. data.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
  112. data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id)
  113. return data.build()
  114. }
  115. return null
  116. }
  117. init {
  118. setHasOptionsMenu(true)
  119. NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
  120. conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY)
  121. conversationToken = args.getString(BundleKeys.KEY_ROOM_TOKEN)
  122. hasAvatarSpacing = args.getBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, false)
  123. credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
  124. }
  125. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  126. when (item.itemId) {
  127. android.R.id.home -> {
  128. router.popCurrentController()
  129. return true
  130. }
  131. else -> return super.onOptionsItemSelected(item)
  132. }
  133. }
  134. override fun onAttach(view: View) {
  135. super.onAttach(view)
  136. eventBus?.register(this)
  137. if (databaseStorageModule == null) {
  138. databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken)
  139. }
  140. binding.notificationSettingsView.notificationSettings.setStorageModule(databaseStorageModule)
  141. binding.webinarInfoView.webinarSettings.setStorageModule(databaseStorageModule)
  142. binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog(null) }
  143. binding.leaveConversationAction.setOnClickListener { leaveConversation() }
  144. binding.addParticipantsAction.setOnClickListener { addParticipants() }
  145. fetchRoomInfo()
  146. }
  147. override fun onViewBound(view: View) {
  148. super.onViewBound(view)
  149. if (saveStateHandler == null) {
  150. saveStateHandler = LovelySaveStateHandler()
  151. }
  152. binding.addParticipantsAction.visibility = View.GONE
  153. }
  154. private fun setupWebinaryView() {
  155. if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") &&
  156. (
  157. conversation!!.type == Conversation.ConversationType.ROOM_GROUP_CALL ||
  158. conversation!!.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
  159. ) &&
  160. conversation!!.canModerate(conversationUser)
  161. ) {
  162. binding.webinarInfoView.webinarSettings.visibility = View.VISIBLE
  163. val isLobbyOpenToModeratorsOnly =
  164. conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
  165. (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
  166. .isChecked = isLobbyOpenToModeratorsOnly
  167. reconfigureLobbyTimerView()
  168. binding.webinarInfoView.startTimePreferences.setOnClickListener {
  169. MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
  170. val currentTimeCalendar = Calendar.getInstance()
  171. if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != 0L) {
  172. currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * 1000
  173. }
  174. dateTimePicker(
  175. minDateTime = Calendar.getInstance(),
  176. requireFutureDateTime = true,
  177. currentDateTime = currentTimeCalendar,
  178. show24HoursView = true,
  179. dateTimeCallback = { _,
  180. dateTime ->
  181. reconfigureLobbyTimerView(dateTime)
  182. submitLobbyChanges()
  183. }
  184. )
  185. }
  186. }
  187. (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).setOnCheckedChangeListener { _, _ ->
  188. reconfigureLobbyTimerView()
  189. submitLobbyChanges()
  190. }
  191. } else {
  192. binding.webinarInfoView.webinarSettings.visibility = View.GONE
  193. }
  194. }
  195. fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
  196. val isChecked =
  197. (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked
  198. if (dateTime != null && isChecked) {
  199. conversation!!.lobbyTimer = (dateTime.timeInMillis - (dateTime.time.seconds * 1000)) / 1000
  200. } else if (!isChecked) {
  201. conversation!!.lobbyTimer = 0
  202. }
  203. conversation!!.lobbyState = if (isChecked) Conversation.LobbyState
  204. .LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
  205. if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != java.lang.Long.MIN_VALUE && conversation!!.lobbyTimer != 0L) {
  206. binding.webinarInfoView.startTimePreferences.setSummary(
  207. DateUtils.getLocalDateStringFromTimestampForLobby(
  208. conversation!!.lobbyTimer
  209. )
  210. )
  211. } else {
  212. binding.webinarInfoView.startTimePreferences.setSummary(R.string.nc_manual)
  213. }
  214. if (isChecked) {
  215. binding.webinarInfoView.startTimePreferences.visibility = View.VISIBLE
  216. } else {
  217. binding.webinarInfoView.startTimePreferences.visibility = View.GONE
  218. }
  219. }
  220. fun submitLobbyChanges() {
  221. val state = if (
  222. (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked
  223. ) 1 else 0
  224. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  225. ncApi?.setLobbyForConversation(
  226. ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token),
  227. ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl, conversation!!.token),
  228. state,
  229. conversation!!.lobbyTimer
  230. )
  231. ?.subscribeOn(Schedulers.io())
  232. ?.observeOn(AndroidSchedulers.mainThread())
  233. ?.subscribe(object : Observer<GenericOverall> {
  234. override fun onComplete() {
  235. }
  236. override fun onSubscribe(d: Disposable) {
  237. }
  238. override fun onNext(t: GenericOverall) {
  239. }
  240. override fun onError(e: Throwable) {
  241. }
  242. })
  243. }
  244. private fun showLovelyDialog(dialogId: Int, savedInstanceState: Bundle) {
  245. when (dialogId) {
  246. ID_DELETE_CONVERSATION_DIALOG -> showDeleteConversationDialog(savedInstanceState)
  247. else -> {
  248. }
  249. }
  250. }
  251. @Subscribe(threadMode = ThreadMode.MAIN)
  252. fun onMessageEvent(eventStatus: EventStatus) {
  253. getListOfParticipants()
  254. }
  255. override fun onDetach(view: View) {
  256. super.onDetach(view)
  257. eventBus?.unregister(this)
  258. }
  259. private fun showDeleteConversationDialog(savedInstanceState: Bundle?) {
  260. if (activity != null) {
  261. LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL)
  262. .setTopColorRes(R.color.nc_darkRed)
  263. .setIcon(
  264. DisplayUtils.getTintedDrawable(
  265. context!!.resources,
  266. R.drawable.ic_delete_black_24dp, R.color.bg_default
  267. )
  268. )
  269. .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
  270. .setTitle(R.string.nc_delete_call)
  271. .setMessage(R.string.nc_delete_conversation_more)
  272. .setPositiveButton(R.string.nc_delete) { deleteConversation() }
  273. .setNegativeButton(R.string.nc_cancel, null)
  274. .setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler!!)
  275. .setSavedInstanceState(savedInstanceState)
  276. .show()
  277. }
  278. }
  279. override fun onSaveViewState(view: View, outState: Bundle) {
  280. saveStateHandler!!.saveInstanceState(outState)
  281. super.onSaveViewState(view, outState)
  282. }
  283. override fun onRestoreViewState(view: View, savedViewState: Bundle) {
  284. super.onRestoreViewState(view, savedViewState)
  285. if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
  286. // Dialog won't be restarted automatically, so we need to call this method.
  287. // Each dialog knows how to restore its state
  288. showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState)
  289. }
  290. }
  291. private fun setupAdapter() {
  292. if (activity != null) {
  293. if (adapter == null) {
  294. adapter = FlexibleAdapter(recyclerViewItems, activity, true)
  295. }
  296. val layoutManager = SmoothScrollLinearLayoutManager(activity)
  297. binding.recyclerView.layoutManager = layoutManager
  298. binding.recyclerView.setHasFixedSize(true)
  299. binding.recyclerView.adapter = adapter
  300. adapter!!.addListener(this)
  301. }
  302. }
  303. private fun handleParticipants(participants: List<Participant>) {
  304. var userItem: UserItem
  305. var participant: Participant
  306. recyclerViewItems = ArrayList()
  307. var ownUserItem: UserItem? = null
  308. for (i in participants.indices) {
  309. participant = participants[i]
  310. userItem = UserItem(participant, conversationUser, null)
  311. if (participant.sessionId != null) {
  312. userItem.isOnline = !participant.sessionId.equals("0")
  313. } else {
  314. userItem.isOnline = !participant.sessionIds!!.isEmpty()
  315. }
  316. if (participant.getActorType() == USERS && participant.getActorId() == conversationUser!!.userId) {
  317. ownUserItem = userItem
  318. ownUserItem.model.sessionId = "-1"
  319. ownUserItem.isOnline = true
  320. } else {
  321. recyclerViewItems.add(userItem)
  322. }
  323. }
  324. Collections.sort(recyclerViewItems, UserItemComparator())
  325. if (ownUserItem != null) {
  326. recyclerViewItems.add(0, ownUserItem)
  327. }
  328. setupAdapter()
  329. binding.participantsListCategory.visibility = View.VISIBLE
  330. adapter!!.updateDataSet(recyclerViewItems)
  331. }
  332. /**
  333. override fun getTitle(): String? {
  334. return if (hasAvatarSpacing) {
  335. " " + resources!!.getString(R.string.nc_conversation_menu_conversation_info)
  336. } else {
  337. resources!!.getString(R.string.nc_conversation_menu_conversation_info)
  338. }
  339. }
  340. */
  341. override val title: String?
  342. get() =
  343. if (hasAvatarSpacing) {
  344. " " + resources!!.getString(R.string.nc_conversation_menu_conversation_info)
  345. } else {
  346. resources!!.getString(R.string.nc_conversation_menu_conversation_info)
  347. }
  348. private fun getListOfParticipants() {
  349. var apiVersion = 1
  350. // FIXME Fix API checking with guests?
  351. if (conversationUser != null) {
  352. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  353. }
  354. ncApi?.getPeersForCall(
  355. credentials,
  356. ApiUtils.getUrlForParticipants(apiVersion, conversationUser!!.baseUrl, conversationToken)
  357. )
  358. ?.subscribeOn(Schedulers.io())
  359. ?.observeOn(AndroidSchedulers.mainThread())
  360. ?.subscribe(object : Observer<ParticipantsOverall> {
  361. override fun onSubscribe(d: Disposable) {
  362. participantsDisposable = d
  363. }
  364. override fun onNext(participantsOverall: ParticipantsOverall) {
  365. handleParticipants(participantsOverall.ocs.data)
  366. }
  367. override fun onError(e: Throwable) {
  368. }
  369. override fun onComplete() {
  370. participantsDisposable!!.dispose()
  371. }
  372. })
  373. }
  374. internal fun addParticipants() {
  375. val bundle = Bundle()
  376. val existingParticipantsId = arrayListOf<String>()
  377. for (userItem in recyclerViewItems) {
  378. if (userItem.model.getActorType() == USERS) {
  379. existingParticipantsId.add(userItem.model.getActorId())
  380. }
  381. }
  382. bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
  383. bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId)
  384. bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token)
  385. getRouter().pushController(
  386. (
  387. RouterTransaction.with(
  388. ContactsController(bundle)
  389. )
  390. .pushChangeHandler(
  391. HorizontalChangeHandler()
  392. )
  393. .popChangeHandler(
  394. HorizontalChangeHandler()
  395. )
  396. )
  397. )
  398. }
  399. private fun leaveConversation() {
  400. workerData?.let {
  401. WorkManager.getInstance().enqueue(
  402. OneTimeWorkRequest.Builder(
  403. LeaveConversationWorker::class
  404. .java
  405. ).setInputData(it).build()
  406. )
  407. popTwoLastControllers()
  408. }
  409. }
  410. private fun deleteConversation() {
  411. workerData?.let {
  412. WorkManager.getInstance().enqueue(
  413. OneTimeWorkRequest.Builder(
  414. DeleteConversationWorker::class.java
  415. ).setInputData(it).build()
  416. )
  417. popTwoLastControllers()
  418. }
  419. }
  420. private fun popTwoLastControllers() {
  421. var backstack = router.backstack
  422. backstack = backstack.subList(0, backstack.size - 2)
  423. router.setBackstack(backstack, HorizontalChangeHandler())
  424. }
  425. private fun fetchRoomInfo() {
  426. var apiVersion = 1
  427. // FIXME Fix API checking with guests?
  428. if (conversationUser != null) {
  429. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  430. }
  431. ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
  432. ?.subscribeOn(Schedulers.io())
  433. ?.observeOn(AndroidSchedulers.mainThread())
  434. ?.subscribe(object : Observer<RoomOverall> {
  435. override fun onSubscribe(d: Disposable) {
  436. roomDisposable = d
  437. }
  438. override fun onNext(roomOverall: RoomOverall) {
  439. conversation = roomOverall.ocs.data
  440. val conversationCopy = conversation
  441. if (conversationCopy!!.canModerate(conversationUser)) {
  442. binding.addParticipantsAction.visibility = View.VISIBLE
  443. } else {
  444. binding.addParticipantsAction.visibility = View.GONE
  445. }
  446. if (isAttached && (!isBeingDestroyed || !isDestroyed)) {
  447. binding.ownOptions.visibility = View.VISIBLE
  448. setupWebinaryView()
  449. if (!conversation!!.canLeave(conversationUser)) {
  450. binding.leaveConversationAction.visibility = View.GONE
  451. } else {
  452. binding.leaveConversationAction.visibility = View.VISIBLE
  453. }
  454. if (!conversation!!.canDelete(conversationUser)) {
  455. binding.deleteConversationAction.visibility = View.GONE
  456. } else {
  457. binding.deleteConversationAction.visibility = View.VISIBLE
  458. }
  459. if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) {
  460. binding.notificationSettingsView.muteCalls.visibility = View.GONE
  461. }
  462. getListOfParticipants()
  463. binding.progressBar.visibility = View.GONE
  464. binding.conversationInfoName.visibility = View.VISIBLE
  465. binding.displayNameText.text = conversation!!.displayName
  466. if (conversation!!.description != null && !conversation!!.description.isEmpty()) {
  467. binding.descriptionText.text = conversation!!.description
  468. binding.conversationDescription.visibility = View.VISIBLE
  469. }
  470. loadConversationAvatar()
  471. adjustNotificationLevelUI()
  472. binding.notificationSettingsView.notificationSettings.visibility = View.VISIBLE
  473. }
  474. }
  475. override fun onError(e: Throwable) {
  476. }
  477. override fun onComplete() {
  478. roomDisposable!!.dispose()
  479. }
  480. })
  481. }
  482. private fun adjustNotificationLevelUI() {
  483. if (conversation != null) {
  484. if (conversationUser != null && CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "notification-levels")) {
  485. binding.notificationSettingsView.conversationInfoMessageNotifications.isEnabled = true
  486. binding.notificationSettingsView.conversationInfoMessageNotifications.alpha = 1.0f
  487. if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
  488. val stringValue: String =
  489. when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
  490. 1 -> "always"
  491. 2 -> "mention"
  492. 3 -> "never"
  493. else -> "mention"
  494. }
  495. binding.notificationSettingsView.conversationInfoMessageNotifications.value = stringValue
  496. } else {
  497. setProperNotificationValue(conversation)
  498. }
  499. } else {
  500. binding.notificationSettingsView.conversationInfoMessageNotifications.isEnabled = false
  501. binding.notificationSettingsView.conversationInfoMessageNotifications.alpha = 0.38f
  502. setProperNotificationValue(conversation)
  503. }
  504. }
  505. }
  506. private fun setProperNotificationValue(conversation: Conversation?) {
  507. if (conversation!!.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
  508. // hack to see if we get mentioned always or just on mention
  509. if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "mention-flag")) {
  510. binding.notificationSettingsView.conversationInfoMessageNotifications.value = "always"
  511. } else {
  512. binding.notificationSettingsView.conversationInfoMessageNotifications.value = "mention"
  513. }
  514. } else {
  515. binding.notificationSettingsView.conversationInfoMessageNotifications.value = "mention"
  516. }
  517. }
  518. private fun loadConversationAvatar() {
  519. when (conversation!!.type) {
  520. Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) {
  521. val draweeController = Fresco.newDraweeControllerBuilder()
  522. .setOldController(binding.avatarImage.controller)
  523. .setAutoPlayAnimations(true)
  524. .setImageRequest(
  525. DisplayUtils.getImageRequestForUrl(
  526. ApiUtils.getUrlForAvatarWithName(
  527. conversationUser!!.baseUrl,
  528. conversation!!.name, R.dimen.avatar_size_big
  529. ),
  530. conversationUser
  531. )
  532. )
  533. .build()
  534. binding.avatarImage.controller = draweeController
  535. }
  536. Conversation.ConversationType.ROOM_GROUP_CALL -> binding.avatarImage.hierarchy.setPlaceholderImage(
  537. R.drawable.ic_circular_group
  538. )
  539. Conversation.ConversationType.ROOM_PUBLIC_CALL -> binding.avatarImage.hierarchy.setPlaceholderImage(
  540. R.drawable.ic_circular_link
  541. )
  542. Conversation.ConversationType.ROOM_SYSTEM -> {
  543. val layers = arrayOfNulls<Drawable>(2)
  544. layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
  545. layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
  546. val layerDrawable = LayerDrawable(layers)
  547. binding.avatarImage.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
  548. }
  549. else -> {
  550. }
  551. }
  552. }
  553. private fun toggleModeratorStatus(apiVersion: Int, participant: Participant) {
  554. val subscriber = object : Observer<GenericOverall> {
  555. override fun onSubscribe(d: Disposable) {
  556. }
  557. override fun onNext(genericOverall: GenericOverall) {
  558. getListOfParticipants()
  559. }
  560. @SuppressLint("LongLogTag")
  561. override fun onError(e: Throwable) {
  562. Log.e(TAG, "Error toggling moderator status", e)
  563. }
  564. override fun onComplete() {
  565. }
  566. }
  567. if (participant.type == Participant.ParticipantType.MODERATOR ||
  568. participant.type == Participant.ParticipantType.GUEST_MODERATOR
  569. ) {
  570. ncApi?.demoteAttendeeFromModerator(
  571. credentials,
  572. ApiUtils.getUrlForRoomModerators(
  573. apiVersion,
  574. conversationUser!!.baseUrl,
  575. conversation!!.token
  576. ),
  577. participant.attendeeId
  578. )
  579. ?.subscribeOn(Schedulers.io())
  580. ?.observeOn(AndroidSchedulers.mainThread())
  581. ?.subscribe(subscriber)
  582. } else if (participant.type == Participant.ParticipantType.USER ||
  583. participant.type == Participant.ParticipantType.GUEST
  584. ) {
  585. ncApi?.promoteAttendeeToModerator(
  586. credentials,
  587. ApiUtils.getUrlForRoomModerators(
  588. apiVersion,
  589. conversationUser!!.baseUrl,
  590. conversation!!.token
  591. ),
  592. participant.attendeeId
  593. )
  594. ?.subscribeOn(Schedulers.io())
  595. ?.observeOn(AndroidSchedulers.mainThread())
  596. ?.subscribe(subscriber)
  597. }
  598. }
  599. private fun toggleModeratorStatusLegacy(apiVersion: Int, participant: Participant) {
  600. val subscriber = object : Observer<GenericOverall> {
  601. override fun onSubscribe(d: Disposable) {
  602. }
  603. override fun onNext(genericOverall: GenericOverall) {
  604. getListOfParticipants()
  605. }
  606. @SuppressLint("LongLogTag")
  607. override fun onError(e: Throwable) {
  608. Log.e(TAG, "Error toggling moderator status", e)
  609. }
  610. override fun onComplete() {
  611. }
  612. }
  613. if (participant.type == Participant.ParticipantType.MODERATOR) {
  614. ncApi?.demoteModeratorToUser(
  615. credentials,
  616. ApiUtils.getUrlForRoomModerators(
  617. apiVersion,
  618. conversationUser!!.baseUrl,
  619. conversation!!.token
  620. ),
  621. participant.userId
  622. )
  623. ?.subscribeOn(Schedulers.io())
  624. ?.observeOn(AndroidSchedulers.mainThread())
  625. ?.subscribe(subscriber)
  626. } else if (participant.type == Participant.ParticipantType.USER) {
  627. ncApi?.promoteUserToModerator(
  628. credentials,
  629. ApiUtils.getUrlForRoomModerators(
  630. apiVersion,
  631. conversationUser!!.baseUrl,
  632. conversation!!.token
  633. ),
  634. participant.userId
  635. )
  636. ?.subscribeOn(Schedulers.io())
  637. ?.observeOn(AndroidSchedulers.mainThread())
  638. ?.subscribe(subscriber)
  639. }
  640. }
  641. fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
  642. if (apiVersion >= ApiUtils.APIv4) {
  643. ncApi?.removeAttendeeFromConversation(
  644. credentials,
  645. ApiUtils.getUrlForAttendees(
  646. apiVersion,
  647. conversationUser!!.baseUrl,
  648. conversation!!.token
  649. ),
  650. participant.attendeeId
  651. )
  652. ?.subscribeOn(Schedulers.io())
  653. ?.observeOn(AndroidSchedulers.mainThread())
  654. ?.subscribe(object : Observer<GenericOverall> {
  655. override fun onSubscribe(d: Disposable) {
  656. }
  657. override fun onNext(genericOverall: GenericOverall) {
  658. getListOfParticipants()
  659. }
  660. @SuppressLint("LongLogTag")
  661. override fun onError(e: Throwable) {
  662. Log.e(TAG, "Error removing attendee from conversation", e)
  663. }
  664. override fun onComplete() {
  665. }
  666. })
  667. } else {
  668. if (participant.type == Participant.ParticipantType.GUEST ||
  669. participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
  670. ) {
  671. ncApi?.removeParticipantFromConversation(
  672. credentials,
  673. ApiUtils.getUrlForRemovingParticipantFromConversation(
  674. conversationUser!!.baseUrl,
  675. conversation!!.token,
  676. true
  677. ),
  678. participant.sessionId
  679. )
  680. ?.subscribeOn(Schedulers.io())
  681. ?.observeOn(AndroidSchedulers.mainThread())
  682. ?.subscribe(object : Observer<GenericOverall> {
  683. override fun onSubscribe(d: Disposable) {
  684. }
  685. override fun onNext(genericOverall: GenericOverall) {
  686. getListOfParticipants()
  687. }
  688. @SuppressLint("LongLogTag")
  689. override fun onError(e: Throwable) {
  690. Log.e(TAG, "Error removing guest from conversation", e)
  691. }
  692. override fun onComplete() {
  693. }
  694. })
  695. } else {
  696. ncApi?.removeParticipantFromConversation(
  697. credentials,
  698. ApiUtils.getUrlForRemovingParticipantFromConversation(
  699. conversationUser!!.baseUrl,
  700. conversation!!.token,
  701. false
  702. ),
  703. participant.userId
  704. )
  705. ?.subscribeOn(Schedulers.io())
  706. ?.observeOn(AndroidSchedulers.mainThread())
  707. ?.subscribe(object : Observer<GenericOverall> {
  708. override fun onSubscribe(d: Disposable) {
  709. }
  710. override fun onNext(genericOverall: GenericOverall) {
  711. getListOfParticipants()
  712. }
  713. @SuppressLint("LongLogTag")
  714. override fun onError(e: Throwable) {
  715. Log.e(TAG, "Error removing user from conversation", e)
  716. }
  717. override fun onComplete() {
  718. }
  719. })
  720. }
  721. }
  722. }
  723. override fun onItemClick(view: View?, position: Int): Boolean {
  724. if (!conversation!!.canModerate(conversationUser)) {
  725. return true
  726. }
  727. val userItem = adapter?.getItem(position) as UserItem
  728. val participant = userItem.model
  729. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  730. if (participant.getActorType() == USERS && participant.getActorId() == conversationUser!!.userId) {
  731. if (participant.attendeePin.isNotEmpty()) {
  732. val items = mutableListOf(
  733. BasicListItemWithImage(
  734. R.drawable.ic_lock_grey600_24px,
  735. context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
  736. )
  737. )
  738. MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
  739. cornerRadius(res = R.dimen.corner_radius)
  740. title(text = participant.displayName)
  741. listItemsWithImage(items = items) { dialog, index, _ ->
  742. if (index == 0) {
  743. removeAttendeeFromConversation(apiVersion, participant)
  744. }
  745. }
  746. }
  747. }
  748. return true
  749. }
  750. if (participant.type == Participant.ParticipantType.OWNER) {
  751. // Can not moderate owner
  752. return true
  753. }
  754. if (participant.getActorType() == GROUPS) {
  755. val items = mutableListOf(
  756. BasicListItemWithImage(
  757. R.drawable.ic_delete_grey600_24dp,
  758. context!!.getString(R.string.nc_remove_group_and_members)
  759. )
  760. )
  761. MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
  762. cornerRadius(res = R.dimen.corner_radius)
  763. title(text = participant.displayName)
  764. listItemsWithImage(items = items) { dialog, index, _ ->
  765. if (index == 0) {
  766. removeAttendeeFromConversation(apiVersion, participant)
  767. }
  768. }
  769. }
  770. return true
  771. }
  772. val items = mutableListOf(
  773. BasicListItemWithImage(
  774. R.drawable.ic_lock_grey600_24px,
  775. context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
  776. ),
  777. BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_promote)),
  778. BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_demote)),
  779. BasicListItemWithImage(
  780. R.drawable.ic_delete_grey600_24dp,
  781. context!!.getString(R.string.nc_remove_participant)
  782. )
  783. )
  784. if (participant.type == Participant.ParticipantType.MODERATOR ||
  785. participant.type == Participant.ParticipantType.GUEST_MODERATOR
  786. ) {
  787. items.removeAt(1)
  788. } else if (participant.type == Participant.ParticipantType.USER ||
  789. participant.type == Participant.ParticipantType.GUEST
  790. ) {
  791. items.removeAt(2)
  792. } else {
  793. // Self joined users can not be promoted nor demoted
  794. items.removeAt(2)
  795. items.removeAt(1)
  796. }
  797. if (participant.attendeePin.isEmpty()) {
  798. items.removeAt(0)
  799. }
  800. if (items.isNotEmpty()) {
  801. MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
  802. cornerRadius(res = R.dimen.corner_radius)
  803. title(text = participant.displayName)
  804. listItemsWithImage(items = items) { dialog, index, _ ->
  805. var actionToTrigger = index
  806. if (participant.attendeePin.isEmpty()) {
  807. actionToTrigger++
  808. }
  809. if (participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) {
  810. actionToTrigger++
  811. }
  812. if (actionToTrigger == 0) {
  813. // Pin, nothing to do
  814. } else if (actionToTrigger == 1) {
  815. // Promote/demote
  816. if (apiVersion >= ApiUtils.APIv4) {
  817. toggleModeratorStatus(apiVersion, participant)
  818. } else {
  819. toggleModeratorStatusLegacy(apiVersion, participant)
  820. }
  821. } else if (actionToTrigger == 2) {
  822. // Remove from conversation
  823. removeAttendeeFromConversation(apiVersion, participant)
  824. }
  825. }
  826. }
  827. }
  828. return true
  829. }
  830. companion object {
  831. private const val TAG = "ConversationInfoController"
  832. private const val ID_DELETE_CONVERSATION_DIALOG = 0
  833. }
  834. /**
  835. * Comparator for participants, sorts by online-status, moderator-status and display name.
  836. */
  837. class UserItemComparator : Comparator<UserItem> {
  838. override fun compare(left: UserItem, right: UserItem): Int {
  839. val leftIsGroup = left.model.actorType == GROUPS
  840. val rightIsGroup = right.model.actorType == GROUPS
  841. if (leftIsGroup != rightIsGroup) {
  842. // Groups below participants
  843. return if (rightIsGroup) {
  844. -1
  845. } else {
  846. 1
  847. }
  848. }
  849. if (left.isOnline && !right.isOnline) {
  850. return -1
  851. } else if (!left.isOnline && right.isOnline) {
  852. return 1
  853. }
  854. val moderatorTypes = ArrayList<Participant.ParticipantType>()
  855. moderatorTypes.add(Participant.ParticipantType.MODERATOR)
  856. moderatorTypes.add(Participant.ParticipantType.OWNER)
  857. moderatorTypes.add(Participant.ParticipantType.GUEST_MODERATOR)
  858. if (moderatorTypes.contains(left.model.type) && !moderatorTypes.contains(right.model.type)) {
  859. return -1
  860. } else if (!moderatorTypes.contains(left.model.type) && moderatorTypes.contains(right.model.type)) {
  861. return 1
  862. }
  863. return left.model.displayName.toLowerCase(Locale.ROOT).compareTo(
  864. right.model.displayName.toLowerCase(Locale.ROOT)
  865. )
  866. }
  867. }
  868. }