In this article, the third part of the Build a Twitter Clone series, you will add support for tweet reactions (likes and comments), threads, and a notifications page.
Part 1 focuses on creating the Twitter layout, authenticating users with Stream, adding the create tweet feature, and displaying the home page activity feeds. Part 2 focuses on creating a profile page for users and adding the follow-users feature. Please check out these parts, if you haven't, before continuing with this part.
Add Tweet Reactions
From the previous steps, I have walked you through building the Twitter layout and the TweetBlock component:
This component shows four actions: comment, retweet, like, and share. For the scope of this tutorial, we will only focus on the comment and like actions which are currently not functional. So, let's make them functional.
Add a Like Reaction
You will create a custom hook for the like reaction functionality to easily manage it. In Part 1, we concluded with src/components/Tweet/TweetBlock.js having an onToggleLike
function in the TweetBlock
component, which currently does nothing:
const onToggleLike = () => {
// toggle like reaction
}
To make this function work, firstly, let's create the hook. Create a new file src/hooks/useLike.js with the following code:
import { useFeedContext } from 'react-activity-feed'
export default function useLike() {
const feed = useFeedContext()
const toggleLike = async (activity, hasLikedTweet) => {
await feed.onToggleReaction('like', activity)
}
return { toggleLike }
}
Thefeed
object from the useFeedContext
hook has different methods that can be applied to the activities in the feed the TweetBlock
is used. This feed can be the timeline feed for the homepage or the user feed for the profile page.
The toggleLike
function from the hook receives two arguments: the activity
to be liked/unliked and a hasLikedTweet
boolean, which is true if the logged-in user has liked the tweet already. You will use the hasLikedTweet
argument later when you add notifications.
The onToggleReaction
method on the feed
object takes a type of reaction (in this case, like) and the activity it should be applied to (the current activity the TweetBlock
component is used for), and it toggles between liking and unliking for a logged-in user.
To add the like reaction functionality, import this hook to the TweetBlock component:
// other imports
import useLike from '../../hooks/useLike'
Then update the onToggleLike
function to this:
const onToggleLike = async () => {
await toggleLike(activity, hasLikedTweet)
}
To test this, go to a tweet in your application either made by the logged-in user or a different user and click on the heart icon. You should have this when you click:
The toggle happens when you click it again.
In Part 1, we applied styles for the heart icon to be red when clicked, just in case you're wondering 😅.
You can also test this by logging in with a different user and liking the same tweet. You will see the like count incremented:
Add a Comment Reaction
The current state of the comment functionality is that when a user clicks the comment icon on a tweet block, the comment dialog shows, and the user can type a comment, but on submitting, nothing happens. In previous parts, we concluded with src/components/Tweet/TweetBlock.js having the CommentDialog
component attached to an onPostComment
function that does nothing:
const onPostComment = async (text) => {
// create comment
}
To add the comment reaction, we will make this a custom hook. This functionality will be used in the TweetBlock component and the Thread component (for when a tweet is expanded to show comments).
Create a new file src/hooks/useComment.js with the following code:
import { useFeedContext } from 'react-activity-feed'
export default function useComment() {
const feed = useFeedContext()
const createComment = async (text, activity) => {
await feed.onAddReaction('comment', activity, {
text,
})
}
return {
createComment,
}
}
With the onAddReaction
method of the feed
object, you can add the comment reaction to an activity and pass the comment text.
To use this hook in src/components/Tweet/TweetBlock.js, first import it:
// other imports
import useComment from '../../hooks/useComment'
Then, get the createComment
function in the TweetBlock
component:
const { createComment } = useComment()
And finally, update the onPostComment
function to this:
const onPostComment = async (text) => {
await createComment(text, activity)
}
With this addition, when you enter a comment, you will see the comment reactions incremented.
So far, we have added the like and comment reactions, but we haven't added threads yet. A thread view will show a tweet expanded, showing the comments in a tweet. So, let's add that next.
Add a Tweet Thread page
The thread page shows a single tweet, the tweet action buttons, a comment form, and the comments made on the tweet:
This thread view is broken into sections, so we'll build it section by section.
Create the ThreadHeader Component
The ThreadHeader component shows the back button and the tweet text.
Create a new file src/components/Thread/ThreadHeader.js, and paste the following:
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import ArrowLeft from '../Icons/ArrowLeft'
const Header = styled.header`
display: flex;
align-items: center;
padding: 15px;
button {
width: 25px;
height: 20px;
margin-right: 40px;
}
span {
font-size: 20px;
color: white;
font-weight: bold;
}
`
export default function ThreadHeader() {
const navigate = useNavigate()
const navigateBack = () => {
navigate(-1)
}
return (
<Header>
<button onClick={navigateBack}>
<ArrowLeft size={20} color="white" />
</button>
<span>Tweet</span>
</Header>
)
}
Using useNavigate
from react-router-dom
, you can navigate the user to the previous page they were on in the history session.
Create the TweetContent Component
This component shows the tweet information, the tweet action buttons, a tweet form to add a comment, and tweet blocks for comments.
The tweet blocks in this component are a little different from the normal tweet blocks we created in Part 1. As you will notice, this block does not have reactions. To avoid so much conditional rendering going on in the TweetBlock component, you will create another tweet block component--TweetCommentBlock.
Create a TweetCommentBlock Component
Create a new file src/components/Thread/TweetCommentBlock.js. Start with imports and styles:
import styled from 'styled-components'
import { formatStringWithLink } from '../../utils/string'
import More from '../Icons/More'
import TweetActorName from '../Tweet/TweetActorName'
const Block = styled.div`
display: flex;
border-bottom: 1px solid #333;
padding: 15px 0;
.user-image {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 15px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.comment-tweet {
flex: 1;
.link {
display: block;
padding-bottom: 5px;
text-decoration: none;
}
&__text {
color: white;
font-size: 15px;
line-height: 20px;
margin-top: 3px;
&--link {
color: var(--theme-color);
text-decoration: none;
}
}
}
.more {
width: 30px;
height: 20px;
display: flex;
opacity: 0.6;
}
`
And for the component:
export default function TweetCommentBlock({ comment }) {
const { user, data: tweetComment } = comment
return (
<Block to="/">
<div className="user-image">
<img src={user.data.image} alt="" />
</div>
<div className="comment-tweet">
<div>
<TweetActorName
name={user.data.name}
id={user.id}
time={comment.created_at}
/>
<div className="tweet__details">
<p
className="comment-tweet__text"
dangerouslySetInnerHTML={{
__html: formatStringWithLink(
tweetComment.text,
'tweet__text--link'
).replace(/\n/g, '<br/>'),
}}
/>
</div>
</div>
</div>
<button className="more">
<More size={18} color="white" />
</button>
</Block>
)
}
The TweetCommentBlock
receives the comment
prop, a comment activity object. From the comment
object, you can get the user
and the data
object (which you have assigned to the tweetComment
variable).
Composing the TweetContent Component
Create a new file src/components/Thread/TweetContent.js. Add the imports for the component:
import { format } from 'date-fns'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { useState } from 'react'
import { formatStringWithLink } from '../../utils/string'
import BarChart from '../Icons/BarChart'
import Comment from '../Icons/Comment'
import Retweet from '../Icons/Retweet'
import Heart from '../Icons/Heart'
import Upload from '../Icons/Upload'
import TweetForm from '../Tweet/TweetForm'
import TweetCommentBlock from './TweetCommentBlock'
import CommentDialog from '../Tweet/CommentDialog'
import More from '../Icons/More'
import useComment from '../../hooks/useComment'
import useLike from '../../hooks/useLike'
There are many icons here for the actions for the tweet. Also, you will use the useComment
hook here for the comment form.
Next, the styles:
const Container = styled.div`
padding: 10px 15px;
.user {
display: flex;
text-decoration: none;
&__image {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 15px;
img {
width: 100%;
height: 100%;
}
}
&__name {
&--name {
color: white;
font-weight: bold;
}
&--id {
color: #52575b;
font-size: 14px;
}
}
&__option {
margin-left: auto;
}
}
.tweet {
margin-top: 20px;
a {
text-decoration: none;
color: var(--theme-color);
}
&__text {
color: white;
font-size: 20px;
}
&__time,
&__analytics,
&__reactions,
&__reactors {
height: 50px;
display: flex;
align-items: center;
border-bottom: 1px solid #555;
font-size: 15px;
color: #888;
}
&__time {
&--date {
margin-left: 12px;
position: relative;
&::after {
position: absolute;
content: '';
width: 2px;
height: 2px;
background-color: #777;
border-radius: 50%;
top: 0;
bottom: 0;
left: -7px;
margin: auto 0;
}
}
}
&__analytics {
&__text {
margin-left: 7px;
}
}
&__reactions {
&__likes {
display: flex;
.reaction-count {
color: white;
font-weight: bold;
}
.reaction-label {
margin-left: 4px;
}
}
}
&__reactors {
justify-content: space-between;
padding: 0 50px;
}
}
.write-reply {
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #555;
}
`
Next, the component:
export default function TweetContent({ activity }) {
const feed = useFeedContext()
const { client } = useStreamContext()
const { createComment } = useComment()
const { toggleLike } = useLike()
const time = format(new Date(activity.time), 'p')
const date = format(new Date(activity.time), 'PP')
const tweet = activity.object.data
const tweetActor = activity.actor.data
const [commentDialogOpened, setCommentDialogOpened] = useState(false)
let hasLikedTweet = false
if (activity?.own_reactions?.like) {
const myReaction = activity.own_reactions.like.find(
(l) => l.user.id === client.userId
)
hasLikedTweet = Boolean(myReaction)
}
const onToggleLike = async () => {
await toggleLike(activity, hasLikedTweet)
feed.refresh()
}
const reactors = [
{
id: 'comment',
Icon: Comment,
onClick: () => setCommentDialogOpened(true),
},
{ id: 'retweet', Icon: Retweet },
{
id: 'heart',
Icon: Heart,
onClick: onToggleLike,
},
{ id: 'upload', Icon: Upload },
]
const onPostComment = async (text) => {
await createComment(text, activity)
feed.refresh()
}
}
Just like I showed you in Part 1, the hasLikedTweet
variable is initialized and updated to hold a boolean value if the logged-in user has liked this tweet or not.
Similar to the like reaction functionality you created earlier, the onToggleLike
function here uses the onToggleReaction
method on the feed
object. Also, the refresh
method on the feed
object is used to refresh the feed. This part is relevant because, unlike the FlatFeed
component, which automatically refreshes upon reactions, the Feed
component, which you will soon use, does not.
Also, the onPostComment
function uses the createComment
function from the useComment
hook and refreshes the feed after a successful comment.
Next, the UI:
export default function TweetContent() {
//
return (
<>
{commentDialogOpened && (
<CommentDialog
activity={activity}
onPostComment={onPostComment}
onClickOutside={() => setCommentDialogOpened(false)}
/>
)}
<Container>
<Link to={`/${tweetActor.id}`} className="user">
<div className="user__image">
<img src={tweetActor.image} alt="" />
</div>
<div className="user__name">
<span className="user__name--name">{tweetActor.name}</span>
<span className="user__name--id">@{tweetActor.id}</span>
</div>
<div className="user__option">
<More color="#777" size={20} />
</div>
</Link>
<div className="tweet">
<p
className="tweet__text"
dangerouslySetInnerHTML={{
__html: formatStringWithLink(
tweet.text,
'tweet__text--link'
).replace(/\n/g, '<br/>'),
}}
/>
<div className="tweet__time">
<span className="tweet__time--time">{time}</span>
<span className="tweet__time--date">{date}</span>
</div>
<div className="tweet__analytics">
<BarChart color="#888" />
<span className="tweet__analytics__text">View Tweet Analytics</span>
</div>
<div className="tweet__reactions">
<div className="tweet__reactions__likes">
<span className="reaction-count">
{activity.reaction_counts.like || '0'}
</span>
<span className="reaction-label">Likes</span>
</div>
</div>
<div className="tweet__reactors">
{reactors.map((action, i) => (
<button onClick={action.onClick} key={`reactor-${i}`}>
<action.Icon
color={
action.id === 'heart' && hasLikedTweet
? 'var(--theme-color)'
: '#888'
}
fill={action.id === 'heart' && hasLikedTweet && true}
size={20}
/>
</button>
))}
</div>
</div>
<div className="write-reply">
<TweetForm
onSubmit={onPostComment}
submitText="Reply"
collapsedOnMount={true}
placeholder="Tweet your reply"
replyingTo={tweetActor.id}
/>
</div>
{activity.latest_reactions?.comment?.map((comment) => (
<TweetCommentBlock key={comment.id} comment={comment} />
))}
</Container>
</>
)
}
There are two ways to make comments in the UI. First, there's the comment form where users can type a comment and submit. The second way is by clicking the comment icon, which opens the CommentDialog
component for typing a comment.
On the activity
object, you loop through the latest_reactions.comment
array to display the comments with the TweetCommentBlock
component.
Create the ThreadContent Component
This component is made up of the ThreadHeader and TweetContent components. Create a new file called src/components/Thread/ThreadContent.js. Start with the imports:
import { useEffect, useState } from 'react'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import { useParams } from 'react-router-dom'
import LoadingIndicator from '../LoadingIndicator'
import TweetContent from './TweetContent'
import ThreadHeader from './ThreadHeader'
With useParams
, you will get the id
of the tweet from the URL. Tweet links exist in this format: /[actorId]/status/[tweetActivityId].
Next, the component:
export default function ThreadContent() {
const { client } = useStreamContext()
const { id } = useParams()
const feed = useFeedContext()
const [activity, setActivity] = useState(null)
useEffect(() => {
if (feed.refreshing || !feed.hasDoneRequest) return
const activityPaths = feed.feedManager.getActivityPaths(id) || []
if (activityPaths.length) {
const targetActivity = feed.feedManager.state.activities
.getIn([...activityPaths[0]])
.toJS()
setActivity(targetActivity)
}
}, [feed.refreshing])
if (!client || !activity) return <LoadingIndicator />
return (
<div>
<ThreadHeader />
<TweetContent activity={activity} />
</div>
)
}
feedManager.getActivityPaths
of the feed
object returns an array with an id for the current tweet link. This line is essential to ensure that the activity exists. If it returns an empty array, then the tweet link does not exist.
feed.feedManager.state.activities
is an immutable Map (created with Immutabe.js), so you get the activity object using getIn
and toJS
methods.
With the activity
obtained, you pass it to the TweetContent
component.
Create the Thread Page
Create a new file called src/pages/Thread.js and paste the following:
import { Feed, useStreamContext } from 'react-activity-feed'
import { useParams } from 'react-router-dom'
import Layout from '../components/Layout'
import ThreadContent from '../components/Thread/ThreadContent'
const FEED_ENRICH_OPTIONS = {
withRecentReactions: true,
withOwnReactions: true,
withReactionCounts: true,
withOwnChildren: true,
}
export default function Thread() {
const { user } = useStreamContext()
const { user_id } = useParams()
return (
<Layout>
<Feed
feedGroup={user.id === user_id ? 'user' : 'timeline'}
options={FEED_ENRICH_OPTIONS}
notify
>
<ThreadContent />
</Feed>
</Layout>
)
}
For the feedGroup
, you check if the currently logged-in user made the tweet, of which you use "user", and if it's a different user, you use "timeline". This is because a tweet exists in one of these feeds, not on both.
The FEED_ENRICH_OPTIONS
is relevant so you can get the reactions with each activity. Without this, you will have to make a separate API request to get the comments in the TweetContent
component.
Lastly, you need to create a route for this component. Go to src/components/App.js. Import the thread page:
// other imports
import Thread from './pages/Thread'
And add a route for this component:
<Route element={<Thread />} path="/:user_id/status/:id" />
With all these plugged in correctly, when you click a tweet block, you will find the thread view. This view also shows the comment reactions made to a tweet.
You can make more comments using the comment dialog or the comment form:
Add the Notifications Page
The notifications page will show new follows, likes, and comment notifications:
The idea with the notifications implementation is to create activities in the notification feed (created in Part 1 when creating feed groups when actions occur). This implies that when you trigger a "like" action, you create an activity in the notification feed with the "like" verb and a reference to the tweet you liked. Similarly, you'll do the same for comments and follow actions.
Before creating a Notifications page, let's start by creating these activities upon these actions we want notifications for.
Create a useNotification hook
Since notifications will be used for different things, making the functionality a hook would be easier to manage. Create a new file src/hooks/useNotification.js with the following code:
import { useStreamContext } from 'react-activity-feed'
export default function useNotification() {
const { client } = useStreamContext()
const createNotification = async (userId, verb, data, reference = {}) => {
const userNotificationFeed = client.feed('notification', userId)
const newActivity = {
verb,
object: reference,
...data,
}
await userNotificationFeed.addActivity(newActivity)
}
return { createNotification }
}
The returned createNotification
function from the hook receives four arguments:
-
userId
: id of the user you want to add the notification for -
verb
: the label for the activity -
data
: for other properties to add to the activity, for example, the text of a comment -
reference
: this is optional, but it can be used for referencing a collection, like a tweet, for example
Create Notifications on Reactions and Follows
In this section, you will use this hook on reactions and follow actions.
Create Notifications on Like Reactions
Go to src/hooks/useLike.js to add the hook. First, import the hook:
// other imports
import useNotification from './useNotification'
import { useStreamContext } from 'react-activity-feed'
You will need the user
object from the useStreamContext
hook, as you will see soon.
Import the createNotification
function and the user
object:
// ...
const { createNotification } = useNotification()
const { user } = useStreamContext()
Then, update the toggleLike
function to create a notification on liking a tweet:
const toggleLike = async (activity, hasLikedTweet) => {
const actor = activity.actor
await feed.onToggleReaction('like', activity)
if (!hasLikedTweet && actor.id !== user.id) {
// then it is not the logged in user liking their own tweet
createNotification(actor.id, 'like', {}, `SO:tweet:${activity.object.id}`)
}
}
The toggleLike
function first checks if the tweet has not been liked and the actor of the tweet is not the same as the logged-in user. This check is necessary to ensure that the user does not get a notification upon liking their tweet.
In the last argument, the reference passed to the createNotification
function refers to the tweet collection.
When you like a tweet, a new activity is added to the notification feed. You can try this by going to a different user's account and liking one of @getstream_io's tweets. On the Feeds Explorer on your dashboard, you will see the notification:getstream_io created:
And when you browse the activities in this feed, you will find the new like activity you created:
Because you created a notification feed group (in Part 1), you can see the is_read
and is_seen
property. Also, the activities are grouped if they are similar.
Create Notifications on Comment Reactions
Similar to what you did in the previous step, go to src/hooks/useComment.js and import the required hooks:
import { useStreamContext } from 'react-activity-feed'
import useNotification from './useNotification'
Next, get the createNotification
function and user
object in the useComment
hook:
// ...
const { createNotification } = useNotification()
const { user } = useStreamContext()
And finally, update the createComment
function:
const createComment = async (text, activity) => {
const actor = activity.actor
await feed.onAddReaction('comment', activity, {
text,
})
if (actor.id !== user.id) {
// then it is not the logged in user commenting on their own tweet
createNotification(
actor.id,
'comment',
{
text,
},
`SO:tweet:${activity.object.id}`
)
}
}
The createComment
function also ensures that there are no notifications sent if the same actor of tweet comments on the tweet.
You can test this notification by commenting on a tweet and checking your Feed's explorer.
Create Notifications on Follow Actions
One more notification you want to add is for follow actions. In the useFollow hook in src/hooks/useFollow.js, import the notification hook:
// other imports
import useNotification from './useNotification'
Then, update the toggleFollow
function to this:
const { createNotification } = useNotification()
const toggleFollow = async () => {
const action = isFollowing ? 'unfollow' : 'follow'
if (action === 'follow') {
await createNotification(userId, 'follow')
}
const timelineFeed = client.feed('timeline', client.userId)
await timelineFeed[action]('user', userId)
setIsFollowing((isFollowing) => !isFollowing)
}
In this function, you check if the action is follow and create a follow activity in the notification feed.
You can also test this by following a user and checking your Feeds dashboard.
With these notifications created, now you want to display them.
Create a NotificationContent Component
This component houses the notification header and the notifications for different actions.
To display the different activities in the notifications feed, you will use the NotificationFeed. This component displays the notifications in groups. But you will provide a custom component to handle this grouping.
Creating Grouping Components for Notifications
There are three forms of notifications: like, comment, and follow notifications. The structure of the group is like this:
{
activities: [...activities created on like action],
activity_count: NUMBER OF ACTIVITIES,
actor_count: NUMBER OF ACTORS IN THE ACTIVITIES,
created_at: ...,
group: GROUP ID BASED ON VERB AND DATE,
id: ...,
is_read: ...,
is_seen: ...,
verb: VERB OF GROUPED ACTIVITIES,
}
Let's create grouping components for them.
Create a LikeNotification Group Component
Create a new file src/components/Notification/LikeNotification.js. Add the imports and styles:
import { useStreamContext } from 'react-activity-feed'
import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Heart from '../Icons/Heart'
const Block = styled.button`
padding: 15px;
border-bottom: 1px solid #333;
display: flex;
a {
color: white;
}
span {
display: inline-block;
}
.right {
margin-left: 20px;
flex: 1;
}
.liked-actors__images {
display: flex;
&__image {
width: 35px;
height: 35px;
border-radius: 50%;
overflow: hidden;
margin-right: 10px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.liked-actors__text {
margin-top: 10px;
color: white;
font-size: 15px;
.liked-actor__name {
font-weight: bold;
&:hover {
text-decoration: underline;
}
}
}
.tweet-text {
display: block;
color: #888;
margin-top: 10px;
}
`
With the useNavigate
hook, you will navigate to the tweet that was liked when a user clicks on the notification.
Next, for the component:
export default function LikeNotification({ likedActivities }) {
const likedGroup = {}
const navigate = useNavigate()
const { user } = useStreamContext()
likedActivities.forEach((act) => {
if (act.object.id in likedGroup) {
likedGroup[act.object.id].push(act)
} else likedGroup[act.object.id] = [act]
})
}
This component receives the activities
array from the like group.
You create a likedGroup
object that groups activities by the tweet they were made on. The grouping from the notification feeds contains different like activities on tweets.
The next step is to loop over the likedGroup
to display the like notifications:
export default function LikeNotification({ likedActivities }) {
// ...
return (
<>
{Object.keys(likedGroup).map((groupKey) => {
const activities = likedGroup[groupKey]
const lastActivity = activities[0]
const tweetLink = `/${user.id}/status/${lastActivity.object.id}`
return (
<Block
className="active"
onClick={() => navigate(tweetLink)}
key={groupKey}
>
<Heart color="var(--theme-color)" size={25} fill={true} />
<div className="right">
<div className="liked-actors__images">
{activities.map((act) => (
<Link
to={`/${act.actor.id}`}
key={act.id}
className="liked-actors__images__image"
>
<img src={act.actor.data.image} alt="" />
</Link>
))}
</div>
<span className="liked-actors__text">
<Link
className="liked-actor__name"
to={`/${lastActivity.actor.id}`}
>
{lastActivity.actor.data.name}
</Link>{' '}
<span to={tweetLink}>
{activities.length > 1 &&
`and ${activities.length - 1} others`}{' '}
liked your Tweet
</span>
</span>
<p className="tweet-text">{lastActivity.object.data.text}</p>
</div>
</Block>
)
})}
</>
)
}
You loop over each tweet in the likedGroup
and also loop over the like activities in the tweet to display the author's information.
Create a CommentNotification Group Component
Create a new file src/components/Notification/CommentNotification.js. Add the imports and styles:
import { Link, useNavigate } from 'react-router-dom'
import { useStreamContext } from 'react-activity-feed'
import styled from 'styled-components'
import { generateTweetLink } from '../../utils/links'
import TweetActorName from '../Tweet/TweetActorName'
const Block = styled.button`
padding: 15px;
border-bottom: 1px solid #333;
display: flex;
a {
color: white;
}
.user__image {
width: 35px;
height: 35px;
overflow: hidden;
border-radius: 50%;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user__details {
margin-left: 20px;
flex: 1;
}
.user__reply-to {
color: #555;
font-size: 15px;
margin-top: 3px;
a {
color: var(--theme-color);
&:hover {
text-decoration: underline;
}
}
}
.user__text {
display: block;
color: white;
margin-top: 10px;
}
`
Next, the component:
export default function CommentNotification({ commentActivities }) {
const navigate = useNavigate()
const { user } = useStreamContext()
return (
<>
{commentActivities.map((cAct) => {
const actor = cAct.actor
const tweetLink = generateTweetLink(cAct.replyTo, cAct.object.id)
return (
<Block key={cAct.id} onClick={() => navigate(tweetLink)}>
<Link to={`/${actor.id}`} className="user__image">
<img src={actor.data.image} alt="" />
</Link>
<div className="user__details">
<TweetActorName
id={actor.id}
name={actor.data.name}
time={cAct.time}
/>
<span className="user__reply-to">
Replying to <Link to={`/${user.id}`}>@{user.id}</Link>
<p className="user__text">{cAct.text}</p>
</span>
</div>
</Block>
)
})}
</>
)
}
This component receives the commentActivities
prop, which is the activities
array from the comment group. In this component, you loop through the comments and display the user information and the comment text.
Create a FollowNotification Group Component
Create a new file src/components/Notification/FollowNotification.js. Add the imports and styles:
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import User from '../Icons/User'
const Block = styled.div`
padding: 15px;
border-bottom: 1px solid #333;
display: flex;
a {
color: white;
}
.right {
margin-left: 20px;
flex: 1;
}
.actors__images {
display: flex;
&__image {
width: 35px;
height: 35px;
border-radius: 50%;
overflow: hidden;
margin-right: 10px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.actors__text {
margin-top: 10px;
color: white;
font-size: 15px;
span {
display: inline-block;
}
.actors__name {
font-weight: bold;
&:hover {
text-decoration: underline;
}
}
}
`
Next, the component:
export default function FollowNotification({ followActivities }) {
const firstActivity = followActivities[0]
return (
<Block>
<User color="#1c9bef" size={25} />
<div className="right">
<div className="actors__images">
{followActivities.map((follow) => {
return (
<Link
to={`/${follow.actor.id}`}
className="actors__images__image"
key={follow.id}
>
<img src={follow.actor.data.image} alt="" />
</Link>
)
})}
</div>
<p className="actors__text">
<Link className="actors__name" to={`/${firstActivity.actor.id}`}>
{firstActivity.actor.data.name}
</Link>{' '}
<span>
{followActivities.length > 1 &&
`and ${followActivities.length - 1} others`}{' '}
followed you
</span>
</p>
</div>
</Block>
)
}
This component receives the followActivities
prop, which is the activities
array of the follow group. In this component, you get the first activity from the array so that you can display, "Person A and 5 others followed you".
With these group components created, you can put them together to form a NotificationGroup component.
Create a NotificationGroup Component
Create a new file src/components/Notification/NotificationGroup.js file. Add imports and styles:
import { useEffect, useRef } from 'react'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import styled from 'styled-components'
import CommentNotification from './CommentNotification'
import FollowNotification from './FollowNotification'
import LikeNotification from './LikeNotification'
const Container = styled.div`
button {
width: 100%;
}
`
Next, the component:
export default function NotificationGroup({ activityGroup }) {
const feed = useFeedContext()
const notificationContainerRef = useRef()
const activities = activityGroup.activities
const { user, client } = useStreamContext()
useEffect(() => {
// stop event propagation on links
if (!notificationContainerRef.current) return
const anchorTags = notificationContainerRef.current.querySelectorAll('a')
anchorTags.forEach((element) => {
element.addEventListener('click', (e) => e.stopPropagation())
})
return () =>
anchorTags.forEach((element) => {
element.addEventListener('click', (e) => e.stopPropagation())
})
}, [])
useEffect(() => {
const notifFeed = client.feed('notification', user.id)
notifFeed.subscribe((data) => {
if (data.new.length) {
feed.refresh()
}
})
return () => notifFeed.unsubscribe()
}, [])
}
In the first useEffect
expression, you stop event propagation on all links in the container ref. The relevance of this is that when you click on a user's name in a like notification block, you don't want the notification block to also navigate to the tweet that was liked.
In the second useEffect
expression, you subscribe to the notification feed of the logged-in user. On new notifications, you call the refresh
method on the feed
object so that the new notifications are displayed.
Finally, for this component, the UI:
export default function NotificationGroup() {
// ...
return (
<Container ref={notificationContainerRef}>
{activityGroup.verb === 'like' && (
<LikeNotification likedActivities={activities} />
)}
{activityGroup.verb === 'follow' && (
<FollowNotification followActivities={activities} />
)}
{activityGroup.verb === 'comment' && (
<CommentNotification commentActivities={activities} />
)}
</Container>
)
}
In the UI, you check the verb of the group and render the group notification accordingly.
Composing the NotificationContent Component
Create a new file src/components/Notification/NotificationContent.js. Add the imports and styles:
import classNames from 'classnames'
import { useState } from 'react'
import { NotificationFeed } from 'react-activity-feed'
import styled from 'styled-components'
import NotificationGroup from './NotificationGroup'
const Container = styled.div`
h1 {
padding: 15px;
font-size: 16px;
color: white;
}
.tab-list {
margin-top: 10px;
border-bottom: 1px solid #333;
display: grid;
grid-template-columns: 1fr 1fr;
.tab {
color: #777;
padding: 0 35px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 15px;
&:hover {
background-color: #111;
}
&__label {
position: relative;
padding: 20px 30px;
&.active {
color: white;
&::after {
content: '';
height: 3px;
width: 100%;
background-color: var(--theme-color);
border-radius: 40px;
position: absolute;
bottom: 0;
left: 0;
}
}
}
}
}
`
Next, the component:
const tabList = [
{
id: 'all',
label: 'All',
},
{
id: 'mentions',
label: 'Mentions',
},
]
export default function NotificationContent() {
const [activeTab, setActiveTab] = useState(tabList[0].id)
return (
<Container>
<h1>Notifications</h1>
<div className="tab-list">
{tabList.map((tab) => (
<button
onClick={() => setActiveTab(tab.id)}
className="tab"
key={tab.id}
>
<span
className={classNames(
'tab__label',
activeTab === tab.id && 'active'
)}
>
{tab.label}
</span>
</button>
))}
</div>
<NotificationFeed Group={NotificationGroup} />
</Container>
)
}
Although the tab list is not functional, it's nice to have. In this component, you use the NotificationFeed
and pass the NotificationGroup
component to the Group
prop.
Creating the Notifications Page
Create a new file src/pages/Notifications.js with the following code:
import Layout from '../components/Layout'
import NotificationContent from '../components/Notification/NotificationContent'
export default function Notifications() {
return (
<Layout>
<NotificationContent />
</Layout>
)
}
And also, add a route in App.js for this page:
// other imports
import Notifications from './pages/Notifications'
<Route element={<Notifications />} path="/notifications" />
Show a Notification Counter
When a user has unread notifications, you will show the count of those notifications in a badge on the Notifications link:
This notification link exists in the LeftSide component. Go to src/components/LeftSide.js and import useEffect
:
// other imports
import { useEffect } from 'react'
When this component mounts, you will query the notification feed of the logged-in user, get the notifications that haven't been seen (the is_seen
property will be false
), and display the count. In the LeftSide
component, add the following:
export default function LeftSide({ onClickTweet }) {
// ...other things
const { client, userData } = useStreamContext()
useEffect(() => {
if (!userData || location.pathname === `/notifications`) return
let notifFeed
async function init() {
notifFeed = client.feed('notification', userData.id)
const notifications = await notifFeed.get()
const unread = notifications.results.filter(
(notification) => !notification.is_seen
)
setNewNotifications(unread.length)
notifFeed.subscribe((data) => {
setNewNotifications(newNotifications + data.new.length)
})
}
init()
return () => notifFeed?.unsubscribe()
}, [userData])
// other things
}
When the component mounts, you create an init
function and evoke it. In this function, you get all the activities in the notification feed; then, you filter out the notifications that have been seen to find the unread ones. Next, you update the newNotifications
state with the length of the unread array.
Also, you subscribe to the notification feed so that when a new activity is added to the notification feed, you update the newNotifications
state.
Remember earlier you triggered some notifications on getstream_io's account by liking, commenting on their tweet, and following them. Now when you log into getstream_io's account and click the notifications link on the left sidebar, you will see the notification activities made on their feed like this:
And there you have it, your Twitter clone!
Conclusion
There are more features that can be added to this clone project, but we have focused on some functionalities that allow you to understand activity feeds and how Stream feeds provides solutions for feed-based applications.
Find the complete source code of the clone in this repository.
Please give the react-activity-feed repository a star if you enjoyed this tutorial.
As a recap:
- in Part 1, we built most of the layout and shared components and also added the create-tweet feature
- in Part 2, we added a profile page for users and also created the follow-user functionality
- in this part, we added support for like and comment reactions and created notifications for each action.
Overall in this Twitter clone, you should now understand the concept of:
- activity feeds (tweets or notification activities)
- subscribing to a feed (following a user)
There are many more ways you apply feeds. You can use them in forums (where a user can subscribe to a topic or discussion), e-commerce platforms (where users can follow a product feed and get updated when new related products are added), and social media platforms.
We have other feeds SDKs to allow you to integrate feeds in different languages and platforms. Do check it out.
Top comments (0)