ChatListViewModel.swift 12 KB

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