ChatController.kt 143 KB

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