├── .babelrc ├── .buckconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .watchmanconfig ├── App ├── Actions │ └── index.js ├── AppContainer.js ├── Components │ ├── AllRead.js │ ├── Button.js │ ├── ButtonBrowser.js │ ├── ErrorPage.js │ ├── Loading.js │ ├── Notification.js │ ├── RepositoryTitle.js │ ├── Setting.js │ ├── SettingUp.js │ └── Toolbar.js ├── Middleware │ └── Sound.js ├── Navigation │ ├── NavbarBackButton.js │ ├── NavbarButton.js │ ├── NavbarTitle.js │ ├── NavigationBar.js │ ├── RouteMapper.js │ ├── Routes.js │ └── SceneContainer.js ├── Reducers │ ├── Auth.js │ ├── Notifications.js │ ├── Search.js │ ├── Settings.js │ └── index.js ├── Routes │ ├── GithubView.js │ ├── LoginView.js │ ├── Notifications.js │ ├── OAuthView.js │ └── SettingsView.js ├── Store │ └── configureStore.js └── Utils │ ├── Constants.js │ └── Sound.js ├── LICENSE ├── README.md ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ └── fonts │ │ │ ├── Entypo.ttf │ │ │ ├── EvilIcons.ttf │ │ │ ├── Feather.ttf │ │ │ ├── FontAwesome.ttf │ │ │ ├── Foundation.ttf │ │ │ ├── Ionicons.ttf │ │ │ ├── MaterialCommunityIcons.ttf │ │ │ ├── MaterialIcons.ttf │ │ │ ├── Octicons.ttf │ │ │ ├── SimpleLineIcons.ttf │ │ │ └── Zocial.ttf │ │ ├── java │ │ └── com │ │ │ └── gitify │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.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 ├── app.json ├── images ├── logo.png └── press.png ├── index.js ├── ios ├── Gitify-tvOS │ └── Info.plist ├── Gitify-tvOSTests │ └── Info.plist ├── Gitify.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── Gitify-tvOS.xcscheme │ │ └── Gitify.xcscheme ├── Gitify │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── GitifyTests │ ├── GitifyTests.m │ └── Info.plist ├── package.json ├── scripts ├── emulator └── run ├── sounds └── digi.wav └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | node: true, 5 | es6: true, 6 | jest: true, 7 | jasmine: true, 8 | }, 9 | "extends": [ 10 | "dabapps/react", 11 | "dabapps/react-native", 12 | "dabapps/es6", 13 | ], 14 | "rules": { 15 | "react/jsx-indent-props": 0, 16 | "react/jsx-handler-names": 0, 17 | "react-native/no-inline-styles": 0, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.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/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | android/app/gitify.keystore 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - '6.12.2' 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | install: 13 | - yarn 14 | 15 | script: 16 | - yarn test 17 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /App/Actions/index.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | 3 | import { fromJS } from 'immutable'; 4 | 5 | // Settings 6 | export const APP_LOADED = 'APP_LOADED'; 7 | export function appLoaded() { 8 | return { 9 | type: APP_LOADED 10 | }; 11 | } 12 | 13 | export const UPDATE_SETTING = 'UPDATE_SETTING'; 14 | export function updateSetting(setting, value) { 15 | return { 16 | type: UPDATE_SETTING, 17 | setting: setting, 18 | value: value 19 | }; 20 | } 21 | 22 | 23 | // Auth 24 | 25 | export const FETCH_TOKEN_REQUEST = 'FETCH_TOKEN_REQUEST'; 26 | export const FETCH_TOKEN_SUCCESS = 'FETCH_TOKEN_SUCCESS'; 27 | export const FETCH_TOKEN_FAILURE = 'FETCH_TOKEN_FAILURE'; 28 | 29 | export function fetchTokenRequest() { 30 | return { 31 | type: FETCH_TOKEN_REQUEST 32 | }; 33 | } 34 | 35 | export function fetchTokenSuccess(payload) { 36 | return { 37 | type: FETCH_TOKEN_SUCCESS, 38 | payload 39 | }; 40 | } 41 | 42 | export function fetchTokenFailure() { 43 | return { 44 | type: FETCH_TOKEN_FAILURE 45 | }; 46 | } 47 | 48 | export function fetchToken(data) { 49 | return (dispatch) => { 50 | dispatch(fetchTokenRequest()); 51 | 52 | return fetch('https://github.com/login/oauth/access_token', { 53 | method: 'POST', 54 | headers: { 55 | 'Accept': 'application/json', 56 | 'Content-Type': 'application/json', 57 | 'Cache-Control': 'no-cache' 58 | }, 59 | body: JSON.stringify(data) 60 | }) 61 | .then(response => { 62 | if (!response.ok) { 63 | throw Error(response.statusText); 64 | } 65 | return response.json(); 66 | }) 67 | .then(json => { 68 | dispatch(fetchTokenSuccess(json)); 69 | }) 70 | .catch(() => { 71 | dispatch(fetchTokenFailure()); 72 | }); 73 | }; 74 | } 75 | 76 | export const LOGOUT = 'LOGOUT'; 77 | export function logout() { 78 | return { 79 | type: LOGOUT 80 | }; 81 | } 82 | 83 | 84 | // Notifications 85 | 86 | export const FETCH_NOTIFICATIONS_REQUEST = 'FETCH_NOTIFICATIONS_REQUEST'; 87 | export const FETCH_NOTIFICATIONS_SUCCESS = 'FETCH_NOTIFICATIONS_SUCCESS'; 88 | export const FETCH_NOTIFICATIONS_FAILURE = 'FETCH_NOTIFICATIONS_FAILURE'; 89 | 90 | export function fetchNotificationsRequest(isReFetching) { 91 | return { 92 | type: FETCH_NOTIFICATIONS_REQUEST, 93 | meta: { 94 | isReFetching 95 | } 96 | }; 97 | } 98 | 99 | export function fetchNotificationsSuccess(payload) { 100 | return { 101 | type: FETCH_NOTIFICATIONS_SUCCESS, 102 | payload 103 | }; 104 | } 105 | 106 | export function fetchNotificationsFailure() { 107 | return { 108 | type: FETCH_NOTIFICATIONS_FAILURE 109 | }; 110 | } 111 | 112 | export function fetchNotifications(isReFetching = false) { 113 | return (dispatch, getState) => { 114 | dispatch(fetchNotificationsRequest(isReFetching)); 115 | const token = 'token ' + getState().auth.get('token'); 116 | const isParticipating = getState().settings.get('participating') ? 'true' : 'false'; 117 | 118 | return fetch(`https://api.github.com/notifications?participating=${isParticipating}`, { 119 | method: 'GET', 120 | headers: { 121 | 'Accept': 'application/json', 122 | 'Authorization': token, 123 | 'Content-Type': 'application/json', 124 | 'Cache-Control': 'no-cache' 125 | }, 126 | }) 127 | .then(response => { 128 | if (!response.ok) { 129 | throw Error(response.statusText); 130 | } 131 | return response.json(); 132 | }) 133 | .then(json => { 134 | dispatch(fetchNotificationsSuccess(fromJS(json))); 135 | }) 136 | .catch(() => { 137 | dispatch(fetchNotificationsFailure()); 138 | }); 139 | }; 140 | } 141 | 142 | 143 | // Single Notification 144 | 145 | export const MARK_NOTIFICATION_REQUEST = 'MARK_NOTIFICATION_REQUEST'; 146 | export const MARK_NOTIFICATION_SUCCESS = 'MARK_NOTIFICATION_SUCCESS'; 147 | export const MARK_NOTIFICATION_FAILURE = 'MARK_NOTIFICATION_FAILURE'; 148 | 149 | export function markNotificationRequest() { 150 | return { 151 | type: MARK_NOTIFICATION_REQUEST 152 | }; 153 | } 154 | 155 | export function markNotificationSuccess(id) { 156 | return { 157 | type: MARK_NOTIFICATION_SUCCESS, 158 | id 159 | }; 160 | } 161 | 162 | export function markNotificationFailure() { 163 | return { 164 | type: MARK_NOTIFICATION_FAILURE 165 | }; 166 | } 167 | 168 | export function markNotification(id) { 169 | return (dispatch, getState) => { 170 | dispatch(markNotificationRequest()); 171 | const token = 'token ' + getState().auth.get('token'); 172 | return fetch(`https://api.github.com/notifications/threads/${id}`, { 173 | method: 'GET', 174 | headers: { 175 | 'Accept': 'application/json', 176 | 'Authorization': token, 177 | 'Content-Type': 'application/json', 178 | 'Cache-Control': 'no-cache' 179 | }, 180 | }) 181 | .then(response => { 182 | if (!response.ok) { 183 | throw Error(response.statusText); 184 | } 185 | return response.json(); 186 | }) 187 | .then(json => { 188 | dispatch(markNotificationSuccess(json.id)); 189 | }) 190 | .catch(() => { 191 | dispatch(markNotificationFailure()); 192 | }); 193 | }; 194 | } 195 | 196 | 197 | // Repo's Notification 198 | 199 | export const MARK_REPO_NOTIFICATION_REQUEST = 'MARK_REPO_NOTIFICATION_REQUEST'; 200 | export const MARK_REPO_NOTIFICATION_SUCCESS = 'MARK_REPO_NOTIFICATION_SUCCESS'; 201 | export const MARK_REPO_NOTIFICATION_FAILURE = 'MARK_REPO_NOTIFICATION_FAILURE'; 202 | 203 | 204 | export function markRepoNotificationsRequest() { 205 | return { 206 | type: MARK_REPO_NOTIFICATION_REQUEST 207 | }; 208 | } 209 | 210 | export function markRepoNotificationsSuccess(repoFullName) { 211 | return { 212 | type: MARK_REPO_NOTIFICATION_SUCCESS, 213 | repoFullName 214 | }; 215 | } 216 | 217 | export function markRepoNotificationsFailure() { 218 | return { 219 | type: MARK_REPO_NOTIFICATION_FAILURE 220 | }; 221 | } 222 | 223 | export function markRepoNotifications(loginId, repoId, repoFullName) { 224 | return (dispatch, getState) => { 225 | dispatch(markRepoNotificationsRequest()); 226 | const token = 'token ' + getState().auth.get('token'); 227 | return fetch(`https://api.github.com/repos/${loginId}/${repoId}/notifications`, { 228 | method: 'PUT', 229 | headers: { 230 | 'Accept': 'application/json', 231 | 'Authorization': token, 232 | 'Content-Type': 'application/json' 233 | }, 234 | body: JSON.stringify({}) 235 | }) 236 | .then(response => { 237 | if (!response.ok) { 238 | throw Error(response.statusText); 239 | } 240 | dispatch(markRepoNotificationsSuccess(repoFullName)); 241 | }) 242 | .catch(() => { 243 | dispatch(markRepoNotificationsFailure()); 244 | }); 245 | }; 246 | } 247 | 248 | 249 | // Search 250 | 251 | export const SEARCH_NOTIFICATIONS = 'SEARCH_NOTIFICATIONS'; 252 | export function searchNotifications(query) { 253 | return { 254 | type: SEARCH_NOTIFICATIONS, 255 | query: query 256 | }; 257 | } 258 | 259 | export const CLEAR_SEARCH = 'CLEAR_SEARCH'; 260 | export function clearSearch() { 261 | return { 262 | type: CLEAR_SEARCH 263 | }; 264 | } 265 | -------------------------------------------------------------------------------- /App/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { 6 | BackHandler, 7 | StyleSheet 8 | } from 'react-native'; 9 | 10 | import { Navigator } from 'react-native-deprecated-custom-components'; 11 | 12 | import Constants from './Utils/Constants'; 13 | import NavigationBar from './Navigation/NavigationBar'; 14 | import SceneContainer from './Navigation/SceneContainer'; 15 | import SettingUp from './Components/SettingUp'; 16 | import RouteMapper from './Navigation/RouteMapper'; 17 | import Routes from './Navigation/Routes'; 18 | 19 | const styles = StyleSheet.create({ 20 | navbar: { 21 | backgroundColor: Constants.NAVBAR_BG, 22 | flexDirection: 'row', 23 | justifyContent: 'center', 24 | } 25 | }); 26 | 27 | class AppContainer extends React.Component { 28 | static propTypes = { 29 | isLoggedIn: PropTypes.bool.isRequired, 30 | loaded: PropTypes.bool.isRequired, 31 | }; 32 | 33 | constructor(props) { 34 | super(props); 35 | 36 | this.navigator; 37 | } 38 | 39 | componentWillMount() { 40 | BackHandler.addEventListener('hardwareBackPress', () => { 41 | if (this.navigator && this.navigator.getCurrentRoutes().length > 1) { 42 | this.navigator.pop(); 43 | return true; 44 | } 45 | return false; 46 | }); 47 | } 48 | 49 | renderScene(route, navigator) { 50 | this.navigator = navigator; 51 | 52 | return ( 53 | { 58 | if (route.index > 0) { 59 | navigator.pop(); 60 | } 61 | }} 62 | {...this.props} 63 | /> 64 | ); 65 | } 66 | 67 | _getInitialRoute() { 68 | if (this.props.isLoggedIn) { 69 | return Routes.Notifications(); 70 | } 71 | return Routes.LoginView(); 72 | } 73 | 74 | render() { 75 | if (!this.props.loaded) { 76 | return ; 77 | } 78 | 79 | const initialRoute = this._getInitialRoute(); 80 | return ( 81 | } 85 | /> 86 | ); 87 | } 88 | } 89 | 90 | function mapStateToProps(state) { 91 | return { 92 | loaded: state.settings.get('loaded', false), 93 | isLoggedIn: state.auth.get('token') !== null 94 | }; 95 | } 96 | 97 | export default connect(mapStateToProps, null)(AppContainer); 98 | -------------------------------------------------------------------------------- /App/Components/AllRead.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Constants from '../Utils/Constants'; 6 | 7 | import { 8 | StyleSheet, 9 | Text, 10 | TouchableHighlight, 11 | View 12 | } from 'react-native'; 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | justifyContent: 'center' 18 | }, 19 | wrapper: { 20 | flex: 1, 21 | alignItems: 'center', 22 | justifyContent: 'center' 23 | }, 24 | button: { 25 | alignSelf: 'center', 26 | paddingVertical: 5, 27 | paddingHorizontal: 10, 28 | marginTop: 20, 29 | borderRadius: 5, 30 | borderWidth: 1, 31 | }, 32 | footerWrapper: { 33 | padding: 20 34 | }, 35 | heading: { 36 | fontSize: 28, 37 | fontWeight: '300', 38 | marginBottom: 5, 39 | }, 40 | subheading: { 41 | fontSize: 18 42 | }, 43 | emoji: { 44 | fontSize: 44, 45 | }, 46 | hintTitle: { 47 | fontSize: 14, 48 | fontWeight: '500', 49 | marginTop: 50, 50 | marginBottom: 10, 51 | textAlign: 'center' 52 | }, 53 | hint: { 54 | marginHorizontal: 30, 55 | fontSize: 13, 56 | textAlign: 'center' 57 | } 58 | }); 59 | 60 | export default class AllRead extends React.Component { 61 | static propTypes = { 62 | onReload: PropTypes.func.isRequired 63 | }; 64 | 65 | render() { 66 | const message = _.sample(Constants.ALLREAD_MESSAGES); 67 | const emoji = _.sample(Constants.ALLREAD_EMOJIS); 68 | const hint = _.sample(Constants.HINTS); 69 | 70 | return ( 71 | 72 | 73 | {message} 74 | No new notifications. 75 | {emoji} 76 | this.props.onReload()} 80 | > 81 | Reload 82 | 83 | 84 | 85 | 86 | Hint 87 | {hint} 88 | 89 | 90 | ); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /App/Components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Constants from '../Utils/Constants'; 5 | 6 | import { 7 | StyleSheet, 8 | Text, 9 | TouchableHighlight 10 | } from 'react-native'; 11 | 12 | let styles = StyleSheet.create({ 13 | button: { 14 | borderRadius: Constants.BASE_BORDER_RADIUS, 15 | borderWidth: 1.5, 16 | borderColor: Constants.THEME_PRIMARY, 17 | paddingHorizontal: 15, 18 | paddingVertical: 12, 19 | marginVertical: 5 20 | }, 21 | buttonText: { 22 | color: Constants.THEME_PRIMARY, 23 | fontSize: Constants.BUTTON_FONT_SIZE, 24 | textAlign: 'center', 25 | fontWeight: 'bold' 26 | } 27 | }); 28 | 29 | export default class Button extends React.Component { 30 | 31 | static propTypes = { 32 | text: PropTypes.string.isRequired, 33 | onPress: PropTypes.func.isRequired, 34 | disabled: PropTypes.bool, 35 | style: PropTypes.object 36 | }; 37 | 38 | static defaultProps = { 39 | style: {} 40 | }; 41 | 42 | onPress() { 43 | if (this.props.disabled) { 44 | return; 45 | } 46 | this.props.onPress(); 47 | } 48 | 49 | render() { 50 | 51 | return ( 52 | 58 | {this.props.text} 59 | 60 | ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /App/Components/ButtonBrowser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/FontAwesome'; 4 | 5 | import Constants from '../Utils/Constants'; 6 | 7 | import { 8 | StyleSheet, 9 | TouchableHighlight 10 | } from 'react-native'; 11 | 12 | let styles = StyleSheet.create({ 13 | button: { 14 | marginHorizontal: 5, 15 | paddingHorizontal: 10, 16 | paddingVertical: 10, 17 | }, 18 | icon: { 19 | fontSize: 24, 20 | color: '#FFF' 21 | }, 22 | disabledIcon: { // eslint-disable-line react-native/no-unused-styles 23 | color: 'rgba(255, 255, 255, .5)' 24 | } 25 | }); 26 | 27 | export default class ButtonBrowser extends React.Component { 28 | 29 | static propTypes = { 30 | icon: PropTypes.string.isRequired, 31 | onPress: PropTypes.func.isRequired, 32 | disabled: PropTypes.bool, 33 | style: PropTypes.object.isRequired, 34 | }; 35 | 36 | static defaultProps = { 37 | style: {} 38 | }; 39 | 40 | onPress() { 41 | if (this.props.disabled) { 42 | return; 43 | } 44 | this.props.onPress(); 45 | } 46 | 47 | render() { 48 | const iconStyles = this.props.disabled ? styles.disabledIcon : {}; 49 | 50 | return ( 51 | this.onPress()} 56 | > 57 | 58 | 59 | ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /App/Components/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Constants from '../Utils/Constants'; 6 | 7 | import { 8 | StyleSheet, 9 | Text, 10 | TouchableHighlight, 11 | View 12 | } from 'react-native'; 13 | 14 | const styles = StyleSheet.create({ 15 | containerWrapper: { 16 | flex: 1, 17 | }, 18 | container: { 19 | flex: 1, 20 | justifyContent: 'center' 21 | }, 22 | wrapper: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center' 26 | }, 27 | button: { 28 | alignSelf: 'center', 29 | paddingVertical: 5, 30 | paddingHorizontal: 10, 31 | marginTop: 20, 32 | borderRadius: 5, 33 | borderWidth: 1, 34 | }, 35 | footerWrapper: { 36 | padding: 20 37 | }, 38 | heading: { 39 | fontSize: 28, 40 | fontWeight: '300', 41 | marginBottom: 5, 42 | }, 43 | subheading: { 44 | fontSize: 18 45 | }, 46 | emoji: { 47 | fontSize: 44, 48 | }, 49 | hintTitle: { 50 | fontSize: 14, 51 | fontWeight: '500', 52 | marginTop: 50, 53 | marginBottom: 10, 54 | textAlign: 'center' 55 | }, 56 | hint: { 57 | marginHorizontal: 30, 58 | fontSize: 13, 59 | textAlign: 'center' 60 | } 61 | }); 62 | 63 | export default class ErrorPage extends React.Component { 64 | static propTypes = { 65 | onReload: PropTypes.func.isRequired, 66 | subheading: PropTypes.string.isRequired 67 | }; 68 | 69 | render() { 70 | const emoji = _.sample(Constants.ERROR_EMOJIS); 71 | const hint = _.sample(Constants.HINTS); 72 | 73 | return ( 74 | 78 | 79 | Oops something went wrong. 80 | {this.props.subheading} 81 | {emoji} 82 | this.props.onReload()} 86 | > 87 | Reload 88 | 89 | 90 | 91 | 92 | Hint 93 | {hint} 94 | 95 | 96 | ); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /App/Components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import PropTypes from 'prop-types'; 3 | 4 | import Constants from '../Utils/Constants'; 5 | 6 | import { 7 | ActivityIndicator, 8 | StyleSheet, 9 | Text, 10 | View, 11 | } from 'react-native'; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | position: 'absolute', 17 | top: 0, 18 | left: 0, 19 | right: 0, 20 | bottom: 0, 21 | backgroundColor: Constants.BG_COLOR, 22 | alignItems: 'center', 23 | justifyContent: 'center' 24 | }, 25 | loadingText: { 26 | fontSize: 20, 27 | margin: 15 28 | } 29 | }); 30 | 31 | const Loading = props => { 32 | if (!props.isLoading) { 33 | return null; 34 | } 35 | 36 | const text = props.text && ' ' + props.text; 37 | 38 | return ( 39 | 40 | 41 | {!props.hideText && ( 42 | Loading{text} 43 | )} 44 | 45 | ); 46 | }; 47 | 48 | Loading.propTypes = { 49 | isLoading: PropTypes.bool.isRequired, 50 | hideText: PropTypes.bool, 51 | text: PropTypes.string, 52 | style: PropTypes.object, 53 | }; 54 | 55 | export default Loading; 56 | -------------------------------------------------------------------------------- /App/Components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Icon from 'react-native-vector-icons/Octicons'; 5 | 6 | import { markNotification } from '../Actions'; 7 | import Constants from '../Utils/Constants'; 8 | import Routes from '../Navigation/Routes'; 9 | 10 | import { 11 | Linking, 12 | StyleSheet, 13 | Text, 14 | TouchableHighlight, 15 | View 16 | } from 'react-native'; 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | flex: 1, 21 | flexDirection: 'row', 22 | alignItems: 'center', 23 | paddingHorizontal: 5, 24 | paddingVertical: 5, 25 | backgroundColor: '#FFF', 26 | borderBottomWidth: 1, 27 | borderBottomColor: Constants.THEME_ALT, 28 | }, 29 | typeIcon: { 30 | fontSize: 20, 31 | marginLeft: 15 32 | }, 33 | title: { 34 | flex: 1, 35 | marginHorizontal: 10 36 | }, 37 | iconWrapper: { 38 | paddingVertical: 5, 39 | 40 | }, 41 | checkIcon: { 42 | fontSize: 20, 43 | marginRight: 15, 44 | color: Constants.THEME_ALT 45 | } 46 | }); 47 | 48 | class Notification extends React.Component { 49 | static propTypes = { 50 | navigator: PropTypes.object.isRequired, 51 | inBrowser: PropTypes.bool.isRequired, 52 | details: PropTypes.object.isRequired, 53 | markNotification: PropTypes.func.isRequired, 54 | }; 55 | 56 | _getTypeIcon() { 57 | switch (this.props.details.getIn(['subject', 'type'])) { 58 | case 'Issue': 59 | return 'issue-opened'; 60 | case 'PullRequest': 61 | return 'git-pull-request'; 62 | case 'Commit': 63 | return 'git-commit'; 64 | case 'Release': 65 | return 'tag'; 66 | default: 67 | return 'question'; 68 | } 69 | } 70 | 71 | markAsRead() { 72 | this.props.markNotification(this.props.details.get('id')); 73 | } 74 | 75 | openNotification() { 76 | let url = this.props.details.getIn(['subject', 'url']) 77 | .replace('api.github.com/repos', 'www.github.com'); 78 | if (url.indexOf('/pulls/') !== -1) { 79 | url = url.replace('/pulls/', '/pull/'); 80 | } 81 | 82 | if (this.props.inBrowser) { 83 | Linking.openURL(url); 84 | } else { 85 | const route = Routes.GithubView({ url }); 86 | this.props.navigator.push(route); 87 | } 88 | } 89 | 90 | render() { 91 | const details = this.props.details; 92 | 93 | return ( 94 | 95 | 96 | 97 | this.openNotification()} 100 | underlayColor="#FFF" 101 | > 102 | {details.getIn(['subject', 'title'])} 103 | 104 | 105 | this.markAsRead()} 108 | underlayColor="#FFF" 109 | > 110 | 111 | 112 | 113 | ); 114 | } 115 | } 116 | 117 | function mapStateToProps(state) { 118 | return { 119 | inBrowser: state.settings.get('inBrowser') 120 | }; 121 | } 122 | 123 | export default connect(mapStateToProps, { markNotification })(Notification); 124 | -------------------------------------------------------------------------------- /App/Components/RepositoryTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Icon from 'react-native-vector-icons/Octicons'; 5 | 6 | import { markRepoNotifications } from '../Actions'; 7 | import Constants from '../Utils/Constants'; 8 | 9 | import { 10 | Image, 11 | StyleSheet, 12 | Text, 13 | TouchableHighlight, 14 | View 15 | } from 'react-native'; 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | flexDirection: 'row', 21 | paddingVertical: 10, 22 | paddingHorizontal: 10, 23 | backgroundColor: Constants.REPO_TITLE_BG, 24 | alignItems: 'center' 25 | }, 26 | avatar: { 27 | width: 25, 28 | height: 25, 29 | marginLeft: 5, 30 | borderRadius: 12.5 31 | }, 32 | title: { 33 | flex: 1, 34 | marginHorizontal: 10, 35 | fontSize: 16, 36 | fontWeight: 'bold' 37 | }, 38 | checkIcon: { 39 | fontSize: 20, 40 | marginRight: 10, 41 | color: Constants.THEME_ALT 42 | } 43 | }); 44 | 45 | class RepositoryTitle extends React.Component { 46 | 47 | static propTypes = { 48 | details: PropTypes.object.isRequired, 49 | markRepoNotifications: PropTypes.func.isRequired 50 | }; 51 | 52 | markAsRead() { 53 | const loginId = this.props.details.data.getIn(['owner', 'login']); 54 | const repoId = this.props.details.data.get('name'); 55 | const fullName = this.props.details.data.get('full_name'); 56 | return this.props.markRepoNotifications(loginId, repoId, fullName); 57 | } 58 | 59 | render() { 60 | const firstRepoDetails = this.props.details.data[0].get('repository'); 61 | const avatar_url = firstRepoDetails.getIn(['owner', 'avatar_url']); 62 | 63 | return ( 64 | 65 | 66 | {firstRepoDetails.get('full_name')} 67 | 68 | this.markAsRead()} 70 | underlayColor={Constants.REPO_TITLE_BG} 71 | > 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | export default connect(null, { markRepoNotifications })(RepositoryTitle); 80 | -------------------------------------------------------------------------------- /App/Components/Setting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Constants from '../Utils/Constants'; 5 | 6 | import { 7 | StyleSheet, 8 | Switch, 9 | Text, 10 | View 11 | } from 'react-native'; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flexDirection: 'row', 16 | alignItems: 'center', 17 | justifyContent: 'space-between', 18 | paddingVertical: 5, 19 | marginVertical: 10, 20 | }, 21 | text: { 22 | fontSize: 18, 23 | fontWeight: '500' 24 | } 25 | }); 26 | 27 | export default class Setting extends React.Component { 28 | 29 | static propTypes = { 30 | title: PropTypes.string.isRequired, 31 | value: PropTypes.bool.isRequired, 32 | onChange: PropTypes.func.isRequired 33 | }; 34 | 35 | render() { 36 | return ( 37 | 38 | {this.props.title} 39 | 40 | this.props.onChange(value)} 42 | onTintColor={Constants.BRAND_SUCCESS} 43 | tintColor={Constants.THEME_ALT} 44 | value={this.props.value} 45 | /> 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /App/Components/SettingUp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | import { 4 | StyleSheet, 5 | Text, 6 | View, 7 | } from 'react-native'; 8 | 9 | const styles = StyleSheet.create({ 10 | container: { 11 | flex: 1, 12 | alignItems: 'center', 13 | justifyContent: 'center' 14 | }, 15 | text: { 16 | fontSize: 18, 17 | fontWeight: 'bold' 18 | } 19 | }); 20 | 21 | export default class SettingUp extends React.Component { 22 | render() { 23 | return ( 24 | 25 | Setting Up Gitify 26 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /App/Components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Icon from 'react-native-vector-icons/Octicons'; 5 | 6 | import { searchNotifications } from '../Actions'; 7 | import Constants from '../Utils/Constants'; 8 | 9 | import { 10 | Platform, 11 | StyleSheet, 12 | Text, 13 | TextInput, 14 | View, 15 | } from 'react-native'; 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flexDirection: 'row', 20 | backgroundColor: Constants.TOOLBAR_BG, 21 | alignItems: 'center', 22 | }, 23 | countWrapper: { 24 | backgroundColor: Constants.BRAND_SUCCESS, 25 | flexDirection: 'row', 26 | alignItems: 'center', 27 | paddingHorizontal: 15, 28 | paddingVertical: 12, 29 | }, 30 | countIcon: { 31 | marginRight: 7.5, 32 | color: '#FFF', 33 | fontSize: 18, 34 | }, 35 | countText: { 36 | color: '#FFF', 37 | fontSize: 18, 38 | fontWeight: '500', 39 | }, 40 | textInput: { 41 | flex: 1, 42 | marginLeft: 5, 43 | paddingHorizontal: 15, 44 | color: '#FFF', 45 | textAlign: 'right', 46 | 47 | ...Platform.select({ 48 | android: { 49 | height: 40 50 | }, 51 | }) 52 | } 53 | }); 54 | 55 | class Toolbar extends React.Component { 56 | 57 | static propTypes = { 58 | searchNotifications: PropTypes.func.isRequired, 59 | count: PropTypes.number.isRequired, 60 | query: PropTypes.string, 61 | }; 62 | 63 | render() { 64 | return ( 65 | 66 | 67 | 68 | {this.props.count} 69 | 70 | 71 | this.props.searchNotifications(text)} 74 | value={this.props.query} 75 | placeholder="Search Repositories" 76 | placeholderTextColor="#FFF" 77 | autoCapitalize="none" 78 | autoCorrect={false} 79 | returnKeyType="search" 80 | /> 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default connect(null, { searchNotifications })(Toolbar); 87 | -------------------------------------------------------------------------------- /App/Middleware/Sound.js: -------------------------------------------------------------------------------- 1 | import { FETCH_NOTIFICATIONS_SUCCESS } from '../Actions'; 2 | import SoundHelper from '../Utils/Sound'; 3 | 4 | export default store => next => action => { 5 | 6 | switch (action.type) { 7 | 8 | case FETCH_NOTIFICATIONS_SUCCESS: 9 | const playSound = store.getState().settings.get('playSound'); 10 | const previousNotifications = store.getState().notifications.get('response').map(obj => obj.get('id')); 11 | const newNotifications = action.payload.filter((obj) => previousNotifications.includes(obj.get('id'))); 12 | 13 | if (newNotifications.length && playSound) { 14 | SoundHelper.play(); 15 | } 16 | break; 17 | 18 | } 19 | 20 | return next(action); 21 | }; 22 | -------------------------------------------------------------------------------- /App/Navigation/NavbarBackButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/Octicons'; 4 | 5 | import Constants from '../Utils/Constants'; 6 | 7 | import { 8 | StyleSheet, 9 | TouchableHighlight, 10 | View, 11 | } from 'react-native'; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | flexDirection: 'row', 17 | alignItems: 'center' 18 | }, 19 | toolbarButton: { 20 | paddingHorizontal: 10, 21 | }, 22 | icon: { 23 | fontSize: Constants.NAVBAR_BUTTON_ICON_SIZE, 24 | color: '#FFF' 25 | } 26 | }); 27 | 28 | export default class NavigationButton extends React.Component { 29 | static contextTypes = { 30 | drawer: PropTypes.object 31 | }; 32 | 33 | static propTypes = { 34 | navigator: PropTypes.object.isRequired, 35 | } 36 | 37 | _goBack() { 38 | this.props.navigator.pop(); 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 | this._goBack()} 48 | > 49 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /App/Navigation/NavbarButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/Octicons'; 4 | 5 | import Constants from '../Utils/Constants'; 6 | import Routes from './Routes'; 7 | 8 | import { 9 | StyleSheet, 10 | TouchableHighlight, 11 | View 12 | } from 'react-native'; 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | flexDirection: 'row', 18 | alignItems: 'center' 19 | }, 20 | toolbarButton: { 21 | paddingHorizontal: 10, 22 | }, 23 | icon: { 24 | fontSize: Constants.NAVBAR_BUTTON_ICON_SIZE - 7.5, 25 | color: '#FFF' 26 | } 27 | }); 28 | 29 | export default class NavigationButton extends React.Component { 30 | static propTypes = { 31 | navigator: PropTypes.object.isRequired, 32 | }; 33 | 34 | _noDuplicatesPush(route) { 35 | let routes = this.props.navigator.getCurrentRoutes(); 36 | if (routes[routes.length - 1].id !== route.id) { 37 | this.props.navigator.push(route); 38 | } 39 | } 40 | 41 | _goToSettings() { 42 | this._noDuplicatesPush(Routes.SettingsView()); 43 | } 44 | 45 | render() { 46 | return ( 47 | 48 | this._goToSettings()} 52 | > 53 | 54 | 55 | 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /App/Navigation/NavbarTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | StyleSheet, 6 | Text, 7 | View, 8 | } from 'react-native'; 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | flex: 1, 13 | justifyContent: 'center', 14 | }, 15 | title: { 16 | fontSize: 18, 17 | textAlign: 'center', 18 | color: '#FFF' 19 | } 20 | }); 21 | 22 | export default class NavigationTitle extends React.Component { 23 | static propTypes = { 24 | route: PropTypes.object.isRequired, 25 | }; 26 | 27 | render() { 28 | return ( 29 | 30 | {this.props.route.title} 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /App/Navigation/NavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { Navigator } from 'react-native-deprecated-custom-components'; 3 | 4 | export default class NavigationBar extends Navigator.NavigationBar { 5 | render() { 6 | let routes = this.props.navState.routeStack; 7 | 8 | if (routes.length) { 9 | let route = routes[routes.length - 1]; 10 | 11 | if (route.displayNavBar === false) { 12 | return null; 13 | } 14 | } 15 | 16 | return super.render(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /App/Navigation/RouteMapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | import NavigationTitle from './NavbarTitle'; 4 | import NavigationButton from './NavbarButton'; 5 | import NavigationBackButton from './NavbarBackButton'; 6 | 7 | export default { 8 | 9 | LeftButton(route, navigator, index) { 10 | if (index === 0) { 11 | return null; 12 | } 13 | 14 | return ( 15 | 16 | ); 17 | }, 18 | 19 | RightButton(route, navigator, index) { 20 | if (route.id === 'settings-view') { 21 | return null; 22 | } 23 | 24 | return ( 25 | 31 | ); 32 | }, 33 | 34 | Title(route) { 35 | return ( 36 | 37 | ); 38 | }, 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /App/Navigation/Routes.js: -------------------------------------------------------------------------------- 1 | import LoginView from '../Routes/LoginView'; 2 | import SettingsView from '../Routes/SettingsView'; 3 | import NotificationsView from '../Routes/Notifications'; 4 | import OAuthView from '../Routes/OAuthView'; 5 | import GithubView from '../Routes/GithubView'; 6 | 7 | export default { 8 | LoginView() { 9 | return { 10 | id: 'login-view', 11 | title: 'Login', 12 | component: LoginView, 13 | displayNavBar: false 14 | }; 15 | }, 16 | 17 | Notifications(props) { 18 | return { 19 | id: 'notifications-view', 20 | title: 'Notifications', 21 | component: NotificationsView, 22 | passProps: props, 23 | displayNavBar: true 24 | }; 25 | }, 26 | 27 | OAuth(props) { 28 | return { 29 | id: 'oauth-view', 30 | title: 'Authentication', 31 | component: OAuthView, 32 | passProps: props, 33 | displayNavBar: true 34 | }; 35 | }, 36 | 37 | SettingsView(props) { 38 | return { 39 | id: 'settings-view', 40 | title: 'Settings', 41 | component: SettingsView, 42 | passProps: props, 43 | displayNavBar: true 44 | }; 45 | }, 46 | 47 | GithubView(props) { 48 | return { 49 | id: 'github-view', 50 | title: 'GitHub', 51 | component: GithubView, 52 | passProps: props, 53 | displayNavBar: true 54 | }; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /App/Navigation/SceneContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | Platform, 6 | StatusBar, 7 | StyleSheet, 8 | View 9 | } from 'react-native'; 10 | 11 | import Constants from '../Utils/Constants'; 12 | 13 | let styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | backgroundColor: Constants.BG_COLOR 17 | } 18 | }); 19 | 20 | export default class SceneContainer extends React.Component { 21 | static propTypes = { 22 | navigator: PropTypes.object.isRequired, 23 | route: PropTypes.object.isRequired, 24 | // authUrl: PropTypes.string.isRequired, 25 | }; 26 | 27 | componentWillMount() { 28 | this.paintStatusBar(); 29 | } 30 | 31 | componentWillReceiveProps() { 32 | this.paintStatusBar(); 33 | } 34 | 35 | paintStatusBar() { 36 | if (Platform.OS !== 'ios') { return; } 37 | 38 | if (this.props.route.displayNavBar) { 39 | StatusBar.setBarStyle('light-content', true); 40 | } else { 41 | StatusBar.setBarStyle('default', false); 42 | } 43 | } 44 | 45 | render() { 46 | const Component = this.props.route.component; 47 | const navbarStyle = {marginTop: (this.props.route.displayNavBar ? Constants.NAVBAR_HEIGHT : 0 )}; 48 | 49 | return ( 50 | 51 | 56 | 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /App/Reducers/Auth.js: -------------------------------------------------------------------------------- 1 | import immutable from 'immutable'; 2 | import * as actions from '../Actions'; 3 | 4 | const initialState = immutable.Map({ 5 | isFetching: false, 6 | errored: false, 7 | token: null 8 | }); 9 | 10 | export default function reducer(state = initialState, action) { 11 | switch (action.type) { 12 | case actions.FETCH_TOKEN_REQUEST: 13 | return state 14 | .set('errored', false) 15 | .set('isFetching', true); 16 | case actions.FETCH_TOKEN_SUCCESS: 17 | return state 18 | .set('errored', false) 19 | .set('isFetching', false) 20 | .set('token', action.payload.access_token); 21 | case actions.FETCH_TOKEN_FAILURE: 22 | return state 23 | .set('errored', true) 24 | .set('isFetching', false) 25 | .set('token', null); 26 | case actions.LOGOUT: 27 | return state 28 | .set('token', null); 29 | default: 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /App/Reducers/Notifications.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { List, Map } from 'immutable'; 3 | import * as actions from '../Actions'; 4 | 5 | const initialState = Map({ 6 | isFetching: false, 7 | isReFetching: false, 8 | errored: false, 9 | response: List() 10 | }); 11 | 12 | export default function reducer(state = initialState, action) { 13 | switch (action.type) { 14 | case actions.FETCH_NOTIFICATIONS_REQUEST: 15 | if (action.meta.isReFetching) { 16 | return state 17 | .set('errored', false) 18 | .set('isReFetching', true); 19 | } else { 20 | return state 21 | .set('errored', false) 22 | .set('isFetching', true) 23 | .set('response', List()); 24 | } 25 | case actions.FETCH_NOTIFICATIONS_SUCCESS: 26 | return state 27 | .set('errored', false) 28 | .set('isFetching', false) 29 | .set('isReFetching', false) 30 | .set('response', action.payload); 31 | case actions.FETCH_NOTIFICATIONS_FAILURE: 32 | return state 33 | .set('errored', true) 34 | .set('isFetching', false) 35 | .set('isReFetching', false); 36 | case actions.MARK_NOTIFICATION_SUCCESS: 37 | const id = action.id; 38 | return state 39 | .set('response', _.without(state.get('response'), _.findWhere(state.get('response'), {id}))); 40 | case actions.MARK_REPO_NOTIFICATION_SUCCESS: 41 | const repoFullName = action.repoFullName; 42 | return state 43 | .set('response', _.reject(state.get('response'), (obj) => obj.repository.full_name === repoFullName)); 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /App/Reducers/Search.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | import { 4 | SEARCH_NOTIFICATIONS, CLEAR_SEARCH, FETCH_NOTIFICATIONS_REQUEST 5 | } from '../Actions'; 6 | 7 | const initialState = Map({ 8 | query: '' 9 | }); 10 | 11 | export default function reducer(state = initialState, action) { 12 | switch (action.type) { 13 | case SEARCH_NOTIFICATIONS: 14 | return state 15 | .set('query', action.query); 16 | case CLEAR_SEARCH: 17 | case FETCH_NOTIFICATIONS_REQUEST: 18 | return state 19 | .set('query', ''); 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Reducers/Settings.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import * as actions from '../Actions'; 3 | 4 | const initialState = Map({ 5 | loaded: false, 6 | participating: false, 7 | playSound: false, 8 | inBrowser: false 9 | }); 10 | 11 | export default function reducer(state = initialState, action) { 12 | switch (action.type) { 13 | case actions.APP_LOADED: 14 | return state 15 | .set('loaded', true); 16 | case actions.UPDATE_SETTING: 17 | return state 18 | .set(action.setting, action.value); 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /App/Reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import * as storage from 'redux-storage'; 3 | import merger from 'redux-storage-merger-immutablejs'; 4 | 5 | import auth from './Auth'; 6 | import notifications from './Notifications'; 7 | import search from './Search'; 8 | import settings from './Settings'; 9 | 10 | export default storage.reducer(combineReducers({ 11 | auth, 12 | notifications, 13 | search, 14 | settings 15 | }), merger); 16 | -------------------------------------------------------------------------------- /App/Routes/GithubView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ButtonBrowser from '../Components/ButtonBrowser'; 5 | import Constants from '../Utils/Constants'; 6 | 7 | import { 8 | StyleSheet, 9 | View, 10 | WebView, 11 | } from 'react-native'; 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | }, 17 | toolbar: { 18 | flexDirection: 'row', 19 | alignItems: 'center', 20 | paddingHorizontal: 10, 21 | backgroundColor: Constants.TOOLBAR_BG, 22 | }, 23 | toolbarLeft: { 24 | flex: .5, 25 | flexDirection: 'row', 26 | alignItems: 'center', 27 | 28 | }, 29 | toolbarRight: { 30 | flex: .5, 31 | flexDirection: 'row', 32 | alignItems: 'center', 33 | justifyContent: 'flex-end' 34 | } 35 | }); 36 | 37 | export default class GithubView extends React.Component { 38 | static propTypes = { 39 | url: PropTypes.string.isRequired 40 | }; 41 | 42 | constructor(props) { 43 | super(props); 44 | 45 | this.state = { 46 | backButtonEnabled: false, 47 | forwardButtonEnabled: false, 48 | url: props.url 49 | }; 50 | } 51 | 52 | goBack() { 53 | this.webView.goBack(); 54 | } 55 | 56 | goForward() { 57 | this.webView.goForward(); 58 | } 59 | 60 | reload() { 61 | this.webView.reload(); 62 | } 63 | 64 | onNavigationStateChange(navState) { 65 | this.setState({ 66 | backButtonEnabled: navState.canGoBack, 67 | forwardButtonEnabled: navState.canGoForward, 68 | url: navState.url 69 | }); 70 | } 71 | 72 | render() { 73 | return ( 74 | 75 | { 77 | this.webView = el; 78 | }} 79 | source={{uri: this.state.url}} 80 | onNavigationStateChange={this.onNavigationStateChange.bind(this)} 81 | automaticallyAdjustContentInsets 82 | startInLoadingState 83 | /> 84 | 85 | 86 | 87 | this.goBack()} 90 | disabled={!this.state.backButtonEnabled} 91 | /> 92 | this.goForward()} 95 | disabled={!this.state.forwardButtonEnabled} 96 | /> 97 | 98 | 99 | 100 | this.reload()} /> 101 | 102 | 103 | 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /App/Routes/LoginView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | Image, 6 | StyleSheet, 7 | Text, 8 | View 9 | } from 'react-native'; 10 | 11 | import Button from '../Components/Button'; 12 | import Constants from '../Utils/Constants'; 13 | import Routes from '../Navigation/Routes'; 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flex: 1, 18 | }, 19 | loginWrapper: { 20 | flex: 1, 21 | justifyContent: 'center', 22 | alignItems: 'center' 23 | }, 24 | logo: { 25 | width: 125, 26 | height: 125 27 | }, 28 | text: { 29 | fontSize: 18, 30 | fontWeight: 'bold', 31 | marginVertical: 10 32 | }, 33 | description: { 34 | fontSize: 16, 35 | textAlign: 'center', 36 | marginBottom: 20 37 | } 38 | }); 39 | 40 | export default class LoginView extends React.Component { 41 | static propTypes = { 42 | navigator: PropTypes.object.isRequired, 43 | }; 44 | 45 | doOAuth() { 46 | const authUrl = [ 47 | 'https://github.com/login/oauth/authorize', 48 | '?client_id=' + Constants.oAuthOptions.client_id, 49 | '&client_secret=' + Constants.oAuthOptions.client_secret, 50 | '&scope=' + Constants.oAuthOptions.scopes 51 | ].join(''); 52 | 53 | const route = Routes.OAuth({authUrl}); 54 | this.props.navigator.push(route); 55 | } 56 | 57 | render() { 58 | return ( 59 | 60 | 61 | 62 | Gitify Mobile 63 | GitHub Notifications{'\n'} in your pocket 64 |