merge main

This commit is contained in:
Hailey
2024-06-18 12:11:32 -07:00
25 changed files with 474 additions and 350 deletions
+7 -4
View File
@@ -26,9 +26,11 @@ export const snapPoints = ['60%']
export function Component({
settings,
onChange,
onConfirm,
}: {
settings: ThreadgateSetting[]
onChange: (settings: ThreadgateSetting[]) => void
onChange?: (settings: ThreadgateSetting[]) => void
onConfirm?: (settings: ThreadgateSetting[]) => void
}) {
const pal = usePalette('default')
const {closeModal} = useModalControls()
@@ -38,12 +40,12 @@ export function Component({
const onPressEverybody = () => {
setSelected([])
onChange([])
onChange?.([])
}
const onPressNobody = () => {
setSelected([{type: 'nobody'}])
onChange([{type: 'nobody'}])
onChange?.([{type: 'nobody'}])
}
const onPressAudience = (setting: ThreadgateSetting) => {
@@ -57,7 +59,7 @@ export function Component({
newSelected.splice(i, 1)
}
setSelected(newSelected)
onChange(newSelected)
onChange?.(newSelected)
}
return (
@@ -124,6 +126,7 @@ export function Component({
testID="confirmBtn"
onPress={() => {
closeModal()
onConfirm?.(selected)
}}
style={styles.btn}
accessibilityRole="button"
+23 -2
View File
@@ -25,7 +25,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {countLines} from 'lib/strings/helpers'
import {niceDate} from 'lib/strings/time'
import {s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {isNative, isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {atoms as a} from '#/alf'
@@ -189,6 +189,7 @@ let PostThreadItemLoaded = ({
const itemTitle = _(msg`Post by ${post.author.handle}`)
const authorHref = makeProfileLink(post.author)
const authorTitle = post.author.handle
const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
const likesHref = React.useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
@@ -395,7 +396,11 @@ let PostThreadItemLoaded = ({
</View>
</View>
</View>
<WhoCanReply post={post} />
<WhoCanReply
post={post}
isThreadAuthor={isThreadAuthor}
style={{borderBottomWidth: isNative ? 1 : 0}}
/>
</>
)
} else {
@@ -578,7 +583,9 @@ let PostThreadItemLoaded = ({
post={post}
style={{
marginTop: 4,
borderBottomWidth: 1,
}}
isThreadAuthor={isThreadAuthor}
/>
</>
)
@@ -681,6 +688,20 @@ function ExpandedPostDetails({
)
}
function getThreadAuthor(
post: AppBskyFeedDefs.PostView,
record: AppBskyFeedPost.Record,
): string {
if (!record.reply) {
return post.author.did
}
try {
return new AtUri(record.reply.root.uri).host
} catch {
return ''
}
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: hairlineWidth,
+142 -98
View File
@@ -1,128 +1,172 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyFeedThreadgate,
AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
import {createThreadgate} from '#/lib/api'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
import {colors} from '#/lib/styles'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread'
import {
ThreadgateSetting,
threadgateViewToSettings,
} from '#/state/queries/threadgate'
import {useAgent} from '#/state/session'
import * as Toast from 'view/com/util/Toast'
import {Button} from '#/components/Button'
import {TextLink} from '../util/Link'
import {Text} from '../util/text/Text'
export function WhoCanReply({
post,
isThreadAuthor,
style,
}: {
post: AppBskyFeedDefs.PostView
isThreadAuthor: boolean
style?: StyleProp<ViewStyle>
}) {
const {track} = useAnalytics()
const {_} = useLingui()
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const agent = useAgent()
const queryClient = useQueryClient()
const {openModal} = useModalControls()
const containerStyles = useColorSchemeStyle(
{
borderColor: pal.colors.unreadNotifBorder,
backgroundColor: pal.colors.unreadNotifBg,
},
{
borderColor: pal.colors.unreadNotifBorder,
backgroundColor: pal.colors.unreadNotifBg,
},
)
const iconStyles = useColorSchemeStyle(
{
backgroundColor: colors.blue3,
},
{
backgroundColor: colors.blue3,
},
)
const textStyles = useColorSchemeStyle(
{color: colors.gray7},
{color: colors.blue5},
{color: colors.blue1},
)
const record = React.useMemo(
() =>
post.threadgate &&
AppBskyFeedThreadgate.isRecord(post.threadgate.record) &&
AppBskyFeedThreadgate.validateRecord(post.threadgate.record).success
? post.threadgate.record
: null,
const hoverStyles = useColorSchemeStyle(
{
backgroundColor: colors.white,
},
{
backgroundColor: pal.colors.background,
},
)
const settings = React.useMemo(
() => threadgateViewToSettings(post.threadgate),
[post],
)
if (record) {
return (
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
gap: isMobile ? 8 : 10,
paddingHorizontal: isMobile ? 16 : 18,
paddingVertical: 12,
borderWidth: 1,
borderLeftWidth: isMobile ? 0 : 1,
borderRightWidth: isMobile ? 0 : 1,
},
containerStyles,
style,
]}>
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: 19,
},
iconStyles,
]}>
<FontAwesomeIcon
icon={['far', 'comments']}
size={16}
color={'#fff'}
/>
</View>
<View style={{flex: 1}}>
<Text type="sm" style={[{flexWrap: 'wrap'}, textStyles]}>
{!record.allow?.length ? (
<Trans>Replies to this thread are disabled</Trans>
) : (
<Trans>
Only{' '}
{record.allow.map((rule, i) => (
<>
<Rule
key={`rule-${i}`}
rule={rule}
post={post}
lists={post.threadgate!.lists}
/>
<Separator
key={`sep-${i}`}
i={i}
length={record.allow!.length}
/>
</>
))}{' '}
can reply.
</Trans>
)}
</Text>
</View>
</View>
)
const isRootPost = !('reply' in post.record)
const onPressEdit = () => {
track('Post:EditThreadgateOpened')
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
openModal({
name: 'threadgate',
settings,
async onConfirm(newSettings: ThreadgateSetting[]) {
try {
if (newSettings.length) {
await createThreadgate(agent, post.uri, newSettings)
} else {
await agent.api.com.atproto.repo.deleteRecord({
repo: agent.session!.did,
collection: 'app.bsky.feed.threadgate',
rkey: new AtUri(post.uri).rkey,
})
}
Toast.show('Thread settings updated')
queryClient.invalidateQueries({
queryKey: [POST_THREAD_RQKEY_ROOT],
})
track('Post:ThreadgateEdited')
} catch (err) {
Toast.show(
'There was an issue. Please check your internet connection and try again.',
)
logger.error('Failed to edit threadgate', {message: err})
}
},
})
}
return null
if (!isRootPost) {
return null
}
if (!settings.length && !isThreadAuthor) {
return null
}
return (
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingLeft: 18,
paddingRight: 14,
paddingVertical: 10,
borderTopWidth: 1,
},
pal.border,
containerStyles,
style,
]}>
<View style={{flex: 1, paddingVertical: 6}}>
<Text type="sm" style={[{flexWrap: 'wrap'}, textStyles]}>
{!settings.length ? (
<Trans>Everybody can reply.</Trans>
) : settings[0].type === 'nobody' ? (
<Trans>Replies to this thread are disabled.</Trans>
) : (
<Trans>
Only{' '}
{settings.map((rule, i) => (
<>
<Rule
key={`rule-${i}`}
rule={rule}
post={post}
lists={post.threadgate!.lists}
/>
<Separator key={`sep-${i}`} i={i} length={settings.length} />
</>
))}{' '}
can reply.
</Trans>
)}
</Text>
</View>
{isThreadAuthor && (
<View>
<Button label={_(msg`Edit`)} onPress={onPressEdit}>
{({hovered}) => (
<View
style={[
hovered && hoverStyles,
{paddingVertical: 6, paddingHorizontal: 8, borderRadius: 8},
]}>
<Text type="sm" style={pal.link}>
<Trans>Edit</Trans>
</Text>
</View>
)}
</Button>
</View>
)}
</View>
)
}
function Rule({
@@ -130,15 +174,15 @@ function Rule({
post,
lists,
}: {
rule: any
rule: ThreadgateSetting
post: AppBskyFeedDefs.PostView
lists: AppBskyGraphDefs.ListViewBasic[] | undefined
}) {
const pal = usePalette('default')
if (AppBskyFeedThreadgate.isMentionRule(rule)) {
if (rule.type === 'mention') {
return <Trans>mentioned users</Trans>
}
if (AppBskyFeedThreadgate.isFollowingRule(rule)) {
if (rule.type === 'following') {
return (
<Trans>
users followed by{' '}
@@ -151,7 +195,7 @@ function Rule({
</Trans>
)
}
if (AppBskyFeedThreadgate.isListRule(rule)) {
if (rule.type === 'list') {
const list = lists?.find(l => l.uri === rule.list)
if (list) {
const listUrip = new AtUri(list.uri)
+4 -2
View File
@@ -15,12 +15,14 @@ export function TimeElapsed({
const ago = useGetTimeAgo()
const format = timeToString ?? ago
const tick = useTickEveryMinute()
const [timeElapsed, setTimeAgo] = React.useState(() => format(timestamp))
const [timeElapsed, setTimeAgo] = React.useState(() =>
format(timestamp, tick),
)
const [prevTick, setPrevTick] = React.useState(tick)
if (prevTick !== tick) {
setPrevTick(tick)
setTimeAgo(format(timestamp))
setTimeAgo(format(timestamp, tick))
}
return children({timeElapsed})
+26 -19
View File
@@ -7,7 +7,7 @@ import {
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
RichText as RichTextAPI,
@@ -22,12 +22,15 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {getTranslatorLink} from '#/locale/helpers'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {usePostDeleteMutation} from '#/state/queries/post'
import {
usePostDeleteMutation,
useThreadMuteMutationQueue,
} from '#/state/queries/post'
import {useSession} from '#/state/session'
import {getCurrentRoute} from 'lib/routes/helpers'
import {shareUrl} from 'lib/sharing'
@@ -62,9 +65,7 @@ import * as Toast from '../Toast'
let PostDropdownBtn = ({
testID,
postAuthor,
postCid,
postUri,
post,
postFeedContext,
record,
richText,
@@ -74,9 +75,7 @@ let PostDropdownBtn = ({
timestamp,
}: {
testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic
postCid: string
postUri: string
post: Shadow<AppBskyFeedDefs.PostView>
postFeedContext: string | undefined
record: AppBskyFeedPost.Record
richText: RichTextAPI
@@ -92,8 +91,6 @@ let PostDropdownBtn = ({
const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl
const langPrefs = useLanguagePrefs()
const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute()
const postDeleteMutation = usePostDeleteMutation()
const hiddenPosts = useHiddenPosts()
const {hidePost} = useHiddenPostsApi()
@@ -107,9 +104,15 @@ let PostDropdownBtn = ({
const loggedOutWarningPromptControl = useDialogControl()
const embedPostControl = useDialogControl()
const sendViaChatControl = useDialogControl()
const postUri = post.uri
const postCid = post.cid
const postAuthor = post.author
const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri)
const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
post,
rootUri,
)
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
const isAuthor = postAuthor.did === currentAccount?.did
@@ -162,18 +165,22 @@ let PostDropdownBtn = ({
const onToggleThreadMute = React.useCallback(() => {
try {
const muted = toggleThreadMute(rootUri)
if (muted) {
if (isThreadMuted) {
unmuteThread()
Toast.show(_(msg`You will now receive notifications for this thread`))
} else {
muteThread()
Toast.show(
_(msg`You will no longer receive notifications for this thread`),
)
} else {
Toast.show(_(msg`You will now receive notifications for this thread`))
}
} catch (e) {
logger.error('Failed to toggle thread mute', {message: e})
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to toggle thread mute', {message: e})
Toast.show(_(msg`Failed to toggle thread mute, please try again`))
}
}
}, [rootUri, toggleThreadMute, _])
}, [isThreadMuted, unmuteThread, _, muteThread])
const onCopyPostText = React.useCallback(() => {
const str = richTextToString(richText, true)
+1 -3
View File
@@ -319,9 +319,7 @@ let PostCtrls = ({
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
<PostDropdownBtn
testID="postDropdownBtn"
postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
post={post}
postFeedContext={feedContext}
record={record}
richText={richText}
+3 -1
View File
@@ -7,6 +7,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {getEntries} from '#/logger/logDump'
import {useTickEveryMinute} from '#/state/shell'
import {useSetMinimalShellMode} from '#/state/shell'
import {usePalette} from 'lib/hooks/usePalette'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
@@ -24,6 +25,7 @@ export function LogScreen({}: NativeStackScreenProps<
const setMinimalShellMode = useSetMinimalShellMode()
const [expanded, setExpanded] = React.useState<string[]>([])
const timeAgo = useGetTimeAgo()
const tick = useTickEveryMinute()
useFocusEffect(
React.useCallback(() => {
@@ -72,7 +74,7 @@ export function LogScreen({}: NativeStackScreenProps<
/>
) : undefined}
<Text type="sm" style={[styles.ts, pal.textLight]}>
{timeAgo(entry.timestamp)}
{timeAgo(entry.timestamp, tick)}
</Text>
</TouchableOpacity>
{expanded.includes(entry.id) ? (