ChatViewController.swift 102 KB

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