Skip to content

Instantly share code, notes, and snippets.

@technoplato
Created September 8, 2019 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save technoplato/90fc11294d71860eb046f98f5da8ea5f to your computer and use it in GitHub Desktop.
Save technoplato/90fc11294d71860eb046f98f5da8ea5f to your computer and use it in GitHub Desktop.
Likes with Firestore
import React from 'react'
import { FlatList, View, Share } from 'react-native'
import firestore from '@react-native-firebase/firestore'
import PostItem from './PostItem'
import ShowNewPostsButton from './ShowNewPostsButton'
export default class PostsList extends React.PureComponent {
// Staged posts are posts that have been added remotely but not shown yet.
state = { posts: {}, staged: {} }
PAGE_SIZE = 10
async componentDidMount() {
this.postsRef = firestore().collection('posts')
this.oldestPostTime = new Date().getTime()
this.next = this.postsRef.orderBy('created', 'desc').limit(this.PAGE_SIZE)
this.changesUnsubscribe = () =>
console.log(
'This method will be used to unsubscribe our listener when we fetch older posts.'
)
this.loadMorePosts()
}
loadMorePosts = async () => {
if (this.state.noOlderPostsAvailable) return
this.changesUnsubscribe()
const newPosts = await this.next.get().then(this.parsePostsSnapshot)
const numNewPosts = newPosts.length
const noOlderPostsAvailable = numNewPosts < this.PAGE_SIZE
const posts = { ...this.state.posts }
newPosts.forEach(post => (posts[post.id] = post))
this.oldestPostTime = newPosts[numNewPosts - 1].created
this.next = firestore()
.collection('posts')
.orderBy('created', 'desc')
.startAt(this.oldestPostTime)
.limit(this.PAGE_SIZE)
await new Promise(res =>
this.setState({ posts, noOlderPostsAvailable }, () => res())
)
this.changesUnsubscribe = firestore()
.collection('posts')
.orderBy('created', 'desc')
.endAt(this.oldestPostTime)
.onSnapshot(this.onPostsUpdated)
}
parsePostsSnapshot = collection => {
const numPosts = collection.size
if (numPosts === 0) {
console.log(
"0 docs fetched, `parsePostsSnapshot` shouldn't have been called."
)
return []
} else if (numPosts < this.PAGE_SIZE) {
console.log('No older posts exist. Only listen for new posts now.')
}
return collection.docs.map(doc => this.prunePost(doc.data()))
}
onPostsUpdated = postsCollection => {
const posts = { ...this.state.posts }
postsCollection.docChanges().forEach(({ type, doc }) => {
const post = doc.data()
if (type === 'added') {
if (!posts[post.id]) {
// If the post is already present, do not add it again.
// Firestore snapshot does not have simple functionality to only
// listen to changes on windows of data.
this.stagePost(post)
}
}
if (type === 'modified') {
posts[post.id] = this.prunePost(post)
}
if (type === 'removed') {
delete posts[post.id]
}
})
this.setState({ posts })
}
prunePost = post => ({
...post,
liked: post.likes.includes(this.props.userId),
likes: null
})
stagePost = post => {
const staged = { ...this.state.staged }
staged[post.id] = this.prunePost(post)
this.setState({ staged })
}
handleLikePressed = async (postId, wasLiked) => {
// Optimistic update
this.setLocalPostLikeStatus(postId, !wasLiked)
const updateSucceeded = await this.setRemotePostLikeStatus(
postId,
!wasLiked
)
// Revert to previous like state if update fails
if (!updateSucceeded) {
this.setLocalPostLikeStatus(postId, wasLiked)
}
}
setLocalPostLikeStatus = (postId, isLiked) => {
this.setState(state => {
const posts = { ...state.posts }
posts[postId].liked = isLiked
return { posts }
})
}
setRemotePostLikeStatus = async (postId, isLiked) => {
const likes = isLiked
? firestore.FieldValue.arrayUnion(this.props.userId)
: firestore.FieldValue.arrayRemove(this.props.userId)
const likeCount = firestore.FieldValue.increment(isLiked ? 1 : -1)
try {
await this.postsRef.doc(postId).update({
likes,
likeCount
})
return true
} catch (e) {
console.log(e)
return false
}
}
_renderItem = ({ item }) => (
<PostItem
item={item}
id={item.id}
onPressLike={this.handleLikePressed}
liked={item.liked}
title={item.title}
likeCount={item.likeCount}
viewCount={item.viewCount}
navigation={this.props.navigation}
onAvatarPressed={this.onAvatarPressed}
onCommentPressed={this.onCommentPressed}
onSharePressed={this.onSharePressed}
/>
)
render() {
const { staged } = this.state
return (
<View>
<ShowNewPostsButton staged={staged} onPress={this.showNewPosts} />
<FlatList
viewabilityConfig={{
itemVisiblePercentThreshold: 100,
minimumViewTime: 3000
}}
onViewableItemsChanged={this.onViewableItemsChanged}
contentContainerStyle={{ paddingBottom: 200 }}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
data={this.data()}
extraData={this.state}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem}
onEndReachedThreshold={2}
onEndReached={this.onEndReached}
initialNumToRender={3}
/>
</View>
)
}
data = () => {
return Object.values(this.state.posts).sort((p1, p2) =>
p1.created <= p2.created ? 1 : -1
)
}
onViewableItemsChanged = info => {
info.changed
.filter(item => item.isViewable)
.forEach(({ item }) => {
firestore()
.collection('posts')
.doc(item.id)
.update({ viewCount: firestore.FieldValue.increment(1) })
})
}
onEndReached = distance => {
this.loadMorePosts()
}
showNewPosts = () => {
const { posts, staged } = this.state
const withNewPosts = { ...posts, ...staged }
this.setState({ posts: withNewPosts, staged: {} })
}
_keyExtractor = item => item.id
onCommentPressed = post => {
this.props.navigation.navigate('Comments', {
post: post
})
}
onAvatarPressed = (userId, username) => {
this.props.navigation.navigate('PublicProfile', {
userId: userId,
username: username
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment