ChatController.kt 138 KB

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