516 lines
21 KiB
Swift
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()
|
|
}
|
|
}
|