ChatController.kt 138 KB

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