| /* |
| * Copyright (C) 2017-2019 Savoir-faire Linux Inc. |
| * |
| * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> |
| * Author: Quentin Muret <quentin.muret@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| import RxSwift |
| import SwiftyBeaver |
| |
| class SmartlistViewModel: Stateable, ViewModel { |
| |
| private let log = SwiftyBeaver.self |
| |
| // MARK: - Rx Stateable |
| private let stateSubject = PublishSubject<State>() |
| lazy var state: Observable<State> = { |
| return self.stateSubject.asObservable() |
| }() |
| |
| fileprivate let disposeBag = DisposeBag() |
| |
| //Services |
| fileprivate let conversationsService: ConversationsService |
| fileprivate let nameService: NameService |
| fileprivate let accountsService: AccountsService |
| fileprivate let contactsService: ContactsService |
| fileprivate let networkService: NetworkService |
| |
| let searchBarText = Variable<String>("") |
| var isSearching: Observable<Bool>! |
| var conversations: Observable<[ConversationSection]>! |
| var searchResults: Observable<[ConversationSection]>! |
| var hideNoConversationsMessage: Observable<Bool>! |
| var searchStatus = PublishSubject<String>() |
| var connectionState = PublishSubject<ConnectionType>() |
| |
| fileprivate var filteredResults = Variable([ConversationViewModel]()) |
| fileprivate var contactFoundConversation = Variable<ConversationViewModel?>(nil) |
| fileprivate var conversationViewModels = [ConversationViewModel]() |
| |
| func networkConnectionState() -> ConnectionType { |
| return self.networkService.connectionState.value |
| } |
| |
| let injectionBag: InjectionBag |
| |
| // swiftlint:disable function_body_length |
| // swiftlint:disable cyclomatic_complexity |
| required init(with injectionBag: InjectionBag) { |
| self.conversationsService = injectionBag.conversationsService |
| self.nameService = injectionBag.nameService |
| self.accountsService = injectionBag.accountService |
| self.contactsService = injectionBag.contactsService |
| self.networkService = injectionBag.networkService |
| self.injectionBag = injectionBag |
| |
| // Observe connectivity changes |
| self.networkService.connectionStateObservable |
| .subscribe(onNext: { value in |
| self.connectionState.onNext(value) |
| }) |
| .disposed(by: self.disposeBag) |
| |
| //Create observable from sorted conversations and flatMap them to view models |
| let conversationsObservable: Observable<[ConversationViewModel]> = self.conversationsService.conversationsForCurrentAccount.map({ [weak self] conversations in |
| return conversations |
| .sorted(by: { conversation1, conversations2 in |
| |
| guard let lastMessage1 = conversation1.messages.last, |
| let lastMessage2 = conversations2.messages.last else { |
| return true |
| } |
| |
| return lastMessage1.receivedDate > lastMessage2.receivedDate |
| }) |
| .filter({ self?.contactsService.contact(withRingId: $0.recipientRingId) != nil || (!$0.messages.isEmpty && (self?.contactsService.contactRequest(withRingId: $0.recipientRingId) == nil)) |
| }) |
| .compactMap({ conversationModel in |
| |
| var conversationViewModel: ConversationViewModel? |
| |
| //Get the current ConversationViewModel if exists or create it |
| if let foundConversationViewModel = self?.conversationViewModels.filter({ conversationViewModel in |
| return conversationViewModel.conversation.value == conversationModel |
| }).first { |
| conversationViewModel = foundConversationViewModel |
| } else if let contactFound = self?.contactFoundConversation.value, contactFound.conversation.value == conversationModel { |
| conversationViewModel = contactFound |
| self?.conversationViewModels.append(contactFound) |
| } else { |
| conversationViewModel = ConversationViewModel(with: injectionBag) |
| conversationViewModel?.conversation = Variable<ConversationModel>(conversationModel) |
| self?.conversationViewModels.append(conversationViewModel!) |
| } |
| |
| return conversationViewModel |
| }) |
| }) |
| |
| //Create observable from conversations viewModels to ConversationSection |
| self.conversations = conversationsObservable.map({ conversationsViewModels in |
| return [ConversationSection(header: "", items: conversationsViewModels)] |
| }).observeOn(MainScheduler.instance) |
| |
| //Create observable from filtered conversatiosn and contact founds viewModels to ConversationSection |
| self.searchResults = Observable<[ConversationSection]>.combineLatest(self.contactFoundConversation.asObservable(), |
| self.filteredResults.asObservable(), |
| resultSelector: { contactFoundConversation, filteredResults in |
| |
| var sections = [ConversationSection]() |
| if !filteredResults.isEmpty { |
| sections.append(ConversationSection(header: L10n.Smartlist.conversations, items: filteredResults)) |
| } else if contactFoundConversation != nil { |
| sections.append(ConversationSection(header: L10n.Smartlist.results, items: [contactFoundConversation!])) |
| } |
| |
| return sections |
| }).observeOn(MainScheduler.instance) |
| |
| self.hideNoConversationsMessage = Observable |
| .combineLatest( self.conversations, self.searchBarText.asObservable(), resultSelector: { conversations, searchBarText in |
| return !conversations.first!.items.isEmpty || !searchBarText.isEmpty |
| }).observeOn(MainScheduler.instance) |
| |
| //Observes if the user is searching |
| self.isSearching = searchBarText.asObservable().map({ text in |
| return !text.isEmpty |
| }).observeOn(MainScheduler.instance) |
| |
| //Observes search bar text |
| searchBarText.asObservable().observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self] text in |
| self.search(withText: text) |
| }).disposed(by: disposeBag) |
| |
| //Observe username lookup |
| self.nameService.usernameLookupStatus.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self, unowned injectionBag] usernameLookupStatus in |
| if usernameLookupStatus.state == .found && (usernameLookupStatus.name == self.searchBarText.value || usernameLookupStatus.address == self.searchBarText.value) { |
| if let conversation = self.conversationViewModels.filter({ conversationViewModel in |
| conversationViewModel.conversation.value.recipientRingId == usernameLookupStatus.address |
| }).first { |
| self.contactFoundConversation.value = conversation |
| } else { |
| if self.contactFoundConversation.value?.conversation.value |
| .recipientRingId != usernameLookupStatus.address { |
| |
| var ringId = "" |
| var accountId = "" |
| if let account = self.accountsService.currentAccount { |
| accountId = account.id |
| if let uri = AccountModelHelper(withAccount: account).ringId { |
| ringId = uri |
| } |
| } |
| |
| //Create new converation |
| let conversation = ConversationModel(withRecipientRingId: usernameLookupStatus.address, accountId: accountId, accountUri: ringId) |
| let newConversation = ConversationViewModel(with: injectionBag) |
| newConversation.conversation = Variable<ConversationModel>(conversation) |
| self.contactFoundConversation.value = newConversation |
| } |
| } |
| self.searchStatus.onNext("") |
| } else { |
| if self.filteredResults.value.isEmpty |
| && self.contactFoundConversation.value == nil { |
| self.searchStatus.onNext(L10n.Smartlist.noResults) |
| } else { |
| self.searchStatus.onNext("") |
| } |
| } |
| }).disposed(by: disposeBag) |
| } |
| |
| fileprivate func search(withText text: String) { |
| guard let currentAccount = self.accountsService.currentAccount else { return } |
| |
| self.contactFoundConversation.value = nil |
| self.filteredResults.value.removeAll() |
| self.searchStatus.onNext("") |
| |
| if text.isEmpty {return} |
| |
| //Filter conversations |
| let filteredConversations = self.conversationViewModels |
| .filter({conversationViewModel in |
| conversationViewModel.conversation.value.recipientRingId == text |
| }) |
| |
| if !filteredConversations.isEmpty { |
| self.filteredResults.value = filteredConversations |
| } |
| |
| if !text.isSHA1() { |
| self.nameService.lookupName(withAccount: "", nameserver: "", name: text) |
| self.searchStatus.onNext(L10n.Smartlist.searching) |
| return |
| } |
| |
| if self.contactFoundConversation.value?.conversation.value.recipientRingId != text { |
| let accountId = self.accountsService.currentAccount?.id ?? "" |
| let jamiId = AccountModelHelper(withAccount: currentAccount).ringId ?? "" |
| //Create new converation |
| let conversation = ConversationModel(withRecipientRingId: text.replacingOccurrences(of: "ring:", with: ""), accountId: accountId, accountUri: jamiId) |
| let newConversation = ConversationViewModel(with: self.injectionBag) |
| newConversation.conversation = Variable<ConversationModel>(conversation) |
| self.contactFoundConversation.value = newConversation |
| } |
| } |
| |
| func delete(conversationViewModel: ConversationViewModel) { |
| |
| if let index = self.conversationViewModels.index(where: ({ cvm in |
| cvm.conversation.value == conversationViewModel.conversation.value |
| })) { |
| |
| self.conversationsService |
| .clearHistory(conversation: conversationViewModel.conversation.value, |
| keepConversation: false) |
| self.conversationViewModels.remove(at: index) |
| } |
| } |
| |
| func clear(conversationViewModel: ConversationViewModel) { |
| |
| if let index = self.conversationViewModels.index(where: ({ cvm in |
| cvm.conversation.value == conversationViewModel.conversation.value |
| })) { |
| |
| self.conversationsService |
| .clearHistory(conversation: conversationViewModel.conversation.value, |
| keepConversation: true) |
| self.conversationViewModels.remove(at: index) |
| } |
| } |
| |
| func blockConversationsContact(conversationViewModel: ConversationViewModel) { |
| if let index = self.conversationViewModels.index(where: ({ cvm in |
| cvm.conversation.value == conversationViewModel.conversation.value |
| })) { |
| let contactRingId = conversationViewModel.conversation.value.recipientRingId |
| let accountId = conversationViewModel.conversation.value.accountId |
| let removeCompleted = self.contactsService.removeContact(withRingId: contactRingId, |
| ban: true, |
| withAccountId: accountId) |
| removeCompleted.asObservable() |
| .subscribe(onCompleted: { [weak self] in |
| self?.conversationsService |
| .clearHistory(conversation: conversationViewModel.conversation.value, |
| keepConversation: false) |
| self?.conversationViewModels.remove(at: index) |
| }).disposed(by: self.disposeBag) |
| } |
| } |
| |
| func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) { |
| self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: |
| conversationViewModel)) |
| } |
| |
| func showQRCode() { |
| self.stateSubject.onNext(ConversationState.qrCode()) |
| } |
| } |