ChatController.kt 104 KB

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