ChatController.kt 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Marcel Hibbe
  6. * @author Andy Scherzinger
  7. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  8. * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  9. * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. */
  24. package com.nextcloud.talk.controllers
  25. import android.app.Activity.RESULT_OK
  26. import android.content.ClipData
  27. import android.content.Context
  28. import android.content.Intent
  29. import android.content.pm.PackageManager
  30. import android.content.res.Resources
  31. import android.graphics.Bitmap
  32. import android.graphics.PorterDuff
  33. import android.graphics.drawable.ColorDrawable
  34. import android.net.Uri
  35. import android.os.Bundle
  36. import android.os.Handler
  37. import android.text.Editable
  38. import android.text.InputFilter
  39. import android.text.TextUtils
  40. import android.text.TextWatcher
  41. import android.util.Log
  42. import android.util.TypedValue
  43. import android.view.Gravity
  44. import android.view.Menu
  45. import android.view.MenuInflater
  46. import android.view.MenuItem
  47. import android.view.View
  48. import android.widget.AbsListView
  49. import android.widget.ImageButton
  50. import android.widget.ImageView
  51. import android.widget.PopupMenu
  52. import android.widget.RelativeLayout
  53. import android.widget.Space
  54. import android.widget.Toast
  55. import androidx.appcompat.view.ContextThemeWrapper
  56. import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
  57. import androidx.emoji.text.EmojiCompat
  58. import androidx.emoji.widget.EmojiTextView
  59. import androidx.recyclerview.widget.LinearLayoutManager
  60. import androidx.recyclerview.widget.RecyclerView
  61. import androidx.work.Data
  62. import androidx.work.OneTimeWorkRequest
  63. import androidx.work.WorkManager
  64. import autodagger.AutoInjector
  65. import coil.load
  66. import com.bluelinelabs.conductor.RouterTransaction
  67. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  68. import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
  69. import com.facebook.common.executors.UiThreadImmediateExecutorService
  70. import com.facebook.common.references.CloseableReference
  71. import com.facebook.datasource.DataSource
  72. import com.facebook.drawee.backends.pipeline.Fresco
  73. import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
  74. import com.facebook.imagepipeline.image.CloseableImage
  75. import com.google.android.flexbox.FlexboxLayout
  76. import com.nextcloud.talk.R
  77. import com.nextcloud.talk.activities.MagicCallActivity
  78. import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
  79. import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
  80. import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder
  81. import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
  82. import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
  83. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  84. import com.nextcloud.talk.api.NcApi
  85. import com.nextcloud.talk.application.NextcloudTalkApplication
  86. import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
  87. import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
  88. import com.nextcloud.talk.components.filebrowser.controllers.BrowserForSharingController
  89. import com.nextcloud.talk.controllers.base.NewBaseController
  90. import com.nextcloud.talk.controllers.util.viewBinding
  91. import com.nextcloud.talk.databinding.ControllerChatBinding
  92. import com.nextcloud.talk.events.UserMentionClickEvent
  93. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  94. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  95. import com.nextcloud.talk.models.database.CapabilitiesUtil
  96. import com.nextcloud.talk.models.database.UserEntity
  97. import com.nextcloud.talk.models.json.chat.ChatMessage
  98. import com.nextcloud.talk.models.json.chat.ChatOverall
  99. import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
  100. import com.nextcloud.talk.models.json.chat.ReadStatus
  101. import com.nextcloud.talk.models.json.conversations.Conversation
  102. import com.nextcloud.talk.models.json.conversations.RoomOverall
  103. import com.nextcloud.talk.models.json.conversations.RoomsOverall
  104. import com.nextcloud.talk.models.json.generic.GenericOverall
  105. import com.nextcloud.talk.models.json.mention.Mention
  106. import com.nextcloud.talk.presenters.MentionAutocompletePresenter
  107. import com.nextcloud.talk.ui.dialog.AttachmentDialog
  108. import com.nextcloud.talk.utils.ApiUtils
  109. import com.nextcloud.talk.utils.ConductorRemapping
  110. import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
  111. import com.nextcloud.talk.utils.DateUtils
  112. import com.nextcloud.talk.utils.DisplayUtils
  113. import com.nextcloud.talk.utils.KeyboardUtils
  114. import com.nextcloud.talk.utils.MagicCharPolicy
  115. import com.nextcloud.talk.utils.NotificationUtils
  116. import com.nextcloud.talk.utils.UriUtils
  117. import com.nextcloud.talk.utils.bundle.BundleKeys
  118. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
  119. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
  120. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  121. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
  122. import com.nextcloud.talk.utils.database.user.UserUtils
  123. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  124. import com.nextcloud.talk.utils.text.Spans
  125. import com.nextcloud.talk.webrtc.MagicWebSocketInstance
  126. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  127. import com.otaliastudios.autocomplete.Autocomplete
  128. import com.stfalcon.chatkit.commons.ImageLoader
  129. import com.stfalcon.chatkit.commons.models.IMessage
  130. import com.stfalcon.chatkit.messages.MessageHolders
  131. import com.stfalcon.chatkit.messages.MessagesListAdapter
  132. import com.stfalcon.chatkit.utils.DateFormatter
  133. import com.vanniktech.emoji.EmojiPopup
  134. import com.yarolegovich.lovelydialog.LovelyStandardDialog
  135. import io.reactivex.Observer
  136. import io.reactivex.android.schedulers.AndroidSchedulers
  137. import io.reactivex.disposables.Disposable
  138. import io.reactivex.schedulers.Schedulers
  139. import org.greenrobot.eventbus.EventBus
  140. import org.greenrobot.eventbus.Subscribe
  141. import org.greenrobot.eventbus.ThreadMode
  142. import org.parceler.Parcels
  143. import retrofit2.HttpException
  144. import retrofit2.Response
  145. import java.net.HttpURLConnection
  146. import java.util.ArrayList
  147. import java.util.Date
  148. import java.util.HashMap
  149. import java.util.Objects
  150. import javax.inject.Inject
  151. @AutoInjector(NextcloudTalkApplication::class)
  152. class ChatController(args: Bundle) :
  153. NewBaseController(
  154. R.layout.controller_chat,
  155. args
  156. ),
  157. MessagesListAdapter.OnLoadMoreListener,
  158. MessagesListAdapter.Formatter<Date>,
  159. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  160. MessageHolders.ContentChecker<IMessage> {
  161. private val binding: ControllerChatBinding? by viewBinding(ControllerChatBinding::bind)
  162. @Inject
  163. @JvmField
  164. var ncApi: NcApi? = null
  165. @Inject
  166. @JvmField
  167. var userUtils: UserUtils? = null
  168. @Inject
  169. @JvmField
  170. var eventBus: EventBus? = null
  171. val disposableList = ArrayList<Disposable>()
  172. var roomToken: String? = null
  173. val conversationUser: UserEntity?
  174. val roomPassword: String
  175. var credentials: String? = null
  176. var currentConversation: Conversation? = null
  177. var inConversation = false
  178. var historyRead = false
  179. var globalLastKnownFutureMessageId = -1
  180. var globalLastKnownPastMessageId = -1
  181. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  182. var mentionAutocomplete: Autocomplete<*>? = null
  183. var layoutManager: LinearLayoutManager? = null
  184. var lookingIntoFuture = false
  185. var newMessagesCount = 0
  186. var startCallFromNotification: Boolean? = null
  187. val roomId: String
  188. val voiceOnly: Boolean
  189. var isFirstMessagesProcessing = true
  190. var isLeavingForConversation: Boolean = false
  191. var isLinkPreviewAllowed: Boolean = false
  192. var wasDetached: Boolean = false
  193. var emojiPopup: EmojiPopup? = null
  194. var myFirstMessage: CharSequence? = null
  195. var checkingLobbyStatus: Boolean = false
  196. var conversationInfoMenuItem: MenuItem? = null
  197. var conversationVoiceCallMenuItem: MenuItem? = null
  198. var conversationVideoMenuItem: MenuItem? = null
  199. var magicWebSocketInstance: MagicWebSocketInstance? = null
  200. var lobbyTimerHandler: Handler? = null
  201. val roomJoined: Boolean = false
  202. var pastPreconditionFailed = false
  203. var futurePreconditionFailed = false
  204. val filesToUpload: MutableList<String> = ArrayList()
  205. var sharedText: String
  206. init {
  207. setHasOptionsMenu(true)
  208. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  209. this.conversationUser = args.getParcelable(KEY_USER_ENTITY)
  210. this.roomId = args.getString(KEY_ROOM_ID, "")
  211. this.roomToken = args.getString(KEY_ROOM_TOKEN, "")
  212. this.sharedText = args.getString(BundleKeys.KEY_SHARED_TEXT, "")
  213. if (args.containsKey(KEY_ACTIVE_CONVERSATION)) {
  214. this.currentConversation = Parcels.unwrap<Conversation>(args.getParcelable(KEY_ACTIVE_CONVERSATION))
  215. }
  216. this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
  217. if (conversationUser?.userId == "?") {
  218. credentials = null
  219. } else {
  220. credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
  221. }
  222. if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
  223. this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)
  224. }
  225. this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false)
  226. }
  227. private fun getRoomInfo() {
  228. val shouldRepeat = CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "webinary-lobby")
  229. if (shouldRepeat) {
  230. checkingLobbyStatus = true
  231. }
  232. if (conversationUser != null) {
  233. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  234. ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, roomToken))
  235. ?.subscribeOn(Schedulers.io())
  236. ?.observeOn(AndroidSchedulers.mainThread())
  237. ?.subscribe(object : Observer<RoomOverall> {
  238. override fun onSubscribe(d: Disposable) {
  239. disposableList.add(d)
  240. }
  241. override fun onNext(roomOverall: RoomOverall) {
  242. currentConversation = roomOverall.ocs.data
  243. loadAvatarForStatusBar()
  244. setTitle()
  245. setupMentionAutocomplete()
  246. checkReadOnlyState()
  247. checkLobbyState()
  248. if (!inConversation) {
  249. joinRoomWithPassword()
  250. }
  251. }
  252. override fun onError(e: Throwable) {
  253. }
  254. override fun onComplete() {
  255. if (shouldRepeat) {
  256. if (lobbyTimerHandler == null) {
  257. lobbyTimerHandler = Handler()
  258. }
  259. lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000)
  260. }
  261. }
  262. })
  263. }
  264. }
  265. private fun handleFromNotification() {
  266. var apiVersion = 1
  267. // FIXME Can this be called for guests?
  268. if (conversationUser != null) {
  269. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  270. }
  271. ncApi?.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl))
  272. ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
  273. ?.subscribe(object : Observer<RoomsOverall> {
  274. override fun onSubscribe(d: Disposable) {
  275. disposableList.add(d)
  276. }
  277. override fun onNext(roomsOverall: RoomsOverall) {
  278. for (conversation in roomsOverall.ocs.data) {
  279. if (roomId == conversation.roomId) {
  280. roomToken = conversation.token
  281. currentConversation = conversation
  282. setTitle()
  283. getRoomInfo()
  284. break
  285. }
  286. }
  287. }
  288. override fun onError(e: Throwable) {
  289. }
  290. override fun onComplete() {
  291. }
  292. })
  293. }
  294. private fun loadAvatarForStatusBar() {
  295. if (inOneToOneCall() && activity != null && conversationVoiceCallMenuItem != null) {
  296. val avatarSize = DisplayUtils.convertDpToPixel(
  297. conversationVoiceCallMenuItem?.icon!!
  298. .intrinsicWidth.toFloat(),
  299. activity
  300. ).toInt()
  301. val imageRequest = DisplayUtils.getImageRequestForUrl(
  302. ApiUtils.getUrlForAvatarWithNameAndPixels(
  303. conversationUser?.baseUrl,
  304. currentConversation?.name, avatarSize / 2
  305. ),
  306. conversationUser!!
  307. )
  308. val imagePipeline = Fresco.getImagePipeline()
  309. val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
  310. dataSource.subscribe(
  311. object : BaseBitmapDataSubscriber() {
  312. override fun onNewResultImpl(bitmap: Bitmap?) {
  313. if (actionBar != null && bitmap != null && resources != null) {
  314. val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap)
  315. roundedBitmapDrawable.isCircular = true
  316. roundedBitmapDrawable.setAntiAlias(true)
  317. actionBar?.setIcon(roundedBitmapDrawable)
  318. }
  319. }
  320. override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {}
  321. },
  322. UiThreadImmediateExecutorService.getInstance()
  323. )
  324. }
  325. }
  326. private fun inOneToOneCall() = currentConversation != null && currentConversation?.type != null &&
  327. currentConversation?.type == Conversation.ConversationType
  328. .ROOM_TYPE_ONE_TO_ONE_CALL
  329. override fun onViewBound(view: View) {
  330. actionBar?.show()
  331. var adapterWasNull = false
  332. if (adapter == null) {
  333. binding?.progressBar?.visibility = View.VISIBLE
  334. adapterWasNull = true
  335. val messageHolders = MessageHolders()
  336. messageHolders.setIncomingTextConfig(
  337. MagicIncomingTextMessageViewHolder::class.java,
  338. R.layout.item_custom_incoming_text_message
  339. )
  340. messageHolders.setOutcomingTextConfig(
  341. MagicOutcomingTextMessageViewHolder::class.java,
  342. R.layout.item_custom_outcoming_text_message
  343. )
  344. messageHolders.setIncomingImageConfig(
  345. MagicPreviewMessageViewHolder::class.java,
  346. R.layout.item_custom_incoming_preview_message
  347. )
  348. messageHolders.setOutcomingImageConfig(
  349. MagicPreviewMessageViewHolder::class.java,
  350. R.layout.item_custom_outcoming_preview_message
  351. )
  352. messageHolders.registerContentType(
  353. CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java,
  354. R.layout.item_system_message, MagicSystemMessageViewHolder::class.java,
  355. R.layout.item_system_message,
  356. this
  357. )
  358. messageHolders.registerContentType(
  359. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  360. MagicUnreadNoticeMessageViewHolder::class.java,
  361. R.layout.item_date_header,
  362. MagicUnreadNoticeMessageViewHolder::class.java,
  363. R.layout.item_date_header, this
  364. )
  365. var senderId = ""
  366. if (!conversationUser?.userId.equals("?")) {
  367. senderId = "users/" + conversationUser?.userId
  368. } else {
  369. senderId = currentConversation?.getActorType() + "/" + currentConversation?.getActorId()
  370. }
  371. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: " + senderId)
  372. adapter = TalkMessagesListAdapter(
  373. senderId,
  374. messageHolders,
  375. ImageLoader { imageView, url, payload ->
  376. val draweeController = Fresco.newDraweeControllerBuilder()
  377. .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser))
  378. .setControllerListener(DisplayUtils.getImageControllerListener(imageView))
  379. .setOldController(imageView.controller)
  380. .setAutoPlayAnimations(true)
  381. .build()
  382. imageView.controller = draweeController
  383. }
  384. )
  385. } else {
  386. binding?.messagesListView?.visibility = View.VISIBLE
  387. }
  388. binding?.messagesListView?.setAdapter(adapter)
  389. adapter?.setLoadMoreListener(this)
  390. adapter?.setDateHeadersFormatter { format(it) }
  391. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  392. layoutManager = binding?.messagesListView?.layoutManager as LinearLayoutManager?
  393. binding?.popupBubbleView?.setRecyclerView(binding?.messagesListView)
  394. binding?.popupBubbleView?.setPopupBubbleListener { context ->
  395. if (newMessagesCount != 0) {
  396. val scrollPosition: Int
  397. if (newMessagesCount - 1 < 0) {
  398. scrollPosition = 0
  399. } else {
  400. scrollPosition = newMessagesCount - 1
  401. }
  402. Handler().postDelayed({ binding?.messagesListView?.smoothScrollToPosition(scrollPosition) }, 200)
  403. }
  404. }
  405. if (args.containsKey("showToggleChat") && args.getBoolean("showToggleChat")) {
  406. binding?.callControlToggleChat?.visibility = View.VISIBLE
  407. wasDetached = true
  408. }
  409. binding?.callControlToggleChat?.setOnClickListener {
  410. (activity as MagicCallActivity).showCall()
  411. }
  412. binding?.messagesListView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  413. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  414. super.onScrollStateChanged(recyclerView, newState)
  415. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  416. if (newMessagesCount != 0 && layoutManager != null) {
  417. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
  418. newMessagesCount = 0
  419. if (binding?.popupBubbleView?.isShown == true) {
  420. binding?.popupBubbleView?.hide()
  421. }
  422. }
  423. }
  424. }
  425. }
  426. })
  427. val filters = arrayOfNulls<InputFilter>(1)
  428. val lengthFilter = CapabilitiesUtil.getMessageMaxLength(conversationUser) ?: 1000
  429. filters[0] = InputFilter.LengthFilter(lengthFilter)
  430. binding?.messageInputView?.inputEditText?.filters = filters
  431. binding?.messageInputView?.inputEditText?.addTextChangedListener(object : TextWatcher {
  432. override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
  433. }
  434. override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
  435. if (s.length >= lengthFilter) {
  436. binding?.messageInputView?.inputEditText?.error = String.format(
  437. Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
  438. Integer.toString(lengthFilter)
  439. )
  440. } else {
  441. binding?.messageInputView?.inputEditText?.error = null
  442. }
  443. val editable = binding?.messageInputView?.inputEditText?.editableText
  444. if (editable != null && binding?.messageInputView?.inputEditText != null) {
  445. val mentionSpans = editable.getSpans(
  446. 0, binding?.messageInputView?.inputEditText!!.length(),
  447. Spans.MentionChipSpan::class.java
  448. )
  449. var mentionSpan: Spans.MentionChipSpan
  450. for (i in mentionSpans.indices) {
  451. mentionSpan = mentionSpans[i]
  452. if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd(mentionSpan)) {
  453. if (editable.subSequence(
  454. editable.getSpanStart(mentionSpan),
  455. editable.getSpanEnd(mentionSpan)
  456. ).toString().trim { it <= ' ' } != mentionSpan.label
  457. ) {
  458. editable.removeSpan(mentionSpan)
  459. }
  460. }
  461. }
  462. }
  463. }
  464. override fun afterTextChanged(s: Editable) {
  465. }
  466. })
  467. binding?.messageInputView?.inputEditText?.setText(sharedText)
  468. binding?.messageInputView?.setAttachmentsListener {
  469. activity?.let { AttachmentDialog(it, this).show() }
  470. }
  471. binding?.messageInputView?.button?.setOnClickListener { v -> submitMessage() }
  472. binding?.messageInputView?.button?.contentDescription = resources?.getString(
  473. R.string
  474. .nc_description_send_message_button
  475. )
  476. if (currentConversation != null && currentConversation?.roomId != null) {
  477. loadAvatarForStatusBar()
  478. setTitle()
  479. }
  480. if (adapterWasNull) {
  481. // we're starting
  482. if (TextUtils.isEmpty(roomToken)) {
  483. handleFromNotification()
  484. } else {
  485. getRoomInfo()
  486. }
  487. }
  488. super.onViewBound(view)
  489. }
  490. private fun checkReadOnlyState() {
  491. if (currentConversation != null) {
  492. if (currentConversation?.shouldShowLobby(conversationUser) ?: false ||
  493. currentConversation?.conversationReadOnlyState != null &&
  494. currentConversation?.conversationReadOnlyState ==
  495. Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  496. ) {
  497. conversationVoiceCallMenuItem?.icon?.alpha = 99
  498. conversationVideoMenuItem?.icon?.alpha = 99
  499. binding?.messageInputView?.visibility = View.GONE
  500. } else {
  501. if (conversationVoiceCallMenuItem != null) {
  502. conversationVoiceCallMenuItem?.icon?.alpha = 255
  503. }
  504. if (conversationVideoMenuItem != null) {
  505. conversationVideoMenuItem?.icon?.alpha = 255
  506. }
  507. if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)
  508. ) {
  509. binding?.messageInputView?.visibility = View.GONE
  510. } else {
  511. binding?.messageInputView?.visibility = View.VISIBLE
  512. }
  513. }
  514. }
  515. }
  516. private fun checkLobbyState() {
  517. if (currentConversation != null && currentConversation?.isLobbyViewApplicable(conversationUser) ?: false) {
  518. if (!checkingLobbyStatus) {
  519. getRoomInfo()
  520. }
  521. if (currentConversation?.shouldShowLobby(conversationUser) ?: false) {
  522. binding?.lobby?.lobbyView?.visibility = View.VISIBLE
  523. binding?.messagesListView?.visibility = View.GONE
  524. binding?.messageInputView?.visibility = View.GONE
  525. binding?.progressBar?.visibility = View.GONE
  526. if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer !=
  527. 0L
  528. ) {
  529. binding?.lobby?.lobbyTextView?.text = String.format(
  530. resources!!.getString(R.string.nc_lobby_waiting_with_date),
  531. DateUtils.getLocalDateStringFromTimestampForLobby(
  532. currentConversation?.lobbyTimer
  533. ?: 0
  534. )
  535. )
  536. } else {
  537. binding?.lobby?.lobbyTextView?.setText(R.string.nc_lobby_waiting)
  538. }
  539. } else {
  540. binding?.lobby?.lobbyView?.visibility = View.GONE
  541. binding?.messagesListView?.visibility = View.VISIBLE
  542. binding?.messageInputView?.inputEditText?.visibility = View.VISIBLE
  543. if (isFirstMessagesProcessing && pastPreconditionFailed) {
  544. pastPreconditionFailed = false
  545. pullChatMessages(0)
  546. } else if (futurePreconditionFailed) {
  547. futurePreconditionFailed = false
  548. pullChatMessages(1)
  549. }
  550. }
  551. } else {
  552. binding?.lobby?.lobbyView?.visibility = View.GONE
  553. binding?.messagesListView?.visibility = View.VISIBLE
  554. binding?.messageInputView?.inputEditText?.visibility = View.VISIBLE
  555. }
  556. }
  557. override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
  558. if (requestCode == REQUEST_CODE_CHOOSE_FILE) {
  559. if (resultCode == RESULT_OK) {
  560. try {
  561. checkNotNull(intent)
  562. filesToUpload.clear()
  563. intent.clipData?.let {
  564. for (index in 0 until it.itemCount) {
  565. filesToUpload.add(it.getItemAt(index).uri.toString())
  566. }
  567. } ?: run {
  568. checkNotNull(intent.data)
  569. intent.data.let {
  570. filesToUpload.add(intent.data.toString())
  571. }
  572. }
  573. require(filesToUpload.isNotEmpty())
  574. val filenamesWithLinebreaks = StringBuilder("\n")
  575. for (file in filesToUpload) {
  576. val filename = UriUtils.getFileName(Uri.parse(file), context)
  577. filenamesWithLinebreaks.append(filename).append("\n")
  578. }
  579. val confirmationQuestion = when (filesToUpload.size) {
  580. 1 -> context?.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
  581. String.format(it, title)
  582. }
  583. else -> context?.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
  584. String.format(it, title)
  585. }
  586. }
  587. LovelyStandardDialog(activity)
  588. .setPositiveButtonColorRes(R.color.nc_darkGreen)
  589. .setTitle(confirmationQuestion)
  590. .setMessage(filenamesWithLinebreaks.toString())
  591. .setPositiveButton(R.string.nc_yes) { v ->
  592. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
  593. uploadFiles(filesToUpload)
  594. } else {
  595. UploadAndShareFilesWorker.requestStoragePermission(this)
  596. }
  597. }
  598. .setNegativeButton(R.string.nc_no) {}
  599. .show()
  600. } catch (e: IllegalStateException) {
  601. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  602. .show()
  603. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  604. } catch (e: IllegalArgumentException) {
  605. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  606. .show()
  607. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  608. }
  609. }
  610. }
  611. }
  612. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  613. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION &&
  614. grantResults.isNotEmpty() &&
  615. grantResults[0] == PackageManager.PERMISSION_GRANTED
  616. ) {
  617. Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
  618. uploadFiles(filesToUpload)
  619. } else {
  620. Toast.makeText(context, context?.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG).show()
  621. }
  622. }
  623. private fun uploadFiles(files: MutableList<String>) {
  624. try {
  625. require(files.isNotEmpty())
  626. val data: Data = Data.Builder()
  627. .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray())
  628. .putString(
  629. UploadAndShareFilesWorker.NC_TARGETPATH,
  630. CapabilitiesUtil.getAttachmentFolder(conversationUser)
  631. )
  632. .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken)
  633. .build()
  634. val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
  635. .setInputData(data)
  636. .build()
  637. WorkManager.getInstance().enqueue(uploadWorker)
  638. Toast.makeText(
  639. context, context?.getString(R.string.nc_upload_in_progess),
  640. Toast.LENGTH_LONG
  641. ).show()
  642. } catch (e: IllegalArgumentException) {
  643. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
  644. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  645. }
  646. }
  647. fun sendSelectLocalFileIntent() {
  648. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  649. type = "*/*"
  650. addCategory(Intent.CATEGORY_OPENABLE)
  651. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  652. }
  653. startActivityForResult(
  654. Intent.createChooser(
  655. action,
  656. context?.resources?.getString(
  657. R.string.nc_upload_choose_local_files
  658. )
  659. ),
  660. REQUEST_CODE_CHOOSE_FILE
  661. )
  662. }
  663. fun showBrowserScreen(browserType: BrowserController.BrowserType) {
  664. val bundle = Bundle()
  665. bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap<BrowserController.BrowserType>(browserType))
  666. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(conversationUser))
  667. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  668. router.pushController(
  669. RouterTransaction.with(BrowserForSharingController(bundle))
  670. .pushChangeHandler(VerticalChangeHandler())
  671. .popChangeHandler(VerticalChangeHandler())
  672. )
  673. }
  674. private fun showConversationInfoScreen() {
  675. val bundle = Bundle()
  676. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
  677. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  678. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall())
  679. router.pushController(
  680. RouterTransaction.with(ConversationInfoController(bundle))
  681. .pushChangeHandler(HorizontalChangeHandler())
  682. .popChangeHandler(HorizontalChangeHandler())
  683. )
  684. }
  685. private fun setupMentionAutocomplete() {
  686. val elevation = 6f
  687. resources?.let {
  688. val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
  689. val presenter = MentionAutocompletePresenter(activity, roomToken)
  690. val callback = MentionAutocompleteCallback(
  691. activity,
  692. conversationUser,
  693. binding?.messageInputView?.inputEditText
  694. )
  695. if (mentionAutocomplete == null && binding?.messageInputView?.inputEditText != null) {
  696. mentionAutocomplete = Autocomplete.on<Mention>(binding?.messageInputView?.inputEditText)
  697. .with(elevation)
  698. .with(backgroundDrawable)
  699. .with(MagicCharPolicy('@'))
  700. .with(presenter)
  701. .with(callback)
  702. .build()
  703. }
  704. }
  705. }
  706. override fun onAttach(view: View) {
  707. super.onAttach(view)
  708. eventBus?.register(this)
  709. if (conversationUser?.userId != "?" &&
  710. CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "mention-flag") ?: false &&
  711. activity != null
  712. ) {
  713. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener { v -> showConversationInfoScreen() }
  714. }
  715. isLeavingForConversation = false
  716. ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  717. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomId
  718. ApplicationWideCurrentRoomHolder.getInstance().isInCall = false
  719. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  720. isLinkPreviewAllowed = appPreferences?.areLinkPreviewsAllowed ?: false
  721. val smileyButton = binding?.messageInputView?.findViewById<ImageButton>(R.id.smileyButton)
  722. emojiPopup = binding?.messageInputView?.inputEditText?.let {
  723. EmojiPopup.Builder.fromRootView(view).setOnEmojiPopupShownListener {
  724. if (resources != null) {
  725. smileyButton?.setColorFilter(
  726. resources!!.getColor(R.color.colorPrimary),
  727. PorterDuff.Mode.SRC_IN
  728. )
  729. }
  730. }.setOnEmojiPopupDismissListener {
  731. smileyButton?.setColorFilter(
  732. resources!!.getColor(R.color.emoji_icons),
  733. PorterDuff.Mode.SRC_IN
  734. )
  735. }.setOnEmojiClickListener { emoji,
  736. imageView ->
  737. binding?.messageInputView?.inputEditText?.editableText?.append(" ")
  738. }.build(it)
  739. }
  740. smileyButton?.setOnClickListener {
  741. emojiPopup?.toggle()
  742. }
  743. binding?.messageInputView?.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
  744. cancelReply()
  745. }
  746. if (activity != null) {
  747. KeyboardUtils(activity, getView(), false)
  748. }
  749. cancelNotificationsForCurrentConversation()
  750. if (inConversation) {
  751. if (wasDetached) {
  752. currentConversation?.sessionId = "0"
  753. wasDetached = false
  754. joinRoomWithPassword()
  755. }
  756. }
  757. }
  758. private fun cancelReply() {
  759. binding?.messageInputView?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility = View.GONE
  760. binding?.messageInputView?.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
  761. binding?.messageInputView?.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE
  762. }
  763. private fun cancelNotificationsForCurrentConversation() {
  764. if (conversationUser != null) {
  765. if (!TextUtils.isEmpty(roomToken)) {
  766. NotificationUtils.cancelExistingNotificationsForRoom(
  767. applicationContext,
  768. conversationUser,
  769. roomToken!!
  770. )
  771. }
  772. }
  773. }
  774. override fun onDetach(view: View) {
  775. super.onDetach(view)
  776. if (!isLeavingForConversation) {
  777. // current room is still "active", we need the info
  778. ApplicationWideCurrentRoomHolder.getInstance().clear()
  779. }
  780. eventBus?.unregister(this)
  781. if (activity != null) {
  782. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  783. }
  784. if (conversationUser != null &&
  785. activity != null &&
  786. !activity?.isChangingConfigurations!! &&
  787. !isLeavingForConversation
  788. ) {
  789. wasDetached = true
  790. leaveRoom()
  791. }
  792. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  793. mentionAutocomplete?.dismissPopup()
  794. }
  795. }
  796. override val title: String
  797. get() =
  798. if (currentConversation?.displayName != null) {
  799. " " + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  800. } else {
  801. ""
  802. }
  803. public override fun onDestroy() {
  804. super.onDestroy()
  805. if (activity != null) {
  806. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  807. }
  808. if (actionBar != null) {
  809. actionBar?.setIcon(null)
  810. }
  811. adapter = null
  812. inConversation = false
  813. }
  814. private fun dispose() {
  815. for (disposable in disposableList) {
  816. if (!disposable.isDisposed()) {
  817. disposable.dispose()
  818. }
  819. }
  820. }
  821. private fun joinRoomWithPassword() {
  822. if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) ||
  823. currentConversation?.sessionId == "0"
  824. ) {
  825. var apiVersion = 1
  826. // FIXME Fix API checking with guests?
  827. if (conversationUser != null) {
  828. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  829. }
  830. ncApi?.joinRoom(
  831. credentials,
  832. ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken),
  833. roomPassword
  834. )
  835. ?.subscribeOn(Schedulers.io())
  836. ?.observeOn(AndroidSchedulers.mainThread())
  837. ?.retry(3)
  838. ?.subscribe(object : Observer<RoomOverall> {
  839. override fun onSubscribe(d: Disposable) {
  840. disposableList.add(d)
  841. }
  842. override fun onNext(roomOverall: RoomOverall) {
  843. inConversation = true
  844. currentConversation?.sessionId = roomOverall.ocs.data.sessionId
  845. ApplicationWideCurrentRoomHolder.getInstance().session =
  846. currentConversation?.sessionId
  847. setupWebsocket()
  848. checkLobbyState()
  849. if (isFirstMessagesProcessing) {
  850. pullChatMessages(0)
  851. } else {
  852. pullChatMessages(1, 0)
  853. }
  854. if (magicWebSocketInstance != null) {
  855. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  856. roomToken,
  857. currentConversation?.sessionId
  858. )
  859. }
  860. if (startCallFromNotification != null && startCallFromNotification ?: false) {
  861. startCallFromNotification = false
  862. startACall(voiceOnly)
  863. }
  864. }
  865. override fun onError(e: Throwable) {
  866. }
  867. override fun onComplete() {
  868. }
  869. })
  870. } else {
  871. inConversation = true
  872. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId
  873. if (magicWebSocketInstance != null) {
  874. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  875. roomToken,
  876. currentConversation?.sessionId
  877. )
  878. }
  879. if (isFirstMessagesProcessing) {
  880. pullChatMessages(0)
  881. } else {
  882. pullChatMessages(1)
  883. }
  884. }
  885. }
  886. private fun leaveRoom() {
  887. var apiVersion = 1
  888. // FIXME Fix API checking with guests?
  889. if (conversationUser != null) {
  890. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  891. }
  892. ncApi?.leaveRoom(
  893. credentials,
  894. ApiUtils.getUrlForParticipantsActive(
  895. apiVersion,
  896. conversationUser?.baseUrl,
  897. roomToken
  898. )
  899. )
  900. ?.subscribeOn(Schedulers.io())
  901. ?.observeOn(AndroidSchedulers.mainThread())
  902. ?.subscribe(object : Observer<GenericOverall> {
  903. override fun onSubscribe(d: Disposable) {
  904. disposableList.add(d)
  905. }
  906. override fun onNext(genericOverall: GenericOverall) {
  907. checkingLobbyStatus = false
  908. if (lobbyTimerHandler != null) {
  909. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  910. }
  911. if (magicWebSocketInstance != null && currentConversation != null) {
  912. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  913. "",
  914. currentConversation?.sessionId
  915. )
  916. }
  917. if (!isDestroyed && !isBeingDestroyed && !wasDetached) {
  918. router.popCurrentController()
  919. }
  920. }
  921. override fun onError(e: Throwable) {}
  922. override fun onComplete() {
  923. dispose()
  924. }
  925. })
  926. }
  927. private fun submitMessage() {
  928. if (binding?.messageInputView?.inputEditText != null) {
  929. val editable = binding?.messageInputView?.inputEditText!!.editableText
  930. val mentionSpans = editable.getSpans(
  931. 0, editable.length,
  932. Spans.MentionChipSpan::class.java
  933. )
  934. var mentionSpan: Spans.MentionChipSpan
  935. for (i in mentionSpans.indices) {
  936. mentionSpan = mentionSpans[i]
  937. var mentionId = mentionSpan.id
  938. if (mentionId.contains(" ") || mentionId.startsWith("guest/")) {
  939. mentionId = "\"" + mentionId + "\""
  940. }
  941. editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
  942. }
  943. binding?.messageInputView?.inputEditText?.setText("")
  944. val replyMessageId: Int? = view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
  945. sendMessage(
  946. editable,
  947. if (
  948. view
  949. ?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  950. ?.visibility == View.VISIBLE
  951. ) replyMessageId else null
  952. )
  953. cancelReply()
  954. }
  955. }
  956. private fun sendMessage(message: CharSequence, replyTo: Int?) {
  957. if (conversationUser != null) {
  958. val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  959. ncApi!!.sendChatMessage(
  960. credentials,
  961. ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, roomToken),
  962. message,
  963. conversationUser.displayName,
  964. replyTo
  965. )
  966. ?.subscribeOn(Schedulers.io())
  967. ?.observeOn(AndroidSchedulers.mainThread())
  968. ?.subscribe(object : Observer<GenericOverall> {
  969. override fun onSubscribe(d: Disposable) {
  970. // unused atm
  971. }
  972. override fun onNext(genericOverall: GenericOverall) {
  973. myFirstMessage = message
  974. if (binding?.popupBubbleView?.isShown == true) {
  975. binding?.popupBubbleView?.hide()
  976. }
  977. binding?.messagesListView?.smoothScrollToPosition(0)
  978. }
  979. override fun onError(e: Throwable) {
  980. if (e is HttpException) {
  981. val code = e.code()
  982. if (Integer.toString(code).startsWith("2")) {
  983. myFirstMessage = message
  984. if (binding?.popupBubbleView?.isShown == true) {
  985. binding?.popupBubbleView?.hide()
  986. }
  987. binding?.messagesListView?.smoothScrollToPosition(0)
  988. }
  989. }
  990. }
  991. override fun onComplete() {
  992. // unused atm
  993. }
  994. })
  995. }
  996. }
  997. private fun setupWebsocket() {
  998. if (conversationUser != null) {
  999. if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) != null) {
  1000. magicWebSocketInstance =
  1001. WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id)
  1002. } else {
  1003. magicWebSocketInstance = null
  1004. }
  1005. }
  1006. }
  1007. fun pullChatMessages(lookIntoFuture: Int, setReadMarker: Int = 1, xChatLastCommonRead: Int? = null) {
  1008. if (!inConversation) {
  1009. return
  1010. }
  1011. if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)) {
  1012. // return
  1013. }
  1014. val fieldMap = HashMap<String, Int>()
  1015. fieldMap["includeLastKnown"] = 0
  1016. if (lookIntoFuture > 0) {
  1017. lookingIntoFuture = true
  1018. } else if (isFirstMessagesProcessing) {
  1019. if (currentConversation != null) {
  1020. globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage
  1021. globalLastKnownPastMessageId = currentConversation!!.lastReadMessage
  1022. fieldMap["includeLastKnown"] = 1
  1023. }
  1024. }
  1025. val timeout = if (lookingIntoFuture) {
  1026. 30
  1027. } else {
  1028. 0
  1029. }
  1030. fieldMap["timeout"] = timeout
  1031. fieldMap["lookIntoFuture"] = lookIntoFuture
  1032. fieldMap["limit"] = 100
  1033. fieldMap["setReadMarker"] = setReadMarker
  1034. val lastKnown: Int
  1035. if (lookIntoFuture > 0) {
  1036. lastKnown = globalLastKnownFutureMessageId
  1037. } else {
  1038. lastKnown = globalLastKnownPastMessageId
  1039. }
  1040. fieldMap["lastKnownMessageId"] = lastKnown
  1041. xChatLastCommonRead?.let {
  1042. fieldMap["lastCommonReadId"] = it
  1043. }
  1044. if (!wasDetached) {
  1045. var apiVersion = 1
  1046. // FIXME this is a best guess, guests would need to get the capabilities themselves
  1047. if (conversationUser != null) {
  1048. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1049. }
  1050. if (lookIntoFuture > 0) {
  1051. val finalTimeout = timeout
  1052. ncApi?.pullChatMessages(
  1053. credentials,
  1054. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
  1055. )
  1056. ?.subscribeOn(Schedulers.io())
  1057. ?.observeOn(AndroidSchedulers.mainThread())
  1058. ?.takeWhile { observable -> inConversation && !wasDetached }
  1059. ?.subscribe(object : Observer<Response<*>> {
  1060. override fun onSubscribe(d: Disposable) {
  1061. disposableList.add(d)
  1062. }
  1063. override fun onNext(response: Response<*>) {
  1064. if (response.code() == 304) {
  1065. pullChatMessages(1, setReadMarker, xChatLastCommonRead)
  1066. } else if (response.code() == 412) {
  1067. futurePreconditionFailed = true
  1068. } else {
  1069. processMessages(response, true, finalTimeout)
  1070. }
  1071. }
  1072. override fun onError(e: Throwable) {
  1073. // unused atm
  1074. }
  1075. override fun onComplete() {
  1076. // unused atm
  1077. }
  1078. })
  1079. } else {
  1080. ncApi?.pullChatMessages(
  1081. credentials,
  1082. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
  1083. )
  1084. ?.subscribeOn(Schedulers.io())
  1085. ?.observeOn(AndroidSchedulers.mainThread())
  1086. ?.takeWhile { observable -> inConversation && !wasDetached }
  1087. ?.subscribe(object : Observer<Response<*>> {
  1088. override fun onSubscribe(d: Disposable) {
  1089. disposableList.add(d)
  1090. }
  1091. override fun onNext(response: Response<*>) {
  1092. if (response.code() == 412) {
  1093. pastPreconditionFailed = true
  1094. } else {
  1095. processMessages(response, false, 0)
  1096. }
  1097. }
  1098. override fun onError(e: Throwable) {
  1099. // unused atm
  1100. }
  1101. override fun onComplete() {
  1102. // unused atm
  1103. }
  1104. })
  1105. }
  1106. }
  1107. }
  1108. private fun processMessages(response: Response<*>, isFromTheFuture: Boolean, timeout: Int) {
  1109. val xChatLastGivenHeader: String? = response.headers().get("X-Chat-Last-Given")
  1110. val xChatLastCommonRead = response.headers().get("X-Chat-Last-Common-Read")?.let {
  1111. Integer.parseInt(it)
  1112. }
  1113. if (response.headers().size > 0 && !TextUtils.isEmpty(xChatLastGivenHeader)) {
  1114. val header = Integer.parseInt(xChatLastGivenHeader!!)
  1115. if (header > 0) {
  1116. if (isFromTheFuture) {
  1117. globalLastKnownFutureMessageId = header
  1118. } else {
  1119. if (globalLastKnownFutureMessageId == -1) {
  1120. globalLastKnownFutureMessageId = header
  1121. }
  1122. globalLastKnownPastMessageId = header
  1123. }
  1124. }
  1125. }
  1126. if (response.code() == 200) {
  1127. val chatOverall = response.body() as ChatOverall?
  1128. val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
  1129. if (isFirstMessagesProcessing) {
  1130. cancelNotificationsForCurrentConversation()
  1131. isFirstMessagesProcessing = false
  1132. binding?.progressBar?.visibility = View.GONE
  1133. binding?.messagesListView?.visibility = View.VISIBLE
  1134. }
  1135. var countGroupedMessages = 0
  1136. if (!isFromTheFuture) {
  1137. for (i in chatMessageList.indices) {
  1138. if (chatMessageList.size > i + 1) {
  1139. if (TextUtils.isEmpty(chatMessageList[i].systemMessage) &&
  1140. TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) &&
  1141. chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
  1142. countGroupedMessages < 4 && DateFormatter.isSameDay(
  1143. chatMessageList[i].createdAt,
  1144. chatMessageList[i + 1].createdAt
  1145. )
  1146. ) {
  1147. chatMessageList[i].isGrouped = true
  1148. countGroupedMessages++
  1149. } else {
  1150. countGroupedMessages = 0
  1151. }
  1152. }
  1153. val chatMessage = chatMessageList[i]
  1154. chatMessage.isOneToOneConversation =
  1155. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1156. chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed
  1157. chatMessage.activeUser = conversationUser
  1158. }
  1159. if (adapter != null) {
  1160. adapter?.addToEnd(chatMessageList, false)
  1161. }
  1162. } else {
  1163. var chatMessage: ChatMessage
  1164. val shouldAddNewMessagesNotice = timeout == 0 && adapter?.itemCount ?: 0 > 0 && chatMessageList.size > 0
  1165. if (shouldAddNewMessagesNotice) {
  1166. val unreadChatMessage = ChatMessage()
  1167. unreadChatMessage.jsonMessageId = -1
  1168. unreadChatMessage.actorId = "-1"
  1169. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  1170. unreadChatMessage.message = context?.getString(R.string.nc_new_messages)
  1171. adapter?.addToStart(unreadChatMessage, false)
  1172. }
  1173. val isThereANewNotice =
  1174. shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1
  1175. for (i in chatMessageList.indices) {
  1176. chatMessage = chatMessageList[i]
  1177. chatMessage.activeUser = conversationUser
  1178. chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed
  1179. val shouldScroll =
  1180. !isThereANewNotice &&
  1181. !shouldAddNewMessagesNotice &&
  1182. layoutManager?.findFirstVisibleItemPosition() == 0 ||
  1183. adapter != null &&
  1184. adapter?.itemCount == 0
  1185. if (!shouldAddNewMessagesNotice && !shouldScroll) {
  1186. if (!binding?.popupBubbleView?.isShown!!) {
  1187. newMessagesCount = 1
  1188. binding?.popupBubbleView?.show()
  1189. } else if (binding?.popupBubbleView?.isShown == true) {
  1190. newMessagesCount++
  1191. }
  1192. } else {
  1193. newMessagesCount = 0
  1194. }
  1195. if (adapter != null) {
  1196. chatMessage.isGrouped = (
  1197. adapter!!.isPreviousSameAuthor(
  1198. chatMessage.actorId,
  1199. -1
  1200. ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0
  1201. )
  1202. chatMessage.isOneToOneConversation =
  1203. (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  1204. adapter?.addToStart(chatMessage, shouldScroll)
  1205. }
  1206. }
  1207. if (shouldAddNewMessagesNotice && adapter != null) {
  1208. layoutManager?.scrollToPositionWithOffset(
  1209. adapter!!.getMessagePositionByIdInReverse("-1"),
  1210. binding!!.messagesListView.height / 2
  1211. )
  1212. }
  1213. }
  1214. // update read status of all messages
  1215. for (message in adapter!!.items) {
  1216. xChatLastCommonRead?.let {
  1217. if (message.item is ChatMessage) {
  1218. val chatMessage = message.item as ChatMessage
  1219. if (chatMessage.jsonMessageId <= it) {
  1220. chatMessage.readStatus = ReadStatus.READ
  1221. } else {
  1222. chatMessage.readStatus = ReadStatus.SENT
  1223. }
  1224. }
  1225. }
  1226. }
  1227. adapter?.notifyDataSetChanged()
  1228. if (inConversation) {
  1229. pullChatMessages(1, 1, xChatLastCommonRead)
  1230. }
  1231. } else if (response.code() == 304 && !isFromTheFuture) {
  1232. if (isFirstMessagesProcessing) {
  1233. cancelNotificationsForCurrentConversation()
  1234. isFirstMessagesProcessing = false
  1235. binding?.progressBar?.visibility = View.GONE
  1236. }
  1237. historyRead = true
  1238. if (!lookingIntoFuture && inConversation) {
  1239. pullChatMessages(1)
  1240. }
  1241. }
  1242. }
  1243. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  1244. if (!historyRead && inConversation) {
  1245. pullChatMessages(0)
  1246. }
  1247. }
  1248. override fun format(date: Date): String {
  1249. return if (DateFormatter.isToday(date)) {
  1250. resources!!.getString(R.string.nc_date_header_today)
  1251. } else if (DateFormatter.isYesterday(date)) {
  1252. resources!!.getString(R.string.nc_date_header_yesterday)
  1253. } else {
  1254. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  1255. }
  1256. }
  1257. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  1258. super.onCreateOptionsMenu(menu, inflater)
  1259. inflater.inflate(R.menu.menu_conversation, menu)
  1260. if (conversationUser?.userId == "?") {
  1261. menu.removeItem(R.id.conversation_info)
  1262. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  1263. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  1264. } else {
  1265. conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
  1266. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  1267. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  1268. loadAvatarForStatusBar()
  1269. }
  1270. }
  1271. override fun onPrepareOptionsMenu(menu: Menu) {
  1272. super.onPrepareOptionsMenu(menu)
  1273. conversationUser?.let {
  1274. if (CapabilitiesUtil.hasSpreedFeatureCapability(it, "read-only-rooms")) {
  1275. checkReadOnlyState()
  1276. }
  1277. }
  1278. }
  1279. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  1280. when (item.itemId) {
  1281. android.R.id.home -> {
  1282. router.popCurrentController()
  1283. return true
  1284. }
  1285. R.id.conversation_video_call -> {
  1286. if (conversationVideoMenuItem?.icon?.alpha == 255) {
  1287. startACall(false)
  1288. return true
  1289. }
  1290. return false
  1291. }
  1292. R.id.conversation_voice_call -> {
  1293. if (conversationVoiceCallMenuItem?.icon?.alpha == 255) {
  1294. startACall(true)
  1295. return true
  1296. }
  1297. return false
  1298. }
  1299. R.id.conversation_info -> {
  1300. showConversationInfoScreen()
  1301. return true
  1302. }
  1303. else -> return super.onOptionsItemSelected(item)
  1304. }
  1305. }
  1306. private fun setDeletionFlagsAndRemoveInfomessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  1307. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  1308. val chatMessageIterator = chatMessageMap.iterator()
  1309. while (chatMessageIterator.hasNext()) {
  1310. val currentMessage = chatMessageIterator.next()
  1311. if (isInfoMessageAboutDeletion(currentMessage)) {
  1312. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
  1313. // if chatMessageMap doesnt't contain message to delete (this happens when lookingIntoFuture),
  1314. // the message to delete has to be modified directly inside the adapter
  1315. setMessageAsDeleted(currentMessage.value.parentMessage)
  1316. } else {
  1317. chatMessageMap[currentMessage.value.parentMessage.id]!!.isDeleted = true
  1318. }
  1319. chatMessageIterator.remove()
  1320. }
  1321. }
  1322. return chatMessageMap.values.toList()
  1323. }
  1324. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  1325. return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
  1326. .SystemMessageType.MESSAGE_DELETED
  1327. }
  1328. private fun startACall(isVoiceOnlyCall: Boolean) {
  1329. isLeavingForConversation = true
  1330. val callIntent = getIntentForCall(isVoiceOnlyCall)
  1331. if (callIntent != null) {
  1332. startActivity(callIntent)
  1333. }
  1334. }
  1335. private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? {
  1336. currentConversation?.let {
  1337. val bundle = Bundle()
  1338. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1339. bundle.putString(KEY_ROOM_ID, roomId)
  1340. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  1341. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  1342. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
  1343. bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName)
  1344. if (isVoiceOnlyCall) {
  1345. bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
  1346. }
  1347. return if (activity != null) {
  1348. val callIntent = Intent(activity, MagicCallActivity::class.java)
  1349. callIntent.putExtras(bundle)
  1350. callIntent
  1351. } else {
  1352. null
  1353. }
  1354. } ?: run {
  1355. return null
  1356. }
  1357. }
  1358. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  1359. PopupMenu(
  1360. ContextThemeWrapper(view?.context, R.style.appActionBarPopupMenu),
  1361. view,
  1362. if (
  1363. message?.user?.id == currentConversation?.actorType + "/" + currentConversation?.actorId
  1364. ) Gravity.END else Gravity.START
  1365. ).apply {
  1366. setOnMenuItemClickListener { item ->
  1367. when (item?.itemId) {
  1368. R.id.action_copy_message -> {
  1369. val clipboardManager =
  1370. activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
  1371. val clipData = ClipData.newPlainText(resources?.getString(R.string.nc_app_name), message?.text)
  1372. clipboardManager.setPrimaryClip(clipData)
  1373. true
  1374. }
  1375. R.id.action_reply_to_message -> {
  1376. val chatMessage = message as ChatMessage?
  1377. chatMessage?.let {
  1378. binding?.messageInputView?.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
  1379. View.GONE
  1380. binding?.messageInputView?.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility =
  1381. View.GONE
  1382. binding?.messageInputView?.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
  1383. View.VISIBLE
  1384. val quotedMessage = binding
  1385. ?.messageInputView
  1386. ?.findViewById<EmojiTextView>(R.id.quotedMessage)
  1387. quotedMessage?.maxLines = 2
  1388. quotedMessage?.ellipsize = TextUtils.TruncateAt.END
  1389. quotedMessage?.text = it.text
  1390. binding?.messageInputView?.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
  1391. it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest)
  1392. conversationUser?.let { currentUser ->
  1393. val quotedMessageImage = binding
  1394. ?.messageInputView
  1395. ?.findViewById<ImageView>(R.id.quotedMessageImage)
  1396. chatMessage.imageUrl?.let { previewImageUrl ->
  1397. quotedMessageImage?.visibility = View.VISIBLE
  1398. val px = TypedValue.applyDimension(
  1399. TypedValue.COMPLEX_UNIT_DIP,
  1400. 96f,
  1401. resources?.displayMetrics
  1402. )
  1403. quotedMessageImage?.maxHeight = px.toInt()
  1404. val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
  1405. layoutParams.flexGrow = 0f
  1406. quotedMessageImage?.layoutParams = layoutParams
  1407. quotedMessageImage?.load(previewImageUrl) {
  1408. addHeader("Authorization", credentials!!)
  1409. }
  1410. } ?: run {
  1411. binding
  1412. ?.messageInputView
  1413. ?.findViewById<ImageView>(R.id.quotedMessageImage)
  1414. ?.visibility = View.GONE
  1415. }
  1416. }
  1417. val quotedChatMessageView = binding
  1418. ?.messageInputView
  1419. ?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  1420. quotedChatMessageView?.tag = message?.jsonMessageId
  1421. quotedChatMessageView?.visibility = View.VISIBLE
  1422. }
  1423. true
  1424. }
  1425. R.id.action_reply_privately -> {
  1426. val apiVersion =
  1427. ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1428. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  1429. apiVersion,
  1430. conversationUser?.baseUrl,
  1431. "1",
  1432. null,
  1433. message?.user?.id?.substring(6),
  1434. null
  1435. )
  1436. ncApi!!.createRoom(
  1437. credentials,
  1438. retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
  1439. )
  1440. .subscribeOn(Schedulers.io())
  1441. .observeOn(AndroidSchedulers.mainThread())
  1442. .subscribe(object : Observer<RoomOverall> {
  1443. override fun onSubscribe(d: Disposable) {}
  1444. override fun onNext(roomOverall: RoomOverall) {
  1445. val bundle = Bundle()
  1446. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  1447. bundle.putString(KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
  1448. bundle.putString(KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
  1449. // FIXME once APIv2 or later is used only, the createRoom already returns all the data
  1450. ncApi!!.getRoom(
  1451. credentials,
  1452. ApiUtils.getUrlForRoom(
  1453. apiVersion, conversationUser?.baseUrl,
  1454. roomOverall.getOcs().getData().getToken()
  1455. )
  1456. )
  1457. .subscribeOn(Schedulers.io())
  1458. .observeOn(AndroidSchedulers.mainThread())
  1459. .subscribe(object : Observer<RoomOverall> {
  1460. override fun onSubscribe(d: Disposable) {
  1461. // unused atm
  1462. }
  1463. override fun onNext(roomOverall: RoomOverall) {
  1464. bundle.putParcelable(
  1465. KEY_ACTIVE_CONVERSATION,
  1466. Parcels.wrap(roomOverall.getOcs().getData())
  1467. )
  1468. remapChatController(
  1469. router, conversationUser!!.id,
  1470. roomOverall.getOcs().getData().getToken(), bundle, true
  1471. )
  1472. }
  1473. override fun onError(e: Throwable) {
  1474. Log.e(TAG, e.message, e)
  1475. }
  1476. override fun onComplete() {
  1477. // unused atm
  1478. }
  1479. })
  1480. }
  1481. override fun onError(e: Throwable) {
  1482. Log.e(TAG, e.message, e)
  1483. }
  1484. override fun onComplete() {
  1485. // unused atm
  1486. }
  1487. })
  1488. true
  1489. }
  1490. R.id.action_delete_message -> {
  1491. var apiVersion = 1
  1492. // FIXME Fix API checking with guests?
  1493. if (conversationUser != null) {
  1494. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1495. }
  1496. ncApi?.deleteChatMessage(
  1497. credentials,
  1498. ApiUtils.getUrlForChatMessage(
  1499. apiVersion,
  1500. conversationUser?.baseUrl,
  1501. roomToken,
  1502. message?.id
  1503. )
  1504. )?.subscribeOn(Schedulers.io())
  1505. ?.observeOn(AndroidSchedulers.mainThread())
  1506. ?.subscribe(object : Observer<ChatOverallSingleMessage> {
  1507. override fun onSubscribe(d: Disposable) {
  1508. // unused atm
  1509. }
  1510. override fun onNext(t: ChatOverallSingleMessage) {
  1511. if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  1512. Toast.makeText(
  1513. context, R.string.nc_delete_message_leaked_to_matterbridge,
  1514. Toast.LENGTH_LONG
  1515. ).show()
  1516. }
  1517. }
  1518. override fun onError(e: Throwable) {
  1519. Log.e(
  1520. TAG,
  1521. "Something went wrong when trying to delete message with id " +
  1522. message?.id,
  1523. e
  1524. )
  1525. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  1526. }
  1527. override fun onComplete() {
  1528. // unused atm
  1529. }
  1530. })
  1531. true
  1532. }
  1533. else -> false
  1534. }
  1535. }
  1536. inflate(R.menu.chat_message_menu)
  1537. menu.findItem(R.id.action_copy_message).isVisible = !(message as ChatMessage).isDeleted
  1538. menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable
  1539. menu.findItem(R.id.action_reply_privately).isVisible = (message as ChatMessage).replyable &&
  1540. conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
  1541. (message as ChatMessage).user.id.startsWith("users/") &&
  1542. (message as ChatMessage).user.id.substring(6) != currentConversation?.actorId &&
  1543. currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1544. menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
  1545. if (menu.hasVisibleItems()) {
  1546. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
  1547. setForceShowIcon(true)
  1548. }
  1549. show()
  1550. }
  1551. }
  1552. }
  1553. private fun setMessageAsDeleted(message: IMessage?) {
  1554. val messageTemp = message as ChatMessage
  1555. messageTemp.isDeleted = true
  1556. messageTemp.isOneToOneConversation =
  1557. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1558. messageTemp.isLinkPreviewAllowed = isLinkPreviewAllowed
  1559. messageTemp.activeUser = conversationUser
  1560. adapter?.update(messageTemp)
  1561. }
  1562. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  1563. if (conversationUser == null) return false
  1564. if (message.systemMessageType != ChatMessage.SystemMessageType.DUMMY) return false
  1565. if (message.isDeleted) return false
  1566. if (message.hasFileAttachment()) return false
  1567. val sixHoursInMillis = 6 * 3600 * 1000
  1568. val isOlderThanSixHours = message
  1569. .createdAt
  1570. ?.before(Date(System.currentTimeMillis() - sixHoursInMillis)) == true
  1571. if (isOlderThanSixHours) return false
  1572. val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
  1573. true
  1574. } else {
  1575. currentConversation!!.isParticipantOwnerOrModerator
  1576. }
  1577. if (!isUserAllowedByPrivileges) return false
  1578. if (!CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "delete-messages")) return false
  1579. return true
  1580. }
  1581. override fun hasContentFor(message: IMessage, type: Byte): Boolean {
  1582. when (type) {
  1583. CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)
  1584. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1"
  1585. }
  1586. return false
  1587. }
  1588. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  1589. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  1590. /*
  1591. switch (webSocketCommunicationEvent.getType()) {
  1592. case "refreshChat":
  1593. if (webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID).equals(Long.toString(conversationUser.getId()))) {
  1594. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  1595. pullChatMessages(2);
  1596. }
  1597. }
  1598. break;
  1599. default:
  1600. }*/
  1601. }
  1602. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  1603. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  1604. if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  1605. currentConversation?.name != userMentionClickEvent.userId
  1606. ) {
  1607. var apiVersion = 1
  1608. // FIXME Fix API checking with guests?
  1609. if (conversationUser != null) {
  1610. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1611. }
  1612. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  1613. apiVersion,
  1614. conversationUser?.baseUrl,
  1615. "1",
  1616. null,
  1617. userMentionClickEvent.userId,
  1618. null
  1619. )
  1620. ncApi?.createRoom(
  1621. credentials,
  1622. retrofitBucket.url, retrofitBucket.queryMap
  1623. )
  1624. ?.subscribeOn(Schedulers.io())
  1625. ?.observeOn(AndroidSchedulers.mainThread())
  1626. ?.subscribe(object : Observer<RoomOverall> {
  1627. override fun onSubscribe(d: Disposable) {
  1628. // unused atm
  1629. }
  1630. override fun onNext(roomOverall: RoomOverall) {
  1631. val conversationIntent = Intent(activity, MagicCallActivity::class.java)
  1632. val bundle = Bundle()
  1633. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  1634. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token)
  1635. bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId)
  1636. if (conversationUser != null) {
  1637. bundle.putParcelable(
  1638. KEY_ACTIVE_CONVERSATION,
  1639. Parcels.wrap(roomOverall.ocs.data)
  1640. )
  1641. conversationIntent.putExtras(bundle)
  1642. ConductorRemapping.remapChatController(
  1643. router, conversationUser.id,
  1644. roomOverall.ocs.data.token, bundle, false
  1645. )
  1646. } else {
  1647. conversationIntent.putExtras(bundle)
  1648. startActivity(conversationIntent)
  1649. Handler().postDelayed(
  1650. {
  1651. if (!isDestroyed && !isBeingDestroyed) {
  1652. router.popCurrentController()
  1653. }
  1654. },
  1655. 100
  1656. )
  1657. }
  1658. }
  1659. override fun onError(e: Throwable) {
  1660. // unused atm
  1661. }
  1662. override fun onComplete() {
  1663. // unused atm
  1664. }
  1665. })
  1666. }
  1667. }
  1668. companion object {
  1669. private val TAG = "ChatController"
  1670. private val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
  1671. private val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
  1672. val REQUEST_CODE_CHOOSE_FILE: Int = 555
  1673. }
  1674. }