ChatController.kt 105 KB

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