ChatViewController.swift 97 KB

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