ChatController.kt 82 KB

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