blob: 4b83da31831afd935fbd818f00b5646e994eff86 [file] [log] [blame]
/*
* Copyright (C) 2017-2019 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@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 Foundation
import RxSwift
import RxCocoa
import os
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable type_body_length
/// This Coordinator drives the conversation navigation (Smartlist / Conversation detail)
class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNavigation {
var presentingVC = [String: Bool]()
var rootViewController: UIViewController {
return self.navigationViewController
}
var childCoordinators = [Coordinator]()
var parentCoordinator: Coordinator?
var smartListViewController = UIViewController()
private var navigationViewController = UINavigationController()
let injectionBag: InjectionBag
let disposeBag = DisposeBag()
let stateSubject = PublishSubject<State>()
let callService: CallsService
let accountService: AccountsService
let conversationService: ConversationsService
let callsProvider: CallsProviderService
let nameService: NameService
let requestsService: RequestsService
required init (with injectionBag: InjectionBag) {
self.injectionBag = injectionBag
self.callService = injectionBag.callService
self.accountService = injectionBag.accountService
self.nameService = injectionBag.nameService
self.conversationService = injectionBag.conversationsService
self.callsProvider = injectionBag.callsProvider
self.requestsService = injectionBag.requestsService
self.addLockFlags()
self.stateSubject
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] (state) in
guard let self = self, let state = state as? ConversationState else { return }
switch state {
case .createNewAccount:
self.createNewAccount()
case .showDialpad(let inCall):
self.showDialpad(inCall: inCall)
case .showGeneralSettings:
self.showGeneralSettings()
case .openAboutJami:
self.openAboutJami()
case .navigateToCall(let call):
self.navigateToCall(call: call)
case .showContactPicker(let callID, let contactCallBack, let conversationCallBack):
self.showContactPicker(callId: callID, contactSelectedCB: contactCallBack, conversationSelectedCB: conversationCallBack)
case .showAccountSettings:
self.showAccountSettings()
case .accountRemoved:
self.popToSmartList()
case .needToOnboard:
self.needToOnboard()
case .accountModeChanged:
self.accountModeChanged()
case .migrateAccount(let accountId):
self.migrateAccount(accountId: accountId)
case .returnToSmartList:
self.popToSmartList()
case .openConversation(let jamiId):
self.openConversation(jamiId: jamiId)
case .openConversationForConversationId(let conversationId, let accountId, let shouldOpenSmarList):
self.openConversation(conversationId: conversationId, accountId: accountId, shouldOpenSmarList: shouldOpenSmarList)
case .openConversationFromCall(let conversation):
self.openConversationFromCall(conversationModel: conversation)
default:
break
}
})
.disposed(by: self.disposeBag)
self.callService.newCall
.asObservable()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { (call) in
self.showIncomingCall(call: call)
})
.disposed(by: self.disposeBag)
self.callbackPlaceCall()
}
func needToOnboard() {
if let parent = self.parentCoordinator as? AppCoordinator {
self.navigationViewController.popViewController(animated: false)
parent.stateSubject.onNext(AppState.needToOnboard(animated: false, isFirstAccount: true))
}
}
func accountModeChanged() {
self.start()
}
func migrateAccount(accountId: String) {
if let parent = self.parentCoordinator as? AppCoordinator {
parent.stateSubject.onNext(AppState.needAccountMigration(accountId: accountId))
}
}
func showAccountSettings() {
let meCoordinator = MeCoordinator(with: self.injectionBag)
meCoordinator.parentCoordinator = self
meCoordinator.setNavigationController(controller: self.navigationViewController)
self.addChildCoordinator(childCoordinator: meCoordinator)
meCoordinator.start()
self.smartListViewController.rx.viewWillAppear
.take(1)
.subscribe(onNext: { [weak self, weak meCoordinator] (_) in
self?.removeChildCoordinator(childCoordinator: meCoordinator)
})
.disposed(by: self.disposeBag)
}
func showIncomingCall(call: CallModel) {
guard let account = self.accountService
.getAccount(fromAccountId: call.accountId),
!call.callId.isEmpty else {
return
}
if self.accountService.boothMode() {
self.callService.refuse(callId: call.callId)
.subscribe()
.disposed(by: self.disposeBag)
return
}
callsProvider.sharedResponseStream
.filter({ [weak call] serviceEvent in
guard serviceEvent.eventType == .callProviderAnswerCall ||
serviceEvent.eventType == .callProviderCancelCall else {
return false
}
guard let callUUID: String = serviceEvent
.getEventInput(ServiceEventInput.callUUID) else {
return false
}
return callUUID == call?.callUUID.uuidString
})
.take(1)
.subscribe(onNext: { [weak self, weak call] serviceEvent in
guard let self = self,
let call = call else { return }
if serviceEvent.eventType == ServiceEventType.callProviderAnswerCall {
self.presentCallScreen(call: call)
}
})
.disposed(by: self.disposeBag)
callsProvider.handleIncomingCall(account: account, call: call)
guard call.getDisplayName() == call.paricipantHash() else { return }
self.nameService.usernameLookupStatus
.filter({ [weak call] lookupNameResponse in
return lookupNameResponse.address != nil &&
lookupNameResponse.address == call?.paricipantHash()
})
.asObservable()
.take(1)
.subscribe(onNext: { [weak call] lookupNameResponse in
// if we have a registered name then we should update the value for it
if let name = lookupNameResponse.name, !name.isEmpty, let call = call {
call.registeredName = name
self.callsProvider.updateRegisteredName(account: account, call: call)
}
})
.disposed(by: self.disposeBag)
self.nameService.lookupAddress(withAccount: account.id, nameserver: "", address: call.participantUri.filterOutHost())
}
func openConversation(jamiId: String) {
guard let account = self.accountService.currentAccount else { return }
let uri = JamiURI(schema: URIType.ring, infoHash: jamiId)
if let conversation = self.getConversationViewModelForParticipant(jamiId: jamiId) {
self.showConversation(withConversationViewModel: conversation)
return
}
let conversation = ConversationModel(withParticipantUri: uri,
accountId: account.id)
let newConversation = ConversationViewModel(with: self.injectionBag)
newConversation.conversation = conversation
self.showConversation(withConversationViewModel: newConversation)
}
func presentCallScreen(call: CallModel) {
if let topController = self.getTopController(),
!topController.isKind(of: (CallViewController).self) {
topController.dismiss(animated: false, completion: nil)
}
self.popToSmartList()
if self.accountService.currentAccount?.id != call.accountId {
self.accountService.currentAccount = self.accountService.getAccount(fromAccountId: call.accountId)
}
if let model = self.getConversationViewModelForParticipant(jamiId: call.paricipantHash()) {
self.showConversation(withConversationViewModel: model)
}
let controller = CallViewController.instantiate(with: self.injectionBag)
controller.viewModel.call = call
self.present(viewController: controller,
withStyle: .appear,
withAnimation: false,
withStateable: controller.viewModel)
}
func createNewAccount() {
if let parent = self.parentCoordinator as? AppCoordinator {
parent.stateSubject.onNext(AppState.addAccount)
}
}
func showDialpad(inCall: Bool) {
let dialpadViewController = DialpadViewController.instantiate(with: self.injectionBag)
dialpadViewController.viewModel.inCallDialpad = inCall
if !inCall {
self.present(viewController: dialpadViewController,
withStyle: .present,
withAnimation: true,
withStateable: dialpadViewController.viewModel)
return
}
if let controller = self.navigationViewController.visibleViewController as? CallViewController {
controller.present(dialpadViewController, animated: true, completion: nil)
}
}
func showContactPicker(callId: String, contactSelectedCB: ((_ contact: [ConferencableItem]) -> Void)? = nil, conversationSelectedCB: ((_ conversationIds: [String]) -> Void)? = nil) {
let contactPickerViewController = ContactPickerViewController.instantiate(with: self.injectionBag)
contactPickerViewController.type = callId.isEmpty ? .forConversation : .forCall
contactPickerViewController.viewModel.currentCallId = callId
contactPickerViewController.viewModel.contactSelectedCB = contactSelectedCB
contactPickerViewController.viewModel.conversationSelectedCB = conversationSelectedCB
if let controller = self.navigationViewController.visibleViewController as? ContactPickerDelegate {
controller.presentContactPicker(contactPickerVC: contactPickerViewController)
}
}
func openAboutJami() {
let aboutJamiController = AboutViewController.instantiate()
self.present(viewController: aboutJamiController, withStyle: .show, withAnimation: true, disposeBag: self.disposeBag)
}
func showGeneralSettings() {
let generalSettingsCoordinator = GeneralSettingsCoordinator(with: self.injectionBag)
generalSettingsCoordinator.parentCoordinator = self
generalSettingsCoordinator.setNavigationController(controller: self.navigationViewController)
self.addChildCoordinator(childCoordinator: generalSettingsCoordinator)
generalSettingsCoordinator.start()
self.smartListViewController.rx.viewWillAppear
.take(1)
.subscribe(onNext: { [weak self, weak generalSettingsCoordinator] (_) in
self?.removeChildCoordinator(childCoordinator: generalSettingsCoordinator)
})
.disposed(by: self.disposeBag)
}
func popToSmartList() {
let viewControllers = navigationViewController.viewControllers
if viewControllers.contains(smartListViewController) {
navigationViewController.popToViewController(smartListViewController, animated: false)
}
}
func openConversationFromCall(conversationModel: ConversationModel) {
guard let navigationController = self.rootViewController as? UINavigationController else { return }
let controllers = navigationController.children
for controller in controllers
where controller.isKind(of: (ConversationViewController).self) {
if let conversationController = controller as? ConversationViewController, conversationController.viewModel.conversation == conversationModel {
navigationController.popToViewController(conversationController, animated: true)
conversationController.becomeFirstResponder()
return
}
}
self.openConversation(conversationId: conversationModel.id, accountId: conversationModel.accountId, shouldOpenSmarList: true)
}
func openConversation(conversationId: String, accountId: String, shouldOpenSmarList: Bool) {
if shouldOpenSmarList {
popToSmartList()
}
if let model = getConversationViewModelForId(conversationId: conversationId) {
self.showConversation(withConversationViewModel: model)
}
if !shouldOpenSmarList {
let viewControllers = navigationViewController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 is SwarmCreationViewController }) {
navigationViewController.viewControllers.remove(at: index)
}
}
}
func pushConversation(participantId: String) {
self.popToSmartList()
guard let account = accountService.currentAccount else {
return
}
guard let uriString = JamiURI(schema: URIType.ring, infoHash: participantId).uriString else {
return
}
if let model = getConversationViewModelForParticipant(jamiId: uriString) {
self.showConversation(withConversationViewModel: model)
return
}
guard let conversation = self.conversationService.getConversationForParticipant(jamiId: participantId, accontId: account.id) else {
return
}
let conversationViewModel = ConversationViewModel(with: self.injectionBag)
conversationViewModel.conversation = conversation
self.showConversation(withConversationViewModel: conversationViewModel)
}
func start() {
let boothMode = self.accountService.boothMode()
if boothMode {
let smartViewController = IncognitoSmartListViewController.instantiate(with: self.injectionBag)
self.present(viewController: smartViewController, withStyle: .show, withAnimation: true, withStateable: smartViewController.viewModel)
smartListViewController = smartViewController
return
}
// let smartViewController = SwarmCreationViewController.instantiate(with: self.injectionBag)
let smartViewController = SmartlistViewController.instantiate(with: self.injectionBag)
self.present(viewController: smartViewController, withStyle: .show, withAnimation: true, withStateable: smartViewController.viewModel)
smartListViewController = smartViewController
}
func setNavigationController(controller: UINavigationController) {
navigationViewController = controller
}
func getConversationViewModelForParticipant(jamiId: String) -> ConversationViewModel? {
let viewControllers = self.navigationViewController.children
for controller in viewControllers {
if let smartController = controller as? SmartlistViewController {
for model in smartController.viewModel.conversationsModel.conversationViewModels where
model.conversation.isCoredialog() && model.conversation.getParticipants().first?.jamiId == jamiId {
return model
}
}
}
return nil
}
func getConversationViewModelForId(conversationId: String) -> ConversationViewModel? {
let viewControllers = self.navigationViewController.children
for controller in viewControllers {
if let smartController = controller as? SmartlistViewController {
for model in smartController.viewModel.conversationsModel.conversationViewModels where
model.conversation.id == conversationId {
return model
}
}
}
return nil
}
func addLockFlags() {
presentingVC[VCType.contact.rawValue] = false
presentingVC[VCType.conversation.rawValue] = false
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable type_body_length