ChatController.kt 72 KB

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