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 getstream.io/cabin for an overview of all 8 tutorials and a live demo.

Visit getstream.io/cabin for an overview of all the tutorials, as well as a live demo. The source code can be found on the Stream GitHub repository for Cabin, and all blog posts can be found at their respective links below:

  1. Introduction
  2. React
  3. Redux
  4. Stream
  5. Imgix
  6. Keen
  7. Algolia
  8. Mapbox
  9. Best Practices and Gotchas

Introduction

We’re using Stream to power the feeds in CabinStream 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.

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.

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.

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.

Activities

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

https://gist.github.com/nparsons08/6143bd0a5cbf84ebca1e41980ab421a1

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_aggregated (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:

https://gist.github.com/nparsons08/65251335d3fd8a15cd2ca2c9cab4094e

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

https://gist.github.com/nparsons08/570f1171e646ac9c6987f7272823ed84

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:

https://gist.github.com/nparsons08/7407702ea194e8797ae8246e77066994

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

https://gist.github.com/nparsons08/d84fa204ae510baca3309a19208485eb

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: Now, take a look at the server side (API) code that powers an “upload” activity to the feed:

https://gist.github.com/nparsons08/529d430747f2ccb50ace0f5371076379

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. Luckily, Stream makes it easy for us to follow other users’ feeds in just a few simple steps. Take a look:

https://gist.github.com/nparsons08/42d4e8bbb1cd299d04cb1e9ca1fc5495

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 these 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:

https://gist.github.com/nparsons08/3f8c5eaac322a7a2c4d20dcfd0779a4d

Into this enriched object:

https://gist.github.com/nparsons08/dd7c15606959d4f0ea9830bf4a6a1716

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:

https://gist.github.com/nparsons08/d4c3f1e0ed5f68977529b1ea975b52d3

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

https://gist.github.com/nparsons08/0d92efe9ebc3255f3758e711870a09db

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

https://gist.github.com/nparsons08/6e40e4f9150f167bd0664e4c3b41b22d

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.

https://gist.github.com/nparsons08/fb08d60fdbff95ef782f458e65c5a2f3

Once translated, we end up something like this:

https://gist.github.com/nparsons08/5062f5c1dffcff0b4b9eb8f9b5f10fd6

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:

https://gist.github.com/nparsons08/c588106504c9790b20ac1a28a165157b

  1. View code in app/modules/App.js - Subscribe to the changes:

https://gist.github.com/nparsons08/93090b05b2c69677c87d2e618fb2d3ec

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

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 https://getstream.io/cabin or follow @getstream_io on Twitter to stay up to date about the latest Cabin tutorials.

Open Source

Cabin