Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VPN snooze mode #3184

Merged
merged 60 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
8f3c867
Handle the snooze connection status.
samsymons Jun 26, 2024
5bb79dc
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jun 27, 2024
68fab34
Add a snooze debug command.
samsymons Jun 27, 2024
6636459
Snooze 15 seconds instead of 60.
samsymons Jun 27, 2024
5ff8d1e
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 4, 2024
2f8908c
Update BSK branch.
samsymons Jul 4, 2024
171227e
More work on snooze.
samsymons Jul 4, 2024
4167d6f
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 7, 2024
ba0e8e2
Update BSK reference.
samsymons Jul 7, 2024
a2d45b7
Disable snooze UI actions when a request is pending.
samsymons Jul 7, 2024
03c20ec
Show the correct settings VPN cell label when snoozing.
samsymons Jul 9, 2024
8534617
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 9, 2024
54631b0
Show a timer when snooze is active.
samsymons Jul 9, 2024
4851c9d
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 19, 2024
55092c5
Update BSK reference and fix compilation.
samsymons Jul 19, 2024
a709239
Begin working on cleaning up after simplifying the publisher.
samsymons Jul 19, 2024
2bfed91
Send a notification when snoozing (albeit with incorrect copy).
samsymons Jul 20, 2024
a29a5fd
Suppress connection info when not connected.
samsymons Jul 20, 2024
2d10357
Add new VPN intents.
samsymons Jul 21, 2024
4a4aa1a
Wire up snooze to the widget.
samsymons Jul 21, 2024
19b9362
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 25, 2024
fa02b92
Update BSK reference.
samsymons Jul 25, 2024
76f7b6d
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Jul 28, 2024
1144225
Update BSK reference.
samsymons Jul 28, 2024
aeb78fb
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Aug 1, 2024
c046c3f
Update BSK reference.
samsymons Aug 1, 2024
791bde9
Update BSK reference.
samsymons Aug 1, 2024
1f13ae3
Fix test compilation and clean up intents.
samsymons Aug 1, 2024
9454da4
Test the Live Activity.
samsymons Jul 22, 2024
26b9657
Experiment with UI.
samsymons Jul 22, 2024
30f869c
Update UI of the Live Activity.
samsymons Jul 22, 2024
91e473d
Add lock screen Live Activity.
samsymons Jul 22, 2024
d138b39
Latest Live Activity changes.
samsymons Jul 22, 2024
f407fb3
Fix tint color.
samsymons Jul 22, 2024
3e978f7
WIP Live Activity changes.
samsymons Jul 25, 2024
fcca919
Further Live Activity refinement.
samsymons Jul 28, 2024
b863269
End the Live Activity if the app becomes active and the VPN is not sn…
samsymons Jul 29, 2024
fbb4c90
Undo widget change.
samsymons Jul 29, 2024
79c0117
Fix "Resume" button color.
samsymons Jul 29, 2024
6b9e09a
Remove singleton.
samsymons Aug 1, 2024
b94fe14
Fix target membership.
samsymons Aug 1, 2024
f8f55b1
Update copy.
samsymons Aug 1, 2024
ffdf570
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Aug 5, 2024
e736286
Update BSK reference.
samsymons Aug 5, 2024
e30147d
Move copy into UserText.
samsymons Aug 5, 2024
6454939
Remove print statement
samsymons Aug 7, 2024
12195f4
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Aug 7, 2024
f7833dd
Update BSK reference after merging with main.
samsymons Aug 7, 2024
43ce54d
Update fallback duration string.
samsymons Aug 7, 2024
5f252d4
Fix an issue where snooze gets out of sync when you toggle it too fast.
samsymons Aug 8, 2024
ed0b5cb
Fix thread sanitizer warnings.
samsymons Aug 8, 2024
bd21743
Add a VPN-specific animation hack.
samsymons Aug 8, 2024
f506cb7
Split up a conditional view to improve an animation.
samsymons Aug 8, 2024
a03b475
Fix up a text label issue.
samsymons Aug 8, 2024
8ceccce
Merge branch 'main' into sam/vpn-snooze-initial-support
samsymons Aug 12, 2024
4ca99a3
Update BSK reference.
samsymons Aug 12, 2024
1847907
Update comment.
samsymons Aug 12, 2024
691ed3a
Increase snooze to 20 minutes.
samsymons Aug 12, 2024
6629982
Add usage pixels.
samsymons Aug 12, 2024
c490fc1
Set BSK to 183.0.0.
samsymons Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/NetworkProtectionNotificationIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ public enum NetworkProtectionNotificationIdentifier: String {
case superseded = "network-protection.notification.superseded"
case test = "network-protection.notification.test"
case entitlement = "network-protection.notification.entitlement"
case snoozeEnded = "network-protection.notification.snooze-ended"
}
30 changes: 28 additions & 2 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */; };
4B0F3F502B9BFF2100392892 /* NetworkProtectionFAQView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F3F4F2B9BFF2100392892 /* NetworkProtectionFAQView.swift */; };
4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; };
4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */; };
4B37E0502B928CA6009E81CA /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B37E04F2B928CA6009E81CA /* vpn-light-mode.json */; };
4B412ACC2BBB3D0900A39F5E /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */; };
4B45D85C2BE0B115006061B5 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4B45D85B2BE0B115006061B5 /* Subscription */; };
Expand All @@ -229,6 +230,11 @@
4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */; };
4BCBE4602BA7E87100FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */; };
4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */; };
4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02025663298818B100E694E7 /* NetworkExtension.framework */; };
4BD96E0B2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */; };
4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */; };
4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */; };
4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */; };
4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */; };
4BE67B012B96B741007335F7 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B002B96B741007335F7 /* Common */; };
4BE67B032B96B864007335F7 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B022B96B864007335F7 /* ContentBlocking */; };
Expand Down Expand Up @@ -1400,6 +1406,9 @@
4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = "<group>"; };
4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeActivityAttributes.swift; sourceTree = "<group>"; };
4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeLiveActivityManager.swift; sourceTree = "<group>"; };
4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeLiveActivityWidget.swift; sourceTree = "<group>"; };
4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = "<group>"; };
4BF3E4AE2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = "<group>"; };
560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2845,6 +2854,7 @@
buildActionMask = 2147483647;
files = (
8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */,
4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */,
85DF714624F7FE6100C89288 /* Core.framework in Frameworks */,
8512EA4F24ED30D20073EE19 /* WidgetKit.framework in Frameworks */,
4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */,
Expand Down Expand Up @@ -3475,6 +3485,16 @@
name = VPN;
sourceTree = "<group>";
};
4BD96E072C4DCCD1003BC32C /* LiveActivity */ = {
isa = PBXGroup;
children = (
4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */,
4BD96E0A2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift */,
4BD96E0C2C4DCEAA003BC32C /* VPNSnoozeLiveActivityWidget.swift */,
);
name = LiveActivity;
sourceTree = "<group>";
};
566B736E2BECD3DC00FF1959 /* Utilities */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5054,6 +5074,7 @@
EECD94B22A28B8580085C66E /* NetworkProtection */ = {
isa = PBXGroup;
children = (
4BD96E072C4DCCD1003BC32C /* LiveActivity */,
4B37E04E2B928C91009E81CA /* Resources */,
EE01EB412AFC1DE10096AAC9 /* PreferredLocation */,
EE9D68CF2AE00CE000B55EF4 /* VPNSettings */,
Expand Down Expand Up @@ -6862,6 +6883,7 @@
98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */,
F1564F032B7B915F00D454A6 /* AppDelegate+SKAD4.swift in Sources */,
98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */,
4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */,
CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */,
85F200002215C17B006BB258 /* FindInPage.swift in Sources */,
F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */,
Expand Down Expand Up @@ -7022,6 +7044,7 @@
D62EC3C22C248AF800FC9D04 /* DuckNavigationHandling.swift in Sources */,
9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */,
D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */,
4BD96E0B2C4DCE55003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */,
1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */,
C1836CE12C359EC90016D057 /* AutofillBreakageReportCellContentView.swift in Sources */,
6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */,
Expand Down Expand Up @@ -7462,14 +7485,17 @@
buildActionMask = 2147483647;
files = (
853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */,
4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */,
373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */,
4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */,
853273B324FF114700E3C778 /* DeepLinks.swift in Sources */,
853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */,
853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */,
4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */,
4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */,
8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */,
85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */,
4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */,
8544C37C250B827300A0FE73 /* UserText.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -10284,8 +10310,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 180.0.0;
branch = "sam/vpn-snooze-initial-support";
kind = branch;
};
};
9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "92ecebfb4172ab9561959a07d7ef7037aea8c6e1",
"version" : "180.0.0"
"branch" : "sam/vpn-snooze-initial-support",
"revision" : "01f19a272916076de910ea0beabb00592beed175"
}
},
{
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ import WebKit
await stopAndRemoveVPNIfNotAuthenticated()
await refreshShortcuts()
await vpnWorkaround.installRedditSessionWorkaround()

if #available(iOS 17.0, *) {
await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary()
}
}

AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
<true/>
<key>SUBSCRIPTION_APP_GROUP</key>
<string>$(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP)</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations~ipad</key>
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private class DefaultTunnelSessionProvider: TunnelSessionProvider {
extension ConnectionStatusObserverThroughSession {
convenience init() {
self.init(tunnelSessionProvider: DefaultTunnelSessionProvider(),
platformSnoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults),
platformNotificationCenter: .default,
platformDidWakeNotification: UIApplication.didBecomeActiveNotification)
}
Expand Down
10 changes: 10 additions & 0 deletions DuckDuckGo/NetworkProtectionDebugUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ final class NetworkProtectionDebugUtilities {
}
try? await activeSession.sendProviderMessage(message)
}

// MARK: - Snooze

func startSnooze(duration: TimeInterval) async {
guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession() else {
return
}

try? await activeSession.sendProviderMessage(.startSnooze(duration))
}
}

private extension NetworkProtectionSimulationOption {
Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/NetworkProtectionDebugViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ final class NetworkProtectionDebugViewController: UITableViewController {
case shutDown
case showEntitlementMessaging
case resetEntitlementMessaging
case startSnooze
}

enum NetworkPathRows: Int, CaseIterable {
Expand Down Expand Up @@ -372,6 +373,8 @@ final class NetworkProtectionDebugViewController: UITableViewController {
cell.textLabel?.text = "Show Entitlement Messaging"
case .resetEntitlementMessaging:
cell.textLabel?.text = "Reset Entitlement Messaging"
case .startSnooze:
cell.textLabel?.text = "Snooze For 30 Seconds"
case .none:
break
}
Expand All @@ -391,6 +394,10 @@ final class NetworkProtectionDebugViewController: UITableViewController {
UserDefaults.networkProtectionGroupDefaults.enableEntitlementMessaging()
case .resetEntitlementMessaging:
UserDefaults.networkProtectionGroupDefaults.resetEntitlementMessaging()
case .startSnooze:
Task {
await NetworkProtectionDebugUtilities().startSnooze(duration: .seconds(30))
}
case .none:
break
}
Expand Down
32 changes: 28 additions & 4 deletions DuckDuckGo/NetworkProtectionStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct NetworkProtectionStatusView: View {
toggle()
locationDetails()

if statusModel.isNetPEnabled && statusModel.shouldShowConnectionDetails && statusModel.ipAddress != nil {
if statusModel.isNetPEnabled && statusModel.hasServerInfo && !statusModel.isSnoozing {
samsymons marked this conversation as resolved.
Show resolved Hide resolved
connectionDetails()
}

Expand All @@ -47,8 +47,8 @@ struct NetworkProtectionStatusView: View {
.padding(.top, statusModel.error == nil ? 0 : -20)
.if(statusModel.animationsOn, transform: {
$0
.animation(.default, value: statusModel.shouldShowConnectionDetails)
.animation(.default, value: statusModel.shouldShowError)
.animation(.easeOut, value: statusModel.hasServerInfo)
.animation(.easeOut, value: statusModel.shouldShowError)
})
.applyInsetGroupedListStyle()
}
Expand All @@ -70,6 +70,7 @@ struct NetworkProtectionStatusView: View {
.foregroundColor(.init(designSystemColor: .textSecondary))
}
}
.layoutPriority(1)

Toggle("", isOn: Binding(
get: { statusModel.isNetPEnabled },
Expand All @@ -83,6 +84,8 @@ struct NetworkProtectionStatusView: View {
.toggleStyle(SwitchToggleStyle(tint: .init(designSystemColor: .accent)))
}
.padding([.top, .bottom], 2)

snooze()
} header: {
header()
}
Expand Down Expand Up @@ -125,10 +128,31 @@ struct NetworkProtectionStatusView: View {
}
}

@ViewBuilder
samsymons marked this conversation as resolved.
Show resolved Hide resolved
private func snooze() -> some View {
if statusModel.isSnoozing {
Button(UserText.netPStatusViewWakeUp) {
Task {
await statusModel.cancelSnooze()
}
}
.foregroundStyle(Color(designSystemColor: .accent))
.disabled(statusModel.snoozeRequestPending)
} else if statusModel.hasServerInfo {
Button(UserText.netPStatusViewSnooze) {
Task {
await statusModel.startSnooze()
}
}
.foregroundStyle(Color(designSystemColor: .accent))
.disabled(statusModel.snoozeRequestPending)
}
}

@ViewBuilder
private func locationDetails() -> some View {
Section {
if let location = statusModel.location {
if !statusModel.isSnoozing, let location = statusModel.location {
var locationAttributedString: AttributedString {
var attributedString = AttributedString(
statusModel.preferredLocation.isNearest ? "\(location) \(UserText.netPVPNLocationNearest)" : location
Expand Down
Loading
Loading