ChatController.kt 140 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Marcel Hibbe
  6. * @author Andy Scherzinger
  7. * @author Tim Krüger
  8. * Copyright (C) 2021-2022 Tim Krüger <t@timkrueger.me>
  9. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  10. * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  11. * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. */
  26. package com.nextcloud.talk.controllers
  27. import android.Manifest
  28. import android.annotation.SuppressLint
  29. import android.app.Activity.RESULT_OK
  30. import android.content.ClipData
  31. import android.content.ClipboardManager
  32. import android.content.Context
  33. import android.content.Intent
  34. import android.content.pm.PackageManager
  35. import android.content.res.AssetFileDescriptor
  36. import android.content.res.Resources
  37. import android.database.Cursor
  38. import android.graphics.drawable.BitmapDrawable
  39. import android.graphics.drawable.ColorDrawable
  40. import android.graphics.drawable.Drawable
  41. import android.media.MediaPlayer
  42. import android.media.MediaRecorder
  43. import android.net.Uri
  44. import android.os.Build
  45. import android.os.Build.VERSION_CODES.O
  46. import android.os.Bundle
  47. import android.os.Handler
  48. import android.os.Parcelable
  49. import android.os.SystemClock
  50. import android.os.VibrationEffect
  51. import android.os.Vibrator
  52. import android.provider.ContactsContract
  53. import android.provider.MediaStore
  54. import android.text.Editable
  55. import android.text.InputFilter
  56. import android.text.TextUtils
  57. import android.text.TextWatcher
  58. import android.util.Log
  59. import android.util.TypedValue
  60. import android.view.Gravity
  61. import android.view.Menu
  62. import android.view.MenuInflater
  63. import android.view.MenuItem
  64. import android.view.MotionEvent
  65. import android.view.View
  66. import android.view.animation.AlphaAnimation
  67. import android.view.animation.Animation
  68. import android.view.animation.LinearInterpolator
  69. import android.widget.AbsListView
  70. import android.widget.ImageButton
  71. import android.widget.ImageView
  72. import android.widget.PopupMenu
  73. import android.widget.RelativeLayout
  74. import android.widget.Toast
  75. import androidx.appcompat.app.AlertDialog
  76. import androidx.appcompat.view.ContextThemeWrapper
  77. import androidx.core.content.ContextCompat
  78. import androidx.core.content.FileProvider
  79. import androidx.core.content.PermissionChecker
  80. import androidx.core.graphics.drawable.toBitmap
  81. import androidx.core.widget.doAfterTextChanged
  82. import androidx.emoji2.text.EmojiCompat
  83. import androidx.emoji2.widget.EmojiTextView
  84. import androidx.recyclerview.widget.ItemTouchHelper
  85. import androidx.recyclerview.widget.LinearLayoutManager
  86. import androidx.recyclerview.widget.RecyclerView
  87. import androidx.work.Data
  88. import androidx.work.OneTimeWorkRequest
  89. import androidx.work.WorkInfo
  90. import androidx.work.WorkManager
  91. import autodagger.AutoInjector
  92. import coil.imageLoader
  93. import coil.load
  94. import coil.request.ImageRequest
  95. import coil.target.Target
  96. import coil.transform.CircleCropTransformation
  97. import com.bluelinelabs.conductor.RouterTransaction
  98. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  99. import com.google.android.flexbox.FlexboxLayout
  100. import com.google.android.material.dialog.MaterialAlertDialogBuilder
  101. import com.nextcloud.talk.BuildConfig
  102. import com.nextcloud.talk.R
  103. import com.nextcloud.talk.activities.CallActivity
  104. import com.nextcloud.talk.activities.MainActivity
  105. import com.nextcloud.talk.activities.TakePhotoActivity
  106. import com.nextcloud.talk.adapters.messages.CommonMessageInterface
  107. import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
  108. import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
  109. import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
  110. import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
  111. import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder
  112. import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
  113. import com.nextcloud.talk.adapters.messages.MessagePayload
  114. import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder
  115. import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
  116. import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
  117. import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
  118. import com.nextcloud.talk.adapters.messages.OutcomingTextMessageViewHolder
  119. import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
  120. import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
  121. import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
  122. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  123. import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
  124. import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
  125. import com.nextcloud.talk.api.NcApi
  126. import com.nextcloud.talk.application.NextcloudTalkApplication
  127. import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
  128. import com.nextcloud.talk.controllers.base.BaseController
  129. import com.nextcloud.talk.controllers.util.viewBinding
  130. import com.nextcloud.talk.data.user.model.User
  131. import com.nextcloud.talk.databinding.ControllerChatBinding
  132. import com.nextcloud.talk.events.UserMentionClickEvent
  133. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  134. import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
  135. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  136. import com.nextcloud.talk.jobs.ShareOperationWorker
  137. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  138. import com.nextcloud.talk.messagesearch.MessageSearchActivity
  139. import com.nextcloud.talk.models.domain.ReactionAddedModel
  140. import com.nextcloud.talk.models.domain.ReactionDeletedModel
  141. import com.nextcloud.talk.models.json.chat.ChatMessage
  142. import com.nextcloud.talk.models.json.chat.ChatOverall
  143. import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
  144. import com.nextcloud.talk.models.json.chat.ReadStatus
  145. import com.nextcloud.talk.models.json.conversations.Conversation
  146. import com.nextcloud.talk.models.json.conversations.RoomOverall
  147. import com.nextcloud.talk.models.json.conversations.RoomsOverall
  148. import com.nextcloud.talk.models.json.generic.GenericOverall
  149. import com.nextcloud.talk.models.json.mention.Mention
  150. import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
  151. import com.nextcloud.talk.presenters.MentionAutocompletePresenter
  152. import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
  153. import com.nextcloud.talk.repositories.reactions.ReactionsRepository
  154. import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
  155. import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
  156. import com.nextcloud.talk.ui.dialog.AttachmentDialog
  157. import com.nextcloud.talk.ui.dialog.MessageActionsDialog
  158. import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
  159. import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
  160. import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
  161. import com.nextcloud.talk.utils.ApiUtils
  162. import com.nextcloud.talk.utils.ContactUtils
  163. import com.nextcloud.talk.utils.DateConstants
  164. import com.nextcloud.talk.utils.DateUtils
  165. import com.nextcloud.talk.utils.FileUtils
  166. import com.nextcloud.talk.utils.ImageEmojiEditText
  167. import com.nextcloud.talk.utils.MagicCharPolicy
  168. import com.nextcloud.talk.utils.NotificationUtils
  169. import com.nextcloud.talk.utils.ParticipantPermissions
  170. import com.nextcloud.talk.utils.bundle.BundleKeys
  171. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
  172. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
  173. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
  174. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
  175. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
  176. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  177. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
  178. import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
  179. import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
  180. import com.nextcloud.talk.utils.remapchat.ConductorRemapping
  181. import com.nextcloud.talk.utils.remapchat.RemapChatModel
  182. import com.nextcloud.talk.utils.rx.DisposableSet
  183. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  184. import com.nextcloud.talk.utils.text.Spans
  185. import com.nextcloud.talk.webrtc.MagicWebSocketInstance
  186. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  187. import com.otaliastudios.autocomplete.Autocomplete
  188. import com.stfalcon.chatkit.commons.ImageLoader
  189. import com.stfalcon.chatkit.commons.models.IMessage
  190. import com.stfalcon.chatkit.messages.MessageHolders
  191. import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
  192. import com.stfalcon.chatkit.messages.MessagesListAdapter
  193. import com.stfalcon.chatkit.utils.DateFormatter
  194. import com.vanniktech.emoji.EmojiPopup
  195. import io.reactivex.Observer
  196. import io.reactivex.android.schedulers.AndroidSchedulers
  197. import io.reactivex.disposables.Disposable
  198. import io.reactivex.schedulers.Schedulers
  199. import kotlinx.android.synthetic.main.view_message_input.view.*
  200. import org.greenrobot.eventbus.EventBus
  201. import org.greenrobot.eventbus.Subscribe
  202. import org.greenrobot.eventbus.ThreadMode
  203. import org.parceler.Parcels
  204. import retrofit2.HttpException
  205. import retrofit2.Response
  206. import java.io.File
  207. import java.io.IOException
  208. import java.net.HttpURLConnection
  209. import java.text.SimpleDateFormat
  210. import java.util.Date
  211. import java.util.Locale
  212. import java.util.Objects
  213. import java.util.concurrent.ExecutionException
  214. import javax.inject.Inject
  215. import kotlin.math.roundToInt
  216. @AutoInjector(NextcloudTalkApplication::class)
  217. class ChatController(args: Bundle) :
  218. BaseController(
  219. R.layout.controller_chat,
  220. args
  221. ),
  222. MessagesListAdapter.OnLoadMoreListener,
  223. MessagesListAdapter.Formatter<Date>,
  224. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  225. ContentChecker<ChatMessage>,
  226. VoiceMessageInterface,
  227. CommonMessageInterface,
  228. PreviewMessageInterface {
  229. private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
  230. @Inject
  231. lateinit var ncApi: NcApi
  232. @Inject
  233. lateinit var eventBus: EventBus
  234. @Inject
  235. lateinit var reactionsRepository: ReactionsRepository
  236. @Inject
  237. lateinit var permissionUtil: PlatformPermissionUtil
  238. @Inject
  239. lateinit var dateUtils: DateUtils
  240. val disposables = DisposableSet()
  241. var roomToken: String? = null
  242. val conversationUser: User?
  243. private val roomPassword: String
  244. var credentials: String? = null
  245. var currentConversation: Conversation? = null
  246. var inConversation = false
  247. private var historyRead = false
  248. private var globalLastKnownFutureMessageId = -1
  249. private var globalLastKnownPastMessageId = -1
  250. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  251. private var mentionAutocomplete: Autocomplete<*>? = null
  252. var layoutManager: LinearLayoutManager? = null
  253. var pullChatMessagesPending = false
  254. private var lookingIntoFuture = false
  255. var newMessagesCount = 0
  256. var startCallFromNotification: Boolean? = null
  257. val roomId: String
  258. val voiceOnly: Boolean
  259. var isFirstMessagesProcessing = true
  260. private var emojiPopup: EmojiPopup? = null
  261. var myFirstMessage: CharSequence? = null
  262. var checkingLobbyStatus: Boolean = false
  263. private var conversationInfoMenuItem: MenuItem? = null
  264. private var conversationVoiceCallMenuItem: MenuItem? = null
  265. private var conversationVideoMenuItem: MenuItem? = null
  266. private var conversationSharedItemsItem: MenuItem? = null
  267. var magicWebSocketInstance: MagicWebSocketInstance? = null
  268. var lobbyTimerHandler: Handler? = null
  269. var pastPreconditionFailed = false
  270. var futurePreconditionFailed = false
  271. private val filesToUpload: MutableList<String> = ArrayList()
  272. private var sharedText: String
  273. var isVoiceRecordingInProgress: Boolean = false
  274. var currentVoiceRecordFile: String = ""
  275. private var recorder: MediaRecorder? = null
  276. var mediaPlayer: MediaPlayer? = null
  277. lateinit var mediaPlayerHandler: Handler
  278. private var currentlyPlayedVoiceMessage: ChatMessage? = null
  279. private lateinit var participantPermissions: ParticipantPermissions
  280. private var videoURI: Uri? = null
  281. init {
  282. Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
  283. setHasOptionsMenu(true)
  284. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  285. this.conversationUser = args.getParcelable(KEY_USER_ENTITY)
  286. this.roomId = args.getString(KEY_ROOM_ID, "")
  287. this.roomToken = args.getString(KEY_ROOM_TOKEN, "")
  288. this.sharedText = args.getString(BundleKeys.KEY_SHARED_TEXT, "")
  289. Log.d(TAG, " roomToken = $roomToken")
  290. if (roomToken.isNullOrEmpty()) {
  291. Log.d(TAG, " roomToken was null or empty!")
  292. }
  293. if (args.containsKey(KEY_ACTIVE_CONVERSATION)) {
  294. this.currentConversation = Parcels.unwrap<Conversation>(args.getParcelable(KEY_ACTIVE_CONVERSATION))
  295. this.participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
  296. }
  297. this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
  298. credentials = if (conversationUser?.userId == "?") {
  299. null
  300. } else {
  301. ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
  302. }
  303. if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
  304. this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)
  305. }
  306. this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false)
  307. }
  308. private fun getRoomInfo() {
  309. logConversationInfos("getRoomInfo")
  310. val shouldRepeat = CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "webinary-lobby")
  311. if (shouldRepeat) {
  312. checkingLobbyStatus = true
  313. }
  314. if (conversationUser != null) {
  315. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  316. val startNanoTime = System.nanoTime()
  317. Log.d(TAG, "getRoomInfo - getRoom - calling: $startNanoTime")
  318. ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, roomToken))
  319. ?.subscribeOn(Schedulers.io())
  320. ?.observeOn(AndroidSchedulers.mainThread())
  321. ?.subscribe(object : Observer<RoomOverall> {
  322. override fun onSubscribe(d: Disposable) {
  323. disposables.add(d)
  324. }
  325. @Suppress("Detekt.TooGenericExceptionCaught")
  326. override fun onNext(roomOverall: RoomOverall) {
  327. Log.d(TAG, "getRoomInfo - getRoom - got response: $startNanoTime")
  328. currentConversation = roomOverall.ocs!!.data
  329. logConversationInfos("getRoomInfo#onNext")
  330. loadAvatarForStatusBar()
  331. setTitle()
  332. participantPermissions = ParticipantPermissions(conversationUser, currentConversation!!)
  333. try {
  334. setupSwipeToReply()
  335. setupMentionAutocomplete()
  336. checkShowCallButtons()
  337. checkShowMessageInputView()
  338. checkLobbyState()
  339. if (!inConversation) {
  340. joinRoomWithPassword()
  341. } else {
  342. Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped")
  343. }
  344. } catch (npe: NullPointerException) {
  345. // view binding can be null
  346. // since this is called asynchronously and UI might have been destroyed in the meantime
  347. Log.i(TAG, "UI destroyed - view binding already gone")
  348. }
  349. }
  350. override fun onError(e: Throwable) {
  351. Log.e(TAG, "getRoomInfo - getRoom - ERROR", e)
  352. }
  353. override fun onComplete() {
  354. Log.d(TAG, "getRoomInfo - getRoom - onComplete: $startNanoTime")
  355. if (shouldRepeat) {
  356. if (lobbyTimerHandler == null) {
  357. lobbyTimerHandler = Handler()
  358. }
  359. lobbyTimerHandler?.postDelayed({ getRoomInfo() }, LOBBY_TIMER_DELAY)
  360. }
  361. }
  362. })
  363. }
  364. }
  365. private fun setupSwipeToReply() {
  366. if (this::participantPermissions.isInitialized &&
  367. participantPermissions.hasChatPermission() &&
  368. !isReadOnlyConversation()
  369. ) {
  370. val messageSwipeController = MessageSwipeCallback(
  371. activity!!,
  372. object : MessageSwipeActions {
  373. override fun showReplyUI(position: Int) {
  374. val chatMessage = adapter?.items?.get(position)?.item as ChatMessage?
  375. replyToMessage(chatMessage)
  376. }
  377. }
  378. )
  379. withNullableControllerViewBinding {
  380. val itemTouchHelper = ItemTouchHelper(messageSwipeController)
  381. itemTouchHelper.attachToRecyclerView(binding.messagesListView)
  382. }
  383. }
  384. }
  385. private fun handleFromNotification() {
  386. var apiVersion = 1
  387. // FIXME Can this be called for guests?
  388. if (conversationUser != null) {
  389. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  390. }
  391. Log.d(TAG, "handleFromNotification - getRooms - calling")
  392. ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl), false)
  393. ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
  394. ?.subscribe(object : Observer<RoomsOverall> {
  395. override fun onSubscribe(d: Disposable) {
  396. disposables.add(d)
  397. }
  398. override fun onNext(roomsOverall: RoomsOverall) {
  399. Log.d(TAG, "handleFromNotification - getRooms - got response")
  400. for (conversation in roomsOverall.ocs!!.data!!) {
  401. if (roomId == conversation.roomId) {
  402. roomToken = conversation.token
  403. currentConversation = conversation
  404. participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
  405. setTitle()
  406. getRoomInfo()
  407. break
  408. }
  409. }
  410. }
  411. override fun onError(e: Throwable) {
  412. Log.e(TAG, "handleFromNotification - getRooms - ERROR: ", e)
  413. }
  414. override fun onComplete() {
  415. // unused atm
  416. }
  417. })
  418. }
  419. private fun loadAvatarForStatusBar() {
  420. if (isOneToOneConversation() && activity != null) {
  421. val url = ApiUtils.getUrlForAvatar(
  422. conversationUser!!.baseUrl,
  423. currentConversation!!.name,
  424. true
  425. )
  426. val target = object : Target {
  427. private fun setIcon(drawable: Drawable?) {
  428. actionBar?.let {
  429. val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt()
  430. if (drawable != null && avatarSize > 0) {
  431. val bitmap = drawable.toBitmap(avatarSize, avatarSize)
  432. it.setIcon(BitmapDrawable(resources, bitmap))
  433. } else {
  434. Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
  435. }
  436. }
  437. }
  438. override fun onStart(placeholder: Drawable?) {
  439. this.setIcon(placeholder)
  440. }
  441. override fun onSuccess(result: Drawable) {
  442. this.setIcon(result)
  443. }
  444. }
  445. val credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)
  446. context.imageLoader.enqueue(
  447. ImageRequest.Builder(context)
  448. .data(url)
  449. .addHeader("Authorization", credentials)
  450. .placeholder(R.drawable.ic_user)
  451. .transformations(CircleCropTransformation())
  452. .crossfade(true)
  453. .target(target)
  454. .build()
  455. )
  456. }
  457. }
  458. fun isOneToOneConversation() = currentConversation != null && currentConversation?.type != null &&
  459. currentConversation?.type == Conversation.ConversationType
  460. .ROOM_TYPE_ONE_TO_ONE_CALL
  461. @Suppress("Detekt.TooGenericExceptionCaught")
  462. override fun onViewBound(view: View) {
  463. Log.d(TAG, "onViewBound: " + System.identityHashCode(this).toString())
  464. actionBar?.show()
  465. var adapterWasNull = false
  466. if (adapter == null) {
  467. binding.progressBar.visibility = View.VISIBLE
  468. adapterWasNull = true
  469. val messageHolders = MessageHolders()
  470. val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, router)
  471. val payload =
  472. MessagePayload(roomToken!!, currentConversation?.isParticipantOwnerOrModerator, profileBottomSheet)
  473. messageHolders.setIncomingTextConfig(
  474. IncomingTextMessageViewHolder::class.java,
  475. R.layout.item_custom_incoming_text_message,
  476. payload
  477. )
  478. messageHolders.setOutcomingTextConfig(
  479. OutcomingTextMessageViewHolder::class.java,
  480. R.layout.item_custom_outcoming_text_message
  481. )
  482. messageHolders.setIncomingImageConfig(
  483. IncomingPreviewMessageViewHolder::class.java,
  484. R.layout.item_custom_incoming_preview_message,
  485. payload
  486. )
  487. messageHolders.setOutcomingImageConfig(
  488. OutcomingPreviewMessageViewHolder::class.java,
  489. R.layout.item_custom_outcoming_preview_message
  490. )
  491. messageHolders.registerContentType(
  492. CONTENT_TYPE_SYSTEM_MESSAGE,
  493. SystemMessageViewHolder::class.java,
  494. R.layout.item_system_message,
  495. SystemMessageViewHolder::class.java,
  496. R.layout.item_system_message,
  497. this
  498. )
  499. messageHolders.registerContentType(
  500. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  501. UnreadNoticeMessageViewHolder::class.java,
  502. R.layout.item_date_header,
  503. UnreadNoticeMessageViewHolder::class.java,
  504. R.layout.item_date_header,
  505. this
  506. )
  507. messageHolders.registerContentType(
  508. CONTENT_TYPE_LOCATION,
  509. IncomingLocationMessageViewHolder::class.java,
  510. payload,
  511. R.layout.item_custom_incoming_location_message,
  512. OutcomingLocationMessageViewHolder::class.java,
  513. null,
  514. R.layout.item_custom_outcoming_location_message,
  515. this
  516. )
  517. messageHolders.registerContentType(
  518. CONTENT_TYPE_VOICE_MESSAGE,
  519. IncomingVoiceMessageViewHolder::class.java,
  520. payload,
  521. R.layout.item_custom_incoming_voice_message,
  522. OutcomingVoiceMessageViewHolder::class.java,
  523. null,
  524. R.layout.item_custom_outcoming_voice_message,
  525. this
  526. )
  527. messageHolders.registerContentType(
  528. CONTENT_TYPE_POLL,
  529. IncomingPollMessageViewHolder::class.java,
  530. payload,
  531. R.layout.item_custom_incoming_poll_message,
  532. OutcomingPollMessageViewHolder::class.java,
  533. payload,
  534. R.layout.item_custom_outcoming_poll_message,
  535. this
  536. )
  537. messageHolders.registerContentType(
  538. CONTENT_TYPE_LINK_PREVIEW,
  539. IncomingLinkPreviewMessageViewHolder::class.java,
  540. payload,
  541. R.layout.item_custom_incoming_link_preview_message,
  542. OutcomingLinkPreviewMessageViewHolder::class.java,
  543. payload,
  544. R.layout.item_custom_outcoming_link_preview_message,
  545. this
  546. )
  547. val senderId = if (!conversationUser.userId.equals("?")) {
  548. "users/" + conversationUser.userId
  549. } else {
  550. currentConversation?.actorType + "/" + currentConversation?.actorId
  551. }
  552. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: $senderId")
  553. adapter = TalkMessagesListAdapter(
  554. senderId,
  555. messageHolders,
  556. ImageLoader { imageView, url, _ ->
  557. imageView.loadAvatarOrImagePreview(url!!, conversationUser, placeholder = payload as? Drawable)
  558. },
  559. this
  560. )
  561. } else {
  562. binding.messagesListView.visibility = View.VISIBLE
  563. }
  564. binding.messagesListView.setAdapter(adapter)
  565. adapter?.setLoadMoreListener(this)
  566. adapter?.setDateHeadersFormatter { format(it) }
  567. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  568. adapter?.registerViewClickListener(
  569. R.id.playPauseBtn
  570. ) { view, message ->
  571. val filename = message.selectedIndividualHashMap!!["name"]
  572. val file = File(context.cacheDir, filename!!)
  573. if (file.exists()) {
  574. if (message.isPlayingVoiceMessage) {
  575. pausePlayback(message)
  576. } else {
  577. startPlayback(message)
  578. }
  579. } else {
  580. downloadFileToCache(message)
  581. }
  582. }
  583. setupSwipeToReply()
  584. layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
  585. binding.popupBubbleView.setRecyclerView(binding.messagesListView)
  586. binding.popupBubbleView.setPopupBubbleListener { context ->
  587. if (newMessagesCount != 0) {
  588. val scrollPosition = if (newMessagesCount - 1 < 0) {
  589. 0
  590. } else {
  591. newMessagesCount - 1
  592. }
  593. Handler().postDelayed(
  594. {
  595. binding.messagesListView.smoothScrollToPosition(scrollPosition)
  596. },
  597. NEW_MESSAGES_POPUP_BUBBLE_DELAY
  598. )
  599. }
  600. }
  601. viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.popupBubbleView)
  602. binding.messageInputView.setPadding(0, 0, 0, 0)
  603. binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  604. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  605. super.onScrollStateChanged(recyclerView, newState)
  606. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  607. if (newMessagesCount != 0 && layoutManager != null) {
  608. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
  609. newMessagesCount = 0
  610. if (binding.popupBubbleView.isShown) {
  611. binding.popupBubbleView.hide()
  612. }
  613. }
  614. }
  615. }
  616. }
  617. })
  618. val filters = arrayOfNulls<InputFilter>(1)
  619. val lengthFilter = CapabilitiesUtilNew.getMessageMaxLength(conversationUser)
  620. filters[0] = InputFilter.LengthFilter(lengthFilter)
  621. binding.messageInputView.inputEditText?.filters = filters
  622. binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
  623. override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
  624. // unused atm
  625. }
  626. @Suppress("Detekt.TooGenericExceptionCaught")
  627. override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
  628. try {
  629. if (s.length >= lengthFilter) {
  630. binding.messageInputView.inputEditText?.error = String.format(
  631. Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
  632. lengthFilter.toString()
  633. )
  634. } else {
  635. binding.messageInputView.inputEditText?.error = null
  636. }
  637. val editable = binding.messageInputView.inputEditText?.editableText
  638. if (editable != null && binding.messageInputView.inputEditText != null) {
  639. val mentionSpans = editable.getSpans(
  640. 0,
  641. binding.messageInputView.inputEditText!!.length(),
  642. Spans.MentionChipSpan::class.java
  643. )
  644. var mentionSpan: Spans.MentionChipSpan
  645. for (i in mentionSpans.indices) {
  646. mentionSpan = mentionSpans[i]
  647. if (start >= editable.getSpanStart(mentionSpan) &&
  648. start < editable.getSpanEnd(mentionSpan)
  649. ) {
  650. if (editable.subSequence(
  651. editable.getSpanStart(mentionSpan),
  652. editable.getSpanEnd(mentionSpan)
  653. ).toString().trim { it <= ' ' } != mentionSpan.label
  654. ) {
  655. editable.removeSpan(mentionSpan)
  656. }
  657. }
  658. }
  659. }
  660. } catch (npe: NullPointerException) {
  661. // view binding can be null
  662. // since this is called asynchronously and UI might have been destroyed in the meantime
  663. Log.i(TAG, "UI destroyed - view binding already gone")
  664. }
  665. }
  666. override fun afterTextChanged(s: Editable) {
  667. // unused atm
  668. }
  669. })
  670. // Image keyboard support
  671. // See: https://developer.android.com/guide/topics/text/image-keyboard
  672. (binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
  673. uploadFile(it.toString(), false)
  674. }
  675. showMicrophoneButton(true)
  676. binding.messageInputView.messageInput.doAfterTextChanged {
  677. try {
  678. if (binding.messageInputView.messageInput.text.isEmpty()) {
  679. showMicrophoneButton(true)
  680. } else {
  681. showMicrophoneButton(false)
  682. }
  683. } catch (npe: NullPointerException) {
  684. // view binding can be null
  685. // since this is called asynchronously and UI might have been destroyed in the meantime
  686. Log.i(TAG, "UI destroyed - view binding already gone")
  687. }
  688. }
  689. var sliderInitX = 0F
  690. var downX = 0f
  691. var deltaX = 0f
  692. var voiceRecordStartTime = 0L
  693. var voiceRecordEndTime = 0L
  694. binding.messageInputView.recordAudioButton.setOnTouchListener(object : View.OnTouchListener {
  695. override fun onTouch(v: View?, event: MotionEvent?): Boolean {
  696. view.performClick()
  697. when (event?.action) {
  698. MotionEvent.ACTION_DOWN -> {
  699. if (!isRecordAudioPermissionGranted()) {
  700. requestRecordAudioPermissions()
  701. return true
  702. }
  703. if (!UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  704. UploadAndShareFilesWorker.requestStoragePermission(this@ChatController)
  705. return true
  706. }
  707. voiceRecordStartTime = System.currentTimeMillis()
  708. setVoiceRecordFileName()
  709. startAudioRecording(currentVoiceRecordFile)
  710. downX = event.x
  711. showRecordAudioUi(true)
  712. }
  713. MotionEvent.ACTION_CANCEL -> {
  714. Log.d(TAG, "ACTION_CANCEL. same as for UP")
  715. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  716. return true
  717. }
  718. stopAndDiscardAudioRecording()
  719. showRecordAudioUi(false)
  720. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  721. }
  722. MotionEvent.ACTION_UP -> {
  723. Log.d(TAG, "ACTION_UP. stop recording??")
  724. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  725. return true
  726. }
  727. showRecordAudioUi(false)
  728. voiceRecordEndTime = System.currentTimeMillis()
  729. val voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime
  730. if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) {
  731. Log.d(TAG, "voiceRecordDuration: $voiceRecordDuration")
  732. Toast.makeText(
  733. context,
  734. context.getString(R.string.nc_voice_message_hold_to_record_info),
  735. Toast.LENGTH_SHORT
  736. ).show()
  737. stopAndDiscardAudioRecording()
  738. return true
  739. } else {
  740. voiceRecordStartTime = 0L
  741. voiceRecordEndTime = 0L
  742. stopAndSendAudioRecording()
  743. }
  744. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  745. }
  746. MotionEvent.ACTION_MOVE -> {
  747. Log.d(TAG, "ACTION_MOVE.")
  748. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  749. return true
  750. }
  751. showRecordAudioUi(true)
  752. if (sliderInitX == 0.0F) {
  753. sliderInitX = binding.messageInputView.slideToCancelDescription.x
  754. }
  755. val movedX: Float = event.x
  756. deltaX = movedX - downX
  757. // only allow slide to left
  758. if (binding.messageInputView.slideToCancelDescription.x > sliderInitX) {
  759. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  760. }
  761. if (binding.messageInputView.slideToCancelDescription.x < VOICE_RECORD_CANCEL_SLIDER_X) {
  762. Log.d(TAG, "stopping recording because slider was moved to left")
  763. stopAndDiscardAudioRecording()
  764. showRecordAudioUi(false)
  765. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  766. return true
  767. } else {
  768. binding.messageInputView.slideToCancelDescription.x = binding.messageInputView
  769. .slideToCancelDescription.x + deltaX
  770. downX = movedX
  771. }
  772. }
  773. }
  774. return v?.onTouchEvent(event) ?: true
  775. }
  776. })
  777. binding.messageInputView.inputEditText?.setText(sharedText)
  778. binding.messageInputView.setAttachmentsListener {
  779. activity?.let { AttachmentDialog(it, this).show() }
  780. }
  781. binding.messageInputView.button.setOnClickListener { submitMessage(false) }
  782. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-send")) {
  783. binding.messageInputView.button.setOnLongClickListener {
  784. showSendButtonMenu()
  785. true
  786. }
  787. }
  788. binding.messageInputView.button.contentDescription = resources?.getString(
  789. R.string
  790. .nc_description_send_message_button
  791. )
  792. viewThemeUtils.platform.colorImageView(binding.messageInputView.button)
  793. if (currentConversation != null && currentConversation?.roomId != null) {
  794. loadAvatarForStatusBar()
  795. setTitle()
  796. }
  797. if (adapterWasNull) {
  798. Log.d(TAG, "starting for the first time (because adapter was null)")
  799. if (TextUtils.isEmpty(roomToken)) {
  800. handleFromNotification()
  801. } else {
  802. getRoomInfo()
  803. }
  804. }
  805. super.onViewBound(view)
  806. }
  807. private fun showSendButtonMenu() {
  808. val popupMenu = PopupMenu(
  809. ContextThemeWrapper(view?.context, R.style.ChatSendButtonMenu),
  810. binding.messageInputView.button,
  811. Gravity.END
  812. )
  813. popupMenu.inflate(R.menu.chat_send_menu)
  814. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  815. when (item.itemId) {
  816. R.id.send_without_notification -> submitMessage(true)
  817. }
  818. true
  819. }
  820. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  821. popupMenu.setForceShowIcon(true)
  822. }
  823. popupMenu.show()
  824. }
  825. private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
  826. val anchor: View? = if (isVoiceOnlyCall) {
  827. activity?.findViewById(R.id.conversation_voice_call)
  828. } else {
  829. activity?.findViewById(R.id.conversation_video_call)
  830. }
  831. if (anchor != null) {
  832. val popupMenu = PopupMenu(
  833. ContextThemeWrapper(view?.context, R.style.CallButtonMenu),
  834. anchor,
  835. Gravity.END
  836. )
  837. popupMenu.inflate(R.menu.chat_call_menu)
  838. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  839. when (item.itemId) {
  840. R.id.call_without_notification -> startACall(isVoiceOnlyCall, true)
  841. }
  842. true
  843. }
  844. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  845. popupMenu.setForceShowIcon(true)
  846. }
  847. popupMenu.show()
  848. }
  849. }
  850. private fun startPlayback(message: ChatMessage) {
  851. if (!this.isAttached) {
  852. // don't begin to play voice message if screen is not visible anymore.
  853. // this situation might happen if file is downloading but user already left the chatview.
  854. // If user returns to chatview, the old chatview instance is not attached anymore
  855. // and he has to click the play button again (which is considered to be okay)
  856. return
  857. }
  858. initMediaPlayer(message)
  859. if (!mediaPlayer!!.isPlaying) {
  860. mediaPlayer!!.start()
  861. }
  862. mediaPlayerHandler = Handler()
  863. activity?.runOnUiThread(object : Runnable {
  864. override fun run() {
  865. if (mediaPlayer != null) {
  866. val currentPosition: Int = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
  867. message.voiceMessagePlayedSeconds = currentPosition
  868. adapter?.update(message)
  869. }
  870. mediaPlayerHandler.postDelayed(this, SECOND)
  871. }
  872. })
  873. message.isDownloadingVoiceMessage = false
  874. message.isPlayingVoiceMessage = true
  875. adapter?.update(message)
  876. }
  877. private fun pausePlayback(message: ChatMessage) {
  878. if (mediaPlayer!!.isPlaying) {
  879. mediaPlayer!!.pause()
  880. }
  881. message.isPlayingVoiceMessage = false
  882. adapter?.update(message)
  883. }
  884. private fun initMediaPlayer(message: ChatMessage) {
  885. if (message != currentlyPlayedVoiceMessage) {
  886. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  887. }
  888. if (mediaPlayer == null) {
  889. val fileName = message.selectedIndividualHashMap!!["name"]
  890. val absolutePath = context.cacheDir.absolutePath + "/" + fileName
  891. mediaPlayer = MediaPlayer().apply {
  892. setDataSource(absolutePath)
  893. prepare()
  894. }
  895. currentlyPlayedVoiceMessage = message
  896. message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
  897. mediaPlayer!!.setOnCompletionListener {
  898. stopMediaPlayer(message)
  899. }
  900. } else {
  901. Log.e(TAG, "mediaPlayer was not null. This should not happen!")
  902. }
  903. }
  904. private fun stopMediaPlayer(message: ChatMessage) {
  905. message.isPlayingVoiceMessage = false
  906. message.resetVoiceMessage = true
  907. adapter?.update(message)
  908. currentlyPlayedVoiceMessage = null
  909. mediaPlayerHandler.removeCallbacksAndMessages(null)
  910. mediaPlayer?.stop()
  911. mediaPlayer?.release()
  912. mediaPlayer = null
  913. }
  914. override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
  915. if (mediaPlayer != null) {
  916. if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
  917. mediaPlayer!!.seekTo(progress * VOICE_MESSAGE_SEEKBAR_BASE)
  918. }
  919. }
  920. }
  921. @SuppressLint("LongLogTag")
  922. private fun downloadFileToCache(message: ChatMessage) {
  923. message.isDownloadingVoiceMessage = true
  924. adapter?.update(message)
  925. val baseUrl = message.activeUser!!.baseUrl
  926. val userId = message.activeUser!!.userId
  927. val attachmentFolder = CapabilitiesUtilNew.getAttachmentFolder(message.activeUser!!)
  928. val fileName = message.selectedIndividualHashMap!!["name"]
  929. var size = message.selectedIndividualHashMap!!["size"]
  930. if (size == null) {
  931. size = "-1"
  932. }
  933. val fileSize = size.toLong()
  934. val fileId = message.selectedIndividualHashMap!!["id"]
  935. val path = message.selectedIndividualHashMap!!["path"]
  936. // check if download worker is already running
  937. val workers = WorkManager.getInstance(
  938. context
  939. ).getWorkInfosByTag(fileId!!)
  940. try {
  941. for (workInfo in workers.get()) {
  942. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  943. Log.d(TAG, "Download worker for $fileId is already running or scheduled")
  944. return
  945. }
  946. }
  947. } catch (e: ExecutionException) {
  948. Log.e(TAG, "Error when checking if worker already exists", e)
  949. } catch (e: InterruptedException) {
  950. Log.e(TAG, "Error when checking if worker already exists", e)
  951. }
  952. val data: Data = Data.Builder()
  953. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
  954. .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
  955. .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
  956. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
  957. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  958. .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
  959. .build()
  960. val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  961. .setInputData(data)
  962. .addTag(fileId)
  963. .build()
  964. WorkManager.getInstance().enqueue(downloadWorker)
  965. WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
  966. .observeForever { workInfo: WorkInfo ->
  967. if (workInfo.state == WorkInfo.State.SUCCEEDED) {
  968. startPlayback(message)
  969. }
  970. }
  971. }
  972. @SuppressLint("SimpleDateFormat")
  973. private fun setVoiceRecordFileName() {
  974. val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
  975. val date: String = simpleDateFormat.format(Date())
  976. val fileNameWithoutSuffix = String.format(
  977. context.resources.getString(R.string.nc_voice_message_filename),
  978. date,
  979. currentConversation!!.displayName
  980. )
  981. val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX
  982. currentVoiceRecordFile = "${context.cacheDir.absolutePath}/$fileName"
  983. }
  984. private fun showRecordAudioUi(show: Boolean) {
  985. if (show) {
  986. binding.messageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
  987. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
  988. binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
  989. binding.messageInputView.slideToCancelDescription.visibility = View.VISIBLE
  990. binding.messageInputView.attachmentButton.visibility = View.GONE
  991. binding.messageInputView.smileyButton.visibility = View.GONE
  992. binding.messageInputView.messageInput.visibility = View.GONE
  993. binding.messageInputView.messageInput.hint = ""
  994. } else {
  995. binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
  996. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
  997. binding.messageInputView.audioRecordDuration.visibility = View.GONE
  998. binding.messageInputView.slideToCancelDescription.visibility = View.GONE
  999. binding.messageInputView.attachmentButton.visibility = View.VISIBLE
  1000. binding.messageInputView.smileyButton.visibility = View.VISIBLE
  1001. binding.messageInputView.messageInput.visibility = View.VISIBLE
  1002. binding.messageInputView.messageInput.hint =
  1003. context.resources?.getString(R.string.nc_hint_enter_a_message)
  1004. }
  1005. }
  1006. private fun isRecordAudioPermissionGranted(): Boolean {
  1007. return PermissionChecker.checkSelfPermission(
  1008. context,
  1009. Manifest.permission.RECORD_AUDIO
  1010. ) == PermissionChecker.PERMISSION_GRANTED
  1011. }
  1012. private fun startAudioRecording(file: String) {
  1013. binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
  1014. binding.messageInputView.audioRecordDuration.start()
  1015. val animation: Animation = AlphaAnimation(1.0f, 0.0f)
  1016. animation.duration = ANIMATION_DURATION
  1017. animation.interpolator = LinearInterpolator()
  1018. animation.repeatCount = Animation.INFINITE
  1019. animation.repeatMode = Animation.REVERSE
  1020. binding.messageInputView.microphoneEnabledInfo.startAnimation(animation)
  1021. recorder = MediaRecorder().apply {
  1022. setAudioSource(MediaRecorder.AudioSource.MIC)
  1023. setOutputFile(file)
  1024. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
  1025. setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
  1026. try {
  1027. prepare()
  1028. } catch (e: IOException) {
  1029. Log.e(TAG, "prepare for audio recording failed")
  1030. }
  1031. try {
  1032. start()
  1033. isVoiceRecordingInProgress = true
  1034. } catch (e: IllegalStateException) {
  1035. Log.e(TAG, "start for audio recording failed")
  1036. }
  1037. vibrate()
  1038. }
  1039. }
  1040. private fun stopAndSendAudioRecording() {
  1041. stopAudioRecording()
  1042. val uri = Uri.fromFile(File(currentVoiceRecordFile))
  1043. uploadFile(uri.toString(), true)
  1044. }
  1045. private fun stopAndDiscardAudioRecording() {
  1046. stopAudioRecording()
  1047. val cachedFile = File(currentVoiceRecordFile)
  1048. cachedFile.delete()
  1049. }
  1050. @Suppress("Detekt.TooGenericExceptionCaught")
  1051. private fun stopAudioRecording() {
  1052. binding.messageInputView.audioRecordDuration.stop()
  1053. binding.messageInputView.microphoneEnabledInfo.clearAnimation()
  1054. if (isVoiceRecordingInProgress) {
  1055. recorder?.apply {
  1056. try {
  1057. stop()
  1058. release()
  1059. isVoiceRecordingInProgress = false
  1060. Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false")
  1061. } catch (e: RuntimeException) {
  1062. Log.w(TAG, "error while stopping recorder!")
  1063. }
  1064. vibrate()
  1065. }
  1066. recorder = null
  1067. } else {
  1068. Log.e(TAG, "tried to stop audio recorder but it was not recording")
  1069. }
  1070. }
  1071. private fun vibrate() {
  1072. val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
  1073. if (Build.VERSION.SDK_INT >= O) {
  1074. vibrator.vibrate(VibrationEffect.createOneShot(SHORT_VIBRATE, VibrationEffect.DEFAULT_AMPLITUDE))
  1075. } else {
  1076. vibrator.vibrate(SHORT_VIBRATE)
  1077. }
  1078. }
  1079. private fun requestRecordAudioPermissions() {
  1080. requestPermissions(
  1081. arrayOf(
  1082. Manifest.permission.RECORD_AUDIO
  1083. ),
  1084. REQUEST_RECORD_AUDIO_PERMISSION
  1085. )
  1086. }
  1087. private fun requestCameraPermissions() {
  1088. requestPermissions(
  1089. arrayOf(
  1090. Manifest.permission.CAMERA
  1091. ),
  1092. REQUEST_CAMERA_PERMISSION
  1093. )
  1094. }
  1095. private fun requestReadContacts() {
  1096. requestPermissions(
  1097. arrayOf(
  1098. Manifest.permission.READ_CONTACTS
  1099. ),
  1100. REQUEST_READ_CONTACT_PERMISSION
  1101. )
  1102. }
  1103. private fun checkShowCallButtons() {
  1104. if (isAlive()) {
  1105. if (isReadOnlyConversation() || shouldShowLobby()) {
  1106. disableCallButtons()
  1107. } else {
  1108. enableCallButtons()
  1109. }
  1110. }
  1111. }
  1112. private fun checkShowMessageInputView() {
  1113. if (isAlive()) {
  1114. if (isReadOnlyConversation() ||
  1115. shouldShowLobby() ||
  1116. !participantPermissions.hasChatPermission()
  1117. ) {
  1118. binding.messageInputView.visibility = View.GONE
  1119. } else {
  1120. binding.messageInputView.visibility = View.VISIBLE
  1121. }
  1122. }
  1123. }
  1124. private fun shouldShowLobby(): Boolean {
  1125. if (currentConversation != null) {
  1126. return currentConversation?.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
  1127. currentConversation?.canModerate(conversationUser!!) == false &&
  1128. !participantPermissions.canIgnoreLobby()
  1129. }
  1130. return false
  1131. }
  1132. private fun disableCallButtons() {
  1133. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  1134. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1135. conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1136. conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1137. conversationVoiceCallMenuItem?.isEnabled = false
  1138. conversationVideoMenuItem?.isEnabled = false
  1139. } else {
  1140. Log.e(TAG, "call buttons were null when trying to disable them")
  1141. }
  1142. }
  1143. }
  1144. private fun enableCallButtons() {
  1145. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  1146. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1147. conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1148. conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1149. conversationVoiceCallMenuItem?.isEnabled = true
  1150. conversationVideoMenuItem?.isEnabled = true
  1151. } else {
  1152. Log.e(TAG, "call buttons were null when trying to enable them")
  1153. }
  1154. }
  1155. }
  1156. private fun isReadOnlyConversation(): Boolean {
  1157. return currentConversation?.conversationReadOnlyState != null &&
  1158. currentConversation?.conversationReadOnlyState ==
  1159. Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  1160. }
  1161. private fun checkLobbyState() {
  1162. if (currentConversation != null &&
  1163. currentConversation?.isLobbyViewApplicable(conversationUser!!) == true &&
  1164. isAlive()
  1165. ) {
  1166. if (!checkingLobbyStatus) {
  1167. getRoomInfo()
  1168. }
  1169. if (shouldShowLobby()) {
  1170. binding.lobby.lobbyView.visibility = View.VISIBLE
  1171. binding.messagesListView.visibility = View.GONE
  1172. binding.messageInputView.visibility = View.GONE
  1173. binding.progressBar.visibility = View.GONE
  1174. val sb = StringBuilder()
  1175. sb.append(resources!!.getText(R.string.nc_lobby_waiting))
  1176. .append("\n\n")
  1177. if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer !=
  1178. 0L
  1179. ) {
  1180. val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER
  1181. val stringWithStartDate = String.format(
  1182. resources!!.getString(R.string.nc_lobby_start_date),
  1183. dateUtils.getLocalDateTimeStringFromTimestamp(timestampMS)
  1184. )
  1185. val relativeTime = dateUtils.relativeStartTimeForLobby(timestampMS, resources!!)
  1186. sb.append("$stringWithStartDate - $relativeTime")
  1187. .append("\n\n")
  1188. }
  1189. sb.append(currentConversation!!.description)
  1190. binding.lobby.lobbyTextView.text = sb.toString()
  1191. } else {
  1192. binding.lobby.lobbyView.visibility = View.GONE
  1193. binding.messagesListView.visibility = View.VISIBLE
  1194. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  1195. if (isFirstMessagesProcessing && pastPreconditionFailed) {
  1196. pastPreconditionFailed = false
  1197. pullChatMessages(0)
  1198. } else if (futurePreconditionFailed) {
  1199. futurePreconditionFailed = false
  1200. pullChatMessages(1)
  1201. }
  1202. }
  1203. } else {
  1204. binding.lobby.lobbyView.visibility = View.GONE
  1205. binding.messagesListView.visibility = View.VISIBLE
  1206. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  1207. }
  1208. }
  1209. @Throws(IllegalStateException::class)
  1210. override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
  1211. if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
  1212. Log.e(TAG, "resultCode for received intent was != ok")
  1213. return
  1214. }
  1215. when (requestCode) {
  1216. REQUEST_CODE_SELECT_REMOTE_FILES -> {
  1217. val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
  1218. if (pathList?.size!! >= 1) {
  1219. pathList
  1220. .chunked(CHUNK_SIZE)
  1221. .forEach { paths ->
  1222. val data = Data.Builder()
  1223. .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id!!)
  1224. .putString(KEY_ROOM_TOKEN, roomToken)
  1225. .putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
  1226. .build()
  1227. val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
  1228. .setInputData(data)
  1229. .build()
  1230. WorkManager.getInstance().enqueue(worker)
  1231. }
  1232. }
  1233. }
  1234. REQUEST_CODE_CHOOSE_FILE -> {
  1235. try {
  1236. checkNotNull(intent)
  1237. filesToUpload.clear()
  1238. intent.clipData?.let {
  1239. for (index in 0 until it.itemCount) {
  1240. filesToUpload.add(it.getItemAt(index).uri.toString())
  1241. }
  1242. } ?: run {
  1243. checkNotNull(intent.data)
  1244. intent.data.let {
  1245. filesToUpload.add(intent.data.toString())
  1246. }
  1247. }
  1248. require(filesToUpload.isNotEmpty())
  1249. val filenamesWithLineBreaks = StringBuilder("\n")
  1250. for (file in filesToUpload) {
  1251. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1252. filenamesWithLineBreaks.append(filename).append("\n")
  1253. }
  1254. val confirmationQuestion = when (filesToUpload.size) {
  1255. 1 -> context.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
  1256. String.format(it, title.trim())
  1257. }
  1258. else -> context.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
  1259. String.format(it, title.trim())
  1260. }
  1261. }
  1262. val materialAlertDialogBuilder = MaterialAlertDialogBuilder(binding.messageInputView.context)
  1263. .setTitle(confirmationQuestion)
  1264. .setMessage(filenamesWithLineBreaks.toString())
  1265. .setPositiveButton(R.string.nc_yes) { _, _ ->
  1266. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  1267. uploadFiles(filesToUpload)
  1268. } else {
  1269. UploadAndShareFilesWorker.requestStoragePermission(this)
  1270. }
  1271. }
  1272. .setNegativeButton(R.string.nc_no) { _, _ ->
  1273. // unused atm
  1274. }
  1275. viewThemeUtils.dialog.colorMaterialAlertDialogBackground(
  1276. binding.messageInputView.context,
  1277. materialAlertDialogBuilder
  1278. )
  1279. val dialog = materialAlertDialogBuilder.show()
  1280. viewThemeUtils.platform.colorTextButtons(
  1281. dialog.getButton(AlertDialog.BUTTON_POSITIVE),
  1282. dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
  1283. )
  1284. } catch (e: IllegalStateException) {
  1285. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1286. .show()
  1287. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1288. } catch (e: IllegalArgumentException) {
  1289. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1290. .show()
  1291. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1292. }
  1293. }
  1294. REQUEST_CODE_SELECT_CONTACT -> {
  1295. val contactUri = intent?.data ?: return
  1296. val cursor: Cursor? = activity?.contentResolver!!.query(contactUri, null, null, null, null)
  1297. if (cursor != null && cursor.moveToFirst()) {
  1298. val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
  1299. val fileName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + ".vcf"
  1300. val file = File(context.cacheDir, fileName)
  1301. writeContactToVcfFile(cursor, file)
  1302. val shareUri = FileProvider.getUriForFile(
  1303. activity!!,
  1304. BuildConfig.APPLICATION_ID,
  1305. File(file.absolutePath)
  1306. )
  1307. uploadFile(shareUri.toString(), false)
  1308. }
  1309. cursor?.close()
  1310. }
  1311. REQUEST_CODE_PICK_CAMERA -> {
  1312. if (resultCode == RESULT_OK) {
  1313. try {
  1314. filesToUpload.clear()
  1315. if (intent != null && intent.data != null) {
  1316. run {
  1317. intent.data.let {
  1318. filesToUpload.add(intent.data.toString())
  1319. }
  1320. }
  1321. require(filesToUpload.isNotEmpty())
  1322. } else if (videoURI != null) {
  1323. filesToUpload.add(videoURI.toString())
  1324. videoURI = null
  1325. } else {
  1326. throw IllegalStateException("Failed to get data from intent and uri")
  1327. }
  1328. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  1329. uploadFiles(filesToUpload)
  1330. } else {
  1331. UploadAndShareFilesWorker.requestStoragePermission(this)
  1332. }
  1333. } catch (e: IllegalStateException) {
  1334. Toast.makeText(
  1335. context,
  1336. context.resources?.getString(R.string.nc_upload_failed),
  1337. Toast.LENGTH_LONG
  1338. )
  1339. .show()
  1340. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1341. } catch (e: IllegalArgumentException) {
  1342. Toast.makeText(
  1343. context,
  1344. context.resources?.getString(R.string.nc_upload_failed),
  1345. Toast.LENGTH_LONG
  1346. )
  1347. .show()
  1348. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1349. }
  1350. }
  1351. }
  1352. REQUEST_CODE_MESSAGE_SEARCH -> {
  1353. val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
  1354. messageId?.let { id ->
  1355. scrollToMessageWithId(id)
  1356. }
  1357. }
  1358. }
  1359. }
  1360. private fun scrollToMessageWithId(messageId: String) {
  1361. val position = adapter?.items?.indexOfFirst {
  1362. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  1363. }
  1364. if (position != null && position >= 0) {
  1365. binding.messagesListView.smoothScrollToPosition(position)
  1366. } else {
  1367. // TODO show error that we don't have that message?
  1368. }
  1369. }
  1370. private fun writeContactToVcfFile(cursor: Cursor, file: File) {
  1371. val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
  1372. val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
  1373. val fd: AssetFileDescriptor = activity?.contentResolver!!.openAssetFileDescriptor(uri, "r")!!
  1374. fd.use {
  1375. val fis = fd.createInputStream()
  1376. file.createNewFile()
  1377. fis.use { input ->
  1378. file.outputStream().use { output ->
  1379. input.copyTo(output)
  1380. }
  1381. }
  1382. }
  1383. }
  1384. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  1385. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
  1386. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1387. Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
  1388. if (filesToUpload.isNotEmpty()) {
  1389. uploadFiles(filesToUpload)
  1390. }
  1391. } else {
  1392. Toast
  1393. .makeText(context, context.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG)
  1394. .show()
  1395. }
  1396. } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  1397. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1398. // do nothing. user will tap on the microphone again if he wants to record audio..
  1399. } else {
  1400. Toast.makeText(
  1401. context,
  1402. context.getString(R.string.nc_voice_message_missing_audio_permission),
  1403. Toast.LENGTH_LONG
  1404. ).show()
  1405. }
  1406. } else if (requestCode == REQUEST_READ_CONTACT_PERMISSION) {
  1407. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1408. val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
  1409. startActivityForResult(intent, REQUEST_CODE_SELECT_CONTACT)
  1410. } else {
  1411. Toast.makeText(
  1412. context,
  1413. context.getString(R.string.nc_share_contact_permission),
  1414. Toast.LENGTH_LONG
  1415. ).show()
  1416. }
  1417. } else if (requestCode == REQUEST_CAMERA_PERMISSION) {
  1418. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1419. Toast
  1420. .makeText(context, context.getString(R.string.camera_permission_granted), Toast.LENGTH_LONG)
  1421. .show()
  1422. } else {
  1423. Toast
  1424. .makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
  1425. .show()
  1426. }
  1427. }
  1428. }
  1429. private fun uploadFiles(files: MutableList<String>) {
  1430. for (file in files) {
  1431. uploadFile(file, false)
  1432. }
  1433. }
  1434. private fun uploadFile(fileUri: String, isVoiceMessage: Boolean) {
  1435. var metaData = ""
  1436. if (!participantPermissions.hasChatPermission()) {
  1437. Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
  1438. return
  1439. }
  1440. if (isVoiceMessage) {
  1441. metaData = VOICE_MESSAGE_META_DATA
  1442. }
  1443. try {
  1444. require(fileUri.isNotEmpty())
  1445. UploadAndShareFilesWorker.upload(
  1446. fileUri,
  1447. roomToken!!,
  1448. currentConversation?.displayName!!,
  1449. metaData
  1450. )
  1451. } catch (e: IllegalArgumentException) {
  1452. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
  1453. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1454. }
  1455. }
  1456. fun sendSelectLocalFileIntent() {
  1457. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  1458. type = "*/*"
  1459. addCategory(Intent.CATEGORY_OPENABLE)
  1460. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  1461. }
  1462. startActivityForResult(
  1463. Intent.createChooser(
  1464. action,
  1465. context.resources?.getString(
  1466. R.string.nc_upload_choose_local_files
  1467. )
  1468. ),
  1469. REQUEST_CODE_CHOOSE_FILE
  1470. )
  1471. }
  1472. fun sendChooseContactIntent() {
  1473. requestReadContacts()
  1474. }
  1475. fun showBrowserScreen() {
  1476. val sharingFileBrowserIntent = Intent(activity, RemoteFileBrowserActivity::class.java)
  1477. startActivityForResult(sharingFileBrowserIntent, REQUEST_CODE_SELECT_REMOTE_FILES)
  1478. }
  1479. fun showShareLocationScreen() {
  1480. Log.d(TAG, "showShareLocationScreen")
  1481. val bundle = Bundle()
  1482. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1483. router.pushController(
  1484. RouterTransaction.with(LocationPickerController(bundle))
  1485. .pushChangeHandler(HorizontalChangeHandler())
  1486. .popChangeHandler(HorizontalChangeHandler())
  1487. )
  1488. }
  1489. private fun showConversationInfoScreen() {
  1490. val bundle = Bundle()
  1491. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
  1492. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1493. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
  1494. router.pushController(
  1495. RouterTransaction.with(ConversationInfoController(bundle))
  1496. .pushChangeHandler(HorizontalChangeHandler())
  1497. .popChangeHandler(HorizontalChangeHandler())
  1498. )
  1499. }
  1500. private fun setupMentionAutocomplete() {
  1501. if (isAlive()) {
  1502. val elevation = MENTION_AUTO_COMPLETE_ELEVATION
  1503. resources?.let {
  1504. val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
  1505. val presenter = MentionAutocompletePresenter(activity, roomToken)
  1506. val callback = MentionAutocompleteCallback(
  1507. activity,
  1508. conversationUser!!,
  1509. binding.messageInputView.inputEditText,
  1510. viewThemeUtils
  1511. )
  1512. if (mentionAutocomplete == null && binding.messageInputView.inputEditText != null) {
  1513. mentionAutocomplete = Autocomplete.on<Mention>(binding.messageInputView.inputEditText)
  1514. .with(elevation)
  1515. .with(backgroundDrawable)
  1516. .with(MagicCharPolicy('@'))
  1517. .with(presenter)
  1518. .with(callback)
  1519. .build()
  1520. }
  1521. }
  1522. }
  1523. }
  1524. private fun validSessionId(): Boolean {
  1525. return currentConversation != null &&
  1526. !TextUtils.isEmpty(currentConversation?.sessionId) &&
  1527. currentConversation?.sessionId != "0"
  1528. }
  1529. @Suppress("Detekt.TooGenericExceptionCaught")
  1530. override fun onAttach(view: View) {
  1531. super.onAttach(view)
  1532. logConversationInfos("onAttach")
  1533. eventBus.register(this)
  1534. if (conversationUser?.userId != "?" &&
  1535. CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag") &&
  1536. activity != null
  1537. ) {
  1538. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener { v -> showConversationInfoScreen() }
  1539. }
  1540. ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  1541. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
  1542. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  1543. val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
  1544. emojiPopup = binding.messageInputView.inputEditText?.let {
  1545. EmojiPopup(
  1546. rootView = view,
  1547. editText = it,
  1548. onEmojiPopupShownListener = {
  1549. if (resources != null) {
  1550. smileyButton?.setImageDrawable(
  1551. ContextCompat.getDrawable(context, R.drawable.ic_baseline_keyboard_24)
  1552. )
  1553. }
  1554. },
  1555. onEmojiPopupDismissListener = {
  1556. smileyButton?.setImageDrawable(
  1557. ContextCompat.getDrawable(context, R.drawable.ic_insert_emoticon_black_24dp)
  1558. )
  1559. },
  1560. onEmojiClickListener = {
  1561. try {
  1562. binding.messageInputView.inputEditText?.editableText?.append(" ")
  1563. } catch (npe: NullPointerException) {
  1564. // view binding can be null
  1565. // since this is called asynchronously and UI might have been destroyed in the meantime
  1566. Log.i(WebViewLoginController.TAG, "UI destroyed - view binding already gone")
  1567. }
  1568. }
  1569. )
  1570. }
  1571. smileyButton?.setOnClickListener {
  1572. emojiPopup?.toggle()
  1573. }
  1574. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
  1575. cancelReply()
  1576. }
  1577. viewThemeUtils.platform
  1578. .themeImageButton(binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton))
  1579. cancelNotificationsForCurrentConversation()
  1580. Log.d(TAG, "onAttach inConversation: $inConversation")
  1581. if (inConversation) {
  1582. Log.d(TAG, "execute joinRoomWithPassword in onAttach")
  1583. joinRoomWithPassword()
  1584. // replace with getRoomInfo() ? otherwise getRoomInfo is not called periodically after coming
  1585. // back to app when it was in background
  1586. }
  1587. }
  1588. private fun cancelReply() {
  1589. binding.messageInputView.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility = View.GONE
  1590. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
  1591. }
  1592. @Suppress("Detekt.TooGenericExceptionCaught")
  1593. private fun cancelNotificationsForCurrentConversation() {
  1594. if (conversationUser != null) {
  1595. if (!TextUtils.isEmpty(roomToken)) {
  1596. try {
  1597. NotificationUtils.cancelExistingNotificationsForRoom(
  1598. applicationContext,
  1599. conversationUser,
  1600. roomToken!!
  1601. )
  1602. } catch (e: RuntimeException) {
  1603. Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
  1604. }
  1605. }
  1606. }
  1607. }
  1608. override fun onDetach(view: View) {
  1609. super.onDetach(view)
  1610. logConversationInfos("onDetach")
  1611. eventBus.unregister(this)
  1612. if (activity != null) {
  1613. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1614. }
  1615. checkingLobbyStatus = false
  1616. if (lobbyTimerHandler != null) {
  1617. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  1618. }
  1619. if (conversationUser != null && isActivityNotChangingConfigurations() && isNotInCall()) {
  1620. ApplicationWideCurrentRoomHolder.getInstance().clear()
  1621. if (inConversation && validSessionId()) {
  1622. leaveRoom(null, null)
  1623. } else {
  1624. Log.d(TAG, "not leaving room (inConversation is false and/or validSessionId is false)")
  1625. // room might have already been left...
  1626. }
  1627. } else {
  1628. Log.e(TAG, "not leaving room...")
  1629. }
  1630. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  1631. mentionAutocomplete?.dismissPopup()
  1632. }
  1633. }
  1634. private fun isActivityNotChangingConfigurations(): Boolean {
  1635. return activity != null && !activity?.isChangingConfigurations!!
  1636. }
  1637. private fun isNotInCall(): Boolean {
  1638. return !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
  1639. !ApplicationWideCurrentRoomHolder.getInstance().isDialing
  1640. }
  1641. override val title: String
  1642. get() =
  1643. if (currentConversation?.displayName != null) {
  1644. try {
  1645. " " + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  1646. } catch (e: IllegalStateException) {
  1647. " " + currentConversation?.displayName
  1648. }
  1649. } else {
  1650. ""
  1651. }
  1652. public override fun onDestroy() {
  1653. super.onDestroy()
  1654. logConversationInfos("onDestroy")
  1655. if (activity != null) {
  1656. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1657. }
  1658. if (actionBar != null) {
  1659. actionBar?.setIcon(null)
  1660. }
  1661. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  1662. adapter = null
  1663. inConversation = false
  1664. Log.d(TAG, "inConversation was set to false!")
  1665. }
  1666. private fun joinRoomWithPassword() {
  1667. Log.d(TAG, "joinRoomWithPassword. currentConversation==null ?:" + (currentConversation == null).toString())
  1668. if (!validSessionId()) {
  1669. Log.d(TAG, "sessionID was not valid -> joinRoom")
  1670. var apiVersion = 1
  1671. // FIXME Fix API checking with guests?
  1672. if (conversationUser != null) {
  1673. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1674. }
  1675. val startNanoTime = System.nanoTime()
  1676. Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime")
  1677. ncApi.joinRoom(
  1678. credentials,
  1679. ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken),
  1680. roomPassword
  1681. )
  1682. ?.subscribeOn(Schedulers.io())
  1683. ?.observeOn(AndroidSchedulers.mainThread())
  1684. ?.retry(RETRIES)
  1685. ?.subscribe(object : Observer<RoomOverall> {
  1686. override fun onSubscribe(d: Disposable) {
  1687. disposables.add(d)
  1688. }
  1689. @Suppress("Detekt.TooGenericExceptionCaught")
  1690. override fun onNext(roomOverall: RoomOverall) {
  1691. Log.d(TAG, "joinRoomWithPassword - joinRoom - got response: $startNanoTime")
  1692. inConversation = true
  1693. currentConversation?.sessionId = roomOverall.ocs!!.data!!.sessionId
  1694. logConversationInfos("joinRoomWithPassword#onNext")
  1695. ApplicationWideCurrentRoomHolder.getInstance().session =
  1696. currentConversation?.sessionId
  1697. setupWebsocket()
  1698. try {
  1699. checkLobbyState()
  1700. } catch (npe: NullPointerException) {
  1701. // view binding can be null
  1702. // since this is called asynchronously and UI might have been destroyed in the meantime
  1703. Log.i(TAG, "UI destroyed - view binding already gone")
  1704. }
  1705. if (isFirstMessagesProcessing) {
  1706. pullChatMessages(0)
  1707. } else {
  1708. pullChatMessages(1, 0)
  1709. }
  1710. if (magicWebSocketInstance != null) {
  1711. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1712. roomToken,
  1713. currentConversation?.sessionId
  1714. )
  1715. }
  1716. if (startCallFromNotification != null && startCallFromNotification ?: false) {
  1717. startCallFromNotification = false
  1718. startACall(voiceOnly, false)
  1719. }
  1720. }
  1721. override fun onError(e: Throwable) {
  1722. Log.e(TAG, "joinRoomWithPassword - joinRoom - ERROR", e)
  1723. }
  1724. override fun onComplete() {
  1725. // unused atm
  1726. }
  1727. })
  1728. } else {
  1729. Log.d(TAG, "sessionID was valid -> skip joinRoom")
  1730. inConversation = true
  1731. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId
  1732. if (magicWebSocketInstance != null) {
  1733. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1734. roomToken,
  1735. currentConversation?.sessionId
  1736. )
  1737. }
  1738. checkLobbyState()
  1739. if (isFirstMessagesProcessing) {
  1740. pullChatMessages(0)
  1741. } else {
  1742. pullChatMessages(1)
  1743. }
  1744. }
  1745. }
  1746. fun leaveRoom(
  1747. remapChatModel: RemapChatModel?,
  1748. funToCallWhenLeaveSuccessful: ((RemapChatModel) -> Unit)?
  1749. ) {
  1750. logConversationInfos("leaveRoom")
  1751. var apiVersion = 1
  1752. // FIXME Fix API checking with guests?
  1753. if (conversationUser != null) {
  1754. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1755. }
  1756. val startNanoTime = System.nanoTime()
  1757. Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime")
  1758. ncApi.leaveRoom(
  1759. credentials,
  1760. ApiUtils.getUrlForParticipantsActive(
  1761. apiVersion,
  1762. conversationUser?.baseUrl,
  1763. roomToken
  1764. )
  1765. )
  1766. ?.subscribeOn(Schedulers.io())
  1767. ?.observeOn(AndroidSchedulers.mainThread())
  1768. ?.subscribe(object : Observer<GenericOverall> {
  1769. override fun onSubscribe(d: Disposable) {
  1770. disposables.add(d)
  1771. }
  1772. override fun onNext(genericOverall: GenericOverall) {
  1773. Log.d(TAG, "leaveRoom - leaveRoom - got response: $startNanoTime")
  1774. logConversationInfos("leaveRoom#onNext")
  1775. checkingLobbyStatus = false
  1776. if (lobbyTimerHandler != null) {
  1777. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  1778. }
  1779. if (magicWebSocketInstance != null && currentConversation != null) {
  1780. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1781. "",
  1782. currentConversation?.sessionId
  1783. )
  1784. } else {
  1785. Log.e(TAG, "magicWebSocketInstance or currentConversation were null! Failed to leave the room!")
  1786. if (BuildConfig.DEBUG) {
  1787. Toast.makeText(
  1788. context,
  1789. "magicWebSocketInstance or currentConversation were null! Failed to leave the room!",
  1790. Toast.LENGTH_LONG
  1791. ).show()
  1792. }
  1793. }
  1794. currentConversation?.sessionId = "0"
  1795. if (remapChatModel != null && funToCallWhenLeaveSuccessful != null) {
  1796. Log.d(TAG, "a callback action was set and is now executed because room was left successfully")
  1797. funToCallWhenLeaveSuccessful(remapChatModel)
  1798. } else {
  1799. Log.d(TAG, "remapChatController was not set")
  1800. }
  1801. }
  1802. override fun onError(e: Throwable) {
  1803. Log.e(TAG, "leaveRoom - leaveRoom - ERROR", e)
  1804. }
  1805. override fun onComplete() {
  1806. Log.d(TAG, "leaveRoom - leaveRoom - completed: $startNanoTime")
  1807. disposables.dispose()
  1808. }
  1809. })
  1810. }
  1811. private fun submitMessage(sendWithoutNotification: Boolean) {
  1812. if (binding.messageInputView.inputEditText != null) {
  1813. val editable = binding.messageInputView.inputEditText!!.editableText
  1814. val mentionSpans = editable.getSpans(
  1815. 0,
  1816. editable.length,
  1817. Spans.MentionChipSpan::class.java
  1818. )
  1819. var mentionSpan: Spans.MentionChipSpan
  1820. for (i in mentionSpans.indices) {
  1821. mentionSpan = mentionSpans[i]
  1822. var mentionId = mentionSpan.id
  1823. if (mentionId.contains(" ") || mentionId.startsWith("guest/")) {
  1824. mentionId = "\"" + mentionId + "\""
  1825. }
  1826. editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
  1827. }
  1828. binding.messageInputView.inputEditText?.setText("")
  1829. val replyMessageId: Int? = view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
  1830. sendMessage(
  1831. editable,
  1832. if (
  1833. view
  1834. ?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  1835. ?.visibility == View.VISIBLE
  1836. ) replyMessageId else null,
  1837. sendWithoutNotification
  1838. )
  1839. cancelReply()
  1840. }
  1841. }
  1842. private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
  1843. if (conversationUser != null) {
  1844. val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1845. ncApi.sendChatMessage(
  1846. credentials,
  1847. ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, roomToken),
  1848. message,
  1849. conversationUser.displayName,
  1850. replyTo,
  1851. sendWithoutNotification
  1852. )
  1853. ?.subscribeOn(Schedulers.io())
  1854. ?.observeOn(AndroidSchedulers.mainThread())
  1855. ?.subscribe(object : Observer<GenericOverall> {
  1856. override fun onSubscribe(d: Disposable) {
  1857. // unused atm
  1858. }
  1859. @Suppress("Detekt.TooGenericExceptionCaught")
  1860. override fun onNext(genericOverall: GenericOverall) {
  1861. myFirstMessage = message
  1862. try {
  1863. if (binding.popupBubbleView.isShown) {
  1864. binding.popupBubbleView.hide()
  1865. }
  1866. binding.messagesListView.smoothScrollToPosition(0)
  1867. } catch (npe: NullPointerException) {
  1868. // view binding can be null
  1869. // since this is called asynchronously and UI might have been destroyed in the meantime
  1870. Log.i(TAG, "UI destroyed - view binding already gone")
  1871. }
  1872. }
  1873. override fun onError(e: Throwable) {
  1874. if (e is HttpException) {
  1875. val code = e.code()
  1876. if (code.toString().startsWith("2")) {
  1877. myFirstMessage = message
  1878. if (binding.popupBubbleView.isShown) {
  1879. binding.popupBubbleView.hide()
  1880. }
  1881. binding.messagesListView.smoothScrollToPosition(0)
  1882. }
  1883. }
  1884. }
  1885. override fun onComplete() {
  1886. // unused atm
  1887. }
  1888. })
  1889. }
  1890. showMicrophoneButton(true)
  1891. }
  1892. private fun setupWebsocket() {
  1893. if (conversationUser != null) {
  1894. magicWebSocketInstance =
  1895. if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id!!) != null) {
  1896. WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id!!)
  1897. } else {
  1898. Log.d(TAG, "magicWebSocketInstance became null")
  1899. null
  1900. }
  1901. }
  1902. }
  1903. fun pullChatMessages(lookIntoFuture: Int, setReadMarker: Int = 1, xChatLastCommonRead: Int? = null) {
  1904. if (!inConversation) {
  1905. return
  1906. }
  1907. if (pullChatMessagesPending) {
  1908. // Sometimes pullChatMessages may be called before response to a previous call is received.
  1909. // In such cases just ignore the second call. Message processing will continue when response to the
  1910. // earlier call is received.
  1911. // More details: https://github.com/nextcloud/talk-android/pull/1766
  1912. Log.d(TAG, "pullChatMessages - pullChatMessagesPending is true, exiting")
  1913. return
  1914. }
  1915. pullChatMessagesPending = true
  1916. val fieldMap = HashMap<String, Int>()
  1917. fieldMap["includeLastKnown"] = 0
  1918. if (lookIntoFuture > 0) {
  1919. lookingIntoFuture = true
  1920. } else if (isFirstMessagesProcessing) {
  1921. if (currentConversation != null) {
  1922. globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage
  1923. globalLastKnownPastMessageId = currentConversation!!.lastReadMessage
  1924. fieldMap["includeLastKnown"] = 1
  1925. }
  1926. }
  1927. val timeout = if (lookingIntoFuture) {
  1928. LOOKING_INTO_FUTURE_TIMEOUT
  1929. } else {
  1930. 0
  1931. }
  1932. fieldMap["timeout"] = timeout
  1933. fieldMap["lookIntoFuture"] = lookIntoFuture
  1934. fieldMap["limit"] = MESSAGE_PULL_LIMIT
  1935. fieldMap["setReadMarker"] = setReadMarker
  1936. val lastKnown = if (lookIntoFuture > 0) {
  1937. globalLastKnownFutureMessageId
  1938. } else {
  1939. globalLastKnownPastMessageId
  1940. }
  1941. fieldMap["lastKnownMessageId"] = lastKnown
  1942. xChatLastCommonRead?.let {
  1943. fieldMap["lastCommonReadId"] = it
  1944. }
  1945. var apiVersion = 1
  1946. // FIXME this is a best guess, guests would need to get the capabilities themselves
  1947. if (conversationUser != null) {
  1948. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1949. }
  1950. if (lookIntoFuture > 0) {
  1951. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - calling")
  1952. ncApi.pullChatMessages(
  1953. credentials,
  1954. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken),
  1955. fieldMap
  1956. )
  1957. ?.subscribeOn(Schedulers.io())
  1958. ?.observeOn(AndroidSchedulers.mainThread())
  1959. ?.subscribe(object : Observer<Response<*>> {
  1960. override fun onSubscribe(d: Disposable) {
  1961. disposables.add(d)
  1962. }
  1963. @Suppress("Detekt.TooGenericExceptionCaught")
  1964. override fun onNext(response: Response<*>) {
  1965. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - got response")
  1966. pullChatMessagesPending = false
  1967. try {
  1968. if (response.code() == HTTP_CODE_NOT_MODIFIED) {
  1969. Log.d(TAG, "pullChatMessages - quasi recursive call to pullChatMessages")
  1970. pullChatMessages(1, setReadMarker, xChatLastCommonRead)
  1971. } else if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
  1972. futurePreconditionFailed = true
  1973. } else {
  1974. processMessagesResponse(response, true)
  1975. }
  1976. } catch (npe: NullPointerException) {
  1977. // view binding can be null
  1978. // since this is called asynchronously and UI might have been destroyed in the meantime
  1979. Log.i(TAG, "UI destroyed - view binding already gone")
  1980. }
  1981. processExpiredMessages()
  1982. }
  1983. override fun onError(e: Throwable) {
  1984. Log.e(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - ERROR", e)
  1985. pullChatMessagesPending = false
  1986. }
  1987. override fun onComplete() {
  1988. pullChatMessagesPending = false
  1989. }
  1990. })
  1991. } else {
  1992. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - calling")
  1993. ncApi.pullChatMessages(
  1994. credentials,
  1995. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken),
  1996. fieldMap
  1997. )
  1998. ?.subscribeOn(Schedulers.io())
  1999. ?.observeOn(AndroidSchedulers.mainThread())
  2000. ?.subscribe(object : Observer<Response<*>> {
  2001. override fun onSubscribe(d: Disposable) {
  2002. disposables.add(d)
  2003. }
  2004. @Suppress("Detekt.TooGenericExceptionCaught")
  2005. override fun onNext(response: Response<*>) {
  2006. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - got response")
  2007. pullChatMessagesPending = false
  2008. try {
  2009. if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
  2010. pastPreconditionFailed = true
  2011. } else {
  2012. processMessagesResponse(response, false)
  2013. }
  2014. } catch (e: NullPointerException) {
  2015. // view binding can be null
  2016. // since this is called asynchronously and UI might have been destroyed in the meantime
  2017. Log.i(TAG, "UI destroyed - view binding already gone", e)
  2018. }
  2019. processExpiredMessages()
  2020. }
  2021. override fun onError(e: Throwable) {
  2022. Log.e(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - ERROR", e)
  2023. pullChatMessagesPending = false
  2024. }
  2025. override fun onComplete() {
  2026. pullChatMessagesPending = false
  2027. }
  2028. })
  2029. }
  2030. }
  2031. private fun processExpiredMessages() {
  2032. fun deleteExpiredMessages() {
  2033. val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
  2034. val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
  2035. if (adapter?.items != null) {
  2036. for (itemWrapper in adapter?.items!!) {
  2037. if (itemWrapper.item is ChatMessage) {
  2038. val chatMessage = itemWrapper.item as ChatMessage
  2039. if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
  2040. messagesToDelete.add(chatMessage)
  2041. }
  2042. }
  2043. }
  2044. adapter!!.delete(messagesToDelete)
  2045. adapter!!.notifyDataSetChanged()
  2046. }
  2047. }
  2048. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")) {
  2049. deleteExpiredMessages()
  2050. }
  2051. }
  2052. private fun processMessagesResponse(response: Response<*>, isFromTheFuture: Boolean) {
  2053. val xChatLastCommonRead = response.headers()["X-Chat-Last-Common-Read"]?.let {
  2054. Integer.parseInt(it)
  2055. }
  2056. processHeaderChatLastGiven(response, isFromTheFuture)
  2057. if (response.code() == HTTP_CODE_OK) {
  2058. val chatOverall = response.body() as ChatOverall?
  2059. val chatMessageList = handleSystemMessages(chatOverall?.ocs!!.data!!)
  2060. processMessages(chatMessageList, isFromTheFuture, xChatLastCommonRead)
  2061. } else if (response.code() == HTTP_CODE_NOT_MODIFIED && !isFromTheFuture) {
  2062. if (isFirstMessagesProcessing) {
  2063. cancelNotificationsForCurrentConversation()
  2064. isFirstMessagesProcessing = false
  2065. binding.progressBar.visibility = View.GONE
  2066. }
  2067. historyRead = true
  2068. if (!lookingIntoFuture && inConversation) {
  2069. pullChatMessages(1)
  2070. }
  2071. }
  2072. }
  2073. private fun processMessages(
  2074. chatMessageList: List<ChatMessage>,
  2075. isFromTheFuture: Boolean,
  2076. xChatLastCommonRead: Int?
  2077. ) {
  2078. if (chatMessageList.isNotEmpty() &&
  2079. ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
  2080. ) {
  2081. adapter?.clear()
  2082. adapter?.notifyDataSetChanged()
  2083. }
  2084. if (isFirstMessagesProcessing) {
  2085. cancelNotificationsForCurrentConversation()
  2086. isFirstMessagesProcessing = false
  2087. binding.progressBar.visibility = View.GONE
  2088. binding.messagesListView.visibility = View.VISIBLE
  2089. }
  2090. if (isFromTheFuture) {
  2091. processMessagesFromTheFuture(chatMessageList)
  2092. } else {
  2093. processMessagesNotFromTheFuture(chatMessageList)
  2094. }
  2095. updateReadStatusOfAllMessages(xChatLastCommonRead)
  2096. adapter?.notifyDataSetChanged()
  2097. if (inConversation) {
  2098. pullChatMessages(1, 1, xChatLastCommonRead)
  2099. }
  2100. }
  2101. private fun updateReadStatusOfAllMessages(xChatLastCommonRead: Int?) {
  2102. for (message in adapter!!.items) {
  2103. xChatLastCommonRead?.let {
  2104. updateReadStatusOfMessage(message, it)
  2105. }
  2106. }
  2107. }
  2108. private fun updateReadStatusOfMessage(
  2109. message: MessagesListAdapter<IMessage>.Wrapper<Any>,
  2110. xChatLastCommonRead: Int
  2111. ) {
  2112. if (message.item is ChatMessage) {
  2113. val chatMessage = message.item as ChatMessage
  2114. if (chatMessage.jsonMessageId <= xChatLastCommonRead) {
  2115. chatMessage.readStatus = ReadStatus.READ
  2116. } else {
  2117. chatMessage.readStatus = ReadStatus.SENT
  2118. }
  2119. }
  2120. }
  2121. private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>) {
  2122. val shouldAddNewMessagesNotice = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty()
  2123. if (shouldAddNewMessagesNotice) {
  2124. val unreadChatMessage = ChatMessage()
  2125. unreadChatMessage.jsonMessageId = -1
  2126. unreadChatMessage.actorId = "-1"
  2127. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  2128. unreadChatMessage.message = context.getString(R.string.nc_new_messages)
  2129. adapter?.addToStart(unreadChatMessage, false)
  2130. }
  2131. determinePreviousMessageIds(chatMessageList)
  2132. addMessagesToAdapter(shouldAddNewMessagesNotice, chatMessageList)
  2133. if (shouldAddNewMessagesNotice && adapter != null) {
  2134. layoutManager?.scrollToPositionWithOffset(
  2135. adapter!!.getMessagePositionByIdInReverse("-1"),
  2136. binding.messagesListView.height / 2
  2137. )
  2138. }
  2139. }
  2140. private fun addMessagesToAdapter(
  2141. shouldAddNewMessagesNotice: Boolean,
  2142. chatMessageList: List<ChatMessage>
  2143. ) {
  2144. val isThereANewNotice =
  2145. shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1
  2146. for (chatMessage in chatMessageList) {
  2147. chatMessage.activeUser = conversationUser
  2148. val shouldScroll =
  2149. !isThereANewNotice &&
  2150. !shouldAddNewMessagesNotice &&
  2151. layoutManager?.findFirstVisibleItemPosition() == 0 ||
  2152. adapter != null &&
  2153. adapter?.itemCount == 0
  2154. modifyMessageCount(shouldAddNewMessagesNotice, shouldScroll)
  2155. adapter?.let {
  2156. chatMessage.isGrouped = (
  2157. it.isPreviousSameAuthor(
  2158. chatMessage.actorId,
  2159. -1
  2160. ) && it.getSameAuthorLastMessagesCount(chatMessage.actorId) %
  2161. GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
  2162. )
  2163. chatMessage.isOneToOneConversation =
  2164. (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  2165. it.addToStart(chatMessage, shouldScroll)
  2166. }
  2167. }
  2168. }
  2169. private fun modifyMessageCount(shouldAddNewMessagesNotice: Boolean, shouldScroll: Boolean) {
  2170. if (!shouldAddNewMessagesNotice && !shouldScroll) {
  2171. if (!binding.popupBubbleView.isShown) {
  2172. newMessagesCount = 1
  2173. binding.popupBubbleView.show()
  2174. } else if (binding.popupBubbleView.isShown) {
  2175. newMessagesCount++
  2176. }
  2177. } else {
  2178. newMessagesCount = 0
  2179. }
  2180. }
  2181. private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
  2182. var countGroupedMessages = 0
  2183. determinePreviousMessageIds(chatMessageList)
  2184. for (i in chatMessageList.indices) {
  2185. if (chatMessageList.size > i + 1) {
  2186. if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
  2187. chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
  2188. countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
  2189. ) {
  2190. chatMessageList[i].isGrouped = true
  2191. countGroupedMessages++
  2192. } else {
  2193. countGroupedMessages = 0
  2194. }
  2195. }
  2196. val chatMessage = chatMessageList[i]
  2197. chatMessage.isOneToOneConversation =
  2198. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2199. chatMessage.activeUser = conversationUser
  2200. }
  2201. if (adapter != null) {
  2202. adapter?.addToEnd(chatMessageList, false)
  2203. }
  2204. scrollToRequestedMessageIfNeeded()
  2205. }
  2206. private fun determinePreviousMessageIds(chatMessageList: List<ChatMessage>) {
  2207. var previousMessageId = NO_PREVIOUS_MESSAGE_ID
  2208. for (i in chatMessageList.indices.reversed()) {
  2209. val chatMessage = chatMessageList[i]
  2210. if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2211. chatMessage.previousMessageId = previousMessageId
  2212. } else if (adapter?.isEmpty != true) {
  2213. if (adapter!!.items[0].item is ChatMessage) {
  2214. chatMessage.previousMessageId = (adapter!!.items[0].item as ChatMessage).jsonMessageId
  2215. } else if (adapter!!.items.size > 1 && adapter!!.items[1].item is ChatMessage) {
  2216. chatMessage.previousMessageId = (adapter!!.items[1].item as ChatMessage).jsonMessageId
  2217. }
  2218. }
  2219. previousMessageId = chatMessage.jsonMessageId
  2220. }
  2221. }
  2222. private fun processHeaderChatLastGiven(response: Response<*>, isFromTheFuture: Boolean) {
  2223. val xChatLastGivenHeader: String? = response.headers()["X-Chat-Last-Given"]
  2224. val header = if (response.headers().size > 0 &&
  2225. xChatLastGivenHeader?.isNotEmpty() == true
  2226. ) {
  2227. xChatLastGivenHeader.toInt()
  2228. } else {
  2229. return
  2230. }
  2231. if (header > 0) {
  2232. if (isFromTheFuture) {
  2233. globalLastKnownFutureMessageId = header
  2234. } else {
  2235. if (globalLastKnownFutureMessageId == -1) {
  2236. globalLastKnownFutureMessageId = header
  2237. }
  2238. globalLastKnownPastMessageId = header
  2239. }
  2240. }
  2241. }
  2242. private fun scrollToRequestedMessageIfNeeded() {
  2243. args.getString(BundleKeys.KEY_MESSAGE_ID)?.let {
  2244. scrollToMessageWithId(it)
  2245. }
  2246. }
  2247. private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean {
  2248. return TextUtils.isEmpty(messageLeft.systemMessage) &&
  2249. TextUtils.isEmpty(messageRight.systemMessage) &&
  2250. DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt)
  2251. }
  2252. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  2253. if (!historyRead && inConversation) {
  2254. pullChatMessages(0)
  2255. }
  2256. }
  2257. override fun format(date: Date): String {
  2258. return if (DateFormatter.isToday(date)) {
  2259. resources!!.getString(R.string.nc_date_header_today)
  2260. } else if (DateFormatter.isYesterday(date)) {
  2261. resources!!.getString(R.string.nc_date_header_yesterday)
  2262. } else {
  2263. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  2264. }
  2265. }
  2266. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  2267. super.onCreateOptionsMenu(menu, inflater)
  2268. inflater.inflate(R.menu.menu_conversation, menu)
  2269. viewThemeUtils.platform.colorToolbarMenuIcon(
  2270. binding.messageInputView.context,
  2271. menu.findItem(R.id.conversation_voice_call)
  2272. )
  2273. viewThemeUtils.platform.colorToolbarMenuIcon(
  2274. binding.messageInputView.context,
  2275. menu.findItem(R.id.conversation_video_call)
  2276. )
  2277. if (conversationUser?.userId == "?") {
  2278. menu.removeItem(R.id.conversation_info)
  2279. } else {
  2280. conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
  2281. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
  2282. conversationSharedItemsItem = menu.findItem(R.id.shared_items)
  2283. } else {
  2284. menu.removeItem(R.id.shared_items)
  2285. }
  2286. loadAvatarForStatusBar()
  2287. }
  2288. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  2289. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  2290. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  2291. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-call")) {
  2292. Handler().post {
  2293. activity?.findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
  2294. showCallButtonMenu(true)
  2295. true
  2296. }
  2297. }
  2298. Handler().post {
  2299. activity?.findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
  2300. showCallButtonMenu(false)
  2301. true
  2302. }
  2303. }
  2304. }
  2305. } else {
  2306. menu.removeItem(R.id.conversation_video_call)
  2307. menu.removeItem(R.id.conversation_voice_call)
  2308. }
  2309. }
  2310. override fun onPrepareOptionsMenu(menu: Menu) {
  2311. super.onPrepareOptionsMenu(menu)
  2312. conversationUser?.let {
  2313. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(it, "read-only-rooms")) {
  2314. checkShowCallButtons()
  2315. }
  2316. val searchItem = menu.findItem(R.id.conversation_search)
  2317. searchItem.isVisible = CapabilitiesUtilNew.isUnifiedSearchAvailable(it)
  2318. }
  2319. }
  2320. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  2321. return when (item.itemId) {
  2322. android.R.id.home -> {
  2323. (activity as MainActivity).resetConversationsList()
  2324. true
  2325. }
  2326. R.id.conversation_video_call -> {
  2327. startACall(false, false)
  2328. true
  2329. }
  2330. R.id.conversation_voice_call -> {
  2331. startACall(true, false)
  2332. true
  2333. }
  2334. R.id.conversation_info -> {
  2335. showConversationInfoScreen()
  2336. true
  2337. }
  2338. R.id.shared_items -> {
  2339. showSharedItems()
  2340. true
  2341. }
  2342. R.id.conversation_search -> {
  2343. startMessageSearch()
  2344. true
  2345. }
  2346. else -> super.onOptionsItemSelected(item)
  2347. }
  2348. }
  2349. private fun showSharedItems() {
  2350. val intent = Intent(activity, SharedItemsActivity::class.java)
  2351. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2352. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2353. intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable)
  2354. intent.putExtra(
  2355. SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
  2356. currentConversation?.isParticipantOwnerOrModerator
  2357. )
  2358. activity!!.startActivity(intent)
  2359. }
  2360. private fun startMessageSearch() {
  2361. val intent = Intent(activity, MessageSearchActivity::class.java)
  2362. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2363. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2364. startActivityForResult(intent, REQUEST_CODE_MESSAGE_SEARCH)
  2365. }
  2366. private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2367. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2368. val chatMessageIterator = chatMessageMap.iterator()
  2369. while (chatMessageIterator.hasNext()) {
  2370. val currentMessage = chatMessageIterator.next()
  2371. // setDeletionFlagsAndRemoveInfomessages
  2372. if (isInfoMessageAboutDeletion(currentMessage)) {
  2373. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) {
  2374. // if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture),
  2375. // the message to delete has to be modified directly inside the adapter
  2376. setMessageAsDeleted(currentMessage.value.parentMessage)
  2377. } else {
  2378. chatMessageMap[currentMessage.value.parentMessage!!.id]!!.isDeleted = true
  2379. }
  2380. chatMessageIterator.remove()
  2381. }
  2382. // delete reactions system messages
  2383. else if (isReactionsMessage(currentMessage)) {
  2384. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) {
  2385. updateAdapterForReaction(currentMessage.value.parentMessage)
  2386. }
  2387. chatMessageIterator.remove()
  2388. }
  2389. // delete poll system messages
  2390. else if (isPollVotedMessage(currentMessage)) {
  2391. chatMessageIterator.remove()
  2392. }
  2393. }
  2394. return chatMessageMap.values.toList()
  2395. }
  2396. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2397. return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
  2398. .SystemMessageType.MESSAGE_DELETED
  2399. }
  2400. private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2401. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
  2402. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
  2403. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
  2404. }
  2405. private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2406. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
  2407. }
  2408. private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
  2409. val pp = ParticipantPermissions(conversationUser!!, currentConversation!!)
  2410. if (!pp.canStartCall() && currentConversation?.hasCall == false) {
  2411. Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
  2412. } else {
  2413. ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
  2414. val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
  2415. if (callIntent != null) {
  2416. startActivity(callIntent)
  2417. }
  2418. }
  2419. }
  2420. private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
  2421. currentConversation?.let {
  2422. val bundle = Bundle()
  2423. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  2424. bundle.putString(KEY_ROOM_ID, roomId)
  2425. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2426. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  2427. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
  2428. bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
  2429. bundle.putBoolean(
  2430. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
  2431. participantPermissions.canPublishAudio()
  2432. )
  2433. bundle.putBoolean(
  2434. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
  2435. participantPermissions.canPublishVideo()
  2436. )
  2437. if (isVoiceOnlyCall) {
  2438. bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
  2439. }
  2440. if (callWithoutNotification) {
  2441. bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true)
  2442. }
  2443. return if (activity != null) {
  2444. val callIntent = Intent(activity, CallActivity::class.java)
  2445. callIntent.putExtras(bundle)
  2446. callIntent
  2447. } else {
  2448. null
  2449. }
  2450. } ?: run {
  2451. return null
  2452. }
  2453. }
  2454. override fun onClickReaction(chatMessage: ChatMessage, emoji: String) {
  2455. vibrate()
  2456. if (chatMessage.reactionsSelf?.contains(emoji) == true) {
  2457. reactionsRepository.deleteReaction(currentConversation!!, chatMessage, emoji)
  2458. .subscribeOn(Schedulers.io())
  2459. ?.observeOn(AndroidSchedulers.mainThread())
  2460. ?.subscribe(ReactionDeletedObserver())
  2461. } else {
  2462. reactionsRepository.addReaction(currentConversation!!, chatMessage, emoji)
  2463. .subscribeOn(Schedulers.io())
  2464. ?.observeOn(AndroidSchedulers.mainThread())
  2465. ?.subscribe(ReactionAddedObserver())
  2466. }
  2467. }
  2468. override fun onLongClickReactions(chatMessage: ChatMessage) {
  2469. activity?.let {
  2470. ShowReactionsDialog(
  2471. activity!!,
  2472. currentConversation,
  2473. chatMessage,
  2474. conversationUser,
  2475. participantPermissions.hasChatPermission(),
  2476. ncApi
  2477. ).show()
  2478. }
  2479. }
  2480. inner class ReactionAddedObserver : Observer<ReactionAddedModel> {
  2481. override fun onSubscribe(d: Disposable) {
  2482. }
  2483. override fun onNext(reactionAddedModel: ReactionAddedModel) {
  2484. Log.d(TAG, "onNext")
  2485. if (reactionAddedModel.success) {
  2486. updateUiToAddReaction(
  2487. reactionAddedModel.chatMessage,
  2488. reactionAddedModel.emoji
  2489. )
  2490. }
  2491. }
  2492. override fun onError(e: Throwable) {
  2493. Log.d(TAG, "onError")
  2494. }
  2495. override fun onComplete() {
  2496. Log.d(TAG, "onComplete")
  2497. }
  2498. }
  2499. inner class ReactionDeletedObserver : Observer<ReactionDeletedModel> {
  2500. override fun onSubscribe(d: Disposable) {
  2501. }
  2502. override fun onNext(reactionDeletedModel: ReactionDeletedModel) {
  2503. Log.d(TAG, "onNext")
  2504. if (reactionDeletedModel.success) {
  2505. updateUiToDeleteReaction(
  2506. reactionDeletedModel.chatMessage,
  2507. reactionDeletedModel.emoji
  2508. )
  2509. }
  2510. }
  2511. override fun onError(e: Throwable) {
  2512. Log.d(TAG, "onError")
  2513. }
  2514. override fun onComplete() {
  2515. Log.d(TAG, "onComplete")
  2516. }
  2517. }
  2518. override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
  2519. openMessageActionsDialog(chatMessage)
  2520. }
  2521. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  2522. openMessageActionsDialog(message)
  2523. }
  2524. override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
  2525. onOpenMessageActionsDialog(chatMessage)
  2526. }
  2527. private fun openMessageActionsDialog(iMessage: IMessage?) {
  2528. val message = iMessage as ChatMessage
  2529. if (hasVisibleItems(message) && !isSystemMessage(message)) {
  2530. activity?.let {
  2531. MessageActionsDialog(
  2532. this,
  2533. message,
  2534. conversationUser,
  2535. currentConversation,
  2536. isShowMessageDeletionButton(message),
  2537. participantPermissions.hasChatPermission()
  2538. ).show()
  2539. }
  2540. }
  2541. }
  2542. private fun isSystemMessage(message: ChatMessage): Boolean {
  2543. return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
  2544. }
  2545. fun deleteMessage(message: IMessage?) {
  2546. if (!participantPermissions.hasChatPermission()) {
  2547. Log.w(
  2548. TAG,
  2549. "Deletion of message is skipped because of restrictions by permissions. " +
  2550. "This method should not have been called!"
  2551. )
  2552. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2553. } else {
  2554. var apiVersion = 1
  2555. // FIXME Fix API checking with guests?
  2556. if (conversationUser != null) {
  2557. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  2558. }
  2559. ncApi.deleteChatMessage(
  2560. credentials,
  2561. ApiUtils.getUrlForChatMessage(
  2562. apiVersion,
  2563. conversationUser?.baseUrl,
  2564. roomToken,
  2565. message?.id
  2566. )
  2567. )?.subscribeOn(Schedulers.io())
  2568. ?.observeOn(AndroidSchedulers.mainThread())
  2569. ?.subscribe(object : Observer<ChatOverallSingleMessage> {
  2570. override fun onSubscribe(d: Disposable) {
  2571. // unused atm
  2572. }
  2573. override fun onNext(t: ChatOverallSingleMessage) {
  2574. if (t.ocs!!.meta!!.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  2575. Toast.makeText(
  2576. context,
  2577. R.string.nc_delete_message_leaked_to_matterbridge,
  2578. Toast.LENGTH_LONG
  2579. ).show()
  2580. }
  2581. }
  2582. override fun onError(e: Throwable) {
  2583. Log.e(
  2584. TAG,
  2585. "Something went wrong when trying to delete message with id " +
  2586. message?.id,
  2587. e
  2588. )
  2589. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2590. }
  2591. override fun onComplete() {
  2592. // unused atm
  2593. }
  2594. })
  2595. }
  2596. }
  2597. fun replyPrivately(message: IMessage?) {
  2598. val apiVersion =
  2599. ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  2600. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2601. apiVersion,
  2602. conversationUser?.baseUrl,
  2603. "1",
  2604. null,
  2605. message?.user?.id?.substring(INVITE_LENGTH),
  2606. null
  2607. )
  2608. ncApi.createRoom(
  2609. credentials,
  2610. retrofitBucket.url,
  2611. retrofitBucket.queryMap
  2612. )
  2613. .subscribeOn(Schedulers.io())
  2614. .observeOn(AndroidSchedulers.mainThread())
  2615. .subscribe(object : Observer<RoomOverall> {
  2616. override fun onSubscribe(d: Disposable) {
  2617. // unused atm
  2618. }
  2619. override fun onNext(roomOverall: RoomOverall) {
  2620. val bundle = Bundle()
  2621. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2622. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  2623. bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  2624. // FIXME once APIv2+ is used only, the createRoom already returns all the data
  2625. ncApi.getRoom(
  2626. credentials,
  2627. ApiUtils.getUrlForRoom(
  2628. apiVersion,
  2629. conversationUser?.baseUrl,
  2630. roomOverall.ocs!!.data!!.token
  2631. )
  2632. )
  2633. .subscribeOn(Schedulers.io())
  2634. .observeOn(AndroidSchedulers.mainThread())
  2635. .subscribe(object : Observer<RoomOverall> {
  2636. override fun onSubscribe(d: Disposable) {
  2637. // unused atm
  2638. }
  2639. override fun onNext(roomOverall: RoomOverall) {
  2640. bundle.putParcelable(
  2641. KEY_ACTIVE_CONVERSATION,
  2642. Parcels.wrap(roomOverall.ocs!!.data!!)
  2643. )
  2644. ConductorRemapping.remapChatController(
  2645. router,
  2646. conversationUser!!.id!!,
  2647. roomOverall.ocs!!.data!!.token!!,
  2648. bundle,
  2649. true
  2650. )
  2651. }
  2652. override fun onError(e: Throwable) {
  2653. Log.e(TAG, e.message, e)
  2654. }
  2655. override fun onComplete() {
  2656. // unused atm
  2657. }
  2658. })
  2659. }
  2660. override fun onError(e: Throwable) {
  2661. Log.e(TAG, e.message, e)
  2662. }
  2663. override fun onComplete() {
  2664. // unused atm
  2665. }
  2666. })
  2667. }
  2668. fun forwardMessage(message: IMessage?) {
  2669. val bundle = Bundle()
  2670. bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
  2671. bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
  2672. bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomId)
  2673. router.pushController(
  2674. RouterTransaction.with(ConversationsListController(bundle))
  2675. .pushChangeHandler(HorizontalChangeHandler())
  2676. .popChangeHandler(HorizontalChangeHandler())
  2677. )
  2678. }
  2679. fun markAsUnread(message: IMessage?) {
  2680. val chatMessage = message as ChatMessage?
  2681. if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2682. ncApi.setChatReadMarker(
  2683. credentials,
  2684. ApiUtils.getUrlForSetChatReadMarker(
  2685. ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
  2686. conversationUser?.baseUrl,
  2687. roomToken
  2688. ),
  2689. chatMessage.previousMessageId
  2690. )
  2691. .subscribeOn(Schedulers.io())
  2692. .observeOn(AndroidSchedulers.mainThread())
  2693. .subscribe(object : Observer<GenericOverall> {
  2694. override fun onSubscribe(d: Disposable) {
  2695. // unused atm
  2696. }
  2697. override fun onNext(t: GenericOverall) {
  2698. // unused atm
  2699. }
  2700. override fun onError(e: Throwable) {
  2701. Log.e(TAG, e.message, e)
  2702. }
  2703. override fun onComplete() {
  2704. // unused atm
  2705. }
  2706. })
  2707. }
  2708. }
  2709. fun copyMessage(message: IMessage?) {
  2710. val clipboardManager =
  2711. activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  2712. val clipData = ClipData.newPlainText(
  2713. resources?.getString(R.string.nc_app_product_name),
  2714. message?.text
  2715. )
  2716. clipboardManager.setPrimaryClip(clipData)
  2717. }
  2718. private fun hasVisibleItems(message: ChatMessage): Boolean {
  2719. return !message.isDeleted || // copy message
  2720. message.replyable || // reply to
  2721. message.replyable && // reply privately
  2722. conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
  2723. message.user.id.startsWith("users/") &&
  2724. message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
  2725. currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2726. isShowMessageDeletionButton(message) || // delete
  2727. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward
  2728. message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread
  2729. ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
  2730. BuildConfig.DEBUG
  2731. }
  2732. fun replyToMessage(message: IMessage?) {
  2733. val chatMessage = message as ChatMessage?
  2734. chatMessage?.let {
  2735. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
  2736. View.GONE
  2737. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
  2738. View.VISIBLE
  2739. val quotedMessage = binding
  2740. .messageInputView
  2741. .findViewById<EmojiTextView>(R.id.quotedMessage)
  2742. quotedMessage?.maxLines = 2
  2743. quotedMessage?.ellipsize = TextUtils.TruncateAt.END
  2744. quotedMessage?.text = it.text
  2745. binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
  2746. it.actorDisplayName ?: context.getText(R.string.nc_nick_guest)
  2747. conversationUser?.let { currentUser ->
  2748. val quotedMessageImage = binding
  2749. .messageInputView
  2750. .findViewById<ImageView>(R.id.quotedMessageImage)
  2751. chatMessage.imageUrl?.let { previewImageUrl ->
  2752. quotedMessageImage?.visibility = View.VISIBLE
  2753. val px = TypedValue.applyDimension(
  2754. TypedValue.COMPLEX_UNIT_DIP,
  2755. QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
  2756. resources?.displayMetrics
  2757. )
  2758. quotedMessageImage?.maxHeight = px.toInt()
  2759. val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
  2760. layoutParams.flexGrow = 0f
  2761. quotedMessageImage.layoutParams = layoutParams
  2762. quotedMessageImage.load(previewImageUrl) {
  2763. addHeader("Authorization", credentials!!)
  2764. }
  2765. } ?: run {
  2766. binding
  2767. .messageInputView
  2768. .findViewById<ImageView>(R.id.quotedMessageImage)
  2769. ?.visibility = View.GONE
  2770. }
  2771. }
  2772. val quotedChatMessageView = binding
  2773. .messageInputView
  2774. .findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  2775. quotedChatMessageView?.tag = message?.jsonMessageId
  2776. quotedChatMessageView?.visibility = View.VISIBLE
  2777. }
  2778. }
  2779. private fun showMicrophoneButton(show: Boolean) {
  2780. if (show && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
  2781. binding.messageInputView.messageSendButton.visibility = View.GONE
  2782. binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
  2783. } else {
  2784. binding.messageInputView.messageSendButton.visibility = View.VISIBLE
  2785. binding.messageInputView.recordAudioButton.visibility = View.GONE
  2786. }
  2787. }
  2788. private fun setMessageAsDeleted(message: IMessage?) {
  2789. val messageTemp = message as ChatMessage
  2790. messageTemp.isDeleted = true
  2791. messageTemp.isOneToOneConversation =
  2792. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2793. messageTemp.activeUser = conversationUser
  2794. adapter?.update(messageTemp)
  2795. }
  2796. private fun updateAdapterForReaction(message: IMessage?) {
  2797. val messageTemp = message as ChatMessage
  2798. messageTemp.isOneToOneConversation =
  2799. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2800. messageTemp.activeUser = conversationUser
  2801. adapter?.update(messageTemp)
  2802. }
  2803. fun updateUiToAddReaction(message: ChatMessage, emoji: String) {
  2804. if (message.reactions == null) {
  2805. message.reactions = LinkedHashMap()
  2806. }
  2807. if (message.reactionsSelf == null) {
  2808. message.reactionsSelf = ArrayList<String>()
  2809. }
  2810. var amount = message.reactions!![emoji]
  2811. if (amount == null) {
  2812. amount = 0
  2813. }
  2814. message.reactions!![emoji] = amount + 1
  2815. message.reactionsSelf!!.add(emoji)
  2816. adapter?.update(message)
  2817. }
  2818. fun updateUiToDeleteReaction(message: ChatMessage, emoji: String) {
  2819. if (message.reactions == null) {
  2820. message.reactions = LinkedHashMap()
  2821. }
  2822. if (message.reactionsSelf == null) {
  2823. message.reactionsSelf = ArrayList<String>()
  2824. }
  2825. var amount = message.reactions!![emoji]
  2826. if (amount == null) {
  2827. amount = 0
  2828. }
  2829. message.reactions!![emoji] = amount - 1
  2830. message.reactionsSelf!!.remove(emoji)
  2831. adapter?.update(message)
  2832. }
  2833. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  2834. if (conversationUser == null) return false
  2835. val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
  2836. true
  2837. } else {
  2838. currentConversation!!.canModerate(conversationUser)
  2839. }
  2840. val isOlderThanSixHours = message
  2841. .createdAt
  2842. .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
  2843. return when {
  2844. !isUserAllowedByPrivileges -> false
  2845. isOlderThanSixHours -> false
  2846. message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
  2847. message.isDeleted -> false
  2848. message.hasFileAttachment() -> false
  2849. OBJECT_MESSAGE == message.message -> false
  2850. !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
  2851. !participantPermissions.hasChatPermission() -> false
  2852. else -> true
  2853. }
  2854. }
  2855. override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
  2856. return when (type) {
  2857. CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
  2858. CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
  2859. CONTENT_TYPE_POLL -> message.isPoll()
  2860. CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
  2861. CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
  2862. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
  2863. else -> false
  2864. }
  2865. }
  2866. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2867. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  2868. /*
  2869. switch (webSocketCommunicationEvent.getType()) {
  2870. case "refreshChat":
  2871. if (
  2872. webSocketCommunicationEvent
  2873. .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID)
  2874. .equals(Long.toString(conversationUser.getId()))
  2875. ) {
  2876. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  2877. pullChatMessages(2);
  2878. }
  2879. }
  2880. break;
  2881. default:
  2882. }*/
  2883. }
  2884. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2885. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  2886. if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2887. currentConversation?.name != userMentionClickEvent.userId
  2888. ) {
  2889. var apiVersion = 1
  2890. // FIXME Fix API checking with guests?
  2891. if (conversationUser != null) {
  2892. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  2893. }
  2894. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2895. apiVersion,
  2896. conversationUser?.baseUrl,
  2897. "1",
  2898. null,
  2899. userMentionClickEvent.userId,
  2900. null
  2901. )
  2902. ncApi.createRoom(
  2903. credentials,
  2904. retrofitBucket.url,
  2905. retrofitBucket.queryMap
  2906. )
  2907. ?.subscribeOn(Schedulers.io())
  2908. ?.observeOn(AndroidSchedulers.mainThread())
  2909. ?.subscribe(object : Observer<RoomOverall> {
  2910. override fun onSubscribe(d: Disposable) {
  2911. // unused atm
  2912. }
  2913. override fun onNext(roomOverall: RoomOverall) {
  2914. val conversationIntent = Intent(activity, CallActivity::class.java)
  2915. val bundle = Bundle()
  2916. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2917. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  2918. bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  2919. if (conversationUser != null) {
  2920. bundle.putParcelable(
  2921. KEY_ACTIVE_CONVERSATION,
  2922. Parcels.wrap(roomOverall.ocs!!.data)
  2923. )
  2924. conversationIntent.putExtras(bundle)
  2925. ConductorRemapping.remapChatController(
  2926. router,
  2927. conversationUser.id!!,
  2928. roomOverall.ocs!!.data!!.token!!,
  2929. bundle,
  2930. true
  2931. )
  2932. } else {
  2933. conversationIntent.putExtras(bundle)
  2934. startActivity(conversationIntent)
  2935. Handler().postDelayed(
  2936. {
  2937. if (!isDestroyed && !isBeingDestroyed) {
  2938. router.popCurrentController()
  2939. }
  2940. },
  2941. POP_CURRENT_CONTROLLER_DELAY
  2942. )
  2943. }
  2944. }
  2945. override fun onError(e: Throwable) {
  2946. // unused atm
  2947. }
  2948. override fun onComplete() {
  2949. // unused atm
  2950. }
  2951. })
  2952. }
  2953. }
  2954. fun sendPictureFromCamIntent() {
  2955. if (!permissionUtil.isCameraPermissionGranted()) {
  2956. requestCameraPermissions()
  2957. } else {
  2958. startActivityForResult(TakePhotoActivity.createIntent(context), REQUEST_CODE_PICK_CAMERA)
  2959. }
  2960. }
  2961. fun sendVideoFromCamIntent() {
  2962. if (!permissionUtil.isCameraPermissionGranted()) {
  2963. requestCameraPermissions()
  2964. } else {
  2965. Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
  2966. takeVideoIntent.resolveActivity(activity!!.packageManager)?.also {
  2967. val videoFile: File? = try {
  2968. val outputDir = context.cacheDir
  2969. val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
  2970. val date = dateFormat.format(Date())
  2971. val videoName = String.format(
  2972. context.resources.getString(R.string.nc_video_filename),
  2973. date
  2974. )
  2975. File("$outputDir/$videoName$VIDEO_SUFFIX")
  2976. } catch (e: IOException) {
  2977. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2978. Log.e(TAG, "error while creating video file", e)
  2979. null
  2980. }
  2981. videoFile?.also {
  2982. videoURI = FileProvider.getUriForFile(context, context.packageName, it)
  2983. takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
  2984. startActivityForResult(takeVideoIntent, REQUEST_CODE_PICK_CAMERA)
  2985. }
  2986. }
  2987. }
  2988. }
  2989. }
  2990. fun createPoll() {
  2991. val pollVoteDialog = PollCreateDialogFragment.newInstance(
  2992. roomToken!!
  2993. )
  2994. pollVoteDialog.show(
  2995. (activity as MainActivity?)!!.supportFragmentManager,
  2996. TAG
  2997. )
  2998. }
  2999. private fun logConversationInfos(methodName: String) {
  3000. Log.d(TAG, " |-----------------------------------------------")
  3001. Log.d(TAG, " | method: $methodName")
  3002. Log.d(TAG, " | ChatController: " + System.identityHashCode(this).toString())
  3003. Log.d(TAG, " | roomToken: $roomToken")
  3004. Log.d(TAG, " | currentConversation?.displayName: ${currentConversation?.displayName}")
  3005. Log.d(TAG, " | currentConversation?.sessionId: ${currentConversation?.sessionId}")
  3006. Log.d(TAG, " | inConversation: $inConversation")
  3007. Log.d(TAG, " |-----------------------------------------------")
  3008. }
  3009. companion object {
  3010. private const val TAG = "ChatController"
  3011. private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
  3012. private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
  3013. private const val CONTENT_TYPE_LOCATION: Byte = 3
  3014. private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
  3015. private const val CONTENT_TYPE_POLL: Byte = 5
  3016. private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 6
  3017. private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
  3018. private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
  3019. private const val LOBBY_TIMER_DELAY: Long = 5000
  3020. private const val HTTP_CODE_OK: Int = 200
  3021. private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
  3022. private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
  3023. private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
  3024. private const val REQUEST_CODE_MESSAGE_SEARCH: Int = 777
  3025. private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
  3026. private const val REQUEST_READ_CONTACT_PERMISSION = 234
  3027. private const val REQUEST_CAMERA_PERMISSION = 223
  3028. private const val REQUEST_CODE_PICK_CAMERA: Int = 333
  3029. private const val REQUEST_CODE_SELECT_REMOTE_FILES = 888
  3030. private const val OBJECT_MESSAGE: String = "{object}"
  3031. private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
  3032. private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
  3033. private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
  3034. private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
  3035. private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
  3036. private const val VIDEO_SUFFIX = ".mp4"
  3037. private const val SHORT_VIBRATE: Long = 20
  3038. private const val FULLY_OPAQUE_INT: Int = 255
  3039. private const val SEMI_TRANSPARENT_INT: Int = 99
  3040. private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
  3041. private const val SECOND: Long = 1000
  3042. private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
  3043. private const val GROUPED_MESSAGES_THRESHOLD = 4
  3044. private const val GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD = 5
  3045. private const val TOOLBAR_AVATAR_RATIO = 1.5
  3046. private const val HTTP_CODE_NOT_MODIFIED = 304
  3047. private const val HTTP_CODE_PRECONDITION_FAILED = 412
  3048. private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
  3049. private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
  3050. private const val MESSAGE_PULL_LIMIT = 100
  3051. private const val INVITE_LENGTH = 6
  3052. private const val ACTOR_LENGTH = 6
  3053. private const val ANIMATION_DURATION: Long = 750
  3054. private const val RETRIES: Long = 3
  3055. private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
  3056. private const val CHUNK_SIZE: Int = 10
  3057. private const val ONE_SECOND_IN_MILLIS = 1000
  3058. }
  3059. }