├── .flowconfig ├── .gitignore ├── .watchmanconfig ├── Components ├── Feed.ios.js ├── Link.js ├── Story.ios.js ├── StoryDetail.ios.js └── VideoPlaceHolder.js ├── README.md ├── XMLToReactMap.js ├── android ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ ├── react.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── bbcnews │ │ │ └── MainActivity.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 └── settings.gradle ├── index.android.js ├── index.ios.js ├── ios ├── AngularActivityIndicatorViewManager.h ├── AngularActivityIndicatorViewManager.m ├── BBCNews.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── BBCNews.xcscheme ├── BBCNews │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── main.m ├── BBCNewsTests │ ├── BBCNewsTests.m │ └── Info.plist ├── PCAngularActivityIndicatorView.h ├── PCAngularActivityIndicatorView.m └── main.jsbundle └── package.json /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.web.js 5 | .*/*.android.js 6 | 7 | # Some modules have their own node_modules with overlap 8 | .*/node_modules/node-haste/.* 9 | 10 | # Ugh 11 | .*/node_modules/babel.* 12 | .*/node_modules/babylon.* 13 | .*/node_modules/invariant.* 14 | 15 | # Ignore react and fbjs where there are overlaps, but don't ignore 16 | # anything that react-native relies on 17 | .*/node_modules/fbjs/lib/Map.js 18 | .*/node_modules/fbjs/lib/Promise.js 19 | .*/node_modules/fbjs/lib/fetch.js 20 | .*/node_modules/fbjs/lib/ExecutionEnvironment.js 21 | .*/node_modules/fbjs/lib/isEmpty.js 22 | .*/node_modules/fbjs/lib/crc32.js 23 | .*/node_modules/fbjs/lib/ErrorUtils.js 24 | 25 | # Flow has a built-in definition for the 'react' module which we prefer to use 26 | # over the currently-untyped source 27 | .*/node_modules/react/react.js 28 | .*/node_modules/react/lib/React.js 29 | .*/node_modules/react/lib/ReactDOM.js 30 | 31 | # Ignore commoner tests 32 | .*/node_modules/commoner/test/.* 33 | 34 | # See https://github.com/facebook/flow/issues/442 35 | .*/react-tools/node_modules/commoner/lib/reader.js 36 | 37 | # Ignore jest 38 | .*/node_modules/jest-cli/.* 39 | 40 | # Ignore Website 41 | .*/website/.* 42 | 43 | [include] 44 | 45 | [libs] 46 | node_modules/react-native/Libraries/react-native/react-native-interface.js 47 | 48 | [options] 49 | module.system=haste 50 | 51 | munge_underscores=true 52 | 53 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 54 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub' 55 | 56 | suppress_type=$FlowIssue 57 | suppress_type=$FlowFixMe 58 | suppress_type=$FixMe 59 | 60 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-0]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 61 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-0]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 62 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 63 | 64 | [version] 65 | 0.20.1 66 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Components/Feed.ios.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | StyleSheet, 4 | View, 5 | ListView, 6 | TimerMixin, 7 | RefreshControl 8 | } = React; 9 | 10 | var Loader = require('react-native-angular-activity-indicator'); 11 | var Story = require('./Story'); 12 | var Feed = React.createClass({ 13 | 14 | 15 | getInitialState() { 16 | return { 17 | dataSource: new ListView.DataSource({ 18 | rowHasChanged: (row1, row2) => row1 !== row2, 19 | }), 20 | loaded: false, 21 | isAnimating: true, 22 | isRefreshing: false, 23 | }; 24 | }, 25 | 26 | componentDidMount() { 27 | this.fetchData() 28 | }, 29 | 30 | filterNews(news = []) { 31 | return new Promise((res, rej) => { 32 | const filtered = news.filter( item => { 33 | return item.content.format === 'bbc.mobile.news.format.textual' 34 | }) 35 | res(filtered); 36 | }) 37 | 38 | }, 39 | 40 | fetchData() { 41 | this.setState({isRefreshing: true}); 42 | 43 | fetch(`http://trevor-producer-cdn.api.bbci.co.uk/content${this.props.collection || '/cps/news/world'}`) 44 | .then((response) => response.json()) 45 | .then((responseData) => this.filterNews(responseData.relations)) 46 | .then((newsItems) => 47 | { 48 | this.setState({ 49 | dataSource: this.state.dataSource.cloneWithRows(newsItems), 50 | loaded: true, 51 | isRefreshing: false, 52 | isAnimating: false 53 | }) 54 | 55 | 56 | }).done(); 57 | }, 58 | 59 | renderLoading() { 60 | return ( 61 | 62 | 63 | 64 | ); 65 | }, 66 | 67 | renderStories(story) { 68 | return ( 69 | 70 | ); 71 | }, 72 | 73 | render: function() { 74 | 75 | if (!this.state.loaded) { 76 | return this.renderLoading(); 77 | } 78 | 79 | return ( 80 | 81 | 99 | } 100 | /> 101 | ) 102 | } 103 | }); 104 | 105 | var styles = StyleSheet.create({ 106 | 107 | loadingView: { 108 | marginTop: 30, 109 | marginRight:50, 110 | backgroundColor: '#ff00ff', 111 | }, 112 | 113 | listView: { 114 | backgroundColor: '#eee' 115 | }, 116 | 117 | }); 118 | 119 | export default Feed; -------------------------------------------------------------------------------- /Components/Link.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | LinkingIOS 7 | } = React; 8 | 9 | var StoryDetail = require('./StoryDetail'); 10 | 11 | var moment = require('moment'); 12 | 13 | export default class Story extends React.Component { 14 | static propTypes = { 15 | name: React.PropTypes.string, 16 | }; 17 | 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | pressedURL() { 23 | console.log('hi', this.props.url) 24 | 25 | LinkingIOS.openURL(this.props.url) 26 | } 27 | 28 | render() { 29 | 30 | return ( 31 | {this.props.children} 32 | ); 33 | } 34 | } 35 | 36 | var styles = StyleSheet.create({ 37 | hyperlink: { 38 | color: 'black', 39 | fontWeight: 'bold', 40 | textDecorationLine: 'underline' 41 | } 42 | }); 43 | 44 | module.exports = Story; 45 | -------------------------------------------------------------------------------- /Components/Story.ios.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View, 7 | Image, 8 | TouchableHighlight, 9 | } = React; 10 | 11 | import Feed from './Feed'; 12 | var StoryDetail = require('./StoryDetail'); 13 | 14 | var moment = require('moment'); 15 | 16 | export default class Story extends React.Component { 17 | 18 | static propTypes = { 19 | name: React.PropTypes.string, 20 | }; 21 | 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | getCollectionForStory(story) { 27 | console.log('STORY', story) 28 | if (story.content.relations && story.content.relations.length) { 29 | 30 | return story.content.relations.find( item => { 31 | return item.primaryType === 'bbc.mobile.news.collection' 32 | }) 33 | 34 | } else { 35 | throw "No collection found" 36 | } 37 | } 38 | 39 | pressedCollection(collection) { 40 | this.props.navigator.push({ 41 | component: Feed, 42 | title: collection.content.name, 43 | passProps: {collection: collection.content.id, navigator: this.props.navigator} 44 | }) 45 | } 46 | 47 | truncateTitle(title) { 48 | if (title.length > 15) { 49 | return `${title.substring(0, 15)}...` 50 | } else { 51 | return title; 52 | } 53 | } 54 | 55 | pressedStory(story) { 56 | this.props.navigator.push({ 57 | component: StoryDetail, 58 | title: this.truncateTitle(story.content.name), 59 | passProps: {story, navigator: this.props.navigator} 60 | }); 61 | } 62 | 63 | render() { 64 | var story = this.props.story; 65 | var time = moment.unix((story.content.lastUpdated / 1000 )).fromNow(); 66 | var collection = this.getCollectionForStory(story) || {} 67 | 68 | console.log(collection) 69 | 70 | return ( 71 | this.pressedStory(story)}> 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {story.content.name} 81 | 82 | {time} 83 | | 84 | this.pressedCollection(collection)} > 85 | {collection.content ? collection.content.name : ""} 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | 95 | var styles = StyleSheet.create({ 96 | container: { 97 | flex: 1, 98 | marginLeft: 5, 99 | marginRight: 5, 100 | alignItems: 'center', 101 | flexDirection: 'row', 102 | backgroundColor: 'white', 103 | 104 | }, 105 | 106 | textView: { 107 | flex: 1, 108 | paddingTop: 5, 109 | paddingLeft: 5, 110 | paddingRight: 5, 111 | marginLeft: 5, 112 | marginRight: 5, 113 | backgroundColor: 'white', 114 | marginBottom: 5, 115 | }, 116 | 117 | details: { 118 | flex:1, 119 | justifyContent: 'flex-start', 120 | flexDirection: 'row', 121 | }, 122 | 123 | headline: { 124 | flex: 0, 125 | fontWeight: 'bold', 126 | fontSize: 20, 127 | margin: 3, 128 | 129 | }, 130 | 131 | timeStamp: { 132 | flex: 0, 133 | margin: 3, 134 | }, 135 | 136 | collection: { 137 | flex: 0, 138 | color: '#9d0a0e', 139 | margin: 3, 140 | }, 141 | 142 | border: { 143 | padding: 3, 144 | borderLeftWidth: 1, 145 | borderLeftColor: 'black', 146 | borderStyle: 'solid' 147 | }, 148 | 149 | imageContainer: { 150 | flex:1, 151 | height: 200, 152 | alignItems: 'stretch' 153 | }, 154 | 155 | thumbnail: { 156 | flex:1 157 | } 158 | 159 | }); 160 | 161 | module.exports = Story; 162 | -------------------------------------------------------------------------------- /Components/StoryDetail.ios.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View, 7 | Image, 8 | TouchableHighlight, 9 | ScrollView, 10 | LinkingIOS 11 | } = React; 12 | 13 | var moment = require('moment'); 14 | var Link = require('./Link'); 15 | var htmlparser = require('htmlparser'); 16 | var XMLToReactMap = require('../XMLToReactMap'); 17 | 18 | export default class StoryDetail extends React.Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | paragraph: "", 25 | loading: true, 26 | elements: null 27 | } 28 | } 29 | 30 | parseXMLBody(body,cb) { 31 | var handler = new Tautologistics.NodeHtmlParser.DefaultHandler(function (error, dom) { 32 | cb(dom) 33 | }, {enforceEmptyTags: false, ignoreWhitespace: true}); 34 | var parser = new Tautologistics.NodeHtmlParser.Parser(handler); 35 | parser.parseComplete(body); 36 | 37 | } 38 | 39 | 40 | fetchStoryData(cb) { 41 | fetch(`http://trevor-producer-cdn.api.bbci.co.uk/content${this.props.story.content.id}`) 42 | .then((response) => response.json()) 43 | .then((responseData) => { 44 | 45 | const images = responseData.relations.filter( item => { 46 | return item.primaryType === 'bbc.mobile.news.image'; 47 | }) 48 | 49 | const videos = responseData.relations.filter( item => { 50 | return item.primaryType === 'bbc.mobile.news.video'; 51 | }) 52 | 53 | const relations = {images, videos} 54 | 55 | this.parseXMLBody(responseData.body, (result) => { 56 | 57 | cb(result, relations) 58 | }) 59 | }) 60 | .done(); 61 | } 62 | 63 | componentDidMount() { 64 | this.fetchStoryData((result, media) => { 65 | const rootElement = result.find(item => { 66 | return item.name === 'body' 67 | }) 68 | 69 | XMLToReactMap.createReactElementsWithXMLRoot(rootElement, media).then(array => { 70 | var scroll = React.createElement(ScrollView, {contentInset:{top: 0, left: 0, bottom: 64, right: 0}, style:{flex: 1, flexDirection: 'column', backgroundColor: 'white'}, accessibilityLabel:"Story Detail"}, array) 71 | 72 | this.setState({loading: false, elements:scroll}) 73 | }) 74 | }) 75 | } 76 | 77 | render() { 78 | if (this.state.loading) { 79 | return ( 80 | Loading 81 | ) 82 | } 83 | return this.state.elements 84 | 85 | } 86 | } 87 | 88 | var styles = StyleSheet.create({ 89 | 90 | container: { 91 | }, 92 | 93 | paragraph: { 94 | padding: 40, 95 | fontSize: 16, 96 | lineHeight: 20*1.2 97 | }, 98 | 99 | headline: { 100 | position: 'absolute', 101 | bottom: 10, 102 | left: 0, 103 | paddingLeft: 30, 104 | paddingRight: 30, 105 | fontSize: 26, 106 | fontWeight: 'bold', 107 | color: 'white' 108 | 109 | }, 110 | 111 | overlay: { 112 | flex:1, 113 | backgroundColor: 'transparent', 114 | height:200 115 | }, 116 | 117 | thumbnail: { 118 | flex: 1, 119 | }, 120 | 121 | imageContainer: { 122 | flex:1, 123 | height: 300, 124 | alignItems: 'stretch' 125 | } 126 | }) 127 | 128 | module.exports = StoryDetail; 129 | -------------------------------------------------------------------------------- /Components/VideoPlaceHolder.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | AppRegistry, 4 | StyleSheet, 5 | View, 6 | TouchableHighlight, 7 | Image 8 | } = React; 9 | 10 | var Video = require('react-native-video'); 11 | 12 | export default class VideoPlaceHolder extends React.Component { 13 | static propTypes = { 14 | video: React.PropTypes.object.isRequired, 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | loadVideo: false, 21 | imageUrl: '' 22 | } 23 | } 24 | 25 | componentWillMount() { 26 | if (this.props.video.content.relations.length > 0) { 27 | const image = this.props.video.content.relations.find(video => { 28 | return video.primaryType === 'bbc.mobile.news.image'; 29 | }) 30 | 31 | if (image) { 32 | console.log('image tag', image); 33 | this.setState({imageUrl:image.content.href, image: image}); 34 | } 35 | } 36 | } 37 | 38 | pressedPlaceholder() { 39 | this.fetchVideoInfo(this.props.video.content.externalId, (url) => { 40 | this.setState({loadVideo: true, videoUrl: url}) 41 | }) 42 | } 43 | 44 | 45 | fetchVideoInfo(videoId, completion) { 46 | fetch(`http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/format/json/mediaset/journalism-http-tablet/vpid/${videoId}/proto/http/transferformat/hls/`) 47 | .then((response) => response.json()) 48 | .then((responseData) => { 49 | console.log(responseData) 50 | completion(responseData.media[0].connection[0].href); 51 | }) 52 | .done(); 53 | } 54 | 55 | render() { 56 | if (this.state.loadVideo) { 57 | return ( 58 |