ChatController.kt 62 KB

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