Files

516 lines
21 KiB
Swift

//
// ServersPresenter.swift
// PrivadoVPN
//
// Created by Juraldinio on 3/11/21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
protocol ServersModuleOutput: AnyObject {
var serverRecords: [ServerRecord] { get }
var bestRecord: ServerRecord? { get }
var currentPlan: Customer.Plan { get }
var customerURL: String { get }
var order: ServerListRecord.Order { get }
func update(order: ServerListRecord.Order)
func navigate(to route: Route)
func serverSelected(record: ServerRecord)
}
final class ServersPresenter {
private enum Constants {
enum LocalizedString {
static let premiumTitle = "modal.premium.title"
static let premiumDescription = "modal.premium.description"
static let premiumButton = "modal.premium.button"
}
static let premiumUrlPath = "https://privadovpn.com/pricing/"
}
private weak var output: ServersModuleOutput?
weak var viewInput: ServersControllerInput?
weak var favouritesViewInput: FavouriteControllerInput?
weak var serverListViewInput: ServerListControllerInput?
private var preparedRecords: [ServerListRecord] {
guard let output = self.output else {
return []
}
return Self.prepare(using: output.serverRecords, plan: output.currentPlan, favoruties: self.favouriteRecords)
}
private var bestRecord: ServerRecord?
private var currentOrder: ServerListRecord.Order = .latencyASC
private var coordinator: CoreDataCoordinator?
private var favouriteRecords = [FavouriteModel]()
private var favouriteCities = [ServerListCityRecord]()
private var sortOptions: [SortPanelOrder] = [.latencyASC, .latencyDESC, .nameASC, .nameDESC]
private var records: [ServerListRecord] {
didSet {
self.markLastCities()
}
}
// MARK: - Init
init(output: ServersModuleOutput?, coordinator: CoreDataCoordinator?) {
self.output = output
self.coordinator = coordinator
guard let output = output else {
self.records = []
return
}
self.currentOrder = output.order
let favourites = Self.restoreFavourites(using: coordinator)
let records = Self.prepare(using: output.serverRecords, plan: output.currentPlan, favoruties: favourites)
self.records = Self.sort(list: records, plan: output.currentPlan, using: self.currentOrder)
self.bestRecord = self.output?.bestRecord
self.favouriteRecords = favourites
}
// MARK: - Static
private static func prepare(using serverRecords: [ServerRecord], plan: Customer.Plan, favoruties: [FavouriteModel]) -> [ServerListRecord] {
return serverRecords
.map { record -> ServerListCityRecord in
let isFavourite = favoruties.contains { $0.cityName == record.city && $0.isFreemium == record.isFreemium}
return ServerListCityRecord(serverRecord: record, isFavourite: isFavourite, plan: plan)
}.reduce([ServerListRecord]()) { acc, city in
var result = acc
if !city.country.isExist {
// Attention! We need sort countries only in freemium!
let countryRecord = acc.first(where: {
if let country = $0 as? ServerListCountryRecord
, country.code == city.countryCode
, (plan == .freemium ? country.isFreemium == city.isFreemium : true) { return true }
return false
})
// Country record already exists
if let country = countryRecord as? ServerListCountryRecord {
country.add(city: city)
city.country = country
} else {
let countryName = CountryResolver.name(from: city.countryCode)
// Attention! We need sort countries only in freemium!
let country = ServerListCountryRecord(country: countryName,
code: city.countryCode,
flag: city.countryCode,
isFreemium: plan == .freemium ? city.isFreemium : false, plan: city.plan)
country.add(city: city)
city.country = country
result.append(country)
}
}
return result
}
}
private static func sort(list: [ServerListRecord], plan: Customer.Plan, using order: ServerListRecord.Order) -> [ServerListRecord] {
var records = list
.filter { !$0.isNotification } // at first remove all notifications
.sorted(by: { lvalue, rvalue in // at top level we have Country only!
if plan == .freemium
, let lCountry = lvalue as? ServerListCountryRecord
, let rCountry = rvalue as? ServerListCountryRecord {
switch (lCountry.isFreemium, rCountry.isFreemium) {
case (true, true),
(false, false): return Self.areIncreasing(lValue: lvalue, rValue: rvalue, for: order)
case (true, false): return true
case (false, true): return false
}
}
return Self.areIncreasing(lValue: lvalue, rValue: rvalue, for: order)
})
records.forEach {
if let country = $0 as? ServerListCountryRecord {
country.sortCities { lCity, rCity in
Self.areIncreasing(lValue: lCity, rValue: rCity, for: order)
}
}
}
// Insert notification between freemium and not
if plan == .freemium
, let index = records.firstIndex(where: {
if let country = $0 as? ServerListCountryRecord
, !country.isFreemium {
return true
}
return false
}) {
let text = NSLocalizedString("serverlist.upgrade.text", comment: "")
let notification = ServerListNotificationRecord(notification: text)
records.insert(notification, at: index)
}
return records
}
private static func areIncreasing(lValue: ServerListRecord,
rValue: ServerListRecord,
for order: ServerListRecord.Order) -> Bool {
switch order {
case .default: return false
case .nameASC,
.nameDESC:
return Self.checkNames(areAscending: order == .nameASC, lValue: lValue, rValue: rValue)
case .latencyASC,
.latencyDESC:
return Self.checkLatencies(areAscending: order == .latencyASC, lValue: lValue, rValue: rValue)
}
}
private static func filter(list: [ServerListRecord], by searchQuery: String) -> [ServerListRecord] {
guard !searchQuery.isEmpty else { return list }
let query = searchQuery.lowercased()
let filtered = list.filter { record in
if let country = record as? ServerListCountryRecord {
let hasCity = country.cities.contains { $0.city.lowercased().contains(query) }
return country.country.lowercased().contains(query) || hasCity
} else if let city = record as? ServerListCityRecord {
return city.city.lowercased().contains(query)
} else {
return false
}
}
filtered.forEach { record in
if let country = record as? ServerListCountryRecord {
country.filterCities { $0.city.lowercased().contains(query) }
}
}
return filtered
}
private static func checkLatencies(areAscending: Bool,
lValue: ServerListRecord,
rValue: ServerListRecord) -> Bool {
if let lCountry = lValue as? ServerListCountryRecord
, let rCountry = rValue as? ServerListCountryRecord
, let lBestLatency = lCountry.getBestLatency(highest: areAscending)
, let rBestLatency = rCountry.getBestLatency(highest: areAscending) {
return areAscending ? lBestLatency > rBestLatency : lBestLatency < rBestLatency
} else if let lCity = lValue as? ServerListCityRecord
, let rCity = rValue as? ServerListCityRecord
, lCity.countryCode == rCity.countryCode {
return areAscending ? lCity.latency > rCity.latency : lCity.latency < rCity.latency
}
return false
}
private static func checkNames(areAscending: Bool,
lValue: ServerListRecord,
rValue: ServerListRecord) -> Bool {
if let lCountry = lValue as? ServerListCountryRecord
, let rCountry = rValue as? ServerListCountryRecord {
return areAscending ? lCountry.country < rCountry.country : lCountry.country > rCountry.country
} else if let lCity = lValue as? ServerListCityRecord
, let rCity = rValue as? ServerListCityRecord
, lCity.countryCode == rCity.countryCode {
return areAscending ? lCity.city < rCity.city : lCity.city > rCity.city
}
return false
}
private static func restoreFavourites(using coordinator: CoreDataCoordinator?) -> [FavouriteModel] {
guard let coordinator = coordinator else { return [] }
let fetch: NSFetchRequest<FavouriteRecord> = FavouriteRecord.fetchRequest()
let result = coordinator.mainContext.fetch(fetch)
return result.map { FavouriteModel(cityName: $0.city, isFreemium: $0.isFreeemium) }
}
private func markLastCities() {
self.records.forEach { record in
guard let country = record as? ServerListCountryRecord else { return }
country.cities.first { $0.isLast == true }?.isLast = false
country.cities.last?.isLast = true
}
}
private func saveFavourites() {
/// save favourites when leave screen in case user changed list multiple times
guard let coordinator = self.coordinator else { return }
let context = coordinator.createContext()
let request: NSFetchRequest<FavouriteRecord> = FavouriteRecord.fetchRequest()
context.fetch(request).forEach { context.delete($0) }
context.save()
coordinator.save()
self.favouriteRecords.forEach {
let record: FavouriteRecord = context.create()
record.city = $0.cityName
record.isFreeemium = $0.isFreemium
context.save()
}
coordinator.save()
}
}
// MARK: - ServersControllerOutput
extension ServersPresenter: ServersControllerOutput {
func serversViewIsReady() {
self.markLastCities()
self.serverListViewInput?.reloadList()
guard let bestRecord = self.bestRecord else { return }
let titleString = bestRecord.city + ", " + CountryResolver.name(from: bestRecord.countryCode)
self.viewInput?.bestLocationWith(title: titleString, flag: bestRecord.countryCode)
self.viewInput?.configureSortPanel(with: self.sortOptions)
}
func serverListChildrensCount(for record: ServerListRecord?) -> Int {
guard let record = record else {
return self.records.filter { $0.isCountry || $0.isNotification }.count
}
guard record.isCountry
, let countryRecord = record as? ServerListCountryRecord else {
return 0
}
return countryRecord.cities.count
}
func serverListChild(at index: Int, for record: ServerListRecord?) -> ServerListRecord {
guard let record = record else {
return self.records[safe: index] ?? ServerListRecord.empty()
}
guard let country = record as? ServerListCountryRecord
, let city = country.cities[safe: index] else {
return ServerListRecord.empty()
}
return city
}
func serverSelected(at row: Int) {
let cityRecord = self.favouriteCities[row]
self.serverListSelected(cityRecord, cityRecord.country)
}
func serverListSelected(_ record: ServerListRecord, _ country: ServerListCountryRecord?) {
guard let city = record as? ServerListCityRecord, let country = country else { return }
let plan = self.output?.currentPlan ?? .freemium
if plan == .freemium, !city.isFreemium {
CometLogger.shared.event(name: PrivadoConstants.Event.Statistic.Application.premiumServer,
attributes: [
PrivadoConstants.Event.Attributes.extradata: [
PrivadoConstants.Event.Attributes.city: city.city,
PrivadoConstants.Event.Attributes.country: country.country
]
],
secured: nil)
let settings: Route.ModalSettings = .closable(title: NSLocalizedString(Constants.LocalizedString.premiumTitle, comment: ""),
description: NSLocalizedString(Constants.LocalizedString.premiumDescription, comment: ""),
buttonTitle: NSLocalizedString(Constants.LocalizedString.premiumButton, comment: ""),
action: URL(string: Constants.premiumUrlPath))
let route: Route = .modal(settings: settings)
self.output?.navigate(to: route)
return
}
self.output?.serverSelected(record: city.serverRecord)
self.output?.navigate(to: .back)
}
func serverListSelectedFavourite(_ record: ServerListCityRecord) {
if let index = self.favouriteRecords.firstIndex(where: { $0.cityName == record.city && $0.isFreemium == record.isFreemium }) {
record.isFavourite = false
self.favouriteRecords.remove(at: index)
} else {
record.isFavourite = true
self.favouriteRecords.append(FavouriteModel(cityName: record.city, isFreemium: record.isFreemium))
}
self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite }
self.sortFavourites(by: self.currentOrder)
self.favouritesViewInput?.reloadList(searching: false)
}
func serverListSelectedFavourite(at row: Int) {
let city = self.favouriteCities[row]
self.favouriteRecords.removeAll { $0.cityName == city.city && $0.isFreemium == city.isFreemium }
self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.first { $0.city == city.city && $0.isFreemium == city.isFreemium }?.isFavourite = false
self.favouriteCities.remove(at: row)
self.favouritesViewInput?.reloadList(searching: false)
self.serverListViewInput?.reloadList()
}
func serverListWillDisappear() {
self.saveFavourites()
}
func favouriteViewIsReady() {
self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite }
self.sortFavourites(by: self.currentOrder)
self.favouritesViewInput?.reloadList(searching: false)
}
func favouriteRecord(at index: Int) -> ServerListCityRecord {
return self.favouriteCities[index]
}
func favouriteRecordsCount() -> Int {
self.favouriteCities.count
}
func upgradeButtonTapped() {
guard let output = self.output, let url = URL(string: output.customerURL) else { return }
openApplicationRoute(.navigate(url: url))
}
func bestLocationSelected() {
guard let bestRecord = self.output?.bestRecord else { return }
self.output?.serverSelected(record: bestRecord)
self.output?.navigate(to: .back)
}
}
// MARK: - SortPanelOutput
extension ServersPresenter: SearchPanelOutput {
func sortPanelSelected(at index: Int) {
guard let output = self.output else { return }
let order = self.sortOptions[index]
let listOrder: ServerListRecord.Order
switch order {
case .default, .nameASC: listOrder = .nameASC
case .nameDESC: listOrder = .nameDESC
case .latencyASC: listOrder = .latencyASC
case .latencyDESC: listOrder = .latencyDESC
}
self.records = Self.sort(list: self.records, plan: output.currentPlan, using: listOrder)
self.serverListViewInput?.reloadList()
self.sortFavourites(by: listOrder)
self.favouritesViewInput?.reloadList(searching: false)
self.currentOrder = listOrder
}
func sortFavourites(by listOrder: ServerListRecord.Order) {
self.favouriteCities = self.favouriteCities.sorted(by: { lCity, rCity in
switch listOrder {
case .nameASC:
return lCity.city < rCity.city
case .nameDESC:
return lCity.city > rCity.city
case .latencyASC:
return lCity.latency > rCity.latency
case .latencyDESC:
return lCity.latency < rCity.latency
default:
return lCity.latency > rCity.latency
}
})
}
func sortPanelDidSelect(order: SortPanelOrder) {
guard let output = self.output else { return }
let listOrder: ServerListRecord.Order
switch order {
case .default, .nameASC: listOrder = .nameASC
case .nameDESC: listOrder = .nameDESC
case .latencyASC: listOrder = .latencyASC
case .latencyDESC: listOrder = .latencyDESC
}
self.records = Self.sort(list: self.records, plan: output.currentPlan, using: listOrder)
self.serverListViewInput?.reloadList()
self.sortFavourites(by: listOrder)
self.favouritesViewInput?.reloadList(searching: false)
self.currentOrder = listOrder
}
func sortPanelSearch(query: String) {
guard let output = self.output else { return }
var records = [ServerListRecord]()
Self.filter(list: self.preparedRecords, by: query)
.forEach { record in
guard let country = record as? ServerListCountryRecord else {
records.append(record)
return
}
guard country.cities.isEmpty else {
records.append(record)
return
}
let countries = self.preparedRecords
.filter { $0.isCountry }
.filter {
guard let value = $0 as? ServerListCountryRecord else { return false }
return value == country
}
records.append(contentsOf: countries)
}
self.records = Self.sort(list: records,
plan: output.currentPlan,
using: self.currentOrder)
self.serverListViewInput?.reloadList()
self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite }
self.sortFavourites(by: self.currentOrder)
self.favouritesViewInput?.reloadList(searching: true)
}
func sortPanelSearchCancel() {
guard let output = self.output else { return }
self.records = Self.sort(list: self.preparedRecords, plan: output.currentPlan, using: self.currentOrder)
self.serverListViewInput?.reloadList()
self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite }
self.sortFavourites(by: self.currentOrder)
self.favouritesViewInput?.reloadList(searching: true)
self.viewInput?.removeSortPanel()
}
}