ChatController.kt 78 KB

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