Created
September 8, 2019 20:52
-
-
Save technoplato/90fc11294d71860eb046f98f5da8ea5f to your computer and use it in GitHub Desktop.
Likes with Firestore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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