Refactor notifications to use react-query (#1878)

* Move broadcast channel to lib

* Refactor view/com/post/Post and remove temporary 2 components

* Add useModerationOpts hook

* Refactor notifications to use react-query

* Fix: only trigger updates in useModerationOpts when the values have changed

* Implement unread notification tracking

* Add moderation filtering to notifications

* Handle native/push notifications

* Remove dead code

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Paul Frazee 2023-11-12 18:13:11 -08:00 committed by GitHub
parent c584a3378d
commit b445c15cc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 941 additions and 1739 deletions

View file

@ -1,5 +1,4 @@
import React, {useMemo, useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
TouchableOpacity,
@ -9,6 +8,9 @@ import {
} from 'react-native'
import {
AppBskyEmbedImages,
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationOpts,
ProfileModeration,
moderateProfile,
AppBskyEmbedRecordWithMedia,
@ -19,8 +21,7 @@ import {
FontAwesomeIconStyle,
Props,
} from '@fortawesome/react-native-fontawesome'
import {NotificationsFeedItemModel} from 'state/models/feeds/notifications'
import {PostThreadModel} from 'state/models/content/post-thread'
import {FeedNotification} from '#/state/queries/notifications/feed'
import {s, colors} from 'lib/styles'
import {niceDate} from 'lib/strings/time'
import {sanitizeDisplayName} from 'lib/strings/display-names'
@ -33,7 +34,6 @@ import {UserPreviewLink} from '../util/UserPreviewLink'
import {ImageHorzList} from '../util/images/ImageHorzList'
import {Post} from '../post/Post'
import {Link, TextLink} from '../util/Link'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {formatCount} from '../util/numeric/format'
@ -56,40 +56,36 @@ interface Author {
moderation: ProfileModeration
}
export const FeedItem = observer(function FeedItemImpl({
export function FeedItem({
item,
dataUpdatedAt,
moderationOpts,
}: {
item: NotificationsFeedItemModel
item: FeedNotification
dataUpdatedAt: number
moderationOpts: ModerationOpts
}) {
const store = useStores()
const pal = usePalette('default')
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
const itemHref = useMemo(() => {
if (item.isLike || item.isRepost) {
const urip = new AtUri(item.subjectUri)
if (item.type === 'post-like' || item.type === 'repost') {
if (item.subjectUri) {
const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/post/${urip.rkey}`
}
} else if (item.type === 'follow') {
return makeProfileLink(item.notification.author)
} else if (item.type === 'reply') {
const urip = new AtUri(item.notification.uri)
return `/profile/${urip.host}/post/${urip.rkey}`
} else if (item.isFollow) {
return makeProfileLink(item.author)
} else if (item.isReply) {
const urip = new AtUri(item.uri)
return `/profile/${urip.host}/post/${urip.rkey}`
} else if (item.isCustomFeedLike) {
const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/feed/${urip.rkey}`
} else if (item.type === 'feedgen-like') {
if (item.subjectUri) {
const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/feed/${urip.rkey}`
}
}
return ''
}, [item])
const itemTitle = useMemo(() => {
if (item.isLike || item.isRepost) {
return 'Post'
} else if (item.isFollow) {
return item.author.handle
} else if (item.isReply) {
return 'Post'
} else if (item.isCustomFeedLike) {
return 'Custom Feed'
}
}, [item])
const onToggleAuthorsExpanded = () => {
setAuthorsExpanded(currentlyExpanded => !currentlyExpanded)
@ -98,15 +94,12 @@ export const FeedItem = observer(function FeedItemImpl({
const authors: Author[] = useMemo(() => {
return [
{
href: makeProfileLink(item.author),
did: item.author.did,
handle: item.author.handle,
displayName: item.author.displayName,
avatar: item.author.avatar,
moderation: moderateProfile(
item.author,
store.preferences.moderationOpts,
),
href: makeProfileLink(item.notification.author),
did: item.notification.author.did,
handle: item.notification.author.handle,
displayName: item.notification.author.displayName,
avatar: item.notification.author.avatar,
moderation: moderateProfile(item.notification.author, moderationOpts),
},
...(item.additional?.map(({author}) => {
return {
@ -115,33 +108,36 @@ export const FeedItem = observer(function FeedItemImpl({
handle: author.handle,
displayName: author.displayName,
avatar: author.avatar,
moderation: moderateProfile(author, store.preferences.moderationOpts),
moderation: moderateProfile(author, moderationOpts),
}
}) || []),
]
}, [store, item.additional, item.author])
}, [item, moderationOpts])
if (item.additionalPost?.notFound) {
if (item.subjectUri && !item.subject) {
// don't render anything if the target post was deleted or unfindable
return <View />
}
if (item.isReply || item.isMention || item.isQuote) {
if (!item.additionalPost || item.additionalPost?.error) {
// hide errors - it doesnt help the user to show them
return <View />
if (
item.type === 'reply' ||
item.type === 'mention' ||
item.type === 'quote'
) {
if (!item.subject) {
return null
}
return (
<Link
testID={`feedItem-by-${item.author.handle}`}
testID={`feedItem-by-${item.notification.author.handle}`}
href={itemHref}
title={itemTitle}
noFeedback
accessible={false}>
<Post
view={item.additionalPost}
post={item.subject}
dataUpdatedAt={dataUpdatedAt}
style={
item.isRead
item.notification.isRead
? undefined
: {
backgroundColor: pal.colors.unreadNotifBg,
@ -156,23 +152,25 @@ export const FeedItem = observer(function FeedItemImpl({
let action = ''
let icon: Props['icon'] | 'HeartIconSolid'
let iconStyle: Props['style'] = []
if (item.isLike) {
if (item.type === 'post-like') {
action = 'liked your post'
icon = 'HeartIconSolid'
iconStyle = [
s.likeColor as FontAwesomeIconStyle,
{position: 'relative', top: -4},
]
} else if (item.isRepost) {
} else if (item.type === 'repost') {
action = 'reposted your post'
icon = 'retweet'
iconStyle = [s.green3 as FontAwesomeIconStyle]
} else if (item.isFollow) {
} else if (item.type === 'follow') {
action = 'followed you'
icon = 'user-plus'
iconStyle = [s.blue3 as FontAwesomeIconStyle]
} else if (item.isCustomFeedLike) {
action = `liked your custom feed '${new AtUri(item.subjectUri).rkey}'`
} else if (item.type === 'feedgen-like') {
action = `liked your custom feed${
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}}'` : ''
}`
icon = 'HeartIconSolid'
iconStyle = [
s.likeColor as FontAwesomeIconStyle,
@ -184,12 +182,12 @@ export const FeedItem = observer(function FeedItemImpl({
return (
<Link
testID={`feedItem-by-${item.author.handle}`}
testID={`feedItem-by-${item.notification.author.handle}`}
style={[
styles.outer,
pal.view,
pal.border,
item.isRead
item.notification.isRead
? undefined
: {
backgroundColor: pal.colors.unreadNotifBg,
@ -197,9 +195,11 @@ export const FeedItem = observer(function FeedItemImpl({
},
]}
href={itemHref}
title={itemTitle}
noFeedback
accessible={(item.isLike && authors.length === 1) || item.isRepost}>
accessible={
(item.type === 'post-like' && authors.length === 1) ||
item.type === 'repost'
}>
<View style={styles.layoutIcon}>
{/* TODO: Prevent conditional rendering and move toward composable
notifications for clearer accessibility labeling */}
@ -244,24 +244,24 @@ export const FeedItem = observer(function FeedItemImpl({
</>
) : undefined}
<Text style={[pal.text]}> {action}</Text>
<TimeElapsed timestamp={item.indexedAt}>
<TimeElapsed timestamp={item.notification.indexedAt}>
{({timeElapsed}) => (
<Text
style={[pal.textLight, styles.pointer]}
title={niceDate(item.indexedAt)}>
title={niceDate(item.notification.indexedAt)}>
{' ' + timeElapsed}
</Text>
)}
</TimeElapsed>
</Text>
</ExpandListPressable>
{item.isLike || item.isRepost || item.isQuote ? (
<AdditionalPostText additionalPost={item.additionalPost} />
{item.type === 'post-like' || item.type === 'repost' ? (
<AdditionalPostText post={item.subject} />
) : null}
</View>
</Link>
)
})
}
function ExpandListPressable({
hasMultipleAuthors,
@ -423,34 +423,25 @@ function ExpandedAuthorsList({
)
}
function AdditionalPostText({
additionalPost,
}: {
additionalPost?: PostThreadModel
}) {
function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
const pal = usePalette('default')
if (
!additionalPost ||
!additionalPost.thread?.postRecord ||
additionalPost.error
) {
return <View />
if (post && AppBskyFeedPost.isRecord(post?.record)) {
const text = post.record.text
const images = AppBskyEmbedImages.isView(post.embed)
? post.embed.images
: AppBskyEmbedRecordWithMedia.isView(post.embed) &&
AppBskyEmbedImages.isView(post.embed.media)
? post.embed.media.images
: undefined
return (
<>
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
{images && images?.length > 0 && (
<ImageHorzList images={images} style={styles.additionalPostImages} />
)}
</>
)
}
const text = additionalPost.thread?.postRecord.text
const images = AppBskyEmbedImages.isView(additionalPost.thread.post.embed)
? additionalPost.thread.post.embed.images
: AppBskyEmbedRecordWithMedia.isView(additionalPost.thread.post.embed) &&
AppBskyEmbedImages.isView(additionalPost.thread.post.embed.media)
? additionalPost.thread.post.embed.media.images
: undefined
return (
<>
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
{images && images?.length > 0 && (
<ImageHorzList images={images} style={styles.additionalPostImages} />
)}
</>
)
}
const styles = StyleSheet.create({