Cabin — React & Redux Example App — Stream

This is the 4th post in the 8 part tutorial series created by getstream.io. The final result is your own feature-rich, scalable social network app built with React and Redux! Visit cabin.getstream.io for an overview of all 8 tutorials and a live demo. The full source code is available on GitHub.

Introduction

We’re using Stream to power the feeds in Cabin. Stream allows you to build newsfeeds and activity streams without worrying about the scalability, reliability, or maintenance of your feeds. Many companies have invested years in building their feed tech. (If you’re interested in learning more about this problem set, check out the papers mentioned on the stream-framework github repo.)

For a more in-depth look at the features behind Stream, click here, or try the five minute tutorial.

Sign Up For Updates, Win A Sweet Hoodie

If you haven’t already — sign up to receive updates of this series. By signing up, you will receive updates on forthcoming posts as well as tips, and bonus materials. You will also be entered to win a Stream hoodie:

Stay updated about upcoming posts in this React & Redux series:

A Little Background

By the end of this tutorial, we will have built a fully-fledged social application. Cabin relies on three specific feeds that power the application.

Learn about these three feed types:

Timeline

The timeline is the main page of Cabin and shows you the uploads from the people you follow.

Screen Shot 2016-06-09 at 9.50.27 AM

Notification

The second feed is the Notification Feed. If someone comments on your picture, likes it, or follows you it shows up in this feed.

Screen Shot 2016-06-09 at 9.50.50 AM

Incoming Activity

Lastly, we have our Incoming Activity Feed. The Incoming Activity Feed shows the likes, comments and follow activities from the people you follow.

Screen Shot 2016-06-09 at 9.51.15 AM

Activities

Now that we have an understanding of the feeds that power Cabin, let’s take a look at an example activity:

{
"actor": "user:Jack",
"verb": "added",
"object": "the_hill.jpg",
"target": "photo_albums:1",
"to": ["notifications:Jill"]
}

The actor, verb and object fields are a standard way to represent activities. It follows the official activity stream spec. You might display this in your app as:

“Jack added a photo of The Hill to Great Hill Photos — with @Jill.”

The “to” field offers us the ability to essentially CC someone in a notification. For more information about this functionality, check out our info on “targeting” in the Stream documentation.

Setting Up Stream

Let’s get started.

Head over to GetStream.io and create an account!

Next, create a new application in the dashboard with the following feed groups and types:

  • user_posts (flat)
  • user (flat)
  • timeline_flat (flat)
  • timeline_aggregrated (aggregated)
  • notification (notification)

Note: You’ll want to make sure to turn on notifications on all of your feed groups as we’ll be utilizing realtime functionality in this application.

Now that we’ve setup our feed groups, let’s collect the following information, as we’ll need it to initialize the application on both the client and server sides:

  • App ID
  • API Key
  • API Secret

Next up, we’ll need to clone the repository from GitHub:

git clone git@github.com:GetStream/stream-react-example.git

Then, add your credentials to the env.sh file located in the root directory by modifying the value and sourcing the file:

# use vim to open the env file
vim ./env.sh

# modify your credentials
export STREAM_APP_ID=YOUR_STREAM_APP_ID
export STREAM_KEY=YOUR_STREAM_KEY
export STREAM_SECRET=YOUR_STREAM_SECRET

# exit vim and source the environment variables file
source ./env.sh

Okay, nicely executed. Now your environment is setup to run with your Stream credentials! The last thing we’ll need to do is install Stream from npm.

Run the following command from both the /app and /api directories:

npm install getstream --save

Note: You’ll also want to make sure that wherever you’re using Stream, you require it at the top of your file:

// require stream dependency
var stream = require('getstream');

Adding Activities to Feeds

There are two sections where we utilize Stream in the Cabin application, the first being the API, the second being the client (a.k.a. app).

In our case, the API handles the heavy lifting — it adds activities to Stream, in addition to enriching the data when the client makes a request. “Enriching data” refers to the to task of taking an object like the one we referenced in our example above, looking up the associated data, and returning a complete object to be shown to the user.

Take a look at an example of the client side view for uploading a photo:

Screen Shot 2016-06-09 at 10.05.35 AM

Now, take a look at the server side (API) code that powers an “upload” activity to the feed:

// instantiate a new client
var streamClient = stream.connect(YOUR_API_KEY, YOUR_API_SECRET);

// instantiate a feed using feed class 'user_posts' and the actor user id
var userFeed = streamClient.feed('user_posts', ACTOR_USER_ID);

// build activity object
var activity = {
actor: $actorId,
verb: "add",
object: "upload:$objectId",
foreign_id: "foreign_id:$foreignId",
time: new Date()
}

// add activity to the feed
userFeed.addActivity(activity)
.then(function(response) {
console.log(response);
})
.catch(function(err) {
console.log(err);
});

Breaking it down:

  • Instantiate a new API client using our Stream API key and secret.
  • Then instantiate a new feed class using user_posts and the actor’s user id (from the database).
  • Build an activity object:
  • “actor” is the user creating the activity.
  • “verb” is the action being taken.
  • “object” is a descriptor of the action being taken, with the database id of the action (concatenated by a colon).
  • “foreign_id” is the database ID for reference during enrichment.
  • “time” is a timestamp of when the action occurred (in our case, we’re using “now”).
  • Add the activity to the feed.

For more information on adding activities, visit the Stream documentation.

Following Users

Following other users is an important aspect of any social application, especially Cabin.

following

Luckily, Stream makes it easy for us to follow other users’ feeds in just a few simple steps. Take a look:

// instantiate a new client (server side)
var streamClient = stream.connect(config.stream.key, config.stream.secret);

// instantiate a feed using feed class 'timeline_flat' and the user id from the database
var timeline = streamClient.feed('timeline_flat', data.user_id);
timeline.follow('user_posts', data.follower_id);

// instantiate a feed using feed class 'timeline_aggregated' and the user id from the database
var timelineAggregated = streamClient.feed('timeline_aggregated', data.user_id);
timelineAggregated.follow('user', data.follower_id);

// instantiate a feed using feed class 'user' and the user id from the database
var userFeed = streamClient.feed('user', data.user_id);

// build activity object for stream feed
var activity = {
actor: `user:${data.user_id}`,
verb: 'follow',
object: `user:${data.follower_id}`,
foreign_id: `follow:${result.insertId}`,
time: data['created_at'],
to: [`notification:${data.follower_id}`]
};

// add activity to the feed
userFeed.addActivity(activity)
.then(function(response) {
console.log(response);
})
.catch(function(reason) {
console.log(reason);
});

Note: The API is passed both a user_id parameter and a follower_id parameter from the data object. The user_id parameter is the user who is following someone, and the follower_id parameter is the user that is being followed.

With that in mind, let’s break things down a bit:

  • First, we instantiate the streamClient by passing in our new API credentials
  • We then instantiate and follow the following feeds
  • timeline
  • timelineAggregated
  • userFeed
  • Lastly, we build our activity object and add the activity to our Stream feed

Enriching Our Data

Next up is the “enrichment” step of this tutorial.

Stream allows you to store as much data in an activity as you’d like — think of this as metadata. However, in many cases it’s best to store a reference. For example, a user profile can change over time, but you wouldn’t want to have to update every activity associated with that user profile.

For example, the enrichment step would translate this reference:

user:10

Into this enriched object:

{
id: 10,
name: 'Nick',
image: '...'
}

Enrichment is extremely important, as this is when we parse activity data for consumption by the client. It can become a bit unwieldy if you’re not careful.

Given this, we decided to abstract the code into a small library that can be found here.

Take a look at this enrichment code:

// instantiate a new client (server side)
var streamClient = stream.connect(config.stream.key, config.stream.secret);

// instantiate a feed using feed class 'timeline_flat' and user id from params
var timelineFlatFeed = streamClient.feed('timeline_flat', params.user_id);

// get activities from stream
timelineFlatFeed.get()
.then(function(stream) {

// length of activity results
var ln = stream.results.length;

// exit if length is zero
if (!ln) {
res.send(204);
return next();
}

// enrich the activities
var references = streamUtils.referencesFromActivities(stream.results);
streamUtils.loadReferencedObjects(references, params.user_id, function(referencedObjects) {
streamUtils.enrichActivities(stream.results, referencedObjects);
cb(null, stream.results);
});

})
.catch(function(error) {
cb(error);
});

Let’s break it down:

  • First, we instantiate a new stream client using our Stream API key and secret found on our dashboard.
  • We then instantiate a new feed (assigned to the uploads variable) using the timeline_flat class and the user ID from our database. This specifies that we want to bind to the timeline associated with the specified user.
  • From there, we call the “get()” method to retrieve all activities. Activities are returned as an array, which we then pass into our “utils” library for enrichment.

For Cabin, we’re using a MySQL database to store all of our data; however, it’s possible to do build the same with a NoSQL database such as MongoDB — in which case, you may want to have a look at the stream-node package (it takes care of handling enrichment with Mongoose).

At the end of the day, your enrichment steps will likely differ, depending on your database of choice. For this reason, we’re leaving out the queries required to “enrich” data, as it’s different for nearly every use case.

If you have questions about your specific use case, feel free to post on our Stackoverflow page and we’ll do our best to help!

Aggregation

Aggregated feeds allow you to specify a rule by which activities should be grouped.

For example, in the screenshot below, we are aggregating “like” activities, but not the comments and follows associated with the aggregated images.

Screen Shot 2016-06-09 at 9.51.15 AM

Setting up an aggregation rule for Stream is rather straightforward.

Follow along to understand how we setup aggregated feeds:

You simply head to your application on Stream and create a group with the “feed type” of “aggregated” (in our case, the feed groups are “timeline_aggregated” and “notification”).

Then, you can specify the aggregation format that best fits your application. Here’s a screenshot of the aggregation format for Cabin:

Screen Shot 2016-06-09 at 1.39.29 PM

From the screenshot above, you can see that if the aggregation type is “like”, we concatenate the actor with the user id, giving us the following grouping key:

user:3_2016-06-07

If we’re dealing with a comment or follow, we group based on the activity id. (basically not grouping since activity ids are unique)

C4865980-2ce9-11e6-8080-800141fd024b

Activities are stored in Stream as a reference to user ids and upload ids. With that said, we need the full object, not just the reference for the template. The code below queries the database (using our nifty Stream utility) to translate the data.

var references = streamUtils.referencesFromActivities(stream.results);
streamUtils.loadReferencedObjects(references, params.user_id, function(referencedObjects) {

streamUtils.enrichActivities(stream.results, referencedObjects);

// return the enriched activities
console.log(stream.results);

});

Once translated, we end up something like this:

[
{
"activities":[
{
"actor":{
"id":5,
"user_id":5,
"first_name":"Chantz",
"last_name":"Large",
"email_md5":"82f17020a44fcabffd9b1af3169b0688",
"following":1
},
"foreign_id":"follow:6",
"id":"b6b50de0-2c55-11e6-8080-800166d59f5a",
"object":{
"id":2,
"user_id":2,
"first_name":"Nick",
"last_name":"Parsons",
"email_md5":"8671bd5bc583a5ef61a2267659d6c6aa",
"following":0
},
"origin":null,
"target":null,
"time":"2016-06-07T02:15:33.054000",
"to":[
"notification:2"
],
"verb":"follow"
}
],
"activity_count":1,
"actor_count":1,
"created_at":"2016-06-07T02:15:33.054000",
"group":"b6b50de0-2c55-11e6-8080-800166d59f5a",
"id":"b6b50de0-2c55-11e6-8080-800166d59f5a",
"is_read":false,
"is_seen":false,
"updated_at":"2016-06-07T02:15:33.054000",
"verb":"follow"
}
]

Note: Changes to the aggregation format only affect new activities. This can be a bit confusing when configuring the aggregation format.

Real-Time Notifications

One of the many amazing features of Stream is that you can listen to feed changes in real-time.

We use this heavily in our notification feed on Cabin — every time that a new photo is uploaded, we receive a notification via the Stream websocket client letting us know that there’s been an update.

Subscribing to Real-Time Changes

Basics

Subscribing to real-time changes with Stream, at it’s core, is simple. Take a look at the documentation.

Essentially, subscribing to real-time requires two steps:

1. Generate your read-only token on the server side.

2. Use the subscribe() method.

Cabin Code Examples

In our app, it looks a bit more complex as it is done across files — yet the core principles are the same.

1. View code in api/routes/users.js — Generate the token on the server side:

// get tokens from stream client
var tokens = {
timeline: {
flat: streamClient.getReadOnlyToken('timeline_flat', userId),
aggregated: streamClient.getReadOnlyToken('timeline_aggregrated', userId),
},
notification : streamClient.getReadOnlyToken('notification', userId),
};

// user object.assign to insert tokens from stream and jwt
result = Object.assign({}, result[0], { tokens: tokens }, { jwt: jwtToken });

// send response to client
res.send(200, result);
return next();

2. View code in app/modules/App.js — Subscribe to the changes:

// follow 'notifications' feed
this.notification = this.client.feed('notification', this.props.user.id, this.props.tokens.notification)
this.notification
.subscribe(data => {
this.props.dispatch(StreamActions.event(data))
})
.then(() => {
//console.log('Full (Notifications): Connected to faye channel, waiting for realtime updates');
}, (err) => {
console.error('Full (Notifications): Could not estabilsh faye connection', err);
});
Screen Shot 2016-06-09 at 10.26.39 AM

Notification Feeds are similar to aggregated feeds. There are two key differences that make them more suited to building notification systems:

  • Notifications can be marked as “seen” or “read.”
  • You get a real-time count of the number of “unseen” and “unread” notifications.

(If this functionality was not baked into Stream, we would have to spend some serious engineering time setting up and configuring a socket server to handle the heavy lifting of routing updates to our users!)

Check out more information on Notification Feeds by visiting the Stream documentation.

Feed Group Structure Recap

feedtypes_blogimage

Recap the Feed Group Structure:

  • The timeline feeds follow user_posts (containing uploads)
  • The timeline_aggregated follow user feeds (containing likes, comments, follows)
  • notification is populated using the to field

If you write an activity to user_posts:nick it will automatically show up in the timeline feeds of people who follow Nick. Similarly if you add an activity to user:nick it will show up in the timeline_aggregated feeds of people who follow Nick.

Future Improvements

One thing we didn’t try out are the ranking methods. By default, all feeds are ranked in reverse chronological order. If you want to show popular or promoted content higher in the feed you can use the ranking methods. Read this documentation about ranking methods.

Personalization

Another powerful feature is personalization.

Personalization uses analytics and machine learning to tailor a feed to the user’s experience. A good example is the discovery feed you see on Instagram when you open search.

Learn more about personalization.

What Now?

Using Stream as a building block, we were able to build scalable feeds in just a few hours. That’s quite cool if you compare it to the months/years it took popular apps to build their feeds.

In the next post we’ll cover how we’re using imgix to power the real-time image edits for Cabin.

Add your email on cabin.getstream.io or follow @getstream_io on Twitter to stay up to date about the latest Cabin tutorials.

Stay updated to upcoming posts in this React & Redux series:


Originally published at The Stream Blog.