ChatController.kt 104 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Marcel Hibbe
  6. * @author Andy Scherzinger
  7. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  8. * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  9. * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. */
  24. package com.nextcloud.talk.controllers
  25. import android.Manifest
  26. import android.annotation.SuppressLint
  27. import android.app.Activity.RESULT_OK
  28. import android.content.ClipData
  29. import android.content.Context
  30. import android.content.Intent
  31. import android.content.pm.PackageManager
  32. import android.content.res.Resources
  33. import android.graphics.Bitmap
  34. import android.graphics.drawable.ColorDrawable
  35. import android.media.MediaPlayer
  36. import android.media.MediaRecorder
  37. import android.net.Uri
  38. import android.os.Build
  39. import android.os.Build.VERSION_CODES.O
  40. import android.os.Bundle
  41. import android.os.Handler
  42. import android.os.SystemClock
  43. import android.os.VibrationEffect
  44. import android.os.Vibrator
  45. import android.text.Editable
  46. import android.text.InputFilter
  47. import android.text.TextUtils
  48. import android.text.TextWatcher
  49. import android.util.Log
  50. import android.util.TypedValue
  51. import android.view.Gravity
  52. import android.view.Menu
  53. import android.view.MenuInflater
  54. import android.view.MenuItem
  55. import android.view.MotionEvent
  56. import android.view.View
  57. import android.view.animation.AlphaAnimation
  58. import android.view.animation.Animation
  59. import android.view.animation.LinearInterpolator
  60. import android.widget.AbsListView
  61. import android.widget.ImageButton
  62. import android.widget.ImageView
  63. import android.widget.PopupMenu
  64. import android.widget.RelativeLayout
  65. import android.widget.Toast
  66. import androidx.appcompat.view.ContextThemeWrapper
  67. import androidx.core.content.ContextCompat
  68. import androidx.core.content.PermissionChecker
  69. import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
  70. import androidx.core.widget.doAfterTextChanged
  71. import androidx.emoji.text.EmojiCompat
  72. import androidx.emoji.widget.EmojiTextView
  73. import androidx.recyclerview.widget.ItemTouchHelper
  74. import androidx.recyclerview.widget.LinearLayoutManager
  75. import androidx.recyclerview.widget.RecyclerView
  76. import androidx.work.Data
  77. import androidx.work.OneTimeWorkRequest
  78. import androidx.work.WorkInfo
  79. import androidx.work.WorkManager
  80. import autodagger.AutoInjector
  81. import coil.load
  82. import com.bluelinelabs.conductor.RouterTransaction
  83. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  84. import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
  85. import com.facebook.common.executors.UiThreadImmediateExecutorService
  86. import com.facebook.common.references.CloseableReference
  87. import com.facebook.datasource.DataSource
  88. import com.facebook.drawee.backends.pipeline.Fresco
  89. import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
  90. import com.facebook.imagepipeline.image.CloseableImage
  91. import com.google.android.flexbox.FlexboxLayout
  92. import com.nextcloud.talk.R
  93. import com.nextcloud.talk.activities.MagicCallActivity
  94. import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
  95. import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
  96. import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
  97. import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
  98. import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
  99. import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
  100. import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
  101. import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
  102. import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
  103. import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
  104. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  105. import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
  106. import com.nextcloud.talk.api.NcApi
  107. import com.nextcloud.talk.application.NextcloudTalkApplication
  108. import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
  109. import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
  110. import com.nextcloud.talk.components.filebrowser.controllers.BrowserForSharingController
  111. import com.nextcloud.talk.controllers.base.NewBaseController
  112. import com.nextcloud.talk.controllers.util.viewBinding
  113. import com.nextcloud.talk.databinding.ControllerChatBinding
  114. import com.nextcloud.talk.events.UserMentionClickEvent
  115. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  116. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  117. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  118. import com.nextcloud.talk.models.database.CapabilitiesUtil
  119. import com.nextcloud.talk.models.database.UserEntity
  120. import com.nextcloud.talk.models.json.chat.ChatMessage
  121. import com.nextcloud.talk.models.json.chat.ChatOverall
  122. import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
  123. import com.nextcloud.talk.models.json.chat.ReadStatus
  124. import com.nextcloud.talk.models.json.conversations.Conversation
  125. import com.nextcloud.talk.models.json.conversations.RoomOverall
  126. import com.nextcloud.talk.models.json.conversations.RoomsOverall
  127. import com.nextcloud.talk.models.json.generic.GenericOverall
  128. import com.nextcloud.talk.models.json.mention.Mention
  129. import com.nextcloud.talk.presenters.MentionAutocompletePresenter
  130. import com.nextcloud.talk.ui.dialog.AttachmentDialog
  131. import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
  132. import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
  133. import com.nextcloud.talk.utils.ApiUtils
  134. import com.nextcloud.talk.utils.ConductorRemapping
  135. import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
  136. import com.nextcloud.talk.utils.DateUtils
  137. import com.nextcloud.talk.utils.DisplayUtils
  138. import com.nextcloud.talk.utils.KeyboardUtils
  139. import com.nextcloud.talk.utils.MagicCharPolicy
  140. import com.nextcloud.talk.utils.NotificationUtils
  141. import com.nextcloud.talk.utils.UriUtils
  142. import com.nextcloud.talk.utils.bundle.BundleKeys
  143. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
  144. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
  145. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  146. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
  147. import com.nextcloud.talk.utils.database.user.UserUtils
  148. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  149. import com.nextcloud.talk.utils.text.Spans
  150. import com.nextcloud.talk.webrtc.MagicWebSocketInstance
  151. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  152. import com.otaliastudios.autocomplete.Autocomplete
  153. import com.stfalcon.chatkit.commons.ImageLoader
  154. import com.stfalcon.chatkit.commons.models.IMessage
  155. import com.stfalcon.chatkit.messages.MessageHolders
  156. import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
  157. import com.stfalcon.chatkit.messages.MessagesListAdapter
  158. import com.stfalcon.chatkit.utils.DateFormatter
  159. import com.vanniktech.emoji.EmojiPopup
  160. import com.yarolegovich.lovelydialog.LovelyStandardDialog
  161. import io.reactivex.Observer
  162. import io.reactivex.android.schedulers.AndroidSchedulers
  163. import io.reactivex.disposables.Disposable
  164. import io.reactivex.schedulers.Schedulers
  165. import kotlinx.android.synthetic.main.view_message_input.view.*
  166. import org.greenrobot.eventbus.EventBus
  167. import org.greenrobot.eventbus.Subscribe
  168. import org.greenrobot.eventbus.ThreadMode
  169. import org.parceler.Parcels
  170. import retrofit2.HttpException
  171. import retrofit2.Response
  172. import java.io.File
  173. import java.io.IOException
  174. import java.net.HttpURLConnection
  175. import java.text.SimpleDateFormat
  176. import java.util.ArrayList
  177. import java.util.Date
  178. import java.util.HashMap
  179. import java.util.Objects
  180. import java.util.concurrent.ExecutionException
  181. import javax.inject.Inject
  182. @AutoInjector(NextcloudTalkApplication::class)
  183. class ChatController(args: Bundle) :
  184. NewBaseController(
  185. R.layout.controller_chat,
  186. args
  187. ),
  188. MessagesListAdapter.OnLoadMoreListener,
  189. MessagesListAdapter.Formatter<Date>,
  190. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  191. ContentChecker<ChatMessage>,
  192. VoiceMessageInterface {
  193. private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
  194. @Inject
  195. @JvmField
  196. var ncApi: NcApi? = null
  197. @Inject
  198. @JvmField
  199. var userUtils: UserUtils? = null
  200. @Inject
  201. @JvmField
  202. var eventBus: EventBus? = null
  203. val disposableList = ArrayList<Disposable>()
  204. var roomToken: String? = null
  205. val conversationUser: UserEntity?
  206. val roomPassword: String
  207. var credentials: String? = null
  208. var currentConversation: Conversation? = null
  209. var inConversation = false
  210. var historyRead = false
  211. var globalLastKnownFutureMessageId = -1
  212. var globalLastKnownPastMessageId = -1
  213. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  214. var mentionAutocomplete: Autocomplete<*>? = null
  215. var layoutManager: LinearLayoutManager? = null
  216. var lookingIntoFuture = false
  217. var newMessagesCount = 0
  218. var startCallFromNotification: Boolean? = null
  219. val roomId: String
  220. val voiceOnly: Boolean
  221. var isFirstMessagesProcessing = true
  222. var isLeavingForConversation: Boolean = false
  223. var isLinkPreviewAllowed: Boolean = false
  224. var wasDetached: Boolean = false
  225. var emojiPopup: EmojiPopup? = null
  226. var myFirstMessage: CharSequence? = null
  227. var checkingLobbyStatus: Boolean = false
  228. var conversationInfoMenuItem: MenuItem? = null
  229. var conversationVoiceCallMenuItem: MenuItem? = null
  230. var conversationVideoMenuItem: MenuItem? = null
  231. var magicWebSocketInstance: MagicWebSocketInstance? = null
  232. var lobbyTimerHandler: Handler? = null
  233. val roomJoined: Boolean = false
  234. var pastPreconditionFailed = false
  235. var futurePreconditionFailed = false
  236. val filesToUpload: MutableList<String> = ArrayList()
  237. var sharedText: String
  238. var isVoiceRecordingInProgress: Boolean = false
  239. var currentVoiceRecordFile: String = ""
  240. private var recorder: MediaRecorder? = null
  241. var mediaPlayer: MediaPlayer? = null
  242. lateinit var mediaPlayerHandler: Handler
  243. var currentlyPlayedVoiceMessage: ChatMessage? = null
  244. init {
  245. setHasOptionsMenu(true)
  246. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  247. this.conversationUser = args.getParcelable(KEY_USER_ENTITY)
  248. this.roomId = args.getString(KEY_ROOM_ID, "")
  249. this.roomToken = args.getString(KEY_ROOM_TOKEN, "")
  250. this.sharedText = args.getString(BundleKeys.KEY_SHARED_TEXT, "")
  251. if (args.containsKey(KEY_ACTIVE_CONVERSATION)) {
  252. this.currentConversation = Parcels.unwrap<Conversation>(args.getParcelable(KEY_ACTIVE_CONVERSATION))
  253. }
  254. this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
  255. if (conversationUser?.userId == "?") {
  256. credentials = null
  257. } else {
  258. credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
  259. }
  260. if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
  261. this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)
  262. }
  263. this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false)
  264. }
  265. private fun getRoomInfo() {
  266. val shouldRepeat = CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "webinary-lobby")
  267. if (shouldRepeat) {
  268. checkingLobbyStatus = true
  269. }
  270. if (conversationUser != null) {
  271. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  272. ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, roomToken))
  273. ?.subscribeOn(Schedulers.io())
  274. ?.observeOn(AndroidSchedulers.mainThread())
  275. ?.subscribe(object : Observer<RoomOverall> {
  276. override fun onSubscribe(d: Disposable) {
  277. disposableList.add(d)
  278. }
  279. @Suppress("Detekt.TooGenericExceptionCaught")
  280. override fun onNext(roomOverall: RoomOverall) {
  281. currentConversation = roomOverall.ocs.data
  282. loadAvatarForStatusBar()
  283. setTitle()
  284. try {
  285. setupMentionAutocomplete()
  286. checkReadOnlyState()
  287. checkLobbyState()
  288. if (!inConversation) {
  289. joinRoomWithPassword()
  290. }
  291. } catch (npe: NullPointerException) {
  292. // view binding can be null
  293. // since this is called asynchrously and UI might have been destroyed in the meantime
  294. Log.i(TAG, "UI destroyed - view binding already gone")
  295. }
  296. }
  297. override fun onError(e: Throwable) {
  298. }
  299. override fun onComplete() {
  300. if (shouldRepeat) {
  301. if (lobbyTimerHandler == null) {
  302. lobbyTimerHandler = Handler()
  303. }
  304. lobbyTimerHandler?.postDelayed({ getRoomInfo() }, LOBBY_TIMER_DELAY)
  305. }
  306. }
  307. })
  308. }
  309. }
  310. private fun handleFromNotification() {
  311. var apiVersion = 1
  312. // FIXME Can this be called for guests?
  313. if (conversationUser != null) {
  314. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  315. }
  316. ncApi?.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl))
  317. ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
  318. ?.subscribe(object : Observer<RoomsOverall> {
  319. override fun onSubscribe(d: Disposable) {
  320. disposableList.add(d)
  321. }
  322. override fun onNext(roomsOverall: RoomsOverall) {
  323. for (conversation in roomsOverall.ocs.data) {
  324. if (roomId == conversation.roomId) {
  325. roomToken = conversation.token
  326. currentConversation = conversation
  327. setTitle()
  328. getRoomInfo()
  329. break
  330. }
  331. }
  332. }
  333. override fun onError(e: Throwable) {
  334. }
  335. override fun onComplete() {
  336. }
  337. })
  338. }
  339. private fun loadAvatarForStatusBar() {
  340. if (inOneToOneCall() && activity != null && conversationVoiceCallMenuItem != null) {
  341. val avatarSize = DisplayUtils.convertDpToPixel(
  342. conversationVoiceCallMenuItem?.icon!!
  343. .intrinsicWidth.toFloat(),
  344. activity
  345. ).toInt()
  346. val imageRequest = DisplayUtils.getImageRequestForUrl(
  347. ApiUtils.getUrlForAvatarWithNameAndPixels(
  348. conversationUser?.baseUrl,
  349. currentConversation?.name, avatarSize / 2
  350. ),
  351. conversationUser!!
  352. )
  353. val imagePipeline = Fresco.getImagePipeline()
  354. val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
  355. dataSource.subscribe(
  356. object : BaseBitmapDataSubscriber() {
  357. override fun onNewResultImpl(bitmap: Bitmap?) {
  358. if (actionBar != null && bitmap != null && resources != null) {
  359. val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap)
  360. roundedBitmapDrawable.isCircular = true
  361. roundedBitmapDrawable.setAntiAlias(true)
  362. actionBar?.setIcon(roundedBitmapDrawable)
  363. }
  364. }
  365. override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {}
  366. },
  367. UiThreadImmediateExecutorService.getInstance()
  368. )
  369. }
  370. }
  371. private fun inOneToOneCall() = currentConversation != null && currentConversation?.type != null &&
  372. currentConversation?.type == Conversation.ConversationType
  373. .ROOM_TYPE_ONE_TO_ONE_CALL
  374. override fun onViewBound(view: View) {
  375. actionBar?.show()
  376. var adapterWasNull = false
  377. if (adapter == null) {
  378. binding.progressBar.visibility = View.VISIBLE
  379. adapterWasNull = true
  380. val messageHolders = MessageHolders()
  381. messageHolders.setIncomingTextConfig(
  382. MagicIncomingTextMessageViewHolder::class.java,
  383. R.layout.item_custom_incoming_text_message
  384. )
  385. messageHolders.setOutcomingTextConfig(
  386. MagicOutcomingTextMessageViewHolder::class.java,
  387. R.layout.item_custom_outcoming_text_message
  388. )
  389. messageHolders.setIncomingImageConfig(
  390. IncomingPreviewMessageViewHolder::class.java,
  391. R.layout.item_custom_incoming_preview_message
  392. )
  393. messageHolders.setOutcomingImageConfig(
  394. OutcomingPreviewMessageViewHolder::class.java,
  395. R.layout.item_custom_outcoming_preview_message
  396. )
  397. messageHolders.registerContentType(
  398. CONTENT_TYPE_SYSTEM_MESSAGE,
  399. MagicSystemMessageViewHolder::class.java,
  400. R.layout.item_system_message,
  401. MagicSystemMessageViewHolder::class.java,
  402. R.layout.item_system_message,
  403. this
  404. )
  405. messageHolders.registerContentType(
  406. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  407. MagicUnreadNoticeMessageViewHolder::class.java,
  408. R.layout.item_date_header,
  409. MagicUnreadNoticeMessageViewHolder::class.java,
  410. R.layout.item_date_header, this
  411. )
  412. messageHolders.registerContentType(
  413. CONTENT_TYPE_LOCATION,
  414. IncomingLocationMessageViewHolder::class.java,
  415. R.layout.item_custom_incoming_location_message,
  416. OutcomingLocationMessageViewHolder::class.java,
  417. R.layout.item_custom_outcoming_location_message,
  418. this
  419. )
  420. messageHolders.registerContentType(
  421. CONTENT_TYPE_VOICE_MESSAGE,
  422. IncomingVoiceMessageViewHolder::class.java,
  423. R.layout.item_custom_incoming_voice_message,
  424. OutcomingVoiceMessageViewHolder::class.java,
  425. R.layout.item_custom_outcoming_voice_message,
  426. this
  427. )
  428. var senderId = ""
  429. if (!conversationUser?.userId.equals("?")) {
  430. senderId = "users/" + conversationUser?.userId
  431. } else {
  432. senderId = currentConversation?.getActorType() + "/" + currentConversation?.getActorId()
  433. }
  434. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: " + senderId)
  435. adapter = TalkMessagesListAdapter(
  436. senderId,
  437. messageHolders,
  438. ImageLoader { imageView, url, payload ->
  439. val draweeController = Fresco.newDraweeControllerBuilder()
  440. .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser))
  441. .setControllerListener(DisplayUtils.getImageControllerListener(imageView))
  442. .setOldController(imageView.controller)
  443. .setAutoPlayAnimations(true)
  444. .build()
  445. imageView.controller = draweeController
  446. },
  447. this
  448. )
  449. } else {
  450. binding.messagesListView.visibility = View.VISIBLE
  451. }
  452. binding.messagesListView.setAdapter(adapter)
  453. adapter?.setLoadMoreListener(this)
  454. adapter?.setDateHeadersFormatter { format(it) }
  455. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  456. adapter?.registerViewClickListener(
  457. R.id.playPauseBtn
  458. ) { view, message ->
  459. val filename = message.getSelectedIndividualHashMap()["name"]
  460. val file = File(context!!.cacheDir, filename!!)
  461. if (file.exists()) {
  462. if (message.isPlayingVoiceMessage) {
  463. pausePlayback(message)
  464. } else {
  465. startPlayback(message)
  466. }
  467. } else {
  468. downloadFileToCache(message)
  469. }
  470. }
  471. if (context != null) {
  472. val messageSwipeController = MessageSwipeCallback(
  473. activity!!,
  474. object : MessageSwipeActions {
  475. override fun showReplyUI(position: Int) {
  476. val chatMessage = adapter?.items?.get(position)?.item as ChatMessage?
  477. replyToMessage(chatMessage, chatMessage?.jsonMessageId)
  478. }
  479. }
  480. )
  481. val itemTouchHelper = ItemTouchHelper(messageSwipeController)
  482. itemTouchHelper.attachToRecyclerView(binding.messagesListView)
  483. }
  484. layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
  485. binding.popupBubbleView.setRecyclerView(binding.messagesListView)
  486. binding.popupBubbleView.setPopupBubbleListener { context ->
  487. if (newMessagesCount != 0) {
  488. val scrollPosition: Int
  489. if (newMessagesCount - 1 < 0) {
  490. scrollPosition = 0
  491. } else {
  492. scrollPosition = newMessagesCount - 1
  493. }
  494. Handler().postDelayed(
  495. {
  496. binding.messagesListView.smoothScrollToPosition(scrollPosition)
  497. },
  498. NEW_MESSAGES_POPUP_BUBBLE_DELAY
  499. )
  500. }
  501. }
  502. if (args.containsKey("showToggleChat") && args.getBoolean("showToggleChat")) {
  503. binding.callControlToggleChat.visibility = View.VISIBLE
  504. wasDetached = true
  505. }
  506. binding.callControlToggleChat.setOnClickListener {
  507. (activity as MagicCallActivity).showCall()
  508. }
  509. binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  510. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  511. super.onScrollStateChanged(recyclerView, newState)
  512. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  513. if (newMessagesCount != 0 && layoutManager != null) {
  514. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
  515. newMessagesCount = 0
  516. if (binding.popupBubbleView.isShown == true) {
  517. binding.popupBubbleView.hide()
  518. }
  519. }
  520. }
  521. }
  522. }
  523. })
  524. val filters = arrayOfNulls<InputFilter>(1)
  525. val lengthFilter = CapabilitiesUtil.getMessageMaxLength(conversationUser) ?: MESSAGE_MAX_LENGTH
  526. filters[0] = InputFilter.LengthFilter(lengthFilter)
  527. binding.messageInputView.inputEditText?.filters = filters
  528. binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
  529. override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
  530. }
  531. override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
  532. if (s.length >= lengthFilter) {
  533. binding.messageInputView.inputEditText?.error = String.format(
  534. Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
  535. Integer.toString(lengthFilter)
  536. )
  537. } else {
  538. binding.messageInputView.inputEditText?.error = null
  539. }
  540. val editable = binding.messageInputView.inputEditText?.editableText
  541. if (editable != null && binding.messageInputView.inputEditText != null) {
  542. val mentionSpans = editable.getSpans(
  543. 0, binding.messageInputView.inputEditText!!.length(),
  544. Spans.MentionChipSpan::class.java
  545. )
  546. var mentionSpan: Spans.MentionChipSpan
  547. for (i in mentionSpans.indices) {
  548. mentionSpan = mentionSpans[i]
  549. if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd(mentionSpan)) {
  550. if (editable.subSequence(
  551. editable.getSpanStart(mentionSpan),
  552. editable.getSpanEnd(mentionSpan)
  553. ).toString().trim { it <= ' ' } != mentionSpan.label
  554. ) {
  555. editable.removeSpan(mentionSpan)
  556. }
  557. }
  558. }
  559. }
  560. }
  561. override fun afterTextChanged(s: Editable) {
  562. }
  563. })
  564. showMicrophoneButton(true)
  565. binding.messageInputView.messageInput.doAfterTextChanged {
  566. if (binding.messageInputView.messageInput.text.isEmpty()) {
  567. showMicrophoneButton(true)
  568. } else {
  569. showMicrophoneButton(false)
  570. }
  571. }
  572. var sliderInitX = 0F
  573. var downX = 0f
  574. var deltaX = 0f
  575. var voiceRecordStartTime = 0L
  576. var voiceRecordEndTime = 0L
  577. binding.messageInputView.recordAudioButton.setOnTouchListener(object : View.OnTouchListener {
  578. override fun onTouch(v: View?, event: MotionEvent?): Boolean {
  579. view.performClick()
  580. when (event?.action) {
  581. MotionEvent.ACTION_DOWN -> {
  582. if (!isRecordAudioPermissionGranted()) {
  583. requestRecordAudioPermissions()
  584. return true
  585. }
  586. if (!UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
  587. UploadAndShareFilesWorker.requestStoragePermission(this@ChatController)
  588. return true
  589. }
  590. voiceRecordStartTime = System.currentTimeMillis()
  591. setVoiceRecordFileName()
  592. startAudioRecording(currentVoiceRecordFile)
  593. downX = event.x
  594. showRecordAudioUi(true)
  595. }
  596. MotionEvent.ACTION_CANCEL -> {
  597. Log.d(TAG, "ACTION_CANCEL. same as for UP")
  598. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  599. return true
  600. }
  601. stopAndDiscardAudioRecording()
  602. showRecordAudioUi(false)
  603. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  604. }
  605. MotionEvent.ACTION_UP -> {
  606. Log.d(TAG, "ACTION_UP. stop recording??")
  607. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  608. return true
  609. }
  610. showRecordAudioUi(false)
  611. voiceRecordEndTime = System.currentTimeMillis()
  612. val voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime
  613. if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) {
  614. Log.d(TAG, "voiceRecordDuration: " + voiceRecordDuration)
  615. Toast.makeText(
  616. context,
  617. context!!.getString(R.string.nc_voice_message_hold_to_record_info),
  618. Toast.LENGTH_SHORT
  619. ).show()
  620. stopAndDiscardAudioRecording()
  621. return true
  622. } else {
  623. voiceRecordStartTime = 0L
  624. voiceRecordEndTime = 0L
  625. stopAndSendAudioRecording()
  626. }
  627. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  628. }
  629. MotionEvent.ACTION_MOVE -> {
  630. Log.d(TAG, "ACTION_MOVE.")
  631. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  632. return true
  633. }
  634. showRecordAudioUi(true)
  635. if (sliderInitX == 0.0F) {
  636. sliderInitX = binding.messageInputView.slideToCancelDescription.x
  637. }
  638. val movedX: Float = event.x
  639. deltaX = movedX - downX
  640. // only allow slide to left
  641. if (binding.messageInputView.slideToCancelDescription.x > sliderInitX) {
  642. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  643. }
  644. if (binding.messageInputView.slideToCancelDescription.x < VOICE_RECORD_CANCEL_SLIDER_X) {
  645. Log.d(TAG, "stopping recording because slider was moved to left")
  646. stopAndDiscardAudioRecording()
  647. showRecordAudioUi(false)
  648. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  649. return true
  650. } else {
  651. binding.messageInputView.slideToCancelDescription.x = binding.messageInputView
  652. .slideToCancelDescription.x + deltaX
  653. downX = movedX
  654. }
  655. }
  656. }
  657. return v?.onTouchEvent(event) ?: true
  658. }
  659. })
  660. binding.messageInputView.inputEditText?.setText(sharedText)
  661. binding.messageInputView.setAttachmentsListener {
  662. activity?.let { AttachmentDialog(it, this).show() }
  663. }
  664. binding.messageInputView.button.setOnClickListener { v -> submitMessage() }
  665. binding.messageInputView.button.contentDescription = resources?.getString(
  666. R.string
  667. .nc_description_send_message_button
  668. )
  669. if (currentConversation != null && currentConversation?.roomId != null) {
  670. loadAvatarForStatusBar()
  671. setTitle()
  672. }
  673. if (adapterWasNull) {
  674. // we're starting
  675. if (TextUtils.isEmpty(roomToken)) {
  676. handleFromNotification()
  677. } else {
  678. getRoomInfo()
  679. }
  680. }
  681. super.onViewBound(view)
  682. }
  683. private fun startPlayback(message: ChatMessage) {
  684. if (!this.isAttached) {
  685. // don't begin to play voice message if screen is not visible anymore.
  686. // this situation might happen if file is downloading but user already left the chatview.
  687. // If user returns to chatview, the old chatview instance is not attached anymore
  688. // and he has to click the play button again (which is considered to be okay)
  689. return
  690. }
  691. initMediaPlayer(message)
  692. if (!mediaPlayer!!.isPlaying) {
  693. mediaPlayer!!.start()
  694. }
  695. mediaPlayerHandler = Handler()
  696. activity?.runOnUiThread(object : Runnable {
  697. override fun run() {
  698. if (mediaPlayer != null) {
  699. val currentPosition: Int = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
  700. message.voiceMessagePlayedSeconds = currentPosition
  701. adapter?.update(message)
  702. }
  703. mediaPlayerHandler.postDelayed(this, SECOND)
  704. }
  705. })
  706. message.isDownloadingVoiceMessage = false
  707. message.isPlayingVoiceMessage = true
  708. adapter?.update(message)
  709. }
  710. private fun pausePlayback(message: ChatMessage) {
  711. if (mediaPlayer!!.isPlaying) {
  712. mediaPlayer!!.pause()
  713. }
  714. message.isPlayingVoiceMessage = false
  715. adapter?.update(message)
  716. }
  717. private fun initMediaPlayer(message: ChatMessage) {
  718. if (message != currentlyPlayedVoiceMessage) {
  719. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  720. }
  721. if (mediaPlayer == null) {
  722. val fileName = message.getSelectedIndividualHashMap()["name"]
  723. val absolutePath = context!!.cacheDir.absolutePath + "/" + fileName
  724. mediaPlayer = MediaPlayer().apply {
  725. setDataSource(absolutePath)
  726. prepare()
  727. }
  728. currentlyPlayedVoiceMessage = message
  729. message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
  730. mediaPlayer!!.setOnCompletionListener {
  731. stopMediaPlayer(message)
  732. }
  733. } else {
  734. Log.e(TAG, "mediaPlayer was not null. This should not happen!")
  735. }
  736. }
  737. private fun stopMediaPlayer(message: ChatMessage) {
  738. message.isPlayingVoiceMessage = false
  739. message.resetVoiceMessage = true
  740. adapter?.update(message)
  741. currentlyPlayedVoiceMessage = null
  742. mediaPlayerHandler.removeCallbacksAndMessages(null)
  743. mediaPlayer?.stop()
  744. mediaPlayer?.release()
  745. mediaPlayer = null
  746. }
  747. override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
  748. if (mediaPlayer != null) {
  749. if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
  750. mediaPlayer!!.seekTo(progress * VOICE_MESSAGE_SEEKBAR_BASE)
  751. }
  752. }
  753. }
  754. @SuppressLint("LongLogTag")
  755. private fun downloadFileToCache(message: ChatMessage) {
  756. message.isDownloadingVoiceMessage = true
  757. adapter?.update(message)
  758. val baseUrl = message.activeUser.baseUrl
  759. val userId = message.activeUser.userId
  760. val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser)
  761. val fileName = message.getSelectedIndividualHashMap()["name"]
  762. var size = message.getSelectedIndividualHashMap()["size"]
  763. if (size == null) {
  764. size = "-1"
  765. }
  766. val fileSize = Integer.valueOf(size)
  767. val fileId = message.getSelectedIndividualHashMap()["id"]
  768. val path = message.getSelectedIndividualHashMap()["path"]
  769. // check if download worker is already running
  770. val workers = WorkManager.getInstance(
  771. context!!
  772. ).getWorkInfosByTag(fileId!!)
  773. try {
  774. for (workInfo in workers.get()) {
  775. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  776. Log.d(TAG, "Download worker for " + fileId + " is already running or scheduled")
  777. return
  778. }
  779. }
  780. } catch (e: ExecutionException) {
  781. Log.e(TAG, "Error when checking if worker already exists", e)
  782. } catch (e: InterruptedException) {
  783. Log.e(TAG, "Error when checking if worker already exists", e)
  784. }
  785. val data: Data = Data.Builder()
  786. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
  787. .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
  788. .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
  789. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
  790. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  791. .putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
  792. .build()
  793. val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  794. .setInputData(data)
  795. .addTag(fileId)
  796. .build()
  797. WorkManager.getInstance().enqueue(downloadWorker)
  798. WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(downloadWorker.id)
  799. .observeForever { workInfo: WorkInfo ->
  800. if (workInfo.state == WorkInfo.State.SUCCEEDED) {
  801. startPlayback(message)
  802. }
  803. }
  804. }
  805. @SuppressLint("SimpleDateFormat")
  806. private fun setVoiceRecordFileName() {
  807. val pattern = "yyyy-MM-dd HH-mm-ss"
  808. val simpleDateFormat = SimpleDateFormat(pattern)
  809. val date: String = simpleDateFormat.format(Date())
  810. val fileNameWithoutSuffix = String.format(
  811. context!!.resources.getString(R.string.nc_voice_message_filename),
  812. date, currentConversation!!.displayName
  813. )
  814. val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX
  815. currentVoiceRecordFile = "${context!!.cacheDir.absolutePath}/$fileName"
  816. }
  817. private fun showRecordAudioUi(show: Boolean) {
  818. if (show) {
  819. binding.messageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
  820. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
  821. binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
  822. binding.messageInputView.slideToCancelDescription.visibility = View.VISIBLE
  823. binding.messageInputView.attachmentButton.visibility = View.GONE
  824. binding.messageInputView.smileyButton.visibility = View.GONE
  825. binding.messageInputView.messageInput.visibility = View.GONE
  826. binding.messageInputView.messageInput.hint = ""
  827. } else {
  828. binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
  829. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
  830. binding.messageInputView.audioRecordDuration.visibility = View.GONE
  831. binding.messageInputView.slideToCancelDescription.visibility = View.GONE
  832. binding.messageInputView.attachmentButton.visibility = View.VISIBLE
  833. binding.messageInputView.smileyButton.visibility = View.VISIBLE
  834. binding.messageInputView.messageInput.visibility = View.VISIBLE
  835. binding.messageInputView.messageInput.hint =
  836. context?.resources?.getString(R.string.nc_hint_enter_a_message)
  837. }
  838. }
  839. private fun isRecordAudioPermissionGranted(): Boolean {
  840. return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  841. return PermissionChecker.checkSelfPermission(
  842. context!!,
  843. Manifest.permission.RECORD_AUDIO
  844. ) == PermissionChecker.PERMISSION_GRANTED
  845. } else {
  846. true
  847. }
  848. }
  849. private fun startAudioRecording(file: String) {
  850. binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
  851. binding.messageInputView.audioRecordDuration.start()
  852. val animation: Animation = AlphaAnimation(1.0f, 0.0f)
  853. animation.duration = 750
  854. animation.interpolator = LinearInterpolator()
  855. animation.repeatCount = Animation.INFINITE
  856. animation.repeatMode = Animation.REVERSE
  857. binding.messageInputView.microphoneEnabledInfo.startAnimation(animation)
  858. recorder = MediaRecorder().apply {
  859. setAudioSource(MediaRecorder.AudioSource.MIC)
  860. setOutputFile(file)
  861. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
  862. setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
  863. try {
  864. prepare()
  865. } catch (e: IOException) {
  866. Log.e(TAG, "prepare for audio recording failed")
  867. }
  868. try {
  869. start()
  870. isVoiceRecordingInProgress = true
  871. } catch (e: IllegalStateException) {
  872. Log.e(TAG, "start for audio recording failed")
  873. }
  874. vibrate()
  875. }
  876. }
  877. private fun stopAndSendAudioRecording() {
  878. stopAudioRecording()
  879. val uri = Uri.fromFile(File(currentVoiceRecordFile))
  880. uploadFiles(mutableListOf(uri.toString()), true)
  881. }
  882. private fun stopAndDiscardAudioRecording() {
  883. stopAudioRecording()
  884. val cachedFile = File(currentVoiceRecordFile)
  885. cachedFile.delete()
  886. }
  887. @Suppress("Detekt.TooGenericExceptionCaught")
  888. private fun stopAudioRecording() {
  889. binding.messageInputView.audioRecordDuration.stop()
  890. binding.messageInputView.microphoneEnabledInfo.clearAnimation()
  891. if (isVoiceRecordingInProgress) {
  892. recorder?.apply {
  893. try {
  894. stop()
  895. release()
  896. isVoiceRecordingInProgress = false
  897. Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false")
  898. } catch (e: RuntimeException) {
  899. Log.w(TAG, "error while stopping recorder!")
  900. }
  901. vibrate()
  902. }
  903. recorder = null
  904. } else {
  905. Log.e(TAG, "tried to stop audio recorder but it was not recording")
  906. }
  907. }
  908. fun vibrate() {
  909. val vibrator = context?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
  910. if (Build.VERSION.SDK_INT >= O) {
  911. vibrator.vibrate(VibrationEffect.createOneShot(SHORT_VIBRATE, VibrationEffect.DEFAULT_AMPLITUDE))
  912. } else {
  913. vibrator.vibrate(SHORT_VIBRATE)
  914. }
  915. }
  916. private fun requestRecordAudioPermissions() {
  917. requestPermissions(
  918. arrayOf(
  919. Manifest.permission.RECORD_AUDIO
  920. ),
  921. REQUEST_RECORD_AUDIO_PERMISSION
  922. )
  923. }
  924. private fun checkReadOnlyState() {
  925. if (currentConversation != null && isAlive()) {
  926. if (currentConversation?.shouldShowLobby(conversationUser) ?: false ||
  927. currentConversation?.conversationReadOnlyState != null &&
  928. currentConversation?.conversationReadOnlyState ==
  929. Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  930. ) {
  931. conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  932. conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  933. binding.messageInputView.visibility = View.GONE
  934. } else {
  935. if (conversationVoiceCallMenuItem != null) {
  936. conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  937. }
  938. if (conversationVideoMenuItem != null) {
  939. conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  940. }
  941. if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)
  942. ) {
  943. binding.messageInputView.visibility = View.GONE
  944. } else {
  945. binding.messageInputView.visibility = View.VISIBLE
  946. }
  947. }
  948. }
  949. }
  950. private fun checkLobbyState() {
  951. if (currentConversation != null &&
  952. currentConversation?.isLobbyViewApplicable(conversationUser) ?: false &&
  953. isAlive()
  954. ) {
  955. if (!checkingLobbyStatus) {
  956. getRoomInfo()
  957. }
  958. if (currentConversation?.shouldShowLobby(conversationUser) ?: false) {
  959. binding.lobby.lobbyView.visibility = View.VISIBLE
  960. binding.messagesListView.visibility = View.GONE
  961. binding.messageInputView.visibility = View.GONE
  962. binding.progressBar.visibility = View.GONE
  963. if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer !=
  964. 0L
  965. ) {
  966. binding.lobby.lobbyTextView.text = String.format(
  967. resources!!.getString(R.string.nc_lobby_waiting_with_date),
  968. DateUtils.getLocalDateStringFromTimestampForLobby(
  969. currentConversation?.lobbyTimer
  970. ?: 0
  971. )
  972. )
  973. } else {
  974. binding.lobby.lobbyTextView.setText(R.string.nc_lobby_waiting)
  975. }
  976. } else {
  977. binding.lobby.lobbyView.visibility = View.GONE
  978. binding.messagesListView.visibility = View.VISIBLE
  979. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  980. if (isFirstMessagesProcessing && pastPreconditionFailed) {
  981. pastPreconditionFailed = false
  982. pullChatMessages(0)
  983. } else if (futurePreconditionFailed) {
  984. futurePreconditionFailed = false
  985. pullChatMessages(1)
  986. }
  987. }
  988. } else {
  989. binding.lobby.lobbyView.visibility = View.GONE
  990. binding.messagesListView.visibility = View.VISIBLE
  991. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  992. }
  993. }
  994. override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
  995. if (requestCode == REQUEST_CODE_CHOOSE_FILE) {
  996. if (resultCode == RESULT_OK) {
  997. try {
  998. checkNotNull(intent)
  999. filesToUpload.clear()
  1000. intent.clipData?.let {
  1001. for (index in 0 until it.itemCount) {
  1002. filesToUpload.add(it.getItemAt(index).uri.toString())
  1003. }
  1004. } ?: run {
  1005. checkNotNull(intent.data)
  1006. intent.data.let {
  1007. filesToUpload.add(intent.data.toString())
  1008. }
  1009. }
  1010. require(filesToUpload.isNotEmpty())
  1011. val filenamesWithLinebreaks = StringBuilder("\n")
  1012. for (file in filesToUpload) {
  1013. val filename = UriUtils.getFileName(Uri.parse(file), context)
  1014. filenamesWithLinebreaks.append(filename).append("\n")
  1015. }
  1016. val confirmationQuestion = when (filesToUpload.size) {
  1017. 1 -> context?.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
  1018. String.format(it, title)
  1019. }
  1020. else -> context?.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
  1021. String.format(it, title)
  1022. }
  1023. }
  1024. LovelyStandardDialog(activity)
  1025. .setPositiveButtonColorRes(R.color.nc_darkGreen)
  1026. .setTitle(confirmationQuestion)
  1027. .setMessage(filenamesWithLinebreaks.toString())
  1028. .setPositiveButton(R.string.nc_yes) { v ->
  1029. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
  1030. uploadFiles(filesToUpload, false)
  1031. } else {
  1032. UploadAndShareFilesWorker.requestStoragePermission(this)
  1033. }
  1034. }
  1035. .setNegativeButton(R.string.nc_no) {}
  1036. .show()
  1037. } catch (e: IllegalStateException) {
  1038. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1039. .show()
  1040. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1041. } catch (e: IllegalArgumentException) {
  1042. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1043. .show()
  1044. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1045. }
  1046. }
  1047. }
  1048. }
  1049. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  1050. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
  1051. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1052. Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
  1053. if (filesToUpload.isNotEmpty()) {
  1054. uploadFiles(filesToUpload, false)
  1055. }
  1056. } else {
  1057. Toast
  1058. .makeText(context, context?.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG)
  1059. .show()
  1060. }
  1061. } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  1062. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1063. // do nothing. user will tap on the microphone again if he wants to record audio..
  1064. } else {
  1065. Toast.makeText(
  1066. context,
  1067. context!!.getString(R.string.nc_voice_message_missing_audio_permission),
  1068. Toast.LENGTH_LONG
  1069. ).show()
  1070. }
  1071. }
  1072. }
  1073. private fun uploadFiles(files: MutableList<String>, isVoiceMessage: Boolean) {
  1074. var metaData = ""
  1075. if (isVoiceMessage) {
  1076. metaData = VOICE_MESSAGE_META_DATA
  1077. }
  1078. try {
  1079. require(files.isNotEmpty())
  1080. val data: Data = Data.Builder()
  1081. .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray())
  1082. .putString(
  1083. UploadAndShareFilesWorker.NC_TARGETPATH,
  1084. CapabilitiesUtil.getAttachmentFolder(conversationUser)
  1085. )
  1086. .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken)
  1087. .putString(UploadAndShareFilesWorker.META_DATA, metaData)
  1088. .build()
  1089. val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
  1090. .setInputData(data)
  1091. .build()
  1092. WorkManager.getInstance().enqueue(uploadWorker)
  1093. if (!isVoiceMessage) {
  1094. Toast.makeText(
  1095. context, context?.getString(R.string.nc_upload_in_progess),
  1096. Toast.LENGTH_LONG
  1097. ).show()
  1098. }
  1099. } catch (e: IllegalArgumentException) {
  1100. Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
  1101. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1102. }
  1103. }
  1104. fun sendSelectLocalFileIntent() {
  1105. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  1106. type = "*/*"
  1107. addCategory(Intent.CATEGORY_OPENABLE)
  1108. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  1109. }
  1110. startActivityForResult(
  1111. Intent.createChooser(
  1112. action,
  1113. context?.resources?.getString(
  1114. R.string.nc_upload_choose_local_files
  1115. )
  1116. ),
  1117. REQUEST_CODE_CHOOSE_FILE
  1118. )
  1119. }
  1120. fun showBrowserScreen(browserType: BrowserController.BrowserType) {
  1121. val bundle = Bundle()
  1122. bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap<BrowserController.BrowserType>(browserType))
  1123. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(conversationUser))
  1124. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  1125. router.pushController(
  1126. RouterTransaction.with(BrowserForSharingController(bundle))
  1127. .pushChangeHandler(VerticalChangeHandler())
  1128. .popChangeHandler(VerticalChangeHandler())
  1129. )
  1130. }
  1131. fun showShareLocationScreen() {
  1132. Log.d(TAG, "showShareLocationScreen")
  1133. val bundle = Bundle()
  1134. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  1135. router.pushController(
  1136. RouterTransaction.with(LocationPickerController(bundle))
  1137. .pushChangeHandler(HorizontalChangeHandler())
  1138. .popChangeHandler(HorizontalChangeHandler())
  1139. )
  1140. }
  1141. private fun showConversationInfoScreen() {
  1142. val bundle = Bundle()
  1143. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
  1144. bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
  1145. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall())
  1146. router.pushController(
  1147. RouterTransaction.with(ConversationInfoController(bundle))
  1148. .pushChangeHandler(HorizontalChangeHandler())
  1149. .popChangeHandler(HorizontalChangeHandler())
  1150. )
  1151. }
  1152. private fun setupMentionAutocomplete() {
  1153. if (isAlive()) {
  1154. val elevation = 6f
  1155. resources?.let {
  1156. val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
  1157. val presenter = MentionAutocompletePresenter(activity, roomToken)
  1158. val callback = MentionAutocompleteCallback(
  1159. activity,
  1160. conversationUser,
  1161. binding.messageInputView.inputEditText
  1162. )
  1163. if (mentionAutocomplete == null && binding.messageInputView.inputEditText != null) {
  1164. mentionAutocomplete = Autocomplete.on<Mention>(binding.messageInputView.inputEditText)
  1165. .with(elevation)
  1166. .with(backgroundDrawable)
  1167. .with(MagicCharPolicy('@'))
  1168. .with(presenter)
  1169. .with(callback)
  1170. .build()
  1171. }
  1172. }
  1173. }
  1174. }
  1175. override fun onAttach(view: View) {
  1176. super.onAttach(view)
  1177. eventBus?.register(this)
  1178. if (conversationUser?.userId != "?" &&
  1179. CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "mention-flag") ?: false &&
  1180. activity != null
  1181. ) {
  1182. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener { v -> showConversationInfoScreen() }
  1183. }
  1184. isLeavingForConversation = false
  1185. ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  1186. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomId
  1187. ApplicationWideCurrentRoomHolder.getInstance().isInCall = false
  1188. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  1189. isLinkPreviewAllowed = appPreferences?.areLinkPreviewsAllowed ?: false
  1190. val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
  1191. emojiPopup = binding.messageInputView.inputEditText?.let {
  1192. EmojiPopup.Builder.fromRootView(view).setOnEmojiPopupShownListener {
  1193. if (resources != null) {
  1194. smileyButton?.setImageDrawable(
  1195. ContextCompat.getDrawable(context!!, R.drawable.ic_baseline_keyboard_24)
  1196. )
  1197. }
  1198. }.setOnEmojiPopupDismissListener {
  1199. smileyButton?.setImageDrawable(
  1200. ContextCompat.getDrawable(context!!, R.drawable.ic_insert_emoticon_black_24dp)
  1201. )
  1202. }.setOnEmojiClickListener { emoji,
  1203. imageView ->
  1204. binding.messageInputView.inputEditText?.editableText?.append(" ")
  1205. }.build(it)
  1206. }
  1207. smileyButton?.setOnClickListener {
  1208. emojiPopup?.toggle()
  1209. }
  1210. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
  1211. cancelReply()
  1212. }
  1213. if (activity != null) {
  1214. KeyboardUtils(activity, getView(), false)
  1215. }
  1216. cancelNotificationsForCurrentConversation()
  1217. if (inConversation) {
  1218. if (wasDetached) {
  1219. currentConversation?.sessionId = "0"
  1220. wasDetached = false
  1221. joinRoomWithPassword()
  1222. }
  1223. }
  1224. }
  1225. private fun cancelReply() {
  1226. binding.messageInputView.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility = View.GONE
  1227. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
  1228. }
  1229. private fun cancelNotificationsForCurrentConversation() {
  1230. if (conversationUser != null) {
  1231. if (!TextUtils.isEmpty(roomToken)) {
  1232. NotificationUtils.cancelExistingNotificationsForRoom(
  1233. applicationContext,
  1234. conversationUser,
  1235. roomToken!!
  1236. )
  1237. }
  1238. }
  1239. }
  1240. override fun onDetach(view: View) {
  1241. super.onDetach(view)
  1242. if (!isLeavingForConversation) {
  1243. // current room is still "active", we need the info
  1244. ApplicationWideCurrentRoomHolder.getInstance().clear()
  1245. }
  1246. eventBus?.unregister(this)
  1247. if (activity != null) {
  1248. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1249. }
  1250. if (conversationUser != null &&
  1251. activity != null &&
  1252. !activity?.isChangingConfigurations!! &&
  1253. !isLeavingForConversation
  1254. ) {
  1255. wasDetached = true
  1256. leaveRoom()
  1257. }
  1258. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  1259. mentionAutocomplete?.dismissPopup()
  1260. }
  1261. }
  1262. override val title: String
  1263. get() =
  1264. if (currentConversation?.displayName != null) {
  1265. try {
  1266. " " + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  1267. } catch (e: IllegalStateException) {
  1268. " " + currentConversation?.displayName
  1269. }
  1270. } else {
  1271. ""
  1272. }
  1273. public override fun onDestroy() {
  1274. super.onDestroy()
  1275. if (activity != null) {
  1276. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1277. }
  1278. if (actionBar != null) {
  1279. actionBar?.setIcon(null)
  1280. }
  1281. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  1282. adapter = null
  1283. inConversation = false
  1284. }
  1285. private fun dispose() {
  1286. for (disposable in disposableList) {
  1287. if (!disposable.isDisposed()) {
  1288. disposable.dispose()
  1289. }
  1290. }
  1291. }
  1292. private fun joinRoomWithPassword() {
  1293. if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) ||
  1294. currentConversation?.sessionId == "0"
  1295. ) {
  1296. var apiVersion = 1
  1297. // FIXME Fix API checking with guests?
  1298. if (conversationUser != null) {
  1299. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1300. }
  1301. ncApi?.joinRoom(
  1302. credentials,
  1303. ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken),
  1304. roomPassword
  1305. )
  1306. ?.subscribeOn(Schedulers.io())
  1307. ?.observeOn(AndroidSchedulers.mainThread())
  1308. ?.retry(3)
  1309. ?.subscribe(object : Observer<RoomOverall> {
  1310. override fun onSubscribe(d: Disposable) {
  1311. disposableList.add(d)
  1312. }
  1313. @Suppress("Detekt.TooGenericExceptionCaught")
  1314. override fun onNext(roomOverall: RoomOverall) {
  1315. inConversation = true
  1316. currentConversation?.sessionId = roomOverall.ocs.data.sessionId
  1317. ApplicationWideCurrentRoomHolder.getInstance().session =
  1318. currentConversation?.sessionId
  1319. setupWebsocket()
  1320. try {
  1321. checkLobbyState()
  1322. } catch (npe: NullPointerException) {
  1323. // view binding can be null
  1324. // since this is called asynchrously and UI might have been destroyed in the meantime
  1325. Log.i(TAG, "UI destroyed - view binding already gone")
  1326. }
  1327. if (isFirstMessagesProcessing) {
  1328. pullChatMessages(0)
  1329. } else {
  1330. pullChatMessages(1, 0)
  1331. }
  1332. if (magicWebSocketInstance != null) {
  1333. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1334. roomToken,
  1335. currentConversation?.sessionId
  1336. )
  1337. }
  1338. if (startCallFromNotification != null && startCallFromNotification ?: false) {
  1339. startCallFromNotification = false
  1340. startACall(voiceOnly)
  1341. }
  1342. }
  1343. override fun onError(e: Throwable) {
  1344. }
  1345. override fun onComplete() {
  1346. }
  1347. })
  1348. } else {
  1349. inConversation = true
  1350. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId
  1351. if (magicWebSocketInstance != null) {
  1352. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1353. roomToken,
  1354. currentConversation?.sessionId
  1355. )
  1356. }
  1357. if (isFirstMessagesProcessing) {
  1358. pullChatMessages(0)
  1359. } else {
  1360. pullChatMessages(1)
  1361. }
  1362. }
  1363. }
  1364. private fun leaveRoom() {
  1365. var apiVersion = 1
  1366. // FIXME Fix API checking with guests?
  1367. if (conversationUser != null) {
  1368. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1369. }
  1370. ncApi?.leaveRoom(
  1371. credentials,
  1372. ApiUtils.getUrlForParticipantsActive(
  1373. apiVersion,
  1374. conversationUser?.baseUrl,
  1375. roomToken
  1376. )
  1377. )
  1378. ?.subscribeOn(Schedulers.io())
  1379. ?.observeOn(AndroidSchedulers.mainThread())
  1380. ?.subscribe(object : Observer<GenericOverall> {
  1381. override fun onSubscribe(d: Disposable) {
  1382. disposableList.add(d)
  1383. }
  1384. override fun onNext(genericOverall: GenericOverall) {
  1385. checkingLobbyStatus = false
  1386. if (lobbyTimerHandler != null) {
  1387. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  1388. }
  1389. if (magicWebSocketInstance != null && currentConversation != null) {
  1390. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1391. "",
  1392. currentConversation?.sessionId
  1393. )
  1394. }
  1395. if (!isDestroyed && !isBeingDestroyed && !wasDetached) {
  1396. router.popCurrentController()
  1397. }
  1398. }
  1399. override fun onError(e: Throwable) {}
  1400. override fun onComplete() {
  1401. dispose()
  1402. }
  1403. })
  1404. }
  1405. private fun submitMessage() {
  1406. if (binding.messageInputView.inputEditText != null) {
  1407. val editable = binding.messageInputView.inputEditText!!.editableText
  1408. val mentionSpans = editable.getSpans(
  1409. 0, editable.length,
  1410. Spans.MentionChipSpan::class.java
  1411. )
  1412. var mentionSpan: Spans.MentionChipSpan
  1413. for (i in mentionSpans.indices) {
  1414. mentionSpan = mentionSpans[i]
  1415. var mentionId = mentionSpan.id
  1416. if (mentionId.contains(" ") || mentionId.startsWith("guest/")) {
  1417. mentionId = "\"" + mentionId + "\""
  1418. }
  1419. editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
  1420. }
  1421. binding.messageInputView.inputEditText?.setText("")
  1422. val replyMessageId: Int? = view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
  1423. sendMessage(
  1424. editable,
  1425. if (
  1426. view
  1427. ?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  1428. ?.visibility == View.VISIBLE
  1429. ) replyMessageId else null
  1430. )
  1431. cancelReply()
  1432. }
  1433. }
  1434. private fun sendMessage(message: CharSequence, replyTo: Int?) {
  1435. if (conversationUser != null) {
  1436. val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1437. ncApi!!.sendChatMessage(
  1438. credentials,
  1439. ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, roomToken),
  1440. message,
  1441. conversationUser.displayName,
  1442. replyTo
  1443. )
  1444. ?.subscribeOn(Schedulers.io())
  1445. ?.observeOn(AndroidSchedulers.mainThread())
  1446. ?.subscribe(object : Observer<GenericOverall> {
  1447. override fun onSubscribe(d: Disposable) {
  1448. // unused atm
  1449. }
  1450. @Suppress("Detekt.TooGenericExceptionCaught")
  1451. override fun onNext(genericOverall: GenericOverall) {
  1452. myFirstMessage = message
  1453. try {
  1454. if (binding.popupBubbleView.isShown == true) {
  1455. binding.popupBubbleView.hide()
  1456. }
  1457. binding.messagesListView.smoothScrollToPosition(0)
  1458. } catch (npe: NullPointerException) {
  1459. // view binding can be null
  1460. // since this is called asynchrously and UI might have been destroyed in the meantime
  1461. Log.i(TAG, "UI destroyed - view binding already gone")
  1462. }
  1463. }
  1464. override fun onError(e: Throwable) {
  1465. if (e is HttpException) {
  1466. val code = e.code()
  1467. if (Integer.toString(code).startsWith("2")) {
  1468. myFirstMessage = message
  1469. if (binding.popupBubbleView.isShown == true) {
  1470. binding.popupBubbleView.hide()
  1471. }
  1472. binding.messagesListView.smoothScrollToPosition(0)
  1473. }
  1474. }
  1475. }
  1476. override fun onComplete() {
  1477. // unused atm
  1478. }
  1479. })
  1480. }
  1481. showMicrophoneButton(true)
  1482. }
  1483. private fun setupWebsocket() {
  1484. if (conversationUser != null) {
  1485. if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) != null) {
  1486. magicWebSocketInstance =
  1487. WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id)
  1488. } else {
  1489. magicWebSocketInstance = null
  1490. }
  1491. }
  1492. }
  1493. fun pullChatMessages(lookIntoFuture: Int, setReadMarker: Int = 1, xChatLastCommonRead: Int? = null) {
  1494. if (!inConversation) {
  1495. return
  1496. }
  1497. if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)) {
  1498. // return
  1499. }
  1500. val fieldMap = HashMap<String, Int>()
  1501. fieldMap["includeLastKnown"] = 0
  1502. if (lookIntoFuture > 0) {
  1503. lookingIntoFuture = true
  1504. } else if (isFirstMessagesProcessing) {
  1505. if (currentConversation != null) {
  1506. globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage
  1507. globalLastKnownPastMessageId = currentConversation!!.lastReadMessage
  1508. fieldMap["includeLastKnown"] = 1
  1509. }
  1510. }
  1511. val timeout = if (lookingIntoFuture) {
  1512. 30
  1513. } else {
  1514. 0
  1515. }
  1516. fieldMap["timeout"] = timeout
  1517. fieldMap["lookIntoFuture"] = lookIntoFuture
  1518. fieldMap["limit"] = 100
  1519. fieldMap["setReadMarker"] = setReadMarker
  1520. val lastKnown: Int
  1521. if (lookIntoFuture > 0) {
  1522. lastKnown = globalLastKnownFutureMessageId
  1523. } else {
  1524. lastKnown = globalLastKnownPastMessageId
  1525. }
  1526. fieldMap["lastKnownMessageId"] = lastKnown
  1527. xChatLastCommonRead?.let {
  1528. fieldMap["lastCommonReadId"] = it
  1529. }
  1530. if (!wasDetached) {
  1531. var apiVersion = 1
  1532. // FIXME this is a best guess, guests would need to get the capabilities themselves
  1533. if (conversationUser != null) {
  1534. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1535. }
  1536. if (lookIntoFuture > 0) {
  1537. val finalTimeout = timeout
  1538. ncApi?.pullChatMessages(
  1539. credentials,
  1540. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
  1541. )
  1542. ?.subscribeOn(Schedulers.io())
  1543. ?.observeOn(AndroidSchedulers.mainThread())
  1544. ?.takeWhile { observable -> inConversation && !wasDetached }
  1545. ?.subscribe(object : Observer<Response<*>> {
  1546. override fun onSubscribe(d: Disposable) {
  1547. disposableList.add(d)
  1548. }
  1549. @Suppress("Detekt.TooGenericExceptionCaught")
  1550. override fun onNext(response: Response<*>) {
  1551. try {
  1552. if (response.code() == 304) {
  1553. pullChatMessages(1, setReadMarker, xChatLastCommonRead)
  1554. } else if (response.code() == 412) {
  1555. futurePreconditionFailed = true
  1556. } else {
  1557. processMessages(response, true, finalTimeout)
  1558. }
  1559. } catch (npe: NullPointerException) {
  1560. // view binding can be null
  1561. // since this is called asynchrously and UI might have been destroyed in the meantime
  1562. Log.i(TAG, "UI destroyed - view binding already gone")
  1563. }
  1564. }
  1565. override fun onError(e: Throwable) {
  1566. // unused atm
  1567. }
  1568. override fun onComplete() {
  1569. // unused atm
  1570. }
  1571. })
  1572. } else {
  1573. ncApi?.pullChatMessages(
  1574. credentials,
  1575. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
  1576. )
  1577. ?.subscribeOn(Schedulers.io())
  1578. ?.observeOn(AndroidSchedulers.mainThread())
  1579. ?.takeWhile { observable -> inConversation && !wasDetached }
  1580. ?.subscribe(object : Observer<Response<*>> {
  1581. override fun onSubscribe(d: Disposable) {
  1582. disposableList.add(d)
  1583. }
  1584. @Suppress("Detekt.TooGenericExceptionCaught")
  1585. override fun onNext(response: Response<*>) {
  1586. try {
  1587. if (response.code() == 412) {
  1588. pastPreconditionFailed = true
  1589. } else {
  1590. processMessages(response, false, 0)
  1591. }
  1592. } catch (npe: NullPointerException) {
  1593. // view binding can be null
  1594. // since this is called asynchrously and UI might have been destroyed in the meantime
  1595. Log.i(TAG, "UI destroyed - view binding already gone")
  1596. }
  1597. }
  1598. override fun onError(e: Throwable) {
  1599. // unused atm
  1600. }
  1601. override fun onComplete() {
  1602. // unused atm
  1603. }
  1604. })
  1605. }
  1606. }
  1607. }
  1608. private fun processMessages(response: Response<*>, isFromTheFuture: Boolean, timeout: Int) {
  1609. val xChatLastGivenHeader: String? = response.headers().get("X-Chat-Last-Given")
  1610. val xChatLastCommonRead = response.headers().get("X-Chat-Last-Common-Read")?.let {
  1611. Integer.parseInt(it)
  1612. }
  1613. if (response.headers().size > 0 && !TextUtils.isEmpty(xChatLastGivenHeader)) {
  1614. val header = Integer.parseInt(xChatLastGivenHeader!!)
  1615. if (header > 0) {
  1616. if (isFromTheFuture) {
  1617. globalLastKnownFutureMessageId = header
  1618. } else {
  1619. if (globalLastKnownFutureMessageId == -1) {
  1620. globalLastKnownFutureMessageId = header
  1621. }
  1622. globalLastKnownPastMessageId = header
  1623. }
  1624. }
  1625. }
  1626. if (response.code() == HTTP_CODE_OK) {
  1627. val chatOverall = response.body() as ChatOverall?
  1628. val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
  1629. if (isFirstMessagesProcessing) {
  1630. cancelNotificationsForCurrentConversation()
  1631. isFirstMessagesProcessing = false
  1632. binding.progressBar.visibility = View.GONE
  1633. binding.messagesListView.visibility = View.VISIBLE
  1634. }
  1635. var countGroupedMessages = 0
  1636. if (!isFromTheFuture) {
  1637. for (i in chatMessageList.indices) {
  1638. if (chatMessageList.size > i + 1) {
  1639. if (TextUtils.isEmpty(chatMessageList[i].systemMessage) &&
  1640. TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) &&
  1641. chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
  1642. countGroupedMessages < 4 &&
  1643. DateFormatter.isSameDay(chatMessageList[i].createdAt, chatMessageList[i + 1].createdAt)
  1644. ) {
  1645. chatMessageList[i].isGrouped = true
  1646. countGroupedMessages++
  1647. } else {
  1648. countGroupedMessages = 0
  1649. }
  1650. }
  1651. val chatMessage = chatMessageList[i]
  1652. chatMessage.isOneToOneConversation =
  1653. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1654. chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed
  1655. chatMessage.activeUser = conversationUser
  1656. }
  1657. if (adapter != null) {
  1658. adapter?.addToEnd(chatMessageList, false)
  1659. }
  1660. } else {
  1661. var chatMessage: ChatMessage
  1662. val shouldAddNewMessagesNotice = timeout == 0 && adapter?.itemCount ?: 0 > 0 && chatMessageList.size > 0
  1663. if (shouldAddNewMessagesNotice) {
  1664. val unreadChatMessage = ChatMessage()
  1665. unreadChatMessage.jsonMessageId = -1
  1666. unreadChatMessage.actorId = "-1"
  1667. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  1668. unreadChatMessage.message = context?.getString(R.string.nc_new_messages)
  1669. adapter?.addToStart(unreadChatMessage, false)
  1670. }
  1671. val isThereANewNotice =
  1672. shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1
  1673. for (i in chatMessageList.indices) {
  1674. chatMessage = chatMessageList[i]
  1675. chatMessage.activeUser = conversationUser
  1676. chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed
  1677. val shouldScroll =
  1678. !isThereANewNotice &&
  1679. !shouldAddNewMessagesNotice &&
  1680. layoutManager?.findFirstVisibleItemPosition() == 0 ||
  1681. adapter != null &&
  1682. adapter?.itemCount == 0
  1683. if (!shouldAddNewMessagesNotice && !shouldScroll) {
  1684. if (!binding.popupBubbleView.isShown) {
  1685. newMessagesCount = 1
  1686. binding.popupBubbleView.show()
  1687. } else if (binding.popupBubbleView.isShown == true) {
  1688. newMessagesCount++
  1689. }
  1690. } else {
  1691. newMessagesCount = 0
  1692. }
  1693. if (adapter != null) {
  1694. chatMessage.isGrouped = (
  1695. adapter!!.isPreviousSameAuthor(
  1696. chatMessage.actorId,
  1697. -1
  1698. ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0
  1699. )
  1700. chatMessage.isOneToOneConversation =
  1701. (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  1702. adapter?.addToStart(chatMessage, shouldScroll)
  1703. }
  1704. }
  1705. if (shouldAddNewMessagesNotice && adapter != null) {
  1706. layoutManager?.scrollToPositionWithOffset(
  1707. adapter!!.getMessagePositionByIdInReverse("-1"),
  1708. binding.messagesListView.height / 2
  1709. )
  1710. }
  1711. }
  1712. // update read status of all messages
  1713. for (message in adapter!!.items) {
  1714. xChatLastCommonRead?.let {
  1715. if (message.item is ChatMessage) {
  1716. val chatMessage = message.item as ChatMessage
  1717. if (chatMessage.jsonMessageId <= it) {
  1718. chatMessage.readStatus = ReadStatus.READ
  1719. } else {
  1720. chatMessage.readStatus = ReadStatus.SENT
  1721. }
  1722. }
  1723. }
  1724. }
  1725. adapter?.notifyDataSetChanged()
  1726. if (inConversation) {
  1727. pullChatMessages(1, 1, xChatLastCommonRead)
  1728. }
  1729. } else if (response.code() == 304 && !isFromTheFuture) {
  1730. if (isFirstMessagesProcessing) {
  1731. cancelNotificationsForCurrentConversation()
  1732. isFirstMessagesProcessing = false
  1733. binding.progressBar.visibility = View.GONE
  1734. }
  1735. historyRead = true
  1736. if (!lookingIntoFuture && inConversation) {
  1737. pullChatMessages(1)
  1738. }
  1739. }
  1740. }
  1741. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  1742. if (!historyRead && inConversation) {
  1743. pullChatMessages(0)
  1744. }
  1745. }
  1746. override fun format(date: Date): String {
  1747. return if (DateFormatter.isToday(date)) {
  1748. resources!!.getString(R.string.nc_date_header_today)
  1749. } else if (DateFormatter.isYesterday(date)) {
  1750. resources!!.getString(R.string.nc_date_header_yesterday)
  1751. } else {
  1752. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  1753. }
  1754. }
  1755. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  1756. super.onCreateOptionsMenu(menu, inflater)
  1757. inflater.inflate(R.menu.menu_conversation, menu)
  1758. if (conversationUser?.userId == "?") {
  1759. menu.removeItem(R.id.conversation_info)
  1760. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  1761. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  1762. } else {
  1763. conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
  1764. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  1765. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  1766. loadAvatarForStatusBar()
  1767. }
  1768. }
  1769. override fun onPrepareOptionsMenu(menu: Menu) {
  1770. super.onPrepareOptionsMenu(menu)
  1771. conversationUser?.let {
  1772. if (CapabilitiesUtil.hasSpreedFeatureCapability(it, "read-only-rooms")) {
  1773. checkReadOnlyState()
  1774. }
  1775. }
  1776. }
  1777. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  1778. when (item.itemId) {
  1779. android.R.id.home -> {
  1780. router.popCurrentController()
  1781. return true
  1782. }
  1783. R.id.conversation_video_call -> {
  1784. if (conversationVideoMenuItem?.icon?.alpha == FULLY_OPAQUE_INT) {
  1785. startACall(false)
  1786. return true
  1787. }
  1788. return false
  1789. }
  1790. R.id.conversation_voice_call -> {
  1791. if (conversationVoiceCallMenuItem?.icon?.alpha == FULLY_OPAQUE_INT) {
  1792. startACall(true)
  1793. return true
  1794. }
  1795. return false
  1796. }
  1797. R.id.conversation_info -> {
  1798. showConversationInfoScreen()
  1799. return true
  1800. }
  1801. else -> return super.onOptionsItemSelected(item)
  1802. }
  1803. }
  1804. private fun setDeletionFlagsAndRemoveInfomessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  1805. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  1806. val chatMessageIterator = chatMessageMap.iterator()
  1807. while (chatMessageIterator.hasNext()) {
  1808. val currentMessage = chatMessageIterator.next()
  1809. if (isInfoMessageAboutDeletion(currentMessage)) {
  1810. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
  1811. // if chatMessageMap doesnt't contain message to delete (this happens when lookingIntoFuture),
  1812. // the message to delete has to be modified directly inside the adapter
  1813. setMessageAsDeleted(currentMessage.value.parentMessage)
  1814. } else {
  1815. chatMessageMap[currentMessage.value.parentMessage.id]!!.isDeleted = true
  1816. }
  1817. chatMessageIterator.remove()
  1818. }
  1819. }
  1820. return chatMessageMap.values.toList()
  1821. }
  1822. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  1823. return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
  1824. .SystemMessageType.MESSAGE_DELETED
  1825. }
  1826. private fun startACall(isVoiceOnlyCall: Boolean) {
  1827. isLeavingForConversation = true
  1828. val callIntent = getIntentForCall(isVoiceOnlyCall)
  1829. if (callIntent != null) {
  1830. startActivity(callIntent)
  1831. }
  1832. }
  1833. private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? {
  1834. currentConversation?.let {
  1835. val bundle = Bundle()
  1836. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1837. bundle.putString(KEY_ROOM_ID, roomId)
  1838. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  1839. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  1840. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
  1841. bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName)
  1842. if (isVoiceOnlyCall) {
  1843. bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
  1844. }
  1845. return if (activity != null) {
  1846. val callIntent = Intent(activity, MagicCallActivity::class.java)
  1847. callIntent.putExtras(bundle)
  1848. callIntent
  1849. } else {
  1850. null
  1851. }
  1852. } ?: run {
  1853. return null
  1854. }
  1855. }
  1856. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  1857. PopupMenu(
  1858. ContextThemeWrapper(view?.context, R.style.appActionBarPopupMenu),
  1859. view,
  1860. if (
  1861. message?.user?.id == currentConversation?.actorType + "/" + currentConversation?.actorId
  1862. ) Gravity.END else Gravity.START
  1863. ).apply {
  1864. setOnMenuItemClickListener { item ->
  1865. when (item?.itemId) {
  1866. R.id.action_copy_message -> {
  1867. val clipboardManager =
  1868. activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
  1869. val clipData = ClipData.newPlainText(resources?.getString(R.string.nc_app_name), message?.text)
  1870. clipboardManager.setPrimaryClip(clipData)
  1871. true
  1872. }
  1873. R.id.action_reply_to_message -> {
  1874. val chatMessage = message as ChatMessage?
  1875. replyToMessage(chatMessage, message?.jsonMessageId)
  1876. true
  1877. }
  1878. R.id.action_reply_privately -> {
  1879. val apiVersion =
  1880. ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1881. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  1882. apiVersion,
  1883. conversationUser?.baseUrl,
  1884. "1",
  1885. null,
  1886. message?.user?.id?.substring(6),
  1887. null
  1888. )
  1889. ncApi!!.createRoom(
  1890. credentials,
  1891. retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
  1892. )
  1893. .subscribeOn(Schedulers.io())
  1894. .observeOn(AndroidSchedulers.mainThread())
  1895. .subscribe(object : Observer<RoomOverall> {
  1896. override fun onSubscribe(d: Disposable) {
  1897. // unused atm
  1898. }
  1899. override fun onNext(roomOverall: RoomOverall) {
  1900. val bundle = Bundle()
  1901. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  1902. bundle.putString(KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
  1903. bundle.putString(KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
  1904. // FIXME once APIv2+ is used only, the createRoom already returns all the data
  1905. ncApi!!.getRoom(
  1906. credentials,
  1907. ApiUtils.getUrlForRoom(
  1908. apiVersion, conversationUser?.baseUrl,
  1909. roomOverall.getOcs().getData().getToken()
  1910. )
  1911. )
  1912. .subscribeOn(Schedulers.io())
  1913. .observeOn(AndroidSchedulers.mainThread())
  1914. .subscribe(object : Observer<RoomOverall> {
  1915. override fun onSubscribe(d: Disposable) {
  1916. // unused atm
  1917. }
  1918. override fun onNext(roomOverall: RoomOverall) {
  1919. bundle.putParcelable(
  1920. KEY_ACTIVE_CONVERSATION,
  1921. Parcels.wrap(roomOverall.getOcs().getData())
  1922. )
  1923. remapChatController(
  1924. router, conversationUser!!.id,
  1925. roomOverall.getOcs().getData().getToken(), bundle, true
  1926. )
  1927. }
  1928. override fun onError(e: Throwable) {
  1929. Log.e(TAG, e.message, e)
  1930. }
  1931. override fun onComplete() {
  1932. // unused atm
  1933. }
  1934. })
  1935. }
  1936. override fun onError(e: Throwable) {
  1937. Log.e(TAG, e.message, e)
  1938. }
  1939. override fun onComplete() {
  1940. // unused atm
  1941. }
  1942. })
  1943. true
  1944. }
  1945. R.id.action_delete_message -> {
  1946. var apiVersion = 1
  1947. // FIXME Fix API checking with guests?
  1948. if (conversationUser != null) {
  1949. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1950. }
  1951. ncApi?.deleteChatMessage(
  1952. credentials,
  1953. ApiUtils.getUrlForChatMessage(
  1954. apiVersion,
  1955. conversationUser?.baseUrl,
  1956. roomToken,
  1957. message?.id
  1958. )
  1959. )?.subscribeOn(Schedulers.io())
  1960. ?.observeOn(AndroidSchedulers.mainThread())
  1961. ?.subscribe(object : Observer<ChatOverallSingleMessage> {
  1962. override fun onSubscribe(d: Disposable) {
  1963. // unused atm
  1964. }
  1965. override fun onNext(t: ChatOverallSingleMessage) {
  1966. if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  1967. Toast.makeText(
  1968. context, R.string.nc_delete_message_leaked_to_matterbridge,
  1969. Toast.LENGTH_LONG
  1970. ).show()
  1971. }
  1972. }
  1973. override fun onError(e: Throwable) {
  1974. Log.e(
  1975. TAG,
  1976. "Something went wrong when trying to delete message with id " +
  1977. message?.id,
  1978. e
  1979. )
  1980. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  1981. }
  1982. override fun onComplete() {
  1983. // unused atm
  1984. }
  1985. })
  1986. true
  1987. }
  1988. else -> false
  1989. }
  1990. }
  1991. inflate(R.menu.chat_message_menu)
  1992. menu.findItem(R.id.action_copy_message).isVisible = !(message as ChatMessage).isDeleted
  1993. menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable
  1994. menu.findItem(R.id.action_reply_privately).isVisible = (message as ChatMessage).replyable &&
  1995. conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
  1996. (message as ChatMessage).user.id.startsWith("users/") &&
  1997. (message as ChatMessage).user.id.substring(6) != currentConversation?.actorId &&
  1998. currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1999. menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
  2000. if (menu.hasVisibleItems()) {
  2001. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
  2002. setForceShowIcon(true)
  2003. }
  2004. show()
  2005. }
  2006. }
  2007. }
  2008. private fun replyToMessage(chatMessage: ChatMessage?, jsonMessageId: Int?) {
  2009. chatMessage?.let {
  2010. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
  2011. View.GONE
  2012. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
  2013. View.VISIBLE
  2014. val quotedMessage = binding
  2015. .messageInputView
  2016. .findViewById<EmojiTextView>(R.id.quotedMessage)
  2017. quotedMessage?.maxLines = 2
  2018. quotedMessage?.ellipsize = TextUtils.TruncateAt.END
  2019. quotedMessage?.text = it.text
  2020. binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
  2021. it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest)
  2022. conversationUser?.let { currentUser ->
  2023. val quotedMessageImage = binding
  2024. .messageInputView
  2025. .findViewById<ImageView>(R.id.quotedMessageImage)
  2026. chatMessage.imageUrl?.let { previewImageUrl ->
  2027. quotedMessageImage?.visibility = View.VISIBLE
  2028. val px = TypedValue.applyDimension(
  2029. TypedValue.COMPLEX_UNIT_DIP,
  2030. 96f,
  2031. resources?.displayMetrics
  2032. )
  2033. quotedMessageImage?.maxHeight = px.toInt()
  2034. val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
  2035. layoutParams.flexGrow = 0f
  2036. quotedMessageImage.layoutParams = layoutParams
  2037. quotedMessageImage.load(previewImageUrl) {
  2038. addHeader("Authorization", credentials!!)
  2039. }
  2040. } ?: run {
  2041. binding
  2042. .messageInputView
  2043. .findViewById<ImageView>(R.id.quotedMessageImage)
  2044. ?.visibility = View.GONE
  2045. }
  2046. }
  2047. val quotedChatMessageView = binding
  2048. .messageInputView
  2049. .findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  2050. quotedChatMessageView?.tag = jsonMessageId
  2051. quotedChatMessageView?.visibility = View.VISIBLE
  2052. }
  2053. }
  2054. private fun showMicrophoneButton(show: Boolean) {
  2055. if (show && CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
  2056. binding.messageInputView.messageSendButton.visibility = View.GONE
  2057. binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
  2058. } else {
  2059. binding.messageInputView.messageSendButton.visibility = View.VISIBLE
  2060. binding.messageInputView.recordAudioButton.visibility = View.GONE
  2061. }
  2062. }
  2063. private fun setMessageAsDeleted(message: IMessage?) {
  2064. val messageTemp = message as ChatMessage
  2065. messageTemp.isDeleted = true
  2066. messageTemp.isOneToOneConversation =
  2067. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2068. messageTemp.isLinkPreviewAllowed = isLinkPreviewAllowed
  2069. messageTemp.activeUser = conversationUser
  2070. adapter?.update(messageTemp)
  2071. }
  2072. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  2073. if (conversationUser == null) return false
  2074. if (message.systemMessageType != ChatMessage.SystemMessageType.DUMMY) return false
  2075. if (message.isDeleted) return false
  2076. if (message.hasFileAttachment()) return false
  2077. if (OBJECT_MESSAGE.equals(message.message)) return false
  2078. val isOlderThanSixHours = message
  2079. .createdAt
  2080. ?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true
  2081. if (isOlderThanSixHours) return false
  2082. val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
  2083. true
  2084. } else {
  2085. currentConversation!!.isParticipantOwnerOrModerator
  2086. }
  2087. if (!isUserAllowedByPrivileges) return false
  2088. if (!CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "delete-messages")) return false
  2089. return true
  2090. }
  2091. override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
  2092. return when (type) {
  2093. CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
  2094. CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage()
  2095. CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
  2096. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
  2097. else -> false
  2098. }
  2099. }
  2100. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2101. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  2102. /*
  2103. switch (webSocketCommunicationEvent.getType()) {
  2104. case "refreshChat":
  2105. if (
  2106. webSocketCommunicationEvent
  2107. .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID)
  2108. .equals(Long.toString(conversationUser.getId()))
  2109. ) {
  2110. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  2111. pullChatMessages(2);
  2112. }
  2113. }
  2114. break;
  2115. default:
  2116. }*/
  2117. }
  2118. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2119. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  2120. if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2121. currentConversation?.name != userMentionClickEvent.userId
  2122. ) {
  2123. var apiVersion = 1
  2124. // FIXME Fix API checking with guests?
  2125. if (conversationUser != null) {
  2126. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  2127. }
  2128. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2129. apiVersion,
  2130. conversationUser?.baseUrl,
  2131. "1",
  2132. null,
  2133. userMentionClickEvent.userId,
  2134. null
  2135. )
  2136. ncApi?.createRoom(
  2137. credentials,
  2138. retrofitBucket.url, retrofitBucket.queryMap
  2139. )
  2140. ?.subscribeOn(Schedulers.io())
  2141. ?.observeOn(AndroidSchedulers.mainThread())
  2142. ?.subscribe(object : Observer<RoomOverall> {
  2143. override fun onSubscribe(d: Disposable) {
  2144. // unused atm
  2145. }
  2146. override fun onNext(roomOverall: RoomOverall) {
  2147. val conversationIntent = Intent(activity, MagicCallActivity::class.java)
  2148. val bundle = Bundle()
  2149. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2150. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token)
  2151. bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId)
  2152. if (conversationUser != null) {
  2153. bundle.putParcelable(
  2154. KEY_ACTIVE_CONVERSATION,
  2155. Parcels.wrap(roomOverall.ocs.data)
  2156. )
  2157. conversationIntent.putExtras(bundle)
  2158. ConductorRemapping.remapChatController(
  2159. router, conversationUser.id,
  2160. roomOverall.ocs.data.token, bundle, false
  2161. )
  2162. } else {
  2163. conversationIntent.putExtras(bundle)
  2164. startActivity(conversationIntent)
  2165. Handler().postDelayed(
  2166. {
  2167. if (!isDestroyed && !isBeingDestroyed) {
  2168. router.popCurrentController()
  2169. }
  2170. },
  2171. POP_CURRENT_CONTROLLER_DELAY
  2172. )
  2173. }
  2174. }
  2175. override fun onError(e: Throwable) {
  2176. // unused atm
  2177. }
  2178. override fun onComplete() {
  2179. // unused atm
  2180. }
  2181. })
  2182. }
  2183. }
  2184. companion object {
  2185. private const val TAG = "ChatController"
  2186. private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
  2187. private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
  2188. private const val CONTENT_TYPE_LOCATION: Byte = 3
  2189. private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
  2190. private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
  2191. private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
  2192. private const val LOBBY_TIMER_DELAY: Long = 5000
  2193. private const val HTTP_CODE_OK: Int = 200
  2194. private const val MESSAGE_MAX_LENGTH: Int = 1000
  2195. private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
  2196. private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
  2197. private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
  2198. private const val OBJECT_MESSAGE: String = "{object}"
  2199. private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
  2200. private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
  2201. private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
  2202. private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
  2203. private const val SHORT_VIBRATE: Long = 20
  2204. private const val FULLY_OPAQUE_INT: Int = 255
  2205. private const val SEMI_TRANSPARENT_INT: Int = 99
  2206. private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
  2207. private const val SECOND: Long = 1000
  2208. }
  2209. }