ChatController.kt 83 KB

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