ConversationInfoController.kt 47 KB

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