ChatController.kt 74 KB

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