ChatListViewModel.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import UIKit
  2. import DcCore
  3. // MARK: - ChatListViewModelProtocol
  4. protocol ChatListViewModelProtocol: class, UISearchResultsUpdating {
  5. var onChatListUpdate: VoidFunction? { get set }
  6. var isArchive: Bool { get }
  7. var numberOfSections: Int { get }
  8. func numberOfRowsIn(section: Int) -> Int
  9. func cellDataFor(section: Int, row: Int) -> AvatarCellViewModel
  10. func msgIdFor(row: Int) -> Int?
  11. func chatIdFor(section: Int, row: Int) -> Int? // needed to differentiate betweeen deaddrop / archive / default
  12. // search related
  13. var searchActive: Bool { get }
  14. func beginSearch()
  15. func endSearch()
  16. func titleForHeaderIn(section: Int) -> String? // only visible on search results
  17. var emptySearchText: String? { get }
  18. func deleteChat(chatId: Int)
  19. func archiveChatToggle(chatId: Int)
  20. func pinChatToggle(chatId: Int)
  21. func refreshData()
  22. }
  23. // MARK: - ChatListViewModel
  24. class ChatListViewModel: NSObject, ChatListViewModelProtocol {
  25. var onChatListUpdate: VoidFunction?
  26. private var inBgSearch = false
  27. private var needsAnotherBgSearch = false
  28. enum ChatListSectionType {
  29. case chats
  30. case contacts
  31. case messages
  32. }
  33. var isArchive: Bool
  34. private let dcContext: DcContext
  35. var searchActive: Bool = false
  36. // if searchfield is empty we show default chat list
  37. private var showSearchResults: Bool {
  38. return searchActive && searchText.containsCharacters()
  39. }
  40. private var chatList: DcChatlist!
  41. // for search filtering
  42. private var searchText: String = ""
  43. private var searchResultChatList: DcChatlist?
  44. private var searchResultContactIds: [Int] = []
  45. private var searchResultMessageIds: [Int] = []
  46. // to manage sections dynamically
  47. private var searchResultsChatsSection: ChatListSectionType = .chats
  48. private var searchResultsContactsSection: ChatListSectionType = .contacts
  49. private var searchResultsMessagesSection: ChatListSectionType = .messages
  50. private var searchResultSections: [ChatListSectionType] = []
  51. init(dcContext: DcContext, isArchive: Bool) {
  52. self.isArchive = isArchive
  53. self.dcContext = dcContext
  54. super.init()
  55. updateChatList(notifyListener: true)
  56. }
  57. private func updateChatList(notifyListener: Bool) {
  58. var gclFlags: Int32 = 0
  59. if isArchive {
  60. gclFlags |= DC_GCL_ARCHIVED_ONLY
  61. } else if RelayHelper.sharedInstance.isForwarding() {
  62. gclFlags |= DC_GCL_FOR_FORWARDING
  63. }
  64. self.chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
  65. if notifyListener, let onChatListUpdate = onChatListUpdate {
  66. if Thread.isMainThread {
  67. onChatListUpdate()
  68. } else {
  69. DispatchQueue.main.async {
  70. onChatListUpdate()
  71. }
  72. }
  73. }
  74. }
  75. var numberOfSections: Int {
  76. if showSearchResults {
  77. return searchResultSections.count
  78. }
  79. return 1
  80. }
  81. func numberOfRowsIn(section: Int) -> Int {
  82. if showSearchResults {
  83. switch searchResultSections[section] {
  84. case .chats:
  85. return searchResultChatList?.length ?? 0
  86. case .contacts:
  87. return searchResultContactIds.count
  88. case .messages:
  89. return searchResultMessageIds.count
  90. }
  91. }
  92. return chatList.length
  93. }
  94. func cellDataFor(section: Int, row: Int) -> AvatarCellViewModel {
  95. if showSearchResults {
  96. switch searchResultSections[section] {
  97. case .chats:
  98. return makeChatCellViewModel(index: row, searchText: searchText)
  99. case .contacts:
  100. if row >= 0 && row < searchResultContactIds.count {
  101. return ContactCellViewModel.make(contactId: searchResultContactIds[row], searchText: searchText, dcContext: dcContext)
  102. } else {
  103. logger.warning("search: requested contact index \(row) not in range 0..\(searchResultContactIds.count)")
  104. }
  105. case .messages:
  106. if row >= 0 && row < searchResultMessageIds.count {
  107. return makeMessageCellViewModel(msgId: searchResultMessageIds[row])
  108. } else {
  109. logger.warning("search: requested message index \(row) not in range 0..\(searchResultMessageIds.count)")
  110. }
  111. }
  112. }
  113. return makeChatCellViewModel(index: row, searchText: "")
  114. }
  115. func titleForHeaderIn(section: Int) -> String? {
  116. if showSearchResults {
  117. switch searchResultSections[section] {
  118. case .chats:
  119. return String.localized(stringID: "n_chats", count: numberOfRowsIn(section: section))
  120. case .contacts:
  121. return String.localized(stringID: "n_contacts", count: numberOfRowsIn(section: section))
  122. case .messages:
  123. let count = numberOfRowsIn(section: section)
  124. var ret = String.localized(stringID: "n_messages", count: count)
  125. if count==1000 {
  126. // a count of 1000 results may be limited, see documentation of dc_search_msgs()
  127. // (formatting may be "1.000" or "1,000", so just skip the first digit)
  128. ret = ret.replacingOccurrences(of: "000", with: "000+")
  129. }
  130. return ret
  131. }
  132. }
  133. return nil
  134. }
  135. func chatIdFor(section: Int, row: Int) -> Int? {
  136. let cellData = cellDataFor(section: section, row: row)
  137. switch cellData.type {
  138. case .deaddrop(let data):
  139. return data.chatId
  140. case .chat(let data):
  141. return data.chatId
  142. case .contact:
  143. return nil
  144. case .profile:
  145. return nil
  146. }
  147. }
  148. func msgIdFor(row: Int) -> Int? {
  149. if showSearchResults {
  150. return nil
  151. }
  152. return chatList.getMsgId(index: row)
  153. }
  154. func refreshData() {
  155. updateChatList(notifyListener: true)
  156. }
  157. func beginSearch() {
  158. searchActive = true
  159. }
  160. func endSearch() {
  161. searchActive = false
  162. searchText = ""
  163. resetSearch()
  164. }
  165. var emptySearchText: String? {
  166. if searchActive && numberOfSections == 0 {
  167. return searchText
  168. }
  169. return nil
  170. }
  171. func deleteChat(chatId: Int) {
  172. dcContext.deleteChat(chatId: chatId)
  173. NotificationManager.removeNotificationsForChat(chatId: chatId)
  174. }
  175. func archiveChatToggle(chatId: Int) {
  176. let chat = dcContext.getChat(chatId: chatId)
  177. dcContext.archiveChat(chatId: chatId, archive: !chat.isArchived)
  178. updateChatList(notifyListener: false)
  179. }
  180. func pinChatToggle(chatId: Int) {
  181. let chat: DcChat = dcContext.getChat(chatId: chatId)
  182. let pinned = chat.visibility==DC_CHAT_VISIBILITY_PINNED
  183. self.dcContext.setChatVisibility(chatId: chatId, visibility: pinned ? DC_CHAT_VISIBILITY_NORMAL : DC_CHAT_VISIBILITY_PINNED)
  184. updateChatList(notifyListener: false)
  185. }
  186. }
  187. private extension ChatListViewModel {
  188. /// MARK: - avatarCellViewModel factory
  189. func makeChatCellViewModel(index: Int, searchText: String) -> AvatarCellViewModel {
  190. let list: DcChatlist = searchResultChatList ?? chatList
  191. let chatId = list.getChatId(index: index)
  192. let summary = list.getSummary(index: index)
  193. if let msgId = msgIdFor(row: index), chatId == DC_CHAT_ID_DEADDROP {
  194. return ChatCellViewModel(dcContext: dcContext, deaddropCellData: DeaddropCellData(chatId: chatId, msgId: msgId, summary: summary))
  195. }
  196. let chat = dcContext.getChat(chatId: chatId)
  197. let unreadMessages = dcContext.getUnreadMessages(chatId: chatId)
  198. var chatTitleIndexes: [Int] = []
  199. if searchText.containsCharacters() {
  200. let chatName = chat.name
  201. chatTitleIndexes = chatName.containsExact(subSequence: searchText)
  202. }
  203. let viewModel = ChatCellViewModel(
  204. dcContext: dcContext,
  205. chatData: ChatCellData(
  206. chatId: chatId,
  207. highlightMsgId: nil,
  208. summary: summary,
  209. unreadMessages: unreadMessages
  210. ),
  211. titleHighlightIndexes: chatTitleIndexes
  212. )
  213. return viewModel
  214. }
  215. func makeMessageCellViewModel(msgId: Int) -> AvatarCellViewModel {
  216. let msg: DcMsg = DcMsg(id: msgId)
  217. let chatId: Int = msg.chatId
  218. let chat: DcChat = dcContext.getChat(chatId: chatId)
  219. let summary: DcLot = msg.summary(chat: chat)
  220. let unreadMessages = dcContext.getUnreadMessages(chatId: chatId)
  221. let viewModel = ChatCellViewModel(
  222. dcContext: dcContext,
  223. chatData: ChatCellData(
  224. chatId: chatId,
  225. highlightMsgId: msgId,
  226. summary: summary,
  227. unreadMessages: unreadMessages
  228. )
  229. )
  230. let subtitle = viewModel.subtitle
  231. viewModel.subtitleHighlightIndexes = subtitle.containsExact(subSequence: searchText)
  232. return viewModel
  233. }
  234. // MARK: - search
  235. private func updateSearchResultSections() {
  236. var sections: [ChatListSectionType] = []
  237. if let chatList = searchResultChatList, chatList.length > 0 {
  238. sections.append(searchResultsChatsSection)
  239. }
  240. if !searchResultContactIds.isEmpty {
  241. sections.append(searchResultsContactsSection)
  242. }
  243. if !searchResultMessageIds.isEmpty {
  244. sections.append(searchResultsMessagesSection)
  245. }
  246. searchResultSections = sections
  247. }
  248. private func resetSearch() {
  249. searchResultChatList = nil
  250. searchResultContactIds = []
  251. searchResultMessageIds = []
  252. updateSearchResultSections()
  253. }
  254. private func filterContentForSearchText(_ searchText: String) {
  255. if !searchText.isEmpty {
  256. filterAndUpdateList(searchText: searchText)
  257. } else {
  258. // when search input field empty we show default chatList
  259. resetSearch()
  260. }
  261. if let onChatListUpdate = onChatListUpdate {
  262. if Thread.isMainThread {
  263. onChatListUpdate()
  264. } else {
  265. DispatchQueue.main.async {
  266. onChatListUpdate()
  267. }
  268. }
  269. }
  270. }
  271. func filterAndUpdateList(searchText: String) {
  272. var overallCnt = 0
  273. // #1 chats with searchPattern in title bar
  274. searchResultChatList = dcContext.getChatlist(flags: DC_GCL_NO_SPECIALS, queryString: searchText, queryId: 0)
  275. if let chatlist = searchResultChatList {
  276. overallCnt += chatlist.length
  277. }
  278. // #2 contacts with searchPattern in name or in email
  279. if searchText != self.searchText && overallCnt > 0 {
  280. logger.info("... skipping getContacts and searchMessages, more recent search pending")
  281. searchResultContactIds = []
  282. searchResultMessageIds = []
  283. updateSearchResultSections()
  284. return
  285. }
  286. searchResultContactIds = dcContext.getContacts(flags: DC_GCL_ADD_SELF, queryString: searchText)
  287. overallCnt += searchResultContactIds.count
  288. // #3 messages with searchPattern (filtered by dc_core)
  289. if searchText != self.searchText && overallCnt > 0 {
  290. logger.info("... skipping searchMessages, more recent search pending")
  291. searchResultMessageIds = []
  292. updateSearchResultSections()
  293. return
  294. }
  295. if searchText.count <= 1 {
  296. logger.info("... skipping searchMessages, string too short")
  297. searchResultMessageIds = []
  298. updateSearchResultSections()
  299. return
  300. }
  301. searchResultMessageIds = dcContext.searchMessages(searchText: searchText)
  302. updateSearchResultSections()
  303. }
  304. }
  305. // MARK: UISearchResultUpdating
  306. extension ChatListViewModel: UISearchResultsUpdating {
  307. func updateSearchResults(for searchController: UISearchController) {
  308. self.searchText = searchController.searchBar.text ?? ""
  309. if inBgSearch {
  310. needsAnotherBgSearch = true
  311. logger.info("... search call debounced")
  312. } else {
  313. inBgSearch = true
  314. DispatchQueue.global(qos: .userInteractive).async { [weak self] in
  315. usleep(100000)
  316. self?.needsAnotherBgSearch = false
  317. self?.filterContentForSearchText(self?.searchText ?? "")
  318. while self?.needsAnotherBgSearch != false {
  319. usleep(100000)
  320. self?.needsAnotherBgSearch = false
  321. logger.info("... executing debounced search call")
  322. self?.filterContentForSearchText(self?.searchText ?? "")
  323. }
  324. self?.inBgSearch = false
  325. }
  326. }
  327. }
  328. }