From 7bc097a9a0e1b987f697d84d586eaad30d8a1cab Mon Sep 17 00:00:00 2001 From: Aleksey Belouosv Date: Tue, 19 Feb 2019 17:30:21 +0300 Subject: [PATCH] [iOS] refactor local notifications --- iphone/Maps/Bridging-Header.h | 1 + iphone/Maps/Categories/NSDate+TimeDistance.h | 11 + iphone/Maps/Categories/NSDate+TimeDistance.m | 14 + iphone/Maps/Classes/MapsAppDelegate.mm | 158 ++++------ .../BackgroundFetchTask.swift | 14 - .../Maps/Core/Framework/MWMFrameworkHelper.mm | 5 +- .../CoreNotificationWrapper+Core.h | 10 + .../Notifications/CoreNotificationWrapper.h | 19 ++ .../Notifications/CoreNotificationWrapper.mm | 71 +++++ .../Notifications/LocalNotificationManager.h | 20 +- .../Notifications/LocalNotificationManager.mm | 289 ++---------------- .../Notifications/NotificationManager.swift | 49 +++ .../Core/Notifications/Notifications.swift | 83 +++++ iphone/Maps/Maps.xcodeproj/project.pbxproj | 22 ++ 14 files changed, 367 insertions(+), 399 deletions(-) create mode 100644 iphone/Maps/Categories/NSDate+TimeDistance.h create mode 100644 iphone/Maps/Categories/NSDate+TimeDistance.m create mode 100644 iphone/Maps/Core/Notifications/CoreNotificationWrapper+Core.h create mode 100644 iphone/Maps/Core/Notifications/CoreNotificationWrapper.h create mode 100644 iphone/Maps/Core/Notifications/CoreNotificationWrapper.mm create mode 100644 iphone/Maps/Core/Notifications/NotificationManager.swift create mode 100644 iphone/Maps/Core/Notifications/Notifications.swift diff --git a/iphone/Maps/Bridging-Header.h b/iphone/Maps/Bridging-Header.h index f0131f7a59..a69353d072 100644 --- a/iphone/Maps/Bridging-Header.h +++ b/iphone/Maps/Bridging-Header.h @@ -23,6 +23,7 @@ #import "BookmarksVC.h" #import "FacebookNativeAdAdapter.h" #import "LocalNotificationManager.h" +#import "CoreNotificationWrapper.h" #import "MapViewController.h" #import "MWMActivityViewController.h" #import "MWMAlertViewController.h" diff --git a/iphone/Maps/Categories/NSDate+TimeDistance.h b/iphone/Maps/Categories/NSDate+TimeDistance.h new file mode 100644 index 0000000000..60558a0b3c --- /dev/null +++ b/iphone/Maps/Categories/NSDate+TimeDistance.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSDate (TimeDistance) + +- (NSInteger)daysToNow; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iphone/Maps/Categories/NSDate+TimeDistance.m b/iphone/Maps/Categories/NSDate+TimeDistance.m new file mode 100644 index 0000000000..4b5d20c874 --- /dev/null +++ b/iphone/Maps/Categories/NSDate+TimeDistance.m @@ -0,0 +1,14 @@ +#import "NSDate+TimeDistance.h" + +@implementation NSDate (TimeDistance) + +- (NSInteger)daysToNow { + NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay + fromDate:self + toDate:[NSDate date] + options:NSCalendarWrapComponents]; + + return components.day; +} + +@end diff --git a/iphone/Maps/Classes/MapsAppDelegate.mm b/iphone/Maps/Classes/MapsAppDelegate.mm index f1e40f5558..2b51d410c8 100644 --- a/iphone/Maps/Classes/MapsAppDelegate.mm +++ b/iphone/Maps/Classes/MapsAppDelegate.mm @@ -1,5 +1,6 @@ #import "MapsAppDelegate.h" +#import "CoreNotificationWrapper+Core.h" #import "EAGLView.h" #import "LocalNotificationManager.h" #import "MWMAuthorizationCommon.h" @@ -13,6 +14,7 @@ #import "MWMRouter.h" #import "MWMSearch+CoreSpotlight.h" #import "MWMTextToSpeech.h" +#import "NSDate+TimeDistance.h" #import "MapViewController.h" #import "Statistics.h" #import "SwiftBridge.h" @@ -131,11 +133,12 @@ void TrackMarketingAppLaunch() using namespace osm_auth_ios; -@interface MapsAppDelegate () +@interface MapsAppDelegate () @property(nonatomic) NSInteger standbyCounter; @property(nonatomic) MWMBackgroundFetchScheduler * backgroundFetchScheduler; @property(nonatomic) id pendingTransactionHandler; +@property(nonatomic) NotificationManager * notificationManager; @end @@ -372,13 +375,6 @@ using namespace osm_auth_ios; [self commonInit]; - LocalNotificationManager * notificationManager = [LocalNotificationManager sharedManager]; - if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]) - { - NSNotification * notification = launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]; - [notificationManager processNotification:notification.userInfo onLaunch:YES]; - } - if ([Alohalytics isFirstSession]) { [self firstLaunchSetup]; @@ -401,7 +397,9 @@ using namespace osm_auth_ios; [GIDSignIn sharedInstance].clientID = [[NSBundle mainBundle] loadWithPlist:@"GoogleService-Info"][@"CLIENT_ID"]; - [UNUserNotificationCenter currentNotificationCenter].delegate = self; + self.notificationManager = [[NotificationManager alloc] init]; + self.notificationManager.delegate = self; + [UNUserNotificationCenter currentNotificationCenter].delegate = self.notificationManager; if ([MWMFrameworkHelper canUseNetwork]) { [[SubscriptionManager shared] validate]; @@ -429,57 +427,45 @@ using namespace osm_auth_ios; - (void)runBackgroundTasks:(NSArray * _Nonnull)tasks completionHandler:(void (^_Nullable)(UIBackgroundFetchResult))completionHandler { - auto completion = ^(UIBackgroundFetchResult result) { - if (completionHandler) - completionHandler(result); - }; self.backgroundFetchScheduler = - [[MWMBackgroundFetchScheduler alloc] initWithTasks:tasks completionHandler:completion]; + [[MWMBackgroundFetchScheduler alloc] initWithTasks:tasks + completionHandler:^(UIBackgroundFetchResult result) { + if (completionHandler) + completionHandler(result); + }]; [self.backgroundFetchScheduler run]; } - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - auto onTap = ^{ - MapViewController * mapViewController = [MapViewController sharedController]; - [mapViewController.navigationController popToRootViewControllerAnimated:NO]; - [mapViewController showUGCAuth]; - }; - - if ([LocalNotificationManager.sharedManager showUGCNotificationIfNeeded:onTap]) + if ([LocalNotificationManager shouldShowAuthNotification]) { + AuthNotification * n = [[AuthNotification alloc] initWithTitle:L(@"notification_unsent_reviews_title") + text:L(@"notification_unsent_reviews_message")]; + [self.notificationManager showNotification:n]; + [LocalNotificationManager authNotificationWasShown]; completionHandler(UIBackgroundFetchResultNewData); return; } - lightweight::Framework const framework(lightweight::REQUEST_TYPE_NOTIFICATION); - auto const notificationCandidate = framework.GetNotification(); - if (notificationCandidate) + CoreNotificationWrapper * reviewNotification = [LocalNotificationManager reviewNotificationWrapper]; + if (reviewNotification) { - auto const notification = notificationCandidate.get(); - if (notification.GetType() == notifications::NotificationCandidate::Type::UgcReview) - { - auto const place = notification.GetAddress().empty() - ? notification.GetReadableName() - : notification.GetReadableName() + ", " + notification.GetAddress(); - - [LocalNotificationManager.sharedManager - showReviewNotificationForPlace:@(place.c_str()) - onTap:^{ - [Statistics logEvent:kStatUGCReviewNotificationClicked]; - place_page::Info info; - if (GetFramework().MakePlacePageInfo(notification, info)) - [[MapViewController sharedController].controlsManager showPlacePageReview:info]; - }]; - } + NSString * text = reviewNotification.address.length > 0 + ? [NSString stringWithFormat:@"%@, %@", reviewNotification.readableName, reviewNotification.address] + : reviewNotification.readableName; + ReviewNotification * n = [[ReviewNotification alloc] initWithTitle:L(@"notification_leave_review_v2_title") + text:text + notificationWrapper:reviewNotification]; + [self.notificationManager showNotification:n]; + [LocalNotificationManager reviewNotificationWasShown]; } - auto tasks = @[ - [[MWMBackgroundStatisticsUpload alloc] init], [[MWMBackgroundEditsUpload alloc] init], - [[MWMBackgroundUGCUpload alloc] init], [[MWMBackgroundDownloadMapNotification alloc] init] - ]; - [self runBackgroundTasks:tasks completionHandler:completionHandler]; + [self runBackgroundTasks:@[[MWMBackgroundStatisticsUpload new], + [MWMBackgroundEditsUpload new], + [MWMBackgroundUGCUpload new]] + completionHandler:completionHandler]; } - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application @@ -752,45 +738,6 @@ continueUserActivity:(NSUserActivity *)userActivity }; } -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler -{ - if ([LocalNotificationManager isLocalNotification:notification]) - completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound); - else - [MWMPushNotifications userNotificationCenter:center - willPresentNotification:notification - withCompletionHandler:completionHandler]; -} - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center -didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void(^)(void))completionHandler -{ - if ([LocalNotificationManager isLocalNotification:response.notification]) - { - if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) - { - auto userInfo = response.notification.request.content.userInfo; - [[LocalNotificationManager sharedManager] processNotification:userInfo onLaunch:NO]; - } - completionHandler(); - } - else - { - [MWMPushNotifications userNotificationCenter:center - didReceiveNotificationResponse:response - withCompletionHandler:completionHandler]; - } -} - -- (void)application:(UIApplication *)application - didReceiveLocalNotification:(UILocalNotification *)notification -{ - [[LocalNotificationManager sharedManager] processNotification:notification.userInfo onLaunch:NO]; -} - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { m_sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey]; @@ -805,7 +752,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response if (isFBURL) return YES; - for (auto host in @[@"dlink.maps.me", @"dlink.mapsme.devmail.ru"]) + for (NSString * host in @[@"dlink.maps.me", @"dlink.mapsme.devmail.ru"]) { if ([self checkLaunchURL:(url.host.length > 0 && [url.host rangeOfString:host].location != NSNotFound) ? [self convertUniversalLink:url] : url]) @@ -926,8 +873,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return; NSDate * lastLaunchDate = [standartDefaults objectForKey:kUDLastLaunchDateKey]; - NSUInteger daysFromLastLaunch = [self.class daysBetweenNowAndDate:lastLaunchDate]; - if (daysFromLastLaunch > 0) + if (lastLaunchDate.daysToNow > 0) { sessionCount++; [standartDefaults setInteger:sessionCount forKey:kUDSessionsCountKey]; @@ -969,8 +915,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return NO; NSDate * const lastRateRequestDate = [standartDefaults objectForKey:kUDLastRateRequestDate]; - NSUInteger const daysFromLastRateRequest = - [MapsAppDelegate daysBetweenNowAndDate:lastRateRequestDate]; + NSInteger const daysFromLastRateRequest = lastRateRequestDate.daysToNow; // Do not show more than one alert per day. if (lastRateRequestDate != nil && daysFromLastRateRequest == 0) return NO; @@ -1002,20 +947,6 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return YES; } -+ (NSInteger)daysBetweenNowAndDate:(NSDate *)fromDate -{ - if (!fromDate) - return 0; - - NSDate * now = NSDate.date; - NSCalendar * calendar = NSCalendar.currentCalendar; - [calendar rangeOfUnit:NSCalendarUnitDay startDate:&fromDate interval:NULL forDate:fromDate]; - [calendar rangeOfUnit:NSCalendarUnitDay startDate:&now interval:NULL forDate:now]; - NSDateComponents * difference = - [calendar components:NSCalendarUnitDay fromDate:fromDate toDate:now options:0]; - return difference.day; -} - #pragma mark - Showcase - (MWMMyTarget *)myTarget @@ -1028,4 +959,25 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return _myTarget; } +#pragma mark - NotificationManagerDelegate + +- (void)didOpenNotification:(Notification *)notification +{ + if (notification.class == ReviewNotification.class) + { + [Statistics logEvent:kStatUGCReviewNotificationClicked]; + ReviewNotification * reviewNotification = (ReviewNotification *)notification; + place_page::Info info; + if (GetFramework().MakePlacePageInfo(reviewNotification.notificationWrapper.notificationCandidate, info)) + [[MapViewController sharedController].controlsManager showPlacePageReview:info]; + } + else if (notification.class == AuthNotification.class) + { + [Statistics logEvent:@"UGC_UnsentNotification_clicked"]; + MapViewController * mapViewController = [MapViewController sharedController]; + [mapViewController.navigationController popToRootViewControllerAnimated:NO]; + [mapViewController showUGCAuth]; + } +} + @end diff --git a/iphone/Maps/Core/BackgroundFetchScheduler/BackgroundFetchTask/BackgroundFetchTask.swift b/iphone/Maps/Core/BackgroundFetchScheduler/BackgroundFetchTask/BackgroundFetchTask.swift index 6f1e0b80a3..08de54fb10 100644 --- a/iphone/Maps/Core/BackgroundFetchScheduler/BackgroundFetchTask/BackgroundFetchTask.swift +++ b/iphone/Maps/Core/BackgroundFetchScheduler/BackgroundFetchTask/BackgroundFetchTask.swift @@ -60,17 +60,3 @@ final class BackgroundUGCUpload: BackgroundFetchTask { return "UGC upload" } } - -@objc(MWMBackgroundDownloadMapNotification) -final class BackgroundDownloadMapNotification: BackgroundFetchTask { - override var frameworkType: BackgroundFetchTaskFrameworkType { return .full } - - override fileprivate func fire() { - LocalNotificationManager.shared().showDownloadMapNotificationIfNeeded(self.finish) - } - - override var description: String { - return "Download map notification" - } -} - diff --git a/iphone/Maps/Core/Framework/MWMFrameworkHelper.mm b/iphone/Maps/Core/Framework/MWMFrameworkHelper.mm index 792213ae17..642cc2773e 100644 --- a/iphone/Maps/Core/Framework/MWMFrameworkHelper.mm +++ b/iphone/Maps/Core/Framework/MWMFrameworkHelper.mm @@ -6,7 +6,7 @@ #include "Framework.h" #include "base/sunrise_sunset.hpp" -#include "platform/network_policy.hpp" +#include "platform/network_policy_ios.h" @implementation MWMFrameworkHelper @@ -99,8 +99,7 @@ + (BOOL)canUseNetwork { - return platform::GetCurrentNetworkPolicy().CanUse() || - GetPlatform().ConnectionStatus() != Platform::EConnectionType::CONNECTION_WWAN; + return network_policy::CanUseNetwork(); } + (BOOL)isNetworkConnected diff --git a/iphone/Maps/Core/Notifications/CoreNotificationWrapper+Core.h b/iphone/Maps/Core/Notifications/CoreNotificationWrapper+Core.h new file mode 100644 index 0000000000..244d4b3028 --- /dev/null +++ b/iphone/Maps/Core/Notifications/CoreNotificationWrapper+Core.h @@ -0,0 +1,10 @@ +#import "CoreNotificationWrapper.h" + +#include "map/notifications/notification_manager.hpp" + +@interface CoreNotificationWrapper (Core) + +- (instancetype)initWithNotificationCandidate:(notifications::NotificationCandidate const &)notification; +- (notifications::NotificationCandidate)notificationCandidate; + +@end diff --git a/iphone/Maps/Core/Notifications/CoreNotificationWrapper.h b/iphone/Maps/Core/Notifications/CoreNotificationWrapper.h new file mode 100644 index 0000000000..eb45dd6870 --- /dev/null +++ b/iphone/Maps/Core/Notifications/CoreNotificationWrapper.h @@ -0,0 +1,19 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CoreNotificationWrapper : NSObject + +@property(nonatomic, copy) NSString *bestType; +@property(nonatomic, copy) NSString *defaultName; +@property(nonatomic, copy) NSString *readableName; +@property(nonatomic, copy) NSString *address; +@property(nonatomic) double x; +@property(nonatomic) double y; + +- (instancetype _Nullable)initWithNotificationDictionary:(NSDictionary *)dictionary; +- (NSDictionary *)notificationDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iphone/Maps/Core/Notifications/CoreNotificationWrapper.mm b/iphone/Maps/Core/Notifications/CoreNotificationWrapper.mm new file mode 100644 index 0000000000..2a049bddfd --- /dev/null +++ b/iphone/Maps/Core/Notifications/CoreNotificationWrapper.mm @@ -0,0 +1,71 @@ +#import "CoreNotificationWrapper.h" +#import "CoreNotificationWrapper+Core.h" + +static NSString *const kX = @"x"; +static NSString *const kY = @"y"; +static NSString *const kType = @"type"; +static NSString *const kName = @"name"; +static NSString *const kReadableName = @"readableName"; + +@implementation CoreNotificationWrapper + +- (instancetype)initWithNotificationDictionary:(NSDictionary *)dictionary { + self = [super init]; + if (self) { + NSNumber *x = dictionary[kX]; + NSNumber *y = dictionary[kY]; + NSString *type = dictionary[kType]; + NSString *name = dictionary[kName]; + NSString *readableName = dictionary[kReadableName]; + if (x && y && type && name && readableName) { + _x = x.doubleValue; + _y = y.doubleValue; + _bestType = type; + _defaultName = name; + _readableName = readableName; + } else { + return nil; + } + } + + return self; +} + +- (NSDictionary *)notificationDictionary { + return @{ + kX : @(self.x), + kY : @(self.y), + kType : self.bestType, + kName : self.defaultName, + kReadableName : self.readableName + }; +} + +@end + +@implementation CoreNotificationWrapper (Core) + +- (instancetype)initWithNotificationCandidate:(notifications::NotificationCandidate const &)notification { + self = [super init]; + if (self) { + _x = notification.GetPos().x; + _y = notification.GetPos().y; + _defaultName = @(notification.GetDefaultName().c_str()); + _bestType = @(notification.GetBestFeatureType().c_str()); + _readableName = @(notification.GetReadableName().c_str()); + _address = @(notification.GetAddress().c_str()); + } + + return self; +} + +- (notifications::NotificationCandidate)notificationCandidate { + notifications::NotificationCandidate nc; + nc.SetDefaultName(self.defaultName.UTF8String); + nc.SetBestFeatureType(self.bestType.UTF8String); + nc.SetPos(m2::PointD(self.x, self.y)); + nc.SetReadableName(self.readableName.UTF8String); + return nc; +} + +@end diff --git a/iphone/Maps/Core/Notifications/LocalNotificationManager.h b/iphone/Maps/Core/Notifications/LocalNotificationManager.h index b0baac39d1..1035d6a971 100644 --- a/iphone/Maps/Core/Notifications/LocalNotificationManager.h +++ b/iphone/Maps/Core/Notifications/LocalNotificationManager.h @@ -1,18 +1,14 @@ -#import "MWMTypes.h" +NS_ASSUME_NONNULL_BEGIN -#import - -typedef void (^CompletionHandler)(UIBackgroundFetchResult); +@class CoreNotificationWrapper; @interface LocalNotificationManager : NSObject -+ (instancetype)sharedManager; - -+ (BOOL)isLocalNotification:(UNNotification *)notification; - -- (BOOL)showUGCNotificationIfNeeded:(MWMVoidBlock)onTap; -- (void)showReviewNotificationForPlace:(NSString *)place onTap:(MWMVoidBlock)onReviewNotification; -- (void)showDownloadMapNotificationIfNeeded:(CompletionHandler)completionHandler; -- (void)processNotification:(NSDictionary *)userInfo onLaunch:(BOOL)onLaunch; ++ (BOOL)shouldShowAuthNotification; ++ (void)authNotificationWasShown; ++ (CoreNotificationWrapper * _Nullable)reviewNotificationWrapper; ++ (void)reviewNotificationWasShown; @end + +NS_ASSUME_NONNULL_END diff --git a/iphone/Maps/Core/Notifications/LocalNotificationManager.mm b/iphone/Maps/Core/Notifications/LocalNotificationManager.mm index 94154066e9..cbc2f61b84 100644 --- a/iphone/Maps/Core/Notifications/LocalNotificationManager.mm +++ b/iphone/Maps/Core/Notifications/LocalNotificationManager.mm @@ -1,82 +1,23 @@ #import "LocalNotificationManager.h" -#import "CLLocation+Mercator.h" -#import "MWMStorage.h" -#import "MapViewController.h" +#import "CoreNotificationWrapper+Core.h" +#import "NSDate+TimeDistance.h" #import "Statistics.h" -#import "SwiftBridge.h" -#import "3party/Alohalytics/src/alohalytics_objc.h" - -#include "Framework.h" - -#include "storage/country_info_getter.hpp" -#include "storage/storage_helpers.hpp" #include "map/framework_light.hpp" - #include "platform/network_policy_ios.h" -namespace -{ -NSString * const kLocalNotificationNameKey = @"LocalNotificationName"; -NSString * const kUGCNotificationValue = @"UGC"; -NSString * const kReviewNotificationValue = @"ReviewNotification"; -NSString * const kDownloadNotificationValue = @"Download"; -NSString * const kDownloadMapActionKey = @"DownloadMapActionKey"; -NSString * const kDownloadMapActionName = @"DownloadMapActionName"; -NSString * const kDownloadMapCountryId = @"DownloadMapCountryId"; - -NSString * const kFlagsKey = @"DownloadMapNotificationFlags"; - -NSTimeInterval constexpr kRepeatedNotificationIntervalInSeconds = - 3 * 30 * 24 * 60 * 60; // three months -NSString * const kLastUGCNotificationDate = @"LastUGCNotificationDate"; -} // namespace - -using namespace storage; - -@interface LocalNotificationManager () - -@property(nonatomic) CLLocationManager * locationManager; -@property(copy, nonatomic) CompletionHandler downloadMapCompletionHandler; -@property(weak, nonatomic) NSTimer * timer; -@property(copy, nonatomic) MWMVoidBlock onTap; -@property(copy, nonatomic) MWMVoidBlock onReviewNotification; - -@end +static NSString * const kLastUGCNotificationDate = @"LastUGCNotificationDate"; @implementation LocalNotificationManager -+ (instancetype)sharedManager -{ - static LocalNotificationManager * manager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - manager = [[self alloc] init]; - }); - return manager; -} - -+ (BOOL)isLocalNotification:(UNNotification *)notification -{ - auto userInfo = notification.request.content.userInfo; - return userInfo[kLocalNotificationNameKey] != nil; -} - -+ (BOOL)shouldShowUGCNotification ++ (BOOL)shouldShowAuthNotification { if (!network_policy::CanUseNetwork()) return NO; - - auto ud = [NSUserDefaults standardUserDefaults]; - if (NSDate * date = [ud objectForKey:kLastUGCNotificationDate]) + if (NSDate * date = [[NSUserDefaults standardUserDefaults] objectForKey:kLastUGCNotificationDate]) { - auto calendar = [NSCalendar currentCalendar]; - auto components = [calendar components:NSCalendarUnitDay fromDate:date - toDate:[NSDate date] options:NSCalendarWrapComponents]; - - auto constexpr minDaysSinceLast = 5u; - if (components.day <= minDaysSinceLast) + if (date.daysToNow <= 5) return NO; } @@ -88,7 +29,7 @@ using namespace storage; return YES; } -+ (void)UGCNotificationWasShown ++ (void)authNotificationWasShown { auto ud = [NSUserDefaults standardUserDefaults]; [ud setObject:[NSDate date] forKey:kLastUGCNotificationDate]; @@ -96,212 +37,26 @@ using namespace storage; [Statistics logEvent:@"UGC_UnsentNotification_shown"]; } -- (void)dealloc { _locationManager.delegate = nil; } -- (void)processNotification:(NSDictionary *)userInfo onLaunch:(BOOL)onLaunch ++ (CoreNotificationWrapper *)reviewNotificationWrapper { - if ([userInfo[kDownloadMapActionKey] isEqualToString:kDownloadMapActionName]) + lightweight::Framework const framework(lightweight::REQUEST_TYPE_NOTIFICATION); + auto const notificationCandidate = framework.GetNotification(); + if (notificationCandidate) { - [Statistics logEvent:@"'Download Map' Notification Clicked"]; - MapViewController * mapViewController = [MapViewController sharedController]; - [mapViewController.navigationController popToRootViewControllerAnimated:NO]; - - NSString * notificationCountryId = userInfo[kDownloadMapCountryId]; - CountryId const countryId = notificationCountryId.UTF8String; - [Statistics logEvent:kStatDownloaderMapAction - withParameters:@{ - kStatAction : kStatDownload, - kStatIsAuto : kStatNo, - kStatFrom : kStatMap, - kStatScenario : kStatDownload - }]; - [MWMStorage downloadNode:countryId - onSuccess:^{ - GetFramework().ShowNode(countryId); - }]; - } - else if ([userInfo[kLocalNotificationNameKey] isEqualToString:kUGCNotificationValue]) - { - if (self.onTap) - self.onTap(); - - [Statistics logEvent:@"UGC_UnsentNotification_clicked"]; - } - else if ([userInfo[kLocalNotificationNameKey] isEqualToString:kReviewNotificationValue]) - { - if (self.onReviewNotification) - self.onReviewNotification(); - } -} - -#pragma mark - Location Notifications - -- (void)showReviewNotificationForPlace:(NSString *)place onTap:(MWMVoidBlock)onReviewNotification { - [Statistics logEvent:kStatUGCReviewNotificationShown]; - self.onReviewNotification = onReviewNotification; - - UILocalNotification * notification = [[UILocalNotification alloc] init]; - notification.alertTitle = L(@"notification_leave_review_v2_title"); - notification.alertBody = place; - notification.alertAction = L(@"leave_a_review"); - notification.soundName = UILocalNotificationDefaultSoundName; - notification.userInfo = @{kLocalNotificationNameKey : kReviewNotificationValue}; - - [UIApplication.sharedApplication presentLocalNotificationNow:notification]; -} - -- (BOOL)showUGCNotificationIfNeeded:(MWMVoidBlock)onTap -{ - auto application = UIApplication.sharedApplication; - auto identifier = UIBackgroundTaskInvalid; - auto handler = [&identifier] { - [[UIApplication sharedApplication] endBackgroundTask:identifier]; - }; - - identifier = [application beginBackgroundTaskWithExpirationHandler:^{ - handler(); - }]; - - if (![LocalNotificationManager shouldShowUGCNotification]) { - handler(); - return NO; - } - - self.onTap = onTap; - UILocalNotification * notification = [[UILocalNotification alloc] init]; - notification.alertTitle = L(@"notification_unsent_reviews_title"); - notification.alertBody = L(@"notification_unsent_reviews_message"); - notification.alertAction = L(@"authorization_button_sign_in"); - notification.soundName = UILocalNotificationDefaultSoundName; - notification.userInfo = @{kLocalNotificationNameKey : kUGCNotificationValue}; - - [application presentLocalNotificationNow:notification]; - [LocalNotificationManager UGCNotificationWasShown]; - handler(); - return YES; -} - -- (void)showDownloadMapNotificationIfNeeded:(CompletionHandler)completionHandler -{ - NSTimeInterval const completionTimeIndent = 2.0; - NSTimeInterval const backgroundTimeRemaining = - UIApplication.sharedApplication.backgroundTimeRemaining - completionTimeIndent; - if ([CLLocationManager locationServicesEnabled] && backgroundTimeRemaining > 0.0) - { - self.downloadMapCompletionHandler = completionHandler; - self.timer = [NSTimer scheduledTimerWithTimeInterval:backgroundTimeRemaining - target:self - selector:@selector(timerSelector:) - userInfo:nil - repeats:NO]; - LOG(LINFO, ("startUpdatingLocation")); - [self.locationManager startUpdatingLocation]; - } - else - { - LOG(LINFO, ("stopUpdatingLocation")); - [self.locationManager stopUpdatingLocation]; - completionHandler(UIBackgroundFetchResultFailed); - } -} - -- (BOOL)shouldShowNotificationForCountryId:(NSString *)countryId -{ - if (!countryId || countryId.length == 0) - return NO; - NSUserDefaults * ud = NSUserDefaults.standardUserDefaults; - NSDictionary * flags = [ud objectForKey:kFlagsKey]; - NSDate * lastShowDate = flags[countryId]; - return !lastShowDate || - [[NSDate date] timeIntervalSinceDate:lastShowDate] > - kRepeatedNotificationIntervalInSeconds; -} - -- (void)markNotificationShownForCountryId:(NSString *)countryId -{ - NSUserDefaults * ud = NSUserDefaults.standardUserDefaults; - NSMutableDictionary * flags = [[ud objectForKey:kFlagsKey] mutableCopy]; - if (!flags) - flags = [NSMutableDictionary dictionary]; - flags[countryId] = [NSDate date]; - [ud setObject:flags forKey:kFlagsKey]; - [ud synchronize]; -} - -- (void)timerSelector:(id)sender -{ - // Location still was not received but it's time to finish up so system will not kill us. - LOG(LINFO, ("stopUpdatingLocation")); - [self.locationManager stopUpdatingLocation]; - [self performCompletionHandler:UIBackgroundFetchResultFailed]; -} - -- (void)performCompletionHandler:(UIBackgroundFetchResult)result -{ - if (!self.downloadMapCompletionHandler) - return; - self.downloadMapCompletionHandler(result); - self.downloadMapCompletionHandler = nil; -} - -#pragma mark - Location Manager - -- (CLLocationManager *)locationManager -{ - if (!_locationManager) - { - _locationManager = [[CLLocationManager alloc] init]; - _locationManager.delegate = self; - _locationManager.distanceFilter = kCLLocationAccuracyThreeKilometers; - } - return _locationManager; -} - -- (void)locationManager:(CLLocationManager *)manager - didUpdateLocations:(NSArray *)locations -{ - [self.timer invalidate]; - LOG(LINFO, ("stopUpdatingLocation")); - [self.locationManager stopUpdatingLocation]; - NSString * flurryEventName = @"'Download Map' Notification Didn't Schedule"; - UIBackgroundFetchResult result = UIBackgroundFetchResultNoData; - - BOOL const inBackground = - UIApplication.sharedApplication.applicationState == UIApplicationStateBackground; - BOOL const onWiFi = (Platform::ConnectionStatus() == Platform::EConnectionType::CONNECTION_WIFI); - if (inBackground && onWiFi) - { - CLLocation * lastLocation = locations.lastObject; - auto const & mercator = lastLocation.mercator; - auto & f = GetFramework(); - auto const & countryInfoGetter = f.GetCountryInfoGetter(); - if (!IsPointCoveredByDownloadedMaps(mercator, f.GetStorage(), countryInfoGetter)) + auto const notification = notificationCandidate.get(); + if (notification.GetType() == notifications::NotificationCandidate::Type::UgcReview) { - NSString * countryId = @(countryInfoGetter.GetRegionCountryId(mercator).c_str()); - if ([self shouldShowNotificationForCountryId:countryId]) - { - [self markNotificationShownForCountryId:countryId]; - - UILocalNotification * notification = [[UILocalNotification alloc] init]; - notification.alertAction = L(@"download"); - notification.alertBody = L(@"download_map_notification"); - notification.soundName = UILocalNotificationDefaultSoundName; - notification.userInfo = - @{kLocalNotificationNameKey : kDownloadNotificationValue, - kDownloadMapActionKey : kDownloadMapActionName, - kDownloadMapCountryId : countryId}; - - UIApplication * application = UIApplication.sharedApplication; - [application presentLocalNotificationNow:notification]; - - [Alohalytics logEvent:@"suggestedToDownloadMissingMapForCurrentLocation" - atLocation:lastLocation]; - flurryEventName = @"'Download Map' Notification Scheduled"; - result = UIBackgroundFetchResultNewData; - } + CoreNotificationWrapper * w = [[CoreNotificationWrapper alloc] initWithNotificationCandidate:notification]; + return w; } } - [Statistics logEvent:flurryEventName withParameters:@{ @"WiFi" : @(onWiFi) }]; - [self performCompletionHandler:result]; + + return nil; +} + ++ (void)reviewNotificationWasShown +{ + [Statistics logEvent:kStatUGCReviewNotificationShown]; } @end diff --git a/iphone/Maps/Core/Notifications/NotificationManager.swift b/iphone/Maps/Core/Notifications/NotificationManager.swift new file mode 100644 index 0000000000..4e2f93b97e --- /dev/null +++ b/iphone/Maps/Core/Notifications/NotificationManager.swift @@ -0,0 +1,49 @@ +@objc +protocol NotificationManagerDelegate { + func didOpenNotification(_ notification: Notification) +} + +@objc +final class NotificationManager: NSObject { + @objc var delegate: NotificationManagerDelegate? + + @objc + func showNotification(_ notification: Notification) { + let notificationContent = UNMutableNotificationContent() + notificationContent.title = notification.title + notificationContent.body = notification.text + notificationContent.sound = UNNotificationSound.default() + notificationContent.userInfo = notification.userInfo + let notificationRequest = UNNotificationRequest(identifier: notification.identifier, + content: notificationContent, + trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1, + repeats: false)) + UNUserNotificationCenter.current().add(notificationRequest) + } +} + +extension NotificationManager: UNUserNotificationCenterDelegate { + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + if Notification.fromUserInfo(notification.request.content.userInfo) != nil { + completionHandler([.sound, .alert]) + } else { + MWMPushNotifications.userNotificationCenter(center, + willPresent: notification, + withCompletionHandler: completionHandler) + } + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + if let n = Notification.fromUserInfo(response.notification.request.content.userInfo) { + delegate?.didOpenNotification(n) + } else { + MWMPushNotifications.userNotificationCenter(center, + didReceive: response, + withCompletionHandler: completionHandler) + } + } +} diff --git a/iphone/Maps/Core/Notifications/Notifications.swift b/iphone/Maps/Core/Notifications/Notifications.swift new file mode 100644 index 0000000000..bd8b061290 --- /dev/null +++ b/iphone/Maps/Core/Notifications/Notifications.swift @@ -0,0 +1,83 @@ +@objc +class Notification: NSObject { + fileprivate static let kLocalNotificationName = "LocalNotificationName" + fileprivate static let kLocalNotificationTitle = "LocalNotificationTitle" + fileprivate static let kLocalNotificationText = "LocalNotificationText" + let title: String + let text: String + var userInfo: [AnyHashable : Any] { + return [Notification.kLocalNotificationName : identifier, + Notification.kLocalNotificationTitle : title, + Notification.kLocalNotificationText : text] + } + + var identifier: String { + assert(false, "Override in subclass") + return "" + } + + @objc + init(title: String, text: String) { + self.title = title + self.text = text + } + + convenience init?(userInfo: [AnyHashable : Any]) { + if let title = userInfo[Notification.kLocalNotificationTitle] as? String, + let text = userInfo[Notification.kLocalNotificationText] as? String { + self.init(title: title, text: text) + } else { + return nil + } + } + + class func fromUserInfo(_ userInfo: [AnyHashable : Any]) -> Notification? { + if let notificationName = userInfo[Notification.kLocalNotificationName] as? String { + switch notificationName { + case ReviewNotification.identifier: + return ReviewNotification(userInfo: userInfo) + case AuthNotification.identifier: + return AuthNotification(userInfo: userInfo) + default: + return nil + } + } + return nil + } +} + +@objc +class AuthNotification: Notification { + static let identifier = "UGC" + override var identifier: String { return AuthNotification.identifier } +} + +@objc +class ReviewNotification: Notification { + private static let kNotificationObject = "NotificationObject" + static let identifier = "ReviewNotification" + @objc let notificationWrapper: CoreNotificationWrapper + override var identifier: String { return ReviewNotification.identifier } + override var userInfo: [AnyHashable : Any] { + var result = super.userInfo + result[ReviewNotification.kNotificationObject] = notificationWrapper.notificationDictionary() + return result + } + + @objc + init(title: String, text: String, notificationWrapper: CoreNotificationWrapper) { + self.notificationWrapper = notificationWrapper + super.init(title: title, text: text) + } + + convenience init?(userInfo: [AnyHashable : Any]) { + if let title = userInfo[Notification.kLocalNotificationTitle] as? String, + let text = userInfo[Notification.kLocalNotificationText] as? String, + let notificationDictionary = userInfo[ReviewNotification.kNotificationObject] as? [AnyHashable : Any], + let notificationWrapper = CoreNotificationWrapper(notificationDictionary: notificationDictionary) { + self.init(title: title, text: text, notificationWrapper: notificationWrapper) + } else { + return nil + } + } +} diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 8f3a3fe9e7..b77d933dea 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -395,6 +395,8 @@ 4767CDA620AB1F6200BD8166 /* LeftAlignedIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4767CDA520AB1F6200BD8166 /* LeftAlignedIconButton.swift */; }; 4767CDA820AB401000BD8166 /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4767CDA720AB401000BD8166 /* LinkTextView.swift */; }; 4767CDC120B477BA00BD8166 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4767CDC020B477BA00BD8166 /* WelcomeViewController.swift */; }; + 47699A0821F08E37009E6585 /* NSDate+TimeDistance.m in Sources */ = {isa = PBXBuildFile; fileRef = 47699A0721F08E37009E6585 /* NSDate+TimeDistance.m */; }; + 47699A0A21F0C4C8009E6585 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47699A0921F0C4C8009E6585 /* NotificationManager.swift */; }; 477D7AC7218F1515007EE2CB /* IPaidRoutePurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477D7AC6218F1515007EE2CB /* IPaidRoutePurchase.swift */; }; 47800D1D20BEEE2E00072F42 /* TermsOfUseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47800D1C20BEEE2E00072F42 /* TermsOfUseController.swift */; }; 47800D2520C05E3200072F42 /* libFlurry_8.6.1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 47800D2420C05E3200072F42 /* libFlurry_8.6.1.a */; }; @@ -429,6 +431,8 @@ 47E6CB0C2178BA3600EA102B /* SearchBannerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 47E6CB0A2178BA3600EA102B /* SearchBannerCell.xib */; }; 47EF05B321504D8F00EAC269 /* RemoveAdsPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EF05B221504D8F00EAC269 /* RemoveAdsPresentationController.swift */; }; 47F67D1521CAB21B0069754E /* MWMImageCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 47F67D1421CAB21B0069754E /* MWMImageCoder.m */; }; + 47F6E51221F61908004580CA /* CoreNotificationWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 47F6E51121F61908004580CA /* CoreNotificationWrapper.mm */; }; + 47F6E51721FB3C51004580CA /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F6E51621FB3C51004580CA /* Notifications.swift */; }; 47F86CFF20C936FC00FEE291 /* TabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F86CFE20C936FC00FEE291 /* TabView.swift */; }; 47F86D0120C93D8D00FEE291 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F86D0020C93D8D00FEE291 /* TabViewController.swift */; }; 4A300ED51C6DCFD400140018 /* countries-strings in Resources */ = {isa = PBXBuildFile; fileRef = 4A300ED31C6DCFD400140018 /* countries-strings */; }; @@ -1426,6 +1430,9 @@ 4767CDA520AB1F6200BD8166 /* LeftAlignedIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedIconButton.swift; sourceTree = ""; }; 4767CDA720AB401000BD8166 /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; 4767CDC020B477BA00BD8166 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; + 47699A0621F08E37009E6585 /* NSDate+TimeDistance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDate+TimeDistance.h"; sourceTree = ""; }; + 47699A0721F08E37009E6585 /* NSDate+TimeDistance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDate+TimeDistance.m"; sourceTree = ""; }; + 47699A0921F0C4C8009E6585 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 477D7AC6218F1515007EE2CB /* IPaidRoutePurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPaidRoutePurchase.swift; sourceTree = ""; }; 47800D1C20BEEE2E00072F42 /* TermsOfUseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfUseController.swift; sourceTree = ""; }; 47800D2420C05E3200072F42 /* libFlurry_8.6.1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libFlurry_8.6.1.a; sourceTree = ""; }; @@ -1473,6 +1480,10 @@ 47F67D1321CAB21B0069754E /* MWMImageCoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMImageCoder.h; sourceTree = ""; }; 47F67D1421CAB21B0069754E /* MWMImageCoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MWMImageCoder.m; sourceTree = ""; }; 47F67D1621CAB50B0069754E /* IMWMImageCoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IMWMImageCoder.h; sourceTree = ""; }; + 47F6E51021F61908004580CA /* CoreNotificationWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreNotificationWrapper.h; sourceTree = ""; }; + 47F6E51121F61908004580CA /* CoreNotificationWrapper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CoreNotificationWrapper.mm; sourceTree = ""; }; + 47F6E51321F61974004580CA /* CoreNotificationWrapper+Core.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CoreNotificationWrapper+Core.h"; sourceTree = ""; }; + 47F6E51621FB3C51004580CA /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 47F86CFE20C936FC00FEE291 /* TabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabView.swift; sourceTree = ""; }; 47F86D0020C93D8D00FEE291 /* TabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; 4A00DBDE1AB704C400113624 /* drules_proto_dark.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = drules_proto_dark.bin; path = ../../data/drules_proto_dark.bin; sourceTree = ""; }; @@ -2685,6 +2696,8 @@ 33E905452180C40900868CAC /* UIViewController+Authorization.swift */, 347E03981FAC5F1D00426032 /* UIWindow+InputLanguage.swift */, 4767CDA320AAF66B00BD8166 /* NSAttributedString+HTML.swift */, + 47699A0621F08E37009E6585 /* NSDate+TimeDistance.h */, + 47699A0721F08E37009E6585 /* NSDate+TimeDistance.m */, ); path = Categories; sourceTree = ""; @@ -2916,6 +2929,11 @@ 3486B5061E27A4B50069C126 /* LocalNotificationManager.mm */, 3486B50A1E27A4C50069C126 /* MWMPushNotifications.h */, 3486B50B1E27A6DA0069C126 /* MWMPushNotifications.mm */, + 47699A0921F0C4C8009E6585 /* NotificationManager.swift */, + 47F6E51021F61908004580CA /* CoreNotificationWrapper.h */, + 47F6E51121F61908004580CA /* CoreNotificationWrapper.mm */, + 47F6E51321F61974004580CA /* CoreNotificationWrapper+Core.h */, + 47F6E51621FB3C51004580CA /* Notifications.swift */, ); path = Notifications; sourceTree = ""; @@ -5057,6 +5075,7 @@ 342CC5F21C2D7730005F3FE5 /* MWMAuthorizationLoginViewController.mm in Sources */, 340475591E081A4600C92850 /* WebViewController.mm in Sources */, 3404F4992028A20D0090E401 /* BMCCategoryCell.swift in Sources */, + 47F6E51721FB3C51004580CA /* Notifications.swift in Sources */, F62607FD207B790300176C5A /* SpinnerAlert.swift in Sources */, 34F407411E9E1AFF00E57AC0 /* RBBanner.swift in Sources */, 3444DFD21F17620C00E73099 /* MWMMapWidgetsHelper.mm in Sources */, @@ -5213,6 +5232,7 @@ 47F67D1521CAB21B0069754E /* MWMImageCoder.m in Sources */, 34AB66861FC5AA330078E451 /* MWMNavigationInfoView.mm in Sources */, 34C9BD051C6DB693000DC38D /* MWMViewController.mm in Sources */, + 47699A0A21F0C4C8009E6585 /* NotificationManager.swift in Sources */, 331630D12191D74B00BB91A9 /* TagSectionHeaderView.swift in Sources */, F6E2FDA41E097BA00083EBEC /* MWMCuisineEditorViewController.mm in Sources */, 3454D7CB1E07F045004AF2AD /* UIColor+MapsMeColor.mm in Sources */, @@ -5234,6 +5254,7 @@ 3454D7BF1E07F045004AF2AD /* DateComponentsFormatter+ETA.swift in Sources */, 6741AA1C1BF340DE002C974C /* MWMRoutingDisclaimerAlert.mm in Sources */, 34D3B0481E389D05004100F9 /* MWMNoteCell.mm in Sources */, + 47F6E51221F61908004580CA /* CoreNotificationWrapper.mm in Sources */, F6E2FE971E097BA00083EBEC /* ContextViews.mm in Sources */, 344532561F7155540059FBCC /* UGCReviewModel.swift in Sources */, 6741AA1D1BF340DE002C974C /* MWMDownloadTransitMapAlert.mm in Sources */, @@ -5298,6 +5319,7 @@ F6A2184A1CA3F26800BE2CC6 /* MWMEditorViralActivityItem.mm in Sources */, F6E2FED61E097BA00083EBEC /* MWMSearchChangeModeView.mm in Sources */, 33603C85219F0F6300B11FFE /* SharingPropertiesViewController.swift in Sources */, + 47699A0821F08E37009E6585 /* NSDate+TimeDistance.m in Sources */, 34845DB31E165E24003D55B9 /* SearchNoResultsViewController.swift in Sources */, 34AB660B1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm in Sources */, F5BD29FF26AD58255766C51A /* DiscoverySpinnerCell.swift in Sources */,