ChatController.kt 104 KB

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