ChatController.kt 78 KB

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