ConversationInfoController.kt 39 KB

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