Repository: cheeaun/hackerweb-native Branch: master Commit: c823767694a6 Files: 61 Total size: 135.0 KB Directory structure: gitextract_aon_j7yw/ ├── .buckconfig ├── .flowconfig ├── .gitignore ├── .npmignore ├── .watchmanconfig ├── README.md ├── actions/ │ ├── LinkActions.js │ └── StoryActions.js ├── alt.js ├── android/ │ ├── android.iml │ ├── app/ │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ ├── react.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── cheeaun/ │ │ │ └── hackerweb/ │ │ │ └── MainActivity.java │ │ └── res/ │ │ ├── values/ │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-v23/ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── colors.js ├── components/ │ ├── Button.android.js │ ├── Button.ios.js │ ├── Comment.js │ ├── CommentRow.js │ ├── CommentsThread.js │ ├── HTMLView.js │ ├── LoadingIndicator.js │ ├── ProgressBar.js │ └── StoryRow.js ├── index.android.js ├── index.ios.js ├── ios/ │ ├── HackerWeb/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj/ │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── app-icon.imageset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ ├── HackerWeb.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── HackerWeb.xcscheme │ └── HackerWebTests/ │ ├── HackerWebTests.m │ └── Info.plist ├── package.json ├── stores/ │ ├── LinkStore.js │ └── StoryStore.js ├── utils/ │ ├── domainify.js │ ├── showActivity.android.js │ ├── showActivity.ios.js │ ├── showBrowser.android.js │ └── showBrowser.ios.js └── views/ ├── AboutView.js ├── CommentsView.js └── StoriesView.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .buckconfig ================================================ [android] target = Google Inc.:Google APIs:23 [maven_repositories] central = https://repo1.maven.org/maven2 ================================================ FILE: .flowconfig ================================================ [ignore] # We fork some components by platform. .*/*.web.js .*/*.android.js # Some modules have their own node_modules with overlap .*/node_modules/node-haste/.* # Ugh .*/node_modules/babel.* .*/node_modules/babylon.* .*/node_modules/invariant.* # Ignore react and fbjs where there are overlaps, but don't ignore # anything that react-native relies on .*/node_modules/fbjs/lib/Map.js .*/node_modules/fbjs/lib/ErrorUtils.js # Flow has a built-in definition for the 'react' module which we prefer to use # over the currently-untyped source .*/node_modules/react/react.js .*/node_modules/react/lib/React.js .*/node_modules/react/lib/ReactDOM.js .*/__mocks__/.* .*/__tests__/.* .*/commoner/test/source/widget/share.js # Ignore commoner tests .*/node_modules/commoner/test/.* # See https://github.com/facebook/flow/issues/442 .*/react-tools/node_modules/commoner/lib/reader.js # Ignore jest .*/node_modules/jest-cli/.* # Ignore Website .*/website/.* # Ignore generators .*/local-cli/generator.* # Ignore BUCK generated folders .*\.buckd/ # Ignore RNPM .*/local-cli/rnpm/.* .*/node_modules/is-my-json-valid/test/.*\.json .*/node_modules/iconv-lite/encodings/tables/.*\.json .*/node_modules/y18n/test/.*\.json .*/node_modules/spdx-license-ids/spdx-license-ids.json .*/node_modules/spdx-exceptions/index.json .*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json .*/node_modules/resolve/lib/core.json .*/node_modules/jsonparse/samplejson/.*\.json .*/node_modules/json5/test/.*\.json .*/node_modules/ua-parser-js/test/.*\.json .*/node_modules/builtin-modules/builtin-modules.json .*/node_modules/binary-extensions/binary-extensions.json .*/node_modules/url-regex/tlds.json .*/node_modules/joi/.*\.json .*/node_modules/isemail/.*\.json .*/node_modules/tr46/.*\.json [include] [libs] node_modules/react-native/Libraries/react-native/react-native-interface.js node_modules/react-native/flow flow/ [options] module.system=haste esproposal.class_static_fields=enable esproposal.class_instance_fields=enable munge_underscores=true module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] ^0.25.0 ================================================ FILE: .gitignore ================================================ # OSX # .DS_Store # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate project.xcworkspace # Android/IJ # .idea .gradle local.properties # node.js # node_modules/ npm-debug.log # BUCK buck-out/ \.buckd/ android/app/libs android/keystores/debug.keystore # CUSTOM *.keystore ================================================ FILE: .npmignore ================================================ # OSX # .DS_Store # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate # node.js # node_modules/ npm-debug.log ================================================ FILE: .watchmanconfig ================================================ {} ================================================ FILE: README.md ================================================ HackerWeb === A simply readable Hacker News app for iOS and Android. Read about the story here: [Building HackerWeb for iOS](http://cheeaun.com/blog/2016/03/building-hackerweb-ios/). Preview --- ### iOS ![Preview on iOS](media/hackerweb-preview-ios.gif) ### Android ![Preview on Android](media/hackerweb-preview-android.gif) Development --- ### Requirements - [React Native](https://facebook.github.io/react-native/) - [React Native Package Manager](https://github.com/rnpm/rnpm) - [Node.js](https://nodejs.org/) - [Xcode](https://developer.apple.com/xcode/) - [iOS](https://www.apple.com/ios/) **9** - Android SDK API **23** (minimum support: 21) ### Getting started - `npm install` - Install all dependencies - `rnpm link` - Linking all native dependencies ### Implementations - [Alt](http://alt.js.org/) - the Flux thing - [htmlparser2](https://github.com/fb55/htmlparser2) - for parsing the comments HTML - [url-parse](https://github.com/unshiftio/url-parse) - for extracting domains out of story URLs - **iOS** - [react-native-safari-view](https://github.com/naoufal/react-native-safari-view) - the reason why iOS 9 is the minimum requirement - **Android** - [react-native-chrome-custom-tabs](https://github.com/dstaley/react-native-chrome-custom-tabs) - same as SafariView for iOS, but for Chrome - [react-native-android-share](https://github.com/haydenth/react-native-android-share) - for sharing ### Components - `LoadingIndicator` - inspired by [react-native-activity-indicator-ios](https://github.com/pwmckenna/react-native-activity-indicator-ios) - `HTMLView` - inspired by [react-native-htmlview](https://github.com/jsdf/react-native-htmlview), [react-native-htmltext](https://github.com/siuying/react-native-htmltext) and [react-native-html-render](https://github.com/soliury/react-native-html-render) - [react-native-cache-store](https://github.com/cheeaun/react-native-cache-store) - for local caching Similar apps --- - [HackerNews-React-Native](https://github.com/iSimar/HackerNews-React-Native) - [ReactNativeHackerNews](https://github.com/jsdf/ReactNativeHackerNews) License --- [MIT](http://cheeaun.mit-license.org/). ================================================ FILE: actions/LinkActions.js ================================================ 'use strict'; import alt from '../alt'; import CacheStore from 'react-native-cache-store'; class LinkActions { getLinks(){ return (dispatch) => { CacheStore.get('links') .then((links) => dispatch(links || [])) .catch(() => {}); }; } addLink(link){ return (dispatch) => { CacheStore.get('links') .then((links) => { links = links || []; if (links.indexOf(link) < 0){ dispatch(link); links.unshift(link); CacheStore.set('links', links.slice(0, 100)); } }) .catch(() => {}); }; } } export default alt.createActions(LinkActions); ================================================ FILE: actions/StoryActions.js ================================================ 'use strict'; import alt from '../alt'; import CacheStore from 'react-native-cache-store'; const API_HOST = 'https://api.hackerwebapp.com/'; const FETCH_TIMEOUT = 20000; // 20 seconds function fetchTimeout(){ return new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Response timeout.')), FETCH_TIMEOUT); }) }; const MAX_RETRIES = 3; function betterFetch(url, times){ times = times || 0; return new Promise((resolve, reject) => { fetch(url) .then((response) => response.json()) .then(resolve) .catch((times >= MAX_RETRIES) ? reject : (e) => { setTimeout(() => { betterFetch(url, times+1).then(resolve).catch(reject); }, 500); }); }); }; class StoryActions { updateStories(stories){ return stories; } updateMoreStories(stories){ return stories; } flush(){ CacheStore.flush(); // Clears everything } fetchStories(){ return (dispatch) => { dispatch(); var request = () => { Promise.race([ betterFetch(API_HOST + 'news'), fetchTimeout() ]) .then((stories) => { if (!stories || !stories.length) throw new Error('Stories payload is empty'); this.updateStories(stories); CacheStore.set('stories', stories, 10); // 10 minutes }) .catch(this.storiesFailed); // Meanwhile... betterFetch(API_HOST + 'news2') .then((stories) => { if (!stories || !stories.length) return; this.hasMoreStories(); CacheStore.set('stories2', stories, 10); // 10 minutes }) .catch(() => {}); }; CacheStore.get('stories').then((stories) => { if (stories){ this.updateStories(stories); } else { request(); } }).catch(request); CacheStore.get('stories2').then((stories) => { if (stories) this.hasMoreStories(); }).catch(() => {}); }; } hasMoreStories(){ return true; } fetchMoreStories(){ CacheStore.get('stories2').then((stories) => { if (stories) this.updateMoreStories(stories); }).catch(() => {}); } fetchStoriesIfExpired(){ CacheStore.isExpired('stories').then(this.fetchStories).catch(() => {}); } storiesFailed(error){ return error; } updateStory(story){ return story; } fetchStory(id) { return (dispatch) => { dispatch(id); const key = `story-${id}`; var request = () => { Promise.race([ betterFetch(API_HOST + 'item/' + id), fetchTimeout() ]) .then((story) => { if (!story) throw new Error('Story payload is empty'); this.updateStory(story); CacheStore.set(key, story, 5); // 5 minutes }) .catch(this.storyFailed); }; CacheStore.get(key).then((story) => { if (story){ this.updateStory(story); } else { request(); } }).catch(request); }; } storyFailed(error){ return error; } } export default alt.createActions(StoryActions); ================================================ FILE: alt.js ================================================ import Alt from 'alt'; export default new Alt(); ================================================ FILE: android/android.iml ================================================ ================================================ FILE: android/app/BUCK ================================================ import re # To learn about Buck see [Docs](https://buckbuild.com/). # To run your application with Buck: # - install Buck # - `npm start` - to start the packager # - `cd android` # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US` # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck # - `buck install -r android/app` - compile, install and run application # lib_deps = [] for jarfile in glob(['libs/*.jar']): name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) lib_deps.append(':' + name) prebuilt_jar( name = name, binary_jar = jarfile, ) for aarfile in glob(['libs/*.aar']): name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) lib_deps.append(':' + name) android_prebuilt_aar( name = name, aar = aarfile, ) android_library( name = 'all-libs', exported_deps = lib_deps ) android_library( name = 'app-code', srcs = glob([ 'src/main/java/**/*.java', ]), deps = [ ':all-libs', ':build_config', ':res', ], ) android_build_config( name = 'build_config', package = 'cheeaun.hackerweb', ) android_resource( name = 'res', res = 'src/main/res', package = 'cheeaun.hackerweb', ) android_binary( name = 'app', package_type = 'debug', manifest = 'src/main/AndroidManifest.xml', keystore = '//android/keystores:debug', deps = [ ':app-code', ], ) ================================================ FILE: android/app/build.gradle ================================================ apply plugin: "com.android.application" import com.android.build.OutputFile /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets * and bundleReleaseJsAndAssets). * These basically call `react-native bundle` with the correct arguments during the Android build * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the * bundle directly from the development server. Below you can see all the possible configurations * and their defaults. If you decide to add a configuration block, make sure to add it before the * `apply from: "../../node_modules/react-native/react.gradle"` line. * * project.ext.react = [ * // the name of the generated asset file containing your JS bundle * bundleAssetName: "index.android.bundle", * * // the entry file for bundle generation * entryFile: "index.android.js", * * // whether to bundle JS and assets in debug mode * bundleInDebug: false, * * // whether to bundle JS and assets in release mode * bundleInRelease: true, * * // whether to bundle JS and assets in another build variant (if configured). * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants * // The configuration property can be in the following formats * // 'bundleIn${productFlavor}${buildType}' * // 'bundleIn${buildType}' * // bundleInFreeDebug: true, * // bundleInPaidRelease: true, * // bundleInBeta: true, * * // the root of your project, i.e. where "package.json" lives * root: "../../", * * // where to put the JS bundle asset in debug mode * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", * * // where to put the JS bundle asset in release mode * jsBundleDirRelease: "$buildDir/intermediates/assets/release", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in debug mode * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in release mode * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", * * // by default the gradle tasks are skipped if none of the JS files or assets change; this means * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to * // date; if you have any other folders that you want to ignore for performance reasons (gradle * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ * // for example, you might want to remove it from here. * inputExcludes: ["android/**", "ios/**"] * ] */ apply from: "../../node_modules/react-native/react.gradle" /** * Set this to true to create two separate APKs instead of one: * - An APK that only works on ARM devices * - An APK that only works on x86 devices * The advantage is the size of the APK is reduced by about 4MB. * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ def enableSeparateBuildPerCPUArchitecture = false /** * Run Proguard to shrink the Java bytecode in release builds. */ def enableProguardInReleaseBuilds = false android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { applicationId "cheeaun.hackerweb" minSdkVersion 21 targetSdkVersion 23 versionCode 3 versionName "1.2" ndk { abiFilters "armeabi-v7a", "x86" } } signingConfigs { release { storeFile file(MYAPP_RELEASE_STORE_FILE) storePassword MYAPP_RELEASE_STORE_PASSWORD keyAlias MYAPP_RELEASE_KEY_ALIAS keyPassword MYAPP_RELEASE_KEY_PASSWORD } } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK include "armeabi-v7a", "x86" } } buildTypes { release { minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits def versionCodes = ["armeabi-v7a":1, "x86":2] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode } } } } dependencies { compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:23.0.1" compile "com.facebook.react:react-native:+" // From node_modules compile project(':ReactNativeChromeCustomTabs') compile project(':RNAndroidShare') } // Run this once to be able to run the application with BUCK // puts all compile dependencies into folder libs for BUCK to use task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' } ================================================ FILE: android/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Disabling obfuscation is useful if you collect stack traces from production crashes # (unless you are using a system that supports de-obfuscate the stack traces). -dontobfuscate # React Native # Keep our interfaces so they can be used by other ProGuard rules. # See http://sourceforge.net/p/proguard/bugs/466/ -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters # Do not strip any method/class that is annotated with @DoNotStrip -keep @com.facebook.proguard.annotations.DoNotStrip class * -keepclassmembers class * { @com.facebook.proguard.annotations.DoNotStrip *; } -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { void set*(***); *** get*(); } -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } -keep class * extends com.facebook.react.bridge.NativeModule { *; } -keepclassmembers,includedescriptorclasses class * { native ; } -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } -dontwarn com.facebook.react.** # okhttp -keepattributes Signature -keepattributes *Annotation* -keep class okhttp3.** { *; } -keep interface okhttp3.** { *; } -dontwarn okhttp3.** # okio -keep class sun.misc.Unsafe { *; } -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** ================================================ FILE: android/app/react.gradle ================================================ import org.apache.tools.ant.taskdefs.condition.Os def config = project.hasProperty("react") ? project.react : []; def bundleAssetName = config.bundleAssetName ?: "index.android.bundle" def entryFile = config.entryFile ?: "index.android.js" // because elvis operator def elvisFile(thing) { return thing ? file(thing) : null; } def reactRoot = elvisFile(config.root) ?: file("../../") def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"] void runBefore(String dependentTaskName, Task task) { Task dependentTask = tasks.findByPath(dependentTaskName); if (dependentTask != null) { dependentTask.dependsOn task } } gradle.projectsEvaluated { // Grab all build types and product flavors def buildTypes = android.buildTypes.collect { type -> type.name } def productFlavors = android.productFlavors.collect { flavor -> flavor.name } // When no product flavors defined, use empty if (!productFlavors) productFlavors.add('') productFlavors.each { productFlavorName -> buildTypes.each { buildTypeName -> // Create variant and target names def targetName = "${productFlavorName.capitalize()}${buildTypeName.capitalize()}" def targetPath = productFlavorName ? "${productFlavorName}/${buildTypeName}" : "${buildTypeName}" // React js bundle directories def jsBundleDirConfigName = "jsBundleDir${targetName}" def jsBundleDir = elvisFile(config."$jsBundleDirConfigName") ?: file("$buildDir/intermediates/assets/${targetPath}") def resourcesDirConfigName = "resourcesDir${targetName}" def resourcesDir = elvisFile(config."${resourcesDirConfigName}") ?: file("$buildDir/intermediates/res/merged/${targetPath}") def jsBundleFile = file("$jsBundleDir/$bundleAssetName") // Bundle task name for variant def bundleJsAndAssetsTaskName = "bundle${targetName}JsAndAssets" def currentBundleTask = tasks.create( name: bundleJsAndAssetsTaskName, type: Exec) { group = "react" description = "bundle JS and assets for ${targetName}." // Create dirs if they are not there (e.g. the "clean" task just ran) doFirst { jsBundleDir.mkdirs() resourcesDir.mkdirs() } // Set up inputs and outputs so gradle can cache the result inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) outputs.dir jsBundleDir outputs.dir resourcesDir // Set up the call to the react-native cli workingDir reactRoot // Set up dev mode def devEnabled = !targetName.toLowerCase().contains("release") if (Os.isFamily(Os.FAMILY_WINDOWS)) { commandLine "cmd", "/c", "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir } else { commandLine "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir } enabled config."bundleIn${targetName}" || config."bundleIn${buildTypeName.capitalize()}" ?: targetName.toLowerCase().contains("release") } // Hook bundle${productFlavor}${buildType}JsAndAssets into the android build process currentBundleTask.dependsOn("merge${targetName}Resources") currentBundleTask.dependsOn("merge${targetName}Assets") runBefore("processArmeabi-v7a${targetName}Resources", currentBundleTask) runBefore("processX86${targetName}Resources", currentBundleTask) runBefore("processUniversal${targetName}Resources", currentBundleTask) runBefore("process${targetName}Resources", currentBundleTask) } } } ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/java/cheeaun/hackerweb/MainActivity.java ================================================ package cheeaun.hackerweb; import com.facebook.react.ReactActivity; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import java.util.Arrays; import java.util.List; import com.dstaley.ReactNativeChromeCustomTabs.ChromeCustomTabsPackage; import com.blueprintalpha.rnandroidshare.RNAndroidSharePackage; public class MainActivity extends ReactActivity { /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. */ @Override protected String getMainComponentName() { return "HackerWeb"; } /** * Returns whether dev mode should be enabled. * This enables e.g. the dev menu. */ @Override protected boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } /** * A list of packages used by the app. If the app uses additional views * or modules besides the default ones, add more packages here. */ @Override protected List getPackages() { return Arrays.asList( new MainReactPackage(), new ChromeCustomTabsPackage(this), new RNAndroidSharePackage(this) ); } } ================================================ FILE: android/app/src/main/res/values/strings.xml ================================================ HackerWeb ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-v23/styles.xml ================================================ ================================================ FILE: android/build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { mavenLocal() jcenter() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$projectDir/../../node_modules/react-native/android" } } } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip ================================================ FILE: android/gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.useDeprecatedNdk=true ================================================ FILE: android/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: android/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: android/settings.gradle ================================================ rootProject.name = 'HackerWeb' include ':app' include ':ReactNativeChromeCustomTabs', ':app' project(':ReactNativeChromeCustomTabs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-chrome-custom-tabs/android') include ':RNAndroidShare', ':app' project(':RNAndroidShare').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-share') ================================================ FILE: colors.js ================================================ import { Platform } from 'react-native'; const isIOS = Platform.OS === 'ios'; export default { linkColor: '#007aff', viewBackgroundColor: isIOS ? '#efeff4' : '#fafafa', userColor: isIOS ? '#bf223f' : '#b71c1c', opColor: '#fff', opBackgroundColor: isIOS ? '#bf223f' : '#b71c1c', insignificantColor: 'rgba(0,0,0,.54)', sectionInsignificantColor: '#6d6d72', separatorColor: isIOS ? '#c8c7cc' : '#ebebeb', blockCodeBackgroundColor: '#eee', progressBarBackgroundColor: '#eee', progressBarColor: '#007aff', sectionBackgroundColor: '#fff', domainColor: isIOS ? '#003d80' : '#0D47A1', defaultButtonThemeColor: isIOS ? '#848484' : '#f5f5f5', primaryTextColor: isIOS ? null : 'rgba(0,0,0,.87)', toolbarBackgroundColor: isIOS ? null: '#f5f5f5', disabledColor: 'rgba(0,0,0,.38)', } ================================================ FILE: components/Button.android.js ================================================ 'use strict'; import React from 'react'; import { StyleSheet, View, Text, TouchableNativeFeedback, } from 'react-native'; import colors from '../colors'; const styles = StyleSheet.create({ button: { borderRadius: 2, backgroundColor: colors.defaultButtonThemeColor, paddingVertical: 11, paddingHorizontal: 16, }, text: { textAlign: 'center', }, }); export default (props) => { return ( {props.children.map((c) => c.toUpperCase ? c.toUpperCase() : c)} ); } ================================================ FILE: components/Button.ios.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, Text, View, TouchableWithoutFeedback, } from 'react-native'; import colors from '../colors'; const styles = StyleSheet.create({ button: { backgroundColor: '#ffffff', borderRadius: 6, borderColor: colors.defaultButtonThemeColor, borderWidth: StyleSheet.hairlineWidth, paddingVertical: 5, paddingHorizontal: 10, }, text: { color: colors.defaultButtonThemeColor, textAlign: 'center', }, pressedButton: { backgroundColor: colors.defaultButtonThemeColor, }, pressedText: { color: '#ffffff', }, }); export default class Button extends Component { constructor(props){ super(props); this.state = { pressed: false, }; } _onPressIn(){ this.setState({ pressed: true, }); } _onPressOut(){ this.setState({ pressed: false, }); } render(){ const {pressed} = this.state; const {onPress, buttonStyles, pressedButtonStyles, textStyles, pressedTextStyles, children} = this.props; return ( {children} ); } } ================================================ FILE: components/Comment.js ================================================ 'use strict'; import React from 'react'; import { StyleSheet, View, Text, Image, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; import HTMLView from '../components/HTMLView'; import showBrowser from '../utils/showBrowser'; import showActivity from '../utils/showActivity'; import colors from '../colors'; const styles = StyleSheet.create({ comment: { padding: isIOS ? 15 : 16, flex: 1, flexDirection: 'row', }, subComment: { paddingTop: 0, marginTop: -10 }, commentInner: { flex: 1, flexDirection: 'column', }, commentMetadata: { flex: 1, flexDirection: 'row', marginBottom: 8, }, commentUserWrap: { flex: 1, flexWrap: 'wrap', flexDirection: 'row', }, commentUser: { fontWeight: '500', color: colors.userColor, }, opUserContainer: { backgroundColor: colors.opBackgroundColor, paddingVertical: 2, paddingHorizontal: 4, borderRadius: 2, marginLeft: 5, }, opUser: { fontSize: 10, fontWeight: 'bold', color: colors.opColor, }, commentDeleted: { flex: 1, }, commentTime: { color: colors.insignificantColor, }, commentArrowIcon: { width: 8, height: 9, marginRight: 6, marginTop: 4, opacity: isIOS ? 1 : .54, }, }); export default (props) => { const {data, op} = props; const {level, user} = data; const innerComment = data.deleted ? ( [deleted] {data.time_ago} ) : ( {user} {op == user && OP} {data.time_ago} ); return ( 0 && styles.subComment]}> {level > 0 && } {innerComment} ); } ================================================ FILE: components/CommentRow.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, View, LayoutAnimation, } from 'react-native'; import CommentsThread from '../components/CommentsThread'; import Comment from '../components/Comment'; import Button from '../components/Button'; import colors from '../colors'; const styles = StyleSheet.create({ comment: { backgroundColor: colors.sectionBackgroundColor, }, repliesButton: { marginTop: -7, marginRight: 15, marginBottom: 15, marginLeft: 15, }, }); export default class CommentRow extends Component { constructor(props){ super(props); this.state = { expanded: false, }; } _toggleComments(){ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); this.setState({ expanded: !this.state.expanded, }); } render(){ const {op, comment, hasManyComments} = this.props; const {comments} = comment; const commentsThread = ; const hasComments = comments && comments.length; const hasOnlyOneComment = comments.length == 1 && !comments[0].comments.length; if (hasManyComments && hasComments && !hasOnlyOneComment){ let commentsCount = comment.comments.length; (function dive(comments){ comments.forEach(function(c){ var len = c.comments.length; commentsCount += len; if (len) dive(c.comments); }); })(comment.comments); let repliesButton = commentsCount > 0 && ; return ( {repliesButton} {this.state.expanded && commentsThread} ); } else { return ( {commentsThread} ); } } } ================================================ FILE: components/CommentsThread.js ================================================ 'use strict'; import React from 'react'; import { StyleSheet, View, } from 'react-native'; import Comment from './Comment'; const styles = StyleSheet.create({ indentedThread: { marginLeft: 14, }, }); const CommentsThread = (props) => { const {data, op} = props; if (!data || !data.length) return ; return ( {data.map((comment) => { return ( 1 && styles.indentedThread}> ); })} ); } export default CommentsThread; ================================================ FILE: components/HTMLView.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, Text, View, ScrollView, Linking, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import htmlparser from 'htmlparser2'; import colors from '../colors'; const nodeStyles = StyleSheet.create({ p: { color: colors.primaryTextColor, marginBottom: 8, lineHeight: isIOS ? null : 20, }, pre: { paddingVertical: 10, backgroundColor: colors.blockCodeBackgroundColor, borderRadius: 3, marginBottom: 8, }, code: { color: colors.primaryTextColor, fontFamily: isIOS ? 'Menlo' : 'monospace', fontSize: 12, }, a: { color: colors.linkColor, }, i: { fontStyle: 'italic', }, }); function dom2elements(nodes, opts, parentName){ if (!nodes || !nodes.length) return; const {onLinkPress, onLinkLongPress} = opts; return nodes.map((node) => { const {name, type, children} = node; const key = (name || type) + '-' + Math.random(); const style = nodeStyles[name]; if (type == 'tag'){ var elements = dom2elements(children, opts, name); if (!elements) return null; if (name == 'pre'){ return ( {elements} ); } if (name == 'a'){ const {href} = node.attribs; // Steps to make sure children inside is ACTUALLY text const child = children && children.length == 1 && children[0]; const text = child && child.type == 'text' && child.data; return {text || elements}; } return {elements}; } else if (type == 'text'){ const {data} = node; let text; if (parentName == 'code'){ // Trim EOL newline text = data.replace(/\n$/, ''); } else { // Trim ALL newlines, because HTML text = data.replace(/[\n\s\t]+/g, ' '); } return {text}; } }); }; function processDOM(html, opts, callback){ if (typeof opts == 'function'){ callback = opts; opts = {}; } const handler = new htmlparser.DomHandler((err, dom) => { const elements = dom2elements(dom, opts); callback(elements); }); const parser = new htmlparser.Parser(handler, { recognizeSelfClosing: true, lowerCaseAttributeNames: true, lowerCaseTags: true, decodeEntities: true, }); // Clean up HTML first if (!html.match(/^

/i)) html = '

' + html; // Stop

 from being wrapped by 

html = html.replace(/

\s*

/ig, '

');
  if (!html.match(/<\/pre>\s*

/i)){ html = html.replace(/<\/pre>([^<])/ig, '

$1'); } parser.write(html); parser.end(); } export default class HTMLView extends Component { constructor(props){ super(props); this.state = { elements: null, }; this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); } componentDidMount(){ const {html, onLinkPress, onLinkLongPress} = this.props; if (!html) return null; processDOM(html, { onLinkPress: onLinkPress || Linking.openURL, onLinkLongPress: onLinkLongPress || function(){}, }, (elements) => { this.setState({ elements: elements, }); }); } render(){ return ( {this.state.elements} ); } } ================================================ FILE: components/LoadingIndicator.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, Text, View, ActivityIndicatorIOS, ProgressBarAndroid, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; import colors from '../colors'; const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', }, spinner: isIOS ? { width: 30, height: 30, } : { width: 24, height: 24, }, text: { opacity: .6, }, }); export default class LoadingIndicator extends Component { constructor(props){ super(props); this.state = { opacity: 0, }; } componentDidMount(){ this._timer = setTimeout(() => { this.setState({ opacity: 1, }); }, 900); // less than 1 second } componentWillUnmount(){ clearTimeout(this._timer); } render(){ const {opacity} = this.state; if (isIOS){ return ( Loading… ); } return ( ); } } ================================================ FILE: components/ProgressBar.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, View, Animated, } from 'react-native'; import colors from '../colors'; const styles = StyleSheet.create({ bar: { backgroundColor: colors.progressBarBackgroundColor, height: 3, borderRadius: 1.5, overflow: 'hidden', }, progress: { backgroundColor: colors.progressBarColor, height: 3, } }); export default class ProgressBar extends Component { constructor(props){ super(props); this.state = { progressWidth: new Animated.Value(0), }; this._onBarLayout = this._onBarLayout.bind(this); } _onBarLayout(e){ const {value, max} = this.props; const width = e.nativeEvent.layout.width; Animated.spring(this.state.progressWidth, { toValue: value/max*width, duration: 300, }).start(); } render(){ return ( ); } } ================================================ FILE: components/StoryRow.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, View, Text, TouchableOpacity, TouchableHighlight, TouchableNativeFeedback, Image, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; const CrossTouchableHighlight = isIOS ? TouchableHighlight : TouchableNativeFeedback; const CrossTouchableOpacity = isIOS ? TouchableOpacity : TouchableNativeFeedback; import LinkStore from '../stores/LinkStore'; import LinkActions from '../actions/LinkActions'; import domainify from '../utils/domainify'; import colors from '../colors'; const styles = StyleSheet.create({ story: { backgroundColor: colors.sectionBackgroundColor, flexDirection: 'row', alignItems: 'stretch', }, storyPosition: { paddingTop: isIOS ? 10 : 16, paddingLeft: 15, }, storyPositionNumber: { width: 22, textAlign: 'center', color: colors.insignificantColor, fontSize: 17, }, storyInfo: { padding: 10, paddingVertical: isIOS ? 10 : 16, flex: 1, }, storyComments: { padding: isIOS ? 10 : 16, }, storyDisclosure: { paddingVertical: 15, paddingRight: 15, paddingLeft: 5, }, storyTitle: { color: colors.primaryTextColor, fontSize: 17, }, storyTitleVisited: { color: colors.insignificantColor, }, storyDomain: { fontSize: isIOS ? 13 : 14, color: colors.domainColor, }, storyMetadataWrap: { flexWrap: 'wrap', flexDirection: 'row', }, storyMetadata: { fontSize: isIOS ? 13 : 14, color: colors.insignificantColor, }, commentIcon: { width: isIOS ? 20 : 24, height: isIOS ? 19 : 24, marginHorizontal: isIOS ? 2 : 0, marginTop: 3, marginBottom: 2, opacity: isIOS ? 1 : .54, }, disclosureIcon: { width: 8, height: 13, marginLeft: 2, marginTop: 1, }, }); export default class StoryRow extends Component { constructor(props){ super(props); const {data} = props; const {links} = LinkStore.getState(); this.state = { visited: links.indexOf(data.url) >= 0, }; this._onLinkChange = this._onLinkChange.bind(this); } componentDidMount(){ LinkStore.listen(this._onLinkChange); LinkActions.getLinks(); } componentWillUnmount(){ LinkStore.unlisten(this._onLinkChange); } _onLinkChange(state){ this.setState({ visited: state.links.indexOf(this.props.data.url) >= 0, }); } render(){ const {data, position, onCommentPress, ...touchableProps} = this.props; const {url, type, comments_count, points, time_ago} = data; const externalLink = !/^item/i.test(url); const {visited} = this.state; // Turns out that longPress is not common at all in native iOS apps // But I actually like this feature on the browser, thus I'm keeping // this but delay it slightly longer than default 500 const delayLongPress = 1000; return ( {position} {data.title} {externalLink && {domainify(url)}} {(() => { if (type == 'job'){ return {time_ago}; } else { const commentsText = comments_count>0 && · {comments_count} comment{comments_count != 1 && 's'}; return ( {points} point{points != 1 && 's'} by {data.user} {time_ago}{commentsText} ); } })()} {type != 'job' && (() => { if (externalLink){ return ( ); } else if (isIOS){ return ( ); } })()} ); } } ================================================ FILE: index.android.js ================================================ 'use strict'; import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Navigator, AppState, Text, View, ToolbarAndroid, BackAndroid, Linking, } from 'react-native'; import StoryActions from './actions/StoryActions'; import StoriesView from './views/StoriesView'; import CommentsView from './views/CommentsView'; import AboutView from './views/AboutView'; import colors from './colors'; const styles = StyleSheet.create({ container: { flex: 1, }, wrapper: { backgroundColor: colors.viewBackgroundColor, }, toolbar: { backgroundColor: colors.toolbarBackgroundColor, height: 56, elevation: 2, }, }); let _navigator; BackAndroid.addEventListener('hardwareBackPress', () => { if (_navigator.getCurrentRoutes().length == 1){ return false; } _navigator.pop(); return true; }); class HackerWeb extends Component { constructor(props){ super(props); this.state = { currentAppState: AppState.currentState, }; this._handleAppStateChange = this._handleAppStateChange.bind(this); } componentDidMount(){ AppState.addEventListener('change', this._handleAppStateChange); Linking.getInitialURL().then((url) => { if (!url) return; const id = (url.match(/item\?id=([a-z\d]+)/i) || [,null])[1]; if (!id) return; _navigator.push({ id: 'Comments', component: CommentsView, wrapperStyle: styles.wrapper, passProps: { data: {id}, } }); }).catch(() => {}); } componentWillUnmount(){ AppState.removeEventListener('change', this._handleAppStateChange); } _handleAppStateChange(currentAppState){ if (currentAppState == 'active' && this.state.currentAppState != currentAppState){ StoryActions.fetchStoriesIfExpired(); } this.setState({ currentAppState, }); } _navigatorRenderScene(route, navigator){ _navigator = navigator; switch (route.id){ case 'Stories': return { switch (position){ case 0: StoryActions.fetchStories(); break; case 1: navigator.push({ id: 'About', }); break; } }} /> ; case 'Comments': const { component, title, passProps, rightButtonIcon, onRightButtonPress } = route; const TheComponent = component; return ; case 'About': return ; } } render(){ return ( Navigator.SceneConfigs.FloatFromBottomAndroid} /> ); } } AppRegistry.registerComponent('HackerWeb', () => HackerWeb); ================================================ FILE: index.ios.js ================================================ 'use strict'; import React, { Component } from 'react'; import { AppStateIOS, AppRegistry, StyleSheet, Modal, NavigatorIOS, View, Linking, } from 'react-native'; import StoryActions from './actions/StoryActions'; import StoriesView from './views/StoriesView'; import AboutView from './views/AboutView'; import CommentsView from './views/CommentsView'; import colors from './colors'; const styles = StyleSheet.create({ container: { flex: 1, }, wrapper: { backgroundColor: colors.viewBackgroundColor, }, }); class App extends Component { constructor(props){ super(props); this.state = { currentAppState: AppStateIOS.currentState, isAboutVisible: false, }; this._reloadCount = 0; this._reloadCountTimeout = null; this._handleAppStateChange = this._handleAppStateChange.bind(this); this._handleOpenURL = this._handleOpenURL.bind(this); } componentDidMount(){ AppStateIOS.addEventListener('change', this._handleAppStateChange); Linking.addEventListener('url', this._handleOpenURL); } componentWillUnmount(){ AppStateIOS.removeEventListener('change', this._handleAppStateChange); Linking.removeEventListener('url', this._handleOpenURL); } _handleAppStateChange(currentAppState){ if (currentAppState == 'active' && this.state.currentAppState != currentAppState){ StoryActions.fetchStoriesIfExpired(); } this.setState({ currentAppState, }); } _handleOpenURL(e){ const {url} = e; if (!url) return; const id = (url.match(/item\?id=([a-z\d]+)/i) || [,null])[1]; if (!id) return; this.refs.nav.push({ component: CommentsView, wrapperStyle: styles.wrapper, passProps: { data: {id} }, }); } _showAbout(){ this.setState({ isAboutVisible: true, }); } _hideAbout(){ this.setState({ isAboutVisible: false, }); } render(){ const {isAboutVisible} = this.state; return ( { // For the compulsive-type of people who likes to press Reload multiple times this._reloadCount++; clearTimeout(this._reloadCountTimeout); if (this._reloadCount >= 3){ StoryActions.flush(); this._reloadCount = 0; } else { this._reloadCountTimeout = setTimeout(() => this._reloadCount = 0, 3000); } StoryActions.fetchStories(); }, }}/> ); } } AppRegistry.registerComponent('HackerWeb', () => App); ================================================ FILE: ios/HackerWeb/AppDelegate.h ================================================ /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import @interface AppDelegate : UIResponder @property (nonatomic, strong) UIWindow *window; @end ================================================ FILE: ios/HackerWeb/AppDelegate.m ================================================ /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "AppDelegate.h" #import "RCTRootView.h" #import "RCTLinkingManager.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; /** * Loading JavaScript code - uncomment the one you want. * * OPTION 1 * Load from development server. Start the server from the repository root: * * $ npm start * * To run on device, change `localhost` to the IP address of your computer * (you can get this by typing `ifconfig` into the terminal and selecting the * `inet` value under `en0:`) and make sure your computer and iOS device are * on the same Wi-Fi network. */ jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; /** * OPTION 2 * Load from pre-bundled file on disk. The static bundle is automatically * generated by the "Bundle React Native code and images" build step when * running the project on an actual device or running the project on the * simulator in the "Release" build configuration. */ // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"HackerWeb" initialProperties:nil launchOptions:launchOptions]; 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]; return YES; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; } @end ================================================ FILE: ios/HackerWeb/Base.lproj/LaunchScreen.xib ================================================ ================================================ FILE: ios/HackerWeb/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "29x29", "idiom" : "iphone", "filename" : "58.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "97.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "80.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "120.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "120-1.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "180.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/HackerWeb/Images.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/HackerWeb/Images.xcassets/app-icon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "app-icon.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "app-icon@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "app-icon@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/HackerWeb/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.1 CFBundleSignature ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleURLSchemes hackerweb CFBundleVersion 5 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSLocationWhenInUseUsageDescription UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIViewControllerBasedStatusBarAppearance NSLocationWhenInUseUsageDescription NSAppTransportSecurity NSAllowsArbitraryLoads ================================================ FILE: ios/HackerWeb/main.m ================================================ /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import #import "AppDelegate.h" int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: ios/HackerWeb.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 00E356F31AD99517003FC87E /* HackerWebTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* HackerWebTests.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 9B47A38C1C5E057600228361 /* libSafariViewManager.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B47A3891C5E056200228361 /* libSafariViewManager.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; proxyType = 2; remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTActionSheet; }; 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; proxyType = 2; remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTGeolocation; }; 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; proxyType = 2; remoteGlobalIDString = 58B5115D1A9E6B3D00147676; remoteInfo = RCTImage; }; 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; proxyType = 2; remoteGlobalIDString = 58B511DB1A9E6C8500147676; remoteInfo = RCTNetwork; }; 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; proxyType = 2; remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; remoteInfo = RCTVibration; }; 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; proxyType = 1; remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = HackerWeb; }; 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; proxyType = 2; remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTSettings; }; 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; proxyType = 2; remoteGlobalIDString = 3C86DF461ADF2C930047B81A; remoteInfo = RCTWebSocket; }; 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; proxyType = 2; remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; proxyType = 2; remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTLinking; }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; proxyType = 2; remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; 9B47A3881C5E056200228361 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9B47A3841C5E056200228361 /* SafariViewManager.xcodeproj */; proxyType = 2; remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = SafariViewManager; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* HackerWebTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HackerWebTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* HackerWebTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HackerWebTests.m; sourceTree = ""; }; 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* HackerWeb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HackerWeb.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = HackerWeb/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = HackerWeb/AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = HackerWeb/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = HackerWeb/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = HackerWeb/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../node_modules/react-native/React/React.xcodeproj; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../node_modules/react-native/Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; 9B47A3841C5E056200228361 /* SafariViewManager.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SafariViewManager.xcodeproj; path = "../node_modules/react-native-safari-view/SafariViewManager.xcodeproj"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 00E356EB1AD99517003FC87E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9B47A38C1C5E057600228361 /* libSafariViewManager.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 00C302A81ABCB8CE00DB3ED1 /* Products */ = { isa = PBXGroup; children = ( 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, ); name = Products; sourceTree = ""; }; 00C302B61ABCB90400DB3ED1 /* Products */ = { isa = PBXGroup; children = ( 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, ); name = Products; sourceTree = ""; }; 00C302BC1ABCB91800DB3ED1 /* Products */ = { isa = PBXGroup; children = ( 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, ); name = Products; sourceTree = ""; }; 00C302D41ABCB9D200DB3ED1 /* Products */ = { isa = PBXGroup; children = ( 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, ); name = Products; sourceTree = ""; }; 00C302E01ABCB9EE00DB3ED1 /* Products */ = { isa = PBXGroup; children = ( 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, ); name = Products; sourceTree = ""; }; 00E356EF1AD99517003FC87E /* HackerWebTests */ = { isa = PBXGroup; children = ( 00E356F21AD99517003FC87E /* HackerWebTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = HackerWebTests; sourceTree = ""; }; 00E356F01AD99517003FC87E /* Supporting Files */ = { isa = PBXGroup; children = ( 00E356F11AD99517003FC87E /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 139105B71AF99BAD00B5F7CC /* Products */ = { isa = PBXGroup; children = ( 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, ); name = Products; sourceTree = ""; }; 139FDEE71B06529A00C62182 /* Products */ = { isa = PBXGroup; children = ( 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, ); name = Products; sourceTree = ""; }; 13B07FAE1A68108700A75B9A /* HackerWeb */ = { isa = PBXGroup; children = ( 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, ); name = HackerWeb; sourceTree = ""; }; 146834001AC3E56700842450 /* Products */ = { isa = PBXGroup; children = ( 146834041AC3E56700842450 /* libReact.a */, ); name = Products; sourceTree = ""; }; 78C398B11ACF4ADC00677621 /* Products */ = { isa = PBXGroup; children = ( 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, ); name = Products; sourceTree = ""; }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( 9B47A3841C5E056200228361 /* SafariViewManager.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, ); name = Libraries; sourceTree = ""; }; 832341B11AAA6A8300B99B32 /* Products */ = { isa = PBXGroup; children = ( 832341B51AAA6A8300B99B32 /* libRCTText.a */, ); name = Products; sourceTree = ""; }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( 13B07FAE1A68108700A75B9A /* HackerWeb */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* HackerWebTests */, 83CBBA001A601CBA00E9B192 /* Products */, ); indentWidth = 2; sourceTree = ""; tabWidth = 2; }; 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* HackerWeb.app */, 00E356EE1AD99517003FC87E /* HackerWebTests.xctest */, ); name = Products; sourceTree = ""; }; 9B47A3851C5E056200228361 /* Products */ = { isa = PBXGroup; children = ( 9B47A3891C5E056200228361 /* libSafariViewManager.a */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 00E356ED1AD99517003FC87E /* HackerWebTests */ = { isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "HackerWebTests" */; buildPhases = ( 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, ); buildRules = ( ); dependencies = ( 00E356F51AD99517003FC87E /* PBXTargetDependency */, ); name = HackerWebTests; productName = HackerWebTests; productReference = 00E356EE1AD99517003FC87E /* HackerWebTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 13B07F861A680F5B00A75B9A /* HackerWeb */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "HackerWeb" */; buildPhases = ( 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, ); buildRules = ( ); dependencies = ( ); name = HackerWeb; productName = "Hello World"; productReference = 13B07F961A680F5B00A75B9A /* HackerWeb.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0610; ORGANIZATIONNAME = Facebook; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { DevelopmentTeam = 7WDVUPXAPH; }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "HackerWeb" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectReferences = ( { ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; }, { ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; }, { ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; }, { ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; }, { ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; }, { ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; }, { ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; }, { ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; }, { ProductGroup = 139FDEE71B06529A00C62182 /* Products */; ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; }, { ProductGroup = 146834001AC3E56700842450 /* Products */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; }, { ProductGroup = 9B47A3851C5E056200228361 /* Products */; ProjectRef = 9B47A3841C5E056200228361 /* SafariViewManager.xcodeproj */; }, ); projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* HackerWeb */, 00E356ED1AD99517003FC87E /* HackerWebTests */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTActionSheet.a; remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTGeolocation.a; remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTImage.a; remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTNetwork.a; remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTVibration.a; remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTSettings.a; remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTWebSocket.a; remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 146834041AC3E56700842450 /* libReact.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libReact.a; remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTLinking.a; remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libRCTText.a; remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 9B47A3891C5E056200228361 /* libSafariViewManager.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libSafariViewManager.a; remoteRef = 9B47A3881C5E056200228361 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ 00E356EC1AD99517003FC87E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Bundle React Native code and images"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/packager/react-native-xcode.sh"; showEnvVarsInLog = 1; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 00E356EA1AD99517003FC87E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 00E356F31AD99517003FC87E /* HackerWebTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 13B07F861A680F5B00A75B9A /* HackerWeb */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; children = ( 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; path = HackerWeb; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = HackerWebTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HackerWeb.app/HackerWeb"; }; name = Debug; }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", ); INFOPLIST_FILE = HackerWebTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HackerWeb.app/HackerWeb"; }; name = Release; }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEAD_CODE_STRIPPING = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native/Libraries/**", ); INFOPLIST_FILE = "HackerWeb/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = cheeaun.hackerweb; PRODUCT_NAME = HackerWeb; PROVISIONING_PROFILE = ""; }; name = Debug; }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEAD_CODE_STRIPPING = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", "$(SRCROOT)/../node_modules/react-native/Libraries/**", ); INFOPLIST_FILE = "HackerWeb/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = cheeaun.hackerweb; PRODUCT_NAME = HackerWeb; PROVISIONING_PROFILE = ""; }; name = Release; }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", ); IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; 83CBBA211A601CBA00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../node_modules/react-native/React/**", ); IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "HackerWebTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 00E356F61AD99517003FC87E /* Debug */, 00E356F71AD99517003FC87E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "HackerWeb" */ = { isa = XCConfigurationList; buildConfigurations = ( 13B07F941A680F5B00A75B9A /* Debug */, 13B07F951A680F5B00A75B9A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "HackerWeb" */ = { isa = XCConfigurationList; buildConfigurations = ( 83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA211A601CBA00E9B192 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } ================================================ FILE: ios/HackerWeb.xcodeproj/xcshareddata/xcschemes/HackerWeb.xcscheme ================================================ ================================================ FILE: ios/HackerWebTests/HackerWebTests.m ================================================ /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import #import #import "RCTLog.h" #import "RCTRootView.h" #define TIMEOUT_SECONDS 600 #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" @interface HackerWebTests : XCTestCase @end @implementation HackerWebTests - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test { if (test(view)) { return YES; } for (UIView *subview in [view subviews]) { if ([self findSubviewInView:subview matching:test]) { return YES; } } return NO; } - (void)testRendersWelcomeScreen { UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; BOOL foundElement = NO; __block NSString *redboxError = nil; RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { if (level >= RCTLogLevelError) { redboxError = message; } }); while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { return YES; } return NO; }]; } RCTSetLogFunction(RCTDefaultLogFunction); XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); } @end ================================================ FILE: ios/HackerWebTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: package.json ================================================ { "name": "HackerWeb", "version": "0.0.1", "private": true, "scripts": { "start": "node_modules/react-native/packager/packager.sh" }, "dependencies": { "alt": "~0.18.4", "buffer": "~4.6.0", "events": "~1.1.0", "htmlparser2": "~3.9.0", "react": "~15.1.0", "react-addons-pure-render-mixin": "~15.1.0", "react-native": "~0.27.2", "react-native-android-share": "https://github.com/haydenth/react-native-android-share.git", "react-native-cache-store": "~1.0.2", "react-native-chrome-custom-tabs": "~0.0.2", "react-native-safari-view": "~0.4.1", "stream": "~0.0.2", "url-parse": "~1.1.1" } } ================================================ FILE: stores/LinkStore.js ================================================ 'use strict'; import alt from '../alt'; import LinkActions from '../actions/LinkActions'; class LinkStore { constructor(){ this.links = []; this.bindListeners({ handleGetLinks: LinkActions.GET_LINKS, handleAddLink: LinkActions.ADD_LINK, }); } handleGetLinks(links){ this.links = links; } handleAddLink(link){ this.links.push(link); } } export default alt.createStore(LinkStore, 'LinkStore'); ================================================ FILE: stores/StoryStore.js ================================================ 'use strict'; import alt from '../alt'; import StoryActions from '../actions/StoryActions'; class StoryStore { constructor(){ this.stories = []; this.storiesLoading = false; this.storiesError = false; this.story = null; this.storyLoading = false; this.storyError = false; this.hasMoreStories = false; this.bindListeners({ handleFetchStories: StoryActions.FETCH_STORIES, handleFetchMoreStories: StoryActions.FETCH_MORE_STORIES, handleUpdateStories: StoryActions.UPDATE_STORIES, handleUpdateMoreStories: StoryActions.UPDATE_MORE_STORIES, handleHasMoreStories: StoryActions.HAS_MORE_STORIES, handleStoriesFailed: StoryActions.STORIES_FAILED, handleFetchStory: StoryActions.FETCH_STORY, handleUpdateStory: StoryActions.UPDATE_STORY, handleStoryFailed: StoryActions.STORY_FAILED, }); } handleFetchStories(){ this.storiesLoading = true; this.storiesError = false; this.hasMoreStories = false; } handleFetchMoreStories(){ } handleUpdateStories(stories){ this.stories = stories; this.storiesLoading = false; this.storiesError = false; } handleHasMoreStories(){ this.hasMoreStories = true; } handleUpdateMoreStories(stories){ this.stories = this.stories.concat(stories); } handleStoriesFailed(error){ this.storiesLoading = false; this.storiesError = error; } handleFetchStory(id){ if (this.story && this.story.id != id){ this.story = null; this.storyLoading = true; } if (!this.story) this.storyLoading = true; this.storyError = false; } handleUpdateStory(story){ this.story = story; this.storyLoading = false; this.storyError = false; // Update story in stories const stories = this.stories; for (let i=0, l=stories.length; i { if (!url) return; var link = urlparse(url); var domain = link.hostname.replace(/^www\./, ''); var pathname = link.pathname.replace(/^\//, '').split('/')[0]; var pathnameLen = pathname.length; var firstPath = domain.length <= 25 && pathnameLen > 3 && pathnameLen <= 15 && /^[^0-9][^.]+$/.test(pathname) ? ('/' + pathname) : ''; return domain + firstPath; }; ================================================ FILE: utils/showActivity.android.js ================================================ 'use strict'; import AndroidShare from 'react-native-android-share'; export default (url, message) => { if (!url) return; AndroidShare.openChooserWithOptions({ subject: message || url, text: (message ? (message + ' ') : '') + url, }, 'Share via'); } ================================================ FILE: utils/showActivity.ios.js ================================================ 'use strict'; import { ActionSheetIOS } from 'react-native'; export default (url, message) => { if (!url) return; ActionSheetIOS.showShareActionSheetWithOptions({ url: url, message: message || '', }, () => {}, () => {}); } ================================================ FILE: utils/showBrowser.android.js ================================================ 'use strict'; import ChromeCustomTabsClient from 'react-native-chrome-custom-tabs'; import LinkActions from '../actions/LinkActions'; export default (url) => { if (!url) return; ChromeCustomTabsClient.launchCustomTab(url); // Log link visit to History setTimeout(() => LinkActions.addLink(url), 1000); } ================================================ FILE: utils/showBrowser.ios.js ================================================ 'use strict'; import { Linking, AlertIOS } from 'react-native'; import SafariView from 'react-native-safari-view'; import LinkActions from '../actions/LinkActions'; export default (url) => { if (!url) return; SafariView.isAvailable() .then(() => { SafariView.show({ url: url, }); // Log link visit to History setTimeout(() => LinkActions.addLink(url), 1000); }) .catch(() => { Linking.canOpenURL(url, (supported) => { if (!supported){ AlertIOS.alert('Can\'t handle URL: ' + url); } else { Linking.openURL(url); // Log link visit to History setTimeout(() => LinkActions.addLink(url), 1000); } }); }); } ================================================ FILE: views/AboutView.js ================================================ 'use strict'; import React from 'react'; import { StyleSheet, View, Text, Image, ScrollView, TouchableOpacity, TouchableNativeFeedback, Linking, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; const CrossTouchable = isIOS ? TouchableOpacity : TouchableNativeFeedback; // import SafariView from 'react-native-safari-view'; import colors from '../colors'; const hairlineWidth = isIOS ? StyleSheet.hairlineWidth : 1; const styles = StyleSheet.create({ aboutContainer: { marginTop: isIOS ? 34 : 0, paddingVertical: isIOS ? 10 : 32, paddingHorizontal: isIOS ? 15 : 16, borderTopWidth: isIOS ? hairlineWidth : 0, borderTopColor: colors.separatorColor, borderBottomWidth: hairlineWidth, borderBottomColor: colors.separatorColor, backgroundColor: colors.sectionBackgroundColor, flexDirection: 'row', }, appIcon: { width: 60, height: 60, marginRight: 10, borderColor: colors.separatorColor, borderWidth: hairlineWidth, borderRadius: isIOS ? 14 : 2, }, aboutTextContainer: { flex: 1, }, aboutHeading: { color: colors.primaryTextColor, fontWeight: '500', fontSize: 17, }, aboutDescription: { fontSize: 15, color: colors.insignificantColor, }, listContainer: { marginTop: 34, borderTopWidth: hairlineWidth, borderTopColor: colors.separatorColor, borderBottomWidth: hairlineWidth, borderBottomColor: colors.separatorColor, backgroundColor: colors.sectionBackgroundColor, }, listItem: { paddingVertical: isIOS ? 13 : 16, paddingHorizontal: isIOS ? 15 : 16, }, listItemSeparator: { marginLeft: 15, marginTop: -1, height: hairlineWidth, backgroundColor: colors.separatorColor, }, link: { color: colors.linkColor, fontSize: isIOS ? 17 : 16, }, disclaimer: { paddingVertical: 27, paddingHorizontal: 15, }, disclaimerText: { color: colors.sectionInsignificantColor, } }); var linkPress = function(url){ Linking.openURL(url); /* BUG: Once is open, SafariView can't work anymore. if (/^mailto:/.test(url)){ // Note: won't work in Simulator because there's no Mail there Linking.openURL(url); } else { SafariView.show({ url: url }); } */ }; function linksContainer(links){ return {links.map((link, i) => { return ( {link.text} {i < links.length-1 && } ); })} }; export default (props) => { return ( HackerWeb A simply readable Hacker News app. {linksContainer([ isIOS ? { text: '🌟 Rate HackerWeb on the App Store', url: 'http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=1084209377&pageNumber=0&sortOrdering=2&type=Purple+Software&mt=8' } : { text: '🌟 Rate HackerWeb on Google Play', url: 'https://play.google.com/store/apps/details?id=cheeaun.hackerweb' }, { text: '☕️ Buy me a cup of coffee', url: 'https://donorbox.org/support-cheeaun' }, ])} {linksContainer([ { text: 'HackerWeb homepage', url: 'https://hackerwebapp.com/' }, { text: 'Hacker News homepage', url: 'https://news.ycombinator.com/' }, { text: 'Hacker News FAQ', url: 'https://news.ycombinator.com/newsfaq.html' }, { text: 'HackerWeb on GitHub', url: 'https://github.com/cheeaun/hackerweb-native' }, { text: 'Follow @cheeaun', url: 'https://twitter.com/cheeaun' }, { text: 'Send Feedback', url: 'mailto:cheeaun+hackerweb@gmail.com?subject=HackerWeb feedback' }, ])} Built by Lim Chee Aun. Not affiliated with Hacker News or YCombinator. ); } ================================================ FILE: views/CommentsView.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, ActivityIndicatorIOS, View, Text, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, Image, ActionSheetIOS, ListView, LayoutAnimation, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; import ChromeCustomTabsClient from 'react-native-chrome-custom-tabs'; import StoryStore from '../stores/StoryStore'; import StoryActions from '../actions/StoryActions'; import CommentRow from '../components/CommentRow'; import LoadingIndicator from '../components/LoadingIndicator'; import HTMLView from '../components/HTMLView'; import ProgressBar from '../components/ProgressBar'; import showBrowser from '../utils/showBrowser'; import showActivity from '../utils/showActivity'; import domainify from '../utils/domainify'; import colors from '../colors'; const hairlineWidth = isIOS ? StyleSheet.hairlineWidth : 1; const styles = StyleSheet.create({ viewCommentsBlank: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 50, backgroundColor: colors.sectionBackgroundColor, }, footer: { borderTopColor: colors.separatorColor, borderTopWidth: hairlineWidth, height: 30, }, errorContainer: { alignItems: 'center', }, errorText: { opacity: .6, textAlign: 'center', }, retryText: { color: colors.linkColor, textAlign: 'center', }, noCommentsText: { opacity: .6, }, externalArrowIcon: { width: isIOS ? 12 : 10, height: isIOS ? 9 : 10, marginLeft: 2, marginRight: 4, opacity: isIOS ? 1 : .54, }, storyLink: { backgroundColor: colors.viewBackgroundColor, }, storyTitle: { color: colors.primaryTextColor, fontSize: 17, }, storySection: { padding: 15, }, storyDomain: { fontSize: isIOS ? 13 : 14, color: colors.domainColor, }, storyMetadataWrap: { flexWrap: 'wrap', flexDirection: 'row', marginBottom: 2, }, storyMetadata: { fontSize: isIOS ? 13 : 14, color: colors.insignificantColor, }, storyContent: { backgroundColor: colors.sectionBackgroundColor, padding: isIOS ? 15 : 16, borderTopColor: colors.separatorColor, borderTopWidth: hairlineWidth, borderBottomColor: colors.separatorColor, borderBottomWidth: hairlineWidth, marginBottom: 30, }, separator: { backgroundColor: colors.separatorColor, height: hairlineWidth, }, externalLink: { flex: 1, flexDirection: 'row', alignItems: 'center', backgroundColor: colors.viewBackgroundColor, }, pollContainer: { flexDirection: 'row', marginTop: 9, marginBottom: 2, alignItems: 'flex-end', }, pollItem: { color: colors.primaryTextColor, flex: 1, }, pollPoints: { color: colors.insignificantColor, }, touchableLink: { borderRadius: 3, transform: [ {translateX: -2}, {translateY: -2} ] }, touchableLinkInner: { padding: 2 }, }); export default class CommentsView extends Component { constructor(props){ super(props); var { story, storyLoading, storyError } = StoryStore.getState(); this.state = { data: story, dataSource: new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2}), loading: storyLoading, error: storyError, pollDisplay: 'points', }; this._onChange = this._onChange.bind(this); this._fetchStory = this._fetchStory.bind(this); } componentDidMount(){ StoryStore.listen(this._onChange); this._fetchStory(); } componentWillUnmount(){ StoryStore.unlisten(this._onChange); } _fetchStory(){ StoryActions.fetchStory(this.props.data.id); } _onChange(state){ const {story} = state; const comments = (story && story.comments) || []; LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); this.setState({ data: story, dataSource: this.state.dataSource.cloneWithRows(comments), loading: state.storyLoading, error: state.storyError, }); if (isAndroid && story && story.url){ const externalLink = !/^item/i.test(story.url); const url = externalLink ? story.url : `https://news.ycombinator.com/item?id=${story.id}`; ChromeCustomTabsClient.mayLaunchUrl(url); } /* Note: Title update doesn't work yet due to https://github.com/facebook/react-native/issues/476 Hopefully this works https://github.com/bjornco/react-native/commit/5fcb2a8673a2c17f4fdb03327008397a10a9c53a */ if (state.story && state.story.title){ var route = this.props.navigator.navigationContext.currentRoute; if (!route) return; if (route.title != state.story.title){ const {story} = state; route.title = story.title; if (!route.rightButtonIcon) route.rightButtonIcon = require('../images/share-icon.png'); route.onRightButtonPress = showActivity.bind(null, `https://news.ycombinator.com/item?id=${story.id}`, story.title); this.props.navigator.replace(route); } } } _togglePollDisplay(){ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); this.setState({ pollDisplay: this.state.pollDisplay == 'points' ? 'percentage' : 'points', }); } _renderHeader(){ let {data, loading, error, pollDisplay} = this.state; data = data || this.props.data; const commentsText = · {data.comments_count} comment{data.comments_count != 1 && 's'}; const url = data.url; const externalLink = !/^item/i.test(url); let domainText = null; let storyHeader = null; if (externalLink){ domainText = {domainify(data.url)}; storyHeader = ( {data.title} {domainText} ); } else { storyHeader = {data.title}; } const hnShortURL = `news.ycombinator.com/item?id=${data.id}`; const hnURL = `https://${hnShortURL}`; let contentSection; if (data.content){ let pollElements; if (data.poll && data.poll.length){ let maxPoints = Math.max.apply(null, data.poll.map((p) => p.points )); let totalPoints = data.poll.reduce((a, b) => a + b.points, 0); pollElements = data.poll.map((p) => { let {points} = p; let pointsText; if (pollDisplay == 'points'){ pointsText = `${points} point${points != 1 ? 's' : ''}`; } else { pointsText = `${(points/totalPoints*100).toFixed(1)}%`; } return ( {p.item} {pointsText} ); }); } contentSection = ( {pollElements} ); } let commentsSection; if (data && data.comments && data.comments.length){ commentsSection = null; } else if (loading){ commentsSection = ( ); } else if (error){ commentsSection = ( Couldn't load comments. Try again ); } else { commentsSection = ( No comments. ); } return ( {storyHeader} {data.points} points by {data.user} {data.time_ago} {data.comments_count>0 && commentsText} {hnShortURL} {contentSection} {commentsSection} ); } render(){ const {data, dataSource} = this.state; const comments = (data && data.comments) || []; const hasManyComments = JSON.stringify(comments).length > 20*1000; const op = data && data.user; return ( } renderSeparator={(sectionID, rowID) => } renderFooter={() => } /> ); } } ================================================ FILE: views/StoriesView.js ================================================ 'use strict'; import React, { Component } from 'react'; import { StyleSheet, View, Text, ListView, TouchableOpacity, TouchableNativeFeedback, ActionSheetIOS, LayoutAnimation, Platform, } from 'react-native'; const isIOS = Platform.OS === 'ios'; const CrossTouchable = isIOS ? TouchableOpacity : TouchableNativeFeedback; import StoryStore from '../stores/StoryStore'; import StoryActions from '../actions/StoryActions'; import LinkActions from '../actions/LinkActions'; import StoryRow from '../components/StoryRow'; import LoadingIndicator from '../components/LoadingIndicator'; import CommentsView from './CommentsView'; import showBrowser from '../utils/showBrowser'; import showActivity from '../utils/showActivity'; import colors from '../colors'; const hairlineWidth = isIOS ? StyleSheet.hairlineWidth : 1; const styles = StyleSheet.create({ container: { flex: 1, }, navbarSpacing: { marginTop: 64, }, viewLoading: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, viewError: { flex: 1, alignItems: 'center', justifyContent: 'center', }, errorText: { opacity: .6, }, itemSeparator: { height: hairlineWidth, backgroundColor: colors.separatorColor, marginLeft: isIOS ? 15 : 0, marginTop: -hairlineWidth, }, itemHighligtedSeparator: { opacity: 0, }, wrapper: { backgroundColor: colors.viewBackgroundColor, }, moreLink: { padding: 19, textAlign: 'center', color: colors.linkColor, fontSize: 17, textAlignVertical: 'center', }, }); export default class StoriesView extends Component { constructor(props){ super(props); const { stories, storiesLoading, storiesError, hasMoreStories } = StoryStore.getState(); this.state = { stories, loading: storiesLoading, error: storiesError, dataSource: new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2}), hasMoreStories, }; this._onChange = this._onChange.bind(this); } componentDidMount(){ StoryStore.listen(this._onChange); StoryActions.fetchStories(); } componentWillUnmount(){ StoryStore.unlisten(this._onChange); } _onChange(state){ const {stories, storiesLoading, storiesError, hasMoreStories} = state; LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); this.setState({ stories, dataSource: this.state.dataSource.cloneWithRows([].concat(stories)), loading: storiesLoading, error: storiesError, hasMoreStories, }); } _navigateToComments(data){ this.props.navigator.push({ id: 'Comments', title: data.title, component: CommentsView, wrapperStyle: styles.wrapper, rightButtonIcon: require('../images/share-icon.png'), onRightButtonPress: showActivity.bind(null, `https://news.ycombinator.com/item?id=${data.id}`, data.title), passProps: { data: data, } }); // Log link visit to History const {url} = data; if (/^item/i.test(url)) LinkActions.addLink(url); } _renderRow(row, sectionID, rowID, highlightRow){ const position = parseInt(rowID, 10) + 1; const {url} = row; const externalLink = !/^item/i.test(url); const navToComments = this._navigateToComments.bind(this, row); const linkPress = externalLink ? showBrowser.bind(null, url) : navToComments; return ( highlightRow(sectionID, rowID)} onHideUnderlay={() => highlightRow(null, null)}/> ); } _renderSeparator(sectionID, rowID, adjacentRowHighlighted){ return ; } _fetchMoreStories(){ // Delay the fetching a bit to create that sense of something's happening in the background if (this._fetchingMore) return; this._fetchingMore = true; setTimeout(() => { StoryActions.fetchMoreStories(); this._fetchingMore = false; }, 300); } _renderFooter(){ const {hasMoreStories, stories} = this.state; if (hasMoreStories && stories && stories.length <= 30){ return ( More… ); } return null; } render(){ const {loading, error, dataSource} = this.state; if (loading){ return ( ); } if (error){ return ( Couldn't load stories. {error && error.message ? error.message : '😭'} ); } return ( {/* Temporary FIX: Prevent the taps on NavigatorIOS buttons "leaking" into the ListView rows' taps */} {isIOS && false} style={{width: 100, height: 10, position: 'absolute', top: styles.navbarSpacing-2}} />} {isIOS && false} style={{width: 60, height: 10, position: 'absolute', top: styles.navbarSpacing-2, right: 0}} />} ); } }