ChatController.kt 142 KB

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