ChatViewController.swift 106 KB

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