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:
parent
c584a3378d
commit
b445c15cc9
29 changed files with 941 additions and 1739 deletions
|
@ -1,6 +1,8 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {Linking, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
|
@ -8,41 +10,83 @@ import {
|
|||
NativeDropdown,
|
||||
DropdownItem as NativeDropdownItem,
|
||||
} from './NativeDropdown'
|
||||
import * as Toast from '../Toast'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {useStores} from '#/state'
|
||||
import {usePostDeleteMutation} from '#/state/queries/post'
|
||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export function PostDropdownBtn({
|
||||
testID,
|
||||
itemUri,
|
||||
itemCid,
|
||||
itemHref,
|
||||
isAuthor,
|
||||
isThreadMuted,
|
||||
onCopyPostText,
|
||||
onOpenTranslate,
|
||||
onToggleThreadMute,
|
||||
onDeletePost,
|
||||
post,
|
||||
record,
|
||||
style,
|
||||
}: {
|
||||
testID: string
|
||||
itemUri: string
|
||||
itemCid: string
|
||||
itemHref: string
|
||||
itemTitle: string
|
||||
isAuthor: boolean
|
||||
isThreadMuted: boolean
|
||||
onCopyPostText: () => void
|
||||
onOpenTranslate: () => void
|
||||
onToggleThreadMute: () => void
|
||||
onDeletePost: () => void
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const mutedThreads = useMutedThreads()
|
||||
const toggleThreadMute = useToggleThreadMute()
|
||||
const postDeleteMutation = usePostDeleteMutation()
|
||||
|
||||
const rootUri = record.reply?.root?.uri || post.uri
|
||||
const isThreadMuted = mutedThreads.includes(rootUri)
|
||||
const isAuthor = post.author.did === store.me.did
|
||||
const href = React.useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = React.useCallback(() => {
|
||||
postDeleteMutation.mutateAsync({uri: post.uri}).then(
|
||||
() => {
|
||||
Toast.show('Post deleted')
|
||||
},
|
||||
e => {
|
||||
logger.error('Failed to delete post', {error: e})
|
||||
Toast.show('Failed to delete post, please try again')
|
||||
},
|
||||
)
|
||||
}, [post, postDeleteMutation])
|
||||
|
||||
const onToggleThreadMute = React.useCallback(() => {
|
||||
try {
|
||||
const muted = toggleThreadMute(rootUri)
|
||||
if (muted) {
|
||||
Toast.show('You will no longer receive notifications for this thread')
|
||||
} else {
|
||||
Toast.show('You will now receive notifications for this thread')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to toggle thread mute', {error: e})
|
||||
}
|
||||
}, [rootUri, toggleThreadMute])
|
||||
|
||||
const onCopyPostText = React.useCallback(() => {
|
||||
Clipboard.setString(record?.text || '')
|
||||
Toast.show('Copied to clipboard')
|
||||
}, [record])
|
||||
|
||||
const onOpenTranslate = React.useCallback(() => {
|
||||
Linking.openURL(translatorUrl)
|
||||
}, [translatorUrl])
|
||||
|
||||
const dropdownItems: NativeDropdownItem[] = [
|
||||
{
|
||||
|
@ -76,7 +120,7 @@ export function PostDropdownBtn({
|
|||
{
|
||||
label: 'Share',
|
||||
onPress() {
|
||||
const url = toShareUrl(itemHref)
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
},
|
||||
testID: 'postDropdownShareBtn',
|
||||
|
@ -113,8 +157,8 @@ export function PostDropdownBtn({
|
|||
onPress() {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: itemUri,
|
||||
cid: itemCid,
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownReportBtn',
|
||||
|
@ -155,7 +199,7 @@ export function PostDropdownBtn({
|
|||
<NativeDropdown
|
||||
testID={testID}
|
||||
items={dropdownItems}
|
||||
accessibilityLabel={_(msg`More post options`)}
|
||||
accessibilityLabel="More post options"
|
||||
accessibilityHint="">
|
||||
<View style={style}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
||||
|
|
|
@ -1,210 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Linking, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {
|
||||
NativeDropdown,
|
||||
DropdownItem as NativeDropdownItem,
|
||||
} from './NativeDropdown'
|
||||
import * as Toast from '../Toast'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {useStores} from '#/state'
|
||||
import {usePostDeleteMutation} from '#/state/queries/post'
|
||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export function PostDropdownBtn({
|
||||
testID,
|
||||
post,
|
||||
record,
|
||||
style,
|
||||
}: {
|
||||
testID: string
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const theme = useTheme()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const mutedThreads = useMutedThreads()
|
||||
const toggleThreadMute = useToggleThreadMute()
|
||||
const postDeleteMutation = usePostDeleteMutation()
|
||||
|
||||
const rootUri = record.reply?.root?.uri || post.uri
|
||||
const isThreadMuted = mutedThreads.includes(rootUri)
|
||||
const isAuthor = post.author.did === store.me.did
|
||||
const href = React.useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = React.useCallback(() => {
|
||||
postDeleteMutation.mutateAsync({uri: post.uri}).then(
|
||||
() => {
|
||||
Toast.show('Post deleted')
|
||||
},
|
||||
e => {
|
||||
logger.error('Failed to delete post', {error: e})
|
||||
Toast.show('Failed to delete post, please try again')
|
||||
},
|
||||
)
|
||||
}, [post, postDeleteMutation])
|
||||
|
||||
const onToggleThreadMute = React.useCallback(() => {
|
||||
try {
|
||||
const muted = toggleThreadMute(rootUri)
|
||||
if (muted) {
|
||||
Toast.show('You will no longer receive notifications for this thread')
|
||||
} else {
|
||||
Toast.show('You will now receive notifications for this thread')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to toggle thread mute', {error: e})
|
||||
}
|
||||
}, [rootUri, toggleThreadMute])
|
||||
|
||||
const onCopyPostText = React.useCallback(() => {
|
||||
Clipboard.setString(record?.text || '')
|
||||
Toast.show('Copied to clipboard')
|
||||
}, [record])
|
||||
|
||||
const onOpenTranslate = React.useCallback(() => {
|
||||
Linking.openURL(translatorUrl)
|
||||
}, [translatorUrl])
|
||||
|
||||
const dropdownItems: NativeDropdownItem[] = [
|
||||
{
|
||||
label: 'Translate',
|
||||
onPress() {
|
||||
onOpenTranslate()
|
||||
},
|
||||
testID: 'postDropdownTranslateBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'character.book.closed',
|
||||
},
|
||||
android: 'ic_menu_sort_alphabetically',
|
||||
web: 'language',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Copy post text',
|
||||
onPress() {
|
||||
onCopyPostText()
|
||||
},
|
||||
testID: 'postDropdownCopyTextBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'doc.on.doc',
|
||||
},
|
||||
android: 'ic_menu_edit',
|
||||
web: ['far', 'paste'],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Share',
|
||||
onPress() {
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
},
|
||||
testID: 'postDropdownShareBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'separator',
|
||||
},
|
||||
{
|
||||
label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
|
||||
onPress() {
|
||||
onToggleThreadMute()
|
||||
},
|
||||
testID: 'postDropdownMuteThreadBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'separator',
|
||||
},
|
||||
!isAuthor && {
|
||||
label: 'Report post',
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownReportBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
isAuthor && {
|
||||
label: 'separator',
|
||||
},
|
||||
isAuthor && {
|
||||
label: 'Delete post',
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: 'Delete this post?',
|
||||
message: 'Are you sure? This can not be undone.',
|
||||
onPressConfirm: onDeletePost,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownDeleteBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_menu_delete',
|
||||
web: ['far', 'trash-can'],
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as NativeDropdownItem[]
|
||||
|
||||
return (
|
||||
<EventStopper>
|
||||
<NativeDropdown
|
||||
testID={testID}
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More post options"
|
||||
accessibilityHint="">
|
||||
<View style={style}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
|
@ -6,6 +6,7 @@ import {
|
|||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
|
||||
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
|
||||
|
@ -17,160 +18,155 @@ import {RepostButton} from './RepostButton'
|
|||
import {Haptics} from 'lib/haptics'
|
||||
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
usePostLikeMutation,
|
||||
usePostUnlikeMutation,
|
||||
usePostRepostMutation,
|
||||
usePostUnrepostMutation,
|
||||
} from '#/state/queries/post'
|
||||
|
||||
interface PostCtrlsOpts {
|
||||
itemUri: string
|
||||
itemCid: string
|
||||
itemHref: string
|
||||
itemTitle: string
|
||||
isAuthor: boolean
|
||||
author: {
|
||||
did: string
|
||||
handle: string
|
||||
displayName?: string | undefined
|
||||
avatar?: string | undefined
|
||||
}
|
||||
text: string
|
||||
indexedAt: string
|
||||
export function PostCtrls({
|
||||
big,
|
||||
post,
|
||||
record,
|
||||
style,
|
||||
onPressReply,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
replyCount?: number
|
||||
repostCount?: number
|
||||
likeCount?: number
|
||||
isReposted: boolean
|
||||
isLiked: boolean
|
||||
isThreadMuted: boolean
|
||||
onPressReply: () => void
|
||||
onPressToggleRepost: () => Promise<void>
|
||||
onPressToggleLike: () => Promise<void>
|
||||
onCopyPostText: () => void
|
||||
onOpenTranslate: () => void
|
||||
onToggleThreadMute: () => void
|
||||
onDeletePost: () => void
|
||||
}
|
||||
|
||||
export function PostCtrls(opts: PostCtrlsOpts) {
|
||||
}) {
|
||||
const store = useStores()
|
||||
const theme = useTheme()
|
||||
const {closeModal} = useModalControls()
|
||||
const postLikeMutation = usePostLikeMutation()
|
||||
const postUnlikeMutation = usePostUnlikeMutation()
|
||||
const postRepostMutation = usePostRepostMutation()
|
||||
const postUnrepostMutation = usePostUnrepostMutation()
|
||||
|
||||
const defaultCtrlColor = React.useMemo(
|
||||
() => ({
|
||||
color: theme.palette.default.postCtrl,
|
||||
}),
|
||||
[theme],
|
||||
) as StyleProp<ViewStyle>
|
||||
|
||||
const onPressToggleLike = React.useCallback(async () => {
|
||||
if (!post.viewer?.like) {
|
||||
Haptics.default()
|
||||
postLikeMutation.mutate({
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
likeCount: post.likeCount || 0,
|
||||
})
|
||||
} else {
|
||||
postUnlikeMutation.mutate({
|
||||
postUri: post.uri,
|
||||
likeUri: post.viewer.like,
|
||||
likeCount: post.likeCount || 0,
|
||||
})
|
||||
}
|
||||
}, [post, postLikeMutation, postUnlikeMutation])
|
||||
|
||||
const onRepost = useCallback(() => {
|
||||
closeModal()
|
||||
if (!opts.isReposted) {
|
||||
if (!post.viewer?.repost) {
|
||||
Haptics.default()
|
||||
opts.onPressToggleRepost().catch(_e => undefined)
|
||||
postRepostMutation.mutate({
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
repostCount: post.repostCount || 0,
|
||||
})
|
||||
} else {
|
||||
opts.onPressToggleRepost().catch(_e => undefined)
|
||||
postUnrepostMutation.mutate({
|
||||
postUri: post.uri,
|
||||
repostUri: post.viewer.repost,
|
||||
repostCount: post.repostCount || 0,
|
||||
})
|
||||
}
|
||||
}, [opts, closeModal])
|
||||
}, [post, closeModal, postRepostMutation, postUnrepostMutation])
|
||||
|
||||
const onQuote = useCallback(() => {
|
||||
closeModal()
|
||||
store.shell.openComposer({
|
||||
quote: {
|
||||
uri: opts.itemUri,
|
||||
cid: opts.itemCid,
|
||||
text: opts.text,
|
||||
author: opts.author,
|
||||
indexedAt: opts.indexedAt,
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
text: record.text,
|
||||
author: post.author,
|
||||
indexedAt: post.indexedAt,
|
||||
},
|
||||
})
|
||||
Haptics.default()
|
||||
}, [
|
||||
opts.author,
|
||||
opts.indexedAt,
|
||||
opts.itemCid,
|
||||
opts.itemUri,
|
||||
opts.text,
|
||||
store.shell,
|
||||
closeModal,
|
||||
])
|
||||
|
||||
const onPressToggleLikeWrapper = async () => {
|
||||
if (!opts.isLiked) {
|
||||
Haptics.default()
|
||||
await opts.onPressToggleLike().catch(_e => undefined)
|
||||
} else {
|
||||
await opts.onPressToggleLike().catch(_e => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
}, [post, record, store.shell, closeModal])
|
||||
return (
|
||||
<View style={[styles.ctrls, opts.style]}>
|
||||
<View style={[styles.ctrls, style]}>
|
||||
<TouchableOpacity
|
||||
testID="replyBtn"
|
||||
style={[styles.ctrl, !opts.big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||
onPress={opts.onPressReply}
|
||||
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||
onPress={onPressReply}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Reply (${opts.replyCount} ${
|
||||
opts.replyCount === 1 ? 'reply' : 'replies'
|
||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||
post.replyCount === 1 ? 'reply' : 'replies'
|
||||
})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
<CommentBottomArrow
|
||||
style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
|
||||
style={[defaultCtrlColor, big ? s.mt2 : styles.mt1]}
|
||||
strokeWidth={3}
|
||||
size={opts.big ? 20 : 15}
|
||||
size={big ? 20 : 15}
|
||||
/>
|
||||
{typeof opts.replyCount !== 'undefined' ? (
|
||||
{typeof post.replyCount !== 'undefined' ? (
|
||||
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
|
||||
{opts.replyCount}
|
||||
{post.replyCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
<RepostButton {...opts} onRepost={onRepost} onQuote={onQuote} />
|
||||
<RepostButton
|
||||
big={big}
|
||||
isReposted={!!post.viewer?.repost}
|
||||
repostCount={post.repostCount}
|
||||
onRepost={onRepost}
|
||||
onQuote={onQuote}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="likeBtn"
|
||||
style={[styles.ctrl, !opts.big && styles.ctrlPad]}
|
||||
onPress={onPressToggleLikeWrapper}
|
||||
style={[styles.ctrl, !big && styles.ctrlPad]}
|
||||
onPress={onPressToggleLike}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${opts.isLiked ? 'Unlike' : 'Like'} (${
|
||||
opts.likeCount
|
||||
} ${pluralize(opts.likeCount || 0, 'like')})`}
|
||||
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
|
||||
post.likeCount
|
||||
} ${pluralize(post.likeCount || 0, 'like')})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
|
||||
{opts.isLiked ? (
|
||||
<HeartIconSolid
|
||||
style={styles.ctrlIconLiked}
|
||||
size={opts.big ? 22 : 16}
|
||||
/>
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
{post.viewer?.like ? (
|
||||
<HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
|
||||
) : (
|
||||
<HeartIcon
|
||||
style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
|
||||
style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
|
||||
strokeWidth={3}
|
||||
size={opts.big ? 20 : 16}
|
||||
size={big ? 20 : 16}
|
||||
/>
|
||||
)}
|
||||
{typeof opts.likeCount !== 'undefined' ? (
|
||||
{typeof post.likeCount !== 'undefined' ? (
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={
|
||||
opts.isLiked
|
||||
post.viewer?.like
|
||||
? [s.bold, s.red3, s.f15, s.ml5]
|
||||
: [defaultCtrlColor, s.f15, s.ml5]
|
||||
}>
|
||||
{opts.likeCount}
|
||||
{post.likeCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
{opts.big ? undefined : (
|
||||
{big ? undefined : (
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
itemUri={opts.itemUri}
|
||||
itemCid={opts.itemCid}
|
||||
itemHref={opts.itemHref}
|
||||
itemTitle={opts.itemTitle}
|
||||
isAuthor={opts.isAuthor}
|
||||
isThreadMuted={opts.isThreadMuted}
|
||||
onCopyPostText={opts.onCopyPostText}
|
||||
onOpenTranslate={opts.onOpenTranslate}
|
||||
onToggleThreadMute={opts.onToggleThreadMute}
|
||||
onDeletePost={opts.onDeletePost}
|
||||
post={post}
|
||||
record={record}
|
||||
style={styles.ctrlPad}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {PostDropdownBtn} from '../forms/PostDropdownBtn2'
|
||||
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useStores} from 'state/index'
|
||||
import {RepostButton} from './RepostButton'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
usePostLikeMutation,
|
||||
usePostUnlikeMutation,
|
||||
usePostRepostMutation,
|
||||
usePostUnrepostMutation,
|
||||
} from '#/state/queries/post'
|
||||
|
||||
export function PostCtrls({
|
||||
big,
|
||||
post,
|
||||
record,
|
||||
style,
|
||||
onPressReply,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const theme = useTheme()
|
||||
const {closeModal} = useModalControls()
|
||||
const postLikeMutation = usePostLikeMutation()
|
||||
const postUnlikeMutation = usePostUnlikeMutation()
|
||||
const postRepostMutation = usePostRepostMutation()
|
||||
const postUnrepostMutation = usePostUnrepostMutation()
|
||||
|
||||
const defaultCtrlColor = React.useMemo(
|
||||
() => ({
|
||||
color: theme.palette.default.postCtrl,
|
||||
}),
|
||||
[theme],
|
||||
) as StyleProp<ViewStyle>
|
||||
|
||||
const onPressToggleLike = React.useCallback(async () => {
|
||||
if (!post.viewer?.like) {
|
||||
Haptics.default()
|
||||
postLikeMutation.mutate({
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
likeCount: post.likeCount || 0,
|
||||
})
|
||||
} else {
|
||||
postUnlikeMutation.mutate({
|
||||
postUri: post.uri,
|
||||
likeUri: post.viewer.like,
|
||||
likeCount: post.likeCount || 0,
|
||||
})
|
||||
}
|
||||
}, [post, postLikeMutation, postUnlikeMutation])
|
||||
|
||||
const onRepost = useCallback(() => {
|
||||
closeModal()
|
||||
if (!post.viewer?.repost) {
|
||||
Haptics.default()
|
||||
postRepostMutation.mutate({
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
repostCount: post.repostCount || 0,
|
||||
})
|
||||
} else {
|
||||
postUnrepostMutation.mutate({
|
||||
postUri: post.uri,
|
||||
repostUri: post.viewer.repost,
|
||||
repostCount: post.repostCount || 0,
|
||||
})
|
||||
}
|
||||
}, [post, closeModal, postRepostMutation, postUnrepostMutation])
|
||||
|
||||
const onQuote = useCallback(() => {
|
||||
closeModal()
|
||||
store.shell.openComposer({
|
||||
quote: {
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
text: record.text,
|
||||
author: post.author,
|
||||
indexedAt: post.indexedAt,
|
||||
},
|
||||
})
|
||||
Haptics.default()
|
||||
}, [post, record, store.shell, closeModal])
|
||||
return (
|
||||
<View style={[styles.ctrls, style]}>
|
||||
<TouchableOpacity
|
||||
testID="replyBtn"
|
||||
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||
onPress={onPressReply}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||
post.replyCount === 1 ? 'reply' : 'replies'
|
||||
})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
<CommentBottomArrow
|
||||
style={[defaultCtrlColor, big ? s.mt2 : styles.mt1]}
|
||||
strokeWidth={3}
|
||||
size={big ? 20 : 15}
|
||||
/>
|
||||
{typeof post.replyCount !== 'undefined' ? (
|
||||
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
|
||||
{post.replyCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
<RepostButton
|
||||
big={big}
|
||||
isReposted={!!post.viewer?.repost}
|
||||
repostCount={post.repostCount}
|
||||
onRepost={onRepost}
|
||||
onQuote={onQuote}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="likeBtn"
|
||||
style={[styles.ctrl, !big && styles.ctrlPad]}
|
||||
onPress={onPressToggleLike}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
|
||||
post.likeCount
|
||||
} ${pluralize(post.likeCount || 0, 'like')})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
{post.viewer?.like ? (
|
||||
<HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
|
||||
) : (
|
||||
<HeartIcon
|
||||
style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
|
||||
strokeWidth={3}
|
||||
size={big ? 20 : 16}
|
||||
/>
|
||||
)}
|
||||
{typeof post.likeCount !== 'undefined' ? (
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={
|
||||
post.viewer?.like
|
||||
? [s.bold, s.red3, s.f15, s.ml5]
|
||||
: [defaultCtrlColor, s.f15, s.ml5]
|
||||
}>
|
||||
{post.likeCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
{big ? undefined : (
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
record={record}
|
||||
style={styles.ctrlPad}
|
||||
/>
|
||||
)}
|
||||
{/* used for adding pad to the right side */}
|
||||
<View />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ctrls: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
ctrl: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
ctrlPad: {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
},
|
||||
ctrlIconLiked: {
|
||||
color: colors.like,
|
||||
},
|
||||
mt1: {
|
||||
marginTop: 1,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue