ConversationInfoController.kt 40 KB

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