ChatViewController.swift 94 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197
  1. import MapKit
  2. import QuickLook
  3. import UIKit
  4. import AVFoundation
  5. import DcCore
  6. import SDWebImage
  7. class ChatViewController: UITableViewController {
  8. var dcContext: DcContext
  9. let outgoingAvatarOverlap: CGFloat = 17.5
  10. let loadCount = 30
  11. let chatId: Int
  12. var messageIds: [Int] = []
  13. var msgChangedObserver: NSObjectProtocol?
  14. var incomingMsgObserver: NSObjectProtocol?
  15. var chatModifiedObserver: NSObjectProtocol?
  16. var ephemeralTimerModifiedObserver: NSObjectProtocol?
  17. private var isInitial = true
  18. private var isVisibleToUser: Bool = false
  19. private var keepKeyboard: Bool = false
  20. private var wasInputBarFirstResponder = false
  21. lazy var isGroupChat: Bool = {
  22. return dcContext.getChat(chatId: chatId).isGroup
  23. }()
  24. lazy var draft: DraftModel = {
  25. let draft = DraftModel(dcContext: dcContext, chatId: chatId)
  26. return draft
  27. }()
  28. // search related
  29. private var activateSearch: Bool = false
  30. private var searchMessageIds: [Int] = []
  31. private var searchResultIndex: Int = 0
  32. private var debounceTimer: Timer?
  33. lazy var searchController: UISearchController = {
  34. let searchController = UISearchController(searchResultsController: nil)
  35. searchController.obscuresBackgroundDuringPresentation = false
  36. searchController.searchBar.placeholder = String.localized("search")
  37. searchController.searchBar.delegate = self
  38. searchController.delegate = self
  39. searchController.searchResultsUpdater = self
  40. searchController.searchBar.inputAccessoryView = messageInputBar
  41. searchController.searchBar.autocorrectionType = .yes
  42. searchController.searchBar.keyboardType = .default
  43. return searchController
  44. }()
  45. public lazy var searchAccessoryBar: ChatSearchAccessoryBar = {
  46. let view = ChatSearchAccessoryBar()
  47. view.delegate = self
  48. view.translatesAutoresizingMaskIntoConstraints = false
  49. view.isEnabled = false
  50. return view
  51. }()
  52. public lazy var backgroundContainer: UIImageView = {
  53. let view = UIImageView()
  54. view.contentMode = .scaleAspectFill
  55. if let backgroundImageName = UserDefaults.standard.string(forKey: Constants.Keys.backgroundImageName) {
  56. view.sd_setImage(with: Utils.getBackgroundImageURL(name: backgroundImageName),
  57. placeholderImage: nil,
  58. options: [.retryFailed]) { [weak self] (_, error, _, _) in
  59. if let error = error {
  60. logger.error("Error loading background image: \(error.localizedDescription)" )
  61. DispatchQueue.main.async { [weak self] in
  62. self?.setDefaultBackgroundImage(view: view)
  63. }
  64. }
  65. }
  66. } else {
  67. setDefaultBackgroundImage(view: view)
  68. }
  69. return view
  70. }()
  71. /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
  72. lazy var messageInputBar: InputBarAccessoryView = {
  73. let inputBar = InputBarAccessoryView()
  74. return inputBar
  75. }()
  76. lazy var draftArea: DraftArea = {
  77. let view = DraftArea()
  78. view.translatesAutoresizingMaskIntoConstraints = false
  79. view.delegate = self
  80. view.inputBarAccessoryView = messageInputBar
  81. return view
  82. }()
  83. public lazy var editingBar: ChatEditingBar = {
  84. let view = ChatEditingBar()
  85. view.delegate = self
  86. view.translatesAutoresizingMaskIntoConstraints = false
  87. return view
  88. }()
  89. public lazy var contactRequestBar: ChatContactRequestBar = {
  90. let chat = dcContext.getChat(chatId: chatId)
  91. let view = ChatContactRequestBar(useDeleteButton: chat.isGroup && !chat.isMailinglist)
  92. view.delegate = self
  93. view.translatesAutoresizingMaskIntoConstraints = false
  94. return view
  95. }()
  96. open override var shouldAutorotate: Bool {
  97. return false
  98. }
  99. private weak var timer: Timer?
  100. lazy var navBarTap: UITapGestureRecognizer = {
  101. UITapGestureRecognizer(target: self, action: #selector(chatProfilePressed))
  102. }()
  103. private var locationStreamingItem: UIBarButtonItem = {
  104. let indicator = LocationStreamingIndicator()
  105. return UIBarButtonItem(customView: indicator)
  106. }()
  107. private lazy var muteItem: UIBarButtonItem = {
  108. let imageView = UIImageView()
  109. imageView.tintColor = DcColors.defaultTextColor
  110. imageView.image = #imageLiteral(resourceName: "volume_off").withRenderingMode(.alwaysTemplate)
  111. imageView.translatesAutoresizingMaskIntoConstraints = false
  112. imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
  113. imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
  114. return UIBarButtonItem(customView: imageView)
  115. }()
  116. private lazy var ephemeralMessageItem: UIBarButtonItem = {
  117. let imageView = UIImageView()
  118. imageView.tintColor = DcColors.defaultTextColor
  119. imageView.image = #imageLiteral(resourceName: "ephemeral_timer").withRenderingMode(.alwaysTemplate)
  120. imageView.translatesAutoresizingMaskIntoConstraints = false
  121. imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
  122. imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
  123. return UIBarButtonItem(customView: imageView)
  124. }()
  125. private lazy var initialsBadge: InitialsBadge = {
  126. let badge: InitialsBadge
  127. badge = InitialsBadge(size: 28, accessibilityLabel: String.localized("menu_view_profile"))
  128. badge.setLabelFont(UIFont.systemFont(ofSize: 14))
  129. badge.accessibilityTraits = .button
  130. return badge
  131. }()
  132. private lazy var badgeItem: UIBarButtonItem = {
  133. return UIBarButtonItem(customView: initialsBadge)
  134. }()
  135. private lazy var contextMenu: ContextMenuProvider = {
  136. let copyItem = ContextMenuProvider.ContextMenuItem(
  137. title: String.localized("global_menu_edit_copy_desktop"),
  138. imageName: "doc.on.doc",
  139. action: #selector(BaseMessageCell.messageCopy),
  140. onPerform: { [weak self] indexPath in
  141. guard let self = self else { return }
  142. let id = self.messageIds[indexPath.row]
  143. let msg = self.dcContext.getMessage(id: id)
  144. let pasteboard = UIPasteboard.general
  145. if msg.type == DC_MSG_TEXT || msg.type == DC_MSG_VIDEOCHAT_INVITATION {
  146. pasteboard.string = msg.text
  147. } else {
  148. pasteboard.string = msg.summary(chars: 10000000)
  149. }
  150. }
  151. )
  152. let infoItem = ContextMenuProvider.ContextMenuItem(
  153. title: String.localized("info"),
  154. imageName: "info",
  155. action: #selector(BaseMessageCell.messageInfo),
  156. onPerform: { [weak self] indexPath in
  157. guard let self = self else { return }
  158. let msg = self.dcContext.getMessage(id: self.messageIds[indexPath.row])
  159. let msgViewController = MessageInfoViewController(dcContext: self.dcContext, message: msg)
  160. if let ctrl = self.navigationController {
  161. ctrl.pushViewController(msgViewController, animated: true)
  162. }
  163. }
  164. )
  165. let deleteItem = ContextMenuProvider.ContextMenuItem(
  166. title: String.localized("delete"),
  167. imageName: "trash",
  168. isDestructive: true,
  169. action: #selector(BaseMessageCell.messageDelete),
  170. onPerform: { [weak self] indexPath in
  171. DispatchQueue.main.async { [weak self] in
  172. guard let self = self else { return }
  173. self.tableView.becomeFirstResponder()
  174. let msg = self.dcContext.getMessage(id: self.messageIds[indexPath.row])
  175. self.askToDeleteMessage(id: msg.id)
  176. }
  177. }
  178. )
  179. let forwardItem = ContextMenuProvider.ContextMenuItem(
  180. title: String.localized("forward"),
  181. imageName: "ic_forward_white_36pt",
  182. action: #selector(BaseMessageCell.messageForward),
  183. onPerform: { [weak self] indexPath in
  184. guard let self = self else { return }
  185. let msg = self.dcContext.getMessage(id: self.messageIds[indexPath.row])
  186. RelayHelper.sharedInstance.setForwardMessage(messageId: msg.id)
  187. self.navigationController?.popViewController(animated: true)
  188. }
  189. )
  190. let replyItem = ContextMenuProvider.ContextMenuItem(
  191. title: String.localized("notify_reply_button"),
  192. imageName: "ic_reply",
  193. action: #selector(BaseMessageCell.messageReply),
  194. onPerform: { indexPath in
  195. DispatchQueue.main.async { [weak self] in
  196. self?.replyToMessage(at: indexPath)
  197. }
  198. }
  199. )
  200. let selectMoreItem = ContextMenuProvider.ContextMenuItem(
  201. title: String.localized("select_more"),
  202. imageName: "checkmark.circle",
  203. action: #selector(BaseMessageCell.messageSelectMore),
  204. onPerform: { indexPath in
  205. DispatchQueue.main.async { [weak self] in
  206. guard let self = self else { return }
  207. let messageId = self.messageIds[indexPath.row]
  208. self.setEditing(isEditing: true, selectedAtIndexPath: indexPath)
  209. if UIAccessibility.isVoiceOverRunning {
  210. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
  211. UIAccessibility.post(notification: .layoutChanged, argument: self?.tableView.cellForRow(at: indexPath))
  212. })
  213. }
  214. }
  215. }
  216. )
  217. let dcChat = dcContext.getChat(chatId: chatId)
  218. let config = ContextMenuProvider()
  219. if #available(iOS 13.0, *), dcChat.canSend {
  220. let mainContextMenu = ContextMenuProvider.ContextMenuItem(submenuitems: [replyItem, forwardItem, infoItem, copyItem, deleteItem])
  221. config.setMenu([mainContextMenu, selectMoreItem])
  222. } else if dcChat.canSend {
  223. config.setMenu([forwardItem, infoItem, copyItem, deleteItem, selectMoreItem])
  224. } else {
  225. config.setMenu([forwardItem, infoItem, copyItem, deleteItem])
  226. }
  227. return config
  228. }()
  229. /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly.
  230. private lazy var audioController = AudioController(dcContext: dcContext, chatId: chatId, delegate: self)
  231. private lazy var keyboardManager: KeyboardManager? = {
  232. let manager = KeyboardManager()
  233. return manager
  234. }()
  235. var showCustomNavBar = true
  236. var highlightedMsg: Int?
  237. private lazy var mediaPicker: MediaPicker? = {
  238. let mediaPicker = MediaPicker(navigationController: navigationController)
  239. mediaPicker.delegate = self
  240. return mediaPicker
  241. }()
  242. var emptyStateView: EmptyStateLabel = {
  243. let view = EmptyStateLabel()
  244. view.isHidden = true
  245. return view
  246. }()
  247. init(dcContext: DcContext, chatId: Int, highlightedMsg: Int? = nil) {
  248. self.dcContext = dcContext
  249. self.chatId = chatId
  250. self.highlightedMsg = highlightedMsg
  251. super.init(nibName: nil, bundle: nil)
  252. hidesBottomBarWhenPushed = true
  253. }
  254. required init?(coder _: NSCoder) {
  255. fatalError("init(coder:) has not been implemented")
  256. }
  257. override func loadView() {
  258. super.loadView()
  259. self.tableView = ChatTableView(messageInputBar: messageInputBar)
  260. self.tableView.delegate = self
  261. self.tableView.dataSource = self
  262. self.view = self.tableView
  263. }
  264. override func viewDidLoad() {
  265. super.viewDidLoad()
  266. tableView.backgroundView = backgroundContainer
  267. tableView.register(TextMessageCell.self, forCellReuseIdentifier: "text")
  268. tableView.register(ImageTextCell.self, forCellReuseIdentifier: "image")
  269. tableView.register(FileTextCell.self, forCellReuseIdentifier: "file")
  270. tableView.register(InfoMessageCell.self, forCellReuseIdentifier: "info")
  271. tableView.register(AudioMessageCell.self, forCellReuseIdentifier: "audio")
  272. tableView.register(VideoInviteCell.self, forCellReuseIdentifier: "video_invite")
  273. tableView.register(WebxdcCell.self, forCellReuseIdentifier: "webxdc")
  274. tableView.rowHeight = UITableView.automaticDimension
  275. tableView.separatorStyle = .none
  276. tableView.keyboardDismissMode = .interactive
  277. navigationController?.setNavigationBarHidden(false, animated: false)
  278. if #available(iOS 13.0, *) {
  279. navigationController?.navigationBar.scrollEdgeAppearance = navigationController?.navigationBar.standardAppearance
  280. }
  281. navigationItem.backButtonTitle = String.localized("chat")
  282. definesPresentationContext = true
  283. // Binding to the tableView will enable interactive dismissal
  284. keyboardManager?.bind(to: tableView)
  285. keyboardManager?.on(event: .didChangeFrame) { [weak self] _ in
  286. guard let self = self else { return }
  287. if self.isLastRowVisible() && !self.tableView.isDragging && !self.tableView.isDecelerating && self.highlightedMsg == nil && !self.isInitial {
  288. self.scrollToBottom()
  289. }
  290. }.on(event: .willChangeFrame) { [weak self] _ in
  291. guard let self = self else { return }
  292. if self.isLastRowVisible() && !self.tableView.isDragging && !self.tableView.isDecelerating && self.highlightedMsg == nil && !self.isInitial {
  293. self.scrollToBottom()
  294. }
  295. }
  296. if !dcContext.isConfigured() {
  297. // TODO: display message about nothing being configured
  298. return
  299. }
  300. configureEmptyStateView()
  301. let dcChat = dcContext.getChat(chatId: chatId)
  302. if dcChat.canSend {
  303. configureUIForWriting()
  304. } else if dcChat.isContactRequest {
  305. configureContactRequestBar()
  306. } else {
  307. messageInputBar.isHidden = true
  308. }
  309. loadMessages()
  310. }
  311. private func configureUIForWriting() {
  312. configureMessageInputBar()
  313. draft.parse(draftMsg: dcContext.getDraft(chatId: chatId))
  314. messageInputBar.inputTextView.text = draft.text
  315. configureDraftArea(draft: draft, animated: false)
  316. tableView.allowsMultipleSelectionDuringEditing = true
  317. }
  318. private func getTopInsetHeight() -> CGFloat {
  319. let navigationBarHeight = navigationController?.navigationBar.bounds.height ?? 0
  320. if let root = UIApplication.shared.keyWindow?.rootViewController {
  321. return navigationBarHeight + root.view.safeAreaInsets.top
  322. }
  323. return UIApplication.shared.statusBarFrame.height + navigationBarHeight
  324. }
  325. private func startTimer() {
  326. stopTimer()
  327. timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
  328. // reload table
  329. DispatchQueue.main.async { [weak self] in
  330. guard let self = self,
  331. let appDelegate = UIApplication.shared.delegate as? AppDelegate
  332. else { return }
  333. if appDelegate.appIsInForeground() {
  334. self.messageIds = self.dcContext.getChatMsgs(chatId: self.chatId)
  335. self.reloadData()
  336. } else {
  337. logger.warning("startTimer() must not be executed in background")
  338. }
  339. }
  340. }
  341. }
  342. public func activateSearchOnAppear() {
  343. activateSearch = true
  344. navigationItem.searchController = self.searchController
  345. }
  346. private func stopTimer() {
  347. if let timer = timer {
  348. timer.invalidate()
  349. }
  350. timer = nil
  351. }
  352. private func configureEmptyStateView() {
  353. emptyStateView.addCenteredTo(parentView: view)
  354. }
  355. override func viewWillAppear(_ animated: Bool) {
  356. super.viewWillAppear(animated)
  357. // this will be removed in viewWillDisappear
  358. navigationController?.navigationBar.addGestureRecognizer(navBarTap)
  359. if showCustomNavBar {
  360. updateTitle(chat: dcContext.getChat(chatId: chatId))
  361. }
  362. tableView.becomeFirstResponder()
  363. if activateSearch {
  364. activateSearch = false
  365. DispatchQueue.main.async { [weak self] in
  366. self?.searchController.isActive = true
  367. }
  368. }
  369. if let msgId = self.highlightedMsg, self.messageIds.firstIndex(of: msgId) != nil {
  370. UIView.animate(withDuration: 0.1, delay: 0, options: .allowAnimatedContent, animations: { [weak self] in
  371. self?.scrollToMessage(msgId: msgId, animated: false)
  372. }, completion: { [weak self] finished in
  373. if finished {
  374. guard let self = self else { return }
  375. self.highlightedMsg = nil
  376. self.isInitial = false
  377. self.updateScrollDownButtonVisibility()
  378. }
  379. })
  380. } else {
  381. UIView.animate(withDuration: 0.1, delay: 0, options: .allowAnimatedContent, animations: { [weak self] in
  382. guard let self = self else { return }
  383. if self.isInitial {
  384. self.scrollToLastUnseenMessage()
  385. }
  386. }, completion: { [weak self] finished in
  387. guard let self = self else { return }
  388. if finished {
  389. self.isInitial = false
  390. }
  391. })
  392. }
  393. if RelayHelper.sharedInstance.isForwarding() {
  394. askToForwardMessage()
  395. } else if RelayHelper.sharedInstance.isMailtoHandling() {
  396. messageInputBar.inputTextView.text = RelayHelper.sharedInstance.mailtoDraft
  397. RelayHelper.sharedInstance.finishMailto()
  398. }
  399. prepareContextMenu()
  400. }
  401. override func viewDidAppear(_ animated: Bool) {
  402. super.viewDidAppear(animated)
  403. AppStateRestorer.shared.storeLastActiveChat(chatId: chatId)
  404. // things that do not affect the chatview
  405. // and are delayed after the view is displayed
  406. DispatchQueue.global(qos: .background).async { [weak self] in
  407. guard let self = self else { return }
  408. self.dcContext.marknoticedChat(chatId: self.chatId)
  409. }
  410. handleUserVisibility(isVisible: true)
  411. // this block ensures that if a swipe-to-dismiss gesture was cancelled, the UI recovers
  412. if wasInputBarFirstResponder {
  413. messageInputBar.inputTextView.becomeFirstResponder()
  414. } else {
  415. tableView.becomeFirstResponder()
  416. }
  417. }
  418. override func viewWillDisappear(_ animated: Bool) {
  419. super.viewWillDisappear(animated)
  420. // the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
  421. navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
  422. wasInputBarFirstResponder = messageInputBar.inputTextView.isFirstResponder
  423. if !wasInputBarFirstResponder {
  424. tableView.resignFirstResponder()
  425. }
  426. }
  427. override func viewDidDisappear(_ animated: Bool) {
  428. super.viewDidDisappear(animated)
  429. AppStateRestorer.shared.resetLastActiveChat()
  430. handleUserVisibility(isVisible: false)
  431. audioController.stopAnyOngoingPlaying()
  432. messageInputBar.inputTextView.resignFirstResponder()
  433. wasInputBarFirstResponder = false
  434. keyboardManager = nil
  435. }
  436. override func willMove(toParent parent: UIViewController?) {
  437. super.willMove(toParent: parent)
  438. if parent == nil {
  439. // logger.debug("chat observer: remove")
  440. removeObservers()
  441. draft.save(context: dcContext)
  442. } else {
  443. // logger.debug("chat observer: setup")
  444. setupObservers()
  445. }
  446. }
  447. private func setupObservers() {
  448. let nc = NotificationCenter.default
  449. msgChangedObserver = nc.addObserver(
  450. forName: dcNotificationChanged,
  451. object: nil,
  452. queue: OperationQueue.main
  453. ) { [weak self] notification in
  454. guard let self = self else { return }
  455. if let ui = notification.userInfo {
  456. let dcChat = self.dcContext.getChat(chatId: self.chatId)
  457. if dcChat.canSend, let id = ui["message_id"] as? Int, id > 0 {
  458. let msg = self.dcContext.getMessage(id: id)
  459. if msg.isInfo,
  460. let parent = msg.parent,
  461. parent.type == DC_MSG_WEBXDC {
  462. self.refreshMessages()
  463. } else {
  464. self.updateMessage(msg)
  465. }
  466. } else {
  467. self.refreshMessages()
  468. DispatchQueue.main.async {
  469. self.updateScrollDownButtonVisibility()
  470. }
  471. }
  472. if self.showCustomNavBar {
  473. self.updateTitle(chat: dcChat)
  474. }
  475. }
  476. }
  477. incomingMsgObserver = nc.addObserver(
  478. forName: dcNotificationIncoming,
  479. object: nil, queue: OperationQueue.main
  480. ) { [weak self] notification in
  481. guard let self = self else { return }
  482. if let ui = notification.userInfo {
  483. if self.chatId == ui["chat_id"] as? Int {
  484. if let id = ui["message_id"] as? Int {
  485. if id > 0 {
  486. self.insertMessage(self.dcContext.getMessage(id: id))
  487. }
  488. }
  489. self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
  490. }
  491. }
  492. }
  493. chatModifiedObserver = nc.addObserver(
  494. forName: dcNotificationChatModified,
  495. object: nil, queue: OperationQueue.main
  496. ) { [weak self] notification in
  497. guard let self = self else { return }
  498. if let ui = notification.userInfo, self.chatId == ui["chat_id"] as? Int {
  499. let dcChat = self.dcContext.getChat(chatId: self.chatId)
  500. if dcChat.canSend {
  501. if self.messageInputBar.isHidden {
  502. self.configureUIForWriting()
  503. self.messageInputBar.isHidden = false
  504. }
  505. } else if !dcChat.isContactRequest {
  506. if !self.messageInputBar.isHidden {
  507. self.messageInputBar.isHidden = true
  508. }
  509. }
  510. }
  511. }
  512. ephemeralTimerModifiedObserver = nc.addObserver(
  513. forName: dcEphemeralTimerModified,
  514. object: nil, queue: OperationQueue.main
  515. ) { [weak self] _ in
  516. guard let self = self else { return }
  517. self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
  518. }
  519. nc.addObserver(self,
  520. selector: #selector(applicationDidBecomeActive(_:)),
  521. name: UIApplication.didBecomeActiveNotification,
  522. object: nil)
  523. nc.addObserver(self,
  524. selector: #selector(applicationWillResignActive(_:)),
  525. name: UIApplication.willResignActiveNotification,
  526. object: nil)
  527. }
  528. private func removeObservers() {
  529. let nc = NotificationCenter.default
  530. if let msgChangedObserver = self.msgChangedObserver {
  531. nc.removeObserver(msgChangedObserver)
  532. }
  533. if let incomingMsgObserver = self.incomingMsgObserver {
  534. nc.removeObserver(incomingMsgObserver)
  535. }
  536. if let chatModifiedObserver = self.chatModifiedObserver {
  537. nc.removeObserver(chatModifiedObserver)
  538. }
  539. if let ephemeralTimerModifiedObserver = self.ephemeralTimerModifiedObserver {
  540. nc.removeObserver(ephemeralTimerModifiedObserver)
  541. }
  542. nc.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
  543. nc.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
  544. }
  545. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  546. let lastSectionVisibleBeforeTransition = self.isLastRowVisible(checkTopCellPostion: true, checkBottomCellPosition: true, allowPartialVisibility: true)
  547. coordinator.animate(
  548. alongsideTransition: { [weak self] _ in
  549. guard let self = self else { return }
  550. if self.showCustomNavBar {
  551. self.navigationItem.setRightBarButton(self.badgeItem, animated: true)
  552. }
  553. if lastSectionVisibleBeforeTransition {
  554. self.scrollToBottom(animated: false)
  555. }
  556. },
  557. completion: {[weak self] _ in
  558. guard let self = self else { return }
  559. self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
  560. if lastSectionVisibleBeforeTransition {
  561. DispatchQueue.main.async { [weak self] in
  562. self?.reloadData()
  563. self?.scrollToBottom(animated: false)
  564. }
  565. }
  566. }
  567. )
  568. super.viewWillTransition(to: size, with: coordinator)
  569. }
  570. @objc func applicationDidBecomeActive(_ notification: NSNotification) {
  571. if navigationController?.visibleViewController == self {
  572. handleUserVisibility(isVisible: true)
  573. }
  574. }
  575. @objc func applicationWillResignActive(_ notification: NSNotification) {
  576. if navigationController?.visibleViewController == self {
  577. handleUserVisibility(isVisible: false)
  578. draft.save(context: dcContext)
  579. }
  580. }
  581. func handleUserVisibility(isVisible: Bool) {
  582. isVisibleToUser = isVisible
  583. if isVisible {
  584. startTimer()
  585. markSeenMessagesInVisibleArea()
  586. } else {
  587. stopTimer()
  588. }
  589. }
  590. /// UITableView methods
  591. override func numberOfSections(in tableView: UITableView) -> Int {
  592. return 1
  593. }
  594. override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  595. return messageIds.count
  596. }
  597. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  598. _ = handleUIMenu()
  599. let id = messageIds[indexPath.row]
  600. if id == DC_MSG_ID_DAYMARKER {
  601. let cell = tableView.dequeueReusableCell(withIdentifier: "info", for: indexPath) as? InfoMessageCell ?? InfoMessageCell()
  602. if messageIds.count > indexPath.row + 1 {
  603. var nextMessageId = messageIds[indexPath.row + 1]
  604. if nextMessageId == DC_MSG_ID_MARKER1 && messageIds.count > indexPath.row + 2 {
  605. nextMessageId = messageIds[indexPath.row + 2]
  606. }
  607. let nextMessage = dcContext.getMessage(id: nextMessageId)
  608. cell.update(text: DateUtils.getDateString(date: nextMessage.sentDate), weight: .bold)
  609. } else {
  610. cell.update(text: "ErrDaymarker")
  611. }
  612. return cell
  613. } else if id == DC_MSG_ID_MARKER1 {
  614. // unread messages marker
  615. let cell = tableView.dequeueReusableCell(withIdentifier: "info", for: indexPath) as? InfoMessageCell ?? InfoMessageCell()
  616. let freshMsgsCount = self.messageIds.count - (indexPath.row + 1)
  617. cell.update(text: String.localized(stringID: "chat_n_new_messages", count: freshMsgsCount))
  618. return cell
  619. }
  620. let message = dcContext.getMessage(id: id)
  621. if message.isInfo {
  622. let cell = tableView.dequeueReusableCell(withIdentifier: "info", for: indexPath) as? InfoMessageCell ?? InfoMessageCell()
  623. cell.update(text: message.text)
  624. return cell
  625. }
  626. let cell: BaseMessageCell
  627. switch Int32(message.type) {
  628. case DC_MSG_VIDEOCHAT_INVITATION:
  629. let videoInviteCell = tableView.dequeueReusableCell(withIdentifier: "video_invite", for: indexPath) as? VideoInviteCell ?? VideoInviteCell()
  630. videoInviteCell.showSelectionBackground(tableView.isEditing)
  631. videoInviteCell.update(dcContext: dcContext, msg: message)
  632. return videoInviteCell
  633. case DC_MSG_IMAGE, DC_MSG_GIF, DC_MSG_VIDEO, DC_MSG_STICKER:
  634. cell = tableView.dequeueReusableCell(withIdentifier: "image", for: indexPath) as? ImageTextCell ?? ImageTextCell()
  635. case DC_MSG_FILE:
  636. if message.isSetupMessage {
  637. cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
  638. message.text = String.localized("autocrypt_asm_click_body")
  639. } else {
  640. cell = tableView.dequeueReusableCell(withIdentifier: "file", for: indexPath) as? FileTextCell ?? FileTextCell()
  641. }
  642. case DC_MSG_WEBXDC:
  643. cell = tableView.dequeueReusableCell(withIdentifier: "webxdc", for: indexPath) as? WebxdcCell ?? WebxdcCell()
  644. case DC_MSG_AUDIO, DC_MSG_VOICE:
  645. let audioMessageCell: AudioMessageCell = tableView.dequeueReusableCell(withIdentifier: "audio",
  646. for: indexPath) as? AudioMessageCell ?? AudioMessageCell()
  647. audioController.update(audioMessageCell, with: message.id)
  648. cell = audioMessageCell
  649. default:
  650. cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
  651. }
  652. var showAvatar = isGroupChat && !message.isFromCurrentSender
  653. var showName = isGroupChat
  654. if message.overrideSenderName != nil {
  655. showAvatar = !message.isFromCurrentSender
  656. showName = true
  657. }
  658. cell.baseDelegate = self
  659. cell.showSelectionBackground(tableView.isEditing)
  660. cell.update(dcContext: dcContext,
  661. msg: message,
  662. messageStyle: configureMessageStyle(for: message, at: indexPath),
  663. showAvatar: showAvatar,
  664. showName: showName,
  665. searchText: searchController.searchBar.text,
  666. highlight: !searchMessageIds.isEmpty && message.id == searchMessageIds[searchResultIndex])
  667. return cell
  668. }
  669. public override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  670. if !decelerate {
  671. markSeenMessagesInVisibleArea()
  672. updateScrollDownButtonVisibility()
  673. }
  674. }
  675. public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  676. markSeenMessagesInVisibleArea()
  677. updateScrollDownButtonVisibility()
  678. }
  679. override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  680. markSeenMessagesInVisibleArea()
  681. updateScrollDownButtonVisibility()
  682. }
  683. private func updateScrollDownButtonVisibility() {
  684. messageInputBar.scrollDownButton.isHidden = isLastRowVisible(checkTopCellPostion: true, checkBottomCellPosition: true, allowPartialVisibility: true)
  685. }
  686. private func configureContactRequestBar() {
  687. messageInputBar.separatorLine.backgroundColor = DcColors.colorDisabled
  688. messageInputBar.setMiddleContentView(contactRequestBar, animated: false)
  689. messageInputBar.setLeftStackViewWidthConstant(to: 0, animated: false)
  690. messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
  691. messageInputBar.padding = UIEdgeInsets(top: 6, left: 0, bottom: 6, right: 0)
  692. messageInputBar.setStackViewItems([], forStack: .top, animated: false)
  693. messageInputBar.onScrollDownButtonPressed = scrollToBottom
  694. }
  695. private func configureDraftArea(draft: DraftModel, animated: Bool = true) {
  696. if searchController.isActive {
  697. messageInputBar.setMiddleContentView(searchAccessoryBar, animated: false)
  698. messageInputBar.setLeftStackViewWidthConstant(to: 0, animated: false)
  699. messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
  700. messageInputBar.setStackViewItems([], forStack: .top, animated: false)
  701. messageInputBar.padding = UIEdgeInsets(top: 6, left: 0, bottom: 6, right: 0)
  702. return
  703. }
  704. draftArea.configure(draft: draft)
  705. if draft.isEditing {
  706. messageInputBar.setMiddleContentView(editingBar, animated: false)
  707. messageInputBar.setLeftStackViewWidthConstant(to: 0, animated: false)
  708. messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
  709. messageInputBar.padding = UIEdgeInsets(top: 6, left: 0, bottom: 6, right: 0)
  710. } else {
  711. messageInputBar.setMiddleContentView(messageInputBar.inputTextView, animated: false)
  712. messageInputBar.setLeftStackViewWidthConstant(to: 40, animated: false)
  713. messageInputBar.setRightStackViewWidthConstant(to: 40, animated: false)
  714. messageInputBar.padding = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 12)
  715. }
  716. messageInputBar.setStackViewItems([draftArea], forStack: .top, animated: animated)
  717. }
  718. override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  719. let swipeAction = UISwipeActionsConfiguration(actions: [])
  720. return swipeAction
  721. }
  722. override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  723. let message = dcContext.getMessage(id: messageIds[indexPath.row])
  724. let dcChat = dcContext.getChat(chatId: chatId)
  725. if !dcChat.canSend || message.isInfo || message.type == DC_MSG_VIDEOCHAT_INVITATION {
  726. return nil
  727. }
  728. let action = UIContextualAction(style: .normal, title: nil,
  729. handler: { [weak self] (_, _, completionHandler) in
  730. self?.keepKeyboard = true
  731. self?.replyToMessage(at: indexPath)
  732. completionHandler(true)
  733. })
  734. if #available(iOS 13.0, *) {
  735. action.image = UIImage(named: traitCollection.userInterfaceStyle == .light ? "ic_reply_black" : "ic_reply")
  736. action.backgroundColor = DcColors.chatBackgroundColor.withAlphaComponent(0.25)
  737. } else {
  738. action.image = UIImage(named: "ic_reply_black")
  739. action.backgroundColor = .systemBlue
  740. }
  741. action.image?.accessibilityTraits = .button
  742. action.image?.accessibilityLabel = String.localized("menu_reply")
  743. let configuration = UISwipeActionsConfiguration(actions: [action])
  744. return configuration
  745. }
  746. func replyToMessage(at indexPath: IndexPath) {
  747. let message = dcContext.getMessage(id: self.messageIds[indexPath.row])
  748. self.draft.setQuote(quotedMsg: message)
  749. self.configureDraftArea(draft: self.draft)
  750. focusInputTextView()
  751. }
  752. func markSeenMessagesInVisibleArea() {
  753. if isVisibleToUser,
  754. let indexPaths = tableView.indexPathsForVisibleRows {
  755. let visibleMessagesIds = indexPaths.map { UInt32(messageIds[$0.row]) }
  756. if !visibleMessagesIds.isEmpty {
  757. DispatchQueue.global(qos: .background).async { [weak self] in
  758. self?.dcContext.markSeenMessages(messageIds: visibleMessagesIds)
  759. }
  760. }
  761. }
  762. }
  763. func markSeenMessage(id: Int) {
  764. if isVisibleToUser {
  765. DispatchQueue.global(qos: .background).async { [weak self] in
  766. self?.dcContext.markSeenMessages(messageIds: [UInt32(id)])
  767. }
  768. }
  769. }
  770. override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  771. return tableView.cellForRow(at: indexPath) as? SelectableCell != nil
  772. }
  773. override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  774. if let cell = tableView.cellForRow(at: indexPath) as? SelectableCell {
  775. cell.showSelectionBackground(tableView.isEditing)
  776. return indexPath
  777. }
  778. return nil
  779. }
  780. override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
  781. if tableView.isEditing {
  782. handleEditingBar()
  783. }
  784. }
  785. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  786. if tableView.isEditing {
  787. handleEditingBar()
  788. return
  789. }
  790. let messageId = messageIds[indexPath.row]
  791. let message = dcContext.getMessage(id: messageId)
  792. if message.isSetupMessage {
  793. didTapAsm(msg: message, orgText: "")
  794. } else if message.type == DC_MSG_FILE ||
  795. message.type == DC_MSG_AUDIO ||
  796. message.type == DC_MSG_VOICE {
  797. showMediaGalleryFor(message: message)
  798. } else if message.type == DC_MSG_VIDEOCHAT_INVITATION {
  799. if let url = NSURL(string: message.getVideoChatUrl()) {
  800. UIApplication.shared.open(url as URL)
  801. }
  802. } else if message.isInfo, let parent = message.parent, parent.type == DC_MSG_WEBXDC {
  803. scrollToMessage(msgId: parent.id)
  804. }
  805. _ = handleUIMenu()
  806. }
  807. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  808. messageInputBar.inputTextView.layer.borderColor = DcColors.colorDisabled.cgColor
  809. if #available(iOS 12.0, *),
  810. UserDefaults.standard.string(forKey: Constants.Keys.backgroundImageName) == nil {
  811. backgroundContainer.image = UIImage(named: traitCollection.userInterfaceStyle == .light ? "background_light" : "background_dark")
  812. }
  813. }
  814. func configureMessageStyle(for message: DcMsg, at indexPath: IndexPath) -> UIRectCorner {
  815. var corners: UIRectCorner = []
  816. if message.isFromCurrentSender {
  817. corners.formUnion(.topLeft)
  818. corners.formUnion(.bottomLeft)
  819. corners.formUnion(.topRight)
  820. } else {
  821. corners.formUnion(.topRight)
  822. corners.formUnion(.bottomRight)
  823. corners.formUnion(.topLeft)
  824. }
  825. return corners
  826. }
  827. private func updateTitle(chat: DcChat) {
  828. let titleView = ChatTitleView()
  829. var subtitle = ""
  830. let chatContactIds = chat.getContactIds(dcContext)
  831. if chat.isMailinglist {
  832. subtitle = String.localized("mailing_list")
  833. } else if chat.isBroadcast {
  834. subtitle = String.localized(stringID: "n_recipients", count: chatContactIds.count)
  835. } else if chat.isGroup {
  836. subtitle = String.localized(stringID: "n_members", count: chatContactIds.count)
  837. } else if chat.isDeviceTalk {
  838. subtitle = String.localized("device_talk_subtitle")
  839. } else if chat.isSelfTalk {
  840. subtitle = String.localized("chat_self_talk_subtitle")
  841. } else if chatContactIds.count >= 1 {
  842. subtitle = dcContext.getContact(id: chatContactIds[0]).email
  843. }
  844. titleView.updateTitleView(title: chat.name, subtitle: subtitle)
  845. navigationItem.titleView = titleView
  846. if let image = chat.profileImage {
  847. initialsBadge.setImage(image)
  848. } else {
  849. initialsBadge.setName(chat.name)
  850. initialsBadge.setColor(chat.color)
  851. }
  852. initialsBadge.setVerified(chat.isProtected)
  853. var rightBarButtonItems = [badgeItem]
  854. if chat.isSendingLocations {
  855. rightBarButtonItems.append(locationStreamingItem)
  856. }
  857. if chat.isMuted {
  858. rightBarButtonItems.append(muteItem)
  859. }
  860. if dcContext.getChatEphemeralTimer(chatId: chat.id) > 0 {
  861. rightBarButtonItems.append(ephemeralMessageItem)
  862. }
  863. navigationItem.rightBarButtonItems = rightBarButtonItems
  864. }
  865. @objc
  866. private func refreshMessages() {
  867. self.messageIds = dcContext.getChatMsgs(chatId: chatId)
  868. let wasLastSectionScrolledToBottom = isLastRowScrolledToBottom()
  869. self.reloadData()
  870. if wasLastSectionScrolledToBottom {
  871. self.scrollToBottom(animated: true)
  872. }
  873. self.showEmptyStateView(self.messageIds.isEmpty)
  874. }
  875. private func reloadData() {
  876. let selectredRows = tableView.indexPathsForSelectedRows
  877. tableView.reloadData()
  878. // There's an iOS bug, filling up the console output but which can be ignored: https://developer.apple.com/forums/thread/668295
  879. // [Assert] Attempted to call -cellForRowAtIndexPath: on the table view while it was in the process of updating its visible cells, which is not allowed.
  880. selectredRows?.forEach({ (selectedRow) in
  881. tableView.selectRow(at: selectedRow, animated: false, scrollPosition: .none)
  882. })
  883. }
  884. private func loadMessages() {
  885. // update message ids
  886. var msgIds = dcContext.getChatMsgs(chatId: chatId)
  887. let freshMsgsCount = self.dcContext.getUnreadMessages(chatId: self.chatId)
  888. if freshMsgsCount > 0 && msgIds.count >= freshMsgsCount {
  889. let index = msgIds.count - freshMsgsCount
  890. msgIds.insert(Int(DC_MSG_ID_MARKER1), at: index)
  891. }
  892. self.messageIds = msgIds
  893. self.showEmptyStateView(self.messageIds.isEmpty)
  894. self.reloadData()
  895. }
  896. private func isLastRowScrolledToBottom() -> Bool {
  897. return isLastRowVisible(checkTopCellPostion: false, checkBottomCellPosition: true)
  898. }
  899. // verifies if the last message cell is visible
  900. // - ommitting the parameters results in a simple check if the last message cell is preloaded. In that case it is not guaranteed that the cell is actually visible to the user and not covered e.g. by the messageInputBar
  901. // - if set to true, checkTopCellPosition verifies if the top of the last message cell is visible to the user
  902. // - if set to true, checkBottomCellPosition verifies if the bottom of the last message cell is visible to the user
  903. // - if set to true, allowPartialVisiblity ensures that any part of the last message shown in the visible area results in a true return value.
  904. // Using this flag large messages exceeding actual screen space are handled gracefully. This flag is only taken into account if checkTopCellPostion and checkBottomCellPosition are both set to true
  905. private func isLastRowVisible(checkTopCellPostion: Bool = false, checkBottomCellPosition: Bool = false, allowPartialVisibility: Bool = false) -> Bool {
  906. guard !messageIds.isEmpty else { return false }
  907. let lastIndexPath = IndexPath(item: messageIds.count - 1, section: 0)
  908. if !(checkTopCellPostion || checkBottomCellPosition) {
  909. return tableView.indexPathsForVisibleRows?.contains(lastIndexPath) ?? false
  910. }
  911. guard let window = UIApplication.shared.keyWindow else {
  912. return tableView.indexPathsForVisibleRows?.contains(lastIndexPath) ?? false
  913. }
  914. let rectOfCellInTableView = tableView.rectForRow(at: lastIndexPath)
  915. // convert points to same coordination system
  916. let inputBarTopInWindow = messageInputBar.convert(CGPoint(x: 0, y: messageInputBar.bounds.minY), to: window)
  917. var cellTopInWindow = tableView.convert(CGPoint(x: 0, y: rectOfCellInTableView.minY), to: window)
  918. cellTopInWindow.y = floor(cellTopInWindow.y)
  919. var cellBottomInWindow = tableView.convert(CGPoint(x: 0, y: rectOfCellInTableView.maxY), to: window)
  920. cellBottomInWindow.y = floor(cellBottomInWindow.y)
  921. let tableViewTopInWindow = tableView.convert(CGPoint(x: 0, y: tableView.bounds.minY), to: window)
  922. // check if top and bottom of the message are within the visible area
  923. let isTopVisible = cellTopInWindow.y < inputBarTopInWindow.y && cellTopInWindow.y >= tableViewTopInWindow.y
  924. let isBottomVisible = cellBottomInWindow.y <= inputBarTopInWindow.y && cellBottomInWindow.y >= tableViewTopInWindow.y
  925. // check if the message is visible, but top and bottom of cell exceed visible area
  926. let messageExceedsScreen = cellTopInWindow.y < tableViewTopInWindow.y && cellBottomInWindow.y > inputBarTopInWindow.y
  927. if checkTopCellPostion && checkBottomCellPosition {
  928. return allowPartialVisibility ?
  929. isTopVisible || isBottomVisible || messageExceedsScreen :
  930. isTopVisible && isBottomVisible
  931. } else if checkTopCellPostion {
  932. return isTopVisible
  933. } else {
  934. // checkBottomCellPosition
  935. return isBottomVisible
  936. }
  937. }
  938. private func scrollToBottom() {
  939. scrollToBottom(animated: true)
  940. }
  941. private func scrollToBottom(animated: Bool, focusOnVoiceOver: Bool = false) {
  942. if !messageIds.isEmpty {
  943. DispatchQueue.main.async { [weak self] in
  944. guard let self = self else { return }
  945. let numberOfRows = self.tableView.numberOfRows(inSection: 0)
  946. if numberOfRows > 0 {
  947. self.scrollToRow(at: IndexPath(row: numberOfRows - 1, section: 0), position: .bottom, animated: animated, focusWithVoiceOver: focusOnVoiceOver)
  948. }
  949. }
  950. }
  951. }
  952. private func scrollToLastUnseenMessage() {
  953. DispatchQueue.main.async { [weak self] in
  954. guard let self = self else { return }
  955. if let markerMessageIndex = self.messageIds.firstIndex(of: Int(DC_MSG_ID_MARKER1)) {
  956. let indexPath = IndexPath(row: markerMessageIndex, section: 0)
  957. self.scrollToRow(at: indexPath, animated: false)
  958. } else {
  959. // scroll to bottom
  960. let numberOfRows = self.tableView.numberOfRows(inSection: 0)
  961. if numberOfRows > 0 {
  962. self.scrollToRow(at: IndexPath(row: numberOfRows - 1, section: 0), animated: false)
  963. }
  964. }
  965. }
  966. }
  967. private func scrollToMessage(msgId: Int, animated: Bool = true, scrollToText: Bool = false) {
  968. DispatchQueue.main.async { [weak self] in
  969. guard let self = self else { return }
  970. guard let index = self.messageIds.firstIndex(of: msgId) else {
  971. return
  972. }
  973. let indexPath = IndexPath(row: index, section: 0)
  974. if scrollToText && !UIAccessibility.isVoiceOverRunning {
  975. self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
  976. let cell = self.tableView.cellForRow(at: indexPath)
  977. if let messageCell = cell as? BaseMessageCell {
  978. let textYPos = messageCell.getTextOffset(of: self.searchController.searchBar.text)
  979. let currentYPos = self.tableView.contentOffset.y
  980. let padding: CGFloat = 12
  981. self.tableView.setContentOffset(CGPoint(x: 0,
  982. y: textYPos +
  983. currentYPos -
  984. 2 * UIFont.preferredFont(for: .body, weight: .regular).lineHeight -
  985. padding),
  986. animated: false)
  987. return
  988. }
  989. }
  990. self.scrollToRow(at: indexPath, animated: false)
  991. }
  992. }
  993. private func scrollToRow(at indexPath: IndexPath, position: UITableView.ScrollPosition = .top, animated: Bool, focusWithVoiceOver: Bool = true) {
  994. if UIAccessibility.isVoiceOverRunning && focusWithVoiceOver {
  995. self.tableView.scrollToRow(at: indexPath, at: position, animated: false)
  996. DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { [weak self] in
  997. guard let self = self else { return }
  998. UIAccessibility.post(notification: .screenChanged, argument: self.tableView.cellForRow(at: indexPath))
  999. self.markSeenMessagesInVisibleArea()
  1000. self.updateScrollDownButtonVisibility()
  1001. })
  1002. } else {
  1003. self.tableView.scrollToRow(at: indexPath, at: position, animated: animated)
  1004. }
  1005. }
  1006. private func showEmptyStateView(_ show: Bool) {
  1007. if show {
  1008. let dcChat = dcContext.getChat(chatId: chatId)
  1009. if dcChat.isGroup {
  1010. if dcChat.isBroadcast {
  1011. emptyStateView.text = String.localized("chat_new_broadcast_hint")
  1012. } else if dcChat.isUnpromoted {
  1013. emptyStateView.text = String.localized("chat_new_group_hint")
  1014. } else {
  1015. emptyStateView.text = String.localized("chat_no_messages")
  1016. }
  1017. } else if dcChat.isSelfTalk {
  1018. emptyStateView.text = String.localized("saved_messages_explain")
  1019. } else if dcChat.isDeviceTalk {
  1020. emptyStateView.text = String.localized("device_talk_explain")
  1021. } else {
  1022. emptyStateView.text = String.localizedStringWithFormat(String.localized("chat_new_one_to_one_hint"), dcChat.name)
  1023. }
  1024. emptyStateView.isHidden = false
  1025. } else {
  1026. emptyStateView.isHidden = true
  1027. }
  1028. }
  1029. @objc private func saveDraft() {
  1030. draft.save(context: dcContext)
  1031. }
  1032. private func configureMessageInputBar() {
  1033. messageInputBar.delegate = self
  1034. messageInputBar.inputTextView.tintColor = DcColors.primary
  1035. messageInputBar.inputTextView.placeholder = String.localized("chat_input_placeholder")
  1036. messageInputBar.separatorLine.backgroundColor = DcColors.colorDisabled
  1037. messageInputBar.inputTextView.tintColor = DcColors.primary
  1038. messageInputBar.inputTextView.textColor = DcColors.defaultTextColor
  1039. messageInputBar.inputTextView.backgroundColor = DcColors.inputFieldColor
  1040. messageInputBar.inputTextView.placeholderTextColor = DcColors.placeholderColor
  1041. messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 38)
  1042. messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 38)
  1043. messageInputBar.inputTextView.layer.borderColor = DcColors.colorDisabled.cgColor
  1044. messageInputBar.inputTextView.layer.borderWidth = 1.0
  1045. messageInputBar.inputTextView.layer.cornerRadius = 13.0
  1046. messageInputBar.inputTextView.layer.masksToBounds = true
  1047. messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
  1048. configureInputBarItems()
  1049. messageInputBar.inputTextView.delegate = self
  1050. messageInputBar.inputTextView.imagePasteDelegate = self
  1051. messageInputBar.onScrollDownButtonPressed = scrollToBottom
  1052. }
  1053. private func evaluateInputBar(draft: DraftModel) {
  1054. messageInputBar.sendButton.isEnabled = draft.canSend()
  1055. messageInputBar.sendButton.accessibilityTraits = draft.canSend() ? .button : .notEnabled
  1056. }
  1057. private func configureInputBarItems() {
  1058. messageInputBar.setLeftStackViewWidthConstant(to: 40, animated: false)
  1059. messageInputBar.setRightStackViewWidthConstant(to: 40, animated: false)
  1060. let sendButtonImage = UIImage(named: "paper_plane")?.withRenderingMode(.alwaysTemplate)
  1061. messageInputBar.sendButton.image = sendButtonImage
  1062. messageInputBar.sendButton.accessibilityLabel = String.localized("menu_send")
  1063. messageInputBar.sendButton.accessibilityTraits = .button
  1064. messageInputBar.sendButton.title = nil
  1065. messageInputBar.sendButton.tintColor = UIColor(white: 1, alpha: 1)
  1066. messageInputBar.sendButton.layer.cornerRadius = 20
  1067. messageInputBar.middleContentViewPadding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
  1068. // this adds a padding between textinputfield and send button
  1069. messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
  1070. messageInputBar.sendButton.setSize(CGSize(width: 40, height: 40), animated: false)
  1071. messageInputBar.padding = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 12)
  1072. messageInputBar.shouldManageSendButtonEnabledState = false
  1073. let leftItems = [
  1074. InputBarButtonItem()
  1075. .configure {
  1076. $0.spacing = .fixed(0)
  1077. let clipperIcon = #imageLiteral(resourceName: "ic_attach_file_36pt").withRenderingMode(.alwaysTemplate)
  1078. $0.image = clipperIcon
  1079. $0.tintColor = DcColors.primary
  1080. $0.setSize(CGSize(width: 40, height: 40), animated: false)
  1081. $0.accessibilityLabel = String.localized("menu_add_attachment")
  1082. $0.accessibilityTraits = .button
  1083. }.onSelected {
  1084. $0.tintColor = UIColor.themeColor(light: .lightGray, dark: .darkGray)
  1085. }.onDeselected {
  1086. $0.tintColor = DcColors.primary
  1087. }.onTouchUpInside { [weak self] _ in
  1088. self?.clipperButtonPressed()
  1089. }
  1090. ]
  1091. messageInputBar.setStackViewItems(leftItems, forStack: .left, animated: false)
  1092. // This just adds some more flare
  1093. messageInputBar.sendButton
  1094. .onEnabled { item in
  1095. UIView.animate(withDuration: 0.3, animations: {
  1096. item.backgroundColor = DcColors.primary
  1097. })}
  1098. .onDisabled { item in
  1099. UIView.animate(withDuration: 0.3, animations: {
  1100. item.backgroundColor = DcColors.colorDisabled
  1101. })}
  1102. }
  1103. @objc private func chatProfilePressed() {
  1104. showChatDetail(chatId: chatId)
  1105. }
  1106. @objc private func clipperButtonPressed() {
  1107. showClipperOptions()
  1108. }
  1109. private func showClipperOptions() {
  1110. let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
  1111. let galleryAction = PhotoPickerAlertAction(title: String.localized("gallery"), style: .default, handler: galleryButtonPressed(_:))
  1112. let cameraAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: cameraButtonPressed(_:))
  1113. let documentAction = UIAlertAction(title: String.localized("files"), style: .default, handler: documentActionPressed(_:))
  1114. let voiceMessageAction = UIAlertAction(title: String.localized("voice_message"), style: .default, handler: voiceMessageButtonPressed(_:))
  1115. let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
  1116. let locationStreamingAction = UIAlertAction(title: isLocationStreaming ? String.localized("stop_sharing_location") : String.localized("location"),
  1117. style: isLocationStreaming ? .destructive : .default,
  1118. handler: locationStreamingButtonPressed(_:))
  1119. alert.addAction(cameraAction)
  1120. alert.addAction(galleryAction)
  1121. alert.addAction(documentAction)
  1122. alert.addAction(voiceMessageAction)
  1123. if let config = dcContext.getConfig("webrtc_instance"), !config.isEmpty {
  1124. let videoChatInvitation = UIAlertAction(title: String.localized("videochat"), style: .default, handler: videoChatButtonPressed(_:))
  1125. alert.addAction(videoChatInvitation)
  1126. }
  1127. if UserDefaults.standard.bool(forKey: "location_streaming") {
  1128. alert.addAction(locationStreamingAction)
  1129. }
  1130. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  1131. self.present(alert, animated: true, completion: {
  1132. // unfortunately, voiceMessageAction.accessibilityHint does not work,
  1133. // but this hack does the trick
  1134. if UIAccessibility.isVoiceOverRunning {
  1135. if let view = voiceMessageAction.value(forKey: "__representer") as? UIView {
  1136. view.accessibilityHint = String.localized("a11y_voice_message_hint_ios")
  1137. }
  1138. }
  1139. })
  1140. }
  1141. private func confirmationAlert(title: String, actionTitle: String, actionStyle: UIAlertAction.Style = .default, actionHandler: @escaping ((UIAlertAction) -> Void), cancelHandler: ((UIAlertAction) -> Void)? = nil) {
  1142. let alert = UIAlertController(title: title,
  1143. message: nil,
  1144. preferredStyle: .safeActionSheet)
  1145. alert.addAction(UIAlertAction(title: actionTitle, style: actionStyle, handler: actionHandler))
  1146. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: cancelHandler ?? { _ in
  1147. self.dismiss(animated: true, completion: nil)
  1148. }))
  1149. present(alert, animated: true, completion: nil)
  1150. }
  1151. private func askToDeleteChat() {
  1152. let title = String.localized(stringID: "ask_delete_chat", count: 1)
  1153. confirmationAlert(title: title, actionTitle: String.localized("delete"), actionStyle: .destructive,
  1154. actionHandler: { [weak self] _ in
  1155. guard let self = self else { return }
  1156. // remove message observers early to avoid careless calls to dcContext methods
  1157. self.removeObservers()
  1158. self.dcContext.deleteChat(chatId: self.chatId)
  1159. self.navigationController?.popViewController(animated: true)
  1160. })
  1161. }
  1162. private func askToChatWith(email: String) {
  1163. let contactId = self.dcContext.createContact(name: "", email: email)
  1164. if dcContext.getChatIdByContactId(contactId: contactId) != 0 {
  1165. self.dismiss(animated: true, completion: nil)
  1166. let chatId = self.dcContext.createChatByContactId(contactId: contactId)
  1167. self.showChat(chatId: chatId)
  1168. } else {
  1169. confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
  1170. actionTitle: String.localized("start_chat"),
  1171. actionHandler: { _ in
  1172. self.dismiss(animated: true, completion: nil)
  1173. let chatId = self.dcContext.createChatByContactId(contactId: contactId)
  1174. self.showChat(chatId: chatId)})
  1175. }
  1176. }
  1177. private func askToDeleteMessage(id: Int) {
  1178. self.askToDeleteMessages(ids: [id])
  1179. }
  1180. private func askToDeleteMessages(ids: [Int]) {
  1181. let chat = dcContext.getChat(chatId: chatId)
  1182. let title = chat.isDeviceTalk ?
  1183. String.localized(stringID: "ask_delete_messages_simple", count: ids.count) :
  1184. String.localized(stringID: "ask_delete_messages", count: ids.count)
  1185. confirmationAlert(title: title, actionTitle: String.localized("delete"), actionStyle: .destructive,
  1186. actionHandler: { _ in
  1187. self.dcContext.deleteMessages(msgIds: ids)
  1188. if self.tableView.isEditing {
  1189. self.setEditing(isEditing: false)
  1190. }
  1191. })
  1192. }
  1193. private func askToForwardMessage() {
  1194. let chat = dcContext.getChat(chatId: self.chatId)
  1195. if chat.isSelfTalk {
  1196. RelayHelper.sharedInstance.forward(to: self.chatId)
  1197. refreshMessages()
  1198. } else {
  1199. confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_forward"), chat.name),
  1200. actionTitle: String.localized("menu_forward"),
  1201. actionHandler: { _ in
  1202. RelayHelper.sharedInstance.forward(to: self.chatId)
  1203. self.dismiss(animated: true, completion: nil)},
  1204. cancelHandler: { _ in
  1205. self.dismiss(animated: false, completion: nil)
  1206. self.navigationController?.popViewController(animated: true)})
  1207. }
  1208. }
  1209. // MARK: - coordinator
  1210. private func showChatDetail(chatId: Int) {
  1211. let chat = dcContext.getChat(chatId: chatId)
  1212. if !chat.isGroup {
  1213. if let contactId = chat.getContactIds(dcContext).first {
  1214. let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: contactId)
  1215. navigationController?.pushViewController(contactDetailController, animated: true)
  1216. }
  1217. } else {
  1218. let groupChatDetailViewController = GroupChatDetailViewController(chatId: chatId, dcContext: dcContext)
  1219. navigationController?.pushViewController(groupChatDetailViewController, animated: true)
  1220. }
  1221. }
  1222. func showChat(chatId: Int, messageId: Int? = nil, animated: Bool = true) {
  1223. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  1224. appDelegate.appCoordinator.showChat(chatId: chatId, msgId: messageId, animated: animated, clearViewControllerStack: true)
  1225. }
  1226. }
  1227. private func showDocumentLibrary() {
  1228. mediaPicker?.showDocumentLibrary()
  1229. }
  1230. private func showVoiceMessageRecorder() {
  1231. mediaPicker?.showVoiceRecorder()
  1232. }
  1233. private func showCameraViewController() {
  1234. if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
  1235. self.mediaPicker?.showCamera()
  1236. } else {
  1237. AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
  1238. DispatchQueue.main.async { [weak self] in
  1239. guard let self = self else { return }
  1240. if granted {
  1241. self.mediaPicker?.showCamera()
  1242. } else {
  1243. self.showCameraPermissionAlert()
  1244. }
  1245. }
  1246. })
  1247. }
  1248. }
  1249. private func showCameraPermissionAlert() {
  1250. DispatchQueue.main.async { [weak self] in
  1251. let alert = UIAlertController(title: String.localized("perm_required_title"),
  1252. message: String.localized("perm_ios_explain_access_to_camera_denied"),
  1253. preferredStyle: .alert)
  1254. if let appSettings = URL(string: UIApplication.openSettingsURLString) {
  1255. alert.addAction(UIAlertAction(title: String.localized("open_settings"), style: .default, handler: { _ in
  1256. UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)}))
  1257. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .destructive, handler: nil))
  1258. }
  1259. self?.present(alert, animated: true, completion: nil)
  1260. }
  1261. }
  1262. private func showPhotoVideoLibrary(delegate: MediaPickerDelegate) {
  1263. mediaPicker?.showPhotoVideoLibrary()
  1264. }
  1265. private func showMediaGallery(currentIndex: Int, msgIds: [Int]) {
  1266. let betterPreviewController = PreviewController(dcContext: dcContext, type: .multi(msgIds, currentIndex))
  1267. let nav = UINavigationController(rootViewController: betterPreviewController)
  1268. nav.modalPresentationStyle = .fullScreen
  1269. navigationController?.present(nav, animated: true)
  1270. }
  1271. private func documentActionPressed(_ action: UIAlertAction) {
  1272. showDocumentLibrary()
  1273. }
  1274. private func voiceMessageButtonPressed(_ action: UIAlertAction) {
  1275. showVoiceMessageRecorder()
  1276. }
  1277. private func cameraButtonPressed(_ action: UIAlertAction) {
  1278. showCameraViewController()
  1279. }
  1280. private func galleryButtonPressed(_ action: UIAlertAction) {
  1281. showPhotoVideoLibrary(delegate: self)
  1282. }
  1283. private func locationStreamingButtonPressed(_ action: UIAlertAction) {
  1284. let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
  1285. if isLocationStreaming {
  1286. locationStreamingFor(seconds: 0)
  1287. } else {
  1288. let alert = UIAlertController(title: String.localized("title_share_location"), message: nil, preferredStyle: .safeActionSheet)
  1289. addDurationSelectionAction(to: alert, key: "share_location_for_5_minutes", duration: Time.fiveMinutes)
  1290. addDurationSelectionAction(to: alert, key: "share_location_for_30_minutes", duration: Time.thirtyMinutes)
  1291. addDurationSelectionAction(to: alert, key: "share_location_for_one_hour", duration: Time.oneHour)
  1292. addDurationSelectionAction(to: alert, key: "share_location_for_two_hours", duration: Time.twoHours)
  1293. addDurationSelectionAction(to: alert, key: "share_location_for_six_hours", duration: Time.sixHours)
  1294. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  1295. self.present(alert, animated: true, completion: nil)
  1296. }
  1297. }
  1298. private func videoChatButtonPressed(_ action: UIAlertAction) {
  1299. let chat = dcContext.getChat(chatId: chatId)
  1300. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("videochat_invite_user_to_videochat"), chat.name),
  1301. message: String.localized("videochat_invite_user_hint"),
  1302. preferredStyle: .alert)
  1303. let cancel = UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil)
  1304. let ok = UIAlertAction(title: String.localized("ok"),
  1305. style: .default,
  1306. handler: { _ in
  1307. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  1308. guard let self = self else { return }
  1309. let messageId = self.dcContext.sendVideoChatInvitation(chatId: self.chatId)
  1310. let inviteMessage = self.dcContext.getMessage(id: messageId)
  1311. if let url = NSURL(string: inviteMessage.getVideoChatUrl()) {
  1312. DispatchQueue.main.async {
  1313. UIApplication.shared.open(url as URL)
  1314. }
  1315. }
  1316. }})
  1317. alert.addAction(cancel)
  1318. alert.addAction(ok)
  1319. self.present(alert, animated: true, completion: nil)
  1320. }
  1321. private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
  1322. let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
  1323. self.locationStreamingFor(seconds: duration)
  1324. })
  1325. alert.addAction(action)
  1326. }
  1327. private func locationStreamingFor(seconds: Int) {
  1328. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
  1329. return
  1330. }
  1331. appDelegate.locationManager.shareLocation(chatId: self.chatId, duration: seconds)
  1332. }
  1333. func updateMessage(_ msg: DcMsg) {
  1334. if messageIds.firstIndex(of: msg.id) != nil {
  1335. reloadData()
  1336. } else {
  1337. // new outgoing message
  1338. if msg.state != DC_STATE_OUT_DRAFT,
  1339. msg.chatId == chatId {
  1340. if let newMsgMarkerIndex = messageIds.firstIndex(of: Int(DC_MSG_ID_MARKER1)) {
  1341. messageIds.remove(at: newMsgMarkerIndex)
  1342. }
  1343. insertMessage(msg)
  1344. } else if msg.type == DC_MSG_WEBXDC,
  1345. msg.chatId == chatId {
  1346. // webxdc draft got updated
  1347. draft.draftMsg = msg
  1348. configureDraftArea(draft: draft, animated: false)
  1349. }
  1350. }
  1351. }
  1352. func insertMessage(_ message: DcMsg) {
  1353. markSeenMessage(id: message.id)
  1354. let wasLastSectionScrolledToBottom = isLastRowScrolledToBottom()
  1355. messageIds.append(message.id)
  1356. emptyStateView.isHidden = true
  1357. reloadData()
  1358. if UIAccessibility.isVoiceOverRunning && !message.isFromCurrentSender {
  1359. scrollToBottom(animated: false, focusOnVoiceOver: true)
  1360. } else if wasLastSectionScrolledToBottom || message.isFromCurrentSender {
  1361. scrollToBottom(animated: true)
  1362. } else {
  1363. updateScrollDownButtonVisibility()
  1364. }
  1365. }
  1366. private func sendTextMessage(text: String, quoteMessage: DcMsg?) {
  1367. DispatchQueue.global().async { [weak self] in
  1368. guard let self = self else { return }
  1369. let message = self.dcContext.newMessage(viewType: DC_MSG_TEXT)
  1370. message.text = text
  1371. if let quoteMessage = quoteMessage {
  1372. message.quoteMessage = quoteMessage
  1373. }
  1374. self.dcContext.sendMessage(chatId: self.chatId, message: message)
  1375. }
  1376. }
  1377. private func focusInputTextView() {
  1378. self.messageInputBar.inputTextView.becomeFirstResponder()
  1379. if UIAccessibility.isVoiceOverRunning {
  1380. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
  1381. UIAccessibility.post(notification: .layoutChanged, argument: self?.messageInputBar.inputTextView)
  1382. })
  1383. }
  1384. }
  1385. private func stageDocument(url: NSURL) {
  1386. keepKeyboard = true
  1387. self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.relativePath)
  1388. self.configureDraftArea(draft: self.draft)
  1389. self.focusInputTextView()
  1390. }
  1391. private func stageVideo(url: NSURL) {
  1392. keepKeyboard = true
  1393. DispatchQueue.main.async { [weak self] in
  1394. guard let self = self else { return }
  1395. self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.relativePath)
  1396. self.configureDraftArea(draft: self.draft)
  1397. self.focusInputTextView()
  1398. }
  1399. }
  1400. private func stageImage(url: NSURL) {
  1401. keepKeyboard = true
  1402. DispatchQueue.global().async { [weak self] in
  1403. if let image = ImageFormat.loadImageFrom(url: url as URL) {
  1404. self?.stageImage(image)
  1405. }
  1406. }
  1407. }
  1408. private func stageImage(_ image: UIImage) {
  1409. DispatchQueue.global().async { [weak self] in
  1410. guard let self = self else { return }
  1411. if let pathInCachesDir = ImageFormat.saveImage(image: image, directory: .cachesDirectory) {
  1412. DispatchQueue.main.async {
  1413. if pathInCachesDir.suffix(4).contains(".gif") {
  1414. self.draft.setAttachment(viewType: DC_MSG_GIF, path: pathInCachesDir)
  1415. } else {
  1416. self.draft.setAttachment(viewType: DC_MSG_IMAGE, path: pathInCachesDir)
  1417. }
  1418. self.configureDraftArea(draft: self.draft)
  1419. self.focusInputTextView()
  1420. ImageFormat.deleteImage(atPath: pathInCachesDir)
  1421. }
  1422. }
  1423. }
  1424. }
  1425. private func sendImage(_ image: UIImage, message: String? = nil) {
  1426. DispatchQueue.global().async { [weak self] in
  1427. guard let self = self else { return }
  1428. if let path = ImageFormat.saveImage(image: image, directory: .cachesDirectory) {
  1429. self.sendAttachmentMessage(viewType: DC_MSG_IMAGE, filePath: path, message: message)
  1430. ImageFormat.deleteImage(atPath: path)
  1431. }
  1432. }
  1433. }
  1434. private func sendSticker(_ image: UIImage) {
  1435. DispatchQueue.global().async { [weak self] in
  1436. guard let self = self else { return }
  1437. if let path = ImageFormat.saveImage(image: image, directory: .cachesDirectory) {
  1438. self.sendAttachmentMessage(viewType: DC_MSG_STICKER, filePath: path, message: nil)
  1439. ImageFormat.deleteImage(atPath: path)
  1440. }
  1441. }
  1442. }
  1443. private func sendAttachmentMessage(viewType: Int32, filePath: String, message: String? = nil, quoteMessage: DcMsg? = nil) {
  1444. let msg = draft.draftMsg ?? dcContext.newMessage(viewType: viewType)
  1445. msg.setFile(filepath: filePath)
  1446. msg.text = (message ?? "").isEmpty ? nil : message
  1447. if quoteMessage != nil {
  1448. msg.quoteMessage = quoteMessage
  1449. }
  1450. dcContext.sendMessage(chatId: self.chatId, message: msg)
  1451. }
  1452. private func sendVoiceMessage(url: NSURL) {
  1453. DispatchQueue.global().async { [weak self] in
  1454. guard let self = self else { return }
  1455. let msg = self.dcContext.newMessage(viewType: DC_MSG_VOICE)
  1456. msg.setFile(filepath: url.relativePath, mimeType: "audio/m4a")
  1457. self.dcContext.sendMessage(chatId: self.chatId, message: msg)
  1458. }
  1459. }
  1460. // MARK: - Context menu
  1461. private func prepareContextMenu() {
  1462. UIMenuController.shared.menuItems = contextMenu.menuItems
  1463. UIMenuController.shared.update()
  1464. }
  1465. override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
  1466. let messageId = messageIds[indexPath.row]
  1467. return !(dcContext.getMessage(id: messageId).isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER)
  1468. }
  1469. override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
  1470. return !tableView.isEditing && contextMenu.canPerformAction(action: action)
  1471. }
  1472. override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
  1473. // handle standard actions here, but custom actions never trigger this. it still needs to be present for the menu to display, though.
  1474. contextMenu.performAction(action: action, indexPath: indexPath)
  1475. }
  1476. @available(iOS 13.0, *)
  1477. override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  1478. return makeTargetedPreview(for: configuration)
  1479. }
  1480. @available(iOS 13.0, *)
  1481. override func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  1482. return makeTargetedPreview(for: configuration)
  1483. }
  1484. @available(iOS 13.0, *)
  1485. private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
  1486. guard let messageId = configuration.identifier as? NSString else { return nil }
  1487. guard let index = messageIds.firstIndex(of: messageId.integerValue) else { return nil }
  1488. let indexPath = IndexPath(row: index, section: 0)
  1489. guard let cell = tableView.cellForRow(at: indexPath) else { return nil }
  1490. // clear background, so that background image is still visible
  1491. let parameters = UIPreviewParameters()
  1492. parameters.backgroundColor = .clear
  1493. return UITargetedPreview(view: cell, parameters: parameters)
  1494. }
  1495. // context menu for iOS 13+
  1496. @available(iOS 13, *)
  1497. override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
  1498. let messageId = messageIds[indexPath.row]
  1499. if tableView.isEditing || dcContext.getMessage(id: messageId).isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER {
  1500. return nil
  1501. }
  1502. return UIContextMenuConfiguration(
  1503. identifier: NSString(string: "\(messageId)"),
  1504. previewProvider: nil,
  1505. actionProvider: { [weak self] _ in
  1506. self?.contextMenu.actionProvider(indexPath: indexPath)
  1507. }
  1508. )
  1509. }
  1510. func showWebxdcViewFor(message: DcMsg) {
  1511. let webxdcViewController = WebxdcViewController(dcContext: dcContext, messageId: message.id)
  1512. navigationController?.pushViewController(webxdcViewController, animated: true)
  1513. }
  1514. func showMediaGalleryFor(indexPath: IndexPath) {
  1515. let messageId = messageIds[indexPath.row]
  1516. let message = dcContext.getMessage(id: messageId)
  1517. if message.type != DC_MSG_STICKER {
  1518. showMediaGalleryFor(message: message)
  1519. }
  1520. }
  1521. func showMediaGalleryFor(message: DcMsg) {
  1522. let msgIds = dcContext.getChatMedia(chatId: chatId, messageType: Int32(message.type), messageType2: 0, messageType3: 0)
  1523. let index = msgIds.firstIndex(of: message.id) ?? 0
  1524. showMediaGallery(currentIndex: index, msgIds: msgIds)
  1525. }
  1526. private func didTapAsm(msg: DcMsg, orgText: String) {
  1527. let inputDlg = UIAlertController(
  1528. title: String.localized("autocrypt_continue_transfer_title"),
  1529. message: String.localized("autocrypt_continue_transfer_please_enter_code"),
  1530. preferredStyle: .alert)
  1531. inputDlg.addTextField(configurationHandler: { (textField) in
  1532. textField.placeholder = msg.setupCodeBegin + ".."
  1533. textField.text = orgText
  1534. textField.keyboardType = UIKeyboardType.numbersAndPunctuation // allows entering spaces; decimalPad would require a mask to keep things readable
  1535. })
  1536. inputDlg.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  1537. let okAction = UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  1538. let textField = inputDlg.textFields![0]
  1539. let modText = textField.text ?? ""
  1540. let success = self.dcContext.continueKeyTransfer(msgId: msg.id, setupCode: modText)
  1541. let alert = UIAlertController(
  1542. title: String.localized("autocrypt_continue_transfer_title"),
  1543. message: String.localized(success ? "autocrypt_continue_transfer_succeeded" : "autocrypt_bad_setup_code"),
  1544. preferredStyle: .alert)
  1545. if success {
  1546. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  1547. } else {
  1548. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  1549. let retryAction = UIAlertAction(title: String.localized("autocrypt_continue_transfer_retry"), style: .default, handler: { _ in
  1550. self.didTapAsm(msg: msg, orgText: modText)
  1551. })
  1552. alert.addAction(retryAction)
  1553. alert.preferredAction = retryAction
  1554. }
  1555. self.navigationController?.present(alert, animated: true, completion: nil)
  1556. })
  1557. inputDlg.addAction(okAction)
  1558. inputDlg.preferredAction = okAction // without setting preferredAction, cancel become shown *bold* as the preferred action
  1559. navigationController?.present(inputDlg, animated: true, completion: nil)
  1560. }
  1561. func handleUIMenu() -> Bool {
  1562. if UIMenuController.shared.isMenuVisible {
  1563. UIMenuController.shared.setMenuVisible(false, animated: true)
  1564. return true
  1565. }
  1566. return false
  1567. }
  1568. func handleSelection(indexPath: IndexPath) -> Bool {
  1569. if tableView.isEditing {
  1570. if tableView.indexPathsForSelectedRows?.contains(indexPath) ?? false {
  1571. tableView.deselectRow(at: indexPath, animated: false)
  1572. } else {
  1573. if let cell = tableView.cellForRow(at: indexPath) as? SelectableCell {
  1574. cell.showSelectionBackground(true)
  1575. }
  1576. tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
  1577. }
  1578. handleEditingBar()
  1579. return true
  1580. }
  1581. return false
  1582. }
  1583. func handleEditingBar() {
  1584. if let rows = tableView.indexPathsForSelectedRows,
  1585. !rows.isEmpty {
  1586. editingBar.isEnabled = true
  1587. } else {
  1588. editingBar.isEnabled = false
  1589. }
  1590. }
  1591. func setEditing(isEditing: Bool, selectedAtIndexPath: IndexPath? = nil) {
  1592. self.tableView.setEditing(isEditing, animated: true)
  1593. self.draft.isEditing = isEditing
  1594. self.configureDraftArea(draft: self.draft)
  1595. if let indexPath = selectedAtIndexPath {
  1596. _ = handleSelection(indexPath: indexPath)
  1597. }
  1598. }
  1599. private func setDefaultBackgroundImage(view: UIImageView) {
  1600. if #available(iOS 12.0, *) {
  1601. view.image = UIImage(named: traitCollection.userInterfaceStyle == .light ? "background_light" : "background_dark")
  1602. } else {
  1603. view.image = UIImage(named: "background_light")
  1604. }
  1605. }
  1606. }
  1607. // MARK: - BaseMessageCellDelegate
  1608. extension ChatViewController: BaseMessageCellDelegate {
  1609. @objc func actionButtonTapped(indexPath: IndexPath) {
  1610. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1611. return
  1612. }
  1613. let msg = dcContext.getMessage(id: messageIds[indexPath.row])
  1614. if msg.downloadState != DC_DOWNLOAD_DONE {
  1615. dcContext.downloadFullMessage(id: msg.id)
  1616. } else if msg.type == DC_MSG_WEBXDC {
  1617. showWebxdcViewFor(message: msg)
  1618. } else {
  1619. let fullMessageViewController = FullMessageViewController(dcContext: dcContext, messageId: msg.id)
  1620. navigationController?.pushViewController(fullMessageViewController, animated: true)
  1621. }
  1622. }
  1623. @objc func quoteTapped(indexPath: IndexPath) {
  1624. if handleSelection(indexPath: indexPath) { return }
  1625. _ = handleUIMenu()
  1626. let msg = dcContext.getMessage(id: messageIds[indexPath.row])
  1627. if let quoteMsg = msg.quoteMessage {
  1628. if self.chatId == quoteMsg.chatId {
  1629. scrollToMessage(msgId: quoteMsg.id)
  1630. } else {
  1631. showChat(chatId: quoteMsg.chatId, messageId: quoteMsg.id, animated: false)
  1632. }
  1633. }
  1634. }
  1635. @objc func textTapped(indexPath: IndexPath) {
  1636. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1637. return
  1638. }
  1639. let message = dcContext.getMessage(id: messageIds[indexPath.row])
  1640. if message.isSetupMessage {
  1641. didTapAsm(msg: message, orgText: "")
  1642. }
  1643. }
  1644. @objc func phoneNumberTapped(number: String, indexPath: IndexPath) {
  1645. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1646. return
  1647. }
  1648. let sanitizedNumber = number.filter("0123456789".contains)
  1649. if let phoneURL = URL(string: "tel://\(sanitizedNumber)") {
  1650. UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
  1651. }
  1652. logger.debug("phone number tapped \(sanitizedNumber)")
  1653. }
  1654. @objc func commandTapped(command: String, indexPath: IndexPath) {
  1655. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1656. return
  1657. }
  1658. logger.debug("command tapped \(command)")
  1659. }
  1660. @objc func urlTapped(url: URL, indexPath: IndexPath) {
  1661. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1662. return
  1663. }
  1664. if Utils.isEmail(url: url) {
  1665. logger.debug("tapped on contact")
  1666. let email = Utils.getEmailFrom(url)
  1667. self.askToChatWith(email: email)
  1668. } else {
  1669. UIApplication.shared.open(url)
  1670. }
  1671. }
  1672. @objc func imageTapped(indexPath: IndexPath) {
  1673. if handleUIMenu() || handleSelection(indexPath: indexPath) {
  1674. return
  1675. }
  1676. let message = dcContext.getMessage(id: messageIds[indexPath.row])
  1677. if message.type == DC_MSG_WEBXDC {
  1678. showWebxdcViewFor(message: message)
  1679. } else {
  1680. showMediaGalleryFor(indexPath: indexPath)
  1681. }
  1682. }
  1683. @objc func avatarTapped(indexPath: IndexPath) {
  1684. let message = dcContext.getMessage(id: messageIds[indexPath.row])
  1685. let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: message.fromContactId)
  1686. navigationController?.pushViewController(contactDetailController, animated: true)
  1687. }
  1688. }
  1689. // MARK: - MediaPickerDelegate
  1690. extension ChatViewController: MediaPickerDelegate {
  1691. func onVideoSelected(url: NSURL) {
  1692. stageVideo(url: url)
  1693. }
  1694. func onImageSelected(url: NSURL) {
  1695. stageImage(url: url)
  1696. }
  1697. func onImageSelected(image: UIImage) {
  1698. stageImage(image)
  1699. }
  1700. func onVoiceMessageRecorded(url: NSURL) {
  1701. sendVoiceMessage(url: url)
  1702. }
  1703. func onVoiceMessageRecorderClosed() {
  1704. if UIAccessibility.isVoiceOverRunning {
  1705. UIAccessibility.post(notification: .announcement, argument: nil)
  1706. // we need to wait a little bit, otherwise the UIAccessibility notification is ignored and
  1707. // the first accessibility element on the screen gets selected
  1708. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
  1709. // return to attach button
  1710. UIAccessibility.post(notification: .screenChanged, argument: self?.messageInputBar.leftStackView.subviews.first)
  1711. }
  1712. }
  1713. }
  1714. func onDocumentSelected(url: NSURL) {
  1715. stageDocument(url: url)
  1716. }
  1717. }
  1718. // MARK: - MessageInputBarDelegate
  1719. extension ChatViewController: InputBarAccessoryViewDelegate {
  1720. func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
  1721. keepKeyboard = true
  1722. let trimmedText = text.replacingOccurrences(of: "\u{FFFC}", with: "", options: .literal, range: nil)
  1723. .trimmingCharacters(in: .whitespacesAndNewlines)
  1724. if let filePath = draft.attachment, let viewType = draft.viewType {
  1725. switch viewType {
  1726. case DC_MSG_GIF, DC_MSG_IMAGE, DC_MSG_FILE, DC_MSG_VIDEO, DC_MSG_WEBXDC:
  1727. self.sendAttachmentMessage(viewType: viewType, filePath: filePath, message: trimmedText, quoteMessage: draft.quoteMessage)
  1728. default:
  1729. logger.warning("Unsupported viewType for drafted messages.")
  1730. }
  1731. } else if inputBar.inputTextView.images.isEmpty {
  1732. self.sendTextMessage(text: trimmedText, quoteMessage: draft.quoteMessage)
  1733. } else {
  1734. // only 1 attachment allowed for now, thus it takes the first one
  1735. self.sendImage(inputBar.inputTextView.images[0], message: trimmedText)
  1736. }
  1737. inputBar.inputTextView.text = String()
  1738. inputBar.inputTextView.attributedText = nil
  1739. draft.clear()
  1740. draftArea.cancel()
  1741. }
  1742. func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) {
  1743. draft.text = text
  1744. evaluateInputBar(draft: draft)
  1745. }
  1746. }
  1747. // MARK: - DraftPreviewDelegate
  1748. extension ChatViewController: DraftPreviewDelegate {
  1749. func onCancelQuote() {
  1750. keepKeyboard = true
  1751. draft.setQuote(quotedMsg: nil)
  1752. configureDraftArea(draft: draft)
  1753. focusInputTextView()
  1754. }
  1755. func onCancelAttachment() {
  1756. keepKeyboard = true
  1757. draft.clearAttachment()
  1758. configureDraftArea(draft: draft)
  1759. evaluateInputBar(draft: draft)
  1760. focusInputTextView()
  1761. }
  1762. func onAttachmentAdded() {
  1763. evaluateInputBar(draft: draft)
  1764. }
  1765. func onAttachmentTapped() {
  1766. if let attachmentPath = draft.attachment {
  1767. let attachmentURL = URL(fileURLWithPath: attachmentPath, isDirectory: false)
  1768. if draft.viewType == DC_MSG_WEBXDC, let draftMessage = draft.draftMsg {
  1769. showWebxdcViewFor(message: draftMessage)
  1770. } else {
  1771. let previewController = PreviewController(dcContext: dcContext, type: .single(attachmentURL))
  1772. if #available(iOS 13.0, *), draft.viewType == DC_MSG_IMAGE || draft.viewType == DC_MSG_VIDEO {
  1773. previewController.setEditing(true, animated: true)
  1774. previewController.delegate = self
  1775. }
  1776. let nav = UINavigationController(rootViewController: previewController)
  1777. nav.modalPresentationStyle = .fullScreen
  1778. navigationController?.present(nav, animated: true)
  1779. }
  1780. }
  1781. }
  1782. }
  1783. // MARK: - ChatEditingDelegate
  1784. extension ChatViewController: ChatEditingDelegate {
  1785. func onDeletePressed() {
  1786. if let rows = tableView.indexPathsForSelectedRows {
  1787. let messageIdsToDelete = rows.compactMap { messageIds[$0.row] }
  1788. askToDeleteMessages(ids: messageIdsToDelete)
  1789. }
  1790. }
  1791. func onForwardPressed() {
  1792. if let rows = tableView.indexPathsForSelectedRows {
  1793. let messageIdsToForward = rows.compactMap { messageIds[$0.row] }
  1794. RelayHelper.sharedInstance.setForwardMessages(messageIds: messageIdsToForward)
  1795. self.navigationController?.popViewController(animated: true)
  1796. }
  1797. }
  1798. func onCancelPressed() {
  1799. setEditing(isEditing: false)
  1800. }
  1801. }
  1802. // MARK: - ChatSearchDelegate
  1803. extension ChatViewController: ChatSearchDelegate {
  1804. func onSearchPreviousPressed() {
  1805. logger.debug("onSearch Previous Pressed")
  1806. if searchResultIndex == 0 && !searchMessageIds.isEmpty {
  1807. searchResultIndex = searchMessageIds.count - 1
  1808. } else {
  1809. searchResultIndex -= 1
  1810. }
  1811. scrollToMessage(msgId: searchMessageIds[searchResultIndex], animated: true, scrollToText: true)
  1812. searchAccessoryBar.updateSearchResult(sum: self.searchMessageIds.count, position: searchResultIndex + 1)
  1813. self.reloadData()
  1814. }
  1815. func onSearchNextPressed() {
  1816. logger.debug("onSearch Next Pressed")
  1817. if searchResultIndex == searchMessageIds.count - 1 {
  1818. searchResultIndex = 0
  1819. } else {
  1820. searchResultIndex += 1
  1821. }
  1822. scrollToMessage(msgId: searchMessageIds[searchResultIndex], animated: true, scrollToText: true)
  1823. searchAccessoryBar.updateSearchResult(sum: self.searchMessageIds.count, position: searchResultIndex + 1)
  1824. self.reloadData()
  1825. }
  1826. }
  1827. // MARK: UISearchResultUpdating
  1828. extension ChatViewController: UISearchResultsUpdating {
  1829. func updateSearchResults(for searchController: UISearchController) {
  1830. logger.debug("searchbar: \(String(describing: searchController.searchBar.text))")
  1831. debounceTimer?.invalidate()
  1832. debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
  1833. let searchText = searchController.searchBar.text ?? ""
  1834. DispatchQueue.global(qos: .userInteractive).async { [weak self] in
  1835. guard let self = self else { return }
  1836. let resultIds = self.dcContext.searchMessages(chatId: self.chatId, searchText: searchText)
  1837. DispatchQueue.main.async { [weak self] in
  1838. guard let self = self else { return }
  1839. self.searchMessageIds = resultIds
  1840. self.searchResultIndex = self.searchMessageIds.isEmpty ? 0 : self.searchMessageIds.count - 1
  1841. self.searchAccessoryBar.isEnabled = !resultIds.isEmpty
  1842. self.searchAccessoryBar.updateSearchResult(sum: self.searchMessageIds.count, position: self.searchResultIndex + 1)
  1843. if let lastId = resultIds.last {
  1844. self.scrollToMessage(msgId: lastId, animated: true, scrollToText: true)
  1845. }
  1846. self.reloadData()
  1847. }
  1848. }
  1849. }
  1850. }
  1851. }
  1852. // MARK: - UISearchBarDelegate
  1853. extension ChatViewController: UISearchBarDelegate {
  1854. func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
  1855. configureDraftArea(draft: draft)
  1856. return true
  1857. }
  1858. func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
  1859. configureDraftArea(draft: draft)
  1860. tableView.becomeFirstResponder()
  1861. }
  1862. func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
  1863. searchController.isActive = false
  1864. configureDraftArea(draft: draft)
  1865. tableView.becomeFirstResponder()
  1866. navigationItem.searchController = nil
  1867. reloadData()
  1868. }
  1869. }
  1870. // MARK: - UISearchControllerDelegate
  1871. extension ChatViewController: UISearchControllerDelegate {
  1872. func didPresentSearchController(_ searchController: UISearchController) {
  1873. DispatchQueue.main.async { [weak self] in
  1874. self?.searchController.searchBar.becomeFirstResponder()
  1875. }
  1876. }
  1877. }
  1878. // MARK: - ChatContactRequestBar
  1879. extension ChatViewController: ChatContactRequestDelegate {
  1880. func onAcceptRequest() {
  1881. dcContext.acceptChat(chatId: chatId)
  1882. let chat = dcContext.getChat(chatId: chatId)
  1883. if chat.isMailinglist {
  1884. messageInputBar.isHidden = true
  1885. } else {
  1886. configureUIForWriting()
  1887. }
  1888. }
  1889. func onBlockRequest() {
  1890. dcContext.blockChat(chatId: chatId)
  1891. self.navigationController?.popViewController(animated: true)
  1892. }
  1893. func onDeleteRequest() {
  1894. self.askToDeleteChat()
  1895. }
  1896. }
  1897. // MARK: - QLPreviewControllerDelegate
  1898. extension ChatViewController: QLPreviewControllerDelegate {
  1899. @available(iOS 13.0, *)
  1900. func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
  1901. return .updateContents
  1902. }
  1903. func previewController(_ controller: QLPreviewController, didUpdateContentsOf previewItem: QLPreviewItem) {
  1904. DispatchQueue.main.async { [weak self] in
  1905. guard let self = self else { return }
  1906. self.draftArea.reload(draft: self.draft)
  1907. }
  1908. }
  1909. }
  1910. // MARK: - AudioControllerDelegate
  1911. extension ChatViewController: AudioControllerDelegate {
  1912. func onAudioPlayFailed() {
  1913. let alert = UIAlertController(title: String.localized("error"),
  1914. message: String.localized("cannot_play_unsupported_file_type"),
  1915. preferredStyle: .safeActionSheet)
  1916. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  1917. self.present(alert, animated: true, completion: nil)
  1918. }
  1919. }
  1920. // MARK: - UITextViewDelegate
  1921. extension ChatViewController: UITextViewDelegate {
  1922. func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
  1923. if keepKeyboard {
  1924. DispatchQueue.main.async { [weak self] in
  1925. self?.messageInputBar.inputTextView.becomeFirstResponder()
  1926. }
  1927. keepKeyboard = false
  1928. return false
  1929. }
  1930. return true
  1931. }
  1932. }
  1933. // MARK: - ChatInputTextViewPasteDelegate
  1934. extension ChatViewController: ChatInputTextViewPasteDelegate {
  1935. func onImagePasted(image: UIImage) {
  1936. sendSticker(image)
  1937. }
  1938. }