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
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.

Choosing the Apple TV simulator target in XCode.

Choosing the Apple TV simulator target in XCode.

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.  

Screenshot creating Slack API Token

Screenshot creating Slack API Token.

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.

fetchSlackChannel method
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.

updateSlackView method
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.

Screenshot Slack channel

Screenshot Slack channel.

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.

Screenshot Apple TV Slack App

Screenshot Apple TV Slack App.

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

React Native Apple TV Slack complete code
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);

Say hello@snowball.digital and let’s make your product dream come true

Address line test

Facebook link. Instagram link.