Skip to content
This repository has been archived by the owner on Mar 6, 2024. It is now read-only.

Commit

Permalink
Closes #1151: Add search suggestions (#1581)
Browse files Browse the repository at this point in the history
Closes #1151: Add search suggestions
  • Loading branch information
sblatz authored Nov 16, 2018
1 parent 30c2272 commit a331a77
Show file tree
Hide file tree
Showing 17 changed files with 877 additions and 202 deletions.
26 changes: 24 additions & 2 deletions Blockzilla.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
1DD317E120BF1B3000DFA44E /* OnboardingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD317E020BF1B3000DFA44E /* OnboardingTest.swift */; };
1DE6DA2220BF410400CE337B /* QuickAddAutocompleteURLTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE6DA2120BF410400CE337B /* QuickAddAutocompleteURLTest.swift */; };
1DE903CE20C751D7002E53ED /* FindInPageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE903CD20C751D7002E53ED /* FindInPageTest.swift */; };
24433101219DA46F00778D02 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24433100219DA46F00778D02 /* Debouncer.swift */; };
2696EB04211F540600F0C73F /* SearchHistoryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696EB03211F540600F0C73F /* SearchHistoryUtils.swift */; };
2825C3665AB14081B262F767 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D051AC1857A0EEEBB833D15 /* SystemConfiguration.framework */; };
2B36326E97D2E67E9C684B4B /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80D9AA926EFBD788739486EB /* CoreTelephony.framework */; };
2F2F84BCF076B21DE42D1F29 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C652A61E213D254A86DF5736 /* CoreMedia.framework */; };
30DFF98021810BF20055707C /* SearchSuggestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DFF97F21810BF20055707C /* SearchSuggestClient.swift */; };
31421EB12176492A0015F48B /* TitleActivityItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31421EB02176492A0015F48B /* TitleActivityItemProvider.swift */; };
34056D10515852F370C83A01 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAB8E622E994545108473554 /* QuartzCore.framework */; };
4285B8E0671F40DA543A2E58 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B90C056305836CB297FF79 /* AssetsLibrary.framework */; };
Expand Down Expand Up @@ -79,6 +81,7 @@
B3D23BEF1FA3A9E500D9C50F /* postload.js in Resources */ = {isa = PBXBuildFile; fileRef = B3D23BEE1FA3A9E500D9C50F /* postload.js */; };
B3F4A3171FA136E70029A6F2 /* preload.js in Resources */ = {isa = PBXBuildFile; fileRef = B3F4A3161FA136E60029A6F2 /* preload.js */; };
D00A44111EB8F80A00DB0218 /* Telemetry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D00A44101EB8F80A00DB0218 /* Telemetry.framework */; };
D025F225218B64D600B262D8 /* SearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025F224218B64D600B262D8 /* SearchEngineTests.swift */; };
D0967A811EC50002009D937F /* TelemetryIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0967A801EC50002009D937F /* TelemetryIntegration.swift */; };
D30179071BC6CB19009AD388 /* BlockerEnabledDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30179061BC6CB19009AD388 /* BlockerEnabledDetector.swift */; };
D30179C31BCC6F65009AD388 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30179C21BCC6F65009AD388 /* AboutViewController.swift */; };
Expand Down Expand Up @@ -181,6 +184,9 @@
EB84F96F209380CE00BA6739 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB84F96E209380CE00BA6739 /* URLExtensions.swift */; };
EB84F9712093815500BA6739 /* effective_tld_names.dat in Resources */ = {isa = PBXBuildFile; fileRef = EB84F9702093815500BA6739 /* effective_tld_names.dat */; };
EBE44F4E20ADDF6A005AFEA6 /* SmartLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBE44F4D20ADDF6A005AFEA6 /* SmartLabel.swift */; };
F74E8109218183F400D18535 /* SearchSuggestionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74E8108218183F400D18535 /* SearchSuggestionsTest.swift */; };
F7B3E49D2165C32B00118785 /* SearchSuggestionsPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B3E49C2165C32B00118785 /* SearchSuggestionsPromptView.swift */; };
F7FF8B5B2194084000CCA80F /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FF8B5A2194084000CCA80F /* UIDeviceExtensions.swift */; };
F805722F1DBEE504004339C1 /* WebCacheUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F805722E1DBEE504004339C1 /* WebCacheUtils.swift */; };
F84AFE751DE77FE6005C4DD1 /* LaunchScreen.png in Resources */ = {isa = PBXBuildFile; fileRef = F84AFE721DE77FE6005C4DD1 /* LaunchScreen.png */; };
F84AFE761DE77FE6005C4DD1 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = F84AFE731DE77FE6005C4DD1 /* [email protected] */; };
Expand Down Expand Up @@ -371,8 +377,10 @@
1DD317E020BF1B3000DFA44E /* OnboardingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTest.swift; sourceTree = "<group>"; };
1DE6DA2120BF410400CE337B /* QuickAddAutocompleteURLTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickAddAutocompleteURLTest.swift; sourceTree = "<group>"; };
1DE903CD20C751D7002E53ED /* FindInPageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInPageTest.swift; sourceTree = "<group>"; };
24433100219DA46F00778D02 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
2696EB03211F540600F0C73F /* SearchHistoryUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUtils.swift; sourceTree = "<group>"; };
29B90C056305836CB297FF79 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; };
30DFF97F21810BF20055707C /* SearchSuggestClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestClient.swift; sourceTree = "<group>"; };
31421EB02176492A0015F48B /* TitleActivityItemProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleActivityItemProvider.swift; sourceTree = "<group>"; };
427B752EF11959F9C38B12D6 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
4F1284851FC5E242001A775B /* TPSettingsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TPSettingsTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -566,6 +574,7 @@
D00A440E1EB8F40A00DB0218 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
D00A440F1EB8F40A00DB0218 /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = "<group>"; };
D00A44101EB8F80A00DB0218 /* Telemetry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Telemetry.framework; path = Carthage/Build/iOS/Telemetry.framework; sourceTree = "<group>"; };
D025F224218B64D600B262D8 /* SearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineTests.swift; sourceTree = "<group>"; };
D0967A801EC50002009D937F /* TelemetryIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetryIntegration.swift; sourceTree = "<group>"; };
D30179061BC6CB19009AD388 /* BlockerEnabledDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockerEnabledDetector.swift; sourceTree = "<group>"; };
D30179C21BCC6F65009AD388 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -909,6 +918,9 @@
EB84F9702093815500BA6739 /* effective_tld_names.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = effective_tld_names.dat; sourceTree = "<group>"; };
EBE44F4D20ADDF6A005AFEA6 /* SmartLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartLabel.swift; sourceTree = "<group>"; };
F4C4943B406FCA9B74B4E186 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
F74E8108218183F400D18535 /* SearchSuggestionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestionsTest.swift; sourceTree = "<group>"; };
F7B3E49C2165C32B00118785 /* SearchSuggestionsPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestionsPromptView.swift; sourceTree = "<group>"; };
F7FF8B5A2194084000CCA80F /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = "<group>"; };
F805722E1DBEE504004339C1 /* WebCacheUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebCacheUtils.swift; sourceTree = "<group>"; };
F84AFE721DE77FE6005C4DD1 /* LaunchScreen.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchScreen.png; sourceTree = "<group>"; };
F84AFE731DE77FE6005C4DD1 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1012,6 +1024,7 @@
4FE4E6F51FBB5E2C001BB779 /* TPSidebarBadge.swift */,
4F1284851FC5E242001A775B /* TPSettingsTest.swift */,
166E4BFA212F7DEC0029E2A5 /* UserAgentTest.swift */,
F74E8108218183F400D18535 /* SearchSuggestionsTest.swift */,
);
path = XCUITest;
sourceTree = "<group>";
Expand All @@ -1037,7 +1050,6 @@
745DC5DB1F39221100661635 /* OpenInFocus */ = {
isa = PBXGroup;
children = (
16938C0321010C2D00DCD489 /* InfoPlist.strings */,
745DC5DC1F39221100661635 /* ActionViewController.swift */,
745DC5E11F39221100661635 /* Info.plist */,
742C99D31F3A3AD200717D69 /* Assets.xcassets */,
Expand Down Expand Up @@ -1194,6 +1206,7 @@
D8E0155D1FCF409F00CA3B9F /* ClientTests */ = {
isa = PBXGroup;
children = (
D025F224218B64D600B262D8 /* SearchEngineTests.swift */,
D8E0155E1FCF409F00CA3B9F /* SearchEngineManagerTests.swift */,
D8E015601FCF409F00CA3B9F /* Info.plist */,
D831FEE1205247A400EAE19A /* BrowserViewControllerTests.swift */,
Expand Down Expand Up @@ -1258,6 +1271,7 @@
E4BF2DD51BACE8CA00DA9D68 /* Blockzilla */ = {
isa = PBXGroup;
children = (
30DFF97F21810BF20055707C /* SearchSuggestClient.swift */,
EB84F9702093815500BA6739 /* effective_tld_names.dat */,
EB84F96C2093799800BA6739 /* TrackingProtectionPageStats.swift */,
D50939A81FBF807A005D4316 /* Settings.bundle */,
Expand Down Expand Up @@ -1304,6 +1318,7 @@
D3E54FCE1DEFAE80003E1AFF /* OpenSearchParser.swift */,
D36C1BA91DB02EBB0073C1AB /* OpenUtils.swift */,
D3426AEA1DB7F8AA0016DA5A /* OverlayView.swift */,
F7B3E49C2165C32B00118785 /* SearchSuggestionsPromptView.swift */,
16D716A4211503CD000C8A66 /* PageActionSheetItems.swift */,
16074B6C2114364C003B671F /* PhotonActionSheet.swift */,
D3E251E81DAD714C005918DC /* SafariInstructionsViewController.swift */,
Expand Down Expand Up @@ -1343,6 +1358,8 @@
746BD9271F75C45A00BE8DB9 /* DictionaryExtensions.swift */,
D8E0152B1FB4136E00CA3B9F /* AddSearchEngineViewController.swift */,
2696EB03211F540600F0C73F /* SearchHistoryUtils.swift */,
F7FF8B5A2194084000CCA80F /* UIDeviceExtensions.swift */,
24433100219DA46F00778D02 /* Debouncer.swift */,
);
path = Blockzilla;
sourceTree = "<group>";
Expand Down Expand Up @@ -1678,7 +1695,6 @@
buildActionMask = 2147483647;
files = (
742C99D41F3A3AD200717D69 /* Assets.xcassets in Resources */,
16938C0121010C2D00DCD489 /* InfoPlist.strings in Resources */,
A89766DA1F57DCA9008183C5 /* (null) in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1824,6 +1840,7 @@
4F1284861FC5E242001A775B /* TPSettingsTest.swift in Sources */,
4FE4E6F61FBB5E2C001BB779 /* TPSidebarBadge.swift in Sources */,
1DE6DA2220BF410400CE337B /* QuickAddAutocompleteURLTest.swift in Sources */,
F74E8109218183F400D18535 /* SearchSuggestionsTest.swift in Sources */,
0BA39A861DD2B8E4005F970A /* WebsiteAccessTest.swift in Sources */,
0B0D6BC41F3CDDBB00497D08 /* CollapsedURLTest.swift in Sources */,
);
Expand Down Expand Up @@ -1858,6 +1875,7 @@
D8E0155F1FCF409F00CA3B9F /* SearchEngineManagerTests.swift in Sources */,
050E8B1E2064FECE00DF6090 /* StringExtensionTest.swift in Sources */,
D8E0156E1FD9E40F00CA3B9F /* DomainCompletionTests.swift in Sources */,
D025F225218B64D600B262D8 /* SearchEngineTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1905,9 +1923,12 @@
E40AFB101DC9014700DA5651 /* UserAgent.swift in Sources */,
EB84F96D2093799900BA6739 /* TrackingProtectionPageStats.swift in Sources */,
74584E351FA1077000AF2582 /* SettingsContentViewController.swift in Sources */,
24433101219DA46F00778D02 /* Debouncer.swift in Sources */,
7460BD671F58B1B10096B745 /* GradientProgressBar.swift in Sources */,
D3E54FE01DF0E221003E1AFF /* SearchSettingsViewController.swift in Sources */,
30DFF98021810BF20055707C /* SearchSuggestClient.swift in Sources */,
16D7169C2114EF76000C8A66 /* BlockerToggle.swift in Sources */,
F7B3E49D2165C32B00118785 /* SearchSuggestionsPromptView.swift in Sources */,
D33E9B241E1D93DD00A39A44 /* RequestHandler.swift in Sources */,
D3426AF61DB84E7A0016DA5A /* SearchEngine.swift in Sources */,
746BD9261F75C43C00BE8DB9 /* DataExtensions.swift in Sources */,
Expand All @@ -1919,6 +1940,7 @@
D37DE55D1BCDBD6100906364 /* WaveView.swift in Sources */,
E4BF2DD71BACE8CA00DA9D68 /* AppDelegate.swift in Sources */,
D3E2C9691DA3024800DEBE3D /* InsetButton.swift in Sources */,
F7FF8B5B2194084000CCA80F /* UIDeviceExtensions.swift in Sources */,
F805722F1DBEE504004339C1 /* WebCacheUtils.swift in Sources */,
1D3BEDFB20C5E76B0019B722 /* FindInPageBar.swift in Sources */,
D30DF93E1DC1634F0064736C /* Toast.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Blockzilla/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ModalDelegate, AppSplashC
if let bundleID = Bundle.main.bundleIdentifier {
UserDefaults.standard.removePersistentDomain(forName: bundleID)
}
UserDefaults.standard.removePersistentDomain(forName: AppInfo.sharedContainerIdentifier)
}
setupContinuousDeploymentTooling()
setupErrorTracking()
Expand Down Expand Up @@ -364,6 +365,7 @@ extension AppDelegate {
telemetryConfig.measureUserDefaultsSetting(forKey: SettingsToggle.blockOther, withDefaultValue: Settings.getToggle(.blockOther))
telemetryConfig.measureUserDefaultsSetting(forKey: SettingsToggle.blockFonts, withDefaultValue: Settings.getToggle(.blockFonts))
telemetryConfig.measureUserDefaultsSetting(forKey: SettingsToggle.biometricLogin, withDefaultValue: Settings.getToggle(.biometricLogin))
telemetryConfig.measureUserDefaultsSetting(forKey: SettingsToggle.enableSearchSuggestions, withDefaultValue: Settings.getToggle(.enableSearchSuggestions))

#if DEBUG
telemetryConfig.updateChannel = "debug"
Expand Down
45 changes: 39 additions & 6 deletions Blockzilla/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class BrowserViewController: UIViewController {
fileprivate var urlBar: URLBar!
fileprivate var topURLBarConstraints = [Constraint]()
fileprivate let requestHandler = RequestHandler()
fileprivate let searchSuggestClient = SearchSuggestClient()
fileprivate var findInPageBar: FindInPageBar?
fileprivate var fillerView: UIView?
fileprivate let alertStackView = UIStackView() // All content that appears above the footer should be added to this view. (Find In Page/SnackBars)
Expand Down Expand Up @@ -69,6 +70,7 @@ class BrowserViewController: UIViewController {
}
}

private let searchSuggestionsDebouncer = Debouncer(timeInterval: 0.1)
private var shouldEnsureBrowsingMode = false
private var initialUrl: URL?
var tipManager: TipManager?
Expand Down Expand Up @@ -128,6 +130,7 @@ class BrowserViewController: UIViewController {
overlayView.alpha = 0
overlayView.delegate = self
overlayView.backgroundColor = UIConstants.colors.overlayBackground
overlayView.setSearchSuggestionsPromptViewDelegate(delegate: self)
mainContainerView.addSubview(overlayView)

background.snp.makeConstraints { make in
Expand Down Expand Up @@ -781,9 +784,29 @@ extension BrowserViewController: URLBarDelegate {
}

func urlBar(_ urlBar: URLBar, didEnterText text: String) {
// Hide find in page if the home view is displayed
let isOnHomeView = homeView != nil
overlayView.setSearchQuery(query: text, animated: true, hideFindInPage: isOnHomeView)
let trimmedText = text.trimmingCharacters(in: .whitespaces)
let isOnHomeView = homeView != nil

if Settings.getToggle(.enableSearchSuggestions) && !trimmedText.isEmpty {
searchSuggestionsDebouncer.renewInterval()
searchSuggestionsDebouncer.completion = {
self.searchSuggestClient.getSuggestions(trimmedText, callback: { suggestions, error in
let userInputText = urlBar.userInputText?.trimmingCharacters(in: .whitespaces) ?? ""

// Check if this callback is stale (new user input has been requested)
if userInputText.isEmpty || userInputText != trimmedText {
return
}

if userInputText == trimmedText {
let suggestions = suggestions ?? [trimmedText]
self.overlayView.setSearchQuery(suggestions: suggestions, hideFindInPage: isOnHomeView || text.isEmpty)
}
})
}
} else {
overlayView.setSearchQuery(suggestions: [trimmedText], hideFindInPage: isOnHomeView || text.isEmpty)
}
}

func urlBarDidPressScrollTop(_: URLBar, tap: UITapGestureRecognizer) {
Expand Down Expand Up @@ -1085,11 +1108,10 @@ extension BrowserViewController: OverlayViewDelegate {
}

func overlayView(_ overlayView: OverlayView, didSearchForQuery query: String) {
if let url = searchEngineManager.activeEngine.urlForQuery(query) {
if searchEngineManager.activeEngine.urlForQuery(query) != nil {
Telemetry.default.recordEvent(category: TelemetryEventCategory.action, method: TelemetryEventMethod.selectQuery, object: TelemetryEventObject.searchBar)
Telemetry.default.recordSearch(location: .actionBar, searchEngine: searchEngineManager.activeEngine.getNameOrCustom())
submit(url: url)
urlBar.url = url
urlBar(urlBar, didSubmitText: query)
}

urlBar.dismiss()
Expand Down Expand Up @@ -1139,6 +1161,17 @@ extension BrowserViewController: OverlayViewDelegate {
}
}

extension BrowserViewController: SearchSuggestionsPromptViewDelegate {
func searchSuggestionsPromptView(_ searchSuggestionsPromptView: SearchSuggestionsPromptView, didEnable: Bool) {
UserDefaults.standard.set(true, forKey: SearchSuggestionsPromptView.respondedToSearchSuggestionsPrompt)
Settings.set(didEnable, forToggle: SettingsToggle.enableSearchSuggestions)
overlayView.updateSearchSuggestionsPrompt(hidden: true)
if didEnable, let urlbar = self.urlBar, let value = self.urlBar?.userInputText {
urlBar(urlbar, didEnterText: value)
}
}
}

extension BrowserViewController: WebControllerDelegate {

func webControllerDidStartProvisionalNavigation(_ controller: WebController) {
Expand Down
Loading

0 comments on commit a331a77

Please sign in to comment.