ChatController.kt 81 KB

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