ChatActivity.kt 151 KB

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