ChatActivity.kt 144 KB

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