ConversationInfoController.kt 49 KB

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