This PR contains the work done to add support to the **Wikipedia** app to show a custom location in the map of the *Places* view coming from a places deep link. To provide further details about the work: - [x] implemented the handling of location coordinates coming from a URL inside the function that generates a `NSUSerActivity` instance out of *places* deep links; - [x] added the `wmf_locationFromURL` property to the `NSUSerActivity` via an extension that returns a `CLLocation` instance with latitude and longitude coordinates if provided; - [x] implemented the "centerMap(onLocation:)" function in the `PlacesViewController` view controller that, essentially, center the map of this view controller to a given location; - [x] improved the handling of a *places* user activity in the `WMFAppViewController` view controller to center the map in case a location has been provided via a deep link; - [x] fixed some duplicated code that was blocking the test from compiling. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#2
2153 lines
98 KiB
Objective-C
2153 lines
98 KiB
Objective-C
#import "WMFAppViewController.h"
|
|
@import WMF;
|
|
@import SystemConfiguration;
|
|
#import "Wikipedia-Swift.h"
|
|
|
|
#define DEBUG_THEMES 1
|
|
|
|
// Views
|
|
#import "UIViewController+WMFStoryboardUtilities.h"
|
|
#import "UIApplicationShortcutItem+WMFShortcutItem.h"
|
|
|
|
// View Controllers
|
|
#import "WMFSettingsViewController.h"
|
|
#import "WMFFirstRandomViewController.h"
|
|
|
|
#import "AppDelegate.h"
|
|
|
|
#import "WMFDailyStatsLoggingFunnel.h"
|
|
|
|
#import "Wikipedia-Swift.h"
|
|
#import "EXTScope.h"
|
|
|
|
/**
|
|
* Enums for each tab in the main tab bar.
|
|
*/
|
|
typedef NS_ENUM(NSUInteger, WMFAppTabType) {
|
|
WMFAppTabTypeMain = 0,
|
|
WMFAppTabTypePlaces = 1,
|
|
WMFAppTabTypeSaved = 2,
|
|
WMFAppTabTypeRecent = 3,
|
|
WMFAppTabTypeSearch = 4
|
|
};
|
|
|
|
/**
|
|
* Number of tabs in the main tab bar.
|
|
*
|
|
* @warning Kept as a separate constant to prevent switch statements from being considered inexhaustive. This means we
|
|
* need to make sure it's manually kept in sync by ensuring:
|
|
* - The tab enum we increment is the last one
|
|
* - The first tab enum is initialized to 0
|
|
*
|
|
* @see WMFAppTabType
|
|
*/
|
|
|
|
static NSTimeInterval const WMFTimeBeforeShowingExploreScreenOnLaunch = 24 * 60 * 60;
|
|
|
|
static CFTimeInterval const WMFRemoteAppConfigCheckInterval = 3 * 60 * 60;
|
|
static NSString *const WMFLastRemoteAppConfigCheckAbsoluteTimeKey = @"WMFLastRemoteAppConfigCheckAbsoluteTimeKey";
|
|
|
|
static const NSString *kvo_NSUserDefaults_defaultTabType = @"kvo_NSUserDefaults_defaultTabType";
|
|
static const NSString *kvo_SavedArticlesFetcher_progress = @"kvo_SavedArticlesFetcher_progress";
|
|
|
|
NSString *const WMFLanguageVariantAlertsLibraryVersion = @"WMFLanguageVariantAlertsLibraryVersion";
|
|
|
|
@interface WMFAppViewController () <UITabBarControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate, WMFThemeable, WMFWorkerControllerDelegate, WMFThemeableNavigationControllerDelegate, WMFAppTabBarDelegate>
|
|
|
|
@property (nonatomic, strong) WMFPeriodicWorkerController *periodicWorkerController;
|
|
@property (nonatomic, strong) WMFBackgroundFetcherController *backgroundFetcherController;
|
|
@property (nonatomic, strong) WMFReachabilityNotifier *reachabilityNotifier;
|
|
|
|
@property (nonatomic, strong) WMFViewControllerTransitionsController *transitionsController;
|
|
|
|
@property (nonatomic, strong) WMFSettingsViewController *settingsViewController;
|
|
@property (nonatomic, strong, readonly) ExploreViewController *exploreViewController;
|
|
@property (nonatomic, strong, readonly) SearchViewController *searchViewController;
|
|
@property (nonatomic, strong, readonly) WMFSavedViewController *savedViewController;
|
|
@property (nonatomic, strong, readonly) WMFPlacesViewController *placesViewController;
|
|
@property (nonatomic, strong, readonly) WMFHistoryViewController *recentArticlesViewController;
|
|
|
|
@property (nonatomic, strong) WMFSavedArticlesFetcher *savedArticlesFetcher;
|
|
|
|
@property (nonatomic, strong) WMFMobileViewToMobileHTMLMigrationController *mobileViewToMobileHTMLMigrationController;
|
|
|
|
@property (nonatomic, strong, readwrite) MWKDataStore *dataStore;
|
|
|
|
@property (nonatomic) BOOL isPresentingOnboarding;
|
|
|
|
@property (nonatomic, strong) NSUserActivity *unprocessedUserActivity;
|
|
@property (nonatomic, strong) UIApplicationShortcutItem *unprocessedShortcutItem;
|
|
|
|
@property (nonatomic, strong) NSMutableDictionary *backgroundTasks;
|
|
|
|
@property (nonatomic, strong) WMFNotificationsController *notificationsController;
|
|
|
|
@property (nonatomic, getter=isWaitingToResumeApp) BOOL waitingToResumeApp;
|
|
@property (nonatomic, getter=isMigrationComplete) BOOL migrationComplete;
|
|
@property (nonatomic, getter=isMigrationActive) BOOL migrationActive;
|
|
@property (nonatomic, getter=isResumeComplete) BOOL resumeComplete; // app has fully loaded & login was attempted
|
|
|
|
@property (nonatomic, getter=isCheckingRemoteConfig) BOOL checkingRemoteConfig;
|
|
|
|
@property (nonatomic, copy) NSDictionary *notificationUserInfoToShow;
|
|
|
|
@property (nonatomic, strong) WMFTheme *theme;
|
|
|
|
@property (nonatomic, strong) UINavigationController *settingsNavigationController;
|
|
|
|
@property (nonatomic, strong, readwrite) WMFReadingListsAlertController *readingListsAlertController;
|
|
|
|
@property (nonatomic, strong, readwrite) NSDate *syncStartDate;
|
|
|
|
@property (nonatomic, strong) SavedTabBarItemProgressBadgeManager *savedTabBarItemProgressBadgeManager;
|
|
|
|
@property (nonatomic) BOOL hasSyncErrorBeenShownThisSesssion;
|
|
|
|
@property (nonatomic, strong) WMFReadingListHintController *readingListHintController;
|
|
@property (nonatomic, strong) WMFEditHintController *editHintController;
|
|
|
|
@property (nonatomic, strong) WMFNavigationStateController *navigationStateController;
|
|
@property (nonatomic, strong) WMFTalkPageReplyHintController *talkPageReplyHintController;
|
|
@property (nonatomic, strong) WMFTalkPageTopicHintController *talkPageTopicHintController;
|
|
|
|
@property (nonatomic, strong) WMFConfiguration *configuration;
|
|
@property (nonatomic, strong) WMFViewControllerRouter *router;
|
|
|
|
@end
|
|
|
|
@implementation WMFAppViewController
|
|
@synthesize exploreViewController = _exploreViewController;
|
|
@synthesize searchViewController = _searchViewController;
|
|
@synthesize savedViewController = _savedViewController;
|
|
@synthesize recentArticlesViewController = _recentArticlesViewController;
|
|
@synthesize placesViewController = _placesViewController;
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:[WMFUserDefaultsKey defaultTabType]];
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self];
|
|
}
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
self.configuration = [WMFConfiguration current];
|
|
self.router = [[WMFViewControllerRouter alloc] initWithAppViewController:self router:self.configuration.router];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
self.theme = [[NSUserDefaults standardUserDefaults] themeCompatibleWith:self.traitCollection];
|
|
|
|
self.backgroundTasks = [NSMutableDictionary dictionaryWithCapacity:5];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(navigateToActivityNotification:)
|
|
name:WMFNavigateToActivityNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(userDidChangeTheme:)
|
|
name:WMFReadingThemesControlsViewController.WMFUserDidSelectThemeNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(articleFontSizeWasUpdated:)
|
|
name:WMFFontSizeSliderViewController.WMFArticleFontSizeUpdatedNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(entriesLimitReachedWithNotification:)
|
|
name:[ReadingList entriesLimitReachedNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(readingListsWereSplitNotification:)
|
|
name:[WMFReadingListsController readingListsWereSplitNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(readingListsServerDidConfirmSyncWasEnabledForAccountWithNotification:)
|
|
name:[WMFReadingListsController readingListsServerDidConfirmSyncWasEnabledForAccountNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(syncDidStartNotification:)
|
|
name:[WMFReadingListsController syncDidStartNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(syncDidFinishNotification:)
|
|
name:[WMFReadingListsController syncDidFinishNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(conflictingReadingListNameUpdatedNotification:)
|
|
name:[ReadingList conflictingReadingListNameUpdatedNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(articleSaveToDiskDidFail:)
|
|
name:[WMFSavedArticlesFetcher saveToDiskDidFail]
|
|
object:nil];
|
|
|
|
[[NSUserDefaults standardUserDefaults] addObserver:self
|
|
forKeyPath:[WMFUserDefaultsKey defaultTabType]
|
|
options:NSKeyValueObservingOptionNew
|
|
context:&kvo_NSUserDefaults_defaultTabType];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(exploreFeedPreferencesDidChange:)
|
|
name:WMFExploreFeedPreferencesDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(userWasLoggedOut:)
|
|
name:[WMFAuthenticationManager didLogOutNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(userWasLoggedIn:)
|
|
name:[WMFAuthenticationManager didLogInNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleExploreCenterBadgeNeedsUpdateNotification)
|
|
name:NSNotification.notificationsCenterBadgeNeedsUpdate
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleNotificationsCenterContextDidSave)
|
|
name:NSNotification.notificationsCenterContextDidSave
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(editWasPublished:)
|
|
name:[WMFSectionEditorViewController editWasPublished]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(descriptionEditWasPublished:)
|
|
name:[DescriptionEditViewController didPublishNotification]
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(talkPageReplyWasPublished:)
|
|
name:WMFTalkPageContainerViewController.WMFReplyPublishedNotificationName
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(talkPageTopicWasPublished:)
|
|
name:WMFTalkPageContainerViewController.WMFTopicPublishedNotificationName
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(referenceLinkTapped:)
|
|
name:WMFReferenceLinkTappedNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(voiceOverStatusDidChange)
|
|
name:UIAccessibilityVoiceOverStatusDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(showErrorBanner:)
|
|
name:NSNotification.showErrorBanner
|
|
object:nil];
|
|
|
|
[self setupReadingListsHelpers];
|
|
self.editHintController = [[WMFEditHintController alloc] init];
|
|
self.talkPageReplyHintController = [[WMFTalkPageReplyHintController alloc] init];
|
|
self.talkPageTopicHintController = [[WMFTalkPageTopicHintController alloc] init];
|
|
|
|
self.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeGeneric;
|
|
}
|
|
|
|
- (UIStatusBarStyle)preferredStatusBarStyle {
|
|
return self.theme.preferredStatusBarStyle;
|
|
}
|
|
|
|
- (UIViewController *)childViewControllerForStatusBarStyle {
|
|
return nil;
|
|
}
|
|
|
|
- (BOOL)prefersStatusBarHidden {
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isPresentingOnboarding {
|
|
return [self.presentedViewController isKindOfClass:[WMFWelcomeInitialViewController class]];
|
|
}
|
|
|
|
- (BOOL)uiIsLoaded {
|
|
return self.viewControllers.count > 0;
|
|
}
|
|
|
|
- (NSURL *)siteURL {
|
|
return self.dataStore.primarySiteURL;
|
|
}
|
|
|
|
#pragma mark - Setup
|
|
|
|
- (void)setupControllers {
|
|
self.periodicWorkerController = [[WMFPeriodicWorkerController alloc] initWithInterval:30 initialDelay:1 leeway:15];
|
|
self.periodicWorkerController.delegate = self;
|
|
[self.periodicWorkerController add:self.dataStore.readingListsController];
|
|
[self.periodicWorkerController add:[WMFEventLoggingService sharedInstance]];
|
|
[self.periodicWorkerController add:[WMFMetricsClientBridge sharedInstance]];
|
|
|
|
self.backgroundFetcherController = [[WMFBackgroundFetcherController alloc] init];
|
|
self.backgroundFetcherController.delegate = self;
|
|
[self.backgroundFetcherController add:self.dataStore.readingListsController];
|
|
[self.backgroundFetcherController add:(id<WMFBackgroundFetcher>)self.dataStore.feedContentController];
|
|
[self.backgroundFetcherController add:[WMFEventLoggingService sharedInstance]];
|
|
[self.backgroundFetcherController add:[WMFMetricsClientBridge sharedInstance]];
|
|
}
|
|
|
|
- (void)loadMainUI {
|
|
if ([self uiIsLoaded]) {
|
|
return;
|
|
}
|
|
|
|
[self configureTabController];
|
|
|
|
self.tabBar.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
|
|
|
|
[self applyTheme:self.theme];
|
|
|
|
self.transitionsController = [WMFViewControllerTransitionsController new];
|
|
|
|
self.recentArticlesViewController.dataStore = self.dataStore;
|
|
[self.searchViewController applyTheme:self.theme];
|
|
[self.settingsViewController applyTheme:self.theme];
|
|
|
|
UITabBarItem *savedTabBarItem = [self.savedViewController tabBarItem];
|
|
self.savedTabBarItemProgressBadgeManager = [[SavedTabBarItemProgressBadgeManager alloc] initWithTabBarItem:savedTabBarItem];
|
|
}
|
|
|
|
- (void)configureTabController {
|
|
self.delegate = self;
|
|
|
|
UIViewController *mainViewController = nil;
|
|
|
|
switch ([NSUserDefaults standardUserDefaults].defaultTabType) {
|
|
case WMFAppDefaultTabTypeSettings:
|
|
mainViewController = self.settingsViewController;
|
|
break;
|
|
default:
|
|
mainViewController = self.exploreViewController;
|
|
break;
|
|
}
|
|
|
|
NSArray<UIViewController *> *viewControllers = @[mainViewController, [self placesViewController], [self savedViewController], [self recentArticlesViewController], [self searchViewController]];
|
|
|
|
[self setViewControllers:viewControllers animated:NO];
|
|
|
|
BOOL shouldOpenAppOnSearchTab = [NSUserDefaults standardUserDefaults].wmf_openAppOnSearchTab;
|
|
if (shouldOpenAppOnSearchTab && self.selectedIndex != WMFAppTabTypeSearch) {
|
|
[self setSelectedIndex:WMFAppTabTypeSearch];
|
|
[[self searchViewController] makeSearchBarBecomeFirstResponder];
|
|
} else if (self.selectedIndex != WMFAppTabTypeMain) {
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
}
|
|
}
|
|
|
|
- (void)setupReadingListsHelpers {
|
|
self.readingListsAlertController = [[WMFReadingListsAlertController alloc] init];
|
|
self.readingListHintController = [[WMFReadingListHintController alloc] initWithDataStore:self.dataStore];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDidSaveOrUnsaveArticle:) name:WMFReadingListsController.userDidSaveOrUnsaveArticleNotification object:nil];
|
|
}
|
|
|
|
- (void)userDidSaveOrUnsaveArticle:(NSNotification *)note {
|
|
WMFAssertMainThread(@"User save/unsave article notification should only be posted on the main thread");
|
|
id maybeArticle = [note object];
|
|
if (![maybeArticle isKindOfClass:[WMFArticle class]]) {
|
|
return;
|
|
}
|
|
[self showReadingListHintForArticle:(WMFArticle *)maybeArticle];
|
|
}
|
|
|
|
#pragma mark - Notifications
|
|
|
|
- (void)appWillEnterForegroundWithNotification:(NSNotification *)note {
|
|
// Don't access anything that can't be accessed in the background without starting a background task. For example, don't use anything in the shared app container like all of the Core Data persistent stores
|
|
self.unprocessedUserActivity = nil;
|
|
self.unprocessedShortcutItem = nil;
|
|
}
|
|
|
|
// When the user launches from a terminated state, resume might not finish before didBecomeActive, so these tasks are held until both items complete
|
|
- (void)performTasksThatShouldOccurAfterBecomeActiveAndResume {
|
|
[[SessionsFunnel shared] logSessionStart];
|
|
[self checkRemoteAppConfigIfNecessary];
|
|
[self.periodicWorkerController start];
|
|
[self.savedArticlesFetcher start];
|
|
[self.mobileViewToMobileHTMLMigrationController start];
|
|
}
|
|
|
|
- (void)performTasksThatShouldOccurAfterAnnouncementsUpdated {
|
|
if (self.isResumeComplete) {
|
|
[UserHistoryFunnel.shared logSnapshot];
|
|
}
|
|
}
|
|
|
|
- (void)appDidBecomeActiveWithNotification:(NSNotification *)note {
|
|
// Retry migration if it was terminated by a background task ending
|
|
[self migrateIfNecessary];
|
|
|
|
if (![self uiIsLoaded]) {
|
|
return;
|
|
}
|
|
|
|
if ([self visibleViewController] == self.exploreViewController) {
|
|
self.exploreViewController.isGranularUpdatingEnabled = YES;
|
|
}
|
|
|
|
if (self.isResumeComplete) {
|
|
[self performTasksThatShouldOccurAfterBecomeActiveAndResume];
|
|
[UserHistoryFunnel.shared logSnapshot];
|
|
}
|
|
}
|
|
|
|
- (void)appWillResignActiveWithNotification:(NSNotification *)note {
|
|
if (![self uiIsLoaded]) {
|
|
return;
|
|
}
|
|
|
|
self.exploreViewController.isGranularUpdatingEnabled = NO;
|
|
|
|
[self.navigationStateController saveNavigationStateFor:self.navigationController
|
|
in:self.dataStore.viewContext];
|
|
NSError *saveError = nil;
|
|
if (![self.dataStore save:&saveError]) {
|
|
DDLogError(@"Error saving dataStore: %@", saveError);
|
|
}
|
|
}
|
|
|
|
- (void)appDidEnterBackgroundWithNotification:(NSNotification *)note {
|
|
if (![self uiIsLoaded]) {
|
|
return;
|
|
}
|
|
[self startPauseAppBackgroundTask];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self pauseApp];
|
|
});
|
|
}
|
|
|
|
- (void)preferredLanguagesDidChange:(NSNotification *)note {
|
|
[self updateExploreFeedPreferencesIfNecessaryForChange:note];
|
|
[self.dataStore.feedContentController updateContentSources];
|
|
}
|
|
|
|
/**
|
|
Updates explore feed preferences if new preferred language was appeneded or removed.
|
|
*/
|
|
- (void)updateExploreFeedPreferencesIfNecessaryForChange:(NSNotification *)note {
|
|
if (self.isPresentingOnboarding) {
|
|
return;
|
|
}
|
|
|
|
NSNumber *changeTypeValue = (NSNumber *)[note userInfo][WMFPreferredLanguagesChangeTypeKey];
|
|
WMFPreferredLanguagesChangeType changeType = (WMFPreferredLanguagesChangeType)changeTypeValue.integerValue;
|
|
if (!changeType || (changeType == WMFPreferredLanguagesChangeTypeReorder)) {
|
|
return;
|
|
}
|
|
|
|
MWKLanguageLink *changedLanguage = (MWKLanguageLink *)[note userInfo][WMFPreferredLanguagesLastChangedLanguageKey];
|
|
BOOL appendedNewPreferredLanguage = (changeType == WMFPreferredLanguagesChangeTypeAdd);
|
|
[self.dataStore.feedContentController toggleContentForSiteURL:changedLanguage.siteURL isOn:appendedNewPreferredLanguage waitForCallbackFromCoordinator:NO updateFeed:NO];
|
|
}
|
|
|
|
- (void)readingListsWereSplitNotification:(NSNotification *)note {
|
|
NSInteger entryLimit = [note.userInfo[WMFReadingListsController.readingListsWereSplitNotificationEntryLimitKey] integerValue];
|
|
[[WMFAlertManager sharedInstance] showWarningAlert:[NSString localizedStringWithFormat:WMFLocalizedStringWithDefaultValue(@"reading-lists-split-notification", nil, nil, @"There is a limit of %1$d articles per reading list. Existing lists with more than this limit have been split into multiple lists.", @"Alert message informing user that existing lists exceeding the entry limit have been split into multiple lists. %1$d will be replaced with the maximum number of articles allowed per reading list."), entryLimit] sticky:YES dismissPreviousAlerts:YES tapCallBack:nil];
|
|
}
|
|
|
|
- (void)readingListsServerDidConfirmSyncWasEnabledForAccountWithNotification:(NSNotification *)note {
|
|
BOOL wasSyncEnabledForAccount = [note.userInfo[WMFReadingListsController.readingListsServerDidConfirmSyncWasEnabledForAccountWasSyncEnabledKey] boolValue];
|
|
BOOL wasSyncEnabledOnDevice = [note.userInfo[WMFReadingListsController.readingListsServerDidConfirmSyncWasEnabledForAccountWasSyncEnabledOnDeviceKey] boolValue];
|
|
BOOL wasSyncDisabledOnDevice = [note.userInfo[WMFReadingListsController.readingListsServerDidConfirmSyncWasEnabledForAccountWasSyncDisabledOnDeviceKey] boolValue];
|
|
if (wasSyncEnabledForAccount) {
|
|
[self wmf_showSyncEnabledPanelOncePerLoginWithTheme:self.theme wasSyncEnabledOnDevice:wasSyncEnabledOnDevice];
|
|
} else if (!wasSyncDisabledOnDevice) {
|
|
[self wmf_showEnableReadingListSyncPanelWithTheme:self.theme
|
|
oncePerLogin:true
|
|
didNotPresentPanelCompletion:^{
|
|
[self wmf_showSyncDisabledPanelWithTheme:self.theme wasSyncEnabledOnDevice:wasSyncEnabledOnDevice];
|
|
}
|
|
dismissHandler:nil];
|
|
}
|
|
}
|
|
|
|
- (void)syncDidStartNotification:(NSNotification *)note {
|
|
self.syncStartDate = [NSDate date];
|
|
}
|
|
|
|
- (void)syncDidFinishNotification:(NSNotification *)note {
|
|
NSError *error = (NSError *)note.userInfo[WMFReadingListsController.syncDidFinishErrorKey];
|
|
|
|
// Reminder: kind of class is checked here because `syncDidFinishErrorKey` is sometimes set to a `WMF.ReadingListError` error type which doesn't bridge to Obj-C (causing the call to `wmf_isNetworkConnectionError` to crash).
|
|
if ([error isKindOfClass:[NSError class]] && error.wmf_isNetworkConnectionError) {
|
|
if (!self.hasSyncErrorBeenShownThisSesssion) {
|
|
self.hasSyncErrorBeenShownThisSesssion = YES; // only show sync error once for multiple failed syncs
|
|
[[WMFAlertManager sharedInstance] showWarningAlert:WMFLocalizedStringWithDefaultValue(@"reading-lists-sync-error-no-internet-connection", nil, nil, @"Syncing will resume when internet connection is available", @"Alert message informing user that syncing will resume when internet connection is available.")
|
|
sticky:YES
|
|
dismissPreviousAlerts:NO
|
|
tapCallBack:nil];
|
|
}
|
|
}
|
|
|
|
if (!error) {
|
|
self.hasSyncErrorBeenShownThisSesssion = NO; // reset on successful sync
|
|
if ([[NSDate date] timeIntervalSinceDate:self.syncStartDate] >= 5) {
|
|
NSInteger syncedReadingListsCount = [note.userInfo[WMFReadingListsController.syncDidFinishSyncedReadingListsCountKey] integerValue];
|
|
NSInteger syncedReadingListEntriesCount = [note.userInfo[WMFReadingListsController.syncDidFinishSyncedReadingListEntriesCountKey] integerValue];
|
|
if (syncedReadingListsCount > 0 && syncedReadingListEntriesCount > 0) {
|
|
NSString *alertTitle = [NSString stringWithFormat:WMFLocalizedStringWithDefaultValue(@"reading-lists-large-sync-completed", nil, nil, @"{{PLURAL:%1$d|%1$d article|%1$d articles}} and {{PLURAL:%2$d|%2$d reading list|%2$d reading lists}} synced from your account", @"Alert message informing user that large sync was completed. %1$d will be replaced with the number of articles which were synced and %2$d will be replaced with the number of reading lists which were synced"), syncedReadingListEntriesCount, syncedReadingListsCount];
|
|
[[WMFAlertManager sharedInstance] showSuccessAlert:alertTitle sticky:YES dismissPreviousAlerts:YES tapCallBack:nil];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)conflictingReadingListNameUpdatedNotification:(NSNotification *)note {
|
|
NSString *oldName = (NSString *)note.userInfo[ReadingList.conflictingReadingListNameUpdatedOldNameKey];
|
|
NSString *newName = (NSString *)note.userInfo[ReadingList.conflictingReadingListNameUpdatedNewNameKey];
|
|
NSString *alertTitle = [NSString stringWithFormat:WMFLocalizedStringWithDefaultValue(@"reading-lists-conflicting-reading-list-name-updated", nil, nil, @"Your list '%1$@' has been renamed to '%2$@'", @"Alert message informing user that their reading list was renamed. %1$@ will be replaced the previous name of the list. %2$@ will be replaced with the new name of the list."), oldName, newName];
|
|
[[WMFAlertManager sharedInstance] showWarningAlert:alertTitle
|
|
sticky:YES
|
|
dismissPreviousAlerts:YES
|
|
tapCallBack:nil];
|
|
}
|
|
|
|
- (void)exploreFeedPreferencesDidChange:(NSNotification *)note {
|
|
ExploreFeedPreferencesUpdateCoordinator *exploreFeedPreferencesUpdateCoordinator = (ExploreFeedPreferencesUpdateCoordinator *)note.object;
|
|
[exploreFeedPreferencesUpdateCoordinator coordinateUpdateFrom:self];
|
|
}
|
|
|
|
- (void)voiceOverStatusDidChange {
|
|
[self.exploreViewController updateNavigationBarVisibility];
|
|
}
|
|
|
|
- (void)showErrorBanner:(NSNotification *)notification {
|
|
if ([notification.userInfo[NSNotification.showErrorBannerNSErrorKey] isKindOfClass:[NSError class]]) {
|
|
NSError *error = notification.userInfo[NSNotification.showErrorBannerNSErrorKey];
|
|
[[WMFAlertManager sharedInstance] showErrorAlert:error sticky:NO dismissPreviousAlerts:YES tapCallBack:nil];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Explore feed preferences
|
|
|
|
- (void)updateDefaultTab {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
dispatch_block_t update = ^{
|
|
[self setSelectedIndex:WMFAppTabTypeSearch];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
[self configureTabController];
|
|
};
|
|
if (self.presentedViewController) {
|
|
[self.presentedViewController dismissViewControllerAnimated:YES completion:update];
|
|
} else {
|
|
update();
|
|
}
|
|
});
|
|
}
|
|
|
|
#pragma mark - Hint
|
|
|
|
- (void)showReadingListHintForArticle:(WMFArticle *)article {
|
|
UIViewController<WMFHintPresenting> *visibleHintPresentingViewController = [self visibleHintPresentingViewController];
|
|
if (!visibleHintPresentingViewController) {
|
|
return;
|
|
}
|
|
[self toggleHint:self.readingListHintController
|
|
context:@{WMFReadingListHintController.ContextArticleKey: article}];
|
|
}
|
|
|
|
- (void)editWasPublished:(NSNotification *)note {
|
|
if (![NSUserDefaults.standardUserDefaults wmf_didShowFirstEditPublishedPanel]) {
|
|
return;
|
|
}
|
|
[self toggleHint:self.editHintController
|
|
context:nil];
|
|
}
|
|
|
|
- (void)descriptionEditWasPublished:(NSNotification *)note {
|
|
if (![NSUserDefaults.standardUserDefaults didShowDescriptionPublishedPanel]) {
|
|
return;
|
|
}
|
|
[self toggleHint:self.editHintController
|
|
context:nil];
|
|
}
|
|
|
|
- (void)talkPageReplyWasPublished:(NSNotification *)note {
|
|
[self toggleHint:self.talkPageReplyHintController context:nil];
|
|
}
|
|
|
|
- (void)talkPageTopicWasPublished:(NSNotification *)note {
|
|
[self toggleHint:self.talkPageTopicHintController context:nil];
|
|
}
|
|
|
|
- (void)referenceLinkTapped:(NSNotification *)note {
|
|
id maybeURL = [note object];
|
|
if (![maybeURL isKindOfClass:[NSURL class]]) {
|
|
return;
|
|
}
|
|
[self wmf_navigateToURL:maybeURL];
|
|
}
|
|
|
|
- (void)toggleHint:(HintController *)hintController context:(nullable NSDictionary<NSString *, id> *)context {
|
|
UIViewController<WMFHintPresenting> *visibleHintPresentingViewController = [self visibleHintPresentingViewController];
|
|
if (!visibleHintPresentingViewController) {
|
|
return;
|
|
}
|
|
[hintController toggleWithPresenter:visibleHintPresentingViewController
|
|
context:context
|
|
theme:self.theme];
|
|
}
|
|
|
|
- (UIViewController *)visibleViewController {
|
|
UIViewController *visibleViewController = self.navigationController.visibleViewController;
|
|
if (visibleViewController == self) {
|
|
return self.selectedViewController;
|
|
}
|
|
return visibleViewController;
|
|
}
|
|
|
|
- (UIViewController<WMFHintPresenting> *)visibleHintPresentingViewController {
|
|
UIViewController *visibleViewController = [self visibleViewController];
|
|
if (![visibleViewController conformsToProtocol:@protocol(WMFHintPresenting)]) {
|
|
return nil;
|
|
}
|
|
return (UIViewController<WMFHintPresenting> *)visibleViewController;
|
|
}
|
|
|
|
#pragma mark - Background Fetch
|
|
|
|
- (void)performBackgroundFetchWithCompletion:(void (^)(UIBackgroundFetchResult))completion {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (!self.isMigrationComplete || !self.backgroundFetcherController) {
|
|
completion(UIBackgroundFetchResultNoData);
|
|
return;
|
|
}
|
|
|
|
[self.backgroundFetcherController performBackgroundFetch:completion];
|
|
});
|
|
}
|
|
|
|
#pragma mark - Background Processing
|
|
|
|
- (void)performDatabaseHousekeepingWithCompletion:(void (^)(NSError *))completion {
|
|
|
|
WMFDatabaseHousekeeper *housekeeper = [WMFDatabaseHousekeeper new];
|
|
|
|
NSError *housekeepingError = nil;
|
|
[housekeeper performHousekeepingOnManagedObjectContext:self.dataStore.viewContext navigationStateController:self.navigationStateController error:&housekeepingError];
|
|
if (housekeepingError) {
|
|
DDLogError(@"Error on cleanup: %@", housekeepingError);
|
|
housekeepingError = nil;
|
|
}
|
|
|
|
/// Housekeeping for the new talk page cache
|
|
[SharedContainerCacheHousekeeping deleteStaleCachedItemsIn:SharedContainerCacheCommonNames.talkPageCache];
|
|
|
|
completion(housekeepingError);
|
|
}
|
|
|
|
#pragma mark - Background Tasks
|
|
|
|
- (UIBackgroundTaskIdentifier)backgroundTaskIdentifierForKey:(NSString *)key {
|
|
if (!key) {
|
|
return UIBackgroundTaskInvalid;
|
|
}
|
|
@synchronized(self.backgroundTasks) {
|
|
NSNumber *identifierNumber = self.backgroundTasks[key];
|
|
if (!identifierNumber) {
|
|
return UIBackgroundTaskInvalid;
|
|
}
|
|
return [identifierNumber unsignedIntegerValue];
|
|
}
|
|
}
|
|
|
|
- (void)setBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)identifier forKey:(NSString *)key {
|
|
if (!key) {
|
|
return;
|
|
}
|
|
@synchronized(self.backgroundTasks) {
|
|
if (identifier == UIBackgroundTaskInvalid) {
|
|
[self.backgroundTasks removeObjectForKey:key];
|
|
return;
|
|
}
|
|
self.backgroundTasks[key] = @(identifier);
|
|
}
|
|
}
|
|
|
|
- (UIBackgroundTaskIdentifier)pauseAppBackgroundTaskIdentifier {
|
|
return [self backgroundTaskIdentifierForKey:@"pauseApp"];
|
|
}
|
|
|
|
- (void)setPauseAppBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)identifier {
|
|
[self setBackgroundTaskIdentifier:identifier forKey:@"pauseApp"];
|
|
}
|
|
|
|
- (UIBackgroundTaskIdentifier)migrationBackgroundTaskIdentifier {
|
|
return [self backgroundTaskIdentifierForKey:@"migration"];
|
|
}
|
|
|
|
- (void)setMigrationBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)identifier {
|
|
[self setBackgroundTaskIdentifier:identifier forKey:@"migration"];
|
|
}
|
|
|
|
- (UIBackgroundTaskIdentifier)feedContentFetchBackgroundTaskIdentifier {
|
|
return [self backgroundTaskIdentifierForKey:@"feed"];
|
|
}
|
|
|
|
- (void)setFeedContentFetchBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)identifier {
|
|
[self setBackgroundTaskIdentifier:identifier forKey:@"feed"];
|
|
}
|
|
|
|
- (UIBackgroundTaskIdentifier)remoteConfigCheckBackgroundTaskIdentifier {
|
|
return [self backgroundTaskIdentifierForKey:@"remoteConfigCheck"];
|
|
}
|
|
|
|
- (void)setRemoteConfigCheckBackgroundTaskIdentifier:(UIBackgroundTaskIdentifier)identifier {
|
|
[self setBackgroundTaskIdentifier:identifier forKey:@"remoteConfigCheck"];
|
|
}
|
|
|
|
- (void)startRemoteConfigCheckBackgroundTask:(dispatch_block_t)expirationHandler {
|
|
if (self.remoteConfigCheckBackgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
self.remoteConfigCheckBackgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
|
if (expirationHandler) {
|
|
expirationHandler();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)endRemoteConfigCheckBackgroundTask {
|
|
if (self.remoteConfigCheckBackgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
UIBackgroundTaskIdentifier backgroundTaskToStop = self.remoteConfigCheckBackgroundTaskIdentifier;
|
|
self.remoteConfigCheckBackgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
|
[[UIApplication sharedApplication] endBackgroundTask:backgroundTaskToStop];
|
|
}
|
|
|
|
- (void)startPauseAppBackgroundTask {
|
|
if (self.pauseAppBackgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
self.pauseAppBackgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
|
[self endPauseAppBackgroundTask];
|
|
}];
|
|
}
|
|
|
|
- (void)endPauseAppBackgroundTask {
|
|
if (self.pauseAppBackgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
UIBackgroundTaskIdentifier backgroundTaskToStop = self.pauseAppBackgroundTaskIdentifier;
|
|
self.pauseAppBackgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
|
[[UIApplication sharedApplication] endBackgroundTask:backgroundTaskToStop];
|
|
}
|
|
|
|
- (void)startMigrationBackgroundTask:(dispatch_block_t)expirationHandler {
|
|
if (self.migrationBackgroundTaskIdentifier != UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
self.migrationBackgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
|
if (expirationHandler) {
|
|
expirationHandler();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)endMigrationBackgroundTask {
|
|
if (self.migrationBackgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
|
|
UIBackgroundTaskIdentifier backgroundTaskToStop = self.migrationBackgroundTaskIdentifier;
|
|
self.migrationBackgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
|
[[UIApplication sharedApplication] endBackgroundTask:backgroundTaskToStop];
|
|
}
|
|
|
|
- (void)feedContentControllerBusyStateDidChange:(NSNotification *)note {
|
|
if ([note object] != self.dataStore.feedContentController) {
|
|
return;
|
|
}
|
|
|
|
UIBackgroundTaskIdentifier currentTaskIdentifier = self.feedContentFetchBackgroundTaskIdentifier;
|
|
if (self.dataStore.feedContentController.isBusy && currentTaskIdentifier == UIBackgroundTaskInvalid) {
|
|
self.feedContentFetchBackgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"com.wikipedia.background.task.feed.content"
|
|
expirationHandler:^{
|
|
[self.dataStore.feedContentController cancelAllFetches];
|
|
}];
|
|
} else if (!self.dataStore.feedContentController.isBusy && currentTaskIdentifier != UIBackgroundTaskInvalid) {
|
|
self.feedContentFetchBackgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
|
[[UIApplication sharedApplication] endBackgroundTask:currentTaskIdentifier];
|
|
}
|
|
}
|
|
#pragma mark - Launch
|
|
|
|
- (void)launchAppInWindow:(UIWindow *)window waitToResumeApp:(BOOL)waitToResumeApp {
|
|
self.waitingToResumeApp = waitToResumeApp;
|
|
|
|
WMFRootNavigationController *articleNavigationController = [[WMFRootNavigationController alloc] initWithRootViewController:self];
|
|
articleNavigationController.themeableNavigationControllerDelegate = self;
|
|
articleNavigationController.delegate = self;
|
|
articleNavigationController.interactivePopGestureRecognizer.delegate = self;
|
|
articleNavigationController.extendedLayoutIncludesOpaqueBars = YES;
|
|
[articleNavigationController setNavigationBarHidden:YES animated:NO];
|
|
[window setRootViewController:articleNavigationController];
|
|
[window makeKeyAndVisible];
|
|
[articleNavigationController applyTheme:self.theme];
|
|
[self updateUserInterfaceStyleOfViewControllerForCurrentTheme:articleNavigationController];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForegroundWithNotification:) name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActiveWithNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActiveWithNotification:) name:UIApplicationWillResignActiveNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackgroundWithNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedContentControllerBusyStateDidChange:) name:WMFExploreFeedContentControllerBusyStateDidChange object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredLanguagesDidChange:) name:WMFPreferredLanguagesDidChangeNotification object:nil];
|
|
|
|
[self showSplashView];
|
|
|
|
[self migrateIfNecessary];
|
|
}
|
|
|
|
- (void)migrateIfNecessary {
|
|
if (self.isMigrationComplete || self.isMigrationActive) {
|
|
return;
|
|
}
|
|
|
|
__block BOOL migrationsAllowed = YES;
|
|
[self startMigrationBackgroundTask:^{
|
|
migrationsAllowed = NO;
|
|
}];
|
|
|
|
// TODO: pass the cancellationChecker into performLibraryUpdates to allow it to bail early if the background task is ended
|
|
// dispatch_block_t bail = ^{
|
|
// [self endMigrationBackgroundTask];
|
|
// self.migrationActive = NO;
|
|
// };
|
|
// BOOL (^cancellationChecker)() = ^BOOL() {
|
|
// return migrationsAllowed;
|
|
// };
|
|
|
|
self.migrationActive = YES;
|
|
|
|
[self.dataStore
|
|
performLibraryUpdates:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self.migrationComplete = YES;
|
|
self.migrationActive = NO;
|
|
[self endMigrationBackgroundTask];
|
|
[self checkRemoteAppConfigIfNecessary];
|
|
[self setupControllers];
|
|
if (!self.isWaitingToResumeApp) {
|
|
[self resumeApp:NULL];
|
|
}
|
|
});
|
|
}
|
|
needsMigrateBlock:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[(WMFRootNavigationController *)self.navigationController triggerMigratingAnimation];
|
|
});
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Start/Pause/Resume App
|
|
|
|
- (void)hideSplashScreenAndResumeApp {
|
|
self.waitingToResumeApp = NO;
|
|
if (self.isMigrationComplete) {
|
|
[self resumeApp:NULL];
|
|
}
|
|
}
|
|
|
|
// resumeApp: should be called once and only once for every launch from a fully terminated state.
|
|
// It should only be called when the app is active and being shown to the user
|
|
- (void)resumeApp:(dispatch_block_t)completion {
|
|
[self presentOnboardingIfNeededWithCompletion:^(BOOL didShowOnboarding) {
|
|
[self loadMainUI];
|
|
dispatch_block_t done = ^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self presentLanguageVariantAlertsWithCompletion:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self finishResumingApp];
|
|
|
|
if (completion) {
|
|
completion();
|
|
}
|
|
});
|
|
}];
|
|
});
|
|
};
|
|
|
|
if (self.notificationUserInfoToShow) {
|
|
[self hideSplashViewAnimated:!didShowOnboarding];
|
|
[self showNotificationCenterForNotificationInfo:self.notificationUserInfoToShow];
|
|
self.notificationUserInfoToShow = nil;
|
|
done();
|
|
} else if (self.unprocessedUserActivity) {
|
|
[self processUserActivity:self.unprocessedUserActivity
|
|
animated:NO
|
|
completion:^{
|
|
[self hideSplashViewAnimated:!didShowOnboarding];
|
|
done();
|
|
}];
|
|
} else if (self.unprocessedShortcutItem) {
|
|
[self hideSplashViewAnimated:!didShowOnboarding];
|
|
[self processShortcutItem:self.unprocessedShortcutItem
|
|
completion:^(BOOL didProcess) {
|
|
done();
|
|
}];
|
|
} else if (NSUserDefaults.standardUserDefaults.shouldRestoreNavigationStackOnResume) {
|
|
[self.navigationStateController restoreNavigationStateFor:self.navigationController
|
|
in:self.dataStore.viewContext
|
|
with:self.theme
|
|
completion:^{
|
|
[self hideSplashViewAnimated:!didShowOnboarding];
|
|
done();
|
|
}];
|
|
} else if ([self shouldShowExploreScreenOnLaunch]) {
|
|
[self hideSplashViewAnimated:!didShowOnboarding];
|
|
[self showExplore];
|
|
done();
|
|
} else {
|
|
[self hideSplashViewAnimated:true];
|
|
done();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)finishResumingApp {
|
|
|
|
[[WMFDailyStatsLoggingFunnel shared] logAppNumberOfDaysSinceInstall];
|
|
|
|
WMFTaskGroup *resumeAndAnnouncementsCompleteGroup = [WMFTaskGroup new];
|
|
[resumeAndAnnouncementsCompleteGroup enter];
|
|
[self.dataStore.authenticationManager
|
|
attemptLoginWithCompletion:^{
|
|
[self checkRemoteAppConfigIfNecessary];
|
|
if (!self.reachabilityNotifier) {
|
|
@weakify(self);
|
|
self.reachabilityNotifier = [[WMFReachabilityNotifier alloc] initWithHost:WMFConfiguration.current.defaultSiteDomain
|
|
callback:^(BOOL isReachable, SCNetworkReachabilityFlags flags) {
|
|
@strongify(self);
|
|
@weakify(self);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@strongify(self);
|
|
if (isReachable) {
|
|
[self.savedArticlesFetcher start];
|
|
} else {
|
|
[self.savedArticlesFetcher stop];
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
self.resumeComplete = YES;
|
|
[resumeAndAnnouncementsCompleteGroup leave];
|
|
[self performTasksThatShouldOccurAfterBecomeActiveAndResume];
|
|
[self showLoggedOutPanelIfNeeded];
|
|
}];
|
|
|
|
[self.dataStore.feedContentController startContentSources];
|
|
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSDate *feedRefreshDate = [defaults wmf_feedRefreshDate];
|
|
NSDate *now = [NSDate date];
|
|
|
|
BOOL locationAuthorized = [LocationManagerFactory coarseLocationManager].isAuthorized;
|
|
if (!feedRefreshDate || [now timeIntervalSinceDate:feedRefreshDate] > [self timeBeforeRefreshingExploreFeed] || [[NSCalendar wmf_gregorianCalendar] wmf_daysFromDate:feedRefreshDate toDate:now] > 0) {
|
|
[resumeAndAnnouncementsCompleteGroup enter];
|
|
[self.exploreViewController updateFeedSourcesWithDate:nil
|
|
userInitiated:NO
|
|
completion:^{
|
|
[resumeAndAnnouncementsCompleteGroup leave];
|
|
}];
|
|
} else {
|
|
if (locationAuthorized != [defaults wmf_locationAuthorized]) {
|
|
[self.dataStore.feedContentController updateContentSource:[WMFNearbyContentSource class] force:NO completion:NULL];
|
|
}
|
|
// TODO: If full navigation stack is not restored (so we're past the cutoff date), should we still force Continue reading card to appear?
|
|
if (!NSUserDefaults.standardUserDefaults.shouldRestoreNavigationStackOnResume) {
|
|
[self.dataStore.feedContentController updateContentSource:[WMFContinueReadingContentSource class] force:YES completion:NULL];
|
|
}
|
|
|
|
[resumeAndAnnouncementsCompleteGroup enter];
|
|
[self.dataStore.feedContentController updateContentSource:[WMFAnnouncementsContentSource class]
|
|
force:YES
|
|
completion:^{
|
|
[resumeAndAnnouncementsCompleteGroup leave];
|
|
}];
|
|
}
|
|
|
|
[resumeAndAnnouncementsCompleteGroup waitInBackgroundWithCompletion:^{
|
|
[self performTasksThatShouldOccurAfterAnnouncementsUpdated];
|
|
}];
|
|
|
|
[defaults wmf_setLocationAuthorized:locationAuthorized];
|
|
|
|
[self.savedArticlesFetcher start];
|
|
|
|
#if DEBUG && WMF_SHOW_ALL_ALERTS
|
|
[[WMFAlertManager sharedInstance] showErrorAlert:[NSError errorWithDomain:@"WMFTestDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"There was an error"}]
|
|
sticky:YES
|
|
dismissPreviousAlerts:NO
|
|
tapCallBack:^{
|
|
[[WMFAlertManager sharedInstance] showWarningAlert:@"You have been warned about a thing that has a long explanation of why you were warned. You have been warned about a thing that has a long explanation of why you were warned."
|
|
sticky:YES
|
|
dismissPreviousAlerts:NO
|
|
tapCallBack:^{
|
|
[[WMFAlertManager sharedInstance] showSuccessAlert:@"You are successful"
|
|
sticky:YES
|
|
dismissPreviousAlerts:NO
|
|
tapCallBack:^{
|
|
[[WMFAlertManager sharedInstance] showAlert:@"You have been notified" sticky:YES dismissPreviousAlerts:NO tapCallBack:NULL];
|
|
}];
|
|
}];
|
|
}];
|
|
#endif
|
|
}
|
|
|
|
- (NSTimeInterval)timeBeforeRefreshingExploreFeed {
|
|
NSTimeInterval timeInterval = 2 * 60 * 60;
|
|
NSString *key = [WMFFeedDayResponse WMFFeedDayResponseMaxAgeKey];
|
|
NSNumber *value = [self.dataStore.viewContext wmf_numberValueForKey:key];
|
|
if (value) {
|
|
timeInterval = [value doubleValue];
|
|
}
|
|
return timeInterval;
|
|
}
|
|
|
|
- (void)pauseApp {
|
|
[self logSessionEnd];
|
|
|
|
if (![self uiIsLoaded]) {
|
|
[self endPauseAppBackgroundTask];
|
|
return;
|
|
}
|
|
|
|
[[NSUserDefaults standardUserDefaults] wmf_setDidShowSyncDisabledPanel:NO];
|
|
|
|
[self.reachabilityNotifier stop];
|
|
[self.periodicWorkerController stop];
|
|
[self.savedArticlesFetcher stop];
|
|
|
|
// Show all navigation bars so that users will always see search when they re-open the app
|
|
NSArray<UINavigationController *> *allNavControllers = [self allNavigationControllers];
|
|
for (UINavigationController *navC in allNavControllers) {
|
|
UIViewController *vc = [navC visibleViewController];
|
|
if ([vc respondsToSelector:@selector(ensureWikipediaSearchIsShowing)]) {
|
|
[(id)vc ensureWikipediaSearchIsShowing];
|
|
}
|
|
}
|
|
|
|
[self.dataStore.feedContentController stopContentSources];
|
|
[self.dataStore clearMemoryCache];
|
|
|
|
[self endPauseAppBackgroundTask];
|
|
}
|
|
|
|
#pragma mark - Memory Warning
|
|
|
|
- (void)didReceiveMemoryWarning {
|
|
if (![self uiIsLoaded]) {
|
|
return;
|
|
}
|
|
[super didReceiveMemoryWarning];
|
|
self.settingsViewController = nil;
|
|
[self.dataStore clearMemoryCache];
|
|
}
|
|
|
|
#pragma mark - Logging
|
|
|
|
- (void)logSessionEnd {
|
|
[[SessionsFunnel shared] logSessionEnd];
|
|
[[UserHistoryFunnel shared] logSnapshot];
|
|
}
|
|
|
|
#pragma mark - Shortcut
|
|
|
|
- (BOOL)canProcessShortcutItem:(UIApplicationShortcutItem *)item {
|
|
if (!item) {
|
|
return NO;
|
|
}
|
|
if ([item.type isEqualToString:WMFIconShortcutTypeSearch]) {
|
|
return YES;
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeRandom]) {
|
|
return YES;
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeNearby]) {
|
|
return YES;
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeContinueReading]) {
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (void)processShortcutItem:(UIApplicationShortcutItem *)item completion:(void (^)(BOOL))completion {
|
|
if (![self canProcessShortcutItem:item]) {
|
|
if (completion) {
|
|
completion(NO);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (![self uiIsLoaded]) {
|
|
self.unprocessedShortcutItem = item;
|
|
if (completion) {
|
|
completion(YES);
|
|
}
|
|
return;
|
|
}
|
|
self.unprocessedShortcutItem = nil;
|
|
|
|
if ([item.type isEqualToString:WMFIconShortcutTypeSearch]) {
|
|
if (self.visibleArticleViewController) {
|
|
[self showSearchInCurrentNavigationController];
|
|
} else {
|
|
[self switchToSearchAnimated:NO];
|
|
[self.searchViewController makeSearchBarBecomeFirstResponder];
|
|
}
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeRandom]) {
|
|
[self showRandomArticleAnimated:NO];
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeNearby]) {
|
|
[self showNearbyAnimated:NO];
|
|
} else if ([item.type isEqualToString:WMFIconShortcutTypeContinueReading]) {
|
|
[self showLastReadArticleAnimated:NO];
|
|
}
|
|
if (completion) {
|
|
completion(YES);
|
|
}
|
|
}
|
|
|
|
#pragma mark - NSUserActivity
|
|
|
|
- (BOOL)canProcessUserActivity:(NSUserActivity *)activity {
|
|
if (!activity) {
|
|
return NO;
|
|
}
|
|
switch ([activity wmf_type]) {
|
|
case WMFUserActivityTypeExplore:
|
|
case WMFUserActivityTypePlaces:
|
|
case WMFUserActivityTypeSavedPages:
|
|
case WMFUserActivityTypeHistory:
|
|
case WMFUserActivityTypeSearch:
|
|
case WMFUserActivityTypeSettings:
|
|
case WMFUserActivityTypeAppearanceSettings:
|
|
case WMFUserActivityTypeNotificationSettings:
|
|
case WMFUserActivityTypeContent:
|
|
return YES;
|
|
case WMFUserActivityTypeSearchResults:
|
|
return [activity wmf_searchTerm] != nil;
|
|
case WMFUserActivityTypeLink:
|
|
return [activity wmf_linkURL] != nil;
|
|
default:
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (void)navigateToActivityNotification:(NSNotification *)note {
|
|
id object = [note object];
|
|
if ([object isKindOfClass:[NSUserActivity class]]) {
|
|
[self processUserActivity:object
|
|
animated:YES
|
|
completion:^{
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (BOOL)processUserActivity:(NSUserActivity *)activity animated:(BOOL)animated completion:(dispatch_block_t)done {
|
|
if (![self canProcessUserActivity:activity]) {
|
|
done();
|
|
return NO;
|
|
}
|
|
if (![self uiIsLoaded] || self.isWaitingToResumeApp) {
|
|
self.unprocessedUserActivity = activity;
|
|
done();
|
|
return YES;
|
|
}
|
|
self.unprocessedUserActivity = nil;
|
|
|
|
WMFUserActivityType type = [activity wmf_type];
|
|
|
|
switch (type) {
|
|
case WMFUserActivityTypeExplore:
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
[self.navigationController popToRootViewControllerAnimated:animated];
|
|
break;
|
|
case WMFUserActivityTypePlaces: {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypePlaces];
|
|
[self.navigationController popToRootViewControllerAnimated:animated];
|
|
NSURL *articleURL = activity.wmf_linkURL;
|
|
CLLocation *locationFromURL = activity.wmf_locationFromURL;
|
|
if (articleURL || locationFromURL) {
|
|
// For "View on a map" action to succeed, view mode has to be set to map.
|
|
[[self placesViewController] updateViewModeToMap];
|
|
if (locationFromURL) {
|
|
[[self placesViewController] centerMapOnLocation:locationFromURL];
|
|
}
|
|
else if (articleURL) {
|
|
[[self placesViewController] showArticleURL:articleURL];
|
|
}
|
|
}
|
|
} break;
|
|
case WMFUserActivityTypeContent: {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
UINavigationController *navController = self.navigationController;
|
|
[navController popToRootViewControllerAnimated:animated];
|
|
NSURL *url = [activity wmf_contentURL];
|
|
WMFContentGroup *group = [self.dataStore.viewContext contentGroupForURL:url];
|
|
if (group) {
|
|
switch (group.detailType) {
|
|
case WMFFeedDisplayTypePhoto: {
|
|
UIViewController *vc = [group detailViewControllerForPreviewItemAtIndex:0 dataStore:self.dataStore theme:self.theme];
|
|
[self.navigationController presentViewController:vc animated:false completion:nil];
|
|
}
|
|
default: {
|
|
UIViewController *vc = [group detailViewControllerWithDataStore:self.dataStore theme:self.theme];
|
|
if (vc) {
|
|
[navController pushViewController:vc animated:animated];
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
[self.exploreViewController updateFeedSourcesWithDate:nil
|
|
userInitiated:NO
|
|
completion:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
WMFContentGroup *group = [self.dataStore.viewContext contentGroupForURL:url];
|
|
if (group) {
|
|
UIViewController *vc = [group detailViewControllerWithDataStore:self.dataStore theme:self.theme];
|
|
if (vc) {
|
|
[navController pushViewController:vc animated:NO];
|
|
}
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
} break;
|
|
case WMFUserActivityTypeSavedPages:
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeSaved];
|
|
[self.navigationController popToRootViewControllerAnimated:animated];
|
|
break;
|
|
case WMFUserActivityTypeHistory:
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeRecent];
|
|
[self.navigationController popToRootViewControllerAnimated:animated];
|
|
break;
|
|
case WMFUserActivityTypeSearch:
|
|
[self showSearchInCurrentNavigationController];
|
|
break;
|
|
case WMFUserActivityTypeSearchResults:
|
|
[self dismissPresentedViewControllers];
|
|
[self.searchViewController searchAndMakeResultsVisibleForSearchTerm:[activity wmf_searchTerm] animated:animated];
|
|
[self switchToSearchAnimated:animated];
|
|
break;
|
|
case WMFUserActivityTypeSettings:
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
[self showSettingsAnimated:animated];
|
|
break;
|
|
case WMFUserActivityTypeAppearanceSettings: {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
WMFAppearanceSettingsViewController *appearanceSettingsVC = [[WMFAppearanceSettingsViewController alloc] init];
|
|
[appearanceSettingsVC applyTheme:self.theme];
|
|
[self showSettingsWithSubViewController:appearanceSettingsVC animated:animated];
|
|
} break;
|
|
case WMFUserActivityTypeNotificationSettings: {
|
|
WMFPushNotificationsSettingsViewController *pushNotificationsVC = [[WMFPushNotificationsSettingsViewController alloc] initWithAuthenticationManager:self.dataStore.authenticationManager notificationsController:self.notificationsController];
|
|
[pushNotificationsVC applyTheme:self.theme];
|
|
[self dismissPresentedViewControllers];
|
|
switch ([NSUserDefaults standardUserDefaults].defaultTabType) {
|
|
case WMFAppDefaultTabTypeExplore: {
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
[self.navigationController popToRootViewControllerAnimated:YES];
|
|
[self showSettingsWithSubViewController:pushNotificationsVC animated:animated];
|
|
} break;
|
|
case WMFAppDefaultTabTypeSettings: {
|
|
[self.navigationController popToRootViewControllerAnimated:YES];
|
|
[self.navigationController pushViewController:pushNotificationsVC animated:YES];
|
|
} break;
|
|
}
|
|
} break;
|
|
default: {
|
|
NSURL *linkURL = [activity wmf_linkURL];
|
|
// Ensure incoming link is fetched in user's preferred variant if applicable
|
|
if (!linkURL.wmf_languageVariantCode) {
|
|
linkURL.wmf_languageVariantCode = [self.dataStore.languageLinkController preferredLanguageVariantCodeForLanguageCode:linkURL.wmf_languageCode];
|
|
}
|
|
if (!linkURL) {
|
|
done();
|
|
return NO;
|
|
}
|
|
[NSUserActivity wmf_makeActivityActive:activity];
|
|
return [self.router routeURL:linkURL userInfo:activity.userInfo completion:done];
|
|
}
|
|
}
|
|
done();
|
|
[NSUserActivity wmf_makeActivityActive:activity];
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Utilities
|
|
|
|
- (WMFArticleViewController *)showArticleWithURL:(NSURL *)articleURL animated:(BOOL)animated {
|
|
return [self showArticleWithURL:articleURL
|
|
animated:animated
|
|
completion:^{
|
|
}];
|
|
}
|
|
|
|
- (WMFArticleViewController *)showArticleWithURL:(NSURL *)articleURL animated:(BOOL)animated completion:(nonnull dispatch_block_t)completion {
|
|
if (!articleURL.wmf_title) {
|
|
completion();
|
|
return nil;
|
|
}
|
|
WMFArticleViewController *visibleArticleViewController = self.visibleArticleViewController;
|
|
WMFInMemoryURLKey *visibleKey = visibleArticleViewController.articleURL.wmf_inMemoryKey;
|
|
WMFInMemoryURLKey *articleKey = articleURL.wmf_inMemoryKey;
|
|
if (visibleKey && articleKey && [visibleKey isEqualToInMemoryURLKey:articleKey]) {
|
|
if (articleURL.fragment) {
|
|
[visibleArticleViewController showAnchor:articleURL.fragment];
|
|
}
|
|
completion();
|
|
return visibleArticleViewController;
|
|
}
|
|
|
|
UINavigationController *nc = [self currentNavigationController];
|
|
if (!nc) {
|
|
completion();
|
|
return nil;
|
|
}
|
|
|
|
if (nc.presentedViewController) {
|
|
[nc dismissViewControllerAnimated:NO completion:NULL];
|
|
}
|
|
|
|
WMFArticleViewController *articleVC = [[WMFArticleViewController alloc] initWithArticleURL:articleURL dataStore:self.dataStore theme:self.theme schemeHandler:nil];
|
|
articleVC.loadCompletion = completion;
|
|
|
|
#if DEBUG
|
|
if ([[[NSProcessInfo processInfo] environment] objectForKey:@"DYLD_PRINT_STATISTICS"]) {
|
|
NSDate *start = [NSDate date];
|
|
|
|
articleVC.initialSetupCompletion = ^{
|
|
NSDate *end = [NSDate date];
|
|
NSTimeInterval articleLoadTime = [end timeIntervalSinceDate:start];
|
|
DDLogInfo(@"article load time = %f", articleLoadTime);
|
|
};
|
|
}
|
|
#endif
|
|
|
|
[nc pushViewController:articleVC
|
|
animated:YES];
|
|
return articleVC;
|
|
}
|
|
|
|
- (void)swiftCompatibleShowArticleWithURL:(NSURL *)articleURL animated:(BOOL)animated completion:(nonnull dispatch_block_t)completion {
|
|
[self showArticleWithURL:articleURL animated:animated completion:completion];
|
|
}
|
|
|
|
- (BOOL)shouldShowExploreScreenOnLaunch {
|
|
BOOL shouldOpenAppOnSearchTab = [NSUserDefaults standardUserDefaults].wmf_openAppOnSearchTab;
|
|
if (shouldOpenAppOnSearchTab) {
|
|
return NO;
|
|
}
|
|
|
|
NSDate *resignActiveDate = [[NSUserDefaults standardUserDefaults] wmf_appResignActiveDate];
|
|
if (!resignActiveDate) {
|
|
return NO;
|
|
}
|
|
|
|
if (fabs([resignActiveDate timeIntervalSinceNow]) >= WMFTimeBeforeShowingExploreScreenOnLaunch) {
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)mainViewControllerIsDisplayingContent {
|
|
return self.navigationController.viewControllers.count > 1;
|
|
}
|
|
|
|
- (WMFArticleViewController *)visibleArticleViewController {
|
|
UINavigationController *navVC = self.navigationController;
|
|
UIViewController *topVC = navVC.topViewController;
|
|
if ([topVC isKindOfClass:[WMFArticleViewController class]]) {
|
|
return (WMFArticleViewController *)topVC;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (UIViewController *)viewControllerForTab:(WMFAppTabType)tab {
|
|
return self.viewControllers[tab];
|
|
}
|
|
|
|
#pragma mark - Accessors
|
|
|
|
- (WMFSavedArticlesFetcher *)savedArticlesFetcher {
|
|
if (![self uiIsLoaded]) {
|
|
return nil;
|
|
}
|
|
if (!_savedArticlesFetcher) {
|
|
_savedArticlesFetcher = [[WMFSavedArticlesFetcher alloc] initWithDataStore:self.dataStore];
|
|
[_savedArticlesFetcher addObserver:self forKeyPath:WMF_SAFE_KEYPATH(_savedArticlesFetcher, progress) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:&kvo_SavedArticlesFetcher_progress];
|
|
}
|
|
return _savedArticlesFetcher;
|
|
}
|
|
|
|
- (WMFMobileViewToMobileHTMLMigrationController *)mobileViewToMobileHTMLMigrationController {
|
|
if (![self uiIsLoaded]) {
|
|
return nil;
|
|
}
|
|
if (!_mobileViewToMobileHTMLMigrationController) {
|
|
_mobileViewToMobileHTMLMigrationController = [[WMFMobileViewToMobileHTMLMigrationController alloc] initWithDataStore:self.dataStore];
|
|
}
|
|
return _mobileViewToMobileHTMLMigrationController;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
if (context == &kvo_SavedArticlesFetcher_progress) {
|
|
[ProgressContainer shared].articleFetcherProgress = _savedArticlesFetcher.progress;
|
|
} else if (context == &kvo_NSUserDefaults_defaultTabType) {
|
|
[self updateDefaultTab];
|
|
} else {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
}
|
|
}
|
|
|
|
- (WMFNotificationsController *)notificationsController {
|
|
WMFNotificationsController *controller = self.dataStore.notificationsController;
|
|
return controller;
|
|
}
|
|
|
|
- (MWKDataStore *)dataStore {
|
|
return MWKDataStore.shared;
|
|
}
|
|
|
|
- (WMFNavigationStateController *)navigationStateController {
|
|
if (!_navigationStateController) {
|
|
_navigationStateController = [[WMFNavigationStateController alloc] initWithDataStore:self.dataStore];
|
|
}
|
|
return _navigationStateController;
|
|
}
|
|
|
|
- (ExploreViewController *)exploreViewController {
|
|
if (!_exploreViewController) {
|
|
_exploreViewController = [[ExploreViewController alloc] init];
|
|
_exploreViewController.dataStore = self.dataStore;
|
|
_exploreViewController.notificationsCenterPresentationDelegate = self;
|
|
_exploreViewController.settingsPresentationDelegate = self;
|
|
_exploreViewController.tabBarItem.image = [UIImage imageNamed:@"tabbar-explore"];
|
|
_exploreViewController.title = [WMFCommonStrings exploreTabTitle];
|
|
[_exploreViewController applyTheme:self.theme];
|
|
}
|
|
return _exploreViewController;
|
|
}
|
|
|
|
- (void)handleExploreCenterBadgeNeedsUpdateNotification {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self.exploreViewController updateNotificationsCenterButton];
|
|
[self.settingsViewController configureBarButtonItems];
|
|
});
|
|
}
|
|
|
|
- (void)handleNotificationsCenterContextDidSave {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
UIApplication.sharedApplication.applicationIconBadgeNumber = [[self.dataStore.remoteNotificationsController numberOfUnreadNotificationsAndReturnError:nil] integerValue];
|
|
[self.dataStore.remoteNotificationsController updateCacheWithCurrentUnreadNotificationsCountAndReturnError:nil];
|
|
});
|
|
}
|
|
|
|
- (SearchViewController *)searchViewController {
|
|
if (!_searchViewController) {
|
|
_searchViewController = [[SearchViewController alloc] init];
|
|
[_searchViewController applyTheme:self.theme];
|
|
_searchViewController.dataStore = self.dataStore;
|
|
_searchViewController.tabBarItem.image = [UIImage imageNamed:@"search"];
|
|
_searchViewController.title = [WMFCommonStrings searchTitle];
|
|
}
|
|
return _searchViewController;
|
|
}
|
|
|
|
- (WMFSavedViewController *)savedViewController {
|
|
if (!_savedViewController) {
|
|
_savedViewController = [[UIStoryboard storyboardWithName:@"Saved" bundle:nil] instantiateInitialViewController];
|
|
[_savedViewController applyTheme:self.theme];
|
|
_savedViewController.dataStore = self.dataStore;
|
|
_savedViewController.tabBarDelegate = self;
|
|
_savedViewController.tabBarItem.image = [UIImage imageNamed:@"tabbar-save"];
|
|
_savedViewController.title = [WMFCommonStrings savedTabTitle];
|
|
}
|
|
return _savedViewController;
|
|
}
|
|
|
|
- (WMFHistoryViewController *)recentArticlesViewController {
|
|
if (!_recentArticlesViewController) {
|
|
_recentArticlesViewController = [[WMFHistoryViewController alloc] init];
|
|
[_recentArticlesViewController applyTheme:self.theme];
|
|
_recentArticlesViewController.dataStore = self.dataStore;
|
|
_recentArticlesViewController.tabBarItem.image = [UIImage imageNamed:@"tabbar-recent"];
|
|
_recentArticlesViewController.title = [WMFCommonStrings historyTabTitle];
|
|
}
|
|
return _recentArticlesViewController;
|
|
}
|
|
|
|
- (WMFPlacesViewController *)placesViewController {
|
|
if (!_placesViewController) {
|
|
_placesViewController = [[UIStoryboard storyboardWithName:@"Places" bundle:nil] instantiateInitialViewController];
|
|
_placesViewController.dataStore = self.dataStore;
|
|
[_placesViewController applyTheme:self.theme];
|
|
_placesViewController.tabBarItem.image = [UIImage imageNamed:@"tabbar-nearby"];
|
|
_placesViewController.title = [WMFCommonStrings placesTabTitle];
|
|
}
|
|
return _placesViewController;
|
|
}
|
|
|
|
#pragma mark - Onboarding
|
|
|
|
static NSString *const WMFDidShowOnboarding = @"DidShowOnboarding5.3";
|
|
|
|
- (BOOL)shouldShowOnboarding {
|
|
|
|
if (self.unprocessedUserActivity.shouldSkipOnboarding) {
|
|
[self setDidShowOnboarding];
|
|
return NO;
|
|
}
|
|
|
|
NSNumber *didShow = [[NSUserDefaults standardUserDefaults] objectForKey:WMFDidShowOnboarding];
|
|
return !didShow.boolValue;
|
|
}
|
|
|
|
- (void)setDidShowOnboarding {
|
|
[[NSUserDefaults standardUserDefaults] setObject:@YES forKey:WMFDidShowOnboarding];
|
|
|
|
// If the user is onboarding, variant info alerts do not need to be presented
|
|
// So, set the user default to the current library version
|
|
[[NSUserDefaults standardUserDefaults] setInteger:MWKDataStore.currentLibraryVersion forKey:WMFLanguageVariantAlertsLibraryVersion];
|
|
}
|
|
|
|
- (void)presentOnboardingIfNeededWithCompletion:(void (^)(BOOL didShowOnboarding))completion {
|
|
if ([self shouldShowOnboarding]) {
|
|
WMFWelcomeInitialViewController *vc = [WMFWelcomeInitialViewController wmf_viewControllerFromWelcomeStoryboard];
|
|
[vc applyTheme:self.theme];
|
|
vc.completionBlock = ^{
|
|
[self setDidShowOnboarding];
|
|
if (completion) {
|
|
completion(YES);
|
|
}
|
|
};
|
|
vc.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
|
[self presentViewController:vc animated:NO completion:NULL];
|
|
} else {
|
|
if (completion) {
|
|
completion(NO);
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Splash
|
|
|
|
- (void)showSplashView {
|
|
[(WMFRootNavigationController *)self.navigationController showSplashView];
|
|
}
|
|
|
|
- (void)hideSplashViewAnimated:(BOOL)animated {
|
|
[(WMFRootNavigationController *)self.navigationController hideSplashViewAnimated:animated];
|
|
}
|
|
|
|
#pragma mark - Explore VC
|
|
|
|
- (void)showExplore {
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
}
|
|
|
|
#pragma mark - Last Read Article
|
|
|
|
- (void)showLastReadArticleAnimated:(BOOL)animated {
|
|
NSURL *lastRead = [self.dataStore.viewContext wmf_openArticleURL];
|
|
[self showArticleWithURL:lastRead animated:animated];
|
|
}
|
|
|
|
#pragma mark - Show Search
|
|
|
|
- (void)switchToSearchAnimated:(BOOL)animated {
|
|
[self dismissPresentedViewControllers];
|
|
if (self.selectedIndex != WMFAppTabTypeSearch) {
|
|
[self setSelectedIndex:WMFAppTabTypeSearch];
|
|
}
|
|
[self.navigationController popToRootViewControllerAnimated:animated];
|
|
}
|
|
|
|
#pragma mark - App Shortcuts
|
|
|
|
- (void)dismissPresentedViewControllers {
|
|
if (self.presentedViewController) {
|
|
[self dismissViewControllerAnimated:NO completion:NULL];
|
|
}
|
|
|
|
if (self.navigationController.presentedViewController) {
|
|
[self.navigationController dismissViewControllerAnimated:NO completion:NULL];
|
|
}
|
|
}
|
|
|
|
- (void)showRandomArticleAnimated:(BOOL)animated {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
UINavigationController *exploreNavController = self.navigationController;
|
|
WMFFirstRandomViewController *vc = [[WMFFirstRandomViewController alloc] initWithSiteURL:[self siteURL] dataStore:self.dataStore theme:self.theme];
|
|
[vc applyTheme:self.theme];
|
|
[exploreNavController pushViewController:vc animated:animated];
|
|
}
|
|
|
|
- (void)showNearbyAnimated:(BOOL)animated {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypePlaces];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
[[self placesViewController] showNearbyArticles];
|
|
}
|
|
|
|
#pragma mark - App config
|
|
|
|
- (void)checkRemoteAppConfigIfNecessary {
|
|
WMFAssertMainThread(@"Remote app config check must start from the main thread");
|
|
if (self.isCheckingRemoteConfig) {
|
|
return;
|
|
}
|
|
self.checkingRemoteConfig = YES;
|
|
CFAbsoluteTime lastCheckTime = (CFAbsoluteTime)[[self.dataStore.viewContext wmf_numberValueForKey:WMFLastRemoteAppConfigCheckAbsoluteTimeKey] doubleValue];
|
|
CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
|
|
BOOL shouldCheckRemoteConfig = now - lastCheckTime >= WMFRemoteAppConfigCheckInterval || self.dataStore.remoteConfigsThatFailedUpdate != 0;
|
|
if (!shouldCheckRemoteConfig) {
|
|
self.checkingRemoteConfig = NO;
|
|
return;
|
|
}
|
|
self.dataStore.isLocalConfigUpdateAllowed = YES;
|
|
[self startRemoteConfigCheckBackgroundTask:^{
|
|
self.dataStore.isLocalConfigUpdateAllowed = NO;
|
|
[self endRemoteConfigCheckBackgroundTask];
|
|
}];
|
|
[self.dataStore updateLocalConfigurationFromRemoteConfigurationWithCompletion:^(NSError *error) {
|
|
if (!error && self.dataStore.isLocalConfigUpdateAllowed) {
|
|
[self.dataStore.viewContext wmf_setValue:[NSNumber numberWithDouble:now] forKey:WMFLastRemoteAppConfigCheckAbsoluteTimeKey];
|
|
}
|
|
self.checkingRemoteConfig = NO;
|
|
[self endRemoteConfigCheckBackgroundTask];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - UITabBarControllerDelegate
|
|
|
|
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
|
|
[self wmf_hideKeyboard];
|
|
}
|
|
|
|
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item {
|
|
[self logTappedTabBarItem:item inTabBar:tabBar];
|
|
}
|
|
|
|
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
|
|
if (viewController == tabBarController.selectedViewController) {
|
|
switch (tabBarController.selectedIndex) {
|
|
case WMFAppTabTypeMain: {
|
|
ExploreViewController *exploreViewController = (ExploreViewController *)[self exploreViewController];
|
|
[exploreViewController scrollToTop];
|
|
} break;
|
|
case WMFAppTabTypeSearch: {
|
|
SearchViewController *searchViewController = (SearchViewController *)[self searchViewController];
|
|
[searchViewController makeSearchBarBecomeFirstResponder];
|
|
} break;
|
|
}
|
|
// Must return NO if already visible to prevent unintended effect when tapping the Search tab bar button multiple times.
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)updateActiveTitleAccessibilityButton:(UIViewController *)viewController {
|
|
if ([viewController isKindOfClass:[ExploreViewController class]]) {
|
|
ExploreViewController *vc = (ExploreViewController *)viewController;
|
|
vc.titleButton.accessibilityLabel = WMFLocalizedStringWithDefaultValue(@"home-title-accessibility-label", nil, nil, @"Wikipedia, scroll to top of Explore", @"Accessibility heading for the Explore page, indicating that tapping it will scroll to the top of the explore page. \"Explore\" is the same as {{msg-wikimedia|Wikipedia-ios-welcome-explore-title}}.");
|
|
} else if ([viewController isKindOfClass:[WMFArticleViewController class]]) {
|
|
WMFArticleViewController *vc = (WMFArticleViewController *)viewController;
|
|
if (self.selectedIndex == WMFAppTabTypeMain) {
|
|
vc.navigationItem.titleView.accessibilityLabel = WMFLocalizedStringWithDefaultValue(@"home-button-explore-accessibility-label", nil, nil, @"Wikipedia, return to Explore", @"Accessibility heading for articles shown within the explore tab, indicating that tapping it will take you back to explore. \"Explore\" is the same as {{msg-wikimedia|Wikipedia-ios-welcome-explore-title}}.");
|
|
} else if (self.selectedIndex == WMFAppTabTypeSaved) {
|
|
vc.navigationItem.titleView.accessibilityLabel = WMFLocalizedStringWithDefaultValue(@"home-button-saved-accessibility-label", nil, nil, @"Wikipedia, return to Saved", @"Accessibility heading for articles shown within the saved articles tab, indicating that tapping it will take you back to the list of saved articles. \"Saved\" is the same as {{msg-wikimedia|Wikipedia-ios-saved-title}}.");
|
|
} else if (self.selectedIndex == WMFAppTabTypeRecent) {
|
|
vc.navigationItem.titleView.accessibilityLabel = WMFLocalizedStringWithDefaultValue(@"home-button-history-accessibility-label", nil, nil, @"Wikipedia, return to History", @"Accessibility heading for articles shown within the history articles tab, indicating that tapping it will take you back to the history list. \"History\" is the same as {{msg-wikimedia|Wikipedia-ios-history-title}}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - UINavigationControllerDelegate
|
|
|
|
- (void)navigationController:(UINavigationController *)navigationController
|
|
willShowViewController:(UIViewController *)viewController
|
|
animated:(BOOL)animated {
|
|
navigationController.interactivePopGestureRecognizer.delegate = self;
|
|
[self updateActiveTitleAccessibilityButton:viewController];
|
|
}
|
|
|
|
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
|
|
return [self.transitionsController navigationController:navigationController interactionControllerForAnimationController:animationController];
|
|
}
|
|
|
|
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
|
|
return [self.transitionsController navigationController:navigationController animationControllerForOperation:operation fromViewController:fromVC toViewController:toVC];
|
|
}
|
|
|
|
#pragma mark - UIGestureRecognizerDelegate
|
|
|
|
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
|
if (self.navigationController.interactivePopGestureRecognizer == gestureRecognizer) {
|
|
return self.navigationController.viewControllers.count > 1;
|
|
} else if (_settingsViewController.navigationController.interactivePopGestureRecognizer == gestureRecognizer) {
|
|
return _settingsViewController.navigationController.viewControllers.count > 1;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
return ![gestureRecognizer isMemberOfClass:[UIScreenEdgePanGestureRecognizer class]];
|
|
}
|
|
|
|
#pragma mark - UNUserNotificationCenterDelegate
|
|
|
|
// The method will be called on the delegate only if the application is in the foreground. If the method is not implemented or the handler is not called in a timely manner then the notification will not be presented. The application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. This decision should be based on whether the information in the notification is otherwise visible to the user.
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
|
|
if ([notification.request.content.threadIdentifier isEqualToString:EchoModelVersion.current]) {
|
|
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.pushNotificationBannerDidDisplayInForeground object:nil userInfo:notification.request.content.userInfo];
|
|
}
|
|
|
|
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
|
|
}
|
|
|
|
// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from applicationDidFinishLaunching:.
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
|
|
NSDictionary *info = response.notification.request.content.userInfo;
|
|
|
|
if ([response.notification.request.content.threadIdentifier isEqualToString:EchoModelVersion.current]) {
|
|
[self showNotificationCenterForNotificationInfo:info];
|
|
}
|
|
|
|
completionHandler();
|
|
}
|
|
|
|
- (void)showNotificationCenterForNotificationInfo:(NSDictionary *)info {
|
|
if (!self.isMigrationComplete) {
|
|
self.notificationUserInfoToShow = info;
|
|
return;
|
|
}
|
|
[self userDidTapPushNotification];
|
|
}
|
|
|
|
#pragma mark - Themeable
|
|
|
|
- (void)applyTheme:(WMFTheme *)theme toNavigationControllers:(NSArray<UINavigationController *> *)navigationControllers {
|
|
NSMutableSet<UINavigationController *> *foundNavigationControllers = [NSMutableSet setWithCapacity:1];
|
|
for (UINavigationController *nc in navigationControllers) {
|
|
for (UIViewController *vc in nc.viewControllers) {
|
|
if (vc != self && [vc conformsToProtocol:@protocol(WMFThemeable)]) {
|
|
[(id<WMFThemeable>)vc applyTheme:theme];
|
|
}
|
|
if ([vc.presentedViewController isKindOfClass:[UINavigationController class]]) {
|
|
[foundNavigationControllers addObject:(UINavigationController *)vc.presentedViewController];
|
|
}
|
|
}
|
|
|
|
if ([nc.presentedViewController isKindOfClass:[UINavigationController class]]) {
|
|
[foundNavigationControllers addObject:(UINavigationController *)nc.presentedViewController];
|
|
}
|
|
|
|
if ([nc conformsToProtocol:@protocol(WMFThemeable)]) {
|
|
[(id<WMFThemeable>)nc applyTheme:theme];
|
|
}
|
|
}
|
|
|
|
[[UITextField appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTextColor:theme.colors.primaryText];
|
|
|
|
if ([foundNavigationControllers count] > 0) {
|
|
[self applyTheme:theme toNavigationControllers:[foundNavigationControllers allObjects]];
|
|
}
|
|
}
|
|
|
|
- (NSArray<UINavigationController *> *)allNavigationControllers {
|
|
// Navigation controllers
|
|
NSMutableArray<UINavigationController *> *navigationControllers = [NSMutableArray array];
|
|
UINavigationController *navC = self.navigationController;
|
|
if (navC) {
|
|
[navigationControllers addObject:navC];
|
|
}
|
|
if (_settingsNavigationController) {
|
|
[navigationControllers addObject:_settingsNavigationController];
|
|
}
|
|
return navigationControllers;
|
|
}
|
|
|
|
- (void)applyTheme:(WMFTheme *)theme toPresentedViewController:(UIViewController *)viewController {
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
if ([viewController conformsToProtocol:@protocol(WMFThemeable)]) {
|
|
[(id<WMFThemeable>)viewController applyTheme:theme];
|
|
}
|
|
|
|
if ([viewController.presentedViewController isKindOfClass:[UINavigationController class]]) {
|
|
UINavigationController *navController = (UINavigationController *)viewController.presentedViewController;
|
|
[self applyTheme:theme toNavigationControllers:@[navController]];
|
|
} else {
|
|
[self applyTheme:theme toPresentedViewController:viewController.presentedViewController];
|
|
}
|
|
}
|
|
|
|
- (void)applyTheme:(WMFTheme *)theme {
|
|
if (theme == nil) {
|
|
return;
|
|
}
|
|
self.theme = theme;
|
|
|
|
self.view.backgroundColor = theme.colors.baseBackground;
|
|
self.view.tintColor = theme.colors.link;
|
|
|
|
[self.settingsViewController applyTheme:theme];
|
|
[self.exploreViewController applyTheme:theme];
|
|
[self.placesViewController applyTheme:theme];
|
|
[self.savedViewController applyTheme:theme];
|
|
[self.recentArticlesViewController applyTheme:theme];
|
|
[self.searchViewController applyTheme:theme];
|
|
|
|
[self applyTheme:theme toPresentedViewController:self.presentedViewController];
|
|
|
|
[[WMFAlertManager sharedInstance] applyTheme:theme];
|
|
|
|
[self applyTheme:theme toNavigationControllers:[self allNavigationControllers]];
|
|
[self.tabBar applyTheme:theme];
|
|
|
|
[[UISwitch appearance] setOnTintColor:theme.colors.accent];
|
|
|
|
[self.readingListHintController applyTheme:self.theme];
|
|
[self.editHintController applyTheme:self.theme];
|
|
|
|
[self setNeedsStatusBarAppearanceUpdate];
|
|
}
|
|
|
|
- (void)updateAppThemeIfNecessary {
|
|
// self.navigationController is the App's root view controller so rely on its trait collection
|
|
WMFTheme *theme = [NSUserDefaults.standardUserDefaults themeCompatibleWith:self.navigationController.traitCollection];
|
|
if (self.theme != theme) {
|
|
[self applyTheme:theme];
|
|
[self.settingsViewController loadSections];
|
|
}
|
|
}
|
|
|
|
- (void)userDidChangeTheme:(NSNotification *)note {
|
|
NSString *themeName = (NSString *)note.userInfo[WMFReadingThemesControlsViewController.WMFUserDidSelectThemeNotificationThemeNameKey];
|
|
NSNumber *isImageDimmingEnabledNumber = (NSNumber *)note.userInfo[WMFReadingThemesControlsViewController.WMFUserDidSelectThemeNotificationIsImageDimmingEnabledKey];
|
|
if (isImageDimmingEnabledNumber) {
|
|
[NSUserDefaults.standardUserDefaults setWmf_isImageDimmingEnabled:isImageDimmingEnabledNumber.boolValue];
|
|
}
|
|
[NSUserDefaults.standardUserDefaults setThemeName:themeName];
|
|
[self updateUserInterfaceStyleOfViewControllerForCurrentTheme:self.navigationController];
|
|
[self updateAppThemeIfNecessary];
|
|
}
|
|
|
|
- (void)updateUserInterfaceStyleOfViewControllerForCurrentTheme:(UIViewController *)viewController {
|
|
NSString *themeName = [NSUserDefaults.standardUserDefaults themeName];
|
|
if ([WMFTheme isDefaultThemeName:themeName]) {
|
|
viewController.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
|
|
} else if ([WMFTheme isDarkThemeName:themeName]) {
|
|
viewController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
|
|
} else {
|
|
viewController.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
|
|
}
|
|
}
|
|
|
|
- (void)debounceTraitCollectionThemeUpdate {
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(updateAppThemeIfNecessary) object:nil];
|
|
[self performSelector:@selector(updateAppThemeIfNecessary) withObject:nil afterDelay:0.3];
|
|
}
|
|
|
|
- (void)themeableNavigationControllerTraitCollectionDidChange:(nonnull WMFThemeableNavigationController *)navigationController {
|
|
[self debounceTraitCollectionThemeUpdate];
|
|
}
|
|
|
|
#pragma mark - WMFWorkerControllerDelegate
|
|
|
|
- (void)workerControllerWillStart:(WMFWorkerController *)workerController workWithIdentifier:(NSString *)identifier {
|
|
NSString *name = [@[NSStringFromClass([workerController class]), identifier] componentsJoinedByString:@"-"];
|
|
UIBackgroundTaskIdentifier backgroundTaskIdentifier = [UIApplication.sharedApplication beginBackgroundTaskWithName:name
|
|
expirationHandler:^{
|
|
DDLogWarn(@"Ending background task with name: %@", name);
|
|
[workerController cancelWorkWithIdentifier:identifier];
|
|
}];
|
|
[self setBackgroundTaskIdentifier:backgroundTaskIdentifier forKey:identifier];
|
|
}
|
|
|
|
- (void)workerControllerDidEnd:(WMFWorkerController *)workerController workWithIdentifier:(NSString *)identifier {
|
|
UIBackgroundTaskIdentifier backgroundTaskIdentifier = [self backgroundTaskIdentifierForKey:identifier];
|
|
if (backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
|
|
return;
|
|
}
|
|
[UIApplication.sharedApplication endBackgroundTask:backgroundTaskIdentifier];
|
|
}
|
|
|
|
#pragma mark - Article save to disk did fail
|
|
|
|
- (void)articleSaveToDiskDidFail:(NSNotification *)note {
|
|
NSError *error = (NSError *)note.userInfo[[WMFSavedArticlesFetcher saveToDiskDidFailErrorKey]];
|
|
if (error.domain == NSCocoaErrorDomain && error.code == NSFileWriteOutOfSpaceError) {
|
|
[[WMFAlertManager sharedInstance] showErrorAlertWithMessage:WMFLocalizedStringWithDefaultValue(@"article-save-error-not-enough-space", nil, nil, @"You do not have enough space on your device to save this article", @"Alert message informing user that article cannot be save due to insufficient storage available")
|
|
sticky:YES
|
|
dismissPreviousAlerts:YES
|
|
tapCallBack:nil];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Appearance
|
|
|
|
- (void)articleFontSizeWasUpdated:(NSNotification *)note {
|
|
NSNumber *multiplier = (NSNumber *)note.userInfo[WMFFontSizeSliderViewController.WMFArticleFontSizeMultiplierKey];
|
|
[[NSUserDefaults standardUserDefaults] wmf_setArticleFontSizeMultiplier:multiplier];
|
|
}
|
|
|
|
#pragma mark - Search
|
|
|
|
- (void)showSearchInCurrentNavigationController {
|
|
[self showSearchInCurrentNavigationControllerAnimated:YES];
|
|
}
|
|
|
|
- (void)dismissReadingThemesPopoverIfActive {
|
|
if ([self.presentedViewController isKindOfClass:[WMFReadingThemesControlsViewController class]]) {
|
|
[self.presentedViewController dismissViewControllerAnimated:YES completion:nil];
|
|
}
|
|
}
|
|
|
|
- (nullable UINavigationController *)currentNavigationController {
|
|
UIViewController *presented = self.presentedViewController;
|
|
while (presented.presentedViewController != nil) {
|
|
presented = presented.presentedViewController;
|
|
}
|
|
|
|
// This next block fixes a weird bug: https://phabricator.wikimedia.org/T305112#7936784
|
|
if ([NSStringFromClass([presented class]) isEqualToString:@"DDParsecCollectionViewController"] && presented.presentingViewController != nil) {
|
|
presented = presented.presentingViewController;
|
|
}
|
|
|
|
if ([presented isKindOfClass:[UINavigationController class]]) {
|
|
return (UINavigationController *)presented;
|
|
} else {
|
|
return self.navigationController;
|
|
}
|
|
}
|
|
|
|
- (void)showSearchInCurrentNavigationControllerAnimated:(BOOL)animated {
|
|
NSParameterAssert(self.dataStore);
|
|
|
|
[self dismissReadingThemesPopoverIfActive];
|
|
|
|
UINavigationController *nc = self.currentNavigationController;
|
|
if (!nc) {
|
|
return;
|
|
}
|
|
|
|
NSArray *vcs = nc.viewControllers;
|
|
NSMutableArray *mutableVCs = [vcs mutableCopy];
|
|
SearchViewController *searchVC = nil;
|
|
NSInteger index = 1;
|
|
NSInteger limit = vcs.count;
|
|
while (index < limit) {
|
|
UIViewController *vc = vcs[index];
|
|
if ([vc isKindOfClass:[SearchViewController class]]) {
|
|
searchVC = (SearchViewController *)vc;
|
|
[mutableVCs removeObjectAtIndex:index];
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
|
|
if (searchVC) {
|
|
[searchVC clear]; // clear search VC before bringing it forward
|
|
[nc setViewControllers:mutableVCs animated:NO];
|
|
} else {
|
|
searchVC = [[SearchViewController alloc] init];
|
|
searchVC.shouldBecomeFirstResponder = YES;
|
|
searchVC.areRecentSearchesEnabled = YES;
|
|
[searchVC applyTheme:self.theme];
|
|
searchVC.dataStore = self.dataStore;
|
|
}
|
|
|
|
[nc pushViewController:searchVC
|
|
animated:true];
|
|
}
|
|
|
|
- (void)showImportedReadingList:(ReadingList *)readingList {
|
|
[self dismissPresentedViewControllers];
|
|
[self setSelectedIndex:WMFAppTabTypeSaved];
|
|
[self.navigationController popToRootViewControllerAnimated:NO];
|
|
[self.savedViewController toggleCurrentView:WMFSavedViewControllerView.readingListsViewRawValue];
|
|
ReadingListDetailViewController *detailVC = [[ReadingListDetailViewController alloc] initFor:readingList with:self.dataStore fromImport:YES theme:self.theme];
|
|
[self.navigationController pushViewController:detailVC animated:YES];
|
|
}
|
|
|
|
- (nonnull WMFSettingsViewController *)settingsViewController {
|
|
if (!_settingsViewController) {
|
|
WMFSettingsViewController *settingsVC =
|
|
[WMFSettingsViewController settingsViewControllerWithDataStore:self.dataStore];
|
|
[settingsVC applyTheme:self.theme];
|
|
_settingsViewController = settingsVC;
|
|
_settingsViewController.notificationsCenterPresentationDelegate = self;
|
|
_settingsViewController.tabBarItem.image = [UIImage imageNamed:@"tabbar-explore"];
|
|
}
|
|
return _settingsViewController;
|
|
}
|
|
|
|
- (nonnull UINavigationController *)settingsNavigationController {
|
|
if (!_settingsNavigationController) {
|
|
WMFThemeableNavigationController *navController = [[WMFThemeableNavigationController alloc] initWithRootViewController:self.settingsViewController theme:self.theme];
|
|
[self applyTheme:self.theme toNavigationControllers:@[navController]];
|
|
_settingsNavigationController = navController;
|
|
_settingsNavigationController.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
|
_settingsNavigationController.interactivePopGestureRecognizer.delegate = self;
|
|
}
|
|
|
|
if (_settingsNavigationController.viewControllers.firstObject != self.settingsViewController) {
|
|
_settingsNavigationController.viewControllers = @[self.settingsViewController];
|
|
}
|
|
return _settingsNavigationController;
|
|
}
|
|
|
|
- (void)showSettingsWithSubViewController:(nullable UIViewController *)subViewController animated:(BOOL)animated {
|
|
NSParameterAssert(self.dataStore);
|
|
[self dismissPresentedViewControllers];
|
|
|
|
if (subViewController) {
|
|
[self.settingsNavigationController pushViewController:subViewController animated:NO];
|
|
}
|
|
|
|
switch ([NSUserDefaults standardUserDefaults].defaultTabType) {
|
|
case WMFAppDefaultTabTypeSettings:
|
|
[self setSelectedIndex:WMFAppTabTypeMain];
|
|
if (subViewController) {
|
|
[self wmf_pushViewController:subViewController animated:animated];
|
|
}
|
|
break;
|
|
default:
|
|
[self presentViewController:self.settingsNavigationController animated:animated completion:nil];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)showSettingsAnimated:(BOOL)animated {
|
|
[self showSettingsWithSubViewController:nil animated:animated];
|
|
}
|
|
|
|
#pragma mark - WMFReadingListsAlertPresenter
|
|
|
|
- (void)entriesLimitReachedWithNotification:(NSNotification *)notification {
|
|
ReadingList *readingList = (ReadingList *)notification.userInfo[ReadingList.entriesLimitReachedReadingListKey];
|
|
if (readingList) {
|
|
[self.readingListsAlertController showLimitHitForDefaultListPanelIfNecessaryWithPresenter:self dataStore:self.dataStore readingList:readingList theme:self.theme];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Remote Notifications
|
|
|
|
- (void)setRemoteNotificationRegistrationStatusWithDeviceToken:(nullable NSData *)deviceToken error:(nullable NSError *)error {
|
|
[self.notificationsController setRemoteNotificationRegistrationStatusWithDeviceToken:deviceToken error:error];
|
|
}
|
|
|
|
#pragma mark - User was logged out
|
|
|
|
- (void)userWasLoggedOut:(NSNotification *)note {
|
|
[self showLoggedOutPanelIfNeeded];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self.exploreViewController updateNotificationsCenterButton];
|
|
[self.settingsViewController configureBarButtonItems];
|
|
UIApplication.sharedApplication.applicationIconBadgeNumber = 0;
|
|
|
|
if (self.isResumeComplete) {
|
|
[self.dataStore.feedContentController updateContentSource:[WMFAnnouncementsContentSource class]
|
|
force:YES
|
|
completion:nil];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)userWasLoggedIn:(NSNotification *)note {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self.exploreViewController updateNotificationsCenterButton];
|
|
[self.settingsViewController configureBarButtonItems];
|
|
|
|
if (self.isResumeComplete) {
|
|
[self.dataStore.feedContentController updateContentSource:[WMFAnnouncementsContentSource class]
|
|
force:YES
|
|
completion:nil];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)showLoggedOutPanelIfNeeded {
|
|
WMFAuthenticationManager *authenticationManager = self.dataStore.authenticationManager;
|
|
BOOL isUserUnawareOfLogout = authenticationManager.isUserUnawareOfLogout;
|
|
if (!isUserUnawareOfLogout) {
|
|
return;
|
|
}
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self wmf_showLoggedOutPanelWithTheme:self.theme
|
|
dismissHandler:^{
|
|
[authenticationManager userDidAcknowledgeUnintentionalLogout];
|
|
}];
|
|
});
|
|
}
|
|
|
|
#pragma mark - Navigation logging
|
|
|
|
- (void)logTappedTabBarItem:(UITabBarItem *)item inTabBar:(UITabBar *)tabBar {
|
|
if (tabBar.items.count != self.viewControllers.count || self.tabBar != tabBar) {
|
|
NSAssert(false, @"Unexpected tab bar setup for logging tap events.");
|
|
return;
|
|
}
|
|
|
|
NSInteger index = [self.tabBar.items indexOfObject:item];
|
|
if (index != NSNotFound) {
|
|
UIViewController *selectedViewController = self.viewControllers[index];
|
|
|
|
if ([selectedViewController isKindOfClass:[ExploreViewController class]] && [NSUserDefaults standardUserDefaults].defaultTabType == WMFAppDefaultTabTypeExplore) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedExplore];
|
|
} else if ([selectedViewController isKindOfClass:[WMFSettingsViewController class]] && [NSUserDefaults standardUserDefaults].defaultTabType == WMFAppDefaultTabTypeSettings) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedSettingsFromTabBar];
|
|
} else if ([selectedViewController isKindOfClass:[WMFPlacesViewController class]]) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedPlaces];
|
|
} else if ([selectedViewController isKindOfClass:[WMFSavedViewController class]]) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedSaved];
|
|
} else if ([selectedViewController isKindOfClass:[WMFHistoryViewController class]]) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedHistory];
|
|
} else if ([selectedViewController isKindOfClass:[SearchViewController class]]) {
|
|
[[WMFNavigationEventsFunnel shared] logTappedSearch];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)logTappedSettingsFromExplore {
|
|
[[WMFNavigationEventsFunnel shared] logTappedSettingsFromExplore];
|
|
}
|
|
|
|
@end
|