ChatViewController.swift 107 KB

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