Slack Apple TV App written in React Native

I love dashboards and keeping track of notifications. Lately I find that we have been using Slack to centralize our notifications from different projects. I wanted one of our TV screens in the office to always show the latest updates and I wanted this in-brand. The result was an Apple TV App written in React Native. In this post I share the main parts to get this up and running.

Initialize React Native for your Apple TV App.

To initialize your React Native App you basically run the react-native command as shown below.

react-native init snowball_slack_tv

This is standard React Native, but now the step comes to make this an Apple TV App.

Open the snowball_slack_tv.xcodeproj file in XCode. From there you can select your tvOS target and run your test App on the iOS TV simulator. This is almost too simple. But now you have your very first Hello World Apple TV App running. One command and two clicks.

no-alt

Connect to Slack

But I wanted to add an integration with Slack. The first step here is to create an API Token for your Slack Apple TV App. This is done in your Slack App Directory. Hit build -> Make a Custom Integration -> Bots. There you register and you get an API Token as a reward. Remember to keep this API Token secret.

no-alt

Fetching Slack Channel History

In this example I simply choose to use the channels.history endpoint provided from Slack. This gives you the last messages from the given Slack channel, by default the 100 last.

Then I created a function fetchSlackChannel that returns a promise. You use promises to wait for async operations like fetch in Javascript in case you wondered. When the promise is resolved it passes the response data from the channels.history endpoint.

The specifics here are that we create a POST call to the channels.history endpoint. We define the parameters token and channel. The token is the one you got from your Slack settings. The channel ID you can get from e.g. using Postman and do a call to channels.list. In return you get the 100 last messages in that channel.

function fetchSlackChannel() {
return new Promise(function(resolve, reject) {
var token = 'xxx';
fetch('https://slack.com/api/channels.history', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'token=' + token + '&channel=CNLID'
})
.then(response => response.json())
.then(responseData => {
resolve(responseData);
})
.done();
});
}

Running a periodic function in React Native

I wanted to fetch new messages periodically. To achieve that in React Native you can use the setInterval function in your componentDidMount function and you need to use clearInterval on componentWillUnmount. If not you will have a crash when the component is unmounted.

My componentDidMount function runs a function updateSlackView and then creates a callback function that is called every 10 seconds doing the same. The reason why I do this is that the view is updated on load and then updated every 10 seconds.

I added a ListView component to my main view to show the Slack messages. The updateSlackView function fetches the updates from the Slack channel and then sets the state with the refreshed messages. The view is automatically updated in case of any changes.

componentDidMount() {
this.updateSlackView();
this._interval = setInterval(() => {
console.log('Fetching Slack info');
this.updateSlackView();
}, 10000);
}
updateSlackView() {
this.fetchSlackChannel()
.then(slackChannel => {
console.log(slackChannel.messages);
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.setState({ dataSource: ds.cloneWithRows(slackChannel.messages) });
})
.done();
}
componentWillUnmount() {
clearInterval(this._interval);
}

That is it really. The Slack channel used for testing is shown below.

no-alt

The Apple TV App

The resulting Apple TV App is shown here. Not a very feature rich application, but it does connect your Apple TV to Slack. From here it is basically about styling and minor features to get something useful.

no-alt

Complete Code

You can see the complete code below. Hopefully you find it useful.

import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, ListView } from 'react-native';

export default class snowball_slack_tv extends Component {
constructor() {
super();
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = { dataSource: ds.cloneWithRows(['Loading..']) };
}

componentDidMount() {
this.updateSlackView();
this._interval = setInterval(() => {
console.log('Fetching Slack info');
this.updateSlackView();
}, 10000);
}

updateSlackView() {
this.fetchSlackChannel().then(slackChannel => {
console.log(slackChannel.messages);
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.setState({ dataSource: ds.cloneWithRows(slackChannel.messages) });
}).done();
}

componentWillUnmount() {
clearInterval(this._interval);
}

render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to Slack TV by Snowball</Text>
<ListView dataSource={this.state.dataSource}
renderRow={rowData =>
(<Text style={styles.message}>{rowData.text}</Text>)}/>
<Text style={styles.poweredby}>Imagined by Snowball digital.</Text> </View>);
}

fetchSlackChannel() {
return new Promise(function(resolve, reject) {
var token = 'xxx';
fetch('https://slack.com/api/channels.history', {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'token=' + token + '&channel=CID',
}).then(response => response.json()).then(responseData => {
resolve(responseData);
}).done();
});
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#5F626C',
},
welcome: { fontSize: 60, color: '#FFFFFE', textAlign: 'center', margin: 10 },
message: { fontSize: 30, color: '#FFFFFE', textAlign: 'center', margin: 10 },
poweredby: { fontSize: 30, textAlign: 'center', color: '#FFFFFE', marginBottom: 5 },
});
AppRegistry.registerComponent('snowball_slack_tv', () => snowball_slack_tv);