diff --git a/.travis.yml b/.travis.yml index 7b7ee7456..3c97d918c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,6 +53,10 @@ matrix: - chmod +x ./android/gradlew - openssl aes-256-cbc -K $encrypted_70d456c9569f_key -iv $encrypted_70d456c9569f_iv -in ./android/app/releaseFiles.tar.enc -out ./android/app/releaseFiles.tar -d - tar xvf ./android/app/releaseFiles.tar -C ./android/app + + # Doing this because travis seemed to mess up when looking for the keystore file + - mkdir -p ./android/app/app + - tar xvf ./android/app/releaseFiles.tar -C ./android/app/app # - security delete-certificate -c "Apple Worldwide Developer Relations Certification Authority" # add this to resolve 'Could not install WWDR certificate' install: - yarn install diff --git a/android/app/build.gradle b/android/app/build.gradle index f5aaaf9d9..a23e051c4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -115,10 +115,11 @@ android { defaultConfig { applicationId "com.everfox.animetrackerandroid" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 27 + missingDimensionStrategy "RNN.reactNativeVersion", "reactNative51" versionCode 1 - versionName "6.8.1" + versionName "6.9.2" multiDexEnabled true ndk { abiFilters "armeabi-v7a", "x86" @@ -132,6 +133,10 @@ android { dexOptions { jumboMode = true } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } splits { abi { reset() @@ -172,6 +177,15 @@ android { } } +configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + def requested = details.requested + if (requested.group == 'com.android.support' && requested.name != 'multidex' && requested.name != 'multidex-instrumentation') { + details.useVersion "27.1.0" + } + } +} + dependencies { compile project(':react-native-sentry') compile project(':react-native-android-fullscreen-webview') @@ -183,11 +197,13 @@ dependencies { compile project(':react-native-fbsdk') compile project(':react-native-vector-icons') compile project(':react-native-code-push') - compile fileTree(dir: "libs", include: ["*.jar"]) - + + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "com.android.support:appcompat-v7:27.1.0" + implementation "com.facebook.react:react-native:+" // From node_modules + implementation project(':react-native-navigation') + compile 'com.android.support:multidex:1.0.1' - compile "com.android.support:appcompat-v7:27+" - compile "com.facebook.react:react-native:+" // From node_modules compile "com.facebook.fresco:animated-gif:1.3.0" compile 'com.facebook.fresco:animated-webp:1.3.0' compile 'com.facebook.fresco:webpsupport:1.3.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b7d093fa3..2c0ad8184 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ - - + @@ -66,8 +66,10 @@ - - --> + + + + getPackages() { - return Arrays.asList( - new MainReactPackage(), - new RNSentryPackage(MainApplication.this), - new CustomWebViewPackage(), - new FastImageViewPackage(), - new PickerPackage(), - new ReactNativeOneSignalPackage(), - new ReactNativeYouTube(), - new LinearGradientPackage(), - new FBSDKPackage(mCallbackManager), - new VectorIconsPackage(), - new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG) - ); - } - }; + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + }; + return new ReactGateway(this, isDebug(), host); + } @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; + public boolean isDebug() { + return BuildConfig.DEBUG; + } + + protected List getPackages() { + // Add additional packages you require here + // No need to add RnnPackage and MainReactPackage + return Arrays.asList( + new RNSentryPackage(MainApplication.this), + new CustomWebViewPackage(), + new FastImageViewPackage(), + new PickerPackage(), + new ReactNativeOneSignalPackage(), + new ReactNativeYouTube(), + new LinearGradientPackage(), + new FBSDKPackage(mCallbackManager), + new VectorIconsPackage(), + new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG) + ); } + @Override - public void onCreate() { - super.onCreate(); - FacebookSdk.sdkInitialize(getApplicationContext()); - // If you want to use AppEventsLogger to log events. - AppEventsLogger.activateApp(this); - SoLoader.init(this, /* native exopackage */ false); + public List createAdditionalReactPackages() { + return getPackages(); } } diff --git a/android/build.gradle b/android/build.gradle index 22a7cfec7..dec8822e8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,13 @@ buildscript { repositories { + google() + mavenLocal() + mavenCentral() jcenter() - maven { - url 'https://maven.google.com/' - name 'Google' - } } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:3.1.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -18,6 +17,8 @@ buildscript { allprojects { repositories { + google() + mavenCentral() mavenLocal() jcenter() maven { @@ -27,10 +28,6 @@ allprojects { // jitpack repo is necessary to fetch ucrop dependency maven { url "https://jitpack.io" } - maven { - url 'https://maven.google.com/' - name 'Google' - } configurations.all { resolutionStrategy { @@ -44,3 +41,18 @@ allprojects { } } } + +subprojects { subproject -> + afterEvaluate { + if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) { + android { + variantFilter { variant -> + def names = variant.flavors*.name + if (names.contains("reactNative56") || names.contains("reactNative55")) { + setIgnore(true) + } + } + } + } + } +} diff --git a/android/gradle.properties b/android/gradle.properties index 1fd964e90..dd6a36a38 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -17,4 +17,9 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +org.gradle.jvmargs=-Xmx1536M + android.useDeprecatedNdk=true + +# Disable incremental resource processing as it broke release build +android.enableAapt2=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index dbdc05d27..b6517bb1d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 3b53882c9..2aaa69dd9 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,5 +19,7 @@ include ':react-native-vector-icons' project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') include ':react-native-code-push' project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app') +include ':react-native-navigation' +project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/lib/android/app/') include ':app' diff --git a/index.js b/index.js index 466cad47d..7cd13124f 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ -import { AppRegistry } from 'react-native'; +import { Navigation } from 'react-native-navigation'; import { Sentry } from 'react-native-sentry'; -import App from './src/App'; +import { registerScreens, Layouts, defaultOptions } from 'kitsu/navigation'; +import OneSignal from 'react-native-onesignal'; Sentry.config('https://068b9ab849bf4485beb4884adcc5be83:8c57373b9bb4410f99ebfd17878c739a@sentry.io/200469'); @@ -9,4 +10,22 @@ if (!__DEV__) { Sentry.install(); } -AppRegistry.registerComponent('kitsu_mobile', () => App); +// Set notification display +OneSignal.inFocusDisplaying(2); + +console.disableYellowBox = true; + +// If you're using the debugging tools for React Native, the network tab is normally useless +// because it shows network activity to load the JS bundle only. This line causes it to +// use the dev tools XMLHttpRequest object if dev tools is running, making the network +// tab useful again. If dev tools isn't running, this will have no effect. +// NOTE: Disable this if you intend to upload files +GLOBAL.XMLHttpRequest = GLOBAL.originalXMLHttpRequest || GLOBAL.XMLHttpRequest; + +registerScreens(); + +Navigation.events().registerAppLaunchedListener(() => { + Navigation.setDefaultOptions(defaultOptions); + Navigation.setRoot(Layouts.INITIAL); +}); + diff --git a/ios/kitsu_mobile.xcodeproj/project.pbxproj b/ios/kitsu_mobile.xcodeproj/project.pbxproj index 575f9cd77..1ec01059e 100644 --- a/ios/kitsu_mobile.xcodeproj/project.pbxproj +++ b/ios/kitsu_mobile.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 248B166920F481E2003FA937 /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 248B166820F481E2003FA937 /* Entypo.ttf */; }; 248B166A20F481EC003FA937 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B609CFAFF3174D9C8F848129 /* Ionicons.ttf */; }; 24940E1320F2E312006A21B7 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24940E1220F2E312006A21B7 /* StoreKit.framework */; }; + 24F5F4F121420721001620BB /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 24F5F4EC214206EC001620BB /* libReactNativeNavigation.a */; }; 288C7B16940D4F52A8F13835 /* Asap-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = FD61A198D0A948A68EDEFB8F /* Asap-Medium.otf */; }; 2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -193,6 +194,20 @@ remoteGlobalIDString = 3400A8081CEB54A6008A0BC7; remoteInfo = imageCropPicker; }; + 24F5F4EB214206EC001620BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 24F5F4B4214206EC001620BB /* ReactNativeNavigation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D; + remoteInfo = ReactNativeNavigation; + }; + 24F5F4ED214206EC001620BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 24F5F4B4214206EC001620BB /* ReactNativeNavigation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 7B49FEBB1E95090800DEB3EA; + remoteInfo = ReactNativeNavigationTests; + }; 24FCE07220E59E71006E6B7B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83B1A0C828034CA594169087 /* RNSentry.xcodeproj */; @@ -515,6 +530,7 @@ 248B166820F481E2003FA937 /* Entypo.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = ""; }; 24940E1220F2E312006A21B7 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 24EE088E743447C4A67C2CB7 /* imageCropPicker.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = imageCropPicker.xcodeproj; path = "../node_modules/react-native-image-crop-picker/ios/imageCropPicker.xcodeproj"; sourceTree = ""; }; + 24F5F4B4214206EC001620BB /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/lib/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; 268CB22608FD87F20FCBC864 /* libPods-kitsu_mobile.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-kitsu_mobile.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2CA52D4C8B0D4B529A71B14A /* RCTWKWebView.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTWKWebView.xcodeproj; path = "../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView.xcodeproj"; sourceTree = ""; }; 2D02E47B1E0B4A5D006451C7 /* kitsu_mobile-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "kitsu_mobile-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -602,6 +618,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 24F5F4F121420721001620BB /* libReactNativeNavigation.a in Frameworks */, 245ADF0920E0E13E00BA9D04 /* libFastImage.a in Frameworks */, BC5EE05A1FEE844D0004423C /* libRCTPushNotification.a in Frameworks */, 0A6D63601F3DCC8C00181D81 /* libRCTCameraRoll.a in Frameworks */, @@ -814,6 +831,15 @@ name = Products; sourceTree = ""; }; + 24F5F4B5214206EC001620BB /* Products */ = { + isa = PBXGroup; + children = ( + 24F5F4EC214206EC001620BB /* libReactNativeNavigation.a */, + 24F5F4EE214206EC001620BB /* ReactNativeNavigationTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; 24FCE06F20E59E71006E6B7B /* Products */ = { isa = PBXGroup; children = ( @@ -929,6 +955,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 24F5F4B4214206EC001620BB /* ReactNativeNavigation.xcodeproj */, BC5EE0241FEE84310004423C /* RCTPushNotification.xcodeproj */, 7B7089AC1F021AF2008C298B /* ART.xcodeproj */, 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, @@ -1298,6 +1325,10 @@ ProductGroup = 146834001AC3E56700842450 /* Products */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; }, + { + ProductGroup = 24F5F4B5214206EC001620BB /* Products */; + ProjectRef = 24F5F4B4214206EC001620BB /* ReactNativeNavigation.xcodeproj */; + }, { ProductGroup = 24FCE06F20E59E71006E6B7B /* Products */; ProjectRef = 83B1A0C828034CA594169087 /* RNSentry.xcodeproj */; @@ -1402,6 +1433,20 @@ remoteRef = 24249D4B203C048C00FBEE6D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 24F5F4EC214206EC001620BB /* libReactNativeNavigation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReactNativeNavigation.a; + remoteRef = 24F5F4EB214206EC001620BB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 24F5F4EE214206EC001620BB /* ReactNativeNavigationTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = ReactNativeNavigationTests.xctest; + remoteRef = 24F5F4ED214206EC001620BB /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 24FCE07320E59E71006E6B7B /* libRNSentry.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/ios/kitsu_mobile/AppDelegate.m b/ios/kitsu_mobile/AppDelegate.m index fd79b46b3..1cdce72ad 100644 --- a/ios/kitsu_mobile/AppDelegate.m +++ b/ios/kitsu_mobile/AppDelegate.m @@ -8,11 +8,11 @@ */ #import "AppDelegate.h" -#import #import #import #import +#import #if __has_include() #import // This is used for versions of react >= 0.40 #else @@ -28,27 +28,10 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - NSURL *jsCodeLocation; + NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + [ReactNativeNavigation bootstrap:jsCodeLocation launchOptions:launchOptions]; - -#ifdef DEBUG - jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; -#else - jsCodeLocation = [CodePush bundleURL]; -#endif -RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation - moduleName:@"kitsu_mobile" - initialProperties:nil - launchOptions:launchOptions]; -[RNSentry installWithRootView:rootView]; - - rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [UIViewController new]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; - [self.window makeKeyAndVisible]; + [RNSentry installWithBridge:[ReactNativeNavigation getBridge]]; [Fabric with:@[[Crashlytics class]]]; @@ -71,10 +54,13 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [[FBSDKApplicationDelegate sharedInstance] application:application + + BOOL fbsdk = [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; + BOOL deeplink = [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; + return fbsdk || deeplink; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity diff --git a/ios/kitsu_mobile/Info.plist b/ios/kitsu_mobile/Info.plist index 46f28e679..e52621de4 100644 --- a/ios/kitsu_mobile/Info.plist +++ b/ios/kitsu_mobile/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 6.8.1 + 6.9.2 CFBundleSignature ???? CFBundleURLTypes @@ -70,7 +70,7 @@ NSCameraUsageDescription Kitsu needs to access your camera to let you upload photos. NSLocationWhenInUseUsageDescription - + NSPhotoLibraryUsageDescription Kitsu needs to access your photo library to let you upload photos. UIAppFonts diff --git a/package.json b/package.json index 7ed32981f..3a98f823e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kitsu_mobile", - "version": "6.8.1", + "version": "6.9.2", "private": true, "scripts": { "debug": "REACT_DEBUGGER=\"rndebugger-open --open --port 8081\" yarn start", @@ -30,15 +30,16 @@ "react-native-animatable": "^1.2.4", "react-native-animated-progress-bar": "^1.1.0", "react-native-code-push": "^5.2.2", - "react-native-elements": "^0.19.0", + "react-native-deep-linking": "^2.1.0", "react-native-fast-image": "^4.0.14", - "react-native-fbsdk": "^0.7.0", + "react-native-fbsdk": "^0.8.0", "react-native-hyperlink": "^0.0.14", "react-native-image-crop-picker": "hummingbird-me/react-native-image-crop-picker", "react-native-image-zoom-viewer": "^2.2.5", "react-native-keyboard-aware-scroll-view": "^0.5.0", "react-native-linear-gradient": "^2.4.0", "react-native-modal-selector": "^0.0.25", + "react-native-navigation": "^2.0.2555", "react-native-onesignal": "^3.1.1", "react-native-scrollable-tab-view": "hummingbird-me/react-native-scrollable-tab-view#fixes_android", "react-native-sentry": "^0.37.1", @@ -47,7 +48,6 @@ "react-native-vector-icons": "^4.6.0", "react-native-wkwebview-reborn": "^1.21.0", "react-native-youtube": "^1.1.0", - "react-navigation": "^1.4.0", "react-redux": "^5.0.7", "recyclerlistview": "^1.3.4", "redux": "^4.0.0", diff --git a/src/App.js b/src/App.js index fb5b452f7..ac3cab801 100644 --- a/src/App.js +++ b/src/App.js @@ -1,277 +1,169 @@ /* global __DEV__ */ import React, { PureComponent } from 'react'; -import { Platform, View, StatusBar, ActivityIndicator, StyleSheet } from 'react-native'; -import { NavigationActions } from 'react-navigation'; -import { Provider, connect } from 'react-redux'; -import { PersistGate } from 'redux-persist/integration/react'; +import { Platform, View, ActivityIndicator } from 'react-native'; import * as colors from 'kitsu/constants/colors'; import { identity, isNil, isEmpty } from 'lodash'; import { Sentry } from 'react-native-sentry'; import codePush from 'react-native-code-push'; -import OneSignal from 'react-native-onesignal'; -import PropTypes from 'prop-types'; import { fetchAlgoliaKeys } from 'kitsu/store/app/actions'; import { kitsuConfig } from 'kitsu/config/env'; -import { NotificationPopover } from 'kitsu/components/NotificationPopover'; import { KitsuLibrary, KitsuLibraryEvents, KitsuLibraryEventSource } from 'kitsu/utils/kitsuLibrary'; -import { ImageLightbox } from 'kitsu/components/ImageLightbox'; -import { Lightbox } from 'kitsu/utils/lightbox'; -import store, { persistor } from './store/config'; -import Root from './Router'; -import * as types from './store/types'; -import { markNotifications } from './store/feed/actions'; +import { NavigationActions } from 'kitsu/navigation'; +import { fetchCurrentUser } from 'kitsu/store/user/actions'; +import { fetchNotifications } from 'kitsu/store/feed/actions'; +import store, { persistStore } from './store/config'; import * as profile from './store/profile/actions'; -// eslint-disable-next-line -console.disableYellowBox = true; - -// If you're using the debugging tools for React Native, the network tab is normally useless -// because it shows network activity to load the JS bundle only. This line causes it to -// use the dev tools XMLHttpRequest object if dev tools is running, making the network -// tab useful again. If dev tools isn't running, this will have no effect. -// NOTE: Disable this if you intend to upload files -GLOBAL.XMLHttpRequest = GLOBAL.originalXMLHttpRequest || GLOBAL.XMLHttpRequest; - -// A reset action for navigation -let resetAction = null; - class App extends PureComponent { componentWillMount() { - OneSignal.inFocusDisplaying(2); - OneSignal.addEventListener('ids', this.onIds); - OneSignal.addEventListener('registered', this.onPNRegistered); - OneSignal.addEventListener('received', this.onReceived); - OneSignal.addEventListener('opened', this.onOpened); - this.unsubscribe = store.subscribe(this.onStoreUpdate); - this.unsubscribeCreate = KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_CREATE, this.onLibraryEntryCreated); - this.unsubscribeUpdate = KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_UPDATE, this.onLibraryEntryUpdated); - this.unsubscribeDelete = KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_DELETE, this.onLibraryEntryDeleted); + // Register all global app events here + store.subscribe(onStoreUpdate); + KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_CREATE, onLibraryEntryCreated); + KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_UPDATE, onLibraryEntryUpdated); + KitsuLibrary.subscribe(KitsuLibraryEvents.LIBRARY_ENTRY_DELETE, onLibraryEntryDeleted); } componentDidMount() { - OneSignal.requestPermissions({ alert: true, sound: true, badge: true }); - store.dispatch(fetchAlgoliaKeys()); - } - - componentWillUnmount() { - OneSignal.removeEventListener('ids', this.onIds); - OneSignal.removeEventListener('registered', this.onPNRegistered); - OneSignal.removeEventListener('received', this.onReceived); - OneSignal.removeEventListener('opened', this.onOpened); - this.unsubscribe(); - this.unsubscribeCreate(); - this.unsubscribeUpdate(); - this.unsubscribeDelete(); + this.initialize(); } - onStoreUpdate() { - // Check if authentication state changed - const authenticated = store.getState().auth.isAuthenticated; - // If the authentication state changed from true to false then take user to intro screen - if ( - !isNil(this.authenticated) && - !isNil(authenticated) && - this.authenticated !== authenticated && !authenticated - ) { - // Take user back to intro - resetAction = NavigationActions.reset({ - index: 0, - actions: [NavigationActions.navigate({ routeName: 'Intro' })], - key: null, - }); - } + async initialize() { + // Make sure store has been persisted + await persistStore; - // Update sentry - const user = store.getState().user.currentUser; - if (authenticated) { - if (!isEmpty(user)) { - Sentry.setUserContext({ - id: user.id, - email: user.email, - username: user.name, - }); - } - } else { - Sentry.clearContext(); - } + // Fetch keys + store.dispatch(fetchAlgoliaKeys()); - Sentry.setTagsContext({ - environment: kitsuConfig.isProduction ? 'production' : 'staging', - react: true, - version: kitsuConfig.version, - }); + // Navigate to initial page + const { auth, onboarding, app } = store.getState(); + this.navigate(!!auth.isAuthenticated, !!onboarding.completed, app.initialPage); + } - // Set the new authentication value - this.authenticated = authenticated; + navigate(authenticated, onBoardingCompleted, initialTab = 'Feed') { + if (authenticated && onBoardingCompleted) { + this.fetchCurrentUser(); - // Check if we have a reset action that we need to perform - if (this.navigation && resetAction) { - // @Note: `navigation` may not exist as a reference yet due to `PersistGate` - // blocking children from rendering until state has been rehydrated. - this.navigation.dispatch(resetAction); - resetAction = null; + // Show the main screen of the app + NavigationActions.showMainApp(initialTab); + } else if (authenticated) { + // Show onboarding + NavigationActions.showOnboarding(); + } else { + // Show intro screen + NavigationActions.showIntro(); } } - onIds(device) { - console.log(device.userId); - store.dispatch({ type: types.ONESIGNAL_ID_RECEIVED, payload: device.userId }); - } - - onPNRegistered = (notificationData) => { - console.log('device registered', notificationData); + fetchCurrentUser = async () => { + try { + await store.dispatch(fetchCurrentUser()); + store.dispatch(fetchNotifications()); + } catch (e) { + console.warn(e); + } }; - onReceived(notification) { - console.log('Notification received: ', notification); + render() { + // Just render a loading screen here + // Once store is persisted, the initialize function will automatically set the root properley + return ( + + + + ); } +} + +/* + * Events for handling kitsu library and store + */ - onOpened = (openResult) => { - console.group('Opened Notification'); - console.log('Notification', openResult.notification); - console.log('Message: ', openResult.notification.payload.body); - console.log('Data: ', openResult.notification.payload.additionalData); - console.log('isActive: ', openResult.notification.isAppInFocus); - console.log('openResult: ', openResult); - console.groupEnd(); +let isAuthenticated = false; - /** - * Looks like navigating from root router to a nested screen inside the tab - * stack is not possible. Created a hacky TabNavigator with initial screen - * of Notifications. This way user can navigate to related - * notification. - * - * Related issues: react-community/react-navigation - * #1127, #1715, - */ - resetAction = NavigationActions.reset({ - index: 0, - key: null, - actions: [NavigationActions.navigate({ routeName: 'TabsNotification' })], - }); +function onStoreUpdate() { + // Check if authentication state changed + const authenticated = store.getState().auth.isAuthenticated; + const user = store.getState().user.currentUser; - if (this.navigation && resetAction) { - // @Note: `navigation` may not exist as a reference yet due to `PersistGate` - // blocking children from rendering until state has been rehydrated. - this.navigation.dispatch(resetAction); - resetAction = null; + // Update sentry + if (authenticated) { + if (!isEmpty(user)) { + Sentry.setUserContext({ + id: user.id, + email: user.email, + username: user.name, + }); } + } else { + Sentry.clearContext(); } - onLibraryEntryCreated = (data) => { - const { currentUser } = store.getState().user; - - if (!data || !currentUser || !currentUser.id) return; - - // Check to see if we got this event from something other than 'store' - const { status, type, entry, source } = data; - if (!entry || source === KitsuLibraryEventSource.STORE) return; - - // Add the store entry - store.dispatch(profile.onLibraryEntryCreate(entry, currentUser.id, type, status)); + Sentry.setTagsContext({ + environment: kitsuConfig.isProduction ? 'production' : 'staging', + react: true, + version: kitsuConfig.version, + }); + + // If the authentication state changed from `true` to `false` then take user to intro screen + if ( + !isNil(isAuthenticated) && + !isNil(authenticated) && + isAuthenticated !== authenticated && !authenticated + ) { + // Take user back to intro + NavigationActions.showIntro(); } - onLibraryEntryUpdated = (data) => { - const { currentUser } = store.getState().user; + // Set the new authentication value + isAuthenticated = authenticated; +} - if (!data || !currentUser || !currentUser.id) return; +function onLibraryEntryCreated(data) { + const { currentUser } = store.getState().user; - // Check to see if we got this event from something other than 'store' - const { type, oldEntry, newEntry, source } = data; - if (!oldEntry || !newEntry || source === KitsuLibraryEventSource.STORE) return; + if (!data || !currentUser || !currentUser.id) return; - // Update the store entry - store.dispatch(profile.onLibraryEntryUpdate(currentUser.id, type, oldEntry.status, newEntry)); - } + // Check to see if we got this event from something other than 'store' + const { status, type, entry, source } = data; + if (!entry || source === KitsuLibraryEventSource.STORE) return; - onLibraryEntryDeleted = (data) => { - const { currentUser } = store.getState().user; + // Add the store entry + store.dispatch(profile.onLibraryEntryCreate(entry, currentUser.id, type, status)); +} - if (!data || !currentUser || !currentUser.id) return; +function onLibraryEntryUpdated(data) { + const { currentUser } = store.getState().user; - // Check to see if we got this event from something other than 'store' - const { id, type, status, source } = data; - if (!id || source === KitsuLibraryEventSource.STORE) return; + if (!data || !currentUser || !currentUser.id) return; - // Delete the store entry - store.dispatch(profile.onLibraryEntryDelete(id, currentUser.id, type, status)); - } + // Check to see if we got this event from something other than 'store' + const { type, oldEntry, newEntry, source } = data; + if (!oldEntry || !newEntry || source === KitsuLibraryEventSource.STORE) return; - render() { - return ( - - } persistor={persistor}> - - - - ); - } + // Update the store entry + store.dispatch(profile.onLibraryEntryUpdate(currentUser.id, type, oldEntry.status, newEntry)); } -const Loading = () => ( - - - -); +function onLibraryEntryDeleted(data) { + const { currentUser } = store.getState().user; -const RootContainer = ({ inAppNotification, lightBox }) => ( - - - { - this.navigation = nav; - }} - /> - {inAppNotification && inAppNotification.visible && - store.dispatch({ type: types.DISMISS_IN_APP_NOTIFICATION })} - /> - } - { !isEmpty(lightBox) && - Lightbox.hide()} - /> - } - -); - -RootContainer.propTypes = { - inAppNotification: PropTypes.object.isRequired, - lightBox: PropTypes.object, -}; + if (!data || !currentUser || !currentUser.id) return; -RootContainer.defaultProps = { - lightBox: {}, -}; + // Check to see if we got this event from something other than 'store' + const { id, type, status, source } = data; + if (!id || source === KitsuLibraryEventSource.STORE) return; -const styles = StyleSheet.create({ - notification: { - position: 'absolute', - left: 0, - top: 0, - right: 0, - zIndex: 666, - }, -}); - -const ConnectedRoot = connect(({ feed, app }) => ({ - inAppNotification: feed.inAppNotification, - lightBox: app.imageLightbox, -}))(RootContainer); + // Delete the store entry + store.dispatch(profile.onLibraryEntryDelete(id, currentUser.id, type, status)); +} -// Check for Codepush only in production mode (Saves compile time & network calls in development). // FIXME: Codepush is making android crash const wrapper = __DEV__ || Platform.OS === 'android' ? identity : codePush; diff --git a/src/Router.js b/src/Router.js deleted file mode 100644 index 9ccb1d8bf..000000000 --- a/src/Router.js +++ /dev/null @@ -1,50 +0,0 @@ -import { StackNavigator } from 'react-navigation'; -import Routes from './routes'; -import { IntroScreen, RegistrationScreen } from './screens/Intro'; -import { AuthScreen, RecoveryScreen, SplashScreen } from './screens/Auth'; -import navigationOptions from './routes/navigationOptions'; - -const Root = StackNavigator( - { - Splash: { - screen: SplashScreen, - }, - Intro: { - screen: IntroScreen, - }, - Registration: { - screen: RegistrationScreen, - }, - AuthScreen: { - screen: AuthScreen, - }, - Recovery: { - screen: RecoveryScreen, - }, - Onboarding: { - screen: Routes.OnboardingStack, - }, - Tabs: { - screen: Routes.Tabs, - }, - TabsNotification: { - screen: Routes.TabsNotification, - }, - SearchFilter: { - screen: Routes.FilterStack, - }, - QuickUpdate: { - screen: Routes.QuickUpdateStack, - }, - Post: { - screen: Routes.PostStack, - }, - }, - { - headerMode: 'none', - mode: 'modal', - navigationOptions: navigationOptions(undefined, -10), - }, -); - -export default Root; diff --git a/src/assets/img/tabbar_icons/home.png b/src/assets/img/tabbar_icons/home.png index 3b7505872..d19b11fc4 100644 Binary files a/src/assets/img/tabbar_icons/home.png and b/src/assets/img/tabbar_icons/home.png differ diff --git a/src/assets/img/tabbar_icons/home@2x.png b/src/assets/img/tabbar_icons/home@2x.png new file mode 100644 index 000000000..b57b07fd1 Binary files /dev/null and b/src/assets/img/tabbar_icons/home@2x.png differ diff --git a/src/assets/img/tabbar_icons/home@3x.png b/src/assets/img/tabbar_icons/home@3x.png new file mode 100644 index 000000000..48c04bb01 Binary files /dev/null and b/src/assets/img/tabbar_icons/home@3x.png differ diff --git a/src/assets/img/tabbar_icons/index.js b/src/assets/img/tabbar_icons/index.js new file mode 100644 index 000000000..526efed75 --- /dev/null +++ b/src/assets/img/tabbar_icons/index.js @@ -0,0 +1,13 @@ +import homeIcon from './home.png'; +import searchIcon from './search.png'; +import libraryIcon from './library.png'; +import notificationIcon from './notification.png'; +import updateIcon from './update.png'; + +export { + homeIcon, + searchIcon, + libraryIcon, + notificationIcon, + updateIcon, +}; diff --git a/src/assets/img/tabbar_icons/library.png b/src/assets/img/tabbar_icons/library.png index 7f14e0628..969177b63 100644 Binary files a/src/assets/img/tabbar_icons/library.png and b/src/assets/img/tabbar_icons/library.png differ diff --git a/src/assets/img/tabbar_icons/library@2x.png b/src/assets/img/tabbar_icons/library@2x.png new file mode 100644 index 000000000..1b0e11877 Binary files /dev/null and b/src/assets/img/tabbar_icons/library@2x.png differ diff --git a/src/assets/img/tabbar_icons/library@3x.png b/src/assets/img/tabbar_icons/library@3x.png new file mode 100644 index 000000000..271bd5449 Binary files /dev/null and b/src/assets/img/tabbar_icons/library@3x.png differ diff --git a/src/assets/img/tabbar_icons/notification.png b/src/assets/img/tabbar_icons/notification.png index 0e58e9b1a..8ae1a1a9f 100644 Binary files a/src/assets/img/tabbar_icons/notification.png and b/src/assets/img/tabbar_icons/notification.png differ diff --git a/src/assets/img/tabbar_icons/notification@2x.png b/src/assets/img/tabbar_icons/notification@2x.png new file mode 100644 index 000000000..f52e00707 Binary files /dev/null and b/src/assets/img/tabbar_icons/notification@2x.png differ diff --git a/src/assets/img/tabbar_icons/notification@3x.png b/src/assets/img/tabbar_icons/notification@3x.png new file mode 100644 index 000000000..c87a8be74 Binary files /dev/null and b/src/assets/img/tabbar_icons/notification@3x.png differ diff --git a/src/assets/img/tabbar_icons/search.png b/src/assets/img/tabbar_icons/search.png index 54fad1860..b951b0602 100644 Binary files a/src/assets/img/tabbar_icons/search.png and b/src/assets/img/tabbar_icons/search.png differ diff --git a/src/assets/img/tabbar_icons/search@2x.png b/src/assets/img/tabbar_icons/search@2x.png new file mode 100644 index 000000000..e75c27833 Binary files /dev/null and b/src/assets/img/tabbar_icons/search@2x.png differ diff --git a/src/assets/img/tabbar_icons/search@3x.png b/src/assets/img/tabbar_icons/search@3x.png new file mode 100644 index 000000000..9aadd12aa Binary files /dev/null and b/src/assets/img/tabbar_icons/search@3x.png differ diff --git a/src/assets/img/tabbar_icons/update.png b/src/assets/img/tabbar_icons/update.png index fb5f7ac83..4f6070e6b 100644 Binary files a/src/assets/img/tabbar_icons/update.png and b/src/assets/img/tabbar_icons/update.png differ diff --git a/src/assets/img/tabbar_icons/update@2x.png b/src/assets/img/tabbar_icons/update@2x.png new file mode 100644 index 000000000..cdf471c1b Binary files /dev/null and b/src/assets/img/tabbar_icons/update@2x.png differ diff --git a/src/assets/img/tabbar_icons/update@3x.png b/src/assets/img/tabbar_icons/update@3x.png new file mode 100644 index 000000000..43da059e3 Binary files /dev/null and b/src/assets/img/tabbar_icons/update@3x.png differ diff --git a/src/components/Checkbox/component.js b/src/components/Checkbox/component.js new file mode 100644 index 000000000..753bd8436 --- /dev/null +++ b/src/components/Checkbox/component.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { + TouchableOpacity, + View, + Text, + ViewPropTypes, +} from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import { styles } from './styles'; + +export const CheckBox = (props) => { + const { + component, + checked, + iconRight, + title, + center, + right, + containerStyle, + textStyle, + onPress, + onLongPress, + onIconPress, + onLongIconPress, + size, + checkedIcon, + uncheckedIcon, + iconType, + checkedColor, + uncheckedColor, + checkedTitle, + fontFamily, + ...attributes + } = props; + + const Component = component || TouchableOpacity; + let iconName = uncheckedIcon; + if (checked) { + iconName = checkedIcon; + } + + return ( + + + {!iconRight && + } + + {React.isValidElement(title) + ? title + : + {checked ? checkedTitle || title : title} + } + + {iconRight && + } + + + ); +}; + +CheckBox.defaultProps = { + checked: false, + iconRight: false, + right: false, + center: false, + checkedColor: 'green', + uncheckedColor: '#bfbfbf', + checkedIcon: 'check-square-o', + uncheckedIcon: 'square-o', + size: 24, +}; + +CheckBox.propTypes = { + component: PropTypes.any, + checked: PropTypes.bool, + iconRight: PropTypes.bool, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + center: PropTypes.bool, + right: PropTypes.bool, + containerStyle: ViewPropTypes.style, + textStyle: Text.propTypes.style, + onPress: PropTypes.func, + onLongPress: PropTypes.func, + checkedIcon: PropTypes.string, + uncheckedIcon: PropTypes.string, + iconType: PropTypes.string, + size: PropTypes.number, + checkedColor: PropTypes.string, + uncheckedColor: PropTypes.string, + checkedTitle: PropTypes.string, + onIconPress: PropTypes.func, + onLongIconPress: PropTypes.func, + fontFamily: PropTypes.string, +}; diff --git a/src/components/NotificationModal/index.js b/src/components/Checkbox/index.js similarity index 100% rename from src/components/NotificationModal/index.js rename to src/components/Checkbox/index.js diff --git a/src/components/Checkbox/styles.js b/src/components/Checkbox/styles.js new file mode 100644 index 000000000..ec6b5bb96 --- /dev/null +++ b/src/components/Checkbox/styles.js @@ -0,0 +1,32 @@ +import { StyleSheet, Platform } from 'react-native'; + +export const styles = StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + }, + container: { + margin: 5, + marginLeft: 10, + marginRight: 10, + backgroundColor: '#fafafa', + borderColor: '#ededed', + borderWidth: 1, + padding: 10, + borderRadius: 3, + }, + text: { + marginLeft: 10, + marginRight: 10, + color: '#43484d', + ...Platform.select({ + ios: { + fontWeight: 'bold', + }, + android: { + fontFamily: 'sans-serif', + fontWeight: 'bold', + }, + }), + }, +}); diff --git a/src/components/DismissableStackNavigator.js b/src/components/DismissableStackNavigator.js deleted file mode 100644 index 05d3ed0a6..000000000 --- a/src/components/DismissableStackNavigator.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { PureComponent } from 'react'; -import { StackNavigator } from 'react-navigation'; - -export default function DismissableStackNavigator(routes, options) { - const StackNav = StackNavigator(routes, options); - - return class DismissableStackNav extends PureComponent { - static router = StackNav.router; - - render() { - const { state, goBack } = this.props.navigation; - // console.log(state); - const nav = { - ...this.props.navigation, - dismiss: () => { - if (state.routes.length > 2) goBack(state.routes[state.routes.length-1].key); - else goBack(state.key); - }, - }; - return ; - } - }; -} diff --git a/src/components/ImageLightbox/component.js b/src/components/ImageLightbox/component.js index 78f01816b..0ef5d03f5 100644 --- a/src/components/ImageLightbox/component.js +++ b/src/components/ImageLightbox/component.js @@ -143,28 +143,22 @@ export class ImageLightbox extends PureComponent { const shareImage = onShare || this.shareImage; return ( - - shareImage(i && i.url)} - saveToLocalByLongPress={false} - backgroundColor={'rgba(0,0,0,0.97)'} - index={index} - loadingRender={() => ( - - - - )} - renderImage={this.renderImage} - renderFooter={this.renderFooter(imageUrls)} - footerContainerStyle={styles.imageModalFooterContainer} - /> - + shareImage(i && i.url)} + saveToLocalByLongPress={false} + backgroundColor={'rgba(0,0,0,0.97)'} + index={index} + loadingRender={() => ( + + + + )} + renderImage={this.renderImage} + renderFooter={this.renderFooter(imageUrls)} + footerContainerStyle={styles.imageModalFooterContainer} + /> ); } } diff --git a/src/components/MediaCard/component.js b/src/components/MediaCard/component.js index 8a4505a9a..6d15759af 100644 --- a/src/components/MediaCard/component.js +++ b/src/components/MediaCard/component.js @@ -4,6 +4,8 @@ import { PropTypes } from 'prop-types'; import { ProgressBar } from 'kitsu/components/ProgressBar'; import { ProgressiveImage } from 'kitsu/components/ProgressiveImage'; import { Rating } from 'kitsu/components/Rating'; +import { Navigation } from 'react-native-navigation'; +import { Screens } from 'kitsu/navigation'; import { styles } from './styles'; export const MediaCard = ({ @@ -11,7 +13,7 @@ export const MediaCard = ({ cardDimensions, cardStyle, mediaData, - navigate, + componentId, progress, ratingTwenty, ratingSystem, @@ -19,10 +21,15 @@ export const MediaCard = ({ loading, }) => { const onPress = () => { - if (mediaData && mediaData.id && mediaData.type) { - navigate('MediaPages', { - mediaId: mediaData.id, - mediaType: mediaData.type, + if (componentId && mediaData && mediaData.id && mediaData.type) { + Navigation.push(componentId, { + component: { + name: Screens.MEDIA_PAGE, + passProps: { + mediaId: mediaData.id, + mediaType: mediaData.type, + }, + }, }); } }; @@ -74,7 +81,7 @@ MediaCard.propTypes = { cardDimensions: PropTypes.object.isRequired, cardStyle: ViewPropTypes.style, mediaData: PropTypes.object.isRequired, - navigate: PropTypes.func.isRequired, + componentId: PropTypes.any, progress: PropTypes.number, ratingTwenty: PropTypes.number, ratingSystem: PropTypes.string, @@ -90,4 +97,5 @@ MediaCard.defaultProps = { ratingSystem: 'simple', style: null, loading: false, + componentId: null, }; diff --git a/src/components/NavigationHeader/component.js b/src/components/NavigationHeader/component.js index 47a412157..e9235c56b 100644 --- a/src/components/NavigationHeader/component.js +++ b/src/components/NavigationHeader/component.js @@ -3,25 +3,33 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import * as PropTypes from 'prop-types'; import { SimpleHeader } from 'kitsu/components/SimpleHeader'; import { styles } from './styles'; +import { Navigation } from 'react-native-navigation'; -export const NavigationHeader = ({ navigation, title, leftIcon, rightIcon, ...props }) => ( +export const NavigationHeader = ({ navigation, title, leftIcon, rightIcon, componentId, ...props }) => ( : null} - leftAction={() => navigation.goBack(null)} + leftAction={() => { + if (componentId) { + Navigation.pop(componentId); + } + }} rightContent={rightIcon ? : null} {...props} /> ); NavigationHeader.propTypes = { - navigation: PropTypes.object.isRequired, + navigation: PropTypes.object, + componentId: PropTypes.any, leftIcon: PropTypes.string, rightIcon: PropTypes.string, title: PropTypes.string, }; NavigationHeader.defaultProps = { + navigation: null, + componentId: null, title: '', leftIcon: 'chevron-left', rightIcon: null, diff --git a/src/components/NotificationModal/component.js b/src/components/NotificationModal/component.js deleted file mode 100644 index c96cfa770..000000000 --- a/src/components/NotificationModal/component.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { View, ViewPropTypes, Text, StatusBar, Modal, TouchableOpacity } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { PropTypes } from 'prop-types'; -import { parseNotificationData } from 'kitsu/screens/Notifications/NotificationsScreen'; -import { styles } from './styles'; - -/** - * @deprecated Use `NotificationPopover` instead. Will be removed in a future version. -*/ -export const NotificationModal = ({ style, visible, onRequestClose, data, ...otherProps }) => { - if (!data) return null; - const { actorName, actorAvatar, others, text } = parseNotificationData(data.activities); - console.log(actorName, others, text); - - return ( - - - - {}} style={styles.modalContent}> - - - - - {actorName || 'Unknown'} - - - {others && and {others}} - {text} - - - - - ); -}; - -NotificationModal.propTypes = { - ...Modal.propTypes, - style: ViewPropTypes.style, - data: PropTypes.object, - visible: PropTypes.bool.isRequired, - onRequestClose: PropTypes.func.isRequired, -}; -NotificationModal.defaultProps = { - style: null, - data: null, -}; diff --git a/src/components/NotificationModal/styles.js b/src/components/NotificationModal/styles.js deleted file mode 100644 index be6c4537e..000000000 --- a/src/components/NotificationModal/styles.js +++ /dev/null @@ -1,30 +0,0 @@ -import { StyleSheet } from 'react-native'; -import * as colors from 'kitsu/constants/colors'; -import { isX, paddingX } from 'kitsu/utils/isX'; - -export const styles = StyleSheet.create({ - modalWrapper: { - flex: 1, - backgroundColor: colors.transparent, - }, - modalContent: { - paddingTop: 20 + (isX ? paddingX : 0), - paddingHorizontal: 12, - height: 80 + (isX ? paddingX : 0), - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(44, 34, 43, 0.95)', - borderBottomWidth: 1, - borderBottomColor: 'rgb(46, 34, 45)', - }, - modalText: { - color: colors.offWhite, - fontWeight: '700', - fontFamily: 'OpenSans', - fontSize: 12, - margin: 10, - }, - userAvatar: { width: 40, height: 40, borderRadius: 20 }, - activityText: { color: colors.white, fontFamily: 'OpenSans', fontSize: 12 }, - activityTextHighlight: { fontWeight: 'bold', color: colors.tabRed }, -}); diff --git a/src/components/SimpleHeader/styles.js b/src/components/SimpleHeader/styles.js index 0a05d082f..f7820ff6b 100644 --- a/src/components/SimpleHeader/styles.js +++ b/src/components/SimpleHeader/styles.js @@ -7,9 +7,17 @@ import { isX, paddingX } from 'kitsu/utils/isX'; export const styles = StyleSheet.create({ headerContainer: { alignItems: 'flex-end', - backgroundColor: colors.darkPurple, + backgroundColor: colors.listBackPurple, flexDirection: 'row', height: navigationBarHeight + statusBarHeight + (isX ? paddingX : 0), + shadowColor: 'rgba(0,0,0,0.2)', + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.5, + elevation: 3, + zIndex: 2, }, headerItemText: { ...flattenCommon('text'), diff --git a/src/config/env.js b/src/config/env.js index 94b335b3a..6c4980035 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -12,7 +12,7 @@ export const kitsuConfig = { assetsUrl: `${kitsuUrl}/images`, baseUrl: apiUrl, uploadUrl: `${apiUrl}/edge/uploads/_bulk`, - version: '6.8.1', + version: '6.9.2', stream: { API_KEY: 'eb6dvmba4ct3', API_SECRET: diff --git a/src/constants/app.js b/src/constants/app.js index 369270c20..3a6f445fe 100644 --- a/src/constants/app.js +++ b/src/constants/app.js @@ -1,10 +1,14 @@ import { kitsuConfig } from 'kitsu/config/env'; -import { Platform, StatusBar } from 'react-native'; +import { Platform, StatusBar, Dimensions } from 'react-native'; // The height of the navigation bar itself export const navigationBarHeight = 44; export const statusBarHeight = Platform.select({ ios: 20, android: StatusBar.currentHeight }); +// TODO: Probably don't need this if RNN fixes their constants +// Ref: https://github.com/wix/react-native-navigation/issues/3893 +export const bottomTabsHeight = Platform.select({ ios: 64, android: 56 }); + export const defaultAvatar = `${kitsuConfig.assetsUrl}/default_avatar-ff0fd0e960e61855f9fc4a2c5d994379.png`; export const defaultCover = `${kitsuConfig.assetsUrl}/default_cover-7bda2081d0823731a96bbb20b70f4fcf.png`; diff --git a/src/constants/colors.js b/src/constants/colors.js index f33dc8cb9..6acffcfec 100644 --- a/src/constants/colors.js +++ b/src/constants/colors.js @@ -2,7 +2,8 @@ export const red = '#DA5E51'; export const activeRed = '#F87270'; export const lightPink = '#B7A7B6'; -export const tabRed = 'rgb(252, 117, 92)'; +export const tabRed = '#fc755c'; +export const tabInactive = '#A199A1'; /* blues */ export const blue = '#43ABF1'; diff --git a/src/navigation/actions.js b/src/navigation/actions.js new file mode 100644 index 000000000..99f73c9fe --- /dev/null +++ b/src/navigation/actions.js @@ -0,0 +1,82 @@ +import { Navigation } from 'react-native-navigation'; +import { setDeepLinkTabIndex } from 'kitsu/utils/deeplink'; +import * as Screens from './types'; +import * as Layouts from './layouts'; + + +/** + * Show the application intro/auth screen + */ +export function showIntro() { + Navigation.setRoot(Layouts.INTRO); +} + + +/** + * Show the main application screen. + * Tabs that you can select are: Feed, Search, QuickUpdate or Library. + * + * @param {string} [initialTab='Feed'] The initial tab to show. + */ +export async function showMainApp(initialTab = 'Feed') { + // Tabs that user can select to start app on + const tabs = { + Feed: 0, + Search: 1, + QuickUpdate: 2, + Library: 4, + }; + const currentTabIndex = tabs[initialTab] || 0; + + // Set the initial tab index for deep link + setDeepLinkTabIndex(currentTabIndex); + + Navigation.setRoot(Layouts.MAIN); + Navigation.mergeOptions(Screens.BOTTOM_TABS, { + bottomTabs: { + currentTabIndex, + }, + }); +} + + +/** + * Show the onboarding screen + */ +export function showOnboarding() { + Navigation.setRoot(Layouts.ONBOARDING); +} + + +/** + * Show the modal to create post. + * + * @param {*} props Props to pass to the screen + */ +export function showCreatePostModal(props) { + Navigation.showModal({ + stack: { + children: [{ + component: { + name: Screens.FEED_CREATE_POST, + passProps: props, + }, + }], + }, + }); +} + +/** + * Show the image lightbox. + * + * @param {[string]} images An array of string urls. + * @param {number} [initialImageIndex=0] the index of the initial image to show. + */ +export function showLightBox(images, initialImageIndex = 0) { + Navigation.showOverlay({ + component: { + name: Screens.LIGHTBOX, + passProps: { images, initialImageIndex }, + }, + }); +} diff --git a/src/navigation/index.js b/src/navigation/index.js new file mode 100644 index 000000000..db7e01cc2 --- /dev/null +++ b/src/navigation/index.js @@ -0,0 +1,63 @@ +import { Platform } from 'react-native'; +import { listBackPurple, tabRed, tabInactive } from 'kitsu/constants/colors'; +import * as Screens from './types'; +import * as Layouts from './layouts'; +import { registerScreens } from './screens'; +import * as NavigationActions from './actions'; + +const majorVersionIOS = Platform.OS === 'ios' ? parseInt(Platform.Version, 10) : 0; + +// Setting badgeColor on iOS 9 causes crash +const badgeColor = (Platform.OS === 'android' || majorVersionIOS >= 10) ? { + badgeColor: tabRed, +} : {}; + + +// Default styling options +// https://wix.github.io/react-native-navigation/v2/#/docs/styling +// https://github.com/wix/react-native-navigation/issues/3694 +export const defaultOptions = { + sideMenu: { + left: { + // Disable side drawer for everything except feed + enabled: false, + }, + }, + layout: { + backgroundColor: listBackPurple, + orientation: ['portrait'], + }, + topBar: { + // By Default we set the bar to not visible + // This is because there are many components where we use a custom navigation bar + visible: false, + background: { + color: listBackPurple, + }, + backButton: { // android + color: 'white', + }, + buttonColor: 'white', // iOS + title: { + color: 'white', + }, + }, + bottomTabs: { + backgroundColor: listBackPurple, + titleDisplayMode: 'alwaysHide', + }, + bottomTab: { + iconColor: tabInactive, + textColor: tabInactive, + selectedTextColor: tabRed, + selectedIconColor: tabRed, + iconInsets: { // This is for iOS + top: 6, + bottom: -6, + right: 0, + }, + ...badgeColor, + }, +}; + +export { Screens, registerScreens, Layouts, NavigationActions }; diff --git a/src/navigation/layouts/index.js b/src/navigation/layouts/index.js new file mode 100644 index 000000000..233452a7b --- /dev/null +++ b/src/navigation/layouts/index.js @@ -0,0 +1,13 @@ +import * as Screens from 'kitsu/navigation/types'; + +export const INITIAL = { + root: { + component: { + name: Screens.INITIAL, + }, + }, +}; + +export * from './main'; +export * from './intro'; +export * from './onboarding'; diff --git a/src/navigation/layouts/intro.js b/src/navigation/layouts/intro.js new file mode 100644 index 000000000..667f0cb43 --- /dev/null +++ b/src/navigation/layouts/intro.js @@ -0,0 +1,15 @@ +import * as Screens from 'kitsu/navigation/types'; + +export const INTRO = { + root: { + stack: { + children: [ + { + component: { + name: Screens.AUTH_INTRO, + }, + }, + ], + }, + }, +}; diff --git a/src/navigation/layouts/main.js b/src/navigation/layouts/main.js new file mode 100644 index 000000000..77ff4076b --- /dev/null +++ b/src/navigation/layouts/main.js @@ -0,0 +1,118 @@ +import { homeIcon, searchIcon, updateIcon, notificationIcon, libraryIcon } from 'kitsu/assets/img/tabbar_icons'; +import * as Screens from 'kitsu/navigation/types'; + +const feed = { + stack: { + children: [ + { + component: { + id: Screens.FEED, + name: Screens.FEED, + }, + }, + ], + options: { + bottomTab: { + icon: homeIcon, + }, + }, + }, +}; + +const search = { + stack: { + children: [ + { + component: { + id: Screens.SEARCH, + name: Screens.SEARCH, + }, + }, + ], + options: { + bottomTab: { + icon: searchIcon, + }, + }, + }, +}; + +const quickUpdate = { + stack: { + children: [ + { + component: { + id: Screens.QUICK_UPDATE, + name: Screens.QUICK_UPDATE, + }, + }, + ], + options: { + bottomTab: { + icon: updateIcon, + }, + }, + }, +}; + +const notification = { + stack: { + children: [ + { + component: { + id: Screens.NOTIFICATION, + name: Screens.NOTIFICATION, + }, + }, + ], + options: { + bottomTab: { + icon: notificationIcon, + }, + }, + }, +}; + +const library = { + stack: { + children: [ + { + component: { + id: Screens.LIBRARY, + name: Screens.LIBRARY, + }, + }, + ], + options: { + bottomTab: { + icon: libraryIcon, + }, + }, + }, +}; + +export const MAIN = { + root: { + sideMenu: { + id: Screens.SIDEBAR, + left: { + component: { + name: Screens.SIDEBAR, + }, + enabled: false, + }, + center: { + bottomTabs: { + id: Screens.BOTTOM_TABS, + children: [ + feed, + search, + quickUpdate, + notification, + library, + ], + }, + }, + }, + }, +}; diff --git a/src/navigation/layouts/onboarding.js b/src/navigation/layouts/onboarding.js new file mode 100644 index 000000000..1516866aa --- /dev/null +++ b/src/navigation/layouts/onboarding.js @@ -0,0 +1,15 @@ +import * as Screens from 'kitsu/navigation/types'; + +export const ONBOARDING = { + root: { + stack: { + children: [ + { + component: { + name: Screens.ONBOARDING_WELCOME, + }, + }, + ], + }, + }, +}; diff --git a/src/navigation/screens/auth.js b/src/navigation/screens/auth.js new file mode 100644 index 000000000..b895b48f9 --- /dev/null +++ b/src/navigation/screens/auth.js @@ -0,0 +1,11 @@ +import * as Screens from 'kitsu/navigation/types'; + +import { IntroScreen, RegistrationScreen } from 'kitsu/screens/Intro'; +import { AuthScreen, RecoveryScreen } from 'kitsu/screens/Auth'; + +export default authRoutes = { + [Screens.AUTH_INTRO]: IntroScreen, + [Screens.AUTH_REGISTRATION]: RegistrationScreen, + [Screens.AUTH_LOGIN]: AuthScreen, + [Screens.AUTH_RECOVERY]: RecoveryScreen, +}; diff --git a/src/navigation/screens/feed.js b/src/navigation/screens/feed.js new file mode 100644 index 000000000..d0a8c9b58 --- /dev/null +++ b/src/navigation/screens/feed.js @@ -0,0 +1,11 @@ +import Feed from 'kitsu/screens/Feed'; +import PostDetails from 'kitsu/screens/Feed/pages/PostDetails'; +import CreatePost from 'kitsu/screens/Feed/pages/PostCreation/CreatePost'; + +import * as Screens from 'kitsu/navigation/types'; + +export default feedRoutes = { + [Screens.FEED]: Feed, + [Screens.FEED_POST_DETAILS]: PostDetails, + [Screens.FEED_CREATE_POST]: CreatePost, +}; diff --git a/src/navigation/screens/index.js b/src/navigation/screens/index.js new file mode 100644 index 000000000..3ab3623bb --- /dev/null +++ b/src/navigation/screens/index.js @@ -0,0 +1,48 @@ +import { Navigation } from 'react-native-navigation'; +import { Provider } from 'react-redux'; +import store from 'kitsu/store/config'; + +import App from 'kitsu/App'; +import { withNotifications } from 'kitsu/utils/notifications'; +import { withActivityIndicatorHOC } from 'kitsu/utils/deeplink'; +import QuickUpdateScreen from 'kitsu/screens/QuickUpdateScreen'; +import { LightBox } from 'kitsu/screens/LightBox'; + +import * as Screens from 'kitsu/navigation/types'; + +import sidebarRoutes from './sidebar'; +import searchRoutes from './search'; +import feedRoutes from './feed'; +import libraryRoutes from './library'; +import pageRoutes from './pages'; +import authRoutes from './auth'; +import onboardingRoutes from './onboarding'; +import notificationRoutes from './notification'; + +const routes = { + ...feedRoutes, + ...libraryRoutes, + ...sidebarRoutes, + ...searchRoutes, + ...pageRoutes, + ...authRoutes, + ...onboardingRoutes, + ...notificationRoutes, + [Screens.QUICK_UPDATE]: QuickUpdateScreen, + [Screens.LIGHTBOX]: LightBox, + [Screens.INITIAL]: App, +}; + + +function registerComponent(name, callback) { + Navigation.registerComponentWithRedux(name, callback, Provider, store); +} + +/** + * Register the screens for React Native Navigation + */ +export function registerScreens() { + Object.keys(routes).forEach((key) => { + registerComponent(key, () => withNotifications(withActivityIndicatorHOC(routes[key]))); + }); +} diff --git a/src/navigation/screens/library.js b/src/navigation/screens/library.js new file mode 100644 index 000000000..3e98f6294 --- /dev/null +++ b/src/navigation/screens/library.js @@ -0,0 +1,11 @@ +import { LibraryScreen } from 'kitsu/screens/Library/LibraryScreen'; +import { LibrarySearch } from 'kitsu/screens/Library/LibrarySearch'; +import { UserLibraryEditScreen } from 'kitsu/screens/Profiles'; + +import * as Screens from 'kitsu/navigation/types'; + +export default libraryRoutes = { + [Screens.LIBRARY]: LibraryScreen, + [Screens.LIBRARY_SEARCH]: LibrarySearch, + [Screens.LIBRARY_ENTRY_EDIT]: UserLibraryEditScreen, +}; diff --git a/src/navigation/screens/notification.js b/src/navigation/screens/notification.js new file mode 100644 index 000000000..2a54290bb --- /dev/null +++ b/src/navigation/screens/notification.js @@ -0,0 +1,7 @@ +import * as Screens from 'kitsu/navigation/types'; + +import NotificationsScreen from 'kitsu/screens/Notifications/NotificationsScreen'; + +export default notificationRoutes = { + [Screens.NOTIFICATION]: NotificationsScreen, +}; diff --git a/src/navigation/screens/onboarding.js b/src/navigation/screens/onboarding.js new file mode 100644 index 000000000..128844899 --- /dev/null +++ b/src/navigation/screens/onboarding.js @@ -0,0 +1,25 @@ +import { + CreateAccountScreen, + SelectAccountScreen, + WelcomeScreen, + RateScreen, + FavoritesScreen, + ManageLibrary, + RatingSystemScreen, + ImportLibrary, + ImportDetail, +} from 'kitsu/screens/Onboarding/'; + +import * as Screens from 'kitsu/navigation/types'; + +export default onboardingRoutes = { + [Screens.ONBOARDING_WELCOME]: WelcomeScreen, + [Screens.ONBOARDING_CREATE_ACCOUNT]: CreateAccountScreen, + [Screens.ONBOARDING_SELECT_ACCOUNT]: SelectAccountScreen, + [Screens.ONBOARDING_RATE_SCREEN]: RateScreen, + [Screens.ONBOARDING_FAVORITES_SCREEN]: FavoritesScreen, + [Screens.ONBOARDING_MANAGE_LIBRARY]: ManageLibrary, + [Screens.ONBOARDING_IMPORT_LIBRARY]: ImportLibrary, + [Screens.ONBOARDING_IMPORT_DETAIL]: ImportDetail, + [Screens.ONBOARDING_RATING_SYSTEM]: RatingSystemScreen, +}; diff --git a/src/navigation/screens/pages.js b/src/navigation/screens/pages.js new file mode 100644 index 000000000..88e0aa517 --- /dev/null +++ b/src/navigation/screens/pages.js @@ -0,0 +1,13 @@ +import ProfilePages from 'kitsu/screens/Profiles/ProfilePages'; +import MediaPages from 'kitsu/screens/Profiles/MediaPages'; +import UnitPage from 'kitsu/screens/Profiles/MediaPages/pages/Episodes/Unit'; +import { UserLibraryListScreen } from 'kitsu/screens/Profiles'; + +import * as Screens from 'kitsu/navigation/types'; + +export default pageRoutes = { + [Screens.PROFILE_PAGE]: ProfilePages, + [Screens.PROFILE_LIBRARY_LIST]: UserLibraryListScreen, + [Screens.MEDIA_PAGE]: MediaPages, + [Screens.MEDIA_UNIT_DETAIL]: UnitPage, +}; diff --git a/src/navigation/screens/search.js b/src/navigation/screens/search.js new file mode 100644 index 000000000..b9f8ced4a --- /dev/null +++ b/src/navigation/screens/search.js @@ -0,0 +1,19 @@ +import SearchScreen from 'kitsu/screens/Search/SearchScreen'; +import SearchCategory from 'kitsu/screens/Search/SearchCategory'; +import SearchResults from 'kitsu/screens/Search/SearchResults'; +import SeasonScreen from 'kitsu/screens/Search/SeasonScreen'; + +// Filter +import SearchFilter from 'kitsu/screens/Search/SearchFilter'; +import FilterSub from 'kitsu/screens/Search/FilterSub'; + +import * as Screens from 'kitsu/navigation/types'; + +export default searchRoutes = { + [Screens.SEARCH]: SearchScreen, + [Screens.SEARCH_CATEGORY]: SearchCategory, + [Screens.SEARCH_RESULTS]: SearchResults, + [Screens.SEARCH_SEASON]: SeasonScreen, + [Screens.SEARCH_FILTER]: SearchFilter, + [Screens.SEARCH_FILTER_SUB]: FilterSub, +}; diff --git a/src/navigation/screens/sidebar.js b/src/navigation/screens/sidebar.js new file mode 100644 index 000000000..b9b00c871 --- /dev/null +++ b/src/navigation/screens/sidebar.js @@ -0,0 +1,31 @@ +import { + SidebarScreen, + SettingsScreen, + GeneralSettings, + PrivacySettings, + LibrarySettings, + Blocking, + LinkedAccounts, + ImportLibrary, + ImportDetail, + ExportLibrary, + CannyBoard, + AppSettings, +} from 'kitsu/screens/Sidebar'; + +import * as Screens from 'kitsu/navigation/types'; + +export default sidebarRoutes = { + [Screens.SIDEBAR]: SidebarScreen, + [Screens.SIDEBAR_SETTINGS]: SettingsScreen, + [Screens.SIDEBAR_SETTINGS_GENERAL]: GeneralSettings, + [Screens.SIDEBAR_SETTINGS_APP]: AppSettings, + [Screens.SIDEBAR_SETTINGS_LIBRARY]: LibrarySettings, + [Screens.SIDEBAR_SETTINGS_PRIVACY]: PrivacySettings, + [Screens.SIDEBAR_BLOCKING]: Blocking, + [Screens.SIDEBAR_LINKED_ACCOUNTS]: LinkedAccounts, + [Screens.SIDEBAR_EXPORT_LIBRARY]: ExportLibrary, + [Screens.SIDEBAR_IMPORT_LIBRARY]: ImportLibrary, + [Screens.SIDEBAR_IMPORT_DETAIL]: ImportDetail, + [Screens.SIDEBAR_CANNY_BOARD]: CannyBoard, +}; diff --git a/src/navigation/types.js b/src/navigation/types.js new file mode 100644 index 000000000..21b8bf5df --- /dev/null +++ b/src/navigation/types.js @@ -0,0 +1,59 @@ +export const INITIAL = 'navigation.initial'; + +// Not exactly a screen but it lets us get access to the tabbar controller +export const BOTTOM_TABS = 'navigation.bottomtabs'; + +export const AUTH_INTRO = 'navigation.auth.intro'; +export const AUTH_REGISTRATION = 'navigation.auth.registration'; +export const AUTH_LOGIN = 'navigation.auth.login'; +export const AUTH_RECOVERY = 'navigation.auth.recovery'; + +export const ONBOARDING_WELCOME = 'navigation.onboarding.welcome'; +export const ONBOARDING_CREATE_ACCOUNT = 'navigation.onboarding.createaccount'; +export const ONBOARDING_SELECT_ACCOUNT = 'navigation.onboarding.selectaccount'; +export const ONBOARDING_RATE_SCREEN = 'navigation.onboarding.ratescreen'; +export const ONBOARDING_RATING_SYSTEM = 'navigation.onboarding.ratingsystem'; +export const ONBOARDING_FAVORITES_SCREEN = 'navigation.onboarding.favoritesscreen'; +export const ONBOARDING_MANAGE_LIBRARY = 'navigation.onboarding.managelibrary'; +export const ONBOARDING_IMPORT_LIBRARY = 'navigation.onboarding.importlibrary'; +export const ONBOARDING_IMPORT_DETAIL = 'navigation.onboarding.importdetail'; + +export const FEED = 'navigation.feed'; +export const FEED_POST_DETAILS = 'navigation.feed.postdetails'; +export const FEED_CREATE_POST = 'navigation.feed.createpost'; + +export const SEARCH = 'navigation.search'; +export const SEARCH_CATEGORY = 'navigation.search.category'; +export const SEARCH_RESULTS = 'navigation.search.results'; +export const SEARCH_SEASON = 'navigation.search.season'; +export const SEARCH_FILTER = 'navigation.search.filter'; +export const SEARCH_FILTER_SUB = 'navigation.search.filter.sub'; + +export const QUICK_UPDATE = 'navigation.quickupdate'; + +export const NOTIFICATION = 'navigation.notification'; + +export const LIBRARY = 'navigation.library'; +export const LIBRARY_SEARCH = 'navigation.library.search'; +export const LIBRARY_ENTRY_EDIT = 'navigation.library.entryedit'; + +export const PROFILE_PAGE = 'navigation.profile'; +export const PROFILE_LIBRARY_LIST = 'navigation.profile.librarylist'; + +export const MEDIA_PAGE = 'navigation.media'; +export const MEDIA_UNIT_DETAIL = 'navigation.media.episode.unitdetail'; + +export const SIDEBAR = 'navigation.sidebar'; +export const SIDEBAR_SETTINGS = 'navigation.sidebar.settings'; +export const SIDEBAR_SETTINGS_GENERAL = 'navigation.sidebar.settings.general'; +export const SIDEBAR_SETTINGS_PRIVACY = 'navigation.sidebar.settings.privacy'; +export const SIDEBAR_SETTINGS_LIBRARY = 'navigation.sidebar.settings.library'; +export const SIDEBAR_SETTINGS_APP = 'navigation.sidebar.settings.app'; +export const SIDEBAR_BLOCKING = 'navigation.sidebar.blocking'; +export const SIDEBAR_LINKED_ACCOUNTS = 'navigation.sidebar.linkedaccounts'; +export const SIDEBAR_EXPORT_LIBRARY = 'navigation.sidebar.exportlibrary'; +export const SIDEBAR_IMPORT_LIBRARY = 'navigation.sidebar.import.library'; +export const SIDEBAR_IMPORT_DETAIL = 'navigation.sidebar.import.detail'; +export const SIDEBAR_CANNY_BOARD = 'navigation.sidebar.cannyboard'; + +export const LIGHTBOX = 'navigation.lightbox'; diff --git a/src/routes/common.js b/src/routes/common.js deleted file mode 100644 index 93644a7b8..000000000 --- a/src/routes/common.js +++ /dev/null @@ -1,36 +0,0 @@ -import PostDetails from 'kitsu/screens/Feed/pages/PostDetails'; -import MediaPages from 'kitsu/screens/Profiles/MediaPages'; -import UnitPage from 'kitsu/screens/Profiles/MediaPages/pages/Episodes/Unit'; -import * as ProfileScreens from 'kitsu/screens/Profiles'; -import ProfilePages from 'kitsu/screens/Profiles/ProfilePages'; -import CreatePost from 'kitsu/screens/Feed/pages/PostCreation/CreatePost'; -import { LibrarySearch } from 'kitsu/screens/Library/LibrarySearch'; -import { sidebarRoutes } from './sidebar'; - -export const commonRoutes = { - PostDetails: { - screen: PostDetails, - }, - MediaPages: { - screen: MediaPages, - }, - CreatePost: { - screen: CreatePost, - }, - UnitDetails: { - screen: UnitPage, - }, - ProfilePages: { - screen: ProfilePages, - }, - UserLibraryEdit: { - screen: ProfileScreens.UserLibraryEditScreen, - }, - UserLibraryList: { - screen: ProfileScreens.UserLibraryListScreen, - }, - LibrarySearch: { - screen: LibrarySearch, - }, - ...sidebarRoutes, -}; diff --git a/src/routes/feed.js b/src/routes/feed.js deleted file mode 100644 index f08086431..000000000 --- a/src/routes/feed.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Platform } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { StackNavigator } from 'react-navigation'; -import Feed from 'kitsu/screens/Feed'; -import homeIcon from 'kitsu/assets/img/tabbar_icons/home.png'; -import { statusBarHeight, navigationBarHeight } from 'kitsu/constants/app'; -import { commonRoutes } from './common'; -import navigationOptions from './navigationOptions'; - -const options = navigationOptions(); -const FeedStack = StackNavigator( - { - FeedActivity: { - screen: Feed, - }, - ...commonRoutes, - }, - { - mode: 'modal', - navigationOptions: () => ({ - ...options, - headerStyle: { - ...options.headerStyle, - // This may look weird but it's the only way to make the modal status bars look normal - // On ios it auto adds status bar height, where as on android it doesnt :/ - height: navigationBarHeight + Platform.select({ ios: 0, android: statusBarHeight }), - paddingTop: Platform.select({ ios: 0, android: statusBarHeight }), - }, - // eslint-disable-next-line react/prop-types - tabBarIcon: ({ tintColor }) => ( - - ), - }), - }, -); - -export default FeedStack; diff --git a/src/routes/filter.js b/src/routes/filter.js deleted file mode 100644 index 4b509aa9f..000000000 --- a/src/routes/filter.js +++ /dev/null @@ -1,24 +0,0 @@ -import { StackNavigator } from 'react-navigation'; -import SearchFilter from 'kitsu/screens/Search/SearchFilter'; -import FilterSub from 'kitsu/screens/Search/FilterSub'; -import SearchCategory from 'kitsu/screens/Search/SearchCategory'; -import navigationOptions from './navigationOptions'; - -const FilterStack = StackNavigator( - { - Filter: { - screen: SearchFilter, - }, - FilterCategory: { - screen: SearchCategory, - }, - FilterSub: { - screen: FilterSub, - }, - }, - { - navigationOptions: navigationOptions(), - }, -); - -export default FilterStack; diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index 0466714eb..000000000 --- a/src/routes/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import SearchStack from './search'; -import NotifStack from './notification'; -import FilterStack from './filter'; -import Tabs from './tabs'; -import TabsNotification from './tabsNotification'; -import PostStack from './post'; -import QuickUpdateStack from './quickUpdate'; -import FeedStack from './feed'; -import OnboardingStack from './onboarding'; - -export default { - SearchStack, - NotifStack, - QuickUpdateStack, - FilterStack, - Tabs, - TabsNotification, - PostStack, - FeedStack, - OnboardingStack, -}; diff --git a/src/routes/library.js b/src/routes/library.js deleted file mode 100644 index 1ead39ea2..000000000 --- a/src/routes/library.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { Image } from 'react-native'; -import { StackNavigator } from 'react-navigation'; -import library from 'kitsu/assets/img/tabbar_icons/library.png'; -import { LibraryScreen } from 'kitsu/screens/Library/LibraryScreen'; -import { navigationBarHeight, statusBarHeight } from 'kitsu/constants/app'; -import navigationOptions from './navigationOptions'; -import { commonRoutes } from './common'; - -const LibraryStack = StackNavigator({ - LibraryScreen: { - screen: LibraryScreen, - }, - ...commonRoutes, -}, -{ - navigationOptions: () => ({ - ...navigationOptions(navigationBarHeight + statusBarHeight, statusBarHeight), - // eslint-disable-next-line react/prop-types - tabBarIcon: ({ tintColor }) => ( - - ), - }), -}); - -export default LibraryStack; diff --git a/src/routes/navigationOptions.js b/src/routes/navigationOptions.js deleted file mode 100644 index 2350badde..000000000 --- a/src/routes/navigationOptions.js +++ /dev/null @@ -1,21 +0,0 @@ -import { darkPurple, white } from 'kitsu/constants/colors'; -import { navigationBarHeight, statusBarHeight } from 'kitsu/constants/app'; -import { isX, paddingX } from 'kitsu/utils/isX'; - -// The default height, on iOS this will always be 64 (44 + 20) -const defaultHeight = navigationBarHeight + statusBarHeight + (isX ? paddingX : 0); - -export default (headerHeight = defaultHeight, marginTop = 0, extras = {}) => ({ - headerStyle: { - backgroundColor: darkPurple, - height: headerHeight, - }, - headerTitleStyle: { - color: white, - fontFamily: 'OpenSans', - fontWeight: 'bold', - fontSize: 15, - marginTop, - }, - ...extras, -}); diff --git a/src/routes/notification.js b/src/routes/notification.js deleted file mode 100644 index 32d80be48..000000000 --- a/src/routes/notification.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet, Platform } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { StackNavigator } from 'react-navigation'; -import NotificationsScreen from 'kitsu/screens/Notifications/NotificationsScreen'; -import notificationIcon from 'kitsu/assets/img/tabbar_icons/notification.png'; -import * as colors from 'kitsu/constants/colors'; -import { commonRoutes } from './common'; -import navigationOptions from './navigationOptions'; - -const NotifStack = StackNavigator( - { - Notifications: { - screen: NotificationsScreen, - }, - ...commonRoutes, - }, - { - navigationOptions: ({ screenProps }) => ({ - ...navigationOptions( - Platform.select({ ios: 64, android: 74 }), - Platform.select({ ios: 0, android: 20 }), - { - shadowOpacity: 0, - }, - ), - // eslint-disable-next-line react/prop-types - tabBarIcon: ({ tintColor }) => { - const shouldExpand = screenProps.badge > 99; - return ( - - {screenProps && - screenProps.badge > 0 && ( - - - {shouldExpand ? '99+' : screenProps.badge} - - - )} - - - ); - }, - }), - }, -); - -const styles = StyleSheet.create({ - textWrapper: { - position: 'absolute', - top: -7, - left: 12, - backgroundColor: colors.red, - padding: 3, - borderRadius: 16, - alignItems: 'center', - justifyContent: 'center', - zIndex: 2, - minWidth: 16, - }, - textWrapperExpanded: { - minWidth: 22, - }, - text: { - color: colors.white, - minWidth: 14, - fontSize: 10, - fontWeight: '700', - textAlign: 'center', - fontFamily: 'OpenSans', - }, - tabBarIconWrapper: { - justifyContent: 'center', - alignItems: 'center', - width: 21, - height: 21, - }, - tabBarIconWrapperExpanded: { - width: 25, - }, - tabBarIcon: { - width: 21, - height: 21, - }, -}); - -export default NotifStack; diff --git a/src/routes/onboarding.js b/src/routes/onboarding.js deleted file mode 100644 index c2cfbddd6..000000000 --- a/src/routes/onboarding.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { StackNavigator } from 'react-navigation'; -import { - CreateAccountScreen, - SelectAccountScreen, - WelcomeScreen, - RateScreen, - OnboardingHeader, - FavoritesScreen, - ManageLibrary, - RatingSystemScreen, - ImportLibrary, - ImportDetail, -} from 'kitsu/screens/Onboarding/'; -import { IntroScreen } from 'kitsu/screens/Intro'; -import navigationOptions from './navigationOptions'; -import Tabs from './tabs'; - -const OnboardingStack = StackNavigator( - { - WelcomeScreen: { - screen: WelcomeScreen, - }, - CreateAccountScreen: { - screen: CreateAccountScreen, - }, - SelectAccountScreen: { - screen: SelectAccountScreen, - }, - RateScreen: { - screen: RateScreen, - }, - OnboardingHeader: { - screen: OnboardingHeader, - }, - FavoritesScreen: { - screen: FavoritesScreen, - }, - ManageLibrary: { - screen: ManageLibrary, - }, - ImportLibrary: { - screen: ImportLibrary, - }, - ImportDetail: { - screen: ImportDetail, - }, - RatingSystemScreen: { - screen: RatingSystemScreen, - }, - Tabs: { - screen: Tabs, - }, - Intro: { - screen: IntroScreen, - }, - }, - { - mode: 'card', - navigationOptions: ({ navigation }) => ({ - ...navigationOptions(null), - header: ({ getScreenDetails, scene }) => { - const { backEnabled } = getScreenDetails(scene).options; - const { params } = navigation.state; - if (navigation.state.routeName === 'Tabs') return null; - return ( - - ); - }, - }), - }, -); - -class Onboarding extends React.PureComponent { - render() { - return ; - } -} -export default Onboarding; diff --git a/src/routes/post.js b/src/routes/post.js deleted file mode 100644 index 293344f33..000000000 --- a/src/routes/post.js +++ /dev/null @@ -1,18 +0,0 @@ -import { StackNavigator } from 'react-navigation'; -import Feed from 'kitsu/screens/Feed'; -import navigationOptions from './navigationOptions'; -import { commonRoutes } from './common'; - -const PostStack = StackNavigator( - { - Feed: { - screen: Feed, - }, - ...commonRoutes, - }, - { - navigationOptions: navigationOptions(), - }, -); - -export default PostStack; diff --git a/src/routes/quickUpdate.js b/src/routes/quickUpdate.js deleted file mode 100644 index f4cd16aab..000000000 --- a/src/routes/quickUpdate.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { Platform } from 'react-native'; -import FastImage from 'react-native-fast-image'; - -import QuickUpdateScreen from 'kitsu/screens/QuickUpdateScreen'; -import DismissableStackNavigator from 'kitsu/components/DismissableStackNavigator'; -import quickUpdateTabBarImage from 'kitsu/assets/img/tabbar_icons/update.png'; -import navigationOptions from './navigationOptions'; -import { commonRoutes } from './common'; - -const QuickUpdateStack = DismissableStackNavigator( - { - QuickUpdate: { - screen: QuickUpdateScreen, - }, - ...commonRoutes, - }, - { - navigationOptions: () => ({ - ...navigationOptions(50, Platform.select({ ios: -10, android: 20 })), - header: null, - // eslint-disable-next-line react/prop-types - tabBarIcon: ({ tintColor }) => ( - - ), - }), - }, -); - -export default QuickUpdateStack; diff --git a/src/routes/search.js b/src/routes/search.js deleted file mode 100644 index e25319de6..000000000 --- a/src/routes/search.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { Platform } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { StackNavigator } from 'react-navigation'; -import SearchScreen from 'kitsu/screens/Search/SearchScreen'; -import SearchCategory from 'kitsu/screens/Search/SearchCategory'; -import SearchResults from 'kitsu/screens/Search/SearchResults'; -import SeasonScreen from 'kitsu/screens/Search/SeasonScreen'; -import search from 'kitsu/assets/img/tabbar_icons/search.png'; -import navigationOptions from './navigationOptions'; -import { commonRoutes } from './common'; - -const SearchStack = StackNavigator( - { - SearchAll: { - screen: SearchScreen, - }, - SearchCategory: { - screen: SearchCategory, - }, - SearchResults: { - screen: SearchResults, - }, - SeasonScreen: { - screen: SeasonScreen, - }, - ...commonRoutes, - }, - { - navigationOptions: () => ({ - ...navigationOptions(75, Platform.select({ ios: 0, android: 20 })), - // eslint-disable-next-line react/prop-types - tabBarIcon: ({ tintColor }) => ( - - ), - }), - }, -); - -export default SearchStack; diff --git a/src/routes/sidebar.js b/src/routes/sidebar.js deleted file mode 100644 index 8d1f4ea4d..000000000 --- a/src/routes/sidebar.js +++ /dev/null @@ -1,56 +0,0 @@ -import { - SidebarScreen, - SettingsScreen, - GeneralSettings, - PrivacySettings, - LibrarySettings, - Blocking, - LinkedAccounts, - ImportLibrary, - ImportDetail, - ExportLibrary, - CannyBoard, - AppSettings, -} from 'kitsu/screens/Sidebar'; - -export const sidebarRoutes = { - Settings: { - screen: SettingsScreen, - }, - GeneralSettings: { - screen: GeneralSettings, - }, - PrivacySettings: { - screen: PrivacySettings, - }, - LibrarySettings: { - screen: LibrarySettings, - }, - Blocking: { - screen: Blocking, - }, - LinkedAccounts: { - screen: LinkedAccounts, - }, - ExportLibrary: { - screen: ExportLibrary, - }, - AppSettings: { - screen: AppSettings, - }, - ImportLibrary: { - screen: ImportLibrary, - }, - ImportDetail: { - screen: ImportDetail, - }, - SuggestFeatures: { - screen: CannyBoard, - }, - DatabaseRequests: { - screen: CannyBoard, - }, - ReportBugs: { - screen: CannyBoard, - }, -}; diff --git a/src/routes/tabs.js b/src/routes/tabs.js deleted file mode 100644 index bca1d0ddb..000000000 --- a/src/routes/tabs.js +++ /dev/null @@ -1,197 +0,0 @@ -/* eslint react/no-multi-comp:0 */ - -import React, { PureComponent, Component } from 'react'; -import { DrawerNavigator, TabNavigator } from 'react-navigation'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { fetchCurrentUser } from 'kitsu/store/user/actions'; -import { fetchAlgoliaKeys } from 'kitsu/store/app/actions'; -import { fetchNotifications } from 'kitsu/store/feed/actions'; -import { tabRed, listBackPurple } from 'kitsu/constants/colors'; -import { SidebarScreen } from 'kitsu/screens/Sidebar'; -import { isNull } from 'lodash'; - -import SearchStack from './search'; -import NotificationsStack from './notification'; -import QuickUpdateStack from './quickUpdate'; -import FeedStack from './feed'; -import LibraryStack from './library'; - -const TOP_LEVEL_ROUTES = [ - 'FeedActivity', - 'SearchAll', - 'QuickUpdate', - 'Notifications', - 'LibraryScreen', - 'DrawerToggle', - 'DrawerOpen', -]; - -const getRouteName = (state) => { - if (!state) return null; - const route = state.routes[state.index]; - if (route.routes) { - return getRouteName(route); - } - return route.routeName; -}; - -const Tabs = (initialRouteName = 'Feed') => ( - TabNavigator( - { - Feed: { - screen: FeedStack, - }, - Search: { - screen: SearchStack, - }, - QuickUpdate: { - screen: QuickUpdateStack, - }, - Notifications: { - screen: NotificationsStack, - }, - Library: { - screen: LibraryStack, - }, - }, - { - initialRouteName, - lazy: true, - removeClippedSubviews: true, - tabBarPosition: 'bottom', - swipeEnabled: false, - tabBarOptions: { - activeTintColor: tabRed, - inactiveBackgroundColor: listBackPurple, - activeBackgroundColor: listBackPurple, - showLabel: false, - showIcon: true, - iconStyle: { - width: 44, - height: 44, - }, - style: { - height: 44.96, - borderTopWidth: 0, - backgroundColor: listBackPurple, - }, - tabStyle: { - height: 44.96, - borderTopWidth: 0, - }, - indicatorStyle: { - backgroundColor: tabRed, - }, - backgroundColor: listBackPurple, - }, - navigationOptions: ({ navigation }) => { - const route = getRouteName(navigation.state); - - // By default have the drawer be openable - let drawerLockMode = 'unlocked'; - if (route && !TOP_LEVEL_ROUTES.includes(route)) { - drawerLockMode = 'locked-closed'; - } - return { - drawerLockMode, - }; - }, - }, - ) -); - -const Drawer = initialRouteName => ( - DrawerNavigator({ - Tabs: { - screen: Tabs(initialRouteName), - }, - }, { - contentComponent: SidebarScreen, // Use our own component - drawerBackgroundColor: listBackPurple, - }) -); - -/* -This is a really wacky hack job to show a specific tab at the start of the app. -There is no other easy way to navigate to a specific Tab using `NavigationActions.reset`. - -We have to have a wrapper for the `Drawer` otherwise `TabsNav` keeps re-rendering it, -causing it to mess up some nav actions, specifically navigation from the drawer. -*/ -class DrawerWrapper extends PureComponent { - static propTypes = { - initialPage: PropTypes.string, - } - - static defaultProps = { - initialPage: null, - } - - Wrapper = null; - - render() { - const { initialPage, ...otherProps } = this.props; - - // Create the drawer if we haven't - if (!this.Wrapper) { - this.Wrapper = Drawer(initialPage); - } - - return ( - - ); - } -} - -class TabsNav extends PureComponent { - static propTypes = { - badge: PropTypes.number.isRequired, - navigation: PropTypes.object.isRequired, - fetchCurrentUser: PropTypes.func.isRequired, - fetchAlgoliaKeys: PropTypes.func.isRequired, - fetchNotifications: PropTypes.func.isRequired, - initialPage: PropTypes.string, - }; - - static defaultProps = { - initialPage: null, - }; - - componentWillMount() { - this.fetchCurrentUser(); - - // We also fetch keys here because tokens might have been null during app start - this.props.fetchAlgoliaKeys(); - } - - fetchCurrentUser = async () => { - try { - await this.props.fetchCurrentUser(); - this.props.fetchNotifications(); - } catch (e) { - console.warn(e); - } - }; - - render() { - const { navigation: rootNavigation, badge, initialPage } = this.props; - const props = { rootNavigation, badge }; - return ( - - ); - } -} - -const mapper = ({ feed, app }) => ({ - badge: feed.notificationsUnseen, - initialPage: app.initialPage, -}); - -export default connect(mapper, { fetchCurrentUser, fetchAlgoliaKeys, fetchNotifications })(TabsNav); diff --git a/src/routes/tabsNotification.js b/src/routes/tabsNotification.js deleted file mode 100644 index 6d7f08f23..000000000 --- a/src/routes/tabsNotification.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Looks like navigating from root router to a nested screen inside the tab - * stack is not possible. Created a hacky TabNavigator with initial screen of - * Notifications. This way user can navigate to related notification. - * - * Related issues: react-community/react-navigation - * #1127, #335, #1715, - */ - -import React, { PureComponent } from 'react'; -import { DrawerNavigator, TabNavigator } from 'react-navigation'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { fetchCurrentUser } from 'kitsu/store/user/actions'; -import { fetchAlgoliaKeys } from 'kitsu/store/app/actions'; -import { fetchNotifications } from 'kitsu/store/feed/actions'; -import { tabRed, listBackPurple } from 'kitsu/constants/colors'; -import { SidebarScreen } from 'kitsu/screens/Sidebar'; - -import SearchStack from './search'; -import NotificationsStack from './notification'; -import QuickUpdateStack from './quickUpdate'; -import FeedStack from './feed'; -import LibraryStack from './library'; - -const TOP_LEVEL_ROUTES = [ - 'FeedActivity', - 'SearchAll', - 'QuickUpdate', - 'Notifications', - 'LibraryScreen', - 'DrawerToggle', - 'DrawerOpen', -]; - -const Tabs = TabNavigator( - { - Feed: { - screen: FeedStack, - }, - Search: { - screen: SearchStack, - }, - QuickUpdate: { - screen: QuickUpdateStack, - }, - Notifications: { - screen: NotificationsStack, - }, - Library: { - screen: LibraryStack, - }, - }, - { - lazy: true, - removeClippedSubviews: true, - tabBarPosition: 'bottom', - swipeEnabled: false, - tabBarOptions: { - activeTintColor: tabRed, - inactiveBackgroundColor: listBackPurple, - activeBackgroundColor: listBackPurple, - showLabel: false, - showIcon: true, - iconStyle: { - width: 44, - height: 44, - }, - style: { - height: 44.96, - borderTopWidth: 0, - backgroundColor: listBackPurple, - }, - tabStyle: { - height: 44.96, - borderTopWidth: 0, - }, - indicatorStyle: { - backgroundColor: tabRed, - }, - backgroundColor: listBackPurple, - }, - }, -); - -const Drawer = DrawerNavigator({ - Tabs: { - screen: Tabs, - }, -}, { - contentComponent: SidebarScreen, // Use our own component - drawerBackgroundColor: listBackPurple, -}); - -class TabsNav extends PureComponent { - static propTypes = { - badge: PropTypes.number.isRequired, - navigation: PropTypes.object.isRequired, - fetchCurrentUser: PropTypes.func.isRequired, - fetchAlgoliaKeys: PropTypes.func.isRequired, - fetchNotifications: PropTypes.func.isRequired, - }; - - state = { - drawerLockMode: 'unlocked', - }; - - componentWillMount() { - this.fetchCurrentUser(); - - // We also fetch keys here because tokens might have been null during app start - this.props.fetchAlgoliaKeys(); - } - - onNavigationStateChange = (prevState, currentState) => { - const current = this._getRouteName(currentState); - const previous = this._getRouteName(prevState); - // route changed? - if (previous !== current) { - // top-level route? - if (TOP_LEVEL_ROUTES.includes(current)) { - this.setState({ drawerLockMode: 'unlocked' }); - } else { - this.setState({ drawerLockMode: 'locked-closed' }); - } - } - }; - - - fetchCurrentUser = async () => { - try { - await this.props.fetchCurrentUser(); - this.props.fetchNotifications(); - } catch (e) { - console.warn(e); - } - }; - - _getRouteName(state) { - if (!state) return null; - const route = state.routes[state.index]; - if (route.routes) { - return this._getRouteName(route); - } - return route.routeName; - } - - render() { - const { navigation: rootNavigation, badge } = this.props; - const { drawerLockMode } = this.state; - const props = { rootNavigation, badge, drawerLockMode }; - return ( - - ); - } -} - -const mapper = ({ feed }) => ({ - badge: feed.notificationsUnseen, -}); - -export default connect(mapper, { fetchCurrentUser, fetchAlgoliaKeys, fetchNotifications })(TabsNav); diff --git a/src/screens/Auth/AuthScreen.js b/src/screens/Auth/AuthScreen.js index 94a1ceeee..e229c1b24 100644 --- a/src/screens/Auth/AuthScreen.js +++ b/src/screens/Auth/AuthScreen.js @@ -23,12 +23,14 @@ import { TERMS_URL } from 'kitsu/constants/app'; import isEmpty from 'lodash/isEmpty'; import moment from 'moment'; import { Sentry } from 'react-native-sentry'; +import { Navigation } from 'react-native-navigation'; +import { Screens } from 'kitsu/navigation'; import AuthWrapper from './AuthWrapper'; import styles from './styles'; class AuthScreen extends React.Component { state = { - authType: this.props.navigation.state.params.authType, + authType: this.props.authType, loading: false, email: '', username: '', @@ -65,10 +67,10 @@ class AuthScreen extends React.Component { } onSubmitSignup = (isFb) => { - const { navigation } = this.props; + const { componentId } = this.props; const { email, username, password, confirmPassword } = this.state; if (isFb) { - this.props.loginUser(null, navigation, 'login'); + this.props.loginUser(null, componentId, 'login'); } else if ( isEmpty(email) || isEmpty(username) || @@ -85,20 +87,20 @@ class AuthScreen extends React.Component { toastVisible: true, }); } else { - this.props.createUser({ email, username, password: password.trim() }, navigation); + this.props.createUser({ email, username, password: password.trim() }, componentId); } }; onSubmitLogin = () => { const { email, password } = this.state; - const { navigation } = this.props; + const { componentId } = this.props; if (isEmpty(email) || isEmpty(password)) { this.setState({ toastTitle: "Inputs can't be blank", toastVisible: true, }); } else { - this.props.loginUser({ email, password }, navigation); + this.props.loginUser({ email, password }, componentId); } }; @@ -122,7 +124,11 @@ class AuthScreen extends React.Component { }; onForgotPassword = () => { - this.props.navigation.navigate('Recovery'); + Navigation.push(this.props.componentId, { + component: { + name: Screens.AUTH_RECOVERY, + }, + }); }; onDismiss = () => { diff --git a/src/screens/Auth/RecoveryScreen.js b/src/screens/Auth/RecoveryScreen.js index 491b7b9c0..9a07bdbb8 100644 --- a/src/screens/Auth/RecoveryScreen.js +++ b/src/screens/Auth/RecoveryScreen.js @@ -6,16 +6,12 @@ import * as colors from 'kitsu/constants/colors'; import RecoveryForm from 'kitsu/components/Forms/RecoveryForm'; import { Toast } from 'kitsu/components/Toast'; import { loginUser } from 'kitsu/store/auth/actions'; -import AuthWrapper from './AuthWrapper'; +import { Navigation } from 'react-native-navigation'; import { kitsuConfig } from 'kitsu/config/env'; +import AuthWrapper from './AuthWrapper'; import styles from './styles'; class RecoveryScreen extends Component { - static navigationOptions = { - header: null, - gesturesEnabled: false, - }; - state = { email: '', loading: false, @@ -24,7 +20,7 @@ class RecoveryScreen extends Component { }; onDismiss = () => { - this.props.navigation.goBack(); + Navigation.pop(this.props.componentId); } onReset = async () => { @@ -51,10 +47,6 @@ class RecoveryScreen extends Component { this.setState({ [name]: text }); } - goBack = () => { - this.props.navigation.goBack(); - } - render() { return ( @@ -80,7 +72,7 @@ class RecoveryScreen extends Component { handleChange={this.handleChange} onReset={this.onReset} loading={this.props.signingIn || this.state.loading} - onCancel={this.goBack} + onCancel={this.onDismiss} /> @@ -91,7 +83,7 @@ class RecoveryScreen extends Component { RecoveryScreen.propTypes = { signingIn: PropTypes.bool.isRequired, - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, }; RecoveryScreen.defaultProps = { diff --git a/src/screens/Auth/SplashScreen.js b/src/screens/Auth/SplashScreen.js deleted file mode 100644 index 30b16a58f..000000000 --- a/src/screens/Auth/SplashScreen.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { Component } from 'react'; -import { View, Dimensions } from 'react-native'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { NavigationActions } from 'react-navigation'; -import * as colors from 'kitsu/constants/colors'; - -class SplashScreen extends Component { - static navigationOptions = { - header: null, - }; - - componentDidMount() { - const { isAuthenticated, completed, rehydratedAt } = this.props; - if (rehydratedAt) { - this.navigate(isAuthenticated, completed); - } - } - - componentWillReceiveProps(nextProps) { - const { isAuthenticated, completed, rehydratedAt } = nextProps; - if (rehydratedAt) { - this.navigate(isAuthenticated, completed); - } - } - - navigate(authorized, completed) { - const { dispatch } = this.props.navigation; - let resetAction = null; - if (authorized && completed) { - resetAction = NavigationActions.reset({ - index: 0, - actions: [NavigationActions.navigate({ routeName: 'Tabs' })], - key: null, - }); - } else if (authorized) { - resetAction = NavigationActions.reset({ - index: 0, - actions: [NavigationActions.navigate({ routeName: 'Onboarding' })], - key: null, - }); - } else { - resetAction = NavigationActions.reset({ - index: 0, - actions: [NavigationActions.navigate({ routeName: 'Intro' })], - key: null, - }); - } - dispatch(resetAction); - } - - render() { - return ( - - ); - } -} - -const mapStateToProps = ({ auth, onboarding }) => { - const { isAuthenticated, rehydratedAt } = auth; - const { completed } = onboarding; - return { isAuthenticated, rehydratedAt, completed }; -}; - -SplashScreen.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - navigation: PropTypes.object.isRequired, -}; - -export default connect(mapStateToProps)(SplashScreen); diff --git a/src/screens/Auth/index.js b/src/screens/Auth/index.js index bfce6f50e..ae81a4d8f 100644 --- a/src/screens/Auth/index.js +++ b/src/screens/Auth/index.js @@ -1,9 +1,7 @@ import RecoveryScreen from './RecoveryScreen'; -import SplashScreen from './SplashScreen'; import AuthScreen from './AuthScreen'; export { RecoveryScreen, - SplashScreen, AuthScreen, }; diff --git a/src/screens/Feed/components/Comment/component.js b/src/screens/Feed/components/Comment/component.js index 3c8aabea8..5a955adf8 100644 --- a/src/screens/Feed/components/Comment/component.js +++ b/src/screens/Feed/components/Comment/component.js @@ -183,14 +183,14 @@ export class Comment extends PureComponent { onAvatarPress={this.props.onAvatarPress} onReplyPress={() => this.onReplyPress(item)} hideEmbeds={this.props.hideEmbeds} - navigation={this.props.navigation} + componentId={this.props.componentId} isCommentReply /> ) render() { const { - navigation, + componentId, comment, isTruncated, onAvatarPress, @@ -225,7 +225,7 @@ export class Comment extends PureComponent { {name} {!isEmpty(content) && - handleURL(url, navigation)}> + handleURL(url)}> } @@ -317,7 +317,7 @@ Comment.propTypes = { repliesCount: PropTypes.number, createdAt: PropTypes.string, }).isRequired, - navigation: PropTypes.object, + componentId: PropTypes.any, isTruncated: PropTypes.bool, onAvatarPress: PropTypes.func, onReplyPress: PropTypes.func, @@ -331,6 +331,7 @@ Comment.defaultProps = { onReplyPress: null, hideEmbeds: false, isCommentReply: false, + componentId: null, }; export const ToggleReplies = ({ onPress, isLoading, repliesCount }) => ( diff --git a/src/screens/Feed/components/EmbeddedContent/component.js b/src/screens/Feed/components/EmbeddedContent/component.js index 5ad8cb9e0..12de585c5 100644 --- a/src/screens/Feed/components/EmbeddedContent/component.js +++ b/src/screens/Feed/components/EmbeddedContent/component.js @@ -12,10 +12,11 @@ import dataBunny from 'kitsu/assets/img/data-bunny.png'; import { ImageGrid } from 'kitsu/screens/Feed/components/ImageGrid'; import { startCase, isNil, isEmpty } from 'lodash'; import { WebComponent } from 'kitsu/utils/components'; -import { Lightbox } from 'kitsu/utils/lightbox'; +import { Navigation } from 'react-native-navigation'; +import { Screens, NavigationActions } from 'kitsu/navigation'; +import { handleURL } from 'kitsu/utils/url'; import { styles } from './styles'; - class EmbeddedContent extends PureComponent { // The reason for the combination of string or number is that // sometimes the embeds return width/height as strings @@ -63,7 +64,7 @@ class EmbeddedContent extends PureComponent { maxWidth: PropTypes.number.isRequired, minWidth: PropTypes.number, borderRadius: PropTypes.number, - navigation: PropTypes.object, + componentId: PropTypes.any, compact: PropTypes.bool, dataSaver: PropTypes.bool, @@ -80,7 +81,7 @@ class EmbeddedContent extends PureComponent { borderRadius: 0, compact: false, dataSaver: false, - navigation: null, + componentId: null, ignoreDataSaver: false, disabled: false, } @@ -155,7 +156,7 @@ class EmbeddedContent extends PureComponent { borderRadius={borderRadius} compact={compact} onImageTapped={(index) => { - Lightbox.show(filtered, (index || 0)); + NavigationActions.showLightBox(filtered, (index || 0)); }} disabled={disabled} /> @@ -238,7 +239,7 @@ class EmbeddedContent extends PureComponent { const id = embed.kitsu && embed.kitsu.id; if (!id) return null; - const { navigation, maxWidth, disabled } = this.props; + const { componentId, maxWidth, disabled } = this.props; const type = embed.url && embed.url.includes('anime') ? 'anime' : 'manga'; const image = this.getImageUrl(embed); @@ -246,8 +247,13 @@ class EmbeddedContent extends PureComponent { { - if (navigation) { - navigation.navigate('MediaPages', { mediaId: id, mediaType: type }); + if (componentId) { + Navigation.push(componentId, { + component: { + name: Screens.MEDIA_PAGE, + passProps: { mediaId: id, mediaType: type }, + }, + }); } }} disabled={disabled} @@ -275,7 +281,7 @@ class EmbeddedContent extends PureComponent { const id = embed.kitsu && embed.kitsu.id; if (!id) return null; - const { navigation, maxWidth, disabled } = this.props; + const { componentId, maxWidth, disabled } = this.props; const imageUri = this.getImageUrl(embed); const image = (imageUri && imageUri.includes('http') && { uri: imageUri }) || defaultAvatar; @@ -284,8 +290,13 @@ class EmbeddedContent extends PureComponent { { - if (navigation) { - navigation.navigate('ProfilePages', { userId: id }); + if (componentId) { + Navigation.push(componentId, { + component: { + name: Screens.PROFILE_PAGE, + passProps: { userId: id }, + }, + }); } }} disabled={disabled} @@ -309,17 +320,13 @@ class EmbeddedContent extends PureComponent { const { maxWidth, disabled } = this.props; - const openUrl = (url) => { - Linking.openURL(url).catch(err => console.log(`An error occurred while opening url ${url}:`, err)); - }; - const image = this.getImageUrl(embed); const isDescriptionEmpty = isEmpty(embed.description); return ( openUrl(embed.url)} + onPress={() => handleURL(embed.url)} disabled={disabled} > diff --git a/src/screens/Feed/components/GiphyModal/component.js b/src/screens/Feed/components/GiphyModal/component.js index 934680be1..81e731c42 100644 --- a/src/screens/Feed/components/GiphyModal/component.js +++ b/src/screens/Feed/components/GiphyModal/component.js @@ -186,29 +186,31 @@ export class GiphyModal extends PureComponent { leftButtonAction={this.handleCancelPress} rightButtonTitle="" /> - - + + + + ({ + length: bestSpacing.height, + offset: bestSpacing.height * index, + index, + })} + numColumns={bestSpacing.columnCount} + ItemSeparatorComponent={() => } + keyExtractor={item => `${item.id}`} + renderItem={({ item }) => this.renderItem(item, bestSpacing)} /> - ({ - length: bestSpacing.height, - offset: bestSpacing.height * index, - index, - })} - numColumns={bestSpacing.columnCount} - ItemSeparatorComponent={() => } - keyExtractor={item => `${item.id}`} - renderItem={({ item }) => this.renderItem(item, bestSpacing)} - /> ); } diff --git a/src/screens/Feed/components/GiphyModal/styles.js b/src/screens/Feed/components/GiphyModal/styles.js index 5215b923a..67370f39b 100644 --- a/src/screens/Feed/components/GiphyModal/styles.js +++ b/src/screens/Feed/components/GiphyModal/styles.js @@ -1,7 +1,12 @@ import { StyleSheet, Dimensions } from 'react-native'; import * as colors from 'kitsu/constants/colors'; +import { isX, safeAreaInsetX } from 'kitsu/utils/isX'; export const styles = StyleSheet.create({ + container: { + marginBottom: isX ? safeAreaInsetX.bottom : 0, + flex: 1, + }, searchBox: { marginHorizontal: 10, marginBottom: 10, diff --git a/src/screens/Feed/components/MediaModal/component.js b/src/screens/Feed/components/MediaModal/component.js index eb674b0fa..9914c8eea 100644 --- a/src/screens/Feed/components/MediaModal/component.js +++ b/src/screens/Feed/components/MediaModal/component.js @@ -165,7 +165,7 @@ class MediaModal extends PureComponent { rightButtonAction={this.handleDonePress} rightButtonDisabled={isNull(selected)} /> - + { - if (media) navigation.navigate('MediaPages', { mediaId: media.id, mediaType: media.type }); +const navigateToMedia = (media, componentId) => { + if (media) { + Navigation.push(componentId, { + component: { + name: Screens.MEDIA_PAGE, + passProps: { mediaId: media.id, mediaType: media.type }, + }, + }); + } }; -export const MediaTag = ({ disabled, media, episode, navigation, style }) => { +export const MediaTag = ({ disabled, media, episode, componentId, style }) => { if (!media) return null; const episodePrefix = media.type === 'anime' ? 'E' : 'CH'; return ( navigateToMedia(media, navigation)} + onPress={() => navigateToMedia(media, componentId)} style={styles.mediaTag} > @@ -25,7 +34,7 @@ export const MediaTag = ({ disabled, media, episode, navigation, style }) => { {episode && ( navigateToMedia(media, navigation)} + onPress={() => navigateToMedia(media, componentId)} style={styles.episodeTagView} > @@ -45,7 +54,7 @@ MediaTag.propTypes = { episode: PropTypes.shape({ number: PropTypes.number.isRequired, }), - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, disabled: PropTypes.bool, style: ViewPropTypes.style, }; diff --git a/src/screens/Feed/components/Post/component.js b/src/screens/Feed/components/Post/component.js index db489dca2..52c53eb16 100644 --- a/src/screens/Feed/components/Post/component.js +++ b/src/screens/Feed/components/Post/component.js @@ -11,16 +11,18 @@ import { preprocessFeedPosts, preprocessFeedPost } from 'kitsu/utils/preprocessF import { isEmpty, uniqBy } from 'lodash'; import { extractUrls } from 'kitsu/utils/url'; import { FeedCache } from 'kitsu/utils/cache'; +import { Navigation } from 'react-native-navigation'; +import { Screens, NavigationActions } from 'kitsu/navigation'; import { styles } from './styles'; import { PostHeader, PostMain, PostOverlay, PostActions, CommentFlatList } from './components'; // Post export class Post extends PureComponent { static propTypes = { + componentId: PropTypes.any.isRequired, post: PropTypes.object.isRequired, currentUser: PropTypes.object.isRequired, onPostPress: PropTypes.func, - navigation: PropTypes.object.isRequired, } static defaultProps = { @@ -268,7 +270,7 @@ export class Post extends PureComponent { toggleEditor = () => { if (this.props.currentUser) { - this.props.navigation.navigate('CreatePost', { + NavigationActions.showCreatePostModal({ isEditing: true, post: this.state.post, onPostCreated: (post) => { @@ -290,7 +292,7 @@ export class Post extends PureComponent { } render() { - const { navigation, currentUser } = this.props; + const { currentUser, componentId } = this.props; const { post, comment, @@ -328,7 +330,7 @@ export class Post extends PureComponent { commentsCount={commentsCount} taggedMedia={media} taggedEpisode={spoiledUnit} - navigation={navigation} + componentId={componentId} onPress={this.toggleOverlay} /> ) @@ -343,7 +345,7 @@ export class Post extends PureComponent { commentsCount={commentsCount} taggedMedia={media} taggedEpisode={spoiledUnit} - navigation={navigation} + componentId={componentId} onPress={this.onPostPress} /> ); @@ -356,7 +358,14 @@ export class Post extends PureComponent { currentUser={currentUser} avatar={(user.avatar && user.avatar.medium) || defaultAvatar} onAvatarPress={() => { - if (user) navigation.navigate('ProfilePages', { userId: user.id }); + if (user) { + Navigation.push(componentId, { + component: { + name: Screens.PROFILE_PAGE, + passProps: { userId: user.id }, + }, + }); + } }} onEditPress={this.toggleEditor} onDelete={this.deletePost} @@ -389,7 +398,7 @@ export class Post extends PureComponent { latestComments={latestComments} hideEmbeds={nsfw && !overlayRemoved} isTruncated - navigation={navigation} + componentId={componentId} /> )} diff --git a/src/screens/Feed/components/Post/components/CommentFlatList/component.js b/src/screens/Feed/components/Post/components/CommentFlatList/component.js index 6e949a0de..81f1dc6b8 100644 --- a/src/screens/Feed/components/Post/components/CommentFlatList/component.js +++ b/src/screens/Feed/components/Post/components/CommentFlatList/component.js @@ -2,13 +2,15 @@ import React, { PureComponent } from 'react'; import { FlatList, View } from 'react-native'; import PropTypes from 'prop-types'; import { Comment } from 'kitsu/screens/Feed/components/Comment'; +import { Navigation } from 'react-native-navigation'; +import { Screens } from 'kitsu/navigation'; export class CommentFlatList extends PureComponent { static propTypes = { post: PropTypes.object.isRequired, hideEmbeds: PropTypes.bool, latestComments: PropTypes.array.isRequired, - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, isTruncated: PropTypes.bool, } @@ -21,7 +23,7 @@ export class CommentFlatList extends PureComponent { const { post, hideEmbeds, - navigation, + componentId, latestComments, isTruncated, } = this.props; @@ -33,10 +35,15 @@ export class CommentFlatList extends PureComponent { navigation.navigate('ProfilePages', { userId: id })} + onAvatarPress={id => Navigation.push(componentId, { + component: { + name: Screens.PROFILE_PAGE, + passProps: { userId: id }, + }, + })} isTruncated={isTruncated} hideEmbeds={hideEmbeds} - navigation={navigation} + componentId={componentId} /> )} ItemSeparatorComponent={() => } diff --git a/src/screens/Feed/components/Post/components/PostMain/component.js b/src/screens/Feed/components/Post/components/PostMain/component.js index 885541a3e..1cf7ccd38 100644 --- a/src/screens/Feed/components/Post/components/PostMain/component.js +++ b/src/screens/Feed/components/Post/components/PostMain/component.js @@ -20,8 +20,10 @@ export const PostMain = ({ commentsCount, taggedMedia, taggedEpisode, - navigation, + componentId, onPress, + onStatusPress, + showViewParent, }) => { const hasContentAbove = !isEmpty(content) || taggedMedia; return ( @@ -29,7 +31,7 @@ export const PostMain = ({ {!isEmpty(content) && - handleURL(url, navigation)}> + handleURL(url)}> )} @@ -59,10 +61,10 @@ export const PostMain = ({ maxWidth={scene.width} minWidth={scene.width} style={[styles.postImagesView, !hasContentAbove && styles.postImagesView_noText]} - navigation={navigation} + componentId={componentId} /> } - + ); }; @@ -76,8 +78,10 @@ PostMain.propTypes = { commentsCount: PropTypes.number, taggedMedia: PropTypes.object, taggedEpisode: PropTypes.object, - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, onPress: PropTypes.func, + onStatusPress: PropTypes.func, + showViewParent: PropTypes.bool, }; PostMain.defaultProps = { cacheKey: null, @@ -89,4 +93,6 @@ PostMain.defaultProps = { taggedMedia: null, taggedEpisode: null, onPress: null, + onStatusPress: null, + showViewParent: false, }; diff --git a/src/screens/Feed/components/Post/components/PostOverlay/component.js b/src/screens/Feed/components/Post/components/PostOverlay/component.js index 9a8955175..75a8b82b7 100644 --- a/src/screens/Feed/components/Post/components/PostOverlay/component.js +++ b/src/screens/Feed/components/Post/components/PostOverlay/component.js @@ -16,7 +16,7 @@ export const PostOverlay = ({ taggedEpisode, likesCount, commentsCount, - navigation, + componentId, }) => { let postOverlay = ; @@ -35,7 +35,7 @@ export const PostOverlay = ({ )} @@ -46,7 +46,7 @@ export const PostOverlay = ({ PostOverlay.propTypes = { nsfw: PropTypes.bool, spoiler: PropTypes.bool, - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, likesCount: PropTypes.number, commentsCount: PropTypes.number, taggedMedia: PropTypes.object, diff --git a/src/screens/Feed/components/Post/components/PostStatus/component.js b/src/screens/Feed/components/Post/components/PostStatus/component.js index 67a52c25b..e16859b9a 100644 --- a/src/screens/Feed/components/Post/components/PostStatus/component.js +++ b/src/screens/Feed/components/Post/components/PostStatus/component.js @@ -1,25 +1,28 @@ import React from 'react'; -import { View, TouchableWithoutFeedback } from 'react-native'; +import { View, TouchableOpacity } from 'react-native'; import PropTypes from 'prop-types'; import { StyledText } from 'kitsu/components/StyledText'; import { styles } from './styles'; -export const PostStatus = ({ likesCount, commentsCount, onPress }) => ( - +export const PostStatus = ({ showViewParent, likesCount, commentsCount, onPress }) => ( + {likesCount} likes + {showViewParent && View Parent Post} {commentsCount} comments - + ); PostStatus.propTypes = { likesCount: PropTypes.number, commentsCount: PropTypes.number, + showViewParent: PropTypes.bool, onPress: PropTypes.func, }; PostStatus.defaultProps = { + showViewParent: false, likesCount: 0, commentsCount: 0, onPress: null, diff --git a/src/screens/Feed/components/PostCreator/component.js b/src/screens/Feed/components/PostCreator/component.js index a4e749b8c..cf42f2181 100644 --- a/src/screens/Feed/components/PostCreator/component.js +++ b/src/screens/Feed/components/PostCreator/component.js @@ -12,7 +12,6 @@ import { PostTextInput } from 'kitsu/screens/Feed/components/PostTextInput'; import { GiphyModal } from 'kitsu/screens/Feed/components/GiphyModal'; import { MediaModal } from 'kitsu/screens/Feed/components/MediaModal'; import { feedStreams } from 'kitsu/screens/Feed/feedStreams'; -import { CheckBox } from 'react-native-elements'; import { ImageUploader } from 'kitsu/utils/imageUploader'; import { kitsuConfig } from 'kitsu/config/env'; import ImagePicker from 'react-native-image-crop-picker'; @@ -23,6 +22,7 @@ import { prettyBytes } from 'kitsu/utils/prettyBytes'; import Icon from 'react-native-vector-icons/Ionicons'; import { extractUrls } from 'kitsu/utils/url'; import { Sentry } from 'react-native-sentry'; +import { CheckBox } from 'kitsu/components/Checkbox'; import { MediaItem } from './components/MediaItem'; import { EmbedItem } from './components/EmbedItem'; import { styles } from './styles'; diff --git a/src/screens/Feed/components/PostCreator/components/EmbedModal/component.js b/src/screens/Feed/components/PostCreator/components/EmbedModal/component.js index 2c96dfa01..2770503e6 100644 --- a/src/screens/Feed/components/PostCreator/components/EmbedModal/component.js +++ b/src/screens/Feed/components/PostCreator/components/EmbedModal/component.js @@ -83,20 +83,22 @@ export class EmbedModal extends PureComponent { transparent={false} onRequestClose={this.handleCancelPress} > - - } - keyExtractor={item => item} - renderItem={this.renderItem} - /> + + + } + keyExtractor={item => item} + renderItem={this.renderItem} + /> + ); } diff --git a/src/screens/Feed/components/PostCreator/components/EmbedModal/styles.js b/src/screens/Feed/components/PostCreator/components/EmbedModal/styles.js index f44229f39..2de8640a9 100644 --- a/src/screens/Feed/components/PostCreator/components/EmbedModal/styles.js +++ b/src/screens/Feed/components/PostCreator/components/EmbedModal/styles.js @@ -1,7 +1,12 @@ import { StyleSheet, Platform } from 'react-native'; import * as colors from 'kitsu/constants/colors'; +import { isX, safeAreaInsetX } from 'kitsu/utils/isX'; export const styles = StyleSheet.create({ + container: { + marginBottom: isX ? safeAreaInsetX.bottom : 0, + flex: 1, + }, seperator: { height: StyleSheet.hairlineWidth, backgroundColor: colors.lightestGrey, diff --git a/src/screens/Feed/components/PostCreator/styles.js b/src/screens/Feed/components/PostCreator/styles.js index 89d8d5d80..017056885 100644 --- a/src/screens/Feed/components/PostCreator/styles.js +++ b/src/screens/Feed/components/PostCreator/styles.js @@ -1,12 +1,13 @@ import { StyleSheet } from 'react-native'; import * as colors from 'kitsu/constants/colors'; -import { isX, paddingX } from 'kitsu/utils/isX'; +import { isX, paddingX, safeAreaInsetX } from 'kitsu/utils/isX'; import { navigationBarHeight, statusBarHeight } from 'kitsu/constants/app'; export const styles = StyleSheet.create({ main: { flex: 1, backgroundColor: '#FFFFFF', + marginBottom: isX ? safeAreaInsetX.bottom : 0, }, flex: { flex: 1, diff --git a/src/screens/Feed/components/TabBar/styles.js b/src/screens/Feed/components/TabBar/styles.js index c52796102..47655d40d 100644 --- a/src/screens/Feed/components/TabBar/styles.js +++ b/src/screens/Feed/components/TabBar/styles.js @@ -1,12 +1,11 @@ import { StyleSheet } from 'react-native'; import * as colors from 'kitsu/constants/colors'; import { scenePadding } from 'kitsu/screens/Feed/constants'; -import { statusBarHeight, navigationBarHeight } from 'kitsu/constants/app'; +import { navigationBarHeight } from 'kitsu/constants/app'; export const styles = StyleSheet.create({ tabBar: { flexDirection: 'row', - paddingTop: statusBarHeight, paddingHorizontal: scenePadding, backgroundColor: colors.listBackPurple, alignItems: 'center', diff --git a/src/screens/Feed/index.js b/src/screens/Feed/index.js index 4c0c9ce17..7018bad6f 100644 --- a/src/screens/Feed/index.js +++ b/src/screens/Feed/index.js @@ -14,13 +14,17 @@ import { SceneLoader } from 'kitsu/components/SceneLoader'; import { isX, paddingX } from 'kitsu/utils/isX'; import { isEmpty } from 'lodash'; import { FeedCache } from 'kitsu/utils/cache'; +import { Navigation } from 'react-native-navigation'; +import { Screens, NavigationActions } from 'kitsu/navigation'; +import { statusBarHeight } from 'kitsu/constants/app'; +import { registerDeepLinks, unregisterDeepLinks } from 'kitsu/utils/deeplink'; import { feedStreams } from './feedStreams'; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: listBackPurple, - paddingTop: isX ? paddingX : 0, + paddingTop: statusBarHeight + (isX ? paddingX : 0), }, contentContainer: { flex: 1, @@ -30,13 +34,20 @@ const styles = StyleSheet.create({ class Feed extends React.PureComponent { static propTypes = { - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, currentUser: PropTypes.object.isRequired, }; - static navigationOptions = { - header: null, - }; + static options() { + return { + sideMenu: { + left: { + // Enable side drawer only for Feed + enabled: true, + }, + }, + }; + } state = { activeFeed: 'followingFeed', @@ -45,9 +56,14 @@ class Feed extends React.PureComponent { data: [], }; - componentDidMount = () => { + componentDidMount() { + registerDeepLinks(); this.fetchFeed(); - }; + } + + componentWillUnmount() { + unregisterDeepLinks(); + } onRefresh = async () => { this.setState({ refreshing: true }); @@ -55,6 +71,16 @@ class Feed extends React.PureComponent { this.setState({ refreshing: false }); }; + onDrawer = () => { + Navigation.mergeOptions(Screens.SIDEBAR, { + sideMenu: { + left: { + visible: true, + }, + }, + }); + } + setActiveFeed = (activeFeed) => { this.setState( { @@ -150,23 +176,38 @@ class Feed extends React.PureComponent { }; navigateToPost = (props) => { - this.props.navigation.navigate('PostDetails', props); - }; + Navigation.push(this.props.componentId, { + component: { + name: Screens.FEED_POST_DETAILS, + passProps: props, + }, + }); + } navigateToCreatePost = () => { if (this.props.currentUser) { - this.props.navigation.navigate('CreatePost', { + NavigationActions.showCreatePostModal({ onPostCreated: () => this.fetchFeed({ reset: true }), }); } }; navigateToUserProfile = (userId) => { - this.props.navigation.navigate('ProfilePages', { userId }); + Navigation.push(this.props.componentId, { + component: { + name: Screens.PROFILE_PAGE, + passProps: { userId }, + }, + }); }; navigateToMedia = ({ mediaId, mediaType }) => { - this.props.navigation.navigate('MediaPages', { mediaId, mediaType }); + Navigation.push(this.props.componentId, { + component: { + name: Screens.MEDIA_PAGE, + passProps: { mediaId, mediaType }, + }, + }); }; keyExtractor = (item, index) => { @@ -183,7 +224,7 @@ class Feed extends React.PureComponent { post={item} onPostPress={this.navigateToPost} currentUser={this.props.currentUser} - navigation={this.props.navigation} + componentId={this.props.componentId} /> ); case 'comments': @@ -195,10 +236,6 @@ class Feed extends React.PureComponent { } }; - onDrawer = () => { - this.props.navigation.navigate('DrawerToggle'); - } - render() { return ( diff --git a/src/screens/Feed/pages/PostCreation/CreatePost.js b/src/screens/Feed/pages/PostCreation/CreatePost.js index bcf462654..7abbd96b7 100644 --- a/src/screens/Feed/pages/PostCreation/CreatePost.js +++ b/src/screens/Feed/pages/PostCreation/CreatePost.js @@ -1,19 +1,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import { PostCreator } from 'kitsu/screens/Feed/components/PostCreator'; +import { Navigation } from 'react-native-navigation'; export default class CreatePost extends React.PureComponent { static propTypes = { - navigation: PropTypes.object.isRequired, + componentId: PropTypes.any.isRequired, + targetUser: PropTypes.object, + spoiledUnit: PropTypes.object, + nsfw: PropTypes.bool, + spoiler: PropTypes.bool, + media: PropTypes.object, + post: PropTypes.object, + onPostCreated: PropTypes.func, + disableMedia: PropTypes.bool, } - static navigationOptions = { - header: null, - }; + static defaultProps = { + targetUser: null, + spoiledUnit: null, + nsfw: false, + spoiler: false, + media: null, + post: null, + disableMedia: false, + onPostCreated: null, + } render() { - const { navigation } = this.props; const { + componentId, targetUser, spoiledUnit, nsfw, @@ -22,15 +38,15 @@ export default class CreatePost extends React.PureComponent { post, onPostCreated, disableMedia, - } = navigation.state.params; + } = this.props; return ( { - onPostCreated(newPost); - navigation.goBack(null); + if (onPostCreated) onPostCreated(newPost); + Navigation.dismissModal(componentId); }} - onCancel={() => navigation.goBack(null)} + onCancel={() => Navigation.dismissModal(componentId)} post={post} media={media} spoiledUnit={spoiledUnit} diff --git a/src/screens/Feed/pages/PostDetails.js b/src/screens/Feed/pages/PostDetails.js index 46a7fd978..9b894d33b 100644 --- a/src/screens/Feed/pages/PostDetails.js +++ b/src/screens/Feed/pages/PostDetails.js @@ -6,10 +6,12 @@ import { StatusBar, ScrollView, Platform, + TouchableOpacity, + Text, } from 'react-native'; import { PropTypes } from 'prop-types'; import { Kitsu } from 'kitsu/config/api'; -import { defaultAvatar } from 'kitsu/constants/app'; +import { defaultAvatar, statusBarHeight } from 'kitsu/constants/app'; import { PostFooter, PostSection, @@ -24,53 +26,96 @@ import { isX, paddingX } from 'kitsu/utils/isX'; import { preprocessFeedPosts, preprocessFeedPost } from 'kitsu/utils/preprocessFeed'; import * as colors from 'kitsu/constants/colors'; import { extractUrls } from 'kitsu/utils/url'; -import { isEmpty, uniqBy } from 'lodash'; +import { isEmpty, uniqBy, isNull } from 'lodash'; +import { Navigation } from 'react-native-navigation'; +import { Screens } from 'kitsu/navigation'; +import { StyledText } from 'kitsu/components/StyledText'; +import { scenePadding } from 'kitsu/screens/Feed/constants'; export default class PostDetails extends PureComponent { - static navigationOptions = { - header: null, + static propTypes = { + componentId: PropTypes.any.isRequired, + currentUser: PropTypes.object.isRequired, + post: PropTypes.object.isRequired, + postLikesCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + comments: PropTypes.arrayOf(PropTypes.object), + topLevelCommentsCount: PropTypes.number, + commentsCount: PropTypes.number, + like: PropTypes.object, + isLiked: PropTypes.bool, + syncComments: PropTypes.func, + showLoadMoreComments: PropTypes.bool, }; - static propTypes = { - navigation: PropTypes.object.isRequired, + static defaultProps = { + postLikesCount: 0, + comments: [], + topLevelCommentsCount: null, + commentsCount: null, + like: null, + isLiked: false, + syncComments: null, + showLoadMoreComments: false, }; + static options() { + return { + layout: { + backgroundColor: 'white', + }, + }; + } + constructor(props) { super(props); - const { post, postLikesCount } = props.navigation.state.params; + const { post, postLikesCount, comments, like, isLiked } = props; const postLikes = parseInt(postLikesCount, 10) || parseInt(post.postLikesCount, 10) || parseInt(post.likesCount, 10) || 0; + const topLevelCommentsCount = + parseInt(props.topLevelCommentsCount, 10) || + parseInt(post.topLevelCommentsCount, 10) || + parseInt(post.repliesCount, 10) || 0; + + const commentsCount = + parseInt(props.commentsCount, 10) || + parseInt(post.commentsCount, 10) || + parseInt(post.repliesCount, 10) || 0; + this.state = { comment: '', - comments: props.navigation.state.params.comments || [], - topLevelCommentsCount: props.navigation.state.params.topLevelCommentsCount, - commentsCount: props.navigation.state.params.commentsCount, - like: props.navigation.state.params.like, - isLiked: props.navigation.state.params.isLiked, + comments, + topLevelCommentsCount, + commentsCount, + like, + isLiked, postLikesCount: postLikes, - taggedMedia: { - media: { - canonicalTitle: 'Made in Abyss', - }, - episode: 1, - }, isLoadingNextPage: false, isReplying: false, isPostingComment: false, embedUrl: null, + showLoadMoreComments: props.showLoadMoreComments, + isLoadingComments: false, }; } componentDidMount() { - const { comments, like } = this.props.navigation.state.params; + const { comments, like } = this.props; if (!comments || comments.length === 0) { this.fetchComments(); } if (!like) { this.fetchLikes(); } } + onViewAllComment = () => { + if (this.state.isLoadingComments) return; + + this.setState({ comments: [], showLoadMoreComments: false }, () => { + this.fetchComments(); + }); + } + onCommentChanged = comment => this.setState({ comment }); onGifSelected = (gif) => { @@ -87,11 +132,13 @@ export default class PostDetails extends PureComponent { onSubmitComment = async () => { if (isEmpty(this.state.comment.trim()) || this.state.isPostingComment) return; + const { currentUser, post, syncComments } = this.props; + + const isComment = post.type === 'comments'; + this.setState({ isPostingComment: true }); try { - const { currentUser, post, syncComments } = this.props.navigation.state.params; - // Update the embed let embedUrl = this.state.embedUrl; @@ -101,23 +148,37 @@ export default class PostDetails extends PureComponent { embedUrl = links[0]; } + // If we have a reply ref then use that as the parent comment otherwise + // If we have a comment as the main `post` then use its id + let commentId = null; + if (this.replyRef) { + commentId = this.replyRef.comment.id; + } else if (isComment) { + const comment = post.parent ? post.parent : post; + commentId = comment.id; + } + // Check if this is a reply rather than a top-level comment let replyOptions = {}; - if (this.replyRef) { + if (commentId) { replyOptions = { parent: { - id: this.replyRef.comment.id, + id: commentId, type: 'comments', }, ...replyOptions, }; } + + // If we have a comment as the `post` then we need to use its original post id + const postId = isComment ? post.post && post.post.id : post.id; + if (!postId) return; const comment = await Kitsu.create('comments', { content: this.state.comment.trim(), embedUrl, post: { - id: post.id, + id: postId, type: 'posts', }, user: { @@ -136,10 +197,14 @@ export default class PostDetails extends PureComponent { commentsCount: this.state.commentsCount + 1, }); - if (this.replyRef) { + // If we have a reply ref and the main `post` is not a comment + // Then trigger the callback + const shouldCallReplyRefCallback = this.replyRef && !isComment; + if (shouldCallReplyRefCallback) { this.replyRef.callback(comment); this.replyRef = null; } else { + this.replyRef = null; const uniqueComments = uniqBy([...this.state.comments, processed], 'id'); this.setState({ comments: uniqueComments, @@ -177,29 +242,61 @@ export default class PostDetails extends PureComponent { comment: `@${mention} `, isReplying: true, }); - this.replyRef = { comment, mention, name, callback }; + + // If the comment has a parent then use that as the parent comment too + const refComment = comment && comment.parent ? comment.parent : comment; + this.replyRef = { comment: refComment, mention, name, callback }; this.focusOnCommentInput(); }; + onViewParentPress = () => { + const { post, currentUser, componentId } = this.props; + if (!post || !post.post) return; + + Navigation.push(componentId, { + component: { + name: Screens.FEED_POST_DETAILS, + passProps: { + post: post.post, + comments: [], + like: null, + currentUser, + }, + }, + }); + } + toggleLike = async () => { try { - const { currentUser, post } = this.props.navigation.state.params; + const { currentUser, post } = this.props; let { like, isLiked, postLikesCount } = this.state; + const isComment = post.type === 'comments'; + const likesEndpoint = isComment ? 'commentLikes' : 'postLikes'; + this.setState({ isLiked: !isLiked, postLikesCount: isLiked ? postLikesCount - 1 : postLikesCount + 1, }); if (like) { - await Kitsu.destroy('postLikes', like.id); + await Kitsu.destroy(likesEndpoint, like.id); this.setState({ like: null }); } else { - like = await Kitsu.create('postLikes', { + const relationship = isComment ? { + comment: { + id: post.id, + type: 'comments', + }, + } : { post: { id: post.id, type: 'posts', }, + }; + + like = await Kitsu.create(likesEndpoint, { + ...relationship, user: { id: currentUser.id, type: 'users', @@ -218,18 +315,31 @@ export default class PostDetails extends PureComponent { }; fetchComments = async (requestOptions = {}) => { + this.setState({ isLoadingComments: true }); try { - const { post } = this.props.navigation.state.params; + const { post } = this.props; - const comments = await Kitsu.findAll('comments', { - filter: { + let filter = {}; + + // If the main post object is actually a post then we need to get the first level comments + if (post.type === 'posts') { + filter = { postId: post.id, parentId: '_none', - }, + }; + // If however they passed us a comment to be used as the main, then we need to fetch the second level comments + } else if (post.type === 'comments') { + filter = { + parentId: post.id, + }; + } + + const comments = await Kitsu.findAll('comments', { + filter, fields: { users: 'slug,avatar,name', }, - include: 'user,uploads', + include: 'user,uploads,parent,post', sort: '-createdAt', ...requestOptions, }); @@ -237,18 +347,24 @@ export default class PostDetails extends PureComponent { const processed = preprocessFeedPosts(comments); const uniqueComments = uniqBy([...processed.reverse(), ...this.state.comments], 'id'); - this.setState({ comments: uniqueComments }); + this.setState({ comments: uniqueComments, isLoadingComments: false }); } catch (err) { + this.setState({ isLoadingComments: false }); console.log('Error fetching comments: ', err); } }; fetchLikes = async () => { - const { currentUser, post } = this.props.navigation.state.params; + const { currentUser, post } = this.props; + + const isComment = post.type === 'comments'; + const likesEndpoint = isComment ? 'commentLikes' : 'postLikes'; + const idKey = isComment ? 'commentId' : 'postId'; + try { - const likes = await Kitsu.findAll('postLikes', { + const likes = await Kitsu.findAll(likesEndpoint, { filter: { - postId: post.id, + [idKey]: post.id, userId: currentUser.id, }, include: 'user', @@ -270,23 +386,30 @@ export default class PostDetails extends PureComponent { }; goBack = () => { - this.props.navigation.goBack(); + Navigation.pop(this.props.componentId); }; keyExtractor = item => `${item.id}`; navigateToUserProfile = (userId) => { - if (userId) this.props.navigation.navigate('ProfilePages', { userId }); + if (userId) { + Navigation.push(this.props.componentId, { + component: { + name: Screens.PROFILE_PAGE, + passProps: { userId }, + }, + }); + } }; renderItem = ({ item }) => { - const { currentUser, post } = this.props.navigation.state.params; + const { currentUser, post, componentId } = this.props; return ( this.navigateToUserProfile(id)} onReplyPress={(user, callback) => this.onReplyPress(item, user, callback)} /> @@ -296,18 +419,20 @@ export default class PostDetails extends PureComponent { renderItemSeperatorComponent = () => ; render() { - // We expect to have navigated here using react-navigation, and it takes all our props - // and jams them over into this crazy thing. - const { currentUser, post } = this.props.navigation.state.params; + const { currentUser, post, componentId } = this.props; const { comment, comments, commentsCount, topLevelCommentsCount, isLiked, postLikesCount, - isPostingComment } = this.state; + isPostingComment, showLoadMoreComments, isLoadingComments } = this.state; const { id, updatedAt, content, embed, media, spoiledUnit, uploads } = post; return ( @@ -322,7 +447,6 @@ export default class PostDetails extends PureComponent { time={post.createdAt} onBackButtonPress={this.goBack} /> - - - {comments.length === 0 && topLevelCommentsCount > 0 && } - {comments.length > 0 && topLevelCommentsCount > comments.length && ( + {showLoadMoreComments && + + View All Comments + + } + {(isLoadingComments || (comments.length === 0 && topLevelCommentsCount > 0)) && + + } + {comments.length > 0 && topLevelCommentsCount > comments.length && !showLoadMoreComments && ( { const SCREEN_WIDTH = Dimensions.get('window').width; const position = x / SCREEN_WIDTH; if (!this.navigating && position > INTROS.length - 2 + 0.05) { - this.props.navigation.navigate('Registration'); + this.navigateToRegistration(); this.navigating = true; // prevent triggering navigate twice. } else { // abs for -x direction values: prevent -1 value for step @@ -58,14 +62,18 @@ export default class OnboardingScreen extends React.Component { } }; + navigateToRegistration = () => { + Navigation.setStackRoot(this.props.componentId, { + component: { name: Screens.AUTH_REGISTRATION }, + }); + } + renderStep = () => INTROS.map((item, index) => ); renderDots = () => INTROS.map((_, index) => ); render() { - const { navigate } = this.props.navigation; - return ( @@ -88,7 +96,7 @@ export default class OnboardingScreen extends React.Component { style={styles.getStartedButton} title={'Get Started'} titleStyle={styles.getStartedText} - onPress={() => navigate('Registration')} + onPress={this.navigateToRegistration} /> diff --git a/src/screens/Intro/RegistrationScreen.js b/src/screens/Intro/RegistrationScreen.js index d5a314a60..03736f604 100644 --- a/src/screens/Intro/RegistrationScreen.js +++ b/src/screens/Intro/RegistrationScreen.js @@ -9,14 +9,12 @@ import * as colors from 'kitsu/constants/colors'; import { placeholderImage } from 'kitsu/assets/img/intro'; import { kitsuConfig } from 'kitsu/config/env'; import { Sentry } from 'react-native-sentry'; +import { Navigation } from 'react-native-navigation'; +import { Screens } from 'kitsu/navigation'; import { IntroHeader } from './common/'; import styles from './styles'; class RegistrationScreen extends React.Component { - static navigationOptions = { - header: null, - }; - state = { loggingUser: false, topAnime: Array(10).fill({}), @@ -66,13 +64,13 @@ class RegistrationScreen extends React.Component { }; loginFacebook = () => { - const { navigation } = this.props; + const { componentId } = this.props; this.setState({ loggingUser: true }); LoginManager.logOut(); LoginManager.logInWithReadPermissions(['public_profile', 'email']).then( (result) => { if (!result.isCancelled) { - this.props.loginUser(null, navigation, 'registration'); + this.props.loginUser(null, componentId, 'registration'); } else { this.setState({ loggingUser: false }); } @@ -96,6 +94,15 @@ class RegistrationScreen extends React.Component { }); }; + navigateToAuthScreen = (type) => { + Navigation.setStackRoot(this.props.componentId, { + component: { + name: Screens.AUTH_LOGIN, + passProps: { authType: type }, + }, + }); + } + keyExtractor = (item, index) => index.toString(); renderItem = ({ item }) => ( @@ -107,7 +114,6 @@ class RegistrationScreen extends React.Component { ); render() { - const { navigate } = this.props.navigation; const { loggingUser, topAnime, topManga } = this.state; // TODO: make this screen responsive. // TODO: as of react native 0.47, flatlist has inverted prop @@ -156,13 +162,13 @@ class RegistrationScreen extends React.Component {