├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitignore ├── .travis.yml ├── .watchmanconfig ├── App ├── Actions │ ├── AppActions.js │ ├── AuthActions.js │ ├── FollowActions.js │ └── PostActions.js ├── Api │ ├── AuthService.js │ ├── FollowService.js │ ├── HTTPClient.js │ ├── Network.js │ └── PostService.js ├── Components │ ├── AutoScaleText.js │ ├── Button.js │ ├── SegmentedControl.js │ ├── SimpleList.js │ ├── SimpleListItem.js │ ├── SpinnerLoader.js │ ├── Text.js │ └── TextInput.js ├── Constants │ └── AppConstants.js ├── Dispatcher.js ├── Extensions │ ├── AddSpinnerLoader.js │ └── Extension.js ├── Lib │ ├── CSSVarConfig.js │ ├── DateUtil.js │ ├── assignDefined.js │ └── cssVar.js ├── Locale.js ├── Locales │ ├── base.js │ ├── boot.js │ ├── en-GB-xtra.js │ ├── en-GB.js │ ├── en-US.js │ ├── en.js │ └── i18n.js ├── Mixins │ ├── AuthHelper.js │ ├── DispatcherListener.js │ ├── KeyboardListener.js │ ├── ListHelper.js │ ├── NavBarHelper.js │ └── NavigationListener.js ├── Models │ ├── CurrentUser.js │ ├── Environment.js │ ├── Follow.js │ └── Post.js ├── Navigation │ ├── NavigationBar.js │ ├── NavigationButton.js │ ├── NavigationHeader.js │ ├── NavigationTitle.js │ ├── Navigator.js │ ├── Router.js │ └── Routes.js ├── Platform │ ├── Back.android.js │ ├── Back.ios.js │ ├── Keychain.android.js │ ├── Keychain.ios.js │ ├── RefreshableListView.android.js │ ├── RefreshableListView.ios.js │ ├── StatusBar.android.js │ └── StatusBar.ios.js ├── Root.js ├── Root │ ├── Launch.js │ ├── Launcher.js │ ├── LoggedIn.js │ ├── LoggedOut.js │ ├── TestComponents.js │ └── TestRunner.js ├── Screens │ ├── CreatePost.js │ ├── FollowList.js │ ├── Loading.js │ ├── LogIn.js │ ├── PostList.js │ ├── Settings.js │ └── SignUp.js ├── Stores │ ├── CurrentUserStore.js │ ├── DebugStore.js │ ├── EnvironmentStore.js │ ├── FollowListStore.js │ ├── LocalKeyStore.js │ └── PostListStore.js ├── Vendor │ └── react-native-refreshable-listview │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .npmignore │ │ ├── .travis.yml │ │ ├── index.js │ │ └── lib │ │ ├── ControlledRefreshableListView.js │ │ ├── ListView.js │ │ ├── RefreshableListView.js │ │ ├── RefreshingIndicator.js │ │ ├── createElementFrom.js │ │ └── delay.js └── jsVersion.js ├── MIT-LICENSE ├── README.md ├── android ├── Sample.iml ├── app │ ├── BUCK │ ├── app.iml │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── sample │ │ │ ├── EnvironmentManager.java │ │ │ ├── MainActivity.java │ │ │ ├── MainApplication.java │ │ │ ├── SampleConstants.java │ │ │ ├── SamplePackage.java │ │ │ ├── TLSSetup.java │ │ │ ├── TLSSocketFactory.java │ │ │ ├── TestRunnerManager.java │ │ │ └── utils │ │ │ └── LocaleUtils.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── index.android.js ├── index.ios.js ├── ios ├── Debug.xcconfig ├── Podfile ├── Podfile.lock ├── Sample.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── Sample Staging.xcscheme │ │ ├── Sample Test.xcscheme │ │ └── Sample.xcscheme ├── Sample.xcworkspace │ └── contents.xcworkspacedata ├── Sample │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── EnvironmentManager.m │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Sample.entitlements │ ├── TestRunnerManager.m │ └── main.m ├── Staging.xcconfig └── main.jsbundle ├── package.json ├── screenshots ├── create_post.png ├── follows.png ├── show_post.png └── sign_up.png ├── server ├── .nvmrc ├── auth.js ├── follows.js ├── models.js ├── package.json ├── posts.js ├── secured.js └── server.js ├── tasks ├── _compiler.js ├── _translation.js ├── compile.js ├── compiler.js ├── react-native-stub.js ├── translation-en-GB.json ├── translation-en-US.json └── translation.js └── test ├── helpers ├── appium.js ├── bootstrap.js ├── dispatcher.js ├── driver.js ├── fixtures.js ├── packager.js └── server.js └── integration ├── authentication.test.js ├── follows.test.js ├── posts.test.js └── smoke.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"], 3 | "plugins": ["syntax-async-functions", "transform-regenerator"] 4 | } 5 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.android.js 5 | 6 | # Ignore templates with `@flow` in header 7 | .*/local-cli/generator.* 8 | 9 | # Ignore malformed json 10 | .*/node_modules/y18n/test/.*\.json 11 | 12 | # Ignore the website subdir 13 | /website/.* 14 | 15 | # Ignore BUCK generated dirs 16 | /\.buckd/ 17 | 18 | # Ignore unexpected extra @providesModule 19 | .*/node_modules/commoner/test/source/widget/share.js 20 | 21 | # Ignore duplicate module providers 22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root 23 | .*/Libraries/react-native/React.js 24 | .*/Libraries/react-native/ReactNative.js 25 | .*/node_modules/jest-runtime/build/__tests__/.* 26 | 27 | [include] 28 | 29 | [libs] 30 | node_modules/react-native/Libraries/react-native/react-native-interface.js 31 | node_modules/react-native/flow 32 | flow/ 33 | 34 | [options] 35 | module.system=haste 36 | 37 | esproposal.class_static_fields=enable 38 | esproposal.class_instance_fields=enable 39 | 40 | experimental.strict_type_args=true 41 | 42 | munge_underscores=true 43 | 44 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 45 | 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' 46 | 47 | suppress_type=$FlowIssue 48 | suppress_type=$FlowFixMe 49 | suppress_type=$FixMe 50 | 51 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(30\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 52 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(30\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 53 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 54 | 55 | unsafe.enable_getters_and_setters=true 56 | 57 | [version] 58 | ^0.30.0 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | .idea 28 | .gradle 29 | local.properties 30 | 31 | # node.js 32 | # 33 | node_modules/ 34 | npm-debug.log 35 | 36 | # Custom 37 | Pods 38 | /testbuild 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Hepburn 2 | language: objective-c 3 | osx_image: xcode7 4 | xcode_sdk: iphonesimulator9.0 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | - ios/Pods 10 | - ~/.nvm 11 | 12 | before_install: 13 | - export NVM_DIR=~/.nvm 14 | - which nvm || curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.29.0/install.sh | bash 15 | - source ~/.nvm/nvm.sh --install 16 | - nvm install 4.2.3 17 | - brew update 18 | - brew reinstall xctool 19 | - brew reinstall watchman 20 | - npm install 21 | - gem install xcpretty 22 | - gem install cocoapods 23 | - pod install --project-directory=ios 24 | 25 | before_script: 26 | - npm run compile:test 27 | 28 | script: 29 | - npm test 30 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /App/Actions/AppActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | import AppConstants from '../Constants/AppConstants'; 3 | import assign from 'object-assign'; 4 | 5 | var AppActions = { 6 | appLaunched: function() { 7 | Dispatcher.dispatch({ 8 | actionType: AppConstants.APP_LAUNCHED 9 | }); 10 | }, 11 | 12 | reloadCurrentPath: function() { 13 | Dispatcher.dispatch({ 14 | actionType: AppConstants.RELOAD_PATH 15 | }); 16 | }, 17 | 18 | launchRoutePath: function(routePath) { 19 | console.log("Launching! " + routePath); 20 | Dispatcher.dispatch({ 21 | actionType: AppConstants.LAUNCH_ROUTE_PATH, 22 | routePath: routePath 23 | }); 24 | }, 25 | 26 | launchExternalURL: function(url) { 27 | console.log("Launching! " + url); 28 | Dispatcher.dispatch({ 29 | actionType: AppConstants.OPEN_URL, 30 | url: url 31 | }); 32 | }, 33 | 34 | launchItem: function(props) { 35 | if (props.actionType) { 36 | console.log("Action! " + props.actionType); 37 | Dispatcher.dispatch(props); 38 | } 39 | else if (props.routePath) { 40 | console.log(props.routePath); 41 | this.launchRoutePath(props.routePath); 42 | } 43 | else { 44 | console.log("Unknown launchItem"); 45 | } 46 | }, 47 | 48 | launchRelativeItem: function(currentRoute, item) { 49 | var navItem = assign({}, item); // clone so we can mess with it 50 | 51 | if(!navItem.routePath && navItem.replacePath) { 52 | var pieces = currentRoute.routePath.split("/"); 53 | pieces[pieces.length-1] = navItem.replacePath; 54 | navItem.routePath = pieces.join('/'); 55 | } 56 | if(!navItem.routePath && navItem.subPath) { 57 | navItem.routePath = currentRoute.routePath + "/" + navItem.subPath; 58 | } 59 | navItem.currentRoute = currentRoute; 60 | this.launchItem(navItem); 61 | }, 62 | 63 | launchNavItem: function(currentRoute, item) { 64 | var navItem = assign({}, item); // clone so we can mess with it 65 | navItem.targetPath = currentRoute.routePath; 66 | this.launchRelativeItem(currentRoute, navItem); 67 | }, 68 | 69 | }; 70 | 71 | export default AppActions; 72 | -------------------------------------------------------------------------------- /App/Actions/AuthActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | import AppConstants from '../Constants/AppConstants'; 3 | import AuthService from '../Api/AuthService'; 4 | 5 | var AuthActions = { 6 | authCallback: function(callback) { 7 | return function(error, data) { 8 | if(callback) callback(error); 9 | 10 | if (!error) { 11 | Dispatcher.dispatch({ 12 | actionType: AppConstants.LOGIN_USER, 13 | userProps: data.userProps, 14 | token: data.token 15 | }); 16 | } 17 | }; 18 | }, 19 | 20 | submitLogin: function(username, password, callback) { 21 | AuthService.login(username, password, this.authCallback(callback)); 22 | }, 23 | 24 | submitSignup: function(username, password, callback) { 25 | AuthService.signup(username, password, this.authCallback(callback)); 26 | } 27 | }; 28 | 29 | export default AuthActions; 30 | -------------------------------------------------------------------------------- /App/Actions/FollowActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | import AppConstants from '../Constants/AppConstants'; 3 | import FollowService from '../Api/FollowService'; 4 | 5 | var FollowActions = { 6 | 7 | fetchList: function(username, callback) { 8 | FollowService.fetchList(username, function(error, listProps) { 9 | if(callback) callback(error); 10 | 11 | if (!error) { 12 | Dispatcher.dispatch({ 13 | actionType: AppConstants.FOLLOW_LIST_UPDATED, 14 | listProps: listProps 15 | }); 16 | } 17 | }); 18 | } 19 | }; 20 | 21 | export default FollowActions; 22 | -------------------------------------------------------------------------------- /App/Actions/PostActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | import AppConstants from '../Constants/AppConstants'; 3 | import PostService from '../Api/PostService'; 4 | 5 | var PostActions = { 6 | 7 | fetchList: function(username, callback) { 8 | PostService.fetchList(username, function(error, listProps) { 9 | if(callback) callback(error); 10 | 11 | if (!error) { 12 | Dispatcher.dispatch({ 13 | actionType: AppConstants.POST_LIST_UPDATED, 14 | listProps: listProps 15 | }); 16 | } 17 | }); 18 | }, 19 | 20 | createPost: function(content, callback) { 21 | PostService.createPost(content, function(error, postProps) { 22 | if(callback) callback(error); 23 | 24 | if (!error) { 25 | Dispatcher.dispatch({ 26 | actionType: AppConstants.POST_ADDED, 27 | postProps: postProps 28 | }); 29 | } 30 | }); 31 | } 32 | }; 33 | 34 | export default PostActions; 35 | -------------------------------------------------------------------------------- /App/Api/AuthService.js: -------------------------------------------------------------------------------- 1 | import client from '../Api/HTTPClient'; 2 | 3 | var UserService = { 4 | parseAccount: function(response) { 5 | if (!response) return null; 6 | 7 | var data = {}; 8 | data.token = response.token; 9 | data.userProps = { 10 | id: response.id, 11 | username: response.username 12 | }; 13 | return data; 14 | }, 15 | 16 | accountCallback: function(callback) { 17 | return function(error, response) { 18 | var data = UserService.parseAccount(response); 19 | callback(error, data); 20 | }; 21 | }, 22 | 23 | signup: function(username, password, callback) { 24 | client.post("api/signup", {username: username, password: password}, UserService.accountCallback(callback)); 25 | }, 26 | 27 | login: function(username, password, callback) { 28 | client.post("api/login", {username: username, password: password}, UserService.accountCallback(callback)); 29 | } 30 | }; 31 | 32 | export default UserService; 33 | -------------------------------------------------------------------------------- /App/Api/FollowService.js: -------------------------------------------------------------------------------- 1 | import client from '../Api/HTTPClient'; 2 | 3 | var FolllowService = { 4 | parseFolllow: function(response) { 5 | if (!response) return null; 6 | 7 | return { 8 | id: response.id, 9 | username: response.username 10 | }; 11 | }, 12 | 13 | parseFolllows: function(response) { 14 | if (!response) return null; 15 | 16 | var out = {follows: []}; 17 | for(var i in response.follows) { 18 | out.follows.push(FolllowService.parseFolllow(response.follows[i])); 19 | } 20 | out.username = response.username; 21 | return out; 22 | }, 23 | 24 | fetchList: function(username, callback) { 25 | client.get("api/follows/" + username, {}, function(error, response) { 26 | var listProps = FolllowService.parseFolllows(response); 27 | callback(error, listProps); 28 | }); 29 | } 30 | }; 31 | 32 | export default FolllowService; 33 | -------------------------------------------------------------------------------- /App/Api/HTTPClient.js: -------------------------------------------------------------------------------- 1 | // http://visionmedia.github.io/superagent 2 | import superagent from 'superagent'; 3 | 4 | import Network from '../Api/Network'; 5 | 6 | import CurrentUserStore from '../Stores/CurrentUserStore'; 7 | import EnvironmentStore from '../Stores/EnvironmentStore'; 8 | 9 | var HTTPClient = { 10 | wrapper: function(inner) { 11 | return function(error, response) { 12 | Network.completed(); 13 | 14 | if(!inner) return; 15 | // chance to wrap and call original 16 | 17 | var parsed = null; 18 | if(response && response.text && response.text.length > 0) { 19 | try { 20 | parsed = JSON.parse(response.text); 21 | } 22 | catch (e) { 23 | parsed = null; 24 | // TODO: some other error? 25 | console.log("HTTPClient could not parse:\n\n" + response.text); 26 | } 27 | } 28 | 29 | var errorObj = null; 30 | var valueObj = null; 31 | 32 | if (error) { 33 | // error.status => 422 34 | errorObj = {}; 35 | if (error.status) { 36 | errorObj.status = error.status; // 422 37 | } 38 | else { 39 | errorObj.status = 520; // Unknown error 40 | } 41 | 42 | errorObj.errors = []; 43 | if (parsed && parsed.error) { 44 | errorObj.message = parsed.error 45 | } 46 | if (!errorObj.message) { 47 | errorObj.message = 'Server Error: ' + errorObj.status; 48 | } 49 | console.log("http error (" + errorObj.status + "): " + errorObj.message); 50 | } 51 | else { 52 | valueObj = parsed; 53 | } 54 | inner(errorObj, valueObj); 55 | }; 56 | }, 57 | 58 | addHeaders: function(req) { 59 | // TODO: load version from somewhere 60 | var appVersion = "1.0"; 61 | var userAgent = "Sample iPhone v" + appVersion; 62 | var locale = 'en-US'; 63 | 64 | req = req.accept('application/json'); 65 | req = req.type('application/json'); 66 | req = req.set('User-Agent', userAgent); 67 | req = req.set('X-CLIENT-VERSION', appVersion); 68 | req = req.set('X-Sample-User-Agent', userAgent); 69 | req = req.set('X-LOCALE', locale); 70 | 71 | var currentUser = CurrentUserStore.get(); 72 | if (currentUser && currentUser.getToken()) { 73 | req = req.set('Authorization', 'Bearer ' + currentUser.getToken()); 74 | } 75 | 76 | // if (currentUser && currentUser.data.guid) { 77 | // req = req.set('X-GUID', currentUser.data.guid); 78 | // } 79 | // if (currentUser && currentUser.data.ab_decision_group_id) { 80 | // req = req.set('X-AB-DECISION-GROUP-ID', currentUser.data.ab_decision_group_id.toString()); 81 | // } 82 | // if (currentUser && currentUser.data.ab_decision) { 83 | // req = req.set('X-AB-DECISION', currentUser.data.ab_decision); 84 | // } 85 | 86 | return req; 87 | }, 88 | 89 | fetch: function(req, callback) { 90 | req = this.addHeaders(req); 91 | Network.started(); 92 | req.end(this.wrapper(callback)); 93 | }, 94 | 95 | url: function(path) { 96 | var host = EnvironmentStore.get().getApiHost(); 97 | return host + "/" + path; 98 | }, 99 | 100 | post: function(path, values, callback) { 101 | var req = superagent.post(this.url(path)); 102 | if (values) { 103 | req = req.send(values); 104 | } 105 | this.fetch(req, callback); 106 | }, 107 | 108 | put: function(path, values, callback) { 109 | var req = superagent.put(this.url(path)); 110 | if (values) { 111 | req = req.send(values); 112 | } 113 | this.fetch(req, callback); 114 | }, 115 | 116 | 117 | get: function(path, params, callback) { 118 | var req = superagent.get(this.url(path)); 119 | if (params) { 120 | req = req.query(params); 121 | } 122 | this.fetch(req, callback); 123 | } 124 | }; 125 | 126 | export default HTTPClient; 127 | -------------------------------------------------------------------------------- /App/Api/Network.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | import AppConstants from '../Constants/AppConstants'; 3 | 4 | var _httpCount = 0; // TODO: immutable? 5 | 6 | var Network = { 7 | onThread: function(callback) { 8 | // TODO: should this be requestAnimationFrame? 9 | global.setTimeout(callback, 0); 10 | }, 11 | 12 | started: function() { 13 | this.onThread(function() { 14 | if (_httpCount < 0) _httpCount = 0; 15 | _httpCount++; 16 | if (_httpCount == 1) { 17 | Dispatcher.dispatch({ 18 | actionType: AppConstants.NETWORK_ACTIVITY, 19 | isActive: true 20 | }); 21 | } 22 | }); 23 | }, 24 | completed: function() { 25 | this.onThread(function() { 26 | _httpCount--; 27 | if (_httpCount < 0) _httpCount = 0; 28 | if (_httpCount === 0) { 29 | Dispatcher.dispatch({ 30 | actionType: AppConstants.NETWORK_ACTIVITY, 31 | isActive: false 32 | }); 33 | } 34 | }); 35 | } 36 | }; 37 | 38 | export default Network; 39 | -------------------------------------------------------------------------------- /App/Api/PostService.js: -------------------------------------------------------------------------------- 1 | import client from '../Api/HTTPClient'; 2 | 3 | var PostService = { 4 | parsePost: function(response) { 5 | if (!response) return null; 6 | 7 | return { 8 | id: response.id, 9 | content: response.content, 10 | username: response.username 11 | }; 12 | }, 13 | 14 | parsePosts: function(response) { 15 | if (!response) return null; 16 | 17 | var out = {posts: []}; 18 | for(var i in response.posts) { 19 | out.posts.push(PostService.parsePost(response.posts[i])); 20 | } 21 | out.username = response.username; 22 | return out; 23 | }, 24 | 25 | fetchList: function(username, callback) { 26 | client.get("api/posts/" + username, {}, function(error, response) { 27 | var listProps = PostService.parsePosts(response); 28 | callback(error, listProps); 29 | }); 30 | }, 31 | 32 | createPost: function(content, callback) { 33 | client.post("api/posts", {content: content}, function(error, response) { 34 | var postProps = PostService.parsePost(response); 35 | callback(error, postProps); 36 | }); 37 | }, 38 | }; 39 | 40 | export default PostService; 41 | -------------------------------------------------------------------------------- /App/Components/AutoScaleText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactNative, { 4 | NativeModules, 5 | } from 'react-native'; 6 | 7 | const UIManager = NativeModules.UIManager; 8 | 9 | import Text from './Text'; 10 | 11 | class AutoScaleText extends React.Component { 12 | static propTypes = { 13 | ...Text.propTypes, 14 | maxFontSize: React.PropTypes.number.isRequired, 15 | //maxHeight: React.PropTypes.number.isRequired, 16 | color: React.PropTypes.string, 17 | //style: React.PropTypes.oneOfType([ 18 | // React.PropTypes.number, 19 | // React.PropTypes.shape({ 20 | // width: React.PropTypes.number, 21 | // }), 22 | //]), 23 | }; 24 | 25 | static defaultProps = { 26 | color: 'black', 27 | }; 28 | 29 | constructor(props) { 30 | super(props); 31 | 32 | this.state = { 33 | fontSize: props.maxFontSize, 34 | finished: null, 35 | }; 36 | 37 | this.visible = true 38 | } 39 | 40 | determineFontSize = () => { 41 | UIManager.measure(ReactNative.findNodeHandle(this.refs.textView), (x, y, w, h, px, py) => { 42 | if (!this.visible) return; 43 | 44 | var tooBig = this.props.maxHeight && h > this.props.maxHeight; 45 | if (!tooBig) { 46 | tooBig = this.props.maxWidth && w > this.props.maxWidth; 47 | } 48 | if (tooBig) { 49 | this.setState({ 50 | fontSize: this.state.fontSize - 0.5, 51 | }); 52 | this.determineFontSize(); 53 | } 54 | else { 55 | this.setState({finished: true}); 56 | } 57 | }); 58 | }; 59 | 60 | componentWillUnmount() { 61 | this.visible = false; 62 | } 63 | 64 | render() { 65 | return ( 66 | 79 | {this.props.children} 80 | 81 | ); 82 | } 83 | } 84 | 85 | export default AutoScaleText; 86 | -------------------------------------------------------------------------------- /App/Components/SegmentedControl.js: -------------------------------------------------------------------------------- 1 | // https://github.com/ide/react-native-button 2 | 3 | import React from 'react'; 4 | import { 5 | StyleSheet, 6 | TouchableHighlight, 7 | View 8 | } from 'react-native'; 9 | 10 | import cssVar from '../Lib/cssVar'; 11 | import Text from '../Components/Text'; 12 | import AppActions from '../Actions/AppActions'; 13 | 14 | var SegmentedControl = React.createClass({ 15 | segmentComponents: function() { 16 | var out = []; 17 | for(var i = 0; i < this.props.items.length; i++) { 18 | var item = this.props.items[i]; 19 | var testID = null; 20 | if (!item.testID && this.props.appendTestId) { 21 | testID = 'seg' + item.title + '_' + this.props.appendTestId; 22 | } 23 | out.push( 24 | 25 | ); 26 | }; 27 | return out; 28 | }, 29 | 30 | render: function() { 31 | return ( 32 | 33 | 34 | {this.segmentComponents()} 35 | 36 | 37 | ); 38 | } 39 | }); 40 | 41 | var Segment = React.createClass({ 42 | onSelection: function() { 43 | AppActions.launchRelativeItem(this.props.currentRoute, this.props); 44 | }, 45 | 46 | render: function() { 47 | if (this.props.selected) { 48 | return ( 49 | 52 | 53 | 54 | {this.props.title} 55 | 56 | 57 | 58 | ); 59 | } 60 | else { 61 | return ( 62 | 68 | 69 | 70 | {this.props.title} 71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | }); 78 | 79 | 80 | var styles = StyleSheet.create({ 81 | container: { 82 | backgroundColor: cssVar('blue50'), 83 | padding: 10 84 | }, 85 | control: { 86 | flexDirection: 'row', 87 | borderColor: 'white', 88 | borderWidth: 1, 89 | borderRadius: 4 90 | }, 91 | flex: { 92 | flex: 1 93 | }, 94 | button: { 95 | padding: 5, 96 | margin: 1, 97 | justifyContent: 'center', 98 | alignItems: 'center', 99 | }, 100 | selectedSegment: { 101 | backgroundColor: 'white', 102 | }, 103 | linkButton: { 104 | backgroundColor: cssVar('blue50'), 105 | }, 106 | text: { 107 | fontSize: 16 108 | }, 109 | selectedText: { 110 | color: cssVar('blue50'), 111 | }, 112 | linkText: { 113 | color: 'white' 114 | } 115 | }); 116 | 117 | export default SegmentedControl; 118 | -------------------------------------------------------------------------------- /App/Components/SimpleList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ListView 4 | } from 'react-native'; 5 | 6 | import RefreshableListView from '../Platform/RefreshableListView'; 7 | 8 | import SimpleListItem from '../Components/SimpleListItem'; 9 | 10 | var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 11 | 12 | var SimpleList = React.createClass({ 13 | renderRow: function(item, sectionId, rowId) { 14 | var passAlong = {}; 15 | if (this.props.currentRoute) passAlong.currentRoute = this.props.currentRoute; 16 | if (this.props.navigation) passAlong.navigation = this.props.navigation; 17 | if (this.props.nextIcon) passAlong.nextIcon = this.props.nextIcon; 18 | if (this.props.noTap) passAlong.noTap = this.props.noTap; 19 | 20 | if (this.props.getItemProps) { 21 | // swtich it out 22 | item = this.props.getItemProps(item); 23 | } 24 | 25 | return ( 26 | 27 | ); 28 | }, 29 | 30 | render: function() { 31 | var Component = this.props.reloadList ? RefreshableListView : ListView; 32 | return ( 33 | 39 | ); 40 | } 41 | }); 42 | 43 | export default SimpleList; 44 | -------------------------------------------------------------------------------- /App/Components/SimpleListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | TouchableHighlight 6 | } from 'react-native'; 7 | 8 | import cssVar from '../Lib/cssVar'; 9 | 10 | import Text from '../Components/Text'; 11 | import AppActions from '../Actions/AppActions'; 12 | 13 | var SimpleListItem = React.createClass({ 14 | onSelection: function() { 15 | AppActions.launchRelativeItem(this.props.currentRoute, this.props); 16 | }, 17 | 18 | renderTitle: function() { 19 | if (!this.props.title) return null; 20 | 21 | return ( 22 | 23 | {this.props.title} 24 | 25 | ); 26 | }, 27 | 28 | renderSubtitle: function() { 29 | if (!this.props.subtitle) return null; 30 | 31 | return ( 32 | 33 | {this.props.subtitle} 34 | 35 | ); 36 | }, 37 | 38 | renderRightIcon: function() { 39 | if (!this.props.nextIcon) return null; 40 | 41 | // caret-right-semi 42 | return ( 43 | 44 | > 45 | 46 | ); 47 | }, 48 | 49 | renderContent: function() { 50 | return ( 51 | 52 | 53 | {this.renderTitle()} 54 | {this.renderSubtitle()} 55 | 56 | 57 | {this.renderRightIcon()} 58 | 59 | 60 | ); 61 | }, 62 | 63 | render: function() { 64 | if (this.props.noTap) { 65 | return this.renderContent(); 66 | } 67 | 68 | return ( 69 | 70 | 75 | {this.renderContent()} 76 | 77 | 78 | 79 | ); 80 | } 81 | }); 82 | 83 | var styles = StyleSheet.create({ 84 | touch: { 85 | backgroundColor: 'white' 86 | }, 87 | row: { 88 | flexDirection: 'row', 89 | alignItems: 'center', 90 | padding:20 91 | }, 92 | title: { 93 | fontSize: 18, 94 | }, 95 | subtitle: { 96 | paddingTop: 5, 97 | fontSize: 14, 98 | color: cssVar('gray20'), 99 | }, 100 | left: { 101 | flex: 1, 102 | }, 103 | right: { 104 | 105 | }, 106 | rightIcon: { 107 | fontFamily: cssVar('fontIcon'), 108 | color: cssVar('gray30'), 109 | fontSize: 12, 110 | } 111 | }); 112 | 113 | export default SimpleListItem; 114 | -------------------------------------------------------------------------------- /App/Components/SpinnerLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | PanResponder, 5 | PixelRatio, 6 | View, 7 | PropTypes, 8 | ActivityIndicator, 9 | } from 'react-native'; 10 | 11 | import cssVar from '../Lib/cssVar'; 12 | 13 | const SpinnerLoader = React.createClass({ 14 | propTypes: { 15 | spinning: React.PropTypes.bool, 16 | }, 17 | 18 | getInitialState() { 19 | return { 20 | width: 0, 21 | height: 0, 22 | }; 23 | }, 24 | 25 | componentWillMount() { 26 | this._panGesture = PanResponder.create({ 27 | onStartShouldSetPanResponder: (evt, gestureState) => { return false }, 28 | onStartShouldSetPanResponderCapture: (evt, gestureState) => { return false }, 29 | onMoveShouldSetPanResponder: (evt, gestureState) => { return false }, 30 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => { return false }, 31 | }); 32 | }, 33 | 34 | overlayStyle() { 35 | return { 36 | top: 0, 37 | left: 0, 38 | height: this.state.height, 39 | width: this.state.width, 40 | backgroundColor: 'transparent', 41 | alignItems: 'center', 42 | justifyContent: 'center', 43 | position: 'absolute', 44 | }; 45 | }, 46 | 47 | onChildrenLayout(event) { 48 | this.setState({ 49 | height: event.nativeEvent.layout.height, 50 | width: event.nativeEvent.layout.width, 51 | }); 52 | }, 53 | 54 | renderSpinner() { 55 | if (!this.props.spinning || this.state.height === 0) { 56 | return null; 57 | } 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }, 67 | 68 | render() { 69 | return ( 70 | 71 | {this.props.children} 72 | {this.renderSpinner()} 73 | 74 | ); 75 | }, 76 | 77 | }); 78 | 79 | var styles = StyleSheet.create({ 80 | spinnerContainer: { 81 | width: 60, 82 | height: 60, 83 | backgroundColor: 'transparent', 84 | alignItems: 'center', 85 | justifyContent: 'center', 86 | borderRadius: 60 / PixelRatio.get(), 87 | opacity: 0.70, 88 | }, 89 | spinner: { 90 | width: 32, 91 | height: 32, 92 | }, 93 | }); 94 | 95 | export default SpinnerLoader; 96 | -------------------------------------------------------------------------------- /App/Components/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactNative, { 4 | Text, 5 | StyleSheet, 6 | } from 'react-native'; 7 | 8 | import cssVar from '../Lib/cssVar'; 9 | 10 | var _Text = React.createClass({ 11 | propTypes: Text.propTypes, 12 | 13 | setNativeProps() { 14 | var text = this.refs.text; 15 | text.setNativeProps.apply(text, arguments); 16 | }, 17 | render() { 18 | return ( 19 | 24 | ); 25 | } 26 | }) 27 | 28 | var styles = StyleSheet.create({ 29 | text: { 30 | fontFamily: cssVar('fontRegular'), 31 | color: cssVar('gray90'), 32 | fontSize: 8 // make it small to know it's not set 33 | } 34 | }); 35 | 36 | export default _Text; 37 | -------------------------------------------------------------------------------- /App/Components/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | PixelRatio, 5 | StyleSheet, 6 | TextInput, 7 | } from 'react-native'; 8 | 9 | import cssVar from '../Lib/cssVar'; 10 | 11 | class _TextInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | } 15 | 16 | setNativeProps(...args) { 17 | this.refs.input.setNativeProps(...args); 18 | } 19 | 20 | blur() { 21 | this.refs.input.blur(); 22 | } 23 | 24 | focus() { 25 | this.refs.input.focus(); 26 | } 27 | 28 | render() { 29 | return ( 30 | 35 | ); 36 | } 37 | } 38 | 39 | const styles = StyleSheet.create({ 40 | input: { 41 | borderWidth: 1 / PixelRatio.get(), 42 | borderColor: cssVar('gray50'), 43 | padding: 10, 44 | fontFamily: cssVar('fontRegular'), 45 | color: cssVar('gray90'), 46 | fontSize: 8, // make it small to know it's not set 47 | }, 48 | }); 49 | 50 | export default _TextInput; 51 | -------------------------------------------------------------------------------- /App/Constants/AppConstants.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | APP_LAUNCHED: null, 5 | LOGIN_USER: null, 6 | LOGOUT_REQUESTED: null, 7 | RELOAD_PATH: null, 8 | NETWORK_ACTIVITY: null, 9 | LAUNCH_ROUTE_PATH: null, 10 | NETWORK_ACTIVITY: null, 11 | NAVBAR_UPDATE: null, 12 | POST_LIST_UPDATED: null, 13 | POST_ADDED: null, 14 | FOLLOW_LIST_UPDATED: null, 15 | TEST_COMPONENT_ROUTE: null, 16 | DEBUG_CURRENT_ROUTE_PATH_KEY: null, 17 | }); 18 | -------------------------------------------------------------------------------- /App/Dispatcher.js: -------------------------------------------------------------------------------- 1 | import flux from 'flux'; 2 | const {Dispatcher} = flux; 3 | 4 | export default new Dispatcher(); 5 | -------------------------------------------------------------------------------- /App/Extensions/AddSpinnerLoader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import * as Extension from '../Extensions/Extension'; 4 | 5 | import SpinnerLoader from '../Components/SpinnerLoader'; 6 | 7 | var AddSpinnerLoader = { 8 | extensionName: 'AddSpinnerLoader', 9 | 10 | optionalParams: { 11 | spinnerContainerStyles: 'styles for the spinner loader', 12 | }, 13 | 14 | exports: { 15 | methods: ['start', 'stop'] 16 | }, 17 | 18 | getInitialState() { 19 | return { 20 | spinning: false, 21 | } 22 | }, 23 | 24 | start() { 25 | this.setState({spinning: true}) 26 | }, 27 | 28 | stop() { 29 | this.setState({spinning: false}) 30 | }, 31 | 32 | renderExtension() { 33 | return ( 34 | 38 | {this.renderComponent()} 39 | 40 | ) 41 | } 42 | }; 43 | 44 | export default Extension.create(AddSpinnerLoader); 45 | -------------------------------------------------------------------------------- /App/Extensions/Extension.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import invariant from 'invariant'; 3 | import _ from 'underscore'; 4 | 5 | const create = ({extensionName, requiredParams = {}, exports = {}, optionalParams, ...BaseLib}) => { 6 | invariant( 7 | extensionName, 8 | 'extensionName required when defining an extension' 9 | ); 10 | 11 | const Extension = (Component, params = {}) => { 12 | 13 | const componentName = Component.displayName || Component.name; 14 | const containerName = `${extensionName} (${componentName})`; 15 | 16 | _.pairs(requiredParams).forEach(([key, text]) => { 17 | invariant( 18 | params[key], 19 | `Extension params required ${containerName} ${key}: ${text}` 20 | ); 21 | }); 22 | 23 | var ComponentContainer = React.createClass(Object.assign(params, { 24 | mixins: [BaseLib], 25 | 26 | displayName: containerName, 27 | 28 | params: params, 29 | 30 | getInitialState() { 31 | return {}; 32 | }, 33 | 34 | originalComponent() { 35 | if (!this.refs._ExtensionComponent) { return null; } 36 | 37 | if (this.refs._ExtensionComponent.originalComponent) { 38 | return this.refs._ExtensionComponent.originalComponent(); 39 | } 40 | return this.refs._ExtensionComponent; 41 | }, 42 | 43 | // Implement getExtensionProps if you want to add more behavior passed to the Component 44 | // it will allow accessing in the Extended Component with this.props[ExtensionName] 45 | getExtensionProps() { 46 | return { 47 | [extensionName]: Object.assign({variables: this.getExportedVariables()}, this.getExportedMethods()) 48 | } 49 | }, 50 | 51 | getExportedVariables() { 52 | var _variables = {}; 53 | (exports.variables || []).forEach((variableName) => _variables[variableName] = this.state[variableName]); 54 | 55 | return _variables; 56 | }, 57 | 58 | getExportedMethods() { 59 | var _methods = {}; 60 | (exports.methods || []).forEach((methodName) => _methods[methodName] = this[methodName]); 61 | 62 | return _methods; 63 | }, 64 | 65 | renderComponent() { 66 | return ( 67 | 72 | ); 73 | }, 74 | 75 | render() { 76 | if (this.renderExtension) { 77 | return this.renderExtension(); 78 | } 79 | 80 | return this.renderComponent(); 81 | }, 82 | 83 | })); 84 | 85 | return ComponentContainer; 86 | }; 87 | 88 | return Extension; 89 | }; 90 | 91 | export {create}; 92 | -------------------------------------------------------------------------------- /App/Lib/CSSVarConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { 5 | PixelRatio 6 | } from 'react-native'; 7 | 8 | export default { 9 | gray90: '#323A3B', 10 | gray50: '#828A8B', 11 | gray30: '#B4B9B9', 12 | gray20: '#CFD2D3', 13 | gray10: '#EBECEC', 14 | gray5: '#F5F6F6', 15 | 16 | blue50: '#23B4D2', 17 | 18 | // http://iosfonts.com/ 19 | fontRegular: "HelveticaNeue", 20 | fontIcon: "HelveticaNeue", // TODO: get an icon font and include 21 | 22 | listLine: { 23 | backgroundColor: 'rgba(0, 0, 0, 0.1)', 24 | height: 1 / PixelRatio.get(), // thinnest possible line 25 | marginLeft: 10, 26 | }, 27 | listFullLine: { 28 | backgroundColor: 'rgba(0, 0, 0, 0.1)', 29 | height: 1 / PixelRatio.get(), // thinnest possible line 30 | marginLeft: 0, 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /App/Lib/assignDefined.js: -------------------------------------------------------------------------------- 1 | // https://github.com/sindresorhus/object-assign/blob/cad4374651f0d3c869e5f43287e6e99b86b13da0/index.js 2 | 3 | 'use strict'; 4 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 5 | 6 | function ToObject(val) { 7 | if (val == null) { 8 | throw new TypeError('Object.assign cannot be called with null or undefined'); 9 | } 10 | 11 | return Object(val); 12 | } 13 | 14 | function ownEnumerableKeys(obj) { 15 | var keys = Object.getOwnPropertyNames(obj); 16 | 17 | if (Object.getOwnPropertySymbols) { 18 | keys = keys.concat(Object.getOwnPropertySymbols(obj)); 19 | } 20 | 21 | return keys.filter(function (key) { 22 | // this file adds false if not defined 23 | if (typeof obj[key] === "undefined") return false; 24 | 25 | // otherwise normal thing 26 | return propIsEnumerable.call(obj, key); 27 | }); 28 | } 29 | 30 | export default /*Object.assign ||*/ function (target, source) { 31 | var from; 32 | var keys; 33 | var to = ToObject(target); 34 | 35 | for (var s = 1; s < arguments.length; s++) { 36 | from = arguments[s]; 37 | keys = ownEnumerableKeys(Object(from)); 38 | 39 | for (var i = 0; i < keys.length; i++) { 40 | to[keys[i]] = from[keys[i]]; 41 | } 42 | } 43 | 44 | return to; 45 | }; -------------------------------------------------------------------------------- /App/Lib/cssVar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import invariant from 'invariant'; 4 | import CSSVarConfig from '../Lib/CSSVarConfig'; 5 | 6 | var cssVar = function(/*string*/ key) /*string*/ { 7 | invariant(CSSVarConfig[key], 'invalid css variable ' + key); 8 | 9 | return CSSVarConfig[key]; 10 | }; 11 | 12 | export default cssVar; 13 | -------------------------------------------------------------------------------- /App/Locale.js: -------------------------------------------------------------------------------- 1 | import I18n from './Locales/boot'; 2 | 3 | var Manager = function(object) { 4 | this.key = object; 5 | }; 6 | 7 | Manager.prototype.en = function(hash) { 8 | if (I18n.translations['en'][this.key] && !process.env.TEST_UNIT) { 9 | console.warn(`Locale (en) key already exists: ${this.key}`); 10 | } 11 | I18n.translations['en'][this.key] = hash; 12 | return this; 13 | }; 14 | 15 | Manager.prototype.t = function(scope, options) { 16 | if (this.key) { 17 | scope = this.key + '.' + scope; // prepend our key 18 | } 19 | return I18n.t(scope, options); 20 | }; 21 | 22 | var Locale = { 23 | key: function(object, enHash) { 24 | var manager = new Manager(object); 25 | // common case is passing en 26 | if (enHash) { 27 | return manager.en(enHash); 28 | } 29 | else { 30 | return manager; 31 | } 32 | }, 33 | 34 | global: function() { 35 | return new Manager(null); 36 | }, 37 | 38 | lib: function() { 39 | return I18n; 40 | }, 41 | 42 | formatMoneyWithCountryIsoCode: function(cents, countryIsoCode, options = {}) { 43 | const locale = I18n.getLocaleFromCountryIsoCode(countryIsoCode); 44 | return Locale.lib().l('currency', cents, {locale, ...options}); 45 | }, 46 | 47 | formatMoneyWithCurrency: function(cents, currency, options = {}) { 48 | const locale = I18n.getLocaleFromCurrency(currency); 49 | return Locale.lib().l('currency', cents, {locale, ...options}); 50 | }, 51 | }; 52 | 53 | export default Locale; 54 | -------------------------------------------------------------------------------- /App/Locales/base.js: -------------------------------------------------------------------------------- 1 | // these do not get translated, mostly for values or something 2 | 3 | export default { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /App/Locales/boot.js: -------------------------------------------------------------------------------- 1 | import I18n from '../Locales/i18n'; 2 | 3 | I18n.translations['base'] = require('../Locales/base.js').default; // defaults, not translated - likely for set values 4 | I18n.translations['en'] = require('../Locales/en.js').default; // shared for english countries, generally loaded through Locale.register 5 | 6 | I18n.translations['en-US'] = require('../Locales/en-US.js').default; // known ones for US 7 | 8 | I18n.translations['en-GB'] = require('../Locales/en-GB.js').default; // known ones for GB 9 | I18n.translations['en-GB-xtra'] = require('../Locales/en-GB-xtra.js').default; // programmatic - ideally empty and in translated 10 | 11 | 12 | I18n.default_locale = 'en-US'; 13 | I18n.locale = 'en-US'; 14 | 15 | I18n.fallbacks = { 16 | 'en-US': ['en', 'base'], 17 | 'en-GB': ['en-GB-xtra', 'en', 'base'], 18 | }; 19 | 20 | const countryIsoCodesToLocale = { 21 | 'US': 'en-US', 22 | 'GB': 'en-GB', 23 | }; 24 | 25 | const currenciesToLocale = { 26 | 'USD': 'en-US', 27 | 'GBP': 'en-GB', 28 | }; 29 | 30 | I18n.register = function(component, enHash) { 31 | I18n.translations['en'][component] = enHash; 32 | }; 33 | 34 | I18n.getLocaleFromCountryIsoCode = (countryIsoCode) => { 35 | return countryIsoCodesToLocale[countryIsoCode] || countryIsoCodesToLocale['US']; 36 | }; 37 | 38 | I18n.getLocaleFromCurrency = (countryIsoCode) => { 39 | return currenciesToLocale[countryIsoCode] || currenciesToLocale['USD']; 40 | }; 41 | 42 | export default I18n; 43 | -------------------------------------------------------------------------------- /App/Locales/en-GB-xtra.js: -------------------------------------------------------------------------------- 1 | // Programatic GB translations - do not add directly 2 | // Use npm run translation:backfill 3 | 4 | export default {}; -------------------------------------------------------------------------------- /App/Locales/en-GB.js: -------------------------------------------------------------------------------- 1 | // Known GB Translations - generally matches US ones 2 | 3 | export default { 4 | datetime: { 5 | day_and_time: '%a, %b %-d, %-H:%M', 6 | day_header: '%+A - %B %d', 7 | short_day: '%a %b %-d', 8 | short_time: '%-H:%M', 9 | month_day: '%B %-d', 10 | short_month_day: '%b %-d', 11 | month_day_year: '%e %b %Y', 12 | month_day_year_at_time: '%-d %B %Y at %-H:%M', 13 | short_weekday_name: '%a', 14 | long_day_of_month: '%d', 15 | time_zone: '%Z', 16 | }, 17 | number: { 18 | currency: { 19 | format: { 20 | delimiter: ',', 21 | format: '%u%n', 22 | precision: 2, 23 | separator: '.', 24 | significant: false, 25 | strip_insignificant_zeros: false, 26 | unit: '£', 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /App/Locales/en-US.js: -------------------------------------------------------------------------------- 1 | // All US Translations, more or less should match the GB one 2 | 3 | export default { 4 | datetime: { 5 | day_and_time: '%a, %b %-d, %-I:%M %p', 6 | day_header: '%+A - %B %d', 7 | short_day: '%a %b %-d', 8 | short_time: '%-I:%M %p', 9 | month_day: '%B %-d', 10 | short_month_day: '%b %-d', 11 | month_day_year: '%b %e, %Y', 12 | month_day_year_at_time: '%B %-d, %Y at %-I:%M %p', 13 | short_weekday_name: '%a', 14 | long_day_of_month: '%d', 15 | time_zone: '%Z', 16 | }, 17 | number: { 18 | currency: { 19 | format: { 20 | delimiter: ',', 21 | format: '%u%n', 22 | precision: 2, 23 | separator: '.', 24 | significant: false, 25 | strip_insignificant_zeros: false, 26 | unit: '$', 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /App/Locales/en.js: -------------------------------------------------------------------------------- 1 | // base translations that get translated 2 | 3 | export default { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /App/Mixins/DispatcherListener.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../Dispatcher'; 2 | 3 | var DispatcherListener = { 4 | unregisterDispatcher: function() { 5 | if(this.dispatchToken) { 6 | Dispatcher.unregister(this.dispatchToken); 7 | this.dispatchToken = null; 8 | } 9 | }, 10 | 11 | dispatchedActionCallback: function(action) { 12 | if (this.isMounted()) { 13 | if (action.targetPath) { 14 | if (this.props.currentRoute && this.props.currentRoute.routePath === action.targetPath) { 15 | this.dispatchAction(action); 16 | } 17 | } 18 | else { 19 | this.dispatchAction(action); 20 | } 21 | } 22 | }, 23 | 24 | componentDidMount: function() { 25 | this.unregisterDispatcher(); 26 | 27 | var self = this; 28 | this.dispatchToken = Dispatcher.register(this.dispatchedActionCallback); 29 | }, 30 | 31 | componentWillUnmount: function() { 32 | this.unregisterDispatcher(); 33 | }, 34 | }; 35 | 36 | export default DispatcherListener; 37 | -------------------------------------------------------------------------------- /App/Mixins/KeyboardListener.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Keyboard, 5 | } from 'react-native'; 6 | 7 | 8 | var KeyboardListener = { 9 | getInitialState: function() { 10 | return { 11 | keyboardSpace: 0 12 | }; 13 | }, 14 | 15 | isKeyboardVisible: function() { 16 | return this.state.keyboardSpace > 0; 17 | }, 18 | 19 | updateKeyboardSpace: function(e) { 20 | if (this.isMounted() && e && e.endCoordinates) { 21 | this.setState({keyboardSpace: e.endCoordinates.height}); 22 | } 23 | }, 24 | 25 | resetKeyboardSpace: function() { 26 | if (this.isMounted()) { 27 | this.setState({keyboardSpace: 0}); 28 | } 29 | }, 30 | 31 | // onKeyboardHideCallback: function() { 32 | // // TODO handle case when the keyboard never showed up in the first place 33 | // // Might want to use state to check that it did showed up 34 | // if (this.params.onKeyboardHide) { 35 | // this.params.onKeyboardHide.call(this.originalComponent()); 36 | // } 37 | // }, 38 | 39 | 40 | componentDidMount: function() { 41 | this._keyboardSubscriptions = []; 42 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardWillShow', this.updateKeyboardSpace)); 43 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidShow', this.updateKeyboardSpace)); 44 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardWillHide', this.resetKeyboardSpace)); 45 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidHide', this.resetKeyboardSpace)); 46 | //if (this.params.onKeyboardHide) { 47 | // this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidHide', this.onKeyboardHideCallback)); 48 | //} 49 | }, 50 | 51 | componentWillUnmount: function() { 52 | this._keyboardSubscriptions.forEach((subscription) => { 53 | subscription.remove(); 54 | }); 55 | }, 56 | 57 | }; 58 | 59 | export default KeyboardListener; 60 | -------------------------------------------------------------------------------- /App/Mixins/ListHelper.js: -------------------------------------------------------------------------------- 1 | // helper to handle showing lists of things 2 | // parent must implement methods 3 | // required: getListItem, getItemProps 4 | // optional: isListChange, reloadList 5 | // 6 | // and give props 7 | // required: store, currentRoute 8 | // optional: listProps, segment 9 | 10 | import React from 'react'; 11 | import { 12 | View, 13 | StyleSheet, 14 | ListView 15 | } from 'react-native'; 16 | 17 | import Locale from '../Locale'; 18 | 19 | import CurrentUserStore from '../Stores/CurrentUserStore'; 20 | import NavigationListener from '../Mixins/NavigationListener'; 21 | import NavBarHelper from '../Mixins/NavBarHelper'; 22 | 23 | import Loading from '../Screens/Loading'; 24 | import Text from '../Components/Text'; 25 | import SegmentedControl from '../Components/SegmentedControl'; 26 | import SimpleList from '../Components/SimpleList'; 27 | 28 | var ListHelper = { 29 | mixins: [NavigationListener, NavBarHelper], 30 | 31 | getInitialState: function() { 32 | return this.getListState(); 33 | }, 34 | 35 | getListState: function() { 36 | return { 37 | items: this.getListItems() 38 | }; 39 | }, 40 | 41 | onListChange: function(arg) { 42 | if (!this.isListChange || this.isListChange(arg)) { 43 | this.setState(this.getListState()); 44 | } 45 | }, 46 | 47 | onDidFocusNavigation: function() { 48 | // items may have changed 49 | this.setState(this.getListState()); 50 | }, 51 | 52 | componentDidMount: function() { 53 | this.props.store.addChangeListener(this.onListChange); 54 | if (this.reloadList) { 55 | this.reloadList(); 56 | } 57 | }, 58 | 59 | componentWillUnmount: function() { 60 | this.props.store.removeChangeListener(this.onListChange); 61 | }, 62 | 63 | getNavBarState: function() { 64 | var title = this.props.username ? this.props.username : i18n.t('dashboard'); 65 | return { title: title }; 66 | }, 67 | 68 | getUsername: function() { 69 | if (!this.username) { 70 | this.username = this.props.username || CurrentUserStore.get().data.username; 71 | } 72 | return this.username; 73 | }, 74 | 75 | renderItems: function() { 76 | return ( 77 | 85 | ); 86 | }, 87 | 88 | renderEmpty: function() { 89 | return( 90 | 91 | No Items 92 | 93 | ); 94 | }, 95 | 96 | renderHeader: function() { 97 | if (!this.props.segment) return null; 98 | return ( 99 | 100 | ); 101 | }, 102 | 103 | renderContent: function() { 104 | var header = this.renderHeader(); 105 | var content; 106 | if (this.state.items.length === 0) { 107 | content = this.renderEmpty(); 108 | } 109 | else { 110 | content = this.renderItems(); 111 | } 112 | 113 | return ( 114 | 115 | {header} 116 | {content} 117 | 118 | ); 119 | }, 120 | 121 | render: function() { 122 | if (!this.state.items) { 123 | // TODO: load error? 124 | return ; 125 | } 126 | else { 127 | return this.renderContent(); 128 | } 129 | } 130 | }; 131 | 132 | var i18n = Locale.key('ListHelper', { 133 | dashboard: 'Dashboard' 134 | }); 135 | 136 | var styles = StyleSheet.create({ 137 | flex: { 138 | flex: 1 139 | } 140 | }); 141 | 142 | export default ListHelper; 143 | -------------------------------------------------------------------------------- /App/Mixins/NavBarHelper.js: -------------------------------------------------------------------------------- 1 | // parent must implement getNavBarState() 2 | 3 | import TimerMixin from 'react-timer-mixin'; 4 | 5 | import Dispatcher from '../Dispatcher'; 6 | import AppConstants from '../Constants/AppConstants'; 7 | 8 | var NavBarHelper = { 9 | mixins: [TimerMixin], 10 | 11 | componentDidMount: function() { 12 | this.updateNavTitle(); 13 | }, 14 | 15 | componentDidUpdate: function() { 16 | this.updateNavTitle(); 17 | }, 18 | 19 | updateNavTitle: function() { 20 | var updates = this.getNavBarState(); 21 | if (!updates) return; 22 | 23 | if(typeof updates.title !== "undefined") { 24 | this.props.currentRoute.title = updates.title; 25 | } 26 | if (typeof updates.navRightDisabled !== "undefined") { 27 | this.props.currentRoute.navRight.disabled = updates.navRightDisabled; 28 | } 29 | if (typeof updates.navRightIcon !== "undefined") { 30 | this.props.currentRoute.navRight.icon = updates.navRightIcon; 31 | } 32 | 33 | // if called during componentDidLoad, nav bar not loaded yet 34 | // requestAnimationFrame to allow it to finish 35 | var route = this.props.currentRoute; 36 | this.requestAnimationFrame(function() { 37 | Dispatcher.dispatch({ 38 | actionType: AppConstants.NAVBAR_UPDATE, 39 | route: route 40 | }); 41 | }); 42 | }, 43 | }; 44 | 45 | export default NavBarHelper; 46 | -------------------------------------------------------------------------------- /App/Mixins/NavigationListener.js: -------------------------------------------------------------------------------- 1 | var NavigationListener = { 2 | _onDidFocusNavigation: function(event) { 3 | if (event.data.route.routePath === this.props.currentRoute.routePath) { 4 | this._hasNavigationFocus = true; 5 | if(this.onDidFocusNavigation) { 6 | this.onDidFocusNavigation(); 7 | } 8 | } 9 | }, 10 | 11 | _onWillFocusNavigation: function(event) { 12 | if(this._hasNavigationFocus) { 13 | this._hasNavigationFocus = false; 14 | if(this.onDidUnfocusNavigation) { 15 | this.onDidUnfocusNavigation(); 16 | } 17 | } 18 | }, 19 | 20 | hasNavigationFocus: function() { 21 | return !!this._hasNavigationFocus; 22 | }, 23 | 24 | componentWillMount: function() { 25 | this._hasNavigationFocus = false; 26 | // TODO: this._onDidFocusNavigationSub = this.props.navigator.navigationContext.addListener('didfocus', this._onDidFocusNavigation) 27 | // TODO: this._onWillFocusNavigationSub = this.props.navigator.navigationContext.addListener('willfocus', this._onWillFocusNavigation) 28 | }, 29 | 30 | componentWillUnmount: function() { 31 | if(this._onDidFocusNavigationSub) { 32 | this._onDidFocusNavigationSub.remove(); 33 | this._onDidFocusNavigationSub = null; 34 | } 35 | if(this._onWillFocusNavigationSub) { 36 | this._onWillFocusNavigationSub.remove(); 37 | this._onWillFocusNavigationSub = null; 38 | } 39 | } 40 | }; 41 | 42 | export default NavigationListener; 43 | -------------------------------------------------------------------------------- /App/Models/CurrentUser.js: -------------------------------------------------------------------------------- 1 | import assign from '../Lib/assignDefined'; 2 | 3 | var Model = function(options, token) { 4 | this.data = {}; 5 | this.token = token; 6 | this.setAttributes(options); 7 | }; 8 | 9 | Model.prototype.setAttributes = function(options) { 10 | options = (options || {}); 11 | assign(this.data, { 12 | id: options.id, 13 | username: options.username 14 | }); 15 | }; 16 | 17 | Model.prototype.getToken = function() { 18 | return this.token; 19 | }; 20 | 21 | Model.prototype.isLoggedIn = function() { 22 | return !!this.token; 23 | }; 24 | 25 | export default Model; 26 | -------------------------------------------------------------------------------- /App/Models/Environment.js: -------------------------------------------------------------------------------- 1 | import assign from '../Lib/assignDefined'; 2 | import jsVersion from '../jsVersion'; 3 | 4 | var Model = function(options) { 5 | this.data = {}; 6 | this.setAttributes(options); 7 | }; 8 | 9 | Model.prototype.setAttributes = function(options) { 10 | options = (options || {}); 11 | assign(this.data, { 12 | name: options.name, 13 | simulator: options.simulator, 14 | buildCode: parseInt(options.buildCode), 15 | version: options.version, 16 | locale: options.locale 17 | }); 18 | }; 19 | 20 | Model.prototype.getApiHost = function() { 21 | switch(this.data.name) { 22 | case 'test': 23 | return 'http://localhost:3001'; 24 | case 'debug': 25 | return 'http://localhost:3000'; 26 | case 'staging': 27 | return 'https://someday.herokuapp.com'; 28 | default: 29 | throw("Unknown Environment.getApiHost: " + this.data.name); 30 | } 31 | }; 32 | 33 | Model.prototype.combinedBuildCode = function() { 34 | var ios = this.data.buildCode * 1000000; 35 | return ios + jsVersion; 36 | }; 37 | 38 | Model.prototype.displayVersion = function() { 39 | var out = this.data.version; 40 | out += "." + this.data.buildCode; 41 | out += "." + jsVersion; 42 | return out; 43 | }; 44 | 45 | Model.prototype.getLocale = function() { 46 | return this.data.locale; 47 | }; 48 | 49 | export default Model; 50 | -------------------------------------------------------------------------------- /App/Models/Follow.js: -------------------------------------------------------------------------------- 1 | import assign from '../Lib/assignDefined'; 2 | 3 | var Model = function(options) { 4 | this.data = {}; 5 | this.setAttributes(options); 6 | }; 7 | 8 | Model.prototype.setAttributes = function(options) { 9 | options = (options || {}); 10 | assign(this.data, { 11 | id: options.id, 12 | username: options.username 13 | }); 14 | }; 15 | 16 | export default Model; 17 | -------------------------------------------------------------------------------- /App/Models/Post.js: -------------------------------------------------------------------------------- 1 | import assign from '../Lib/assignDefined'; 2 | 3 | var Model = function(options) { 4 | this.data = {}; 5 | this.setAttributes(options); 6 | }; 7 | 8 | Model.prototype.setAttributes = function(options) { 9 | options = (options || {}); 10 | assign(this.data, { 11 | id: options.id, 12 | content: options.content, 13 | username: options.username 14 | }); 15 | }; 16 | 17 | export default Model; 18 | -------------------------------------------------------------------------------- /App/Navigation/NavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | View, 5 | NavigationExperimental, 6 | } from 'react-native'; 7 | 8 | import cssVar from '../Lib/cssVar'; 9 | 10 | import Back from '../Platform/Back'; 11 | import NavigationHeader from '../Navigation/NavigationHeader'; 12 | import Navigator from '../Navigation/Navigator'; 13 | 14 | const { 15 | CardStack: NavigationCardStack, 16 | } = NavigationExperimental; 17 | 18 | var stacksEqual = function(one, two, length) { 19 | if (one.length < length) return false; 20 | if (two.length < length) return false; 21 | 22 | for (var i=0; i < length; i++) { 23 | if (one[i].routePath !== two[i].routePath) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | }; 29 | 30 | var Container = React.createClass({ 31 | render: function() { 32 | var Component = this.props.route.component; 33 | return ( 34 | 38 | 43 | 44 | ); 45 | } 46 | }); 47 | 48 | var NavigationBar = { 49 | getInitialState: function() { 50 | return {}; 51 | }, 52 | 53 | renderHeader: function(sceneProps) { 54 | if (this.props.navBarHidden) { 55 | //return 56 | return null; 57 | } 58 | 59 | return ( 60 | 61 | ); 62 | 63 | }, 64 | 65 | renderScene: function(sceneProps) { 66 | var route = sceneProps.scene.route; 67 | console.log('renderScene: ' + route.routePath); 68 | 69 | return( 70 | 76 | ); 77 | }, 78 | 79 | onLoadedScene: function(component) { 80 | console.log("onLoadedScene"); 81 | if (component) { 82 | this._currentComponent = component.refs.mainComponent; 83 | } 84 | else { 85 | this._currentComponent = null; 86 | } 87 | }, 88 | 89 | componentWillMount: function() { 90 | this.navigation = new Navigator(); 91 | }, 92 | 93 | componentDidMount: function() { 94 | this.navigation.setStack(this.refs.stack); 95 | Back.setNavigator(this.navigation); 96 | }, 97 | 98 | render: function() { 99 | return ( 100 | 101 | 102 | 108 | 109 | ); 110 | } 111 | } 112 | 113 | var styles = StyleSheet.create({ 114 | appContainer: { 115 | flex: 1 116 | }, 117 | navBar: { 118 | backgroundColor: cssVar('blue50'), 119 | }, 120 | scene: { 121 | flex: 1, 122 | backgroundColor: cssVar('gray5'), 123 | }, 124 | sceneHidden: { 125 | marginTop: 0 126 | }, 127 | }); 128 | 129 | 130 | export default NavigationBar; 131 | -------------------------------------------------------------------------------- /App/Navigation/NavigationButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | TouchableOpacity 6 | } from 'react-native'; 7 | 8 | import cssVar from '../Lib/cssVar'; 9 | 10 | import DispatcherListener from '../Mixins/DispatcherListener'; 11 | import AppConstants from '../Constants/AppConstants'; 12 | import AppActions from '../Actions/AppActions'; 13 | 14 | import Text from '../Components/Text'; 15 | 16 | var NavigationButton = React.createClass({ 17 | mixins: [DispatcherListener], 18 | 19 | getInitialState: function() { 20 | return {updatedRoute: null}; 21 | }, 22 | 23 | dispatchAction: function(action) { 24 | switch(action.actionType) { 25 | case AppConstants.NAVBAR_UPDATE: 26 | var route = this.state.updatedRoute || this.props.route; 27 | if (action.route.routePath === route.routePath) { 28 | this.setState({updatedRoute: action.route}); 29 | } 30 | break; 31 | } 32 | }, 33 | 34 | makeButton: function(item, style, callback) { 35 | var styleType; 36 | var text; 37 | 38 | if (item.icon) { 39 | styleType = styles.navBarIcon; 40 | text = item.icon; 41 | } 42 | else { 43 | styleType = styles.navBarText; 44 | text = item.label; 45 | } 46 | 47 | var button = ( 48 | 49 | 50 | {text} 51 | 52 | 53 | ); 54 | 55 | if (item.disabled) { 56 | return button; 57 | } 58 | else { 59 | return ( 60 | 61 | {button} 62 | 63 | ); 64 | } 65 | }, 66 | 67 | renderRight: function() { 68 | var route = this.state.updatedRoute || this.props.route; 69 | if (!route.navRight) return null; 70 | 71 | return this.makeButton(route.navRight, styles.navBarRightButton, function() { 72 | AppActions.launchNavItem(route, route.navRight); 73 | }); 74 | }, 75 | 76 | renderLeft: function() { 77 | var route = this.state.updatedRoute || this.props.route; 78 | if (route.navLeft && !route.navBack) { 79 | return this.makeButton(route.navLeft, styles.navBarLeftButton, function() { 80 | AppActions.launchNavItem(route, route.navLeft); 81 | }); 82 | } 83 | 84 | if (this.props.index === 0) { 85 | return null; 86 | } 87 | 88 | var backLabel = route.navBack || {label: 'back'}; //{icon: 'caret-left-semi'}; 89 | return this.makeButton(backLabel, styles.navBarLeftButton, this.sendBack); 90 | }, 91 | 92 | sendBack: function() { 93 | this.props.navigation.back(); 94 | }, 95 | 96 | render: function() { 97 | switch (this.props.direction) { 98 | case 'left': 99 | return this.renderLeft(); 100 | case 'right': 101 | return this.renderRight(); 102 | default: 103 | throw("Unknown direction: " + this.props.direction); 104 | } 105 | } 106 | }); 107 | 108 | var styles = StyleSheet.create({ 109 | navBarText: { 110 | fontSize: 16, 111 | marginVertical: 10, 112 | }, 113 | navBarIcon: { 114 | fontSize: 20, 115 | marginVertical: 10, 116 | fontFamily: cssVar('fontIcon'), 117 | }, 118 | navBarLeftButton: { 119 | paddingLeft: 10, 120 | }, 121 | navBarRightButton: { 122 | paddingRight: 10, 123 | }, 124 | navBarButtonText: { 125 | color: 'white', 126 | }, 127 | disabledText: { 128 | color: cssVar('gray30') 129 | } 130 | }); 131 | 132 | export default NavigationButton; 133 | -------------------------------------------------------------------------------- /App/Navigation/NavigationHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | NavigationExperimental, 4 | StyleSheet, 5 | } from 'react-native'; 6 | 7 | import cssVar from '../Lib/cssVar'; 8 | 9 | import NavigationTitle from '../Navigation/NavigationTitle'; 10 | import NavigationButton from '../Navigation/NavigationButton'; 11 | 12 | const { 13 | Header: NavigationHeader, 14 | } = NavigationExperimental; 15 | 16 | var _NavigationHeader = React.createClass({ 17 | renderTitle: function (props) { 18 | var route = props.scene.route; 19 | return ; 20 | }, 21 | 22 | renderLeft: function(props) { 23 | var route = props.scene.route; 24 | var index = props.scene.index; 25 | return 26 | }, 27 | 28 | renderRight: function(props) { 29 | var route = props.scene.route; 30 | var index = props.scene.index; 31 | return 32 | }, 33 | 34 | render: function() { 35 | return ( 36 | 43 | ); 44 | } 45 | }); 46 | 47 | var styles = StyleSheet.create({ 48 | appContainer: { 49 | flex: 1 50 | }, 51 | navBar: { 52 | backgroundColor: cssVar('blue50'), 53 | }, 54 | scene: { 55 | flex: 1, 56 | backgroundColor: cssVar('gray5'), 57 | }, 58 | sceneHidden: { 59 | marginTop: 0 60 | }, 61 | header: { 62 | backgroundColor: cssVar('blue50'), 63 | borderBottomWidth: 0, 64 | } 65 | }); 66 | 67 | export default _NavigationHeader; 68 | -------------------------------------------------------------------------------- /App/Navigation/NavigationTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | Dimensions, 6 | Platform, 7 | } from 'react-native'; 8 | 9 | const { width } = Dimensions.get('window'); 10 | 11 | import cssVar from '../Lib/cssVar'; 12 | 13 | import DispatcherListener from '../Mixins/DispatcherListener'; 14 | import AppConstants from '../Constants/AppConstants'; 15 | 16 | import AutoScaleText from '../Components/AutoScaleText'; 17 | 18 | var NavigationTitle = React.createClass({ 19 | mixins: [DispatcherListener], 20 | 21 | getInitialState: function() { 22 | return { updatedTitle: null }; 23 | }, 24 | 25 | dispatchAction: function(action) { 26 | switch(action.actionType) { 27 | case AppConstants.NAVBAR_UPDATE: 28 | if (action.route.routePath == this.props.route.routePath) { 29 | this.setState({updatedTitle: action.route.title}); 30 | } 31 | break; 32 | } 33 | }, 34 | 35 | getButtonPadding() { 36 | const { route } = this.props; 37 | 38 | // for some reason 70 is the magic number for Android 39 | if (Platform.OS === 'android') { 40 | return 70; 41 | } 42 | // if navRight is a text return 70 43 | if (route.navRight && route.navRight.label) { 44 | return 70; 45 | } 46 | // if navLeft is a text return 70 47 | if (route.navLeft && route.navLeft.label) { 48 | return 70; 49 | } 50 | 51 | if (route.navBack && route.navBack.label) { 52 | return 70; 53 | } 54 | 55 | return 40; 56 | }, 57 | 58 | render: function() { 59 | var title = this.state.updatedTitle || this.props.route.title; 60 | return ( 61 | 62 | 68 | {title} 69 | 70 | 71 | ); 72 | } 73 | }); 74 | 75 | const marginVertical = Platform.OS === 'ios' ? 8 : 15; 76 | var styles = StyleSheet.create({ 77 | title: { 78 | flex: 1, 79 | flexDirection: 'row', 80 | alignItems: 'center', 81 | marginHorizontal: 16 82 | }, 83 | 84 | titleText: { 85 | color: 'white', 86 | fontFamily: cssVar('fontRegular'), 87 | flex: 1, 88 | fontSize: 18, 89 | fontWeight: '500', 90 | textAlign: 'center', 91 | //textAlign: Platform.OS === 'ios' ? 'center' : 'left', 92 | paddingBottom: 6, 93 | } 94 | }); 95 | 96 | export default NavigationTitle; 97 | -------------------------------------------------------------------------------- /App/Navigation/Navigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | NavigationExperimental, 4 | } from 'react-native'; 5 | 6 | import AppActions from '../Actions/AppActions'; 7 | 8 | const { 9 | StateUtils: NavigationStateUtils, 10 | } = NavigationExperimental; 11 | 12 | var Navigator = function() { 13 | this.stack = null; 14 | }; 15 | 16 | Navigator.prototype.setStack = function(stack) { 17 | this.stack = stack; 18 | }; 19 | 20 | Navigator.prototype.back = function() { 21 | if (!this.stack) return false; 22 | 23 | var state = this.stack.props.navigationState; 24 | var index = state.index; 25 | if (index === 0) return false; 26 | 27 | var updated = NavigationStateUtils.pop(state); 28 | var routes = updated.routes; 29 | if(routes.length === 0) return false; 30 | 31 | var previous = routes[routes.length-1]; 32 | AppActions.launchRoutePath(previous.routePath); 33 | }; 34 | 35 | export default Navigator; 36 | -------------------------------------------------------------------------------- /App/Navigation/Router.js: -------------------------------------------------------------------------------- 1 | // parseUri 1.2.2 2 | // (c) Steven Levithan 3 | // MIT License 4 | 5 | import Routes from '../Navigation/Routes'; 6 | import assign from 'object-assign'; 7 | 8 | function parseUri (str) { 9 | var o = parseUri.options, 10 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), 11 | uri = {}, 12 | i = 14; 13 | 14 | while (i--) uri[o.key[i]] = m[i] || ""; 15 | 16 | uri[o.q.name] = {}; 17 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { 18 | if ($1) uri[o.q.name][$1] = $2; 19 | }); 20 | 21 | return uri; 22 | }; 23 | 24 | parseUri.options = { 25 | strictMode: false, 26 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 27 | q: { 28 | name: "queryKey", 29 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 30 | }, 31 | parser: { 32 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 33 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 34 | } 35 | }; 36 | 37 | // TODO: can we use https://github.com/visionmedia/page.js 38 | var Router = { 39 | 40 | parse: function(str, parent, defaulted) { 41 | if (!str) { 42 | str = ""; 43 | } 44 | 45 | var toParse = str.toLowerCase(); 46 | if (toParse.indexOf("://") < 0) { 47 | toParse = "sample://" + str; 48 | } 49 | var parsed = parseUri(toParse); 50 | var pieces = parsed.path.split("/"); 51 | pieces.unshift(parsed.host); 52 | pieces = pieces.filter(function(v) { return v && v.length > 0 }); 53 | var current = parent; 54 | var stack = []; 55 | 56 | for(var i=0; i < pieces.length; i++) { 57 | var piece = pieces[i]; 58 | if (piece.length > 0) { 59 | if (!current.parse) { 60 | if(defaulted) { 61 | break; // as deep as we can 62 | } 63 | else { 64 | return null; 65 | } 66 | } 67 | var route = current.parse(piece); 68 | if (route) { 69 | if (current.routePath) { 70 | route.routePath = current.routePath + "/" + piece; 71 | } 72 | else { 73 | route.routePath = piece; 74 | } 75 | 76 | if (route._routerReplace) { 77 | stack[stack.length-1] = route; 78 | } 79 | else if (route._notAddressable) { 80 | if (route._routerAppend && i === (pieces.length-1)) { 81 | // add soemthing to route on the last one (kind of like a forward) 82 | var child = route.parse(route._routerAppend); 83 | child.routePath = route.routePath + "/" + route._routerAppend; 84 | route = child; 85 | stack.push(route); 86 | } 87 | } 88 | else { 89 | if (route._routerAppend && i === (pieces.length-1)) { 90 | // add soemthing to route on the last one (kind of like a forward) 91 | route.routePath = route.routePath + "/" + route._routerAppend; 92 | } 93 | stack.push(route); 94 | } 95 | current = route; // recursive 96 | } 97 | else if(defaulted) { 98 | break; // as deep as we can 99 | } 100 | else { 101 | return null; 102 | } 103 | } 104 | } 105 | 106 | if (stack.length === 0) { 107 | return null; 108 | } 109 | 110 | var found = {}; 111 | // TODO: add query parameters to last item on stack 112 | found.index = stack.length - 1; 113 | for(var j=0; j < stack.length; j++) { 114 | stack[j].key = stack[j].routePath; 115 | } 116 | found.routes = stack; 117 | return found; 118 | } 119 | }; 120 | 121 | export default Router; 122 | -------------------------------------------------------------------------------- /App/Navigation/Routes.js: -------------------------------------------------------------------------------- 1 | import Router from '../Navigation/Router'; 2 | 3 | import Locale from '../Locale'; 4 | 5 | var i18n = Locale.key('Routes', { 6 | login: 'Log in', 7 | signup: 'Sign Up', 8 | settings: 'Settings', 9 | new_post: 'New Post', 10 | cancel: 'Cancel', 11 | account: 'Me' 12 | }); 13 | 14 | var Routes = { 15 | 16 | LogIn: function() { 17 | return { 18 | component: require('../Screens/LogIn').default, 19 | title: i18n.t('login') 20 | }; 21 | }, 22 | 23 | SignUp: function() { 24 | return { 25 | component: require('../Screens/SignUp').default, 26 | title: i18n.t('signup') 27 | }; 28 | }, 29 | 30 | PostList: function(username) { 31 | return { 32 | component: require('../Screens/PostList').default, 33 | title: '', // set to name 34 | passProps: { 35 | username: username 36 | } 37 | }; 38 | }, 39 | 40 | FollowList: function(username) { 41 | return { 42 | component: require('../Screens/FollowList').default, 43 | title: '', // set to name 44 | passProps: { 45 | username: username 46 | } 47 | }; 48 | }, 49 | 50 | Settings: function() { 51 | return { 52 | component: require('../Screens/Settings').default, 53 | title: i18n.t('settings') 54 | }; 55 | }, 56 | 57 | CreatePost: function() { 58 | return { 59 | component: require('../Screens/CreatePost').default, 60 | title: i18n.t('new_post'), 61 | navBack: { 62 | label: i18n.t('cancel') 63 | } 64 | }; 65 | }, 66 | }; 67 | 68 | 69 | var listRoute = function(route, defaultRoute) { 70 | var username = route.passProps ? route.passProps.username : null; 71 | route.parse = function(path) { 72 | switch(path) { 73 | case '_post': 74 | return Routes.CreatePost(); 75 | case '_settings': 76 | // only on 'Dashboard' 77 | if(username) return null; 78 | return Routes.Settings(); 79 | default: 80 | if (!defaultRoute) return null; 81 | return defaultRoute(path); 82 | } 83 | } 84 | 85 | if(!route.navRight) { 86 | route.navRight = { 87 | subPath: '_post', 88 | label: '+' // TODO: icon font 89 | }; 90 | } 91 | 92 | if(!route.navLeft && !username) { 93 | route.navLeft = { 94 | subPath: '_settings', 95 | label: i18n.t('account') // TODO: icon font 96 | }; 97 | } 98 | return route; 99 | }; 100 | 101 | var userRoute = function(username) { 102 | var route = {} 103 | route._notAddressable = true; 104 | route._routerAppend = 'posts'; 105 | 106 | route.parse = function(path) { 107 | switch(path) { 108 | case 'posts': 109 | return listRoute(Routes.PostList(username), function(postId) { 110 | // TOOD: show post 111 | return null; 112 | }); 113 | case 'follows': 114 | return listRoute(Routes.FollowList(username), function(follow) { 115 | // it's a user 116 | return userRoute(follow); 117 | }); 118 | default: 119 | return null; 120 | }; 121 | }; 122 | return route; 123 | }; 124 | 125 | var LoggedIn = { 126 | parse: function(host) { 127 | switch (host) { 128 | case 'dashboard': 129 | return userRoute(null); 130 | default: 131 | return null; 132 | } 133 | } 134 | }; 135 | 136 | var LoggedOut = { 137 | parse: function(host) { 138 | switch (host) { 139 | case 'signup': 140 | return Routes.SignUp(); 141 | case 'login': 142 | return Routes.LogIn(); 143 | default: 144 | return null; 145 | } 146 | } 147 | }; 148 | 149 | export default { 150 | parse: function(str, loggedIn, defaulted) { 151 | var parent = loggedIn ? LoggedIn : LoggedOut; 152 | var found = Router.parse(str, parent, defaulted); 153 | if (!found && defaulted) { 154 | if (loggedIn) { 155 | found = this.parse('dashboard', true, false); 156 | } 157 | else { 158 | found = this.parse('signup', false, false); 159 | } 160 | } 161 | return found; 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /App/Platform/Back.android.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | BackAndroid, 5 | } from 'react-native'; 6 | 7 | import AppActions from '../Actions/AppActions'; 8 | // import DrawerStore from '../Stores/DrawerStore'; 9 | 10 | const Back = { 11 | setNavigator(navigator) { 12 | BackAndroid.addEventListener('hardwareBackPress', () => { 13 | // if (DrawerStore.get().open === true) { 14 | // AppActions.toggleDrawer(); 15 | // return true; 16 | // } 17 | if (navigator && navigator.back()) { 18 | return true; 19 | } 20 | return false; 21 | }); 22 | }, 23 | }; 24 | 25 | export default Back; 26 | -------------------------------------------------------------------------------- /App/Platform/Back.ios.js: -------------------------------------------------------------------------------- 1 | const Back = { 2 | setNavigator(navigator) { 3 | }, 4 | }; 5 | 6 | export default Back; 7 | -------------------------------------------------------------------------------- /App/Platform/Keychain.android.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import LocalKeyStore from '../Stores/LocalKeyStore'; 4 | 5 | const KEYCHAIN_STORAGE_KEY = 'KEYCHAIN'; 6 | 7 | var KeyChain = { 8 | setGenericPassword(username, password, service) { 9 | return new Promise((resolve) => { 10 | LocalKeyStore.setKey(this.getStorageKey(service), {username, password}, (error) => { 11 | resolve(); 12 | }); 13 | }); 14 | }, 15 | 16 | getGenericPassword(service) { 17 | return new Promise((resolve) => { 18 | LocalKeyStore.getKey(this.getStorageKey(service), (error, value) => { 19 | resolve(value); 20 | }); 21 | }); 22 | }, 23 | 24 | resetGenericPassword(service) { 25 | return new Promise((resolve) => { 26 | LocalKeyStore.setKey(this.getStorageKey(service), null, (error) => { 27 | resolve(); 28 | }); 29 | }); 30 | }, 31 | 32 | getStorageKey(service) { 33 | return `${KEYCHAIN_STORAGE_KEY}-${service}`; 34 | }, 35 | 36 | }; 37 | 38 | export default KeyChain; 39 | -------------------------------------------------------------------------------- /App/Platform/Keychain.ios.js: -------------------------------------------------------------------------------- 1 | import * as KeyChain from 'react-native-keychain'; 2 | 3 | export default KeyChain; 4 | -------------------------------------------------------------------------------- /App/Platform/RefreshableListView.android.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | ListView, 5 | RefreshControl, 6 | } from 'react-native'; 7 | 8 | import isPromise from 'is-promise'; 9 | 10 | function delay(time) { 11 | return new Promise((resolve) => setTimeout(resolve, time)); 12 | } 13 | 14 | class RefreshableListView extends React.Component { 15 | 16 | static propTypes = { 17 | loadData: React.PropTypes.func.isRequired, 18 | minDisplayTime: React.PropTypes.number, 19 | minBetweenTime: React.PropTypes.number, 20 | androidRefreshable: React.PropTypes.bool, 21 | }; 22 | 23 | static defaultProps = { 24 | loadData: (() => {}), 25 | minDisplayTime: 300, 26 | minBetweenTime: 300, 27 | androidRefreshable: true, 28 | }; 29 | 30 | constructor(props) { 31 | super(props); 32 | 33 | this.state = {}; 34 | } 35 | 36 | handleRefresh = () => { 37 | if (this.willRefresh) return; 38 | 39 | this.willRefresh = true; 40 | 41 | var loadingDataPromise = new Promise((resolve) => { 42 | var loadDataReturnValue = this.props.loadData(resolve); 43 | 44 | if (isPromise(loadDataReturnValue)) { 45 | loadingDataPromise = loadDataReturnValue; 46 | } 47 | 48 | Promise.all([ 49 | loadingDataPromise, 50 | new Promise((resolve) => this.setState({isRefreshing: true}, resolve)), 51 | delay(this.props.minDisplayTime), 52 | ]) 53 | .then(() => delay(this.props.minBetweenTime)) 54 | .then(() => { 55 | this.willRefresh = false; 56 | this.setState({isRefreshing: false}); 57 | }); 58 | }); 59 | }; 60 | 61 | render() { 62 | const listViewProps = { 63 | dataSource: this.props.dataSource, 64 | renderRow: this.props.renderRow, 65 | renderFooter: this.props.renderFooter, 66 | style: [this.props.style, {flex: 1}], 67 | renderScrollComponent: this.props.renderScrollComponent, 68 | renderHeader: this.props.renderHeaderWrapper, 69 | }; 70 | 71 | const pullToRefreshProps = { 72 | style: [this.props.pullToRefreshStyle, {flex: 1}], 73 | refreshing: this.state.isRefreshing || false, 74 | onRefresh: this.handleRefresh, 75 | enabled: this.props.androidRefreshable, 76 | }; 77 | 78 | return ( 79 | 80 | 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default RefreshableListView; 87 | -------------------------------------------------------------------------------- /App/Platform/RefreshableListView.ios.js: -------------------------------------------------------------------------------- 1 | import RefreshableListView from '../Vendor/react-native-refreshable-listview'; 2 | 3 | export default RefreshableListView; 4 | -------------------------------------------------------------------------------- /App/Platform/StatusBar.android.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var StatusBar = { 4 | setNetworkActive(active) { 5 | }, 6 | setHidden(hidden) { 7 | }, 8 | setStyle(style) { 9 | }, 10 | }; 11 | 12 | export default StatusBar; 13 | -------------------------------------------------------------------------------- /App/Platform/StatusBar.ios.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StatusBar 4 | } from 'react-native'; 5 | 6 | var _StatusBar = { 7 | setNetworkActive: function(active) { 8 | StatusBar.setNetworkActivityIndicatorVisible(active); 9 | }, 10 | 11 | setHidden: function(hidden) { 12 | StatusBar.setHidden(hidden); 13 | } 14 | }; 15 | 16 | export default _StatusBar; 17 | -------------------------------------------------------------------------------- /App/Root/Launch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Text, 5 | View, 6 | StyleSheet 7 | } from 'react-native'; 8 | 9 | 10 | var Launch = React.createClass({ 11 | render: function() { 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | }); 19 | 20 | var styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | backgroundColor: '#F5FCFF', 24 | } 25 | }); 26 | 27 | export default Launch; 28 | -------------------------------------------------------------------------------- /App/Root/Launcher.js: -------------------------------------------------------------------------------- 1 | import AppConstants from '../Constants/AppConstants'; 2 | import Routes from '../Navigation/Routes'; 3 | 4 | import StatusBar from '../Platform/StatusBar'; 5 | 6 | import { 7 | Linking 8 | } from 'react-native'; 9 | 10 | var Launcher = { 11 | launch: function(root, action) { 12 | switch(action.actionType) { 13 | case AppConstants.LAUNCH_ROUTE_PATH: 14 | var routePath = action.routePath; 15 | var loggedIn = root.state && root.state.user.isLoggedIn(); 16 | var parsed = Routes.parse(routePath, loggedIn, false); 17 | if (!parsed) { 18 | alert("Unknown route: " + routePath); 19 | } 20 | else { 21 | root.setState({navigationState: parsed}); 22 | } 23 | break; 24 | case AppConstants.NETWORK_ACTIVITY: 25 | StatusBar.setNetworkActive(action.isActive); 26 | break; 27 | case AppConstants.OPEN_URL: 28 | var url = action.url; 29 | Linking.openURL(url) 30 | break; 31 | case AppConstants.RELOAD_PATH: 32 | // TODO 33 | //root.setState({loading: true}, function() { 34 | // root.setState({loading: false}); 35 | //}); 36 | break; 37 | case AppConstants.TEST_COMPONENT_ROUTE: 38 | var TestComponents = require("../Root/TestComponents").default; 39 | action.routeUnderTest.component = TestComponents.find(action.routeUnderTest.component); 40 | rootComponent.setState({routeUnderTest: action.routeUnderTest}); 41 | break; 42 | default: 43 | break; 44 | } 45 | } 46 | }; 47 | 48 | export default Launcher; 49 | -------------------------------------------------------------------------------- /App/Root/LoggedIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavigationBar from '../Navigation/NavigationBar'; 3 | 4 | var LoggedIn = React.createClass({ 5 | mixins: [NavigationBar], 6 | 7 | getDefaultProps: function() { 8 | return {}; 9 | }, 10 | }); 11 | 12 | export default LoggedIn; 13 | -------------------------------------------------------------------------------- /App/Root/LoggedOut.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import NavigationBar from '../Navigation/NavigationBar'; 4 | 5 | var LoggedOut = React.createClass({ 6 | mixins: [NavigationBar], 7 | 8 | getDefaultProps: function() { 9 | return {navBarHidden: true}; 10 | }, 11 | }); 12 | 13 | export default LoggedOut; 14 | -------------------------------------------------------------------------------- /App/Root/TestComponents.js: -------------------------------------------------------------------------------- 1 | var components = { 2 | 'Components/SimpleListItem': require("../Components/SimpleListItem").default 3 | }; 4 | 5 | var TestComponents = { 6 | find(name) { 7 | return components[name]; 8 | } 9 | }; 10 | 11 | export default TestComponents; 12 | -------------------------------------------------------------------------------- /App/Root/TestRunner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Text, 5 | View, 6 | StyleSheet, 7 | TouchableOpacity, 8 | NativeModules, 9 | } from 'react-native'; 10 | 11 | const { 12 | TestRunnerManager, 13 | DevMenu, 14 | } = NativeModules; 15 | 16 | import client from '../Api/HTTPClient'; 17 | import Dispatcher from '../Dispatcher'; 18 | 19 | import StatusBar from '../Platform/StatusBar'; 20 | 21 | var TestRunner = React.createClass({ 22 | componentDidMount: function() { 23 | StatusBar.setHidden(true); 24 | }, 25 | 26 | resetApp: function() { 27 | TestRunnerManager.reset(function() { 28 | console.log("reset") 29 | }); 30 | }, 31 | 32 | showDevMenu: function() { 33 | DevMenu.show(); 34 | }, 35 | 36 | hookConsole: function() { 37 | global.console = { 38 | log: function(...args) { 39 | client.post("test/console.json", {level: 'log', arguments: args}); 40 | }, 41 | error: function(...args) { 42 | client.post("test/console.json", {level: 'error', arguments: args}); 43 | }, 44 | warn: function(...args) { 45 | client.post("test/console.json", {level: 'warn', arguments: args}); 46 | } 47 | } 48 | }, 49 | 50 | bootstrapApp: function() { 51 | client.get("test/bootstrap.json", null, 52 | (error, response) => { 53 | if (error) { 54 | console.log("BOOTSTRAP ERROR"); 55 | alert('"BOOTSTRAP ERROR"'); 56 | } 57 | else { 58 | this.hookConsole(); 59 | if (response.actions) { 60 | for (var i=0; i 87 | ); 88 | }, 89 | 90 | render: function() { 91 | var containerStyle = (this.props.routeUnderTest ? styles.withComponent : styles.noComponent); 92 | return ( 93 | 94 | {this.renderRouteUnderTest()} 95 | 96 | 97 | 98 | ResetTest 99 | 100 | 101 | 102 | 103 | Bootstrap 104 | 105 | 106 | 107 | 108 | DevMenu 109 | 110 | 111 | 112 | 113 | ) 114 | } 115 | }); 116 | 117 | var styles = StyleSheet.create({ 118 | withComponent: { 119 | flex: 1 120 | }, 121 | noComponent: { 122 | height: 10 123 | }, 124 | bar: { 125 | height: 10, 126 | backgroundColor: 'red', 127 | justifyContent: 'center', 128 | flexDirection: 'row' 129 | }, 130 | action: { 131 | marginHorizontal: 4, 132 | fontSize: 10 133 | } 134 | }); 135 | 136 | export default TestRunner; 137 | -------------------------------------------------------------------------------- /App/Screens/CreatePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet 5 | } from 'react-native'; 6 | 7 | import Locale from '../Locale'; 8 | 9 | import TextInput from '../Components/TextInput'; 10 | import Button from '../Components/Button'; 11 | import PostActions from '../Actions/PostActions'; 12 | 13 | import KeyboardListener from '../Mixins/KeyboardListener'; 14 | 15 | import AddSpinnerLoader from '../Extensions/AddSpinnerLoader'; 16 | 17 | var CreatePost = React.createClass({ 18 | mixins: [KeyboardListener], 19 | 20 | getInitialState: function() { 21 | return { 22 | content: '' 23 | }; 24 | }, 25 | 26 | onSubmitButton: function() { 27 | this.props['AddSpinnerLoader'].start(); 28 | 29 | PostActions.createPost(this.state.content, function(error) { 30 | this.props['AddSpinnerLoader'].stop(); 31 | if (error) { 32 | // TODO: better error handling 33 | alert(error.message); 34 | } 35 | else { 36 | this.props.navigation.back(); 37 | } 38 | }.bind(this)); 39 | }, 40 | 41 | render: function() { 42 | return ( 43 | 44 | this.state.content = event.nativeEvent.text } 53 | /> 54 | 55 | 56 | 59 | 60 | 61 | 62 | ); 63 | } 64 | }); 65 | 66 | var i18n = Locale.key('CreatePost', { 67 | placeholder: 'What do you have to say for yourself?', 68 | submit: 'Submit', 69 | }); 70 | 71 | var styles = StyleSheet.create({ 72 | flex: { 73 | flex: 1 74 | }, 75 | input: { 76 | flex: 1, 77 | fontSize: 16, 78 | backgroundColor: 'white', 79 | padding: 20, 80 | textAlignVertical: 'top' 81 | }, 82 | button: { 83 | // width: 150 84 | }, 85 | footer: { 86 | padding: 10, 87 | flexDirection: 'row' 88 | } 89 | }); 90 | 91 | CreatePost = AddSpinnerLoader(CreatePost); 92 | export default CreatePost; 93 | -------------------------------------------------------------------------------- /App/Screens/FollowList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Locale from '../Locale'; 4 | 5 | import ListHelper from '../Mixins/ListHelper'; 6 | 7 | import FollowListStore from '../Stores/FollowListStore'; 8 | import FollowActions from '../Actions/FollowActions'; 9 | 10 | var i18n = Locale.key('FollowList', { 11 | posts: 'Posts', 12 | follows: 'Follows', 13 | }); 14 | 15 | var FollowList = React.createClass({ 16 | mixins: [ListHelper], 17 | 18 | getDefaultProps: function() { 19 | return { 20 | store: FollowListStore, 21 | listProps: { 22 | nextIcon: true 23 | }, 24 | segment: { 25 | items: [ 26 | { 27 | title: i18n.t('posts'), 28 | replacePath: 'posts' 29 | }, 30 | { 31 | title: i18n.t('follows'), 32 | replacePath: 'follows', 33 | selected: true 34 | } 35 | ] 36 | } 37 | }; 38 | }, 39 | 40 | getListItems: function() { 41 | return FollowListStore.get(this.getUsername()); 42 | }, 43 | 44 | isListChange: function(username) { 45 | return this.getUsername() == username; 46 | }, 47 | 48 | getItemProps: function(follow) { 49 | return { 50 | key: follow.data.id, 51 | title: follow.data.username, 52 | subPath: follow.data.username 53 | } 54 | }, 55 | 56 | reloadList: function() { 57 | console.log("reloading follows: " + this.getUsername()); 58 | FollowActions.fetchList(this.getUsername(), function(error) { 59 | // TODO: handle error 60 | if (error) { 61 | console.log(error.message); 62 | } 63 | }); 64 | } 65 | }); 66 | 67 | export default FollowList; 68 | -------------------------------------------------------------------------------- /App/Screens/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet 5 | } from 'react-native'; 6 | 7 | import Text from '../Components/Text'; 8 | import Button from '../Components/Button'; 9 | import AppActions from '../Actions/AppActions'; 10 | 11 | var Loading = React.createClass({ 12 | onRetryButton: function() { 13 | AppActions.reloadCurrentPath(); 14 | }, 15 | 16 | renderError: function() { 17 | return ( 18 | 19 | 20 | {this.props.error.message} 21 | 22 | 25 | 26 | ); 27 | }, 28 | 29 | renderLoading: function() { 30 | return ( 31 | 32 | 33 | Loading 34 | 35 | 36 | ); 37 | }, 38 | 39 | render: function() { 40 | if (this.props.error) { 41 | return this.renderError(); 42 | } 43 | else { 44 | return this.renderLoading(); 45 | } 46 | } 47 | }); 48 | 49 | var styles = StyleSheet.create({ 50 | container: { 51 | flex: 1, 52 | justifyContent: 'center', 53 | alignItems: 'center' 54 | }, 55 | welcome: { 56 | fontSize: 20, 57 | textAlign: 'center', 58 | margin: 10, 59 | } 60 | }); 61 | 62 | export default Loading; 63 | -------------------------------------------------------------------------------- /App/Screens/LogIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AuthHelper from '../Mixins/AuthHelper'; 4 | import AuthActions from '../Actions/AuthActions'; 5 | 6 | var LogIn = React.createClass({ 7 | mixins: [AuthHelper], 8 | 9 | getDefaultProps: function() { 10 | return { 11 | authType: 'login' 12 | }; 13 | }, 14 | 15 | onAuthButton: function() { 16 | var username = this.state.username; 17 | var password = this.state.password; 18 | AuthActions.submitLogin(username, password, function(error) { 19 | if (error) { 20 | // TODO: better errors 21 | alert(error.message); 22 | } 23 | }); 24 | // TODO: setState to denote busy 25 | }, 26 | }); 27 | 28 | export default LogIn; 29 | -------------------------------------------------------------------------------- /App/Screens/PostList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Locale from '../Locale'; 4 | 5 | import ListHelper from '../Mixins/ListHelper'; 6 | 7 | import PostListStore from '../Stores/PostListStore'; 8 | import PostActions from '../Actions/PostActions'; 9 | 10 | var i18n = Locale.key('PostList', { 11 | posts: 'Posts', 12 | follows: 'Follows', 13 | }); 14 | 15 | var PostList = React.createClass({ 16 | mixins: [ListHelper], 17 | 18 | getDefaultProps: function() { 19 | return { 20 | store: PostListStore, 21 | listProps: { 22 | noTap: true 23 | }, 24 | segment: { 25 | items: [ 26 | { 27 | title: i18n.t('posts'), 28 | replacePath: 'posts', 29 | selected: true 30 | }, 31 | { 32 | title: i18n.t('follows'), 33 | replacePath: 'follows' 34 | } 35 | ] 36 | } 37 | }; 38 | }, 39 | 40 | getListItems: function() { 41 | return PostListStore.get(this.getUsername()); 42 | }, 43 | 44 | isListChange: function(username) { 45 | return this.getUsername() == username; 46 | }, 47 | 48 | getItemProps: function(post) { 49 | return { 50 | key: post.data.id, 51 | title: post.data.content 52 | } 53 | }, 54 | 55 | reloadList: function() { 56 | console.log("reloading posts: " + this.getUsername()); 57 | PostActions.fetchList(this.getUsername(), function(error) { 58 | // TODO: handle error 59 | if (error) { 60 | console.log(error.message); 61 | } 62 | }); 63 | } 64 | }); 65 | 66 | 67 | 68 | export default PostList; 69 | -------------------------------------------------------------------------------- /App/Screens/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | Text 6 | } from 'react-native'; 7 | 8 | import cssVar from '../Lib/cssVar'; 9 | import Locale from '../Locale'; 10 | 11 | import SimpleList from '../Components/SimpleList'; 12 | import AppConstants from '../Constants/AppConstants'; 13 | 14 | import EnvironmentStore from '../Stores/EnvironmentStore'; 15 | 16 | var i18n = Locale.key('Settings', { 17 | logout: 'Log out', 18 | }); 19 | 20 | function getListState() { 21 | var list = []; 22 | list.push({ 23 | title: i18n.t('logout'), 24 | actionType: AppConstants.LOGOUT_REQUESTED 25 | }); 26 | 27 | return { 28 | items: list 29 | }; 30 | }; 31 | 32 | var Settings = React.createClass({ 33 | getInitialState() { 34 | return getListState(); 35 | }, 36 | 37 | render: function() { 38 | return ( 39 | 40 | 41 | 42 | Version {EnvironmentStore.get().displayVersion()} 43 | 44 | 45 | ) 46 | } 47 | }); 48 | 49 | var styles = StyleSheet.create({ 50 | container: { 51 | flex: 1 52 | }, 53 | bottomText: { 54 | padding: 10, 55 | color: cssVar('gray20'), 56 | fontSize: 12 57 | }, 58 | }); 59 | 60 | export default Settings; 61 | -------------------------------------------------------------------------------- /App/Screens/SignUp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AuthHelper from '../Mixins/AuthHelper'; 4 | import AuthActions from '../Actions/AuthActions'; 5 | 6 | var SignUp = React.createClass({ 7 | mixins: [AuthHelper], 8 | 9 | getDefaultProps: function() { 10 | return { 11 | authType: 'signup' 12 | }; 13 | }, 14 | 15 | onAuthButton: function() { 16 | var username = this.state.username; 17 | var password = this.state.password; 18 | AuthActions.submitSignup(username, password, function(error) { 19 | if (error) { 20 | // TODO: better errors 21 | alert(error.message); 22 | } 23 | }); 24 | // TODO: setState to denote busy 25 | }, 26 | }); 27 | 28 | export default SignUp; 29 | -------------------------------------------------------------------------------- /App/Stores/CurrentUserStore.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import assign from 'object-assign'; 3 | import Keychain from '../Platform/Keychain'; 4 | 5 | import CurrentUser from '../Models/CurrentUser'; 6 | import LocalKeyStore from '../Stores/LocalKeyStore'; 7 | import Dispatcher from '../Dispatcher'; 8 | import AppConstants from '../Constants/AppConstants'; 9 | 10 | var CHANGE_EVENT = 'change'; 11 | var LOCAL_STORE_KEY = 'CurrentUser'; 12 | var KEYCHAIN_SERVICE = 'Sample'; 13 | 14 | // null so we know to initialize on app launch 15 | var _singleton = null; 16 | 17 | function initSingleton(storedProps, token, username) { 18 | if (storedProps && token && username) { 19 | if (storedProps.id && storedProps.id.toString() === username.toString()) { 20 | // matches 21 | setUserProps(storedProps, token); 22 | return; 23 | } 24 | } 25 | setUserProps(null, null); 26 | } 27 | 28 | function setUserProps(storedProps, token) { 29 | _singleton = new CurrentUser(storedProps, token); 30 | } 31 | 32 | function saveSingleton() { 33 | if(_singleton) { 34 | LocalKeyStore.setKey(LOCAL_STORE_KEY, _singleton.data); 35 | if (_singleton.data.id && _singleton.getToken()) { 36 | Keychain.setGenericPassword(_singleton.data.id.toString(), _singleton.getToken(), KEYCHAIN_SERVICE); 37 | } 38 | } 39 | } 40 | 41 | function clearData() { 42 | _singleton = new CurrentUser(null, null); 43 | LocalKeyStore.setKey(LOCAL_STORE_KEY, null); 44 | Keychain.resetGenericPassword(KEYCHAIN_SERVICE); 45 | } 46 | 47 | var SingletonStore = assign({}, EventEmitter.prototype, { 48 | get: function() { 49 | return _singleton; 50 | }, 51 | 52 | emitChange: function() { 53 | this.emit(CHANGE_EVENT); 54 | }, 55 | 56 | addChangeListener: function(callback) { 57 | this.on(CHANGE_EVENT, callback); 58 | }, 59 | 60 | removeChangeListener: function(callback) { 61 | this.removeListener(CHANGE_EVENT, callback); 62 | } 63 | }); 64 | 65 | // Register callback to handle all updates 66 | Dispatcher.register(function(action) { 67 | switch(action.actionType) { 68 | case AppConstants.APP_LAUNCHED: 69 | Keychain.getGenericPassword(KEYCHAIN_SERVICE).then((values) => { 70 | const { username, password } = values || {}; 71 | // password is the stored token. 72 | LocalKeyStore.getKey(LOCAL_STORE_KEY, (storeError, props) => { 73 | initSingleton(props, password, username); 74 | SingletonStore.emitChange(); 75 | }); 76 | }).catch((error) => { 77 | console.log(error); 78 | initSingleton({}, null, null); 79 | SingletonStore.emitChange(); 80 | }); 81 | break; 82 | case AppConstants.LOGIN_USER: 83 | case AppConstants.USER_UPDATED: 84 | setUserProps(action.userProps, action.token); 85 | SingletonStore.emitChange(); 86 | saveSingleton(); 87 | break; 88 | case AppConstants.LOGOUT_REQUESTED: 89 | clearData(); 90 | SingletonStore.emitChange(); 91 | break; 92 | default: 93 | // no op 94 | } 95 | }); 96 | 97 | export default SingletonStore; 98 | -------------------------------------------------------------------------------- /App/Stores/DebugStore.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NativeModules } from 'react-native'; 3 | import { EventEmitter } from 'events'; 4 | 5 | import Dispatcher from '../Dispatcher'; 6 | import AppConstants from '../Constants/AppConstants'; 7 | import LocalKeyStore from '../Stores/LocalKeyStore'; 8 | 9 | const EnvironmentManager = NativeModules.EnvironmentManager; 10 | 11 | const CHANGE_EVENT = 'change'; 12 | const DEBUG_CURRENT_ROUTE_PATH_KEY = AppConstants.DEBUG_CURRENT_ROUTE_PATH_KEY; 13 | 14 | var _values = null; 15 | 16 | function setCurrentRoutePath(routePath) { 17 | _values = _values || {}; 18 | _values.currentRoutePath = routePath; 19 | } 20 | 21 | function resetData() { 22 | _values = {}; 23 | } 24 | 25 | var SingletonStore = Object.assign({}, EventEmitter.prototype, { 26 | get() { 27 | return _values; 28 | }, 29 | 30 | emitChange() { 31 | this.emit(CHANGE_EVENT); 32 | }, 33 | 34 | addChangeListener(callback) { 35 | this.on(CHANGE_EVENT, callback); 36 | }, 37 | 38 | removeChangeListener(callback) { 39 | this.removeListener(CHANGE_EVENT, callback); 40 | } 41 | }); 42 | 43 | Dispatcher.register(function(action) { 44 | switch (action.actionType) { 45 | case AppConstants.APP_LAUNCHED: 46 | LocalKeyStore.getKey(DEBUG_CURRENT_ROUTE_PATH_KEY, (error, routePath) => { 47 | setCurrentRoutePath(routePath); 48 | SingletonStore.emitChange(); 49 | }); 50 | break; 51 | case AppConstants.LAUNCH_ROUTE_PATH: 52 | if (action.routePath) { 53 | LocalKeyStore.setKey(DEBUG_CURRENT_ROUTE_PATH_KEY, action.routePath); 54 | setCurrentRoutePath(action.routePath); 55 | } 56 | break; 57 | case AppConstants.LOGOUT_REQUESTED: 58 | resetData(); 59 | LocalKeyStore.setKey(DEBUG_CURRENT_ROUTE_PATH_KEY, ''); 60 | SingletonStore.emitChange(); 61 | break; 62 | default: 63 | // no op 64 | } 65 | }); 66 | 67 | export default SingletonStore; 68 | -------------------------------------------------------------------------------- /App/Stores/EnvironmentStore.js: -------------------------------------------------------------------------------- 1 | import { NativeModules} from 'react-native'; 2 | const EnvironmentManager = NativeModules.EnvironmentManager; 3 | 4 | import {EventEmitter} from 'events'; 5 | import assign from 'object-assign'; 6 | 7 | import Environment from '../Models/Environment'; 8 | import Dispatcher from '../Dispatcher'; 9 | import AppConstants from '../Constants/AppConstants'; 10 | 11 | var CHANGE_EVENT = 'change'; 12 | 13 | // null so we know to initialize on app launch 14 | var _singleton = null; 15 | 16 | function setSingleton(storedProps) { 17 | _singleton = new Environment(storedProps); 18 | } 19 | 20 | var SingletonStore = assign({}, EventEmitter.prototype, { 21 | get: function() { 22 | return _singleton; 23 | }, 24 | 25 | emitChange: function() { 26 | this.emit(CHANGE_EVENT); 27 | }, 28 | 29 | addChangeListener: function(callback) { 30 | this.on(CHANGE_EVENT, callback); 31 | }, 32 | 33 | removeChangeListener: function(callback) { 34 | this.removeListener(CHANGE_EVENT, callback); 35 | } 36 | }); 37 | 38 | // Register callback to handle all updates 39 | Dispatcher.register(function(action) { 40 | switch(action.actionType) { 41 | case AppConstants.APP_LAUNCHED: 42 | EnvironmentManager.get(function(attributes) { 43 | console.log("Environment: " + attributes.name); 44 | setSingleton(attributes); 45 | SingletonStore.emitChange(); 46 | }); 47 | break; 48 | default: 49 | // no op 50 | } 51 | }); 52 | 53 | export default SingletonStore; 54 | -------------------------------------------------------------------------------- /App/Stores/FollowListStore.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import assign from 'object-assign'; 3 | 4 | import Follow from '../Models/Follow'; 5 | import Dispatcher from '../Dispatcher'; 6 | import AppConstants from '../Constants/AppConstants'; 7 | 8 | var CHANGE_EVENT = 'change'; 9 | 10 | // TODO: Immutable? 11 | var _hash = {}; 12 | 13 | function setList(key, list) { 14 | var models = []; 15 | for(var i in list) { 16 | var model = new Follow(list[i]); 17 | models.push(model); 18 | } 19 | _hash[key] = models; 20 | } 21 | 22 | var ModelStore = assign({}, EventEmitter.prototype, { 23 | get: function(key) { 24 | return _hash[key]; 25 | }, 26 | 27 | emitChange: function(key) { 28 | this.emit(CHANGE_EVENT, key); 29 | }, 30 | 31 | addChangeListener: function(callback) { 32 | this.on(CHANGE_EVENT, callback); 33 | }, 34 | 35 | removeChangeListener: function(callback) { 36 | this.removeListener(CHANGE_EVENT, callback); 37 | } 38 | }); 39 | 40 | // Register callback to handle all updates 41 | Dispatcher.register(function(action) { 42 | switch(action.actionType) { 43 | case AppConstants.FOLLOW_LIST_UPDATED: 44 | setList(action.listProps.username, action.listProps.follows); 45 | ModelStore.emitChange(action.listProps.username); 46 | break; 47 | // TODO: save 48 | default: 49 | // no op 50 | } 51 | }); 52 | 53 | export default ModelStore; -------------------------------------------------------------------------------- /App/Stores/LocalKeyStore.js: -------------------------------------------------------------------------------- 1 | // wrapper on local storage technique 2 | import React from 'react'; 3 | import { 4 | AsyncStorage 5 | } from 'react-native'; 6 | 7 | var LocalKeyStore = { 8 | getKey: function(keyName, callback) { 9 | AsyncStorage.getItem(keyName, function(error, value) { // callback: error, value 10 | if (error) { 11 | console.log('Error getting item (' + keyName + ') from local storage! ' + error.message); 12 | if(callback) callback(error, null); 13 | } else { 14 | var json = JSON.parse(value); 15 | if(callback) callback(null, json); 16 | } 17 | }); 18 | }, 19 | 20 | setKey: function(keyName, value, callback) { // callback: error 21 | if (value) { 22 | var encoded = JSON.stringify(value); 23 | AsyncStorage.setItem(keyName, encoded, function(error) { 24 | if (error) { 25 | console.log('Error setting item (' + keyName + ') to local storage! ' + error.message); 26 | if(callback) callback(error); 27 | } else { 28 | if(callback) callback(null); 29 | } 30 | }); 31 | } 32 | else { 33 | // deleting it 34 | // TODO: what if it's not there? does it error 35 | AsyncStorage.removeItem(keyName, function(error) { // callback: error 36 | if (error) { 37 | console.log('Error removing item (' + keyName + ') from local storage! ' + error.message); 38 | if(callback) callback(error); 39 | } else { 40 | if(callback) callback(null); 41 | } 42 | }); 43 | } 44 | }, 45 | }; 46 | 47 | export default LocalKeyStore; 48 | -------------------------------------------------------------------------------- /App/Stores/PostListStore.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import assign from 'object-assign'; 3 | 4 | import Post from '../Models/Post'; 5 | import Dispatcher from '../Dispatcher'; 6 | import AppConstants from '../Constants/AppConstants'; 7 | 8 | var CHANGE_EVENT = 'change'; 9 | 10 | // TODO: Immutable? 11 | var _hash = {}; 12 | 13 | function addModel(key, props) { 14 | if(!_hash[key]) _hash[key] = []; 15 | var model = new Post(props); 16 | _hash[key].unshift(model); 17 | } 18 | 19 | function setList(key, list) { 20 | var models = []; 21 | for(var i in list) { 22 | var model = new Post(list[i]); 23 | models.push(model); 24 | } 25 | _hash[key] = models; 26 | } 27 | 28 | var ModelStore = assign({}, EventEmitter.prototype, { 29 | get: function(key) { 30 | return _hash[key]; 31 | }, 32 | 33 | emitChange: function(key) { 34 | this.emit(CHANGE_EVENT, key); 35 | }, 36 | 37 | addChangeListener: function(callback) { 38 | this.on(CHANGE_EVENT, callback); 39 | }, 40 | 41 | removeChangeListener: function(callback) { 42 | this.removeListener(CHANGE_EVENT, callback); 43 | } 44 | }); 45 | 46 | // Register callback to handle all updates 47 | Dispatcher.register(function(action) { 48 | switch(action.actionType) { 49 | case AppConstants.POST_LIST_UPDATED: 50 | setList(action.listProps.username, action.listProps.posts); 51 | ModelStore.emitChange(action.listProps.username); 52 | break; 53 | case AppConstants.POST_ADDED: 54 | addModel(action.postProps.username, action.postProps); 55 | ModelStore.emitChange(action.postProps.username); 56 | break; 57 | // TODO: save 58 | default: 59 | // no op 60 | } 61 | }); 62 | 63 | export default ModelStore; -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/.eslintignore: -------------------------------------------------------------------------------- 1 | example/ 2 | test/ 3 | **/__tests__/**/*.js 4 | **/__mocks__/**/*.js 5 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | **/__tests__/**/*.js 3 | **/__mocks__/**/*.js 4 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/RefreshableListView') 2 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/ControlledRefreshableListView.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | var ListView = require('./ListView') 4 | var createElementFrom = require('./createElementFrom') 5 | var RefreshingIndicator = require('./RefreshingIndicator') 6 | 7 | const SCROLL_EVENT_THROTTLE = 32 8 | const MIN_PULLDOWN_DISTANCE = 40 9 | 10 | const LISTVIEW_REF = 'listview' 11 | 12 | var ControlledRefreshableListView = React.createClass({ 13 | propTypes: { 14 | onRefresh: PropTypes.func.isRequired, 15 | isRefreshing: PropTypes.bool.isRequired, 16 | refreshDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 17 | refreshingIndictatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 18 | minPulldownDistance: PropTypes.number, 19 | ignoreInertialScroll: PropTypes.bool, 20 | scrollEventThrottle: PropTypes.number, 21 | onScroll: PropTypes.func, 22 | renderHeader: PropTypes.func, 23 | renderHeaderWrapper: PropTypes.func, 24 | onResponderGrant: PropTypes.func, 25 | onResponderRelease: PropTypes.func, 26 | }, 27 | getDefaultProps() { 28 | return { 29 | minPulldownDistance: MIN_PULLDOWN_DISTANCE, 30 | scrollEventThrottle: SCROLL_EVENT_THROTTLE, 31 | ignoreInertialScroll: true, 32 | refreshingIndictatorComponent: RefreshingIndicator, 33 | } 34 | }, 35 | handleScroll(e) { 36 | var scrollY = e.nativeEvent.contentInset.top + e.nativeEvent.contentOffset.y 37 | 38 | if (this.isTouching || (!this.isTouching && !this.props.ignoreInertialScroll)) { 39 | if (scrollY < -this.props.minPulldownDistance) { 40 | if (!this.props.isRefreshing) { 41 | if (this.props.onRefresh) { 42 | this.props.onRefresh() 43 | } 44 | } 45 | } 46 | } 47 | 48 | this.props.onScroll && this.props.onScroll(e) 49 | }, 50 | handleResponderGrant() { 51 | this.isTouching = true 52 | if (this.props.onResponderGrant) { 53 | this.props.onResponderGrant.apply(null, arguments) 54 | } 55 | }, 56 | handleResponderRelease() { 57 | this.isTouching = false 58 | if (this.props.onResponderRelease) { 59 | this.props.onResponderRelease.apply(null, arguments) 60 | } 61 | }, 62 | getScrollResponder() { 63 | return this.refs[LISTVIEW_REF].getScrollResponder() 64 | }, 65 | setNativeProps(props) { 66 | this.refs[LISTVIEW_REF].setNativeProps(props) 67 | }, 68 | renderHeader() { 69 | var description = this.props.refreshDescription 70 | 71 | var refreshingIndictator 72 | if (this.props.isRefreshing) { 73 | refreshingIndictator = createElementFrom(this.props.refreshingIndictatorComponent, {description}) 74 | } else { 75 | refreshingIndictator = null 76 | } 77 | 78 | if (this.props.renderHeaderWrapper) { 79 | return this.props.renderHeaderWrapper(refreshingIndictator) 80 | } else if (this.props.renderHeader) { 81 | console.warn('renderHeader is deprecated. Use renderHeaderWrapper instead.') 82 | return this.props.renderHeader(refreshingIndictator) 83 | } else { 84 | return refreshingIndictator 85 | } 86 | }, 87 | render() { 88 | return ( 89 | 98 | ) 99 | }, 100 | }) 101 | 102 | ControlledRefreshableListView.DataSource = ListView.DataSource 103 | ControlledRefreshableListView.RefreshingIndicator = RefreshingIndicator 104 | 105 | module.exports = ControlledRefreshableListView 106 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/ListView.js: -------------------------------------------------------------------------------- 1 | var {ListView} = require('react-native') 2 | 3 | module.exports = ListView 4 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/RefreshableListView.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | var isPromise = require('is-promise') 4 | var delay = require('./delay') 5 | var ListView = require('./ListView') 6 | var RefreshingIndicator = require('./RefreshingIndicator') 7 | var ControlledRefreshableListView = require('./ControlledRefreshableListView') 8 | 9 | const LISTVIEW_REF = 'listview' 10 | 11 | var RefreshableListView = React.createClass({ 12 | propTypes: { 13 | loadData: PropTypes.func.isRequired, 14 | minDisplayTime: PropTypes.number, 15 | minBetweenTime: PropTypes.number, 16 | // props passed to child 17 | refreshDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 18 | refreshingIndictatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 19 | minPulldownDistance: PropTypes.number, 20 | renderHeaderWrapper: PropTypes.func, 21 | }, 22 | getDefaultProps() { 23 | return { 24 | minDisplayTime: 300, 25 | minBetweenTime: 300, 26 | minPulldownDistance: 40, 27 | refreshingIndictatorComponent: RefreshingIndicator, 28 | } 29 | }, 30 | getInitialState() { 31 | return { 32 | isRefreshing: false, 33 | } 34 | }, 35 | handleRefresh() { 36 | if (this.willRefresh) return 37 | 38 | this.willRefresh = true 39 | 40 | var loadingDataPromise = new Promise((resolve) => { 41 | var loadDataReturnValue = this.props.loadData(resolve) 42 | 43 | if (isPromise(loadDataReturnValue)) { 44 | loadingDataPromise = loadDataReturnValue 45 | } 46 | 47 | Promise.all([ 48 | loadingDataPromise, 49 | new Promise((resolve) => this.setState({isRefreshing: true}, resolve)), 50 | delay(this.props.minDisplayTime), 51 | ]) 52 | .then(() => delay(this.props.minBetweenTime)) 53 | .then(() => { 54 | this.willRefresh = false 55 | this.setState({isRefreshing: false}) 56 | }) 57 | }) 58 | }, 59 | getScrollResponder() { 60 | return this.refs[LISTVIEW_REF].getScrollResponder() 61 | }, 62 | setNativeProps(props) { 63 | this.refs[LISTVIEW_REF].setNativeProps(props) 64 | }, 65 | render() { 66 | return ( 67 | 73 | ) 74 | }, 75 | }) 76 | 77 | RefreshableListView.DataSource = ListView.DataSource 78 | RefreshableListView.RefreshingIndicator = RefreshingIndicator 79 | 80 | module.exports = RefreshableListView 81 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/RefreshingIndicator.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | PropTypes, 3 | isValidElement, 4 | createElement, 5 | } from 'react'; 6 | 7 | import { 8 | View, 9 | Text, 10 | ActivityIndicator, 11 | StyleSheet, 12 | } from 'react-native'; 13 | 14 | var RefreshingIndicator = React.createClass({ 15 | propTypes: { 16 | activityIndicatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 17 | stylesheet: PropTypes.object, 18 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 19 | }, 20 | getDefaultProps() { 21 | return { 22 | activityIndicatorComponent: ActivityIndicator, 23 | } 24 | }, 25 | renderActivityIndicator(style) { 26 | var activityIndicator = this.props.activityIndicatorComponent 27 | 28 | if (isValidElement(activityIndicator)) { 29 | return activityIndicator 30 | } else { // is a component class, not an element 31 | return createElement(activityIndicator, {style}) 32 | } 33 | }, 34 | render() { 35 | var styles = Object.assign({}, stylesheet, this.props.stylesheet) 36 | 37 | return ( 38 | 39 | 40 | 41 | {this.props.description} 42 | 43 | {this.renderActivityIndicator(styles.activityIndicator)} 44 | 45 | 46 | ) 47 | }, 48 | }) 49 | 50 | var stylesheet = StyleSheet.create({ 51 | container: { 52 | flex: 1, 53 | justifyContent: 'space-around', 54 | alignItems: 'center', 55 | }, 56 | wrapper: { 57 | height: 60, 58 | marginTop: 10, 59 | }, 60 | content: { 61 | marginTop: 10, 62 | height: 60, 63 | }, 64 | }) 65 | 66 | module.exports = RefreshingIndicator 67 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/createElementFrom.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var { 3 | cloneElement, 4 | createElement, 5 | isValidElement, 6 | } = React 7 | 8 | 9 | function createElementFrom(elementOrClass, props) { 10 | 11 | if (isValidElement(elementOrClass)) { 12 | return cloneElement(elementOrClass, props) 13 | } else { // is a component class, not an element 14 | return createElement(elementOrClass, props) 15 | } 16 | } 17 | 18 | module.exports = createElementFrom 19 | -------------------------------------------------------------------------------- /App/Vendor/react-native-refreshable-listview/lib/delay.js: -------------------------------------------------------------------------------- 1 | function delay(time) { 2 | return new Promise((resolve) => setTimeout(resolve, time)) 3 | } 4 | 5 | module.exports = delay 6 | -------------------------------------------------------------------------------- /App/jsVersion.js: -------------------------------------------------------------------------------- 1 | export default 3; 2 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 TaskRabbit 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /android/Sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # To learn about Buck see [Docs](https://buckbuild.com/). 4 | # To run your application with Buck: 5 | # - install Buck 6 | # - `npm start` - to start the packager 7 | # - `cd android` 8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 10 | # - `buck install -r android/app` - compile, install and run application 11 | # 12 | 13 | lib_deps = [] 14 | for jarfile in glob(['libs/*.jar']): 15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) 16 | lib_deps.append(':' + name) 17 | prebuilt_jar( 18 | name = name, 19 | binary_jar = jarfile, 20 | ) 21 | 22 | for aarfile in glob(['libs/*.aar']): 23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) 24 | lib_deps.append(':' + name) 25 | android_prebuilt_aar( 26 | name = name, 27 | aar = aarfile, 28 | ) 29 | 30 | android_library( 31 | name = 'all-libs', 32 | exported_deps = lib_deps 33 | ) 34 | 35 | android_library( 36 | name = 'app-code', 37 | srcs = glob([ 38 | 'src/main/java/**/*.java', 39 | ]), 40 | deps = [ 41 | ':all-libs', 42 | ':build_config', 43 | ':res', 44 | ], 45 | ) 46 | 47 | android_build_config( 48 | name = 'build_config', 49 | package = 'com.sample', 50 | ) 51 | 52 | android_resource( 53 | name = 'res', 54 | res = 'src/main/res', 55 | package = 'com.sample', 56 | ) 57 | 58 | android_binary( 59 | name = 'app', 60 | package_type = 'debug', 61 | manifest = 'src/main/AndroidManifest.xml', 62 | keystore = '//android/keystores:debug', 63 | deps = [ 64 | ':app-code', 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.facebook.react.ReactActivity; 6 | 7 | public class MainActivity extends ReactActivity { 8 | 9 | /** 10 | * Returns the name of the main component registered from JavaScript. 11 | * This is used to schedule rendering of the component. 12 | */ 13 | @Override 14 | protected String getMainComponentName() { 15 | return "Sample"; 16 | } 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | TLSSetup.configure(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.shell.MainReactPackage; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 18 | @Override 19 | protected boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | return Arrays.asList( 26 | new MainReactPackage(), 27 | new SamplePackage() 28 | ); 29 | } 30 | }; 31 | 32 | @Override 33 | public ReactNativeHost getReactNativeHost() { 34 | return mReactNativeHost; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/SampleConstants.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | public class SampleConstants { 4 | public static final String APP_PREFS_NAME = "sample-app-prefs"; 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/SamplePackage.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import android.app.Activity; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.bridge.JavaScriptModule; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class SamplePackage implements ReactPackage { 15 | private Activity mActivity; 16 | 17 | //public SamplePackage(Activity activity) { 18 | // mActivity = activity; 19 | //} 20 | 21 | @Override 22 | public List> createJSModules() { 23 | return Collections.emptyList(); 24 | } 25 | 26 | @Override 27 | public List createViewManagers(ReactApplicationContext reactApplicationContext) { 28 | return Collections.emptyList(); 29 | } 30 | 31 | @Override 32 | public List createNativeModules( 33 | ReactApplicationContext reactContext) { 34 | List modules = new ArrayList<>(); 35 | 36 | modules.add(new EnvironmentManager(reactContext)); 37 | modules.add(new TestRunnerManager(reactContext)); 38 | 39 | return modules; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/TLSSetup.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import android.util.Log; 4 | import com.facebook.react.modules.network.OkHttpClientProvider; 5 | import com.facebook.react.modules.network.ReactCookieJarContainer; 6 | import okhttp3.ConnectionSpec; 7 | import okhttp3.OkHttpClient; 8 | import okhttp3.TlsVersion; 9 | 10 | import javax.net.ssl.*; 11 | import java.util.Arrays; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | public class TLSSetup { 15 | 16 | static String TAG = "TLSSetup"; 17 | 18 | public static void configure(){ 19 | try { 20 | SSLContext sc = SSLContext.getInstance("TLSv1.1"); 21 | sc.init(null, null, null); 22 | ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) 23 | .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1) 24 | .build(); 25 | // Taken from OkHttpClientProvider.java 26 | // Set no timeout by default 27 | OkHttpClient sClient = new OkHttpClient.Builder() 28 | .connectTimeout(0, TimeUnit.MILLISECONDS) 29 | .readTimeout(0, TimeUnit.MILLISECONDS) 30 | .writeTimeout(0, TimeUnit.MILLISECONDS) 31 | .cookieJar(new ReactCookieJarContainer()) 32 | // set sslSocketFactory 33 | .sslSocketFactory(new TLSSocketFactory(sc.getSocketFactory())) 34 | // set connectionSpecs 35 | .connectionSpecs(Arrays.asList(cs, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) 36 | .build(); 37 | 38 | OkHttpClientProvider.replaceOkHttpClient(sClient); 39 | } catch (Exception e) { 40 | Log.e(TAG, e.getMessage()); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/TLSSocketFactory.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.Socket; 6 | import java.net.UnknownHostException; 7 | 8 | import javax.net.ssl.SSLSocket; 9 | import javax.net.ssl.SSLSocketFactory; 10 | 11 | /** 12 | * Taken from https://gist.github.com/mlc/549409f649251897ebef 13 | * 14 | * Enables TLS when creating SSLSockets. 15 | * 16 | * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html 17 | * @see SSLSocketFactory 18 | */ 19 | class TLSSocketFactory extends SSLSocketFactory { 20 | final SSLSocketFactory delegate; 21 | 22 | public TLSSocketFactory(SSLSocketFactory delegate) { 23 | this.delegate = delegate; 24 | } 25 | 26 | @Override 27 | public String[] getDefaultCipherSuites() { 28 | return delegate.getDefaultCipherSuites(); 29 | } 30 | 31 | @Override 32 | public String[] getSupportedCipherSuites() { 33 | return delegate.getSupportedCipherSuites(); 34 | } 35 | 36 | @Override 37 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 38 | return patch(delegate.createSocket(s, host, port, autoClose)); 39 | } 40 | 41 | @Override 42 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 43 | return patch(delegate.createSocket(host, port)); 44 | } 45 | 46 | @Override 47 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { 48 | return patch(delegate.createSocket(host, port, localHost, localPort)); 49 | } 50 | 51 | @Override 52 | public Socket createSocket(InetAddress host, int port) throws IOException { 53 | return patch(delegate.createSocket(host, port)); 54 | } 55 | 56 | @Override 57 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { 58 | return patch(delegate.createSocket(address, port, localAddress, localPort)); 59 | } 60 | 61 | private Socket patch(Socket s) { 62 | if (s instanceof SSLSocket) { 63 | ((SSLSocket) s).setEnabledProtocols(((SSLSocket) s).getSupportedProtocols()); 64 | } 65 | return s; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/TestRunnerManager.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import android.content.SharedPreferences; 4 | import android.os.Build; 5 | import android.preference.PreferenceManager; 6 | import com.facebook.react.bridge.*; 7 | import com.facebook.react.devsupport.DevSupportManager; 8 | 9 | import javax.annotation.Nullable; 10 | 11 | import java.lang.reflect.Field; 12 | 13 | public class TestRunnerManager extends ReactContextBaseJavaModule { 14 | private @Nullable DevSupportManager mDevSupportManager; 15 | private @Nullable SharedPreferences mPreferences; 16 | private @Nullable ReactApplicationContext mReactContext; 17 | 18 | public TestRunnerManager (ReactApplicationContext reactContext) { 19 | super(reactContext); 20 | 21 | mReactContext = reactContext; 22 | 23 | setDevSupportManager(); 24 | } 25 | 26 | private void setDevSupportManager() { 27 | try { 28 | Field field = ReactContext.class.getDeclaredField("mNativeModuleCallExceptionHandler"); 29 | field.setAccessible(true); 30 | mDevSupportManager = (DevSupportManager) field.get(mReactContext); 31 | } catch (NoSuchFieldException e) { 32 | e.printStackTrace(); 33 | } catch (IllegalAccessException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | @Override 39 | public String getName() { 40 | return "TestRunnerManager"; 41 | } 42 | 43 | @ReactMethod 44 | public void reset(Callback callback) { 45 | if (mDevSupportManager != null) { 46 | UiThreadUtil.runOnUiThread( 47 | new Runnable() { 48 | @Override 49 | public void run() { 50 | mDevSupportManager.handleReloadJS(); 51 | } 52 | } 53 | ); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/sample/utils/LocaleUtils.java: -------------------------------------------------------------------------------- 1 | package com.sample.utils; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * TaskRabbit created on 5/12/14. 7 | */ 8 | public class LocaleUtils { 9 | 10 | public static Locale getDefault() { 11 | if (null == Locale.getDefault()) { return Locale.US; } 12 | return Locale.getDefault(); 13 | } 14 | 15 | public static String getServerLocale(Locale locale) { 16 | if (null != locale) { 17 | return locale.toString().replace("_", "-"); 18 | } else { 19 | return ""; 20 | } 21 | } 22 | 23 | public static String getDefaultLocaleForServer() { 24 | Locale locale = getDefault(); 25 | return getServerLocale(locale); 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 20 16:55:35 PDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Sample' 2 | 3 | include ':app' 4 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { 5 | AppRegistry, 6 | } from 'react-native'; 7 | 8 | import Root from './App/Root'; 9 | AppRegistry.registerComponent('Sample', () => Root); 10 | -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { 5 | AppRegistry, 6 | } from 'react-native'; 7 | 8 | import Root from './App/Root'; 9 | AppRegistry.registerComponent('Sample', () => Root); 10 | -------------------------------------------------------------------------------- /ios/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig" 2 | 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) kEnvironment="@\"debug\"" -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '8.0' 2 | source 'https://github.com/CocoaPods/Specs.git' 3 | 4 | # inhibit_all_warnings! 5 | 6 | target 'Sample' do 7 | pod 'SimulatorRemoteNotifications', '~> 0.0.3' 8 | end 9 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SimulatorRemoteNotifications (0.0.3) 3 | 4 | DEPENDENCIES: 5 | - SimulatorRemoteNotifications (~> 0.0.3) 6 | 7 | SPEC CHECKSUMS: 8 | SimulatorRemoteNotifications: 1610643e0582b2af67bc254b071ace4077e8ef86 9 | 10 | PODFILE CHECKSUM: 1e7bba3fe4e4d12126efc77f3f430a3dcd1d8be9 11 | 12 | COCOAPODS: 1.1.1 13 | -------------------------------------------------------------------------------- /ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample Staging.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample Test.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Sample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Sample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/Sample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import "RCTRootView.h" 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | NSURL *jsCodeLocation; 19 | 20 | /** 21 | * Loading JavaScript code - uncomment the one you want. 22 | * 23 | * OPTION 1 24 | * Load from development server. Start the server from the repository root: 25 | * 26 | * $ npm start 27 | * 28 | * To run on device, change `localhost` to the IP address of your computer 29 | * (you can get this by typing `ifconfig` into the terminal and selecting the 30 | * `inet` value under `en0:`) and make sure your computer and iOS device are 31 | * on the same Wi-Fi network. 32 | */ 33 | 34 | // jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; 35 | 36 | /** 37 | * OPTION 2 38 | * Load from pre-bundled file on disk. To re-generate the static bundle 39 | * from the root of your project directory, run 40 | * 41 | * $ react-native bundle --minify 42 | * 43 | * see http://facebook.github.io/react-native/docs/runningondevice.html 44 | */ 45 | 46 | // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 47 | 48 | 49 | #ifdef TEST_ENVIRONMENT 50 | // different port 51 | jsCodeLocation = [NSURL URLWithString:@"http://localhost:9091/index.ios.bundle?platform=ios&dev=true"]; 52 | #else 53 | #if TARGET_IPHONE_SIMULATOR 54 | jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; 55 | #else 56 | jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 57 | #endif 58 | #endif 59 | 60 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 61 | moduleName:@"Sample" 62 | initialProperties:nil 63 | launchOptions:launchOptions]; 64 | 65 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 66 | UIViewController *rootViewController = [UIViewController new]; 67 | rootViewController.view = rootView; 68 | self.window.rootViewController = rootViewController; 69 | [self.window makeKeyAndVisible]; 70 | return YES; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /ios/Sample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ios/Sample/EnvironmentManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentManager.m 3 | // Sample 4 | // 5 | 6 | #import "RCTBridgeModule.h" 7 | 8 | @interface EnvironmentManager : NSObject 9 | 10 | @end 11 | 12 | 13 | @implementation EnvironmentManager 14 | 15 | RCT_EXPORT_MODULE() 16 | 17 | RCT_EXPORT_METHOD(get:(RCTResponseSenderBlock)callback) 18 | { 19 | NSString *locale = [[NSLocale currentLocale] localeIdentifier]; 20 | locale = [locale stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; 21 | 22 | NSNumber * simulator = @NO; 23 | NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; 24 | NSString * buildCode = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; 25 | 26 | NSString * envName = kEnvironment; 27 | NSDictionary *passed = [[NSProcessInfo processInfo] environment]; 28 | NSString *override = [passed valueForKey:@"SAMPLE_ENV"]; 29 | if (override) { 30 | envName = override; 31 | } 32 | #ifdef TEST_ENVIRONMENT 33 | envName = @"test"; 34 | #endif 35 | #ifdef STAGING_ENVIRONMENT 36 | envName = @"staging"; 37 | #endif 38 | 39 | 40 | #if TARGET_IPHONE_SIMULATOR 41 | simulator = @YES; 42 | #endif 43 | 44 | callback(@[ @{ 45 | @"name": envName, 46 | @"buildCode": buildCode, 47 | @"simulator": simulator, 48 | @"version": version, 49 | @"locale": locale 50 | }]); 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /ios/Sample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ios/Sample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ios/Sample/Sample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Sample/TestRunnerManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // TestRunnerManager.m 3 | // Tasker 4 | // 5 | // Created by Brian Leonard on 8/9/15. 6 | // Copyright (c) 2015 TaskRabbit. All rights reserved. 7 | // 8 | 9 | #import "RCTBridgeModule.h" 10 | #import "RCTBridge.h" 11 | 12 | @interface TestRunnerManager : NSObject 13 | 14 | @end 15 | 16 | 17 | @implementation TestRunnerManager 18 | 19 | RCT_EXPORT_MODULE() 20 | 21 | RCT_EXPORT_METHOD(reset:(RCTResponseSenderBlock)callback) 22 | { 23 | // delete all data in documents directory 24 | NSString *folderPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 25 | NSError *error = nil; 26 | for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:folderPath error:&error]) { 27 | // If you want to skip certain files 28 | if ([file containsString:@".skip"]) { 29 | // but be sure to clear them out if applicable from your js code 30 | continue; 31 | } 32 | 33 | // otherwise, delete 34 | [[NSFileManager defaultManager] removeItemAtPath:[folderPath stringByAppendingPathComponent:file] error:&error]; 35 | } 36 | 37 | // reload the app 38 | [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil]; 39 | 40 | callback(@[]); 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /ios/Sample/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/Staging.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Sample/Pods-Sample.staging.xcconfig" 2 | 3 | 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) kEnvironment="@\"staging\"" -------------------------------------------------------------------------------- /ios/main.jsbundle: -------------------------------------------------------------------------------- 1 | // Offline JS 2 | // To re-generate the offline bundle, run this from the root of your project: 3 | // 4 | // $ react-native bundle --minify 5 | // 6 | // See http://facebook.github.io/react-native/docs/runningondevice.html for more details. 7 | 8 | throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions'); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeSampleApp", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node_modules/react-native/packager/packager.sh", 7 | "test": "npm run mocha-test test/integration", 8 | "mocha-test": "mocha --recursive --compilers js:babel-core/register", 9 | "compile:test": "TARGET=test node tasks/compile.js", 10 | "compile:staging": "TARGET=staging node tasks/compile.js", 11 | "install:staging": "PHONE=true TARGET=staging node tasks/compile.js", 12 | "translation:dump": "ACTION=dump node tasks/translation.js", 13 | "translation:backfill": "ACTION=backfill node tasks/translation.js", 14 | "translation:times": "ACTION=times node tasks/translation.js", 15 | "translation:check": "ACTION=check node tasks/translation.js" 16 | }, 17 | "dependencies": { 18 | "events": "1.1.0", 19 | "flux": "2.1.1", 20 | "invariant": "2.2.0", 21 | "is-promise": "2.1.0", 22 | "jstz": "1.0.7", 23 | "keymirror": "0.1.1", 24 | "moment-timezone": "0.4.1", 25 | "object-assign": "4.0.1", 26 | "react": "15.4.0", 27 | "react-native": "0.38.0", 28 | "react-native-keychain": "0.2.6", 29 | "react-native-mock": "0.2.7", 30 | "react-timer-mixin": "0.13.3", 31 | "require-all": "2.0.0", 32 | "superagent": "1.8.3", 33 | "underscore": "1.8.3", 34 | "underscore.string": "3.2.2" 35 | }, 36 | "devDependencies": { 37 | "appium": "1.5.1", 38 | "babel-plugin-syntax-async-functions": "^6.5.0", 39 | "babel-plugin-transform-class-properties": "^6.6.0", 40 | "babel-plugin-transform-regenerator": "^6.6.5", 41 | "babel-preset-es2015": "^6.6.0", 42 | "chai": "3.4.0", 43 | "chai-as-promised": "5.1.0", 44 | "colors": "1.1.2", 45 | "ios-deploy": "1.8.2", 46 | "koa": "1.1.2", 47 | "koa-bodyparser": "2.0.1", 48 | "mocha": "2.4.5", 49 | "node-inspector": "0.12.8", 50 | "wd": "0.4.0", 51 | "yiewd": "0.6.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /screenshots/create_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/screenshots/create_post.png -------------------------------------------------------------------------------- /screenshots/follows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/screenshots/follows.png -------------------------------------------------------------------------------- /screenshots/show_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/screenshots/show_post.png -------------------------------------------------------------------------------- /screenshots/sign_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/9ac2f607cdd2a371b410ca56e91209b03a9c58c0/screenshots/sign_up.png -------------------------------------------------------------------------------- /server/.nvmrc: -------------------------------------------------------------------------------- 1 | 4.2.3 -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | var User = require("./models").User; 2 | 3 | var renderAccount = function (user) { 4 | var out = { 5 | id: user.id, 6 | token: user.token, 7 | username: user.username 8 | }; 9 | return out; 10 | }; 11 | 12 | exports.loginUser = function *() { 13 | var params = this.request.body || {}; 14 | var username = (params.username || "").trim(); 15 | var password = (params.password || "").trim(); 16 | 17 | if(!username) this.throw("No username provided", 422); 18 | if(!password) this.throw("No password provided", 422); 19 | 20 | var user = User.findByUsername(username); 21 | if (user && user.authenticate(password)) { 22 | this.status = 200; 23 | this.body = renderAccount(user); 24 | } 25 | else { 26 | this.status = 422; 27 | this.body = {error: "Username and password not found"} 28 | } 29 | }; 30 | 31 | exports.getCurrentUser = function *() { 32 | this.status = 200; 33 | this.body = renderAccount(this.passport.user); 34 | }; 35 | 36 | exports.createUser = function *() { 37 | var params = this.request.body || {}; 38 | var username = (params.username || "").trim(); 39 | var password = (params.password || "").trim(); 40 | 41 | if(!username) this.throw("No username provided", 422); 42 | if(!password) this.throw("No password provided", 422); 43 | 44 | var user = User.findByUsername(username); 45 | if(user) this.throw("User already exists", 422); 46 | 47 | user = User.create(params.username, params.password); 48 | 49 | this.status = 201; 50 | this.body = renderAccount(user); 51 | }; 52 | -------------------------------------------------------------------------------- /server/follows.js: -------------------------------------------------------------------------------- 1 | var User = require('./models').User; 2 | 3 | var renderFollow = function(follow) { 4 | return { 5 | id: follow.follow_id, 6 | username: follow.username 7 | }; 8 | }; 9 | 10 | var renderFollows = function(user) { 11 | var out = { 12 | follows: [], 13 | username: user.username 14 | }; 15 | var follows = user.getFollows(); 16 | for(var i in follows) { 17 | var follow = follows[i]; 18 | out.follows.push(renderFollow(follow)); 19 | } 20 | 21 | return out; 22 | }; 23 | 24 | exports.userFollows = function *() { 25 | var username = this.params.username; 26 | var user = User.findByUsername(username); 27 | if (!user) { 28 | this.throw("No user found", 404); 29 | } 30 | this.status = 200; 31 | this.body = renderFollows(user); 32 | }; 33 | -------------------------------------------------------------------------------- /server/models.js: -------------------------------------------------------------------------------- 1 | // in memory database for testing 2 | 3 | var _postsCount = 0; 4 | var Post = function(user, content) { 5 | this.id = ++_postsCount; 6 | this.user_id = user.id; 7 | this.username = user.username; 8 | this.user = user; 9 | this.content = content; 10 | }; 11 | 12 | Post.create = function(user, content) { 13 | var post = new Post(user, content); 14 | return post; 15 | }; 16 | 17 | 18 | var _followCount = 0; 19 | var Follow = function(user, other) { 20 | this.id = ++_followCount; 21 | this.user_id = user.id; 22 | this.user = user; 23 | this.follow_id = other.id; 24 | this.username = other.username; 25 | this.folows = other; 26 | }; 27 | 28 | Follow.create = function(user, other) { 29 | var follow = new Follow(user, other); 30 | return follow; 31 | }; 32 | 33 | 34 | var _users = {} // id => user object 35 | var _usersCount = 0; 36 | var User = function(username) { 37 | this.id = ++_usersCount; 38 | this.username = username; 39 | this.password = null; 40 | this.posts = []; 41 | this.follows = []; 42 | }; 43 | 44 | User.findByUsername = function(username) { 45 | for(var id in _users) { 46 | if (_users[id].username === username) { 47 | return _users[id]; 48 | } 49 | } 50 | return null; 51 | }; 52 | 53 | User.findByToken = function(token) { 54 | for(var id in _users) { 55 | if (_users[id].token === token) { 56 | return _users[id]; 57 | } 58 | } 59 | return null; 60 | }; 61 | 62 | User.create = function(username, password, token) { 63 | var user = new User(username); 64 | user.password = password; 65 | user.token = token || Math.random().toString(36).substr(2); 66 | _users[user.id] = user; 67 | return user; 68 | }; 69 | 70 | User.prototype.authenticate = function(password) { 71 | return this.password === password; 72 | }; 73 | 74 | User.prototype.addPost = function(content) { 75 | var post = Post.create(this, content); 76 | this.posts.unshift(post); 77 | return post; 78 | }; 79 | 80 | User.prototype.getPosts = function() { 81 | return this.posts; 82 | }; 83 | 84 | User.prototype.addFollow = function(other) { 85 | var follow = Follow.create(this, other); 86 | this.follows.unshift(follow); 87 | return follow; 88 | }; 89 | 90 | User.prototype.getFollows = function() { 91 | return this.follows; 92 | }; 93 | 94 | // Seed some users 95 | var bleonard = User.create('bleonard', 'sample', 'qwertyuiopasdfghjkl'); 96 | var jrichardlai = User.create('jrichardlai', 'sample', 'poiuytrewqlkjhgfdsa'); 97 | var taskrabbit = User.create('taskrabbit', 'sample', 'zxcvbnmqwertlkjhg'); 98 | 99 | bleonard.addPost("When you have a use case and wish an app existed. Then you remember you made it 7 years ago. #boston"); 100 | bleonard.addPost("I keep getting a facebook notification that asks if I know Nathan Cron.\nIt seems like every few days around this time #destiny"); 101 | bleonard.addPost("Learning CSS...\nDaughter: Put it in the middle between the top and bottom.\nDad: (googles again) Are you sure?"); 102 | bleonard.addPost("#reactjs #flux child components are like teenagers. they don't tell parents what they are up to. parent has to hear later from dispatcher."); 103 | 104 | jrichardlai.addPost("The problem with Rails today is that 1/2 the people are afraid Rails is turning into Java and the other 1/2 are trying to turn it into Java"); 105 | jrichardlai.addPost("Thanks @TaskRabbit to allow my party of 10 people to eat at House of Prime Ribs with 0 minutes wait :)! Yummy! #TaskTuesday"); 106 | 107 | taskrabbit.addPost("When you're a #NewParent tasks can really pile up. We can help! Check out our Task of the Week http://tinyurl.com/pxh88f8"); 108 | taskrabbit.addPost("@TaskRabbit CEO @leahbusque talks about confidence necessary to propel your idea forward #DF15WomenLead #df15"); 109 | taskrabbit.addPost("Happiness is the sound of someone else building your IKEA furniture. #taskrabbit"); 110 | 111 | bleonard.addFollow(jrichardlai); 112 | bleonard.addFollow(taskrabbit); 113 | 114 | jrichardlai.addFollow(bleonard); 115 | jrichardlai.addFollow(taskrabbit); 116 | 117 | taskrabbit.addFollow(bleonard); 118 | 119 | exports.User = User; 120 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node --harmony server.js" 7 | }, 8 | "dependencies": { 9 | "koa": "^1.0.0", 10 | "koa-bodyparser": "^2.0.1", 11 | "koa-error": "^1.1.3", 12 | "koa-logger": "^1.3.0", 13 | "koa-passport": "^1.2.0", 14 | "koa-router": "^5.1.2", 15 | "passport": "^0.3.0", 16 | "passport-http-bearer": "^1.0.1", 17 | "password": "^0.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/posts.js: -------------------------------------------------------------------------------- 1 | var User = require('./models').User; 2 | 3 | var renderPost = function(post) { 4 | return { 5 | id: post.id, 6 | content: post.content, 7 | username: post.username 8 | }; 9 | }; 10 | 11 | var renderPosts = function(user) { 12 | var out = { 13 | posts: [], 14 | username: user.username 15 | }; 16 | var posts = user.getPosts(); 17 | for(var i in posts) { 18 | var post = posts[i]; 19 | out.posts.push(renderPost(post)); 20 | } 21 | 22 | return out; 23 | }; 24 | 25 | exports.userPosts = function *() { 26 | var username = this.params.username; 27 | var user = User.findByUsername(username); 28 | if (!user) { 29 | this.throw("No user found", 404); 30 | } 31 | this.status = 200; 32 | this.body = renderPosts(user); 33 | }; 34 | 35 | exports.createPost = function *() { 36 | var user = this.passport.user; 37 | var params = this.request.body || {}; 38 | var content = (params.content || "").trim(); 39 | 40 | if(!content) this.throw("No content provided", 422); 41 | 42 | var post = user.addPost(content); 43 | 44 | this.status = 201; 45 | this.body = renderPost(post); 46 | }; 47 | -------------------------------------------------------------------------------- /server/secured.js: -------------------------------------------------------------------------------- 1 | var passport = require("koa-passport"); 2 | var BearerStrategy = require('passport-http-bearer').Strategy; 3 | var User = require("./models").User; 4 | 5 | passport.use(new BearerStrategy({}, 6 | function(token, done) { 7 | user = User.findByToken(token); 8 | if (user) return done(null, user); 9 | else return done(null, false); 10 | } 11 | )); 12 | 13 | var secured = function *(next) { 14 | var _this = this; 15 | yield passport.authenticate("bearer", { session: false }, 16 | function*(err, user, info) { 17 | if (err) { 18 | throw err; 19 | } 20 | else if (!user) { 21 | _this.status = 401; 22 | _this.body = {error: "Please log in"} 23 | } 24 | else { 25 | _this.passport.user = user; 26 | yield next; 27 | } 28 | }); 29 | }; 30 | 31 | module.exports = secured; 32 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const koa = require("koa"); 4 | const passport = require("koa-passport"); 5 | const errorHandler = require("koa-error"); 6 | const bodyParser = require("koa-bodyparser"); 7 | const logger = require("koa-logger"); 8 | const Router = require("koa-router"); 9 | 10 | const app = module.exports = koa(); 11 | app.use(logger()); 12 | app.use(errorHandler()); 13 | app.use(bodyParser()); 14 | app.use(passport.initialize()); 15 | 16 | var router = new Router(); 17 | 18 | router.use(function *(next) { 19 | // everything json 20 | this.type = "json"; 21 | this.headers['accept'] = "application/json"; 22 | yield next; 23 | }); 24 | 25 | 26 | var secured = require('./secured') 27 | var auth = require("./auth"); 28 | var posts = require("./posts"); 29 | var follows = require("./follows"); 30 | 31 | router.post("/api/signup", auth.createUser); 32 | router.post("/api/login", auth.loginUser); 33 | router.get ("/api/account", secured, auth.getCurrentUser); 34 | router.post("/api/posts", secured, posts.createPost); 35 | router.get ("/api/posts/:username", posts.userPosts); 36 | router.get ("/api/follows/:username", follows.userFollows); 37 | 38 | app.use(router.routes()); 39 | app.listen(3000); 40 | console.log("Server started, listening on port: 3000"); 41 | -------------------------------------------------------------------------------- /tasks/compile.js: -------------------------------------------------------------------------------- 1 | var Compiler = require("./compiler"); 2 | 3 | var compiler = new Compiler('ios', process.env.TARGET, console); 4 | compiler.cleanDirectory(); 5 | compiler.build(); 6 | compiler.zip(); 7 | 8 | if (process.env.PHONE) { 9 | compiler.phoneInstall(); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /tasks/compiler.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | 3 | module.exports = require('./_compiler').default; 4 | -------------------------------------------------------------------------------- /tasks/react-native-stub.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'babel-core/register'; 3 | import 'react-native-mock/mock'; 4 | import 'isomorphic-fetch'; 5 | 6 | import React from 'react'; 7 | import ReactNative from 'react-native'; 8 | // MOCKS 9 | 10 | ReactNative.ListView = React.createClass({ 11 | statics: { 12 | DataSource: () => {}, 13 | }, 14 | 15 | render() { 16 | return null; 17 | }, 18 | }); 19 | ReactNative.Navigator.SceneConfigs = { 20 | HorizontalSwipeJump: {}, 21 | }; 22 | ReactNative.NativeModules = { 23 | ...ReactNative.NativeModules, 24 | }; 25 | 26 | var TextInputState = {}; 27 | var NavigatorNavigationBarStyles = { 28 | General: {}, 29 | }; 30 | 31 | // End MOCKS 32 | 33 | export default function(request, parent, isMain) { 34 | switch (request) { 35 | case 'TextInputState': 36 | return TextInputState; 37 | case 'NavigatorNavigationBarStylesIOS': 38 | return NavigatorNavigationBarStyles; 39 | case 'NavigatorNavigationBarStylesAndroid': 40 | return NavigatorNavigationBarStyles; 41 | case 'ReactNativeART': 42 | return {}; 43 | case 'Platform': 44 | return {}; 45 | case 'ErrorUtils': 46 | return {}; 47 | case 'Portal': 48 | return {}; 49 | case 'React': 50 | return {}; 51 | case 'parseErrorStack': 52 | return {}; 53 | default: 54 | if (React[request]) { 55 | return React[request]; 56 | } 57 | if (/react-native-/.test(request)) { 58 | return {}; 59 | } 60 | } 61 | 62 | // other issues 63 | if (request.match(/.*\.png/g)) { 64 | return 'png'; 65 | } 66 | 67 | return null; 68 | } 69 | -------------------------------------------------------------------------------- /tasks/translation-en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "fail": { 3 | "lower": [ 4 | "organiz", 5 | "neighbor", 6 | "zipcode", 7 | "zip code", 8 | "postal code", 9 | "furniture assembly", 10 | "apartment", 11 | "pantry", 12 | "dmv", 13 | "take-out", 14 | "crisper", 15 | "stand in line", 16 | "wait in line", 17 | "canceled", 18 | "revolutioniz", 19 | "categoriz", 20 | "organiz", 21 | "holiday help", 22 | "vacuum", 23 | "routing number", 24 | "www.taskrabbit.com", 25 | "@taskrabbit.com", 26 | "last name", 27 | "behavior", 28 | "favorite", 29 | "social security", 30 | "$" 31 | ], 32 | "sensitive": [ 33 | "SSN" 34 | ] 35 | }, 36 | "replace": [ 37 | ["rganiz", "rganis"], 38 | ["eighbor", "eighbour"], 39 | ["evolutioniz", "evolutionis"], 40 | ["ategoriz", "ategoris"], 41 | ["vacuum", "hoover"], 42 | ["Vacuum", "Hoover"], 43 | ["Assemble Furniture", "Assemble Flat-pack"], 44 | ["assemble furniture", "assemble flat-pack"], 45 | ["Assemble furniture", "Assemble flat-pack"], 46 | ["Disassemble Furniture", "Disassemble Flat-pack"], 47 | ["disassemble furniture", "disassemble flat-pack"], 48 | ["Disassemble furniture", "Disassemble flat-pack"], 49 | ["Furniture assembly", "Flat-pack assembly"], 50 | ["Furniture Assembly", "Flat-pack Assembly"], 51 | ["furniture assembly", "flat-pack assembly"], 52 | ["Pantry", "Cupboard"], 53 | ["pantry", "cupboard"], 54 | ["sun-porch", "patio"], 55 | ["Sun-porch", "Patio"], 56 | ["DMV", "post office"], 57 | ["Wait in line", "Queue in line"], 58 | ["Wait In Line", "Queue In Line"], 59 | ["wait in line", "queue in line"], 60 | ["Wait in Line", "Queue in Line"], 61 | ["Stand in Line", "Queue in Line"], 62 | ["Stand in line", "Queue in line"], 63 | ["Stand In Line", "Queue In Line"], 64 | ["stand in line", "queue in line"], 65 | ["Apartment", "Flat"], 66 | ["an apartment", "a flat"], 67 | ["apartment", "flat"], 68 | ["crisper", "salad"], 69 | ["Crisper", "Salad"], 70 | ["Yard", "Garden"], 71 | ["yard", "garden"], 72 | ["Take-out", "Takeaway"], 73 | ["take-out", "takeaway"], 74 | ["avorite", "avourite"], 75 | ["Postal code", "Postcode"], 76 | ["Postal Code", "Postcode"], 77 | ["postal code", "postcode"], 78 | ["Zip code", "Postcode"], 79 | ["Zip Code", "Postcode"], 80 | ["zip code", "postcode"], 81 | ["anceled", "ancelled"], 82 | ["Holiday Help", "Christmas Help"], 83 | ["holiday help", "Christmas help"], 84 | ["Social Security Number", "identification"], 85 | ["burner", "hob"], 86 | ["routing number", "sort code"], 87 | ["Routing Number", "Sort Code"], 88 | ["Routing number", "Sort code"], 89 | ["www.taskrabbit.com", "www.taskrabbit.co.uk"], 90 | ["@taskrabbit.com", "@taskrabbit.co.uk"], 91 | ["ehavior", "ehaviour"], 92 | ["last name", "surname"], 93 | ["Last name", "Surname"], 94 | ["Last Name", "Surname"], 95 | ["$", "£"] 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /tasks/translation-en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "fail": { 3 | "lower": [ 4 | "organis", 5 | "neighbour", 6 | "postcode", 7 | "post code", 8 | "postal code", 9 | "takeaway", 10 | "cancelled", 11 | "queue in line", 12 | "revolutionis", 13 | "categoris", 14 | "christmas help", 15 | "hoover", 16 | "lorry", 17 | "behaviour", 18 | "favourite", 19 | "surname", 20 | "sort code", 21 | "www.taskrabbit.co.uk", 22 | "taskrabbit.co.uk", 23 | "£" 24 | ], 25 | "sensitive": [ 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tasks/translation.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | // This will override `node_modules` ignoring - you can alternatively pass 3 | // an array of strings to be explicitly matched or a regex / glob 4 | ignore: /node_modules/, 5 | }); 6 | require('./_translation'); 7 | -------------------------------------------------------------------------------- /test/helpers/bootstrap.js: -------------------------------------------------------------------------------- 1 | var server = require("./server"); 2 | var dispatcher = require("../helpers/dispatcher"); 3 | var assign = require('object-assign'); 4 | 5 | var Bootstrap = function() { 6 | this.actions = []; 7 | }; 8 | 9 | Bootstrap.prototype.login = function() { 10 | this.addAction("LOGIN_USER", dispatcher.login()); 11 | return this; 12 | }; 13 | 14 | Bootstrap.prototype.time = function(time, timezoneOffsetHours) { 15 | this.addAction("STUB_TIME", {time: time, timezoneOffset: -(timezoneOffsetHours * 60)}); 16 | return this; 17 | }; 18 | 19 | Bootstrap.prototype.nav = function(path) { 20 | this.addAction("LAUNCH_ROUTE_PATH", {routePath: path}); 21 | return this; 22 | }; 23 | 24 | Bootstrap.prototype.show = function(componentName, props, state) { 25 | var defaultProps = { 26 | currentRoute: {} 27 | }; 28 | 29 | this.addAction("TEST_COMPONENT_ROUTE", { 30 | routeUnderTest: { 31 | component: componentName, 32 | passProps: assign(defaultProps, props), 33 | setState: state 34 | } 35 | }); 36 | return this; 37 | }; 38 | 39 | Bootstrap.prototype.addAction = function(type, options) { 40 | this.actions.push({type: type, options: options}); 41 | return this; 42 | }; 43 | 44 | Bootstrap.prototype.launch = function* (driver, options) { 45 | if (options && options.debug === true) { 46 | yield this.debug(driver); 47 | } 48 | 49 | var url = 'test/bootstrap.json'; 50 | 51 | for (var i=0; i 0) { 54 | yield driver.sleep(50); 55 | } 56 | server.get(url, { actions: [action] }); 57 | yield driver.elementById('Bootstrap').click(); 58 | } 59 | return this; 60 | }; 61 | 62 | Bootstrap.prototype.debug = function* (driver) { 63 | console.log('Bootstrap debugging'); 64 | 65 | for (var i=0; i<50; i++) { 66 | // Open the menu 67 | try { 68 | yield driver.elementById('DevMenu').click(); 69 | break; 70 | } 71 | catch (e) { 72 | // wait for a bit 73 | yield driver.sleep(500); 74 | } 75 | } 76 | 77 | for (var i=0; i<50; i++) { 78 | // Click on the Menu 79 | // If Debug in Chrome is not displayed then Disable Chrome Debugging should be 80 | try { 81 | try { 82 | yield driver.elementById('Debug in Chrome').click(); 83 | } 84 | catch (e) { 85 | yield driver.elementById('Disable Chrome Debugging'); 86 | } 87 | break; 88 | } 89 | catch (e) { 90 | // wait for a bit 91 | yield driver.sleep(500); 92 | } 93 | } 94 | 95 | for (var i=0; i<50; i++) { 96 | // wait til it reloads 97 | try { 98 | yield driver.elementById('ResetTest'); 99 | break; 100 | } 101 | catch (e) { 102 | // wait for a bit 103 | yield driver.sleep(500); 104 | } 105 | } 106 | }; 107 | 108 | module.exports = function() { 109 | return new Bootstrap(); 110 | }; 111 | -------------------------------------------------------------------------------- /test/helpers/dispatcher.js: -------------------------------------------------------------------------------- 1 | 2 | var Dispatcher = { 3 | login: function() { 4 | return { 5 | token: "fe37a156494cae1f2761ab2845e6cc3446010267", 6 | userProps: { 7 | id: 123, 8 | username: "tester" 9 | } 10 | }; 11 | }, 12 | }; 13 | 14 | module.exports = Dispatcher; 15 | -------------------------------------------------------------------------------- /test/helpers/driver.js: -------------------------------------------------------------------------------- 1 | require('./packager'); 2 | 3 | // APPIUM ----------------- 4 | var child_process = require('child_process'); 5 | var appiumProc = child_process.spawn('appium', [ 6 | '-p', '4724', 7 | '--default-capabilities', '{"fullReset":true}' 8 | ]); 9 | 10 | var Promise = require('Promise'); 11 | 12 | var server = { 13 | host: 'localhost', 14 | port: 4724 // one off from normal 15 | }; 16 | 17 | // { 18 | // host: 'ondemand.saucelabs.com', 19 | // port: 80, 20 | // username: process.env.SAUCE_USERNAME, 21 | // password: process.env.SAUCE_ACCESS_KEY 22 | // }; 23 | 24 | var loadedAppium = null; 25 | 26 | var appiumPromise = new Promise(function (resolve, reject) { 27 | appiumProc.stdout.on('data', function (data) { 28 | if (loadedAppium) return; 29 | console.log('APPIUM: ' + data); 30 | 31 | if (data.indexOf('Appium REST http interface listener started') >= 0) { 32 | loadedAppium = true; 33 | resolve(data); 34 | } 35 | }); 36 | }); 37 | 38 | appiumProc.stderr.on('data', function (data) { 39 | console.log('APPIUM err: ' + data); 40 | appiumProc.kill(); 41 | }); 42 | process.on('exit', function () { 43 | appiumProc.kill(); 44 | }); 45 | 46 | // WD ----------------- 47 | 48 | var realWd = require("wd"); 49 | var wd = require("yiewd"); 50 | var color = require('colors'); 51 | 52 | // KOA ----------------- 53 | 54 | var localServer = require("./server"); 55 | 56 | // Config for Appium 57 | 58 | var UNLIMITED = 100000; 59 | 60 | var caps = { 61 | browserName: '', 62 | 'appium-version': '1.5.1', 63 | platformName: 'iOS', 64 | platformVersion: '9.3', 65 | deviceName: 'iPhone 6s', 66 | autoLaunch: 'true', 67 | newCommandTimeout: UNLIMITED, 68 | app: process.cwd() + "/testbuild/test_ios/sample_ios.zip" 69 | }; 70 | 71 | module.exports = function(callback) { 72 | console.log("DRIVER: starting it up"); 73 | 74 | appiumPromise.then(function () { 75 | console.log("DRIVER: will init"); 76 | driver = wd.remote(server); 77 | 78 | driver.on('status', function(info) { 79 | console.log(info.cyan); 80 | }); 81 | driver.on('command', function(meth, path, data) { 82 | console.log(' > ' + meth.yellow, path.grey, data || ''); 83 | }); 84 | 85 | current = {}; 86 | 87 | handler = function(error, el){ 88 | if (error) { 89 | console.log('error', error); 90 | } 91 | else if(typeof el === 'object'){ 92 | console.log("Returned in current"); 93 | current = el; 94 | } 95 | else { 96 | console.log("Returned following string", el); 97 | } 98 | 99 | }; 100 | 101 | quit = function(){ 102 | driver.quit(function(){ 103 | process.exit(1); 104 | }); 105 | }; 106 | 107 | driver.init(caps, function(){ 108 | console.log('driver started'); 109 | callback({ 110 | driver: driver, 111 | realWd: realWd, 112 | localServer: localServer, 113 | wd: wd, 114 | }); 115 | }); 116 | }); 117 | }; 118 | -------------------------------------------------------------------------------- /test/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | // API Responses 2 | 3 | var Fixtures = { 4 | signup: function() { 5 | return { 6 | "id": 123, 7 | "token": "d3f4g5h67j8", 8 | "username": "tester", 9 | }; 10 | }, 11 | 12 | home: function() { 13 | return { 14 | username: 'tester', 15 | posts: [ 16 | { id: 1, content: "post1", username: "tester" }, 17 | { id: 2, content: "post2", username: "tester" } 18 | ] 19 | }; 20 | }, 21 | 22 | myFollows: function() { 23 | return { 24 | username: 'tester', 25 | follows: [ 26 | { id: 42, username: "friend" }, 27 | { id: 2, username: "follow2" } 28 | ] 29 | }; 30 | }, 31 | 32 | friend: function() { 33 | return { 34 | username: 'friend', 35 | posts: [ 36 | { id: 3, content: "post3", username: "friend" }, 37 | { id: 4, content: "post4", username: "friend" } 38 | ] 39 | }; 40 | }, 41 | 42 | friendFollows: function() { 43 | return { 44 | username: 'friend', 45 | follows: [ 46 | { id: 123, username: "tester" }, 47 | { id: 4, username: "follow4" } 48 | ] 49 | }; 50 | }, 51 | 52 | error: function(message, status, key, code) { 53 | return { 54 | status: (status ? status : 422), 55 | body: { 56 | error: message 57 | } 58 | 59 | }; 60 | } 61 | 62 | }; 63 | 64 | module.exports = Fixtures; 65 | -------------------------------------------------------------------------------- /test/helpers/packager.js: -------------------------------------------------------------------------------- 1 | // ios/node_modules/react-native/packager/packager.sh --port 9091 2 | // node /Users/brian/taskrabbit/tasker/ios/node_modules/react-native/packager/packager.js --port 9091 3 | 4 | var pwd = process.cwd(); 5 | var path = pwd + '/node_modules/react-native/packager/packager.sh' 6 | 7 | var child_process = require('child_process'); 8 | var packager = child_process.spawn(path, ['--port', '9091']); 9 | 10 | var packagerReady = false; 11 | 12 | packager.stdout.on('data', function (data) { 13 | if (packagerReady) return; 14 | console.log('PACKAGER: ' + data); 15 | if (data.indexOf('React packager ready.') >=0) { 16 | packagerReady = true; 17 | } 18 | }); 19 | 20 | packager.stderr.on('data', function (data) { 21 | console.log('PACKAGER err: ' + data); 22 | }); 23 | 24 | packager.on('close', function (code) { 25 | console.log('PACKAGER exited with code: ' + code); 26 | }); 27 | 28 | 29 | process.on('exit', function () { 30 | packager.kill(); 31 | }); 32 | 33 | module.exports = packager; 34 | -------------------------------------------------------------------------------- /test/helpers/server.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | 3 | var _ = require('underscore'); 4 | var colors = require('colors'); 5 | var koa = require('koa'); 6 | var bodyParser = require('koa-bodyparser'); 7 | 8 | var app = koa(); 9 | 10 | app.use(bodyParser()); 11 | 12 | var mockRoutes = {}; 13 | var MockApp = { 14 | before: function() { 15 | // before each test 16 | mockRoutes = {}; 17 | }, 18 | after: function() { 19 | // after each test 20 | mockRoutes = {}; 21 | }, 22 | get: function(path, response, params = null) { 23 | this.stub('GET', path, response, params); 24 | }, 25 | post: function(path, response, params = null) { 26 | this.stub('POST', path, response, params); 27 | }, 28 | put: function(path, response, params = null) { 29 | this.stub('PUT', path, response, params); 30 | }, 31 | delete: function(path, response, params = null) { 32 | this.stub('DELETE', path, response, params); 33 | }, 34 | stub: function(method, path, response, params = null) { 35 | if (path[0]!=='/') path = '/' + path; 36 | console.log('KOA stubbing: %s %s'.magenta, method.blue, path.green); 37 | if (typeof response === 'string') { 38 | response = {body: response, params: params}; 39 | } 40 | if (!response.body) { 41 | // assume it's a json object 42 | response = {body: response, params: params}; 43 | } 44 | response.params = params; 45 | mockRoutes[method + ':' + path] = response; 46 | }, 47 | pageNotFound: function(req) { 48 | console.log('KOA ERROR: URL not registered: %s %s'.red, req.method.blue, req.url.green); 49 | 50 | req.status = 404; 51 | 52 | switch (req.accepts('html', 'json')) { 53 | case 'html': 54 | req.type = 'html'; 55 | req.body = '

Test Page Not Found

'; 56 | break; 57 | case 'json': 58 | // TODO: error message json format from v3? 59 | req.body = { 60 | message: 'Test Page Not Found' 61 | }; 62 | break; 63 | default: 64 | req.type = 'text'; 65 | req.body = 'Test Page Not Found'; 66 | } 67 | }, 68 | process: function(req) { 69 | var key = req.method + ':' + req.url; 70 | var found = mockRoutes[key]; 71 | 72 | if (found) { 73 | if (found.params) { 74 | if (!_.isEqual(req.request.body, found.params)) { 75 | console.log('KOA: %s %s received with wrong params'.red, req.method.blue, req.url.green); 76 | console.log('expected: %s'.red, JSON.stringify(found.params)); 77 | console.log('received: %s'.red, JSON.stringify(req.request.body)); 78 | 79 | return this.pageNotFound(req); 80 | } 81 | } 82 | if (found.body) req.body = found.body; 83 | if (found.status) req.status = found.status; 84 | 85 | console.log('KOA: %s %s'.red, req.method.blue, req.url.green); 86 | // console.log('==== %s', JSON.stringify(found.body).blue); 87 | } else { 88 | this.pageNotFound(req); 89 | } 90 | }, 91 | }; 92 | 93 | 94 | app.use(function *(){ 95 | // look up in current app 96 | if (this.path === '/test/console.json') { 97 | console[this.request.body.level](...["App Log:", ...this.request.body.arguments]); 98 | } 99 | else { 100 | MockApp.process(this); 101 | } 102 | }); 103 | 104 | app.listen(3001); 105 | 106 | module.exports = MockApp; 107 | -------------------------------------------------------------------------------- /test/integration/authentication.test.js: -------------------------------------------------------------------------------- 1 | import it from '../helpers/appium'; 2 | import server from '../helpers/server'; 3 | import fixtures from '../helpers/fixtures'; 4 | 5 | describe("Authentication", () => { 6 | beforeEach(() => { 7 | server.get("/api/posts/tester", fixtures.home()); 8 | }); 9 | 10 | it("should sign up the user and show dashboard", function* (driver, done) { 11 | server.post("api/signup", fixtures.signup()); 12 | 13 | var username = yield driver.elementById('Username'); 14 | var password = yield driver.elementById('Password'); 15 | var button = yield driver.elementById('Sign up'); 16 | yield username.setImmediateValue("tester"); 17 | yield password.setImmediateValue("sample"); 18 | yield button.click(); 19 | 20 | // make sure logged in 21 | yield driver.elementById('Dashboard'); 22 | yield driver.elementById('post1'); 23 | 24 | done(); 25 | }); 26 | 27 | it("should log in and out", function* (driver, done) { 28 | server.post("api/login", fixtures.signup()); 29 | 30 | yield driver.elementById('Already a user? Login here.').click(); 31 | 32 | var username = yield driver.elementById('Username'); 33 | var password = yield driver.elementById('Password'); 34 | var button = yield driver.elementById('Log in'); 35 | yield username.setImmediateValue("tester"); 36 | yield password.setImmediateValue("sample"); 37 | yield button.click(); 38 | 39 | // make sure logged in 40 | yield driver.elementById('Dashboard'); 41 | yield driver.elementById('post1'); 42 | 43 | // show settings, log out 44 | yield driver.elementByXPath('//UIAApplication[1]/UIAWindow[1]/UIAElement[4]').click(); // "Me" 45 | yield driver.elementById('Settings'); 46 | yield driver.elementById('Log out').click(); 47 | 48 | // back on signup 49 | yield driver.elementById('Already a user? Login here.'); 50 | 51 | done(); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/integration/follows.test.js: -------------------------------------------------------------------------------- 1 | import it from '../helpers/appium'; 2 | import server from '../helpers/server'; 3 | import fixtures from '../helpers/fixtures'; 4 | import bootstrap from '../helpers/bootstrap'; 5 | 6 | describe("Follows", () => { 7 | it("should show my follows", function* (driver, done) { 8 | server.get("/api/follows/tester", fixtures.myFollows()); 9 | 10 | yield bootstrap().login().nav("dashboard/follows").launch(driver); 11 | 12 | yield driver.elementById('Dashboard'); 13 | yield driver.elementById('friend'); 14 | yield driver.elementById('follow2'); 15 | 16 | done(); 17 | }); 18 | 19 | it("should show an empty list", function* (driver, done) { 20 | server.get("/api/follows/tester", { username: "tester", follows: [] }); 21 | 22 | yield bootstrap().login().nav("dashboard/follows").launch(driver); 23 | 24 | yield driver.elementById('Follows'); 25 | yield driver.elementById('No items'); 26 | 27 | done(); 28 | }); 29 | 30 | it("should my friends's follows", function* (driver, done) { 31 | server.get("/api/follows/friend", fixtures.friendFollows()); 32 | 33 | yield bootstrap().login().nav("dashboard/follows/friend/follows").launch(driver); 34 | 35 | yield driver.elementById('friend'); 36 | yield driver.elementById('tester'); 37 | yield driver.elementById('follow4'); 38 | 39 | done(); 40 | }); 41 | 42 | it("should navigate follows recursively", function* (driver, done) { 43 | server.get("/api/posts/tester", fixtures.home()); 44 | server.get("/api/posts/friend", fixtures.friend()); 45 | server.get("/api/posts/follow4", { 46 | username: "follow4", 47 | posts: [ { id: 1000, username: 'follow4', content: 'post1000' }] 48 | }); 49 | 50 | server.get("/api/follows/friend", fixtures.friendFollows()); 51 | server.get("/api/follows/tester", fixtures.myFollows()); 52 | server.get("/api/follows/follow4", { username: "follow4", follows: [ { id: 123, username: 'tester' }] }); 53 | 54 | yield bootstrap().login().launch(driver); 55 | 56 | yield driver.elementById("Dashboard"); 57 | yield driver.elementById("post1"); // my post 58 | // yield driver.sleep(90000000); 59 | yield driver.elementById("segFollows_tester").click(); // open my follows 60 | 61 | yield driver.elementById('friend').click(); // click on friend 62 | 63 | yield driver.elementById("friend"); // friend's list 64 | yield driver.elementById("post4"); // friend's post 65 | yield driver.elementById("segFollows_friend").click(); // open friend's follows 66 | 67 | yield driver.elementById('follow4').click(); // click on other 68 | 69 | yield driver.elementById("follow4"); // other's list 70 | yield driver.elementById("post100"); // other's post 71 | yield driver.elementById("segFollows_follow4").click(); // open other's follows 72 | 73 | yield driver.elementById('tester').click(); // click on me 74 | 75 | // TODO: it should NOT say "Dashboard" 76 | yield driver.elementById("tester"); // my list 77 | yield driver.elementById("post1"); // my post 78 | 79 | // Now pop everything 80 | yield driver.elementById("back").click(); // back 81 | yield driver.elementById("follow4"); 82 | 83 | yield driver.elementById("back").click(); // back 84 | yield driver.elementById("friend"); 85 | 86 | yield driver.elementById("back").click(); // back 87 | 88 | yield driver.elementById("Dashboard"); 89 | 90 | done(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/integration/posts.test.js: -------------------------------------------------------------------------------- 1 | import it, {itOnly} from '../helpers/appium'; 2 | import server from '../helpers/server'; 3 | import fixtures from '../helpers/fixtures'; 4 | import bootstrap from '../helpers/bootstrap'; 5 | 6 | describe("Posts", () => { 7 | it("should show my posts", function* (driver, done) { 8 | server.get("/api/posts/tester", fixtures.home()); 9 | 10 | yield bootstrap().login().nav("dashboard").launch(driver); 11 | 12 | yield driver.elementById('Dashboard'); 13 | yield driver.elementById('post1'); 14 | 15 | done(); 16 | }); 17 | 18 | it("should show an empty list", function* (driver, done) { 19 | server.get("/api/posts/tester", { username: "tester", posts: [] }); 20 | 21 | yield bootstrap().login().nav("dashboard").launch(driver); 22 | 23 | yield driver.elementById('Dashboard'); 24 | yield driver.elementById('No items'); 25 | 26 | done(); 27 | }); 28 | 29 | it("should my friends's posts", function* (driver, done) { 30 | server.get("/api/posts/friend", fixtures.friend()); 31 | 32 | yield bootstrap().login().nav("dashboard/follows/friend").launch(driver); 33 | 34 | yield driver.elementById('friend'); 35 | yield driver.elementById('post3'); 36 | 37 | done(); 38 | }); 39 | 40 | it("should create a new post", function* (driver, done) { 41 | var list = fixtures.home(); 42 | server.get("/api/posts/tester", list); 43 | server.post("/api/posts", 44 | {id: 100, content: 'new post here', username: 'tester'}, // return this content 45 | {content: 'new post here'} // expect this content 46 | ); 47 | 48 | yield bootstrap().login().launch(driver); 49 | 50 | yield driver.elementById('+').click(); // new post! 51 | 52 | yield driver.elementById('New Post'); 53 | 54 | yield driver.execute("target.frontMostApp().keyboard().typeString('new post here')"); 55 | 56 | yield driver.elementById('Submit').click(); 57 | 58 | yield driver.elementById('Dashboard'); 59 | 60 | yield driver.elementById('new post here'); // it's there! 61 | 62 | done(); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/integration/smoke.test.js: -------------------------------------------------------------------------------- 1 | import it from '../helpers/appium'; 2 | 3 | describe("integration smoke test", () => { 4 | it("should launch the simulator and compute the sum", function* (driver, done) { 5 | var value = 1 + 1; 6 | value.should.equal(2); 7 | done(); 8 | }); 9 | 10 | }); 11 | --------------------------------------------------------------------------------