ChatActivity.kt 147 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
  5. * SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com>
  6. * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
  7. * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  8. * SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
  9. * SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
  10. * SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
  11. * SPDX-License-Identifier: GPL-3.0-or-later
  12. */
  13. package com.nextcloud.talk.chat
  14. import android.Manifest
  15. import android.annotation.SuppressLint
  16. import android.app.Activity
  17. import android.content.ClipData
  18. import android.content.ClipboardManager
  19. import android.content.Context
  20. import android.content.Intent
  21. import android.content.pm.PackageManager
  22. import android.content.res.AssetFileDescriptor
  23. import android.database.Cursor
  24. import android.graphics.drawable.BitmapDrawable
  25. import android.graphics.drawable.ColorDrawable
  26. import android.graphics.drawable.Drawable
  27. import android.media.MediaPlayer
  28. import android.net.Uri
  29. import android.os.Build
  30. import android.os.Bundle
  31. import android.os.Handler
  32. import android.provider.ContactsContract
  33. import android.provider.MediaStore
  34. import android.text.SpannableStringBuilder
  35. import android.text.TextUtils
  36. import android.util.Log
  37. import android.view.Gravity
  38. import android.view.Menu
  39. import android.view.MenuItem
  40. import android.view.View
  41. import android.view.animation.AccelerateDecelerateInterpolator
  42. import android.widget.AbsListView
  43. import android.widget.FrameLayout
  44. import android.widget.ImageView
  45. import android.widget.PopupMenu
  46. import android.widget.TextView
  47. import androidx.activity.OnBackPressedCallback
  48. import androidx.activity.result.ActivityResult
  49. import androidx.activity.result.contract.ActivityResultContracts
  50. import androidx.appcompat.view.ContextThemeWrapper
  51. import androidx.core.content.FileProvider
  52. import androidx.core.content.PermissionChecker
  53. import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
  54. import androidx.core.graphics.drawable.toBitmap
  55. import androidx.core.text.bold
  56. import androidx.emoji2.text.EmojiCompat
  57. import androidx.fragment.app.DialogFragment
  58. import androidx.fragment.app.commit
  59. import androidx.lifecycle.ViewModelProvider
  60. import androidx.lifecycle.lifecycleScope
  61. import androidx.recyclerview.widget.ItemTouchHelper
  62. import androidx.recyclerview.widget.LinearLayoutManager
  63. import androidx.recyclerview.widget.RecyclerView
  64. import androidx.work.Data
  65. import androidx.work.OneTimeWorkRequest
  66. import androidx.work.WorkInfo
  67. import androidx.work.WorkManager
  68. import autodagger.AutoInjector
  69. import coil.imageLoader
  70. import coil.request.CachePolicy
  71. import coil.request.ImageRequest
  72. import coil.target.Target
  73. import coil.transform.CircleCropTransformation
  74. import com.google.android.material.snackbar.Snackbar
  75. import com.nextcloud.android.common.ui.theme.utils.ColorRole
  76. import com.nextcloud.talk.BuildConfig
  77. import com.nextcloud.talk.R
  78. import com.nextcloud.talk.activities.BaseActivity
  79. import com.nextcloud.talk.activities.CallActivity
  80. import com.nextcloud.talk.activities.TakePhotoActivity
  81. import com.nextcloud.talk.adapters.messages.CallStartedMessageInterface
  82. import com.nextcloud.talk.adapters.messages.CallStartedViewHolder
  83. import com.nextcloud.talk.adapters.messages.CommonMessageInterface
  84. import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
  85. import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
  86. import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
  87. import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
  88. import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder
  89. import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
  90. import com.nextcloud.talk.adapters.messages.MessagePayload
  91. import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder
  92. import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
  93. import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
  94. import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
  95. import com.nextcloud.talk.adapters.messages.OutcomingTextMessageViewHolder
  96. import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
  97. import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
  98. import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
  99. import com.nextcloud.talk.adapters.messages.SystemMessageInterface
  100. import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
  101. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  102. import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
  103. import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
  104. import com.nextcloud.talk.api.NcApi
  105. import com.nextcloud.talk.application.NextcloudTalkApplication
  106. import com.nextcloud.talk.chat.data.model.ChatMessage
  107. import com.nextcloud.talk.chat.viewmodels.ChatViewModel
  108. import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
  109. import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
  110. import com.nextcloud.talk.conversationlist.ConversationsListActivity
  111. import com.nextcloud.talk.data.user.model.User
  112. import com.nextcloud.talk.databinding.ActivityChatBinding
  113. import com.nextcloud.talk.events.UserMentionClickEvent
  114. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  115. import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
  116. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  117. import com.nextcloud.talk.jobs.ShareOperationWorker
  118. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  119. import com.nextcloud.talk.location.LocationPickerActivity
  120. import com.nextcloud.talk.messagesearch.MessageSearchActivity
  121. import com.nextcloud.talk.models.ExternalSignalingServer
  122. import com.nextcloud.talk.models.domain.ConversationModel
  123. import com.nextcloud.talk.models.json.capabilities.SpreedCapability
  124. import com.nextcloud.talk.models.json.chat.ReadStatus
  125. import com.nextcloud.talk.models.json.conversations.ConversationEnums
  126. import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
  127. import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
  128. import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
  129. import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
  130. import com.nextcloud.talk.signaling.SignalingMessageReceiver
  131. import com.nextcloud.talk.signaling.SignalingMessageSender
  132. import com.nextcloud.talk.translate.ui.TranslateActivity
  133. import com.nextcloud.talk.ui.StatusDrawable
  134. import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
  135. import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
  136. import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
  137. import com.nextcloud.talk.ui.dialog.MessageActionsDialog
  138. import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
  139. import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
  140. import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
  141. import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
  142. import com.nextcloud.talk.utils.ApiUtils
  143. import com.nextcloud.talk.utils.AudioUtils
  144. import com.nextcloud.talk.utils.CapabilitiesUtil
  145. import com.nextcloud.talk.utils.ContactUtils
  146. import com.nextcloud.talk.utils.ConversationUtils
  147. import com.nextcloud.talk.utils.DateConstants
  148. import com.nextcloud.talk.utils.DateUtils
  149. import com.nextcloud.talk.utils.DisplayUtils
  150. import com.nextcloud.talk.utils.FileUtils
  151. import com.nextcloud.talk.utils.FileViewerUtils
  152. import com.nextcloud.talk.utils.Mimetype
  153. import com.nextcloud.talk.utils.NotificationUtils
  154. import com.nextcloud.talk.utils.ParticipantPermissions
  155. import com.nextcloud.talk.utils.SpreedFeatures
  156. import com.nextcloud.talk.utils.VibrationUtils
  157. import com.nextcloud.talk.utils.bundle.BundleKeys
  158. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
  159. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
  160. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
  161. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
  162. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
  163. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
  164. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
  165. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  166. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
  167. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
  168. import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
  169. import com.nextcloud.talk.utils.rx.DisposableSet
  170. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  171. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  172. import com.nextcloud.talk.webrtc.WebSocketInstance
  173. import com.otaliastudios.autocomplete.Autocomplete
  174. import com.stfalcon.chatkit.commons.ImageLoader
  175. import com.stfalcon.chatkit.commons.models.IMessage
  176. import com.stfalcon.chatkit.messages.MessageHolders
  177. import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
  178. import com.stfalcon.chatkit.messages.MessagesListAdapter
  179. import com.stfalcon.chatkit.utils.DateFormatter
  180. import io.reactivex.Observer
  181. import io.reactivex.disposables.Disposable
  182. import kotlinx.coroutines.CoroutineScope
  183. import kotlinx.coroutines.Dispatchers
  184. import kotlinx.coroutines.flow.collect
  185. import kotlinx.coroutines.flow.onEach
  186. import kotlinx.coroutines.launch
  187. import kotlinx.coroutines.withContext
  188. import org.greenrobot.eventbus.Subscribe
  189. import org.greenrobot.eventbus.ThreadMode
  190. import retrofit2.HttpException
  191. import java.io.File
  192. import java.io.IOException
  193. import java.net.HttpURLConnection
  194. import java.text.SimpleDateFormat
  195. import java.util.Date
  196. import java.util.Locale
  197. import java.util.concurrent.ExecutionException
  198. import javax.inject.Inject
  199. import kotlin.collections.set
  200. import kotlin.math.roundToInt
  201. @AutoInjector(NextcloudTalkApplication::class)
  202. class ChatActivity :
  203. BaseActivity(),
  204. MessagesListAdapter.OnLoadMoreListener,
  205. MessagesListAdapter.Formatter<Date>,
  206. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  207. ContentChecker<ChatMessage>,
  208. VoiceMessageInterface,
  209. CommonMessageInterface,
  210. PreviewMessageInterface,
  211. SystemMessageInterface,
  212. CallStartedMessageInterface {
  213. var active = false
  214. private lateinit var binding: ActivityChatBinding
  215. @Inject
  216. lateinit var ncApi: NcApi
  217. @Inject
  218. lateinit var permissionUtil: PlatformPermissionUtil
  219. @Inject
  220. lateinit var dateUtils: DateUtils
  221. @Inject
  222. lateinit var viewModelFactory: ViewModelProvider.Factory
  223. lateinit var chatViewModel: ChatViewModel
  224. lateinit var messageInputViewModel: MessageInputViewModel
  225. private val startSelectContactForResult = registerForActivityResult(
  226. ActivityResultContracts
  227. .StartActivityForResult()
  228. ) {
  229. executeIfResultOk(it) { intent ->
  230. onSelectContactResult(intent)
  231. }
  232. }
  233. private val startChooseFileIntentForResult = registerForActivityResult(
  234. ActivityResultContracts.StartActivityForResult()
  235. ) {
  236. executeIfResultOk(it) { intent ->
  237. onChooseFileResult(intent)
  238. }
  239. }
  240. private val startRemoteFileBrowsingForResult = registerForActivityResult(
  241. ActivityResultContracts.StartActivityForResult()
  242. ) {
  243. executeIfResultOk(it) { intent ->
  244. onRemoteFileBrowsingResult(intent)
  245. }
  246. }
  247. private val startMessageSearchForResult =
  248. registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
  249. executeIfResultOk(it) { intent ->
  250. onMessageSearchResult(intent)
  251. }
  252. }
  253. private val startPickCameraIntentForResult = registerForActivityResult(
  254. ActivityResultContracts.StartActivityForResult()
  255. ) {
  256. executeIfResultOk(it) { intent ->
  257. onPickCameraResult(intent)
  258. }
  259. }
  260. override val view: View
  261. get() = binding.root
  262. val disposables = DisposableSet()
  263. var sessionIdAfterRoomJoined: String? = null
  264. lateinit var roomToken: String
  265. var conversationUser: User? = null
  266. lateinit var spreedCapabilities: SpreedCapability
  267. var chatApiVersion: Int = 1
  268. private var roomPassword: String = ""
  269. var credentials: String? = null
  270. var currentConversation: ConversationModel? = null
  271. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  272. var mentionAutocomplete: Autocomplete<*>? = null
  273. var layoutManager: LinearLayoutManager? = null
  274. var pullChatMessagesPending = false
  275. var newMessagesCount = 0
  276. var startCallFromNotification: Boolean = false
  277. var startCallFromRoomSwitch: Boolean = false
  278. // lateinit var roomId: String
  279. var voiceOnly: Boolean = true
  280. private lateinit var path: String
  281. var myFirstMessage: CharSequence? = null
  282. var checkingLobbyStatus: Boolean = false
  283. private var conversationVoiceCallMenuItem: MenuItem? = null
  284. private var conversationVideoMenuItem: MenuItem? = null
  285. var webSocketInstance: WebSocketInstance? = null
  286. var signalingMessageSender: SignalingMessageSender? = null
  287. var externalSignalingServer: ExternalSignalingServer? = null
  288. var getRoomInfoTimerHandler: Handler? = null
  289. private val filesToUpload: MutableList<String> = ArrayList()
  290. lateinit var sharedText: String
  291. var mediaPlayer: MediaPlayer? = null
  292. var mediaPlayerHandler: Handler? = null
  293. private var currentlyPlayedVoiceMessage: ChatMessage? = null
  294. // messy workaround for a mediaPlayer bug, don't delete
  295. private var lastRecordMediaPosition: Int = 0
  296. private var lastRecordedSeeked: Boolean = false
  297. lateinit var participantPermissions: ParticipantPermissions
  298. private var videoURI: Uri? = null
  299. private val onBackPressedCallback = object : OnBackPressedCallback(true) {
  300. override fun handleOnBackPressed() {
  301. chatViewModel.handleChatOnBackPress()
  302. if (currentlyPlayedVoiceMessage != null) {
  303. stopMediaPlayer(currentlyPlayedVoiceMessage!!)
  304. }
  305. val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
  306. intent.putExtras(Bundle())
  307. startActivity(intent)
  308. }
  309. }
  310. private lateinit var messageInputFragment: MessageInputFragment
  311. val typingParticipants = HashMap<String, TypingParticipant>()
  312. var callStarted = false
  313. private var voiceMessageToRestoreId = ""
  314. private var voiceMessageToRestoreAudioPosition = 0
  315. private var voiceMessageToRestoreWasPlaying = false
  316. private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
  317. override fun onSwitchTo(token: String?) {
  318. if (token != null) {
  319. if (CallActivity.active) {
  320. Log.d(TAG, "CallActivity is running. Ignore to switch chat in ChatActivity...")
  321. } else {
  322. switchToRoom(token, false, false)
  323. }
  324. }
  325. }
  326. }
  327. private val conversationMessageListener = object : SignalingMessageReceiver.ConversationMessageListener {
  328. override fun onStartTyping(userId: String?, session: String?) {
  329. val userIdOrGuestSession = userId ?: session
  330. if (isTypingStatusEnabled() && conversationUser?.userId != userIdOrGuestSession) {
  331. var displayName = webSocketInstance?.getDisplayNameForSession(session)
  332. if (displayName != null && !typingParticipants.contains(userIdOrGuestSession)) {
  333. if (displayName == "") {
  334. displayName = context.resources?.getString(R.string.nc_guest)!!
  335. }
  336. runOnUiThread {
  337. val typingParticipant = TypingParticipant(userIdOrGuestSession!!, displayName) {
  338. typingParticipants.remove(userIdOrGuestSession)
  339. updateTypingIndicator()
  340. }
  341. typingParticipants[userIdOrGuestSession] = typingParticipant
  342. updateTypingIndicator()
  343. }
  344. } else if (typingParticipants.contains(userIdOrGuestSession)) {
  345. typingParticipants[userIdOrGuestSession]?.restartTimer()
  346. }
  347. }
  348. }
  349. override fun onStopTyping(userId: String?, session: String?) {
  350. val userIdOrGuestSession = userId ?: session
  351. if (isTypingStatusEnabled() && conversationUser?.userId != userId) {
  352. typingParticipants[userIdOrGuestSession]?.cancelTimer()
  353. typingParticipants.remove(userIdOrGuestSession)
  354. updateTypingIndicator()
  355. }
  356. }
  357. }
  358. override fun onCreate(savedInstanceState: Bundle?) {
  359. super.onCreate(savedInstanceState)
  360. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  361. binding = ActivityChatBinding.inflate(layoutInflater)
  362. setupActionBar()
  363. setContentView(binding.root)
  364. setupSystemColors()
  365. messageInputFragment = MessageInputFragment()
  366. conversationUser = currentUserProvider.currentUser.blockingGet()
  367. handleIntent(intent)
  368. chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
  369. messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
  370. binding.progressBar.visibility = View.VISIBLE
  371. onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
  372. initObservers()
  373. if (savedInstanceState != null) {
  374. // Restore value of members from saved state
  375. var voiceMessageId = savedInstanceState.getString(CURRENT_AUDIO_MESSAGE_KEY, "")
  376. var voiceMessagePosition = savedInstanceState.getInt(CURRENT_AUDIO_POSITION_KEY, 0)
  377. var wasAudioPLaying = savedInstanceState.getBoolean(CURRENT_AUDIO_WAS_PLAYING_KEY, false)
  378. if (!voiceMessageId.equals("")) {
  379. Log.d(RESUME_AUDIO_TAG, "restored voice messageID: " + voiceMessageId)
  380. Log.d(RESUME_AUDIO_TAG, "audio position: " + voiceMessagePosition)
  381. Log.d(RESUME_AUDIO_TAG, "audio was playing: " + wasAudioPLaying.toString())
  382. voiceMessageToRestoreId = voiceMessageId
  383. voiceMessageToRestoreAudioPosition = voiceMessagePosition
  384. voiceMessageToRestoreWasPlaying = wasAudioPLaying
  385. } else {
  386. Log.d(RESUME_AUDIO_TAG, "stored voice message id is empty, not resuming audio playing")
  387. voiceMessageToRestoreId = ""
  388. voiceMessageToRestoreAudioPosition = 0
  389. voiceMessageToRestoreWasPlaying = false
  390. }
  391. } else {
  392. voiceMessageToRestoreId = ""
  393. voiceMessageToRestoreAudioPosition = 0
  394. voiceMessageToRestoreWasPlaying = false
  395. }
  396. }
  397. override fun onNewIntent(intent: Intent) {
  398. super.onNewIntent(intent)
  399. val extras: Bundle? = intent.extras
  400. val requestedRoomSwitch = extras?.getBoolean(KEY_SWITCH_TO_ROOM, false) == true
  401. if (requestedRoomSwitch) {
  402. val newRoomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
  403. val startCallAfterRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
  404. val isVoiceOnlyCall = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
  405. if (newRoomToken != roomToken) {
  406. switchToRoom(newRoomToken, startCallAfterRoomSwitch, isVoiceOnlyCall)
  407. }
  408. } else {
  409. handleIntent(intent)
  410. }
  411. }
  412. private fun handleIntent(intent: Intent) {
  413. val extras: Bundle? = intent.extras
  414. // roomId = extras?.getString(KEY_ROOM_ID).orEmpty()
  415. roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
  416. sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
  417. Log.d(TAG, " roomToken = $roomToken")
  418. if (roomToken.isEmpty()) {
  419. Log.d(TAG, " roomToken was null or empty!")
  420. }
  421. roomPassword = extras?.getString(BundleKeys.KEY_CONVERSATION_PASSWORD).orEmpty()
  422. credentials = if (conversationUser?.userId == "?") {
  423. null
  424. } else {
  425. ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  426. }
  427. startCallFromNotification = extras?.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false) == true
  428. startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
  429. voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
  430. }
  431. override fun onStart() {
  432. super.onStart()
  433. active = true
  434. this.lifecycle.addObserver(AudioUtils)
  435. this.lifecycle.addObserver(chatViewModel)
  436. }
  437. override fun onSaveInstanceState(outState: Bundle) {
  438. if (currentlyPlayedVoiceMessage != null) {
  439. outState.putString(CURRENT_AUDIO_MESSAGE_KEY, currentlyPlayedVoiceMessage!!.id)
  440. outState.putInt(CURRENT_AUDIO_POSITION_KEY, currentlyPlayedVoiceMessage!!.voiceMessagePlayedSeconds)
  441. outState.putBoolean(CURRENT_AUDIO_WAS_PLAYING_KEY, currentlyPlayedVoiceMessage!!.isPlayingVoiceMessage)
  442. Log.d(RESUME_AUDIO_TAG, "Stored current audio message ID: " + currentlyPlayedVoiceMessage!!.id)
  443. Log.d(
  444. RESUME_AUDIO_TAG,
  445. "Audio Position: " + currentlyPlayedVoiceMessage!!.voiceMessagePlayedSeconds
  446. .toString() + " | isPLaying: " + currentlyPlayedVoiceMessage!!.isPlayingVoiceMessage
  447. )
  448. }
  449. chatViewModel.handleOrientationChange()
  450. super.onSaveInstanceState(outState)
  451. }
  452. override fun onStop() {
  453. super.onStop()
  454. active = false
  455. this.lifecycle.removeObserver(AudioUtils)
  456. this.lifecycle.removeObserver(chatViewModel)
  457. }
  458. @SuppressLint("NotifyDataSetChanged")
  459. @Suppress("LongMethod")
  460. private fun initObservers() {
  461. Log.d(TAG, "initObservers Called")
  462. this.lifecycleScope.launch {
  463. chatViewModel.getConversationFlow
  464. .onEach { conversationModel ->
  465. currentConversation = conversationModel
  466. val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  467. val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  468. chatViewModel.setData(
  469. currentConversation!!,
  470. credentials!!,
  471. urlForChatting
  472. )
  473. logConversationInfos("GetRoomSuccessState")
  474. if (adapter == null) {
  475. initAdapter()
  476. binding.messagesListView.setAdapter(adapter)
  477. layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
  478. }
  479. chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
  480. }.collect()
  481. }
  482. chatViewModel.getRoomViewState.observe(this) { state ->
  483. when (state) {
  484. is ChatViewModel.GetRoomSuccessState -> {
  485. // unused atm
  486. }
  487. is ChatViewModel.GetRoomErrorState -> {
  488. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  489. }
  490. else -> {}
  491. }
  492. }
  493. chatViewModel.getCapabilitiesViewState.observe(this) { state ->
  494. when (state) {
  495. is ChatViewModel.GetCapabilitiesUpdateState -> {
  496. if (currentConversation != null) {
  497. spreedCapabilities = state.spreedCapabilities
  498. chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  499. participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
  500. invalidateOptionsMenu()
  501. checkShowCallButtons()
  502. checkLobbyState()
  503. updateRoomTimerHandler()
  504. } else {
  505. Log.w(
  506. TAG,
  507. "currentConversation was null in observer ChatViewModel.GetCapabilitiesUpdateState"
  508. )
  509. }
  510. }
  511. is ChatViewModel.GetCapabilitiesInitialLoadState -> {
  512. if (currentConversation != null) {
  513. spreedCapabilities = state.spreedCapabilities
  514. chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  515. participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
  516. supportFragmentManager.commit {
  517. setReorderingAllowed(true) // optimizes out redundant replace operations
  518. replace(R.id.fragment_container_activity_chat, messageInputFragment)
  519. }
  520. joinRoomWithPassword()
  521. if (conversationUser?.userId != "?" &&
  522. CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
  523. ) {
  524. binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
  525. }
  526. loadAvatarForStatusBar()
  527. setupSwipeToReply()
  528. setActionBarTitle()
  529. checkShowCallButtons()
  530. checkLobbyState()
  531. updateRoomTimerHandler()
  532. val urlForChatting =
  533. ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  534. if (adapter?.isEmpty == true) {
  535. chatViewModel.loadMessages(
  536. withCredentials = credentials!!,
  537. withUrl = urlForChatting
  538. )
  539. }
  540. } else {
  541. Log.w(
  542. TAG,
  543. "currentConversation was null in observer ChatViewModel.GetCapabilitiesInitialLoadState"
  544. )
  545. }
  546. }
  547. is ChatViewModel.GetCapabilitiesErrorState -> {
  548. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  549. }
  550. else -> {}
  551. }
  552. }
  553. chatViewModel.joinRoomViewState.observe(this) { state ->
  554. when (state) {
  555. is ChatViewModel.JoinRoomSuccessState -> {
  556. currentConversation = state.conversationModel
  557. sessionIdAfterRoomJoined = currentConversation!!.sessionId
  558. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation!!.sessionId
  559. // ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = currentConversation!!.roomId
  560. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = currentConversation!!.token
  561. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  562. logConversationInfos("joinRoomWithPassword#onNext")
  563. setupWebsocket()
  564. if (webSocketInstance != null) {
  565. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  566. roomToken,
  567. sessionIdAfterRoomJoined,
  568. externalSignalingServer?.federation
  569. )
  570. }
  571. if (startCallFromNotification != null && startCallFromNotification) {
  572. startCallFromNotification = false
  573. startACall(voiceOnly, false)
  574. }
  575. if (startCallFromRoomSwitch) {
  576. startCallFromRoomSwitch = false
  577. startACall(voiceOnly, true)
  578. }
  579. }
  580. is ChatViewModel.JoinRoomErrorState -> {}
  581. else -> {}
  582. }
  583. }
  584. chatViewModel.leaveRoomViewState.observe(this) { state ->
  585. when (state) {
  586. is ChatViewModel.LeaveRoomSuccessState -> {
  587. logConversationInfos("leaveRoom#onNext")
  588. checkingLobbyStatus = false
  589. if (getRoomInfoTimerHandler != null) {
  590. getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
  591. }
  592. if (webSocketInstance != null && currentConversation != null) {
  593. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  594. "",
  595. sessionIdAfterRoomJoined
  596. )
  597. }
  598. sessionIdAfterRoomJoined = "0"
  599. if (state.funToCallWhenLeaveSuccessful != null) {
  600. Log.d(TAG, "a callback action was set and is now executed because room was left successfully")
  601. state.funToCallWhenLeaveSuccessful.invoke()
  602. }
  603. }
  604. else -> {}
  605. }
  606. }
  607. messageInputViewModel.sendChatMessageViewState.observe(this) { state ->
  608. when (state) {
  609. is MessageInputViewModel.SendChatMessageSuccessState -> {
  610. myFirstMessage = state.message
  611. if (binding.unreadMessagesPopup.isShown == true) {
  612. binding.unreadMessagesPopup.hide()
  613. }
  614. binding.messagesListView.smoothScrollToPosition(0)
  615. }
  616. is MessageInputViewModel.SendChatMessageErrorState -> {
  617. if (state.e is HttpException) {
  618. val code = state.e.code()
  619. if (code.toString().startsWith("2")) {
  620. myFirstMessage = state.message
  621. if (binding.unreadMessagesPopup.isShown == true) {
  622. binding.unreadMessagesPopup.hide()
  623. }
  624. binding.messagesListView.smoothScrollToPosition(0)
  625. }
  626. }
  627. }
  628. else -> {}
  629. }
  630. }
  631. chatViewModel.deleteChatMessageViewState.observe(this) { state ->
  632. when (state) {
  633. is ChatViewModel.DeleteChatMessageSuccessState -> {
  634. if (state.msg.ocs!!.meta!!.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  635. Snackbar.make(
  636. binding.root,
  637. R.string.nc_delete_message_leaked_to_matterbridge,
  638. Snackbar.LENGTH_LONG
  639. ).show()
  640. }
  641. val id = state.msg.ocs!!.data!!.parentMessage!!.id.toString()
  642. val index = adapter?.getMessagePositionById(id) ?: 0
  643. val message = adapter?.items?.get(index)?.item as ChatMessage
  644. setMessageAsDeleted(message)
  645. }
  646. is ChatViewModel.DeleteChatMessageErrorState -> {
  647. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  648. }
  649. else -> {}
  650. }
  651. }
  652. chatViewModel.createRoomViewState.observe(this) { state ->
  653. when (state) {
  654. is ChatViewModel.CreateRoomSuccessState -> {
  655. val bundle = Bundle()
  656. bundle.putString(KEY_ROOM_TOKEN, state.roomOverall.ocs!!.data!!.token)
  657. // bundle.putString(KEY_ROOM_ID, state.roomOverall.ocs!!.data!!.roomId)
  658. leaveRoom {
  659. val chatIntent = Intent(context, ChatActivity::class.java)
  660. chatIntent.putExtras(bundle)
  661. chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  662. startActivity(chatIntent)
  663. }
  664. }
  665. is ChatViewModel.CreateRoomErrorState -> {
  666. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  667. }
  668. else -> {}
  669. }
  670. }
  671. chatViewModel.chatMessageViewState.observe(this) { state ->
  672. when (state) {
  673. is ChatViewModel.ChatMessageStartState -> {
  674. // Handle UI on first load
  675. cancelNotificationsForCurrentConversation()
  676. binding.progressBar.visibility = View.GONE
  677. binding.messagesListView.visibility = View.VISIBLE
  678. collapseSystemMessages()
  679. }
  680. is ChatViewModel.ChatMessageUpdateState -> {
  681. // unused atm
  682. }
  683. is ChatViewModel.ChatMessageErrorState -> {
  684. // unused atm
  685. }
  686. else -> {}
  687. }
  688. }
  689. this.lifecycleScope.launch {
  690. chatViewModel.getMessageFlow
  691. .onEach { pair ->
  692. val lookIntoFuture = pair.first
  693. var chatMessageList = pair.second
  694. chatMessageList = handleSystemMessages(chatMessageList)
  695. determinePreviousMessageIds(chatMessageList)
  696. handleExpandableSystemMessages(chatMessageList)
  697. if (chatMessageList.isNotEmpty() &&
  698. ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
  699. ) {
  700. adapter?.clear()
  701. adapter?.notifyDataSetChanged()
  702. }
  703. if (lookIntoFuture) {
  704. processMessagesFromTheFuture(chatMessageList)
  705. } else {
  706. processMessagesNotFromTheFuture(chatMessageList)
  707. collapseSystemMessages()
  708. }
  709. processExpiredMessages()
  710. processCallStartedMessages(chatMessageList)
  711. adapter?.notifyDataSetChanged()
  712. }
  713. .collect()
  714. }
  715. this.lifecycleScope.launch {
  716. chatViewModel.getUpdateMessageFlow
  717. .onEach {
  718. updateMessageInsideAdapter(it)
  719. }
  720. .collect()
  721. }
  722. this.lifecycleScope.launch {
  723. chatViewModel.getLastCommonReadFlow
  724. .onEach {
  725. updateReadStatusOfAllMessages(it)
  726. processExpiredMessages()
  727. }
  728. .collect()
  729. }
  730. this.lifecycleScope.launch {
  731. chatViewModel.getLastReadMessageFlow
  732. .onEach { lastRead ->
  733. scrollToAndCenterMessageWithId(lastRead.toString())
  734. }
  735. .collect()
  736. }
  737. chatViewModel.reactionDeletedViewState.observe(this) { state ->
  738. when (state) {
  739. is ChatViewModel.ReactionDeletedSuccessState -> {
  740. updateUiToDeleteReaction(
  741. state.reactionDeletedModel.chatMessage,
  742. state.reactionDeletedModel.emoji
  743. )
  744. }
  745. else -> {}
  746. }
  747. }
  748. chatViewModel.reactionAddedViewState.observe(this) { state ->
  749. when (state) {
  750. is ChatViewModel.ReactionAddedSuccessState -> {
  751. updateUiToAddReaction(
  752. state.reactionAddedModel.chatMessage,
  753. state.reactionAddedModel.emoji
  754. )
  755. }
  756. else -> {}
  757. }
  758. }
  759. messageInputViewModel.editMessageViewState.observe(this) { state ->
  760. when (state) {
  761. is MessageInputViewModel.EditMessageSuccessState -> {
  762. when (state.messageEdited.ocs?.meta?.statusCode) {
  763. HTTP_BAD_REQUEST -> {
  764. Snackbar.make(
  765. binding.root,
  766. getString(R.string.edit_error_24_hours_old_message),
  767. Snackbar.LENGTH_LONG
  768. ).show()
  769. }
  770. HTTP_FORBIDDEN -> {
  771. Snackbar.make(
  772. binding.root,
  773. getString(R.string.conversation_is_read_only),
  774. Snackbar.LENGTH_LONG
  775. ).show()
  776. }
  777. HTTP_NOT_FOUND -> {
  778. Snackbar.make(
  779. binding.root,
  780. "Conversation not found",
  781. Snackbar.LENGTH_LONG
  782. ).show()
  783. }
  784. }
  785. val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)"
  786. val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString()
  787. val index = adapter?.getMessagePositionById(id) ?: 0
  788. val message = adapter?.items?.get(index)?.item as ChatMessage
  789. setMessageAsEdited(message, newString)
  790. }
  791. is MessageInputViewModel.EditMessageErrorState -> {
  792. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  793. }
  794. else -> {}
  795. }
  796. }
  797. chatViewModel.getVoiceRecordingLocked.observe(this) { showContiniousVoiceRecording ->
  798. if (showContiniousVoiceRecording) {
  799. binding.voiceRecordingLock.visibility = View.GONE
  800. supportFragmentManager.commit {
  801. setReorderingAllowed(true) // apparently used for optimizations
  802. replace(R.id.fragment_container_activity_chat, MessageInputVoiceRecordingFragment())
  803. }
  804. } else {
  805. supportFragmentManager.commit {
  806. setReorderingAllowed(true)
  807. replace(R.id.fragment_container_activity_chat, MessageInputFragment())
  808. }
  809. }
  810. }
  811. chatViewModel.getVoiceRecordingInProgress.observe(this) { voiceRecordingInProgress ->
  812. VibrationUtils.vibrateShort(context)
  813. binding.voiceRecordingLock.visibility = if (
  814. voiceRecordingInProgress &&
  815. chatViewModel.getVoiceRecordingLocked.value != true
  816. ) {
  817. View.VISIBLE
  818. } else {
  819. View.GONE
  820. }
  821. }
  822. chatViewModel.recordTouchObserver.observe(this) { y ->
  823. binding.voiceRecordingLock.y -= y
  824. }
  825. }
  826. @Suppress("Detekt.TooGenericExceptionCaught")
  827. override fun onResume() {
  828. super.onResume()
  829. logConversationInfos("onResume")
  830. pullChatMessagesPending = false
  831. webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
  832. webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
  833. cancelNotificationsForCurrentConversation()
  834. chatViewModel.getRoom(conversationUser!!, roomToken)
  835. actionBar?.show()
  836. setupSwipeToReply()
  837. binding.unreadMessagesPopup.setRecyclerView(binding.messagesListView)
  838. binding.unreadMessagesPopup.setPopupBubbleListener { _ ->
  839. if (newMessagesCount != 0) {
  840. val scrollPosition = if (newMessagesCount - 1 < 0) {
  841. 0
  842. } else {
  843. newMessagesCount - 1
  844. }
  845. Handler().postDelayed(
  846. {
  847. binding.messagesListView.smoothScrollToPosition(scrollPosition)
  848. },
  849. NEW_MESSAGES_POPUP_BUBBLE_DELAY
  850. )
  851. }
  852. }
  853. binding.scrollDownButton.setOnClickListener {
  854. binding.messagesListView.scrollToPosition(0)
  855. it.visibility = View.GONE
  856. }
  857. binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it.scrollDownButton) }
  858. binding.let { viewThemeUtils.material.themeFAB(it.voiceRecordingLock) }
  859. binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.unreadMessagesPopup) }
  860. binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  861. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  862. super.onScrollStateChanged(recyclerView, newState)
  863. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  864. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() > 0 &&
  865. !binding.unreadMessagesPopup.isShown
  866. ) {
  867. binding.scrollDownButton.visibility = View.VISIBLE
  868. } else {
  869. binding.scrollDownButton.visibility = View.GONE
  870. }
  871. if (newMessagesCount != 0 && layoutManager != null) {
  872. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
  873. newMessagesCount = 0
  874. if (binding.unreadMessagesPopup.isShown) {
  875. binding.unreadMessagesPopup.hide()
  876. }
  877. }
  878. }
  879. }
  880. }
  881. })
  882. loadAvatarForStatusBar()
  883. setActionBarTitle()
  884. viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
  885. }
  886. private fun getLastAdapterId(): Int {
  887. var lastId = 0
  888. if (adapter?.items?.size != 0) {
  889. val item = adapter?.items?.get(0)?.item
  890. if (item != null) {
  891. lastId = (item as ChatMessage).jsonMessageId
  892. } else {
  893. lastId = 0
  894. }
  895. }
  896. return lastId
  897. }
  898. private fun setupActionBar() {
  899. setSupportActionBar(binding.chatToolbar)
  900. binding.chatToolbar.setNavigationOnClickListener {
  901. onBackPressedDispatcher.onBackPressed()
  902. }
  903. supportActionBar?.setDisplayHomeAsUpEnabled(true)
  904. supportActionBar?.setDisplayShowHomeEnabled(true)
  905. supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(R.color.transparent, null)))
  906. setActionBarTitle()
  907. viewThemeUtils.material.themeToolbar(binding.chatToolbar)
  908. }
  909. private fun initAdapter() {
  910. val senderId = if (!conversationUser!!.userId.equals("?")) {
  911. "users/" + conversationUser!!.userId
  912. } else {
  913. currentConversation?.actorType + "/" + currentConversation?.actorId
  914. }
  915. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: $senderId")
  916. adapter = TalkMessagesListAdapter(
  917. senderId,
  918. initMessageHolders(),
  919. ImageLoader { imageView, url, placeholder ->
  920. imageView.loadAvatarOrImagePreview(url!!, conversationUser!!, placeholder as Drawable?)
  921. },
  922. this
  923. )
  924. adapter?.setLoadMoreListener(this)
  925. adapter?.setDateHeadersFormatter { format(it) }
  926. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  927. adapter?.registerViewClickListener(
  928. R.id.playPauseBtn
  929. ) { _, message ->
  930. val filename = message.selectedIndividualHashMap!!["name"]
  931. val file = File(context.cacheDir, filename!!)
  932. if (file.exists()) {
  933. if (message.isPlayingVoiceMessage) {
  934. pausePlayback(message)
  935. } else {
  936. val retrieved = appPreferences.getWaveFormFromFile(filename)
  937. if (retrieved.isEmpty()) {
  938. setUpWaveform(message)
  939. } else {
  940. startPlayback(message)
  941. }
  942. }
  943. } else {
  944. Log.d(TAG, "Downloaded to cache")
  945. downloadFileToCache(message, true) {
  946. setUpWaveform(message)
  947. }
  948. }
  949. }
  950. }
  951. private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true) {
  952. val filename = message.selectedIndividualHashMap!!["name"]
  953. val file = File(context.cacheDir, filename!!)
  954. if (file.exists() && message.voiceMessageFloatArray == null) {
  955. message.isDownloadingVoiceMessage = true
  956. adapter?.update(message)
  957. CoroutineScope(Dispatchers.Default).launch {
  958. val r = AudioUtils.audioFileToFloatArray(file)
  959. appPreferences.saveWaveFormForFile(filename, r.toTypedArray())
  960. message.voiceMessageFloatArray = r
  961. withContext(Dispatchers.Main) {
  962. startPlayback(message, thenPlay)
  963. }
  964. }
  965. } else {
  966. startPlayback(message, thenPlay)
  967. }
  968. }
  969. private fun initMessageHolders(): MessageHolders {
  970. val messageHolders = MessageHolders()
  971. val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, viewThemeUtils)
  972. val payload = MessagePayload(
  973. roomToken,
  974. ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!),
  975. profileBottomSheet
  976. )
  977. messageHolders.setIncomingTextConfig(
  978. IncomingTextMessageViewHolder::class.java,
  979. R.layout.item_custom_incoming_text_message,
  980. payload
  981. )
  982. messageHolders.setOutcomingTextConfig(
  983. OutcomingTextMessageViewHolder::class.java,
  984. R.layout.item_custom_outcoming_text_message
  985. )
  986. messageHolders.setIncomingImageConfig(
  987. IncomingPreviewMessageViewHolder::class.java,
  988. R.layout.item_custom_incoming_preview_message,
  989. payload
  990. )
  991. messageHolders.setOutcomingImageConfig(
  992. OutcomingPreviewMessageViewHolder::class.java,
  993. R.layout.item_custom_outcoming_preview_message
  994. )
  995. messageHolders.registerContentType(
  996. CONTENT_TYPE_CALL_STARTED,
  997. CallStartedViewHolder::class.java,
  998. payload,
  999. R.layout.call_started_message,
  1000. CallStartedViewHolder::class.java,
  1001. payload,
  1002. R.layout.call_started_message,
  1003. this
  1004. )
  1005. messageHolders.registerContentType(
  1006. CONTENT_TYPE_SYSTEM_MESSAGE,
  1007. SystemMessageViewHolder::class.java,
  1008. R.layout.item_system_message,
  1009. SystemMessageViewHolder::class.java,
  1010. R.layout.item_system_message,
  1011. this
  1012. )
  1013. messageHolders.registerContentType(
  1014. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  1015. UnreadNoticeMessageViewHolder::class.java,
  1016. R.layout.item_date_header,
  1017. UnreadNoticeMessageViewHolder::class.java,
  1018. R.layout.item_date_header,
  1019. this
  1020. )
  1021. messageHolders.registerContentType(
  1022. CONTENT_TYPE_LOCATION,
  1023. IncomingLocationMessageViewHolder::class.java,
  1024. payload,
  1025. R.layout.item_custom_incoming_location_message,
  1026. OutcomingLocationMessageViewHolder::class.java,
  1027. null,
  1028. R.layout.item_custom_outcoming_location_message,
  1029. this
  1030. )
  1031. messageHolders.registerContentType(
  1032. CONTENT_TYPE_VOICE_MESSAGE,
  1033. IncomingVoiceMessageViewHolder::class.java,
  1034. payload,
  1035. R.layout.item_custom_incoming_voice_message,
  1036. OutcomingVoiceMessageViewHolder::class.java,
  1037. null,
  1038. R.layout.item_custom_outcoming_voice_message,
  1039. this
  1040. )
  1041. messageHolders.registerContentType(
  1042. CONTENT_TYPE_POLL,
  1043. IncomingPollMessageViewHolder::class.java,
  1044. payload,
  1045. R.layout.item_custom_incoming_poll_message,
  1046. OutcomingPollMessageViewHolder::class.java,
  1047. payload,
  1048. R.layout.item_custom_outcoming_poll_message,
  1049. this
  1050. )
  1051. messageHolders.registerContentType(
  1052. CONTENT_TYPE_LINK_PREVIEW,
  1053. IncomingLinkPreviewMessageViewHolder::class.java,
  1054. payload,
  1055. R.layout.item_custom_incoming_link_preview_message,
  1056. OutcomingLinkPreviewMessageViewHolder::class.java,
  1057. payload,
  1058. R.layout.item_custom_outcoming_link_preview_message,
  1059. this
  1060. )
  1061. return messageHolders
  1062. }
  1063. @Suppress("MagicNumber", "LongMethod")
  1064. private fun updateTypingIndicator() {
  1065. fun ellipsize(text: String): String {
  1066. return DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH)
  1067. }
  1068. val participantNames = ArrayList<String>()
  1069. for (typingParticipant in typingParticipants.values) {
  1070. participantNames.add(typingParticipant.name)
  1071. }
  1072. val typingString: SpannableStringBuilder
  1073. when (typingParticipants.size) {
  1074. 0 -> typingString = SpannableStringBuilder().append(binding.typingIndicator.text)
  1075. // person1 is typing
  1076. 1 -> typingString = SpannableStringBuilder()
  1077. .bold { append(ellipsize(participantNames[0])) }
  1078. .append(WHITESPACE + context.resources?.getString(R.string.typing_is_typing))
  1079. // person1 and person2 are typing
  1080. 2 -> typingString = SpannableStringBuilder()
  1081. .bold { append(ellipsize(participantNames[0])) }
  1082. .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE)
  1083. .bold { append(ellipsize(participantNames[1])) }
  1084. .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing))
  1085. // person1, person2 and person3 are typing
  1086. 3 -> typingString = SpannableStringBuilder()
  1087. .bold { append(ellipsize(participantNames[0])) }
  1088. .append(COMMA)
  1089. .bold { append(ellipsize(participantNames[1])) }
  1090. .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE)
  1091. .bold { append(ellipsize(participantNames[2])) }
  1092. .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing))
  1093. // person1, person2, person3 and 1 other is typing
  1094. 4 -> typingString = SpannableStringBuilder()
  1095. .bold { append(participantNames[0]) }
  1096. .append(COMMA)
  1097. .bold { append(participantNames[1]) }
  1098. .append(COMMA)
  1099. .bold { append(participantNames[2]) }
  1100. .append(WHITESPACE + context.resources?.getString(R.string.typing_1_other))
  1101. // person1, person2, person3 and x others are typing
  1102. else -> {
  1103. val moreTypersAmount = typingParticipants.size - 3
  1104. val othersTyping = context.resources?.getString(R.string.typing_x_others)?.let {
  1105. String.format(it, moreTypersAmount)
  1106. }
  1107. typingString = SpannableStringBuilder()
  1108. .bold { append(participantNames[0]) }
  1109. .append(COMMA)
  1110. .bold { append(participantNames[1]) }
  1111. .append(COMMA)
  1112. .bold { append(participantNames[2]) }
  1113. .append(othersTyping)
  1114. }
  1115. }
  1116. runOnUiThread {
  1117. binding.typingIndicator.text = typingString
  1118. if (participantNames.size > 0) {
  1119. binding.typingIndicatorWrapper.visibility = View.VISIBLE
  1120. binding.typingIndicatorWrapper.animate()
  1121. .translationYBy(DisplayUtils.convertDpToPixel(-18f, context))
  1122. .setInterpolator(AccelerateDecelerateInterpolator())
  1123. .duration = TYPING_INDICATOR_ANIMATION_DURATION
  1124. } else {
  1125. binding.typingIndicatorWrapper.visibility = View.INVISIBLE
  1126. binding.typingIndicatorWrapper.y += DisplayUtils.convertDpToPixel(18f, context)
  1127. }
  1128. }
  1129. }
  1130. private fun isTypingStatusEnabled(): Boolean {
  1131. return webSocketInstance != null &&
  1132. !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!)
  1133. }
  1134. private fun setupSwipeToReply() {
  1135. if (this::participantPermissions.isInitialized &&
  1136. participantPermissions.hasChatPermission() &&
  1137. !isReadOnlyConversation()
  1138. ) {
  1139. val messageSwipeCallback = MessageSwipeCallback(
  1140. this,
  1141. object : MessageSwipeActions {
  1142. override fun showReplyUI(position: Int) {
  1143. val chatMessage = adapter?.items?.getOrNull(position)?.item as ChatMessage?
  1144. if (chatMessage != null) {
  1145. messageInputViewModel.reply(chatMessage)
  1146. }
  1147. }
  1148. }
  1149. )
  1150. val itemTouchHelper = ItemTouchHelper(messageSwipeCallback)
  1151. itemTouchHelper.attachToRecyclerView(binding.messagesListView)
  1152. }
  1153. }
  1154. private fun loadAvatarForStatusBar() {
  1155. if (currentConversation == null) {
  1156. return
  1157. }
  1158. if (isOneToOneConversation()) {
  1159. var url = ApiUtils.getUrlForAvatar(
  1160. conversationUser!!.baseUrl!!,
  1161. currentConversation!!.name,
  1162. true
  1163. )
  1164. if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext!!)) {
  1165. url = "$url/dark"
  1166. }
  1167. val target = object : Target {
  1168. private fun setIcon(drawable: Drawable?) {
  1169. supportActionBar?.let {
  1170. val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt()
  1171. val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context)
  1172. if (drawable != null && avatarSize > 0) {
  1173. val bitmap = drawable.toBitmap(avatarSize, avatarSize)
  1174. val status = StatusDrawable(
  1175. currentConversation!!.status,
  1176. null,
  1177. size,
  1178. 0,
  1179. binding.chatToolbar.context
  1180. )
  1181. viewThemeUtils.talk.themeStatusDrawable(context, status)
  1182. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_avatar)
  1183. .setImageDrawable(BitmapDrawable(resources, bitmap))
  1184. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_status)
  1185. .setImageDrawable(status)
  1186. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_status).contentDescription =
  1187. currentConversation?.status
  1188. binding.chatToolbar.findViewById<FrameLayout>(R.id.chat_toolbar_avatar_container)
  1189. .visibility = View.VISIBLE
  1190. } else {
  1191. Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
  1192. }
  1193. }
  1194. }
  1195. override fun onStart(placeholder: Drawable?) {
  1196. this.setIcon(placeholder)
  1197. }
  1198. override fun onSuccess(result: Drawable) {
  1199. this.setIcon(result)
  1200. }
  1201. }
  1202. val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  1203. if (credentials != null) {
  1204. context.imageLoader.enqueue(
  1205. ImageRequest.Builder(context)
  1206. .data(url)
  1207. .addHeader("Authorization", credentials)
  1208. .transformations(CircleCropTransformation())
  1209. .crossfade(true)
  1210. .target(target)
  1211. .memoryCachePolicy(CachePolicy.DISABLED)
  1212. .diskCachePolicy(CachePolicy.DISABLED)
  1213. .build()
  1214. )
  1215. }
  1216. } else {
  1217. binding.chatToolbar.findViewById<FrameLayout>(R.id.chat_toolbar_avatar_container).visibility = View.GONE
  1218. }
  1219. }
  1220. fun isOneToOneConversation() =
  1221. currentConversation != null && currentConversation?.type != null &&
  1222. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1223. private fun isGroupConversation() =
  1224. currentConversation != null && currentConversation?.type != null &&
  1225. currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL
  1226. private fun isPublicConversation() =
  1227. currentConversation != null && currentConversation?.type != null &&
  1228. currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
  1229. private fun updateRoomTimerHandler() {
  1230. val delayForRecursiveCall = if (shouldShowLobby()) {
  1231. GET_ROOM_INFO_DELAY_LOBBY
  1232. } else {
  1233. GET_ROOM_INFO_DELAY_NORMAL
  1234. }
  1235. if (getRoomInfoTimerHandler == null) {
  1236. getRoomInfoTimerHandler = Handler()
  1237. }
  1238. getRoomInfoTimerHandler?.postDelayed(
  1239. {
  1240. chatViewModel.getRoom(conversationUser!!, roomToken)
  1241. },
  1242. delayForRecursiveCall
  1243. )
  1244. }
  1245. private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
  1246. if (conversationUser != null) {
  1247. runOnUiThread {
  1248. if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
  1249. Snackbar.make(
  1250. binding.root,
  1251. context.resources.getString(R.string.switch_to_main_room),
  1252. Snackbar.LENGTH_LONG
  1253. ).show()
  1254. } else {
  1255. Snackbar.make(
  1256. binding.root,
  1257. context.resources.getString(R.string.switch_to_breakout_room),
  1258. Snackbar.LENGTH_LONG
  1259. ).show()
  1260. }
  1261. }
  1262. val bundle = Bundle()
  1263. bundle.putString(KEY_ROOM_TOKEN, token)
  1264. if (startCallAfterRoomSwitch) {
  1265. bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
  1266. bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
  1267. }
  1268. leaveRoom {
  1269. val chatIntent = Intent(context, ChatActivity::class.java)
  1270. chatIntent.putExtras(bundle)
  1271. chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  1272. startActivity(chatIntent)
  1273. }
  1274. }
  1275. }
  1276. private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
  1277. val anchor: View? = if (isVoiceOnlyCall) {
  1278. findViewById(R.id.conversation_voice_call)
  1279. } else {
  1280. findViewById(R.id.conversation_video_call)
  1281. }
  1282. if (anchor != null) {
  1283. val popupMenu = PopupMenu(
  1284. ContextThemeWrapper(this, R.style.CallButtonMenu),
  1285. anchor,
  1286. Gravity.END
  1287. )
  1288. popupMenu.inflate(R.menu.chat_call_menu)
  1289. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  1290. when (item.itemId) {
  1291. R.id.call_without_notification -> startACall(isVoiceOnlyCall, true)
  1292. }
  1293. true
  1294. }
  1295. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  1296. popupMenu.setForceShowIcon(true)
  1297. }
  1298. popupMenu.show()
  1299. }
  1300. }
  1301. private fun startPlayback(message: ChatMessage, doPlay: Boolean = true) {
  1302. if (!active) {
  1303. // don't begin to play voice message if screen is not visible anymore.
  1304. // this situation might happen if file is downloading but user already left the chatview.
  1305. // If user returns to chatview, the old chatview instance is not attached anymore
  1306. // and he has to click the play button again (which is considered to be okay)
  1307. return
  1308. }
  1309. initMediaPlayer(message)
  1310. mediaPlayer?.let {
  1311. if (!it.isPlaying && doPlay) {
  1312. chatViewModel.audioRequest(true) {
  1313. it.start()
  1314. }
  1315. }
  1316. mediaPlayerHandler = Handler()
  1317. runOnUiThread(object : Runnable {
  1318. override fun run() {
  1319. if (mediaPlayer != null) {
  1320. if (message.isPlayingVoiceMessage) {
  1321. val pos = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
  1322. if (pos < (mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE)) {
  1323. lastRecordMediaPosition = mediaPlayer!!.currentPosition
  1324. message.voiceMessagePlayedSeconds = pos
  1325. message.voiceMessageSeekbarProgress = mediaPlayer!!.currentPosition
  1326. adapter?.update(message)
  1327. } else {
  1328. message.resetVoiceMessage = true
  1329. message.voiceMessagePlayedSeconds = 0
  1330. message.voiceMessageSeekbarProgress = 0
  1331. adapter?.update(message)
  1332. stopMediaPlayer(message)
  1333. }
  1334. }
  1335. }
  1336. mediaPlayerHandler?.postDelayed(this, MILISEC_15)
  1337. }
  1338. })
  1339. message.isDownloadingVoiceMessage = false
  1340. message.isPlayingVoiceMessage = doPlay
  1341. // message.voiceMessagePlayedSeconds = lastRecordMediaPosition / VOICE_MESSAGE_SEEKBAR_BASE
  1342. // message.voiceMessageSeekbarProgress = lastRecordMediaPosition
  1343. // the commented instructions objective was to update audio seekbarprogress
  1344. // in the case in which audio status is paused when the position is resumed
  1345. adapter?.update(message)
  1346. }
  1347. }
  1348. private fun pausePlayback(message: ChatMessage) {
  1349. if (mediaPlayer!!.isPlaying) {
  1350. chatViewModel.audioRequest(false) {
  1351. mediaPlayer!!.pause()
  1352. }
  1353. }
  1354. message.isPlayingVoiceMessage = false
  1355. adapter?.update(message)
  1356. }
  1357. @Suppress("Detekt.TooGenericExceptionCaught")
  1358. private fun initMediaPlayer(message: ChatMessage) {
  1359. if (message != currentlyPlayedVoiceMessage) {
  1360. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  1361. }
  1362. if (mediaPlayer == null) {
  1363. val fileName = message.selectedIndividualHashMap!!["name"]
  1364. val absolutePath = context.cacheDir.absolutePath + "/" + fileName
  1365. try {
  1366. mediaPlayer = MediaPlayer().apply {
  1367. setDataSource(absolutePath)
  1368. prepare()
  1369. setOnPreparedListener {
  1370. currentlyPlayedVoiceMessage = message
  1371. message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
  1372. lastRecordedSeeked = false
  1373. }
  1374. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  1375. setOnMediaTimeDiscontinuityListener { mp, _ ->
  1376. if (lastRecordMediaPosition > ONE_SECOND_IN_MILLIS && !lastRecordedSeeked) {
  1377. mp.seekTo(lastRecordMediaPosition)
  1378. lastRecordedSeeked = true
  1379. }
  1380. }
  1381. // this ensures that audio can be resumed at a given position
  1382. this.seekTo(lastRecordMediaPosition)
  1383. }
  1384. setOnCompletionListener {
  1385. stopMediaPlayer(message)
  1386. }
  1387. }
  1388. } catch (e: Exception) {
  1389. Log.e(TAG, "failed to initialize mediaPlayer", e)
  1390. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  1391. }
  1392. }
  1393. }
  1394. private fun stopMediaPlayer(message: ChatMessage) {
  1395. message.isPlayingVoiceMessage = false
  1396. message.resetVoiceMessage = true
  1397. adapter?.update(message)
  1398. currentlyPlayedVoiceMessage = null
  1399. lastRecordMediaPosition = 0 // this ensures that if audio track is changed, then it is played from the beginning
  1400. mediaPlayerHandler?.removeCallbacksAndMessages(null)
  1401. try {
  1402. mediaPlayer?.let {
  1403. if (it.isPlaying) {
  1404. Log.d(TAG, "media player is stopped")
  1405. chatViewModel.audioRequest(false) {
  1406. it.stop()
  1407. }
  1408. }
  1409. }
  1410. } catch (e: IllegalStateException) {
  1411. Log.e(TAG, "mediaPlayer was not initialized", e)
  1412. } finally {
  1413. mediaPlayer?.release()
  1414. mediaPlayer = null
  1415. }
  1416. }
  1417. override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
  1418. if (mediaPlayer != null) {
  1419. if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
  1420. mediaPlayer!!.seekTo(progress)
  1421. }
  1422. }
  1423. }
  1424. @SuppressLint("NotifyDataSetChanged")
  1425. override fun collapseSystemMessages() {
  1426. adapter?.items?.forEach {
  1427. if (it.item is ChatMessage) {
  1428. val chatMessage = it.item as ChatMessage
  1429. if (isChildOfExpandableSystemMessage(chatMessage)) {
  1430. chatMessage.hiddenByCollapse = true
  1431. }
  1432. chatMessage.isExpanded = false
  1433. }
  1434. }
  1435. adapter?.notifyDataSetChanged()
  1436. }
  1437. private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean {
  1438. return isSystemMessage(chatMessage) &&
  1439. !chatMessage.expandableParent &&
  1440. chatMessage.lastItemOfExpandableGroup != 0
  1441. }
  1442. @SuppressLint("NotifyDataSetChanged")
  1443. override fun expandSystemMessage(chatMessageToExpand: ChatMessage) {
  1444. adapter?.items?.forEach {
  1445. if (it.item is ChatMessage) {
  1446. val belongsToGroupToExpand =
  1447. (it.item as ChatMessage).lastItemOfExpandableGroup == chatMessageToExpand.lastItemOfExpandableGroup
  1448. if (belongsToGroupToExpand) {
  1449. (it.item as ChatMessage).hiddenByCollapse = false
  1450. }
  1451. }
  1452. }
  1453. chatMessageToExpand.isExpanded = true
  1454. adapter?.notifyDataSetChanged()
  1455. }
  1456. @SuppressLint("LongLogTag")
  1457. private fun downloadFileToCache(
  1458. message: ChatMessage,
  1459. openWhenDownloaded: Boolean,
  1460. funToCallWhenDownloadSuccessful: (() -> Unit)
  1461. ) {
  1462. message.isDownloadingVoiceMessage = true
  1463. message.openWhenDownloaded = openWhenDownloaded
  1464. adapter?.update(message)
  1465. val baseUrl = message.activeUser!!.baseUrl
  1466. val userId = message.activeUser!!.userId
  1467. val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(
  1468. message.activeUser!!.capabilities!!
  1469. .spreedCapability!!
  1470. )
  1471. val fileName = message.selectedIndividualHashMap!!["name"]
  1472. var size = message.selectedIndividualHashMap!!["size"]
  1473. if (size == null) {
  1474. size = "-1"
  1475. }
  1476. val fileSize = size.toLong()
  1477. val fileId = message.selectedIndividualHashMap!!["id"]
  1478. val path = message.selectedIndividualHashMap!!["path"]
  1479. // check if download worker is already running
  1480. val workers = WorkManager.getInstance(
  1481. context
  1482. ).getWorkInfosByTag(fileId!!)
  1483. try {
  1484. for (workInfo in workers.get()) {
  1485. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  1486. Log.d(TAG, "Download worker for $fileId is already running or scheduled")
  1487. return
  1488. }
  1489. }
  1490. } catch (e: ExecutionException) {
  1491. Log.e(TAG, "Error when checking if worker already exists", e)
  1492. } catch (e: InterruptedException) {
  1493. Log.e(TAG, "Error when checking if worker already exists", e)
  1494. }
  1495. val data: Data = Data.Builder()
  1496. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
  1497. .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
  1498. .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
  1499. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
  1500. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  1501. .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
  1502. .build()
  1503. val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  1504. .setInputData(data)
  1505. .addTag(fileId)
  1506. .build()
  1507. WorkManager.getInstance().enqueue(downloadWorker)
  1508. WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
  1509. .observeForever { workInfo: WorkInfo ->
  1510. if (workInfo.state == WorkInfo.State.SUCCEEDED) {
  1511. funToCallWhenDownloadSuccessful()
  1512. }
  1513. }
  1514. }
  1515. fun isRecordAudioPermissionGranted(): Boolean {
  1516. return PermissionChecker.checkSelfPermission(
  1517. context,
  1518. Manifest.permission.RECORD_AUDIO
  1519. ) == PERMISSION_GRANTED
  1520. }
  1521. fun requestRecordAudioPermissions() {
  1522. requestPermissions(
  1523. arrayOf(
  1524. Manifest.permission.RECORD_AUDIO
  1525. ),
  1526. REQUEST_RECORD_AUDIO_PERMISSION
  1527. )
  1528. }
  1529. private fun requestCameraPermissions() {
  1530. requestPermissions(
  1531. arrayOf(
  1532. Manifest.permission.CAMERA
  1533. ),
  1534. REQUEST_CAMERA_PERMISSION
  1535. )
  1536. }
  1537. private fun requestReadContacts() {
  1538. requestPermissions(
  1539. arrayOf(
  1540. Manifest.permission.READ_CONTACTS
  1541. ),
  1542. REQUEST_READ_CONTACT_PERMISSION
  1543. )
  1544. }
  1545. private fun requestReadFilesPermissions() {
  1546. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  1547. requestPermissions(
  1548. arrayOf(
  1549. Manifest.permission.READ_MEDIA_IMAGES,
  1550. Manifest.permission.READ_MEDIA_VIDEO,
  1551. Manifest.permission.READ_MEDIA_AUDIO
  1552. ),
  1553. REQUEST_SHARE_FILE_PERMISSION
  1554. )
  1555. } else {
  1556. requestPermissions(
  1557. arrayOf(
  1558. Manifest.permission.READ_EXTERNAL_STORAGE
  1559. ),
  1560. REQUEST_SHARE_FILE_PERMISSION
  1561. )
  1562. }
  1563. }
  1564. private fun checkShowCallButtons() {
  1565. if (isReadOnlyConversation() ||
  1566. shouldShowLobby() ||
  1567. ConversationUtils.isNoteToSelfConversation(currentConversation)
  1568. ) {
  1569. disableCallButtons()
  1570. } else {
  1571. enableCallButtons()
  1572. }
  1573. }
  1574. private fun checkShowMessageInputView() {
  1575. if (isReadOnlyConversation() ||
  1576. shouldShowLobby() ||
  1577. !participantPermissions.hasChatPermission()
  1578. ) {
  1579. binding.fragmentContainerActivityChat.visibility = View.GONE
  1580. } else {
  1581. binding.fragmentContainerActivityChat.visibility = View.VISIBLE
  1582. }
  1583. }
  1584. private fun shouldShowLobby(): Boolean {
  1585. if (currentConversation != null) {
  1586. return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
  1587. currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
  1588. !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
  1589. !participantPermissions.canIgnoreLobby()
  1590. }
  1591. return false
  1592. }
  1593. private fun disableCallButtons() {
  1594. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  1595. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1596. conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1597. conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1598. conversationVoiceCallMenuItem?.isEnabled = false
  1599. conversationVideoMenuItem?.isEnabled = false
  1600. } else {
  1601. Log.e(TAG, "call buttons were null when trying to disable them")
  1602. }
  1603. }
  1604. }
  1605. private fun enableCallButtons() {
  1606. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  1607. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1608. conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1609. conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1610. conversationVoiceCallMenuItem?.isEnabled = true
  1611. conversationVideoMenuItem?.isEnabled = true
  1612. } else {
  1613. Log.e(TAG, "call buttons were null when trying to enable them")
  1614. }
  1615. }
  1616. }
  1617. private fun isReadOnlyConversation(): Boolean {
  1618. return currentConversation?.conversationReadOnlyState != null &&
  1619. currentConversation?.conversationReadOnlyState ==
  1620. ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  1621. }
  1622. private fun checkLobbyState() {
  1623. if (currentConversation != null &&
  1624. ConversationUtils.isLobbyViewApplicable(currentConversation!!, spreedCapabilities)
  1625. ) {
  1626. if (shouldShowLobby()) {
  1627. binding.lobby.lobbyView.visibility = View.VISIBLE
  1628. binding.messagesListView.visibility = View.GONE
  1629. binding.fragmentContainerActivityChat.visibility = View.GONE
  1630. binding.progressBar.visibility = View.GONE
  1631. val sb = StringBuilder()
  1632. sb.append(resources!!.getText(R.string.nc_lobby_waiting))
  1633. .append("\n\n")
  1634. if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer !=
  1635. 0L
  1636. ) {
  1637. val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER
  1638. val stringWithStartDate = String.format(
  1639. resources!!.getString(R.string.nc_lobby_start_date),
  1640. dateUtils.getLocalDateTimeStringFromTimestamp(timestampMS)
  1641. )
  1642. val relativeTime = dateUtils.relativeStartTimeForLobby(timestampMS, resources!!)
  1643. sb.append("$stringWithStartDate - $relativeTime")
  1644. .append("\n\n")
  1645. }
  1646. sb.append(currentConversation!!.description)
  1647. binding.lobby.lobbyTextView.text = sb.toString()
  1648. } else {
  1649. binding.lobby.lobbyView.visibility = View.GONE
  1650. binding.messagesListView.visibility = View.VISIBLE
  1651. checkShowMessageInputView()
  1652. }
  1653. } else {
  1654. binding.lobby.lobbyView.visibility = View.GONE
  1655. binding.messagesListView.visibility = View.VISIBLE
  1656. checkShowMessageInputView()
  1657. }
  1658. }
  1659. private fun onRemoteFileBrowsingResult(intent: Intent?) {
  1660. val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
  1661. if (pathList?.size!! >= 1) {
  1662. pathList
  1663. .chunked(CHUNK_SIZE)
  1664. .forEach { paths ->
  1665. val data = Data.Builder()
  1666. .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id!!)
  1667. .putString(KEY_ROOM_TOKEN, roomToken)
  1668. .putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
  1669. .build()
  1670. val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
  1671. .setInputData(data)
  1672. .build()
  1673. WorkManager.getInstance().enqueue(worker)
  1674. }
  1675. }
  1676. }
  1677. @Throws(IllegalStateException::class)
  1678. private fun onChooseFileResult(intent: Intent?) {
  1679. try {
  1680. checkNotNull(intent)
  1681. filesToUpload.clear()
  1682. intent.clipData?.let {
  1683. for (index in 0 until it.itemCount) {
  1684. filesToUpload.add(it.getItemAt(index).uri.toString())
  1685. }
  1686. } ?: run {
  1687. checkNotNull(intent.data)
  1688. intent.data.let {
  1689. filesToUpload.add(intent.data.toString())
  1690. }
  1691. }
  1692. require(filesToUpload.isNotEmpty())
  1693. val filenamesWithLineBreaks = StringBuilder("\n")
  1694. for (file in filesToUpload) {
  1695. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1696. filenamesWithLineBreaks.append(filename).append("\n")
  1697. }
  1698. val newFragment = FileAttachmentPreviewFragment.newInstance(
  1699. filenamesWithLineBreaks.toString(),
  1700. filesToUpload
  1701. )
  1702. newFragment.setListener { files, caption ->
  1703. uploadFiles(files, caption)
  1704. }
  1705. newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
  1706. } catch (e: IllegalStateException) {
  1707. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1708. Snackbar.make(
  1709. binding.root,
  1710. it,
  1711. Snackbar.LENGTH_LONG
  1712. ).show()
  1713. }
  1714. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1715. } catch (e: IllegalArgumentException) {
  1716. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1717. Snackbar.make(
  1718. binding.root,
  1719. it,
  1720. Snackbar.LENGTH_LONG
  1721. ).show()
  1722. }
  1723. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1724. }
  1725. }
  1726. private fun onSelectContactResult(intent: Intent?) {
  1727. val contactUri = intent?.data ?: return
  1728. val cursor: Cursor? = contentResolver!!.query(contactUri, null, null, null, null)
  1729. if (cursor != null && cursor.moveToFirst()) {
  1730. val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
  1731. val fileName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + ".vcf"
  1732. val file = File(context.cacheDir, fileName)
  1733. writeContactToVcfFile(cursor, file)
  1734. val shareUri = FileProvider.getUriForFile(
  1735. this,
  1736. BuildConfig.APPLICATION_ID,
  1737. File(file.absolutePath)
  1738. )
  1739. uploadFile(shareUri.toString(), false)
  1740. }
  1741. cursor?.close()
  1742. }
  1743. @Throws(IllegalStateException::class)
  1744. private fun onPickCameraResult(intent: Intent?) {
  1745. try {
  1746. filesToUpload.clear()
  1747. if (intent != null && intent.data != null) {
  1748. run {
  1749. intent.data.let {
  1750. filesToUpload.add(intent.data.toString())
  1751. }
  1752. }
  1753. require(filesToUpload.isNotEmpty())
  1754. } else if (videoURI != null) {
  1755. filesToUpload.add(videoURI.toString())
  1756. videoURI = null
  1757. } else {
  1758. error("Failed to get data from intent and uri")
  1759. }
  1760. if (permissionUtil.isFilesPermissionGranted()) {
  1761. val filenamesWithLineBreaks = StringBuilder("\n")
  1762. for (file in filesToUpload) {
  1763. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1764. filenamesWithLineBreaks.append(filename).append("\n")
  1765. }
  1766. val newFragment = FileAttachmentPreviewFragment.newInstance(
  1767. filenamesWithLineBreaks.toString(),
  1768. filesToUpload
  1769. )
  1770. newFragment.setListener { files, caption -> uploadFiles(files, caption) }
  1771. newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
  1772. } else {
  1773. UploadAndShareFilesWorker.requestStoragePermission(this)
  1774. }
  1775. } catch (e: IllegalStateException) {
  1776. Snackbar.make(
  1777. binding.root,
  1778. R.string.nc_upload_failed,
  1779. Snackbar.LENGTH_LONG
  1780. )
  1781. .show()
  1782. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1783. } catch (e: IllegalArgumentException) {
  1784. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1785. Snackbar.make(
  1786. binding.root,
  1787. it,
  1788. Snackbar.LENGTH_LONG
  1789. )
  1790. .show()
  1791. }
  1792. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1793. }
  1794. }
  1795. private fun onMessageSearchResult(intent: Intent?) {
  1796. val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
  1797. messageId?.let { id ->
  1798. scrollToMessageWithId(id)
  1799. }
  1800. }
  1801. private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
  1802. if (result.resultCode == Activity.RESULT_OK) {
  1803. onResult(result.data)
  1804. } else {
  1805. Log.e(TAG, "resultCode for received intent was != ok")
  1806. }
  1807. }
  1808. private fun scrollToMessageWithId(messageId: String) {
  1809. val position = adapter?.items?.indexOfFirst {
  1810. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  1811. }
  1812. if (position != null && position >= 0) {
  1813. binding.messagesListView.scrollToPosition(position)
  1814. } else {
  1815. // TODO show error that we don't have that message?
  1816. }
  1817. }
  1818. private fun scrollToAndCenterMessageWithId(messageId: String) {
  1819. adapter?.let {
  1820. val position = it.getMessagePositionByIdInReverse(messageId)
  1821. if (position != -1) {
  1822. layoutManager?.scrollToPositionWithOffset(
  1823. position,
  1824. binding.messagesListView.height / 2
  1825. )
  1826. }
  1827. }
  1828. }
  1829. private fun writeContactToVcfFile(cursor: Cursor, file: File) {
  1830. val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
  1831. val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
  1832. val fd: AssetFileDescriptor = contentResolver!!.openAssetFileDescriptor(uri, "r")!!
  1833. fd.use {
  1834. val fis = fd.createInputStream()
  1835. file.createNewFile()
  1836. fis.use { input ->
  1837. file.outputStream().use { output ->
  1838. input.copyTo(output)
  1839. }
  1840. }
  1841. }
  1842. }
  1843. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  1844. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  1845. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
  1846. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1847. Log.d(TAG, "upload starting after permissions were granted")
  1848. if (filesToUpload.isNotEmpty()) {
  1849. uploadFiles(filesToUpload)
  1850. }
  1851. } else {
  1852. Snackbar
  1853. .make(binding.root, context.getString(R.string.read_storage_no_permission), Snackbar.LENGTH_LONG)
  1854. .show()
  1855. }
  1856. } else if (requestCode == REQUEST_SHARE_FILE_PERMISSION) {
  1857. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1858. showLocalFilePicker()
  1859. } else {
  1860. Snackbar.make(
  1861. binding.root,
  1862. context.getString(R.string.nc_file_storage_permission),
  1863. Snackbar.LENGTH_LONG
  1864. ).show()
  1865. }
  1866. } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  1867. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1868. // do nothing. user will tap on the microphone again if he wants to record audio..
  1869. } else {
  1870. Snackbar.make(
  1871. binding.root,
  1872. context.getString(R.string.nc_voice_message_missing_audio_permission),
  1873. Snackbar.LENGTH_LONG
  1874. ).show()
  1875. }
  1876. } else if (requestCode == REQUEST_READ_CONTACT_PERMISSION) {
  1877. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1878. val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
  1879. startSelectContactForResult.launch(intent)
  1880. } else {
  1881. Snackbar.make(
  1882. binding.root,
  1883. context.getString(R.string.nc_share_contact_permission),
  1884. Snackbar.LENGTH_LONG
  1885. ).show()
  1886. }
  1887. } else if (requestCode == REQUEST_CAMERA_PERMISSION) {
  1888. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1889. Snackbar
  1890. .make(binding.root, context.getString(R.string.camera_permission_granted), Snackbar.LENGTH_LONG)
  1891. .show()
  1892. } else {
  1893. Snackbar
  1894. .make(binding.root, context.getString(R.string.take_photo_permission), Snackbar.LENGTH_LONG)
  1895. .show()
  1896. }
  1897. }
  1898. }
  1899. private fun uploadFiles(files: MutableList<String>, caption: String = "") {
  1900. for (i in 0 until files.size) {
  1901. if (i == files.size - 1) {
  1902. uploadFile(files[i], false, caption)
  1903. } else {
  1904. uploadFile(files[i], false)
  1905. }
  1906. }
  1907. }
  1908. private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") {
  1909. var metaData = ""
  1910. var room = ""
  1911. if (!participantPermissions.hasChatPermission()) {
  1912. Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
  1913. return
  1914. }
  1915. if (isVoiceMessage) {
  1916. metaData = VOICE_MESSAGE_META_DATA
  1917. }
  1918. if (caption != "") {
  1919. metaData = "{\"caption\":\"$caption\"}"
  1920. }
  1921. if (token == "") room = roomToken else room = token
  1922. chatViewModel.uploadFile(fileUri, room, currentConversation?.displayName!!, metaData)
  1923. }
  1924. private fun showLocalFilePicker() {
  1925. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  1926. type = "*/*"
  1927. addCategory(Intent.CATEGORY_OPENABLE)
  1928. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  1929. }
  1930. startChooseFileIntentForResult.launch(
  1931. Intent.createChooser(
  1932. action,
  1933. context.resources?.getString(
  1934. R.string.nc_upload_choose_local_files
  1935. )
  1936. )
  1937. )
  1938. }
  1939. fun sendSelectLocalFileIntent() {
  1940. if (!permissionUtil.isFilesPermissionGranted()) {
  1941. requestReadFilesPermissions()
  1942. } else {
  1943. showLocalFilePicker()
  1944. }
  1945. }
  1946. fun sendChooseContactIntent() {
  1947. requestReadContacts()
  1948. }
  1949. fun showBrowserScreen() {
  1950. val sharingFileBrowserIntent = Intent(this, RemoteFileBrowserActivity::class.java)
  1951. startRemoteFileBrowsingForResult.launch(sharingFileBrowserIntent)
  1952. }
  1953. fun showShareLocationScreen() {
  1954. Log.d(TAG, "showShareLocationScreen")
  1955. val intent = Intent(this, LocationPickerActivity::class.java)
  1956. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  1957. intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
  1958. startActivity(intent)
  1959. }
  1960. private fun showConversationInfoScreen() {
  1961. val bundle = Bundle()
  1962. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1963. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
  1964. val intent = Intent(this, ConversationInfoActivity::class.java)
  1965. intent.putExtras(bundle)
  1966. startActivity(intent)
  1967. }
  1968. private fun validSessionId(): Boolean {
  1969. return currentConversation != null &&
  1970. sessionIdAfterRoomJoined?.isNotEmpty() == true &&
  1971. sessionIdAfterRoomJoined != "0"
  1972. }
  1973. @Suppress("Detekt.TooGenericExceptionCaught")
  1974. private fun cancelNotificationsForCurrentConversation() {
  1975. if (conversationUser != null) {
  1976. if (!TextUtils.isEmpty(roomToken)) {
  1977. try {
  1978. NotificationUtils.cancelExistingNotificationsForRoom(
  1979. applicationContext,
  1980. conversationUser!!,
  1981. roomToken
  1982. )
  1983. } catch (e: RuntimeException) {
  1984. Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
  1985. }
  1986. }
  1987. }
  1988. }
  1989. override fun onPause() {
  1990. super.onPause()
  1991. logConversationInfos("onPause")
  1992. eventBus.unregister(this)
  1993. webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener)
  1994. webSocketInstance?.getSignalingMessageReceiver()?.removeListener(conversationMessageListener)
  1995. findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1996. checkingLobbyStatus = false
  1997. if (getRoomInfoTimerHandler != null) {
  1998. getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
  1999. }
  2000. if (conversationUser != null && isActivityNotChangingConfigurations() && isNotInCall()) {
  2001. ApplicationWideCurrentRoomHolder.getInstance().clear()
  2002. if (validSessionId()) {
  2003. leaveRoom(null)
  2004. } else {
  2005. Log.d(TAG, "not leaving room (validSessionId is false)")
  2006. }
  2007. } else {
  2008. Log.d(TAG, "not leaving room...")
  2009. }
  2010. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  2011. mentionAutocomplete?.dismissPopup()
  2012. }
  2013. }
  2014. private fun isActivityNotChangingConfigurations(): Boolean {
  2015. return !isChangingConfigurations
  2016. }
  2017. private fun isNotInCall(): Boolean {
  2018. return !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
  2019. !ApplicationWideCurrentRoomHolder.getInstance().isDialing
  2020. }
  2021. private fun setActionBarTitle() {
  2022. val title = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_title)
  2023. viewThemeUtils.platform.colorTextView(title, ColorRole.ON_SURFACE)
  2024. title.text =
  2025. if (currentConversation?.displayName != null) {
  2026. try {
  2027. EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  2028. } catch (e: java.lang.IllegalStateException) {
  2029. currentConversation?.displayName
  2030. error(e)
  2031. }
  2032. } else {
  2033. ""
  2034. }
  2035. if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
  2036. var statusMessage = ""
  2037. if (currentConversation?.statusIcon != null) {
  2038. statusMessage += currentConversation?.statusIcon
  2039. }
  2040. if (currentConversation?.statusMessage != null) {
  2041. statusMessage += currentConversation?.statusMessage
  2042. }
  2043. statusMessageViewContents(statusMessage)
  2044. } else {
  2045. if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL ||
  2046. currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
  2047. ) {
  2048. var descriptionMessage = ""
  2049. descriptionMessage += currentConversation?.description
  2050. statusMessageViewContents(descriptionMessage)
  2051. }
  2052. }
  2053. }
  2054. private fun statusMessageViewContents(statusMessageContent: String) {
  2055. val statusMessageView = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_status_message)
  2056. if (statusMessageContent.isNotEmpty()) {
  2057. viewThemeUtils.platform.colorTextView(statusMessageView, ColorRole.ON_SURFACE)
  2058. statusMessageView.text = statusMessageContent
  2059. statusMessageView.visibility = View.VISIBLE
  2060. } else {
  2061. statusMessageView.visibility = View.GONE
  2062. }
  2063. }
  2064. public override fun onDestroy() {
  2065. super.onDestroy()
  2066. logConversationInfos("onDestroy")
  2067. findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  2068. if (actionBar != null) {
  2069. actionBar?.setIcon(null)
  2070. }
  2071. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) } // FIXME, mediaplayer can sometimes be null here
  2072. adapter = null
  2073. Log.d(TAG, "inConversation was set to false!")
  2074. }
  2075. private fun joinRoomWithPassword() {
  2076. // if ApplicationWideCurrentRoomHolder contains a session (because a call is active), then keep the sessionId
  2077. if (ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken ==
  2078. currentConversation!!.token
  2079. ) {
  2080. sessionIdAfterRoomJoined = ApplicationWideCurrentRoomHolder.getInstance().session
  2081. // ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  2082. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
  2083. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  2084. }
  2085. if (!validSessionId()) {
  2086. Log.d(TAG, "sessionID was not valid -> joinRoom")
  2087. val startNanoTime = System.nanoTime()
  2088. Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime")
  2089. chatViewModel.joinRoom(conversationUser!!, roomToken, roomPassword)
  2090. } else {
  2091. Log.d(TAG, "sessionID was valid -> skip joinRoom")
  2092. setupWebsocket()
  2093. if (webSocketInstance != null) {
  2094. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  2095. roomToken,
  2096. sessionIdAfterRoomJoined,
  2097. externalSignalingServer?.federation
  2098. )
  2099. }
  2100. }
  2101. }
  2102. fun leaveRoom(funToCallWhenLeaveSuccessful: (() -> Unit)?) {
  2103. logConversationInfos("leaveRoom")
  2104. var apiVersion = 1
  2105. // FIXME Fix API checking with guests?
  2106. if (conversationUser != null) {
  2107. apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  2108. }
  2109. val startNanoTime = System.nanoTime()
  2110. Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime")
  2111. chatViewModel.leaveRoom(
  2112. credentials!!,
  2113. ApiUtils.getUrlForParticipantsActive(
  2114. apiVersion,
  2115. conversationUser?.baseUrl!!,
  2116. roomToken
  2117. ),
  2118. funToCallWhenLeaveSuccessful
  2119. )
  2120. }
  2121. private fun setupWebsocket() {
  2122. if (currentConversation == null || conversationUser == null) {
  2123. return
  2124. }
  2125. if (currentConversation!!.remoteServer != null) {
  2126. val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1))
  2127. ncApi!!.getSignalingSettings(
  2128. credentials,
  2129. ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken!!)
  2130. ).blockingSubscribe(object : Observer<SignalingSettingsOverall> {
  2131. override fun onSubscribe(d: Disposable) {
  2132. // unused atm
  2133. }
  2134. override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) {
  2135. if (signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer == null ||
  2136. signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer?.isEmpty() == true
  2137. ) {
  2138. return
  2139. }
  2140. externalSignalingServer = ExternalSignalingServer()
  2141. externalSignalingServer!!.externalSignalingServer = signalingSettingsOverall.ocs!!.settings!!
  2142. .externalSignalingServer
  2143. externalSignalingServer!!.externalSignalingTicket = signalingSettingsOverall.ocs!!.settings!!
  2144. .externalSignalingTicket
  2145. externalSignalingServer!!.federation = signalingSettingsOverall.ocs!!.settings!!.federation
  2146. webSocketInstance = WebSocketConnectionHelper.getExternalSignalingInstanceForServer(
  2147. externalSignalingServer!!.externalSignalingServer,
  2148. conversationUser,
  2149. externalSignalingServer!!.externalSignalingTicket,
  2150. TextUtils.isEmpty(credentials)
  2151. )
  2152. }
  2153. override fun onError(e: Throwable) {
  2154. Log.e(CallActivity.TAG, e.message, e)
  2155. }
  2156. override fun onComplete() {
  2157. // unused atm
  2158. }
  2159. })
  2160. } else {
  2161. webSocketInstance = WebSocketConnectionHelper.getWebSocketInstanceForUser(conversationUser!!)
  2162. }
  2163. if (webSocketInstance == null) {
  2164. Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB")
  2165. }
  2166. signalingMessageSender = webSocketInstance?.signalingMessageSender
  2167. webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
  2168. webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
  2169. }
  2170. private fun processCallStartedMessages(chatMessageList: List<ChatMessage>) {
  2171. try {
  2172. val mostRecentCallSystemMessage = adapter?.items?.first {
  2173. it.item is ChatMessage &&
  2174. (it.item as ChatMessage).systemMessageType in
  2175. listOf(
  2176. ChatMessage.SystemMessageType.CALL_STARTED,
  2177. ChatMessage.SystemMessageType.CALL_JOINED,
  2178. ChatMessage.SystemMessageType.CALL_LEFT,
  2179. ChatMessage.SystemMessageType.CALL_ENDED,
  2180. ChatMessage.SystemMessageType.CALL_TRIED,
  2181. ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE,
  2182. ChatMessage.SystemMessageType.CALL_MISSED
  2183. )
  2184. }?.item
  2185. if (mostRecentCallSystemMessage != null) {
  2186. processMostRecentMessage(
  2187. mostRecentCallSystemMessage as ChatMessage,
  2188. chatMessageList
  2189. )
  2190. }
  2191. } catch (e: NoSuchElementException) {
  2192. Log.d(TAG, "No System messages found $e")
  2193. }
  2194. }
  2195. private fun processExpiredMessages() {
  2196. @SuppressLint("NotifyDataSetChanged")
  2197. fun deleteExpiredMessages() {
  2198. val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
  2199. val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
  2200. if (adapter?.items != null) {
  2201. for (itemWrapper in adapter?.items!!) {
  2202. if (itemWrapper.item is ChatMessage) {
  2203. val chatMessage = itemWrapper.item as ChatMessage
  2204. if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
  2205. messagesToDelete.add(chatMessage)
  2206. }
  2207. }
  2208. }
  2209. adapter!!.delete(messagesToDelete)
  2210. adapter!!.notifyDataSetChanged()
  2211. }
  2212. }
  2213. if (this::spreedCapabilities.isInitialized) {
  2214. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
  2215. deleteExpiredMessages()
  2216. }
  2217. } else {
  2218. Log.w(TAG, "spreedCapabilities are not initialized in processExpiredMessages()")
  2219. }
  2220. }
  2221. private fun updateReadStatusOfAllMessages(xChatLastCommonRead: Int?) {
  2222. if (adapter != null) {
  2223. for (message in adapter!!.items) {
  2224. xChatLastCommonRead?.let {
  2225. updateReadStatusOfMessage(message, it)
  2226. }
  2227. }
  2228. adapter!!.notifyDataSetChanged()
  2229. }
  2230. }
  2231. private fun updateReadStatusOfMessage(
  2232. message: MessagesListAdapter<IMessage>.Wrapper<Any>,
  2233. xChatLastCommonRead: Int
  2234. ) {
  2235. if (message.item is ChatMessage) {
  2236. val chatMessage = message.item as ChatMessage
  2237. if (chatMessage.jsonMessageId <= xChatLastCommonRead) {
  2238. chatMessage.readStatus = ReadStatus.READ
  2239. } else {
  2240. chatMessage.readStatus = ReadStatus.SENT
  2241. }
  2242. }
  2243. }
  2244. private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>) {
  2245. val newMessagesAvailable = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty()
  2246. val insertNewMessagesNotice = shouldInsertNewMessagesNotice(newMessagesAvailable, chatMessageList)
  2247. val scrollToEndOnUpdate = isScrolledToBottom()
  2248. if (insertNewMessagesNotice) {
  2249. updateUnreadMessageInfos(chatMessageList, scrollToEndOnUpdate)
  2250. }
  2251. for (chatMessage in chatMessageList) {
  2252. chatMessage.activeUser = conversationUser
  2253. adapter?.let {
  2254. chatMessage.isGrouped = (
  2255. it.isPreviousSameAuthor(chatMessage.actorId, -1) &&
  2256. it.getSameAuthorLastMessagesCount(chatMessage.actorId) %
  2257. GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
  2258. )
  2259. chatMessage.isOneToOneConversation =
  2260. (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  2261. chatMessage.isFormerOneToOneConversation =
  2262. (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
  2263. it.addToStart(chatMessage, scrollToEndOnUpdate)
  2264. }
  2265. }
  2266. if (insertNewMessagesNotice && scrollToEndOnUpdate && adapter != null) {
  2267. scrollToFirstUnreadMessage()
  2268. }
  2269. }
  2270. private fun isScrolledToBottom() = layoutManager?.findFirstVisibleItemPosition() == 0
  2271. private fun shouldInsertNewMessagesNotice(newMessagesAvailable: Boolean, chatMessageList: List<ChatMessage>) =
  2272. if (newMessagesAvailable) {
  2273. chatMessageList.any { it.actorId != conversationUser!!.userId }
  2274. } else {
  2275. false
  2276. }
  2277. private fun updateUnreadMessageInfos(chatMessageList: List<ChatMessage>, scrollToEndOnUpdate: Boolean) {
  2278. val unreadChatMessage = ChatMessage()
  2279. unreadChatMessage.jsonMessageId = -1
  2280. unreadChatMessage.actorId = "-1"
  2281. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  2282. unreadChatMessage.message = context.getString(R.string.nc_new_messages)
  2283. adapter?.addToStart(unreadChatMessage, false)
  2284. if (scrollToEndOnUpdate) {
  2285. binding.scrollDownButton.visibility = View.GONE
  2286. newMessagesCount = 0
  2287. } else {
  2288. if (binding.unreadMessagesPopup.isShown) {
  2289. newMessagesCount++
  2290. } else {
  2291. newMessagesCount = 1
  2292. binding.scrollDownButton.visibility = View.GONE
  2293. binding.unreadMessagesPopup.show()
  2294. }
  2295. }
  2296. }
  2297. private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
  2298. var countGroupedMessages = 0
  2299. for (i in chatMessageList.indices) {
  2300. if (chatMessageList.size > i + 1) {
  2301. if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
  2302. chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
  2303. countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
  2304. ) {
  2305. chatMessageList[i].isGrouped = true
  2306. countGroupedMessages++
  2307. } else {
  2308. countGroupedMessages = 0
  2309. }
  2310. }
  2311. val chatMessage = chatMessageList[i]
  2312. chatMessage.isOneToOneConversation =
  2313. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2314. chatMessage.isFormerOneToOneConversation =
  2315. (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
  2316. chatMessage.activeUser = conversationUser
  2317. chatMessage.token = roomToken
  2318. }
  2319. if (adapter != null) {
  2320. adapter?.addToEnd(chatMessageList, false)
  2321. }
  2322. scrollToRequestedMessageIfNeeded()
  2323. // FENOM: add here audio resume policy
  2324. resumeAudioPlaybackIfNeeded()
  2325. }
  2326. private fun scrollToFirstUnreadMessage() {
  2327. adapter?.let {
  2328. scrollToAndCenterMessageWithId("-1")
  2329. }
  2330. }
  2331. private fun determinePreviousMessageIds(chatMessageList: List<ChatMessage>) {
  2332. var previousMessageId = NO_PREVIOUS_MESSAGE_ID
  2333. for (i in chatMessageList.indices.reversed()) {
  2334. val chatMessage = chatMessageList[i]
  2335. if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2336. chatMessage.previousMessageId = previousMessageId
  2337. } else {
  2338. adapter?.let {
  2339. if (!it.isEmpty) {
  2340. if (it.items[0].item is ChatMessage) {
  2341. chatMessage.previousMessageId = (it.items[0].item as ChatMessage).jsonMessageId
  2342. } else if (it.items.size > 1 && it.items[1].item is ChatMessage) {
  2343. chatMessage.previousMessageId = (it.items[1].item as ChatMessage).jsonMessageId
  2344. }
  2345. }
  2346. }
  2347. }
  2348. previousMessageId = chatMessage.jsonMessageId
  2349. }
  2350. }
  2351. /**
  2352. * this method must be called after that the adapter has finished loading ChatMessages items
  2353. * it searches by ID the message that was playing,s
  2354. * then, if it finds it, it restores audio position
  2355. * and eventually resumes audio playback
  2356. * @author Giacomo Pacini
  2357. */
  2358. private fun resumeAudioPlaybackIfNeeded() {
  2359. if (voiceMessageToRestoreId != "") {
  2360. Log.d(RESUME_AUDIO_TAG, "begin method to resume audio playback")
  2361. val pair = getItemFromAdapter(voiceMessageToRestoreId)
  2362. currentlyPlayedVoiceMessage = pair?.first
  2363. val voiceMessagePosition = pair?.second!!
  2364. lastRecordMediaPosition = voiceMessageToRestoreAudioPosition * 1000
  2365. Log.d(RESUME_AUDIO_TAG, "trying to resume audio")
  2366. binding.messagesListView.scrollToPosition(voiceMessagePosition)
  2367. // WORKAROUND TO FETCH FILE INFO:
  2368. currentlyPlayedVoiceMessage!!.getImageUrl()
  2369. // see getImageUrl() source code
  2370. setUpWaveform(currentlyPlayedVoiceMessage!!, voiceMessageToRestoreWasPlaying)
  2371. Log.d(RESUME_AUDIO_TAG, "resume audio procedure completed")
  2372. } else {
  2373. Log.d(RESUME_AUDIO_TAG, "No voice message to restore")
  2374. }
  2375. voiceMessageToRestoreId = ""
  2376. voiceMessageToRestoreAudioPosition = 0
  2377. voiceMessageToRestoreWasPlaying = false
  2378. }
  2379. private fun getItemFromAdapter(messageId: String): Pair<ChatMessage, Int>? {
  2380. if (adapter != null) {
  2381. val messagePosition = adapter!!.items!!.indexOfFirst {
  2382. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  2383. }
  2384. if (messagePosition >= 0) {
  2385. val currentItem = adapter?.items?.get(messagePosition)?.item
  2386. if (currentItem is ChatMessage && currentItem.id == messageId) {
  2387. return Pair(currentItem, messagePosition)
  2388. } else {
  2389. Log.d(TAG, "currentItem retrieved was not chatmessage or its id was not correct")
  2390. }
  2391. } else {
  2392. Log.d(TAG, "messagePosition is -1, adapter # of items: " + adapter!!.itemCount)
  2393. }
  2394. } else {
  2395. Log.d(TAG, "TalkMessagesListAdapter is null")
  2396. }
  2397. return null
  2398. }
  2399. private fun scrollToRequestedMessageIfNeeded() {
  2400. intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let {
  2401. scrollToMessageWithId(it)
  2402. }
  2403. }
  2404. private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean {
  2405. return TextUtils.isEmpty(messageLeft.systemMessage) &&
  2406. TextUtils.isEmpty(messageRight.systemMessage) &&
  2407. DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt)
  2408. }
  2409. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  2410. val id = (
  2411. adapter?.items?.last {
  2412. it.item is ChatMessage
  2413. }?.item as ChatMessage
  2414. ).jsonMessageId
  2415. val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  2416. chatViewModel.loadMoreMessages(
  2417. beforeMessageId = id.toLong(),
  2418. withUrl = urlForChatting,
  2419. withCredentials = credentials!!,
  2420. withMessageLimit = MESSAGE_PULL_LIMIT,
  2421. roomToken = currentConversation!!.token
  2422. )
  2423. }
  2424. override fun format(date: Date): String {
  2425. return if (DateFormatter.isToday(date)) {
  2426. resources!!.getString(R.string.nc_date_header_today)
  2427. } else if (DateFormatter.isYesterday(date)) {
  2428. resources!!.getString(R.string.nc_date_header_yesterday)
  2429. } else {
  2430. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  2431. }
  2432. }
  2433. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  2434. super.onCreateOptionsMenu(menu)
  2435. menuInflater.inflate(R.menu.menu_conversation, menu)
  2436. if (conversationUser?.userId == "?") {
  2437. menu.removeItem(R.id.conversation_info)
  2438. } else {
  2439. loadAvatarForStatusBar()
  2440. setActionBarTitle()
  2441. }
  2442. return true
  2443. }
  2444. override fun onPrepareOptionsMenu(menu: Menu): Boolean {
  2445. super.onPrepareOptionsMenu(menu)
  2446. if (this::spreedCapabilities.isInitialized) {
  2447. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
  2448. checkShowCallButtons()
  2449. }
  2450. val searchItem = menu.findItem(R.id.conversation_search)
  2451. searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities)
  2452. if (currentConversation!!.remoteServer != null ||
  2453. !CapabilitiesUtil.isSharedItemsAvailable(spreedCapabilities)
  2454. ) {
  2455. menu.removeItem(R.id.shared_items)
  2456. }
  2457. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  2458. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  2459. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  2460. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
  2461. Handler().post {
  2462. findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
  2463. showCallButtonMenu(true)
  2464. true
  2465. }
  2466. }
  2467. Handler().post {
  2468. findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
  2469. showCallButtonMenu(false)
  2470. true
  2471. }
  2472. }
  2473. }
  2474. } else {
  2475. menu.removeItem(R.id.conversation_video_call)
  2476. menu.removeItem(R.id.conversation_voice_call)
  2477. }
  2478. }
  2479. return true
  2480. }
  2481. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  2482. return when (item.itemId) {
  2483. R.id.conversation_video_call -> {
  2484. startACall(false, false)
  2485. true
  2486. }
  2487. R.id.conversation_voice_call -> {
  2488. startACall(true, false)
  2489. true
  2490. }
  2491. R.id.conversation_info -> {
  2492. showConversationInfoScreen()
  2493. true
  2494. }
  2495. R.id.shared_items -> {
  2496. showSharedItems()
  2497. true
  2498. }
  2499. R.id.conversation_search -> {
  2500. startMessageSearch()
  2501. true
  2502. }
  2503. else -> super.onOptionsItemSelected(item)
  2504. }
  2505. }
  2506. private fun showSharedItems() {
  2507. val intent = Intent(this, SharedItemsActivity::class.java)
  2508. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2509. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2510. intent.putExtra(
  2511. SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
  2512. ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)
  2513. )
  2514. startActivity(intent)
  2515. }
  2516. private fun startMessageSearch() {
  2517. val intent = Intent(this, MessageSearchActivity::class.java)
  2518. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2519. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2520. startMessageSearchForResult.launch(intent)
  2521. }
  2522. private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2523. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2524. val chatMessageIterator = chatMessageMap.iterator()
  2525. while (chatMessageIterator.hasNext()) {
  2526. val currentMessage = chatMessageIterator.next()
  2527. if (isInfoMessageAboutDeletion(currentMessage) ||
  2528. isReactionsMessage(currentMessage) ||
  2529. isPollVotedMessage(currentMessage) ||
  2530. isEditMessage(currentMessage)
  2531. ) {
  2532. chatMessageIterator.remove()
  2533. }
  2534. }
  2535. return chatMessageMap.values.toList()
  2536. }
  2537. private fun handleExpandableSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2538. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2539. val chatMessageIterator = chatMessageMap.iterator()
  2540. while (chatMessageIterator.hasNext()) {
  2541. val currentMessage = chatMessageIterator.next()
  2542. val previousMessage = chatMessageMap[currentMessage.value.previousMessageId.toString()]
  2543. if (isSystemMessage(currentMessage.value) &&
  2544. previousMessage?.systemMessageType == currentMessage.value.systemMessageType
  2545. ) {
  2546. previousMessage?.expandableParent = true
  2547. currentMessage.value.expandableParent = false
  2548. if (currentMessage.value.lastItemOfExpandableGroup == 0) {
  2549. currentMessage.value.lastItemOfExpandableGroup = currentMessage.value.jsonMessageId
  2550. }
  2551. previousMessage?.lastItemOfExpandableGroup = currentMessage.value.lastItemOfExpandableGroup
  2552. previousMessage?.expandableChildrenAmount = currentMessage.value.expandableChildrenAmount + 1
  2553. }
  2554. }
  2555. return chatMessageMap.values.toList()
  2556. }
  2557. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2558. return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage
  2559. .SystemMessageType.MESSAGE_DELETED
  2560. }
  2561. private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2562. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
  2563. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
  2564. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
  2565. }
  2566. private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2567. return currentMessage.value.parentMessageId != null && currentMessage.value.systemMessageType == ChatMessage
  2568. .SystemMessageType.MESSAGE_EDITED
  2569. }
  2570. private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2571. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
  2572. }
  2573. private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
  2574. currentConversation?.let {
  2575. if (conversationUser != null) {
  2576. val pp = ParticipantPermissions(spreedCapabilities, it)
  2577. if (!pp.canStartCall() && currentConversation?.hasCall == false) {
  2578. Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show()
  2579. } else {
  2580. ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
  2581. val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
  2582. if (callIntent != null) {
  2583. startActivity(callIntent)
  2584. }
  2585. }
  2586. }
  2587. }
  2588. }
  2589. private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
  2590. currentConversation?.let {
  2591. val bundle = Bundle()
  2592. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  2593. // bundle.putString(KEY_ROOM_ID, roomId)
  2594. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  2595. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl!!)
  2596. bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
  2597. bundle.putInt(KEY_RECORDING_STATE, it.callRecording)
  2598. bundle.putBoolean(KEY_IS_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(it))
  2599. bundle.putBoolean(
  2600. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
  2601. participantPermissions.canPublishAudio()
  2602. )
  2603. bundle.putBoolean(
  2604. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
  2605. participantPermissions.canPublishVideo()
  2606. )
  2607. if (isVoiceOnlyCall) {
  2608. bundle.putBoolean(KEY_CALL_VOICE_ONLY, true)
  2609. }
  2610. if (callWithoutNotification) {
  2611. bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true)
  2612. }
  2613. if (it.objectType == ConversationEnums.ObjectType.ROOM) {
  2614. bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true)
  2615. }
  2616. val callIntent = Intent(this, CallActivity::class.java)
  2617. callIntent.putExtras(bundle)
  2618. return callIntent
  2619. } ?: run {
  2620. return null
  2621. }
  2622. }
  2623. override fun onClickReaction(chatMessage: ChatMessage, emoji: String) {
  2624. VibrationUtils.vibrateShort(context)
  2625. if (chatMessage.reactionsSelf?.contains(emoji) == true) {
  2626. chatViewModel.deleteReaction(roomToken, chatMessage, emoji)
  2627. } else {
  2628. chatViewModel.addReaction(roomToken, chatMessage, emoji)
  2629. }
  2630. }
  2631. override fun onLongClickReactions(chatMessage: ChatMessage) {
  2632. ShowReactionsDialog(
  2633. this,
  2634. roomToken,
  2635. chatMessage,
  2636. conversationUser,
  2637. participantPermissions.hasChatPermission(),
  2638. ncApi
  2639. ).show()
  2640. }
  2641. override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
  2642. openMessageActionsDialog(chatMessage)
  2643. }
  2644. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  2645. openMessageActionsDialog(message)
  2646. }
  2647. override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
  2648. onOpenMessageActionsDialog(chatMessage)
  2649. }
  2650. private fun openMessageActionsDialog(iMessage: IMessage?) {
  2651. val message = iMessage as ChatMessage
  2652. if (hasVisibleItems(message) && !isSystemMessage(message)) {
  2653. MessageActionsDialog(
  2654. this,
  2655. message,
  2656. conversationUser,
  2657. currentConversation,
  2658. isShowMessageDeletionButton(message),
  2659. participantPermissions.hasChatPermission(),
  2660. spreedCapabilities
  2661. ).show()
  2662. }
  2663. }
  2664. private fun isSystemMessage(message: ChatMessage): Boolean {
  2665. return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
  2666. }
  2667. fun deleteMessage(message: IMessage) {
  2668. if (!participantPermissions.hasChatPermission()) {
  2669. Log.w(
  2670. TAG,
  2671. "Deletion of message is skipped because of restrictions by permissions. " +
  2672. "This method should not have been called!"
  2673. )
  2674. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  2675. } else {
  2676. var apiVersion = 1
  2677. // FIXME Fix API checking with guests?
  2678. if (conversationUser != null) {
  2679. apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  2680. }
  2681. chatViewModel.deleteChatMessages(
  2682. credentials!!,
  2683. ApiUtils.getUrlForChatMessage(
  2684. apiVersion,
  2685. conversationUser?.baseUrl!!,
  2686. roomToken,
  2687. message.id!!
  2688. ),
  2689. message.id!!
  2690. )
  2691. }
  2692. }
  2693. fun replyPrivately(message: IMessage?) {
  2694. val apiVersion =
  2695. ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  2696. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2697. apiVersion,
  2698. conversationUser?.baseUrl!!,
  2699. "1",
  2700. null,
  2701. message?.user?.id?.substring(INVITE_LENGTH),
  2702. null
  2703. )
  2704. chatViewModel.createRoom(
  2705. credentials!!,
  2706. retrofitBucket.url!!,
  2707. retrofitBucket.queryMap!!
  2708. )
  2709. }
  2710. fun forwardMessage(message: IMessage?) {
  2711. val bundle = Bundle()
  2712. bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
  2713. bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
  2714. bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomToken)
  2715. val intent = Intent(this, ConversationsListActivity::class.java)
  2716. intent.putExtras(bundle)
  2717. startActivity(intent)
  2718. }
  2719. fun remindMeLater(message: ChatMessage?) {
  2720. Log.d(TAG, "remindMeLater called")
  2721. val chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1, 1))
  2722. val newFragment: DialogFragment = DateTimePickerFragment.newInstance(roomToken, message!!.id, chatApiVersion)
  2723. newFragment.show(supportFragmentManager, DateTimePickerFragment.TAG)
  2724. }
  2725. fun markAsUnread(message: IMessage?) {
  2726. val chatMessage = message as ChatMessage?
  2727. if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2728. chatViewModel.setChatReadMarker(
  2729. credentials!!,
  2730. ApiUtils.getUrlForChatReadMarker(
  2731. ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1)),
  2732. conversationUser?.baseUrl!!,
  2733. roomToken
  2734. ),
  2735. chatMessage.previousMessageId
  2736. )
  2737. }
  2738. }
  2739. fun copyMessage(message: IMessage?) {
  2740. val clipboardManager =
  2741. getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  2742. val clipData = ClipData.newPlainText(
  2743. resources?.getString(R.string.nc_app_product_name),
  2744. message?.text
  2745. )
  2746. clipboardManager.setPrimaryClip(clipData)
  2747. }
  2748. fun translateMessage(message: IMessage?) {
  2749. val bundle = Bundle()
  2750. bundle.putString(BundleKeys.KEY_TRANSLATE_MESSAGE, message?.text)
  2751. val intent = Intent(this, TranslateActivity::class.java)
  2752. intent.putExtras(bundle)
  2753. startActivity(intent)
  2754. }
  2755. fun share(message: ChatMessage) {
  2756. val filename = message.selectedIndividualHashMap!!["name"]
  2757. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2758. val shareUri = FileProvider.getUriForFile(
  2759. this,
  2760. BuildConfig.APPLICATION_ID,
  2761. File(path)
  2762. )
  2763. val shareIntent: Intent = Intent().apply {
  2764. action = Intent.ACTION_SEND
  2765. putExtra(Intent.EXTRA_STREAM, shareUri)
  2766. type = Mimetype.IMAGE_PREFIX_GENERIC
  2767. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  2768. }
  2769. startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
  2770. }
  2771. fun checkIfSharable(message: ChatMessage) {
  2772. val filename = message.selectedIndividualHashMap!!["name"]
  2773. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2774. val file = File(context.cacheDir, filename!!)
  2775. if (file.exists()) {
  2776. share(message)
  2777. } else {
  2778. downloadFileToCache(message, false) {
  2779. share(message)
  2780. }
  2781. }
  2782. }
  2783. private fun showSaveToStorageWarning(message: ChatMessage) {
  2784. val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
  2785. message.selectedIndividualHashMap!!["name"]!!
  2786. )
  2787. saveFragment.show(
  2788. supportFragmentManager,
  2789. SaveToStorageDialogFragment.TAG
  2790. )
  2791. }
  2792. fun checkIfSaveable(message: ChatMessage) {
  2793. val filename = message.selectedIndividualHashMap!!["name"]
  2794. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2795. val file = File(context.cacheDir, filename!!)
  2796. if (file.exists()) {
  2797. showSaveToStorageWarning(message)
  2798. } else {
  2799. downloadFileToCache(message, false) {
  2800. showSaveToStorageWarning(message)
  2801. }
  2802. }
  2803. }
  2804. fun shareToNotes(message: ChatMessage, roomToken: String) {
  2805. val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  2806. val type = message.getCalculateMessageType()
  2807. var shareUri: Uri? = null
  2808. val data: HashMap<String?, String?>?
  2809. var metaData: String = ""
  2810. var objectId: String = ""
  2811. if (message.hasFileAttachment()) {
  2812. val filename = message.selectedIndividualHashMap!!["name"]
  2813. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2814. shareUri = FileProvider.getUriForFile(
  2815. this,
  2816. BuildConfig.APPLICATION_ID,
  2817. File(path)
  2818. )
  2819. this.grantUriPermission(
  2820. applicationContext.packageName,
  2821. shareUri,
  2822. Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
  2823. )
  2824. } else if (message.hasGeoLocation()) {
  2825. data = message.messageParameters?.get("object")
  2826. objectId = data?.get("id")!!
  2827. val name = data["name"]!!
  2828. val lat = data["latitude"]!!
  2829. val lon = data["longitude"]!!
  2830. metaData =
  2831. "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," +
  2832. "\"longitude\":\"$lon\",\"name\":\"$name\"}"
  2833. }
  2834. when (type) {
  2835. ChatMessage.MessageType.VOICE_MESSAGE -> {
  2836. uploadFile(shareUri.toString(), true, token = roomToken)
  2837. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2838. }
  2839. ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
  2840. val caption = if (message.message != "{file}") message.message else ""
  2841. if (null != shareUri) {
  2842. try {
  2843. context.contentResolver.openInputStream(shareUri)?.close()
  2844. uploadFile(shareUri.toString(), false, caption!!, roomToken)
  2845. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2846. } catch (e: java.lang.Exception) {
  2847. Log.w(TAG, "File corresponding to the uri does not exist " + shareUri.toString())
  2848. downloadFileToCache(message, false) {
  2849. uploadFile(shareUri.toString(), false, caption!!, roomToken)
  2850. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2851. }
  2852. }
  2853. }
  2854. }
  2855. ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> {
  2856. chatViewModel.shareLocationToNotes(
  2857. credentials!!,
  2858. ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl!!, roomToken),
  2859. "geo-location",
  2860. objectId,
  2861. metaData
  2862. )
  2863. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2864. }
  2865. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> {
  2866. chatViewModel.shareToNotes(
  2867. credentials!!,
  2868. ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl!!, roomToken),
  2869. message.message!!,
  2870. conversationUser!!.displayName!!
  2871. )
  2872. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2873. }
  2874. else -> {}
  2875. }
  2876. }
  2877. fun openInFilesApp(message: ChatMessage) {
  2878. val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
  2879. val link = message.selectedIndividualHashMap!!["link"]
  2880. val fileViewerUtils = FileViewerUtils(this, message.activeUser!!)
  2881. fileViewerUtils.openFileInFilesApp(link!!, keyID!!)
  2882. }
  2883. private fun hasVisibleItems(message: ChatMessage): Boolean {
  2884. return !message.isDeleted || // copy message
  2885. message.replyable || // reply to
  2886. message.replyable && // reply privately
  2887. conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" &&
  2888. message.user.id.startsWith("users/") &&
  2889. message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
  2890. currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2891. isShowMessageDeletionButton(message) || // delete
  2892. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward
  2893. message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread
  2894. ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
  2895. BuildConfig.DEBUG
  2896. }
  2897. private fun setMessageAsDeleted(message: IMessage?) {
  2898. val messageTemp = message as ChatMessage
  2899. messageTemp.isDeleted = true
  2900. messageTemp.message = getString(R.string.message_deleted_by_you)
  2901. messageTemp.isOneToOneConversation =
  2902. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2903. messageTemp.activeUser = conversationUser
  2904. adapter?.update(messageTemp)
  2905. }
  2906. private fun setMessageAsEdited(message: IMessage?, newString: String) {
  2907. val messageTemp = message as ChatMessage
  2908. messageTemp.lastEditTimestamp = message.lastEditTimestamp
  2909. messageTemp.message = newString
  2910. val index = adapter?.getMessagePositionById(messageTemp.id)!!
  2911. if (index > 0) {
  2912. val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage
  2913. messageTemp.parentMessageId = adapterMsg.parentMessageId
  2914. }
  2915. messageTemp.isOneToOneConversation =
  2916. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2917. messageTemp.activeUser = conversationUser
  2918. adapter?.update(messageTemp)
  2919. }
  2920. private fun updateMessageInsideAdapter(message: IMessage?) {
  2921. message?.let {
  2922. val messageTemp = message as ChatMessage
  2923. // TODO is this needed?
  2924. messageTemp.isOneToOneConversation =
  2925. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2926. messageTemp.activeUser = conversationUser
  2927. adapter?.update(messageTemp)
  2928. }
  2929. }
  2930. fun updateUiToAddReaction(message: ChatMessage, emoji: String) {
  2931. if (message.reactions == null) {
  2932. message.reactions = LinkedHashMap()
  2933. }
  2934. if (message.reactionsSelf == null) {
  2935. message.reactionsSelf = ArrayList()
  2936. }
  2937. var amount = message.reactions!![emoji]
  2938. if (amount == null) {
  2939. amount = 0
  2940. }
  2941. message.reactions!![emoji] = amount + 1
  2942. message.reactionsSelf!!.add(emoji)
  2943. adapter?.update(message)
  2944. }
  2945. fun updateUiToDeleteReaction(message: ChatMessage, emoji: String) {
  2946. if (message.reactions == null) {
  2947. message.reactions = LinkedHashMap()
  2948. }
  2949. if (message.reactionsSelf == null) {
  2950. message.reactionsSelf = ArrayList()
  2951. }
  2952. var amount = message.reactions!![emoji]
  2953. if (amount == null) {
  2954. amount = 0
  2955. }
  2956. message.reactions!![emoji] = amount - 1
  2957. if (message.reactions!![emoji]!! <= 0) {
  2958. message.reactions!!.remove(emoji)
  2959. }
  2960. message.reactionsSelf!!.remove(emoji)
  2961. adapter?.update(message)
  2962. }
  2963. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  2964. val isUserAllowedByPrivileges = userAllowedByPrivilages(message)
  2965. val isOlderThanSixHours = message
  2966. .createdAt
  2967. .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
  2968. val hasDeleteMessagesUnlimitedCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
  2969. spreedCapabilities,
  2970. SpreedFeatures.DELETE_MESSAGES_UNLIMITED
  2971. )
  2972. return when {
  2973. !isUserAllowedByPrivileges -> false
  2974. !hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false
  2975. message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
  2976. message.isDeleted -> false
  2977. !CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
  2978. !participantPermissions.hasChatPermission() -> false
  2979. hasDeleteMessagesUnlimitedCapability -> true
  2980. else -> true
  2981. }
  2982. }
  2983. fun userAllowedByPrivilages(message: ChatMessage): Boolean {
  2984. if (conversationUser == null) return false
  2985. val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
  2986. true
  2987. } else {
  2988. ConversationUtils.canModerate(currentConversation!!, spreedCapabilities)
  2989. }
  2990. return isUserAllowedByPrivileges
  2991. }
  2992. override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
  2993. return when (type) {
  2994. CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
  2995. CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
  2996. CONTENT_TYPE_POLL -> message.isPoll()
  2997. CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
  2998. CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
  2999. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
  3000. CONTENT_TYPE_CALL_STARTED -> message.id == "-2"
  3001. else -> false
  3002. }
  3003. }
  3004. private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) {
  3005. when (recent.systemMessageType) {
  3006. ChatMessage.SystemMessageType.CALL_STARTED -> { // add CallStartedMessage with id -2
  3007. if (!callStarted) {
  3008. val callStartedChatMessage = ChatMessage()
  3009. callStartedChatMessage.jsonMessageId = CALL_STARTED_ID
  3010. callStartedChatMessage.actorId = "-2"
  3011. val name = if (recent.actorDisplayName.isNullOrEmpty()) "Guest" else recent.actorDisplayName
  3012. callStartedChatMessage.actorDisplayName = name
  3013. callStartedChatMessage.actorType = recent.actorType
  3014. callStartedChatMessage.timestamp = chatMessageList[0].timestamp
  3015. callStartedChatMessage.message = null
  3016. adapter?.addToStart(callStartedChatMessage, false)
  3017. callStarted = true
  3018. }
  3019. } // remove CallStartedMessage with id -2
  3020. ChatMessage.SystemMessageType.CALL_ENDED,
  3021. ChatMessage.SystemMessageType.CALL_MISSED,
  3022. ChatMessage.SystemMessageType.CALL_TRIED,
  3023. ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> {
  3024. adapter?.deleteById("-2")
  3025. callStarted = false
  3026. } // remove message of id -2
  3027. else -> {}
  3028. }
  3029. }
  3030. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  3031. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  3032. /*
  3033. switch (webSocketCommunicationEvent.getType()) {
  3034. case "refreshChat":
  3035. if (
  3036. webSocketCommunicationEvent
  3037. .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID)
  3038. .equals(Long.toString(conversationUser.getId()))
  3039. ) {
  3040. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  3041. pullChatMessages(2);
  3042. }
  3043. }
  3044. break;
  3045. default:
  3046. }*/
  3047. }
  3048. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  3049. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  3050. if (currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  3051. currentConversation?.name != userMentionClickEvent.userId
  3052. ) {
  3053. var apiVersion = 1
  3054. // FIXME Fix API checking with guests?
  3055. if (conversationUser != null) {
  3056. apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  3057. }
  3058. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  3059. apiVersion,
  3060. conversationUser?.baseUrl!!,
  3061. "1",
  3062. null,
  3063. userMentionClickEvent.userId,
  3064. null
  3065. )
  3066. chatViewModel.createRoom(
  3067. credentials!!,
  3068. retrofitBucket.url!!,
  3069. retrofitBucket.queryMap!!
  3070. )
  3071. }
  3072. }
  3073. fun sendPictureFromCamIntent() {
  3074. if (!permissionUtil.isCameraPermissionGranted()) {
  3075. requestCameraPermissions()
  3076. } else {
  3077. startPickCameraIntentForResult.launch(TakePhotoActivity.createIntent(context))
  3078. }
  3079. }
  3080. fun sendVideoFromCamIntent() {
  3081. if (!permissionUtil.isCameraPermissionGranted()) {
  3082. requestCameraPermissions()
  3083. } else {
  3084. Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
  3085. takeVideoIntent.resolveActivity(packageManager)?.also {
  3086. val videoFile: File? = try {
  3087. val outputDir = context.cacheDir
  3088. val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
  3089. val date = dateFormat.format(Date())
  3090. val videoName = String.format(
  3091. context.resources.getString(R.string.nc_video_filename),
  3092. date
  3093. )
  3094. File("$outputDir/$videoName$VIDEO_SUFFIX")
  3095. } catch (e: IOException) {
  3096. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  3097. Log.e(TAG, "error while creating video file", e)
  3098. null
  3099. }
  3100. videoFile?.also {
  3101. videoURI = FileProvider.getUriForFile(context, context.packageName, it)
  3102. takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
  3103. startPickCameraIntentForResult.launch(takeVideoIntent)
  3104. }
  3105. }
  3106. }
  3107. }
  3108. }
  3109. fun createPoll() {
  3110. val pollVoteDialog = PollCreateDialogFragment.newInstance(
  3111. roomToken
  3112. )
  3113. pollVoteDialog.show(supportFragmentManager, TAG)
  3114. }
  3115. fun jumpToQuotedMessage(parentMessage: ChatMessage) {
  3116. var foundMessage = false
  3117. for (position in 0 until (adapter!!.items.size)) {
  3118. val currentItem = adapter?.items?.get(position)?.item
  3119. if (currentItem is ChatMessage && currentItem.id == parentMessage.id) {
  3120. layoutManager!!.scrollToPosition(position)
  3121. foundMessage = true
  3122. break
  3123. }
  3124. }
  3125. if (!foundMessage) {
  3126. Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
  3127. // TODO: show better info
  3128. // TODO: improve handling how this can be avoided. E.g. loading chat until message is reached...
  3129. Snackbar.make(binding.root, "Message was not found", Snackbar.LENGTH_LONG).show()
  3130. }
  3131. }
  3132. override fun joinAudioCall() {
  3133. startACall(true, false)
  3134. }
  3135. override fun joinVideoCall() {
  3136. startACall(false, false)
  3137. }
  3138. private fun logConversationInfos(methodName: String) {
  3139. Log.d(TAG, " |-----------------------------------------------")
  3140. Log.d(TAG, " | method: $methodName")
  3141. Log.d(TAG, " | ChatActivity: " + System.identityHashCode(this).toString())
  3142. Log.d(TAG, " | roomToken: $roomToken")
  3143. Log.d(TAG, " | currentConversation?.displayName: ${currentConversation?.displayName}")
  3144. Log.d(TAG, " | sessionIdAfterRoomJoined: $sessionIdAfterRoomJoined")
  3145. Log.d(TAG, " |-----------------------------------------------")
  3146. }
  3147. fun shareMessageText(message: String) {
  3148. val sendIntent: Intent = Intent().apply {
  3149. action = Intent.ACTION_SEND
  3150. putExtra(Intent.EXTRA_TEXT, message)
  3151. type = "text/plain"
  3152. }
  3153. val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share))
  3154. startActivity(shareIntent)
  3155. }
  3156. companion object {
  3157. val TAG = ChatActivity::class.simpleName
  3158. private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
  3159. private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 2
  3160. private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 3
  3161. private const val CONTENT_TYPE_LOCATION: Byte = 4
  3162. private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 5
  3163. private const val CONTENT_TYPE_POLL: Byte = 6
  3164. private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7
  3165. private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
  3166. private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
  3167. private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000
  3168. private const val HTTP_CODE_OK: Int = 200
  3169. private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
  3170. private const val REQUEST_SHARE_FILE_PERMISSION: Int = 221
  3171. private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
  3172. private const val REQUEST_READ_CONTACT_PERMISSION = 234
  3173. private const val REQUEST_CAMERA_PERMISSION = 223
  3174. private const val OBJECT_MESSAGE: String = "{object}"
  3175. private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
  3176. private const val MINIMUM_VOICE_RECORD_TO_STOP: Int = 200
  3177. private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
  3178. private const val VOICE_RECORD_LOCK_BUTTON_Y: Int = -130
  3179. private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
  3180. private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
  3181. // Samplingrate 22050 was chosen because somehow 44100 failed to playback on safari when recorded on android.
  3182. // Please test with firefox, chrome, safari and mobile clients if changing anything regarding the sound.
  3183. private const val VOICE_MESSAGE_SAMPLING_RATE = 22050
  3184. private const val VOICE_MESSAGE_ENCODING_BIT_RATE = 32000
  3185. private const val VOICE_MESSAGE_CHANNELS = 1
  3186. private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
  3187. private const val VIDEO_SUFFIX = ".mp4"
  3188. private const val FULLY_OPAQUE_INT: Int = 255
  3189. private const val SEMI_TRANSPARENT_INT: Int = 99
  3190. private const val VOICE_MESSAGE_SEEKBAR_BASE = 1000
  3191. private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
  3192. private const val GROUPED_MESSAGES_THRESHOLD = 4
  3193. private const val GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD = 5
  3194. private const val TOOLBAR_AVATAR_RATIO = 1.5
  3195. private const val STATUS_SIZE_IN_DP = 9f
  3196. private const val HTTP_CODE_NOT_MODIFIED = 304
  3197. private const val HTTP_CODE_PRECONDITION_FAILED = 412
  3198. private const val HTTP_BAD_REQUEST = 400
  3199. private const val HTTP_FORBIDDEN = 403
  3200. private const val HTTP_NOT_FOUND = 404
  3201. private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
  3202. private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
  3203. private const val MESSAGE_PULL_LIMIT = 100
  3204. private const val PAGE_SIZE = 100
  3205. private const val INVITE_LENGTH = 6
  3206. private const val ACTOR_LENGTH = 6
  3207. private const val ANIMATION_DURATION: Long = 750
  3208. private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
  3209. private const val CHUNK_SIZE: Int = 10
  3210. private const val ONE_SECOND_IN_MILLIS = 1000
  3211. private const val SAMPLE_RATE = 8000
  3212. private const val VOICE_RECORDING_LOCK_ANIMATION_DURATION = 500
  3213. private const val AUDIO_VALUE_MAX = 40
  3214. private const val AUDIO_VALUE_MIN = 20
  3215. private const val AUDIO_VALUE_SLEEP: Long = 50
  3216. private const val WHITESPACE = " "
  3217. private const val COMMA = ", "
  3218. private const val TYPING_INDICATOR_ANIMATION_DURATION = 200L
  3219. private const val TYPING_INDICATOR_MAX_NAME_LENGTH = 14
  3220. private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L
  3221. private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L
  3222. private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping"
  3223. private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
  3224. private const val CALL_STARTED_ID = -2
  3225. private const val MILISEC_15: Long = 15
  3226. private const val LINEBREAK = "\n"
  3227. private const val CURSOR_KEY = "_cursor"
  3228. private const val CURRENT_AUDIO_MESSAGE_KEY = "CURRENT_AUDIO_MESSAGE"
  3229. private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
  3230. private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
  3231. private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
  3232. }
  3233. }