diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 05d1591f..e0b89980 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -84,6 +84,7 @@ export const createHitslop = (size: number): Insets => ({
export const HITSLOP_10 = createHitslop(10)
export const HITSLOP_20 = createHitslop(20)
export const HITSLOP_30 = createHitslop(30)
+export const POST_CTRL_HITSLOP = {top: 5, bottom: 10, left: 10, right: 10}
export const BACK_HITSLOP = HITSLOP_30
export const MAX_POST_LINES = 25
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 6d03029d..92b529db 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -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 {isNative, isWeb} from 'platform/detection'
+import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {atoms as a} from '#/alf'
@@ -35,7 +35,7 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {PostHider} from '../../../components/moderation/PostHider'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
-import {WhoCanReply} from '../threadgate/WhoCanReply'
+import {WhoCanReplyBlock, WhoCanReplyInline} from '../threadgate/WhoCanReply'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Link, TextLink} from '../util/Link'
import {formatCount} from '../util/numeric/format'
@@ -340,6 +340,7 @@ let PostThreadItemLoaded = ({
@@ -396,11 +397,6 @@ let PostThreadItemLoaded = ({
-
>
)
} else {
@@ -579,14 +575,7 @@ let PostThreadItemLoaded = ({
) : undefined}
-
+
>
)
}
@@ -654,10 +643,12 @@ function PostOuterWrapper({
function ExpandedPostDetails({
post,
+ isThreadAuthor,
needsTranslation,
translatorUrl,
}: {
post: AppBskyFeedDefs.PostView
+ isThreadAuthor: boolean
needsTranslation: boolean
translatorUrl: string
}) {
@@ -670,14 +661,23 @@ function ExpandedPostDetails({
}, [openLink, translatorUrl])
return (
-
- {niceDate(post.indexedAt)}
+
+ {niceDate(post.indexedAt)}
+
{needsTranslation && (
<>
- ·
+ ·
Translate
diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx
index 3ffbaa7a..3f9970f5 100644
--- a/src/view/com/threadgate/WhoCanReply.tsx
+++ b/src/view/com/threadgate/WhoCanReply.tsx
@@ -1,16 +1,20 @@
import React from 'react'
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
-import {AppBskyFeedDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
+import {
+ AppBskyFeedDefs,
+ AppBskyFeedGetPostThread,
+ AppBskyGraphDefs,
+ AtUri,
+ BskyAgent,
+} 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 {until} from '#/lib/async/until'
+import {HITSLOP_10} from '#/lib/constants'
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'
@@ -21,84 +25,31 @@ import {
} from '#/state/queries/threadgate'
import {useAgent} from '#/state/session'
import * as Toast from 'view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {useDialogControl} from '#/components/Dialog'
+import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
+import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
+import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
+import {Text} from '#/components/Typography'
import {TextLink} from '../util/Link'
-import {Text} from '../util/text/Text'
-export function WhoCanReply({
- post,
- isThreadAuthor,
- style,
-}: {
+interface WhoCanReplyProps {
post: AppBskyFeedDefs.PostView
isThreadAuthor: boolean
style?: StyleProp
-}) {
- const {track} = useAnalytics()
- const {_} = useLingui()
- const pal = usePalette('default')
- const agent = useAgent()
- const queryClient = useQueryClient()
- const {openModal} = useModalControls()
- const containerStyles = useColorSchemeStyle(
- {
- backgroundColor: pal.colors.unreadNotifBg,
- },
- {
- backgroundColor: pal.colors.unreadNotifBg,
- },
- )
- const textStyles = useColorSchemeStyle(
- {color: colors.blue5},
- {color: colors.blue1},
- )
- const hoverStyles = useColorSchemeStyle(
- {
- backgroundColor: colors.white,
- },
- {
- backgroundColor: pal.colors.background,
- },
- )
- const settings = React.useMemo(
- () => threadgateViewToSettings(post.threadgate),
- [post],
- )
- 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})
- }
- },
- })
- }
+export function WhoCanReplyInline({
+ post,
+ isThreadAuthor,
+ style,
+}: WhoCanReplyProps) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const infoDialogControl = useDialogControl()
+ const {settings, isRootPost, onPressEdit} = useWhoCanReply(post)
if (!isRootPost) {
return null
@@ -107,65 +58,201 @@ export function WhoCanReply({
return null
}
+ const isEverybody = settings.length === 0
+ const isNobody = !!settings.find(gate => gate.type === 'nobody')
+ const description = isEverybody
+ ? _(msg`Everybody can reply`)
+ : isNobody
+ ? _(msg`Replies disabled`)
+ : _(msg`Some people can reply`)
+
return (
-
-
-
- {!settings.length ? (
- Everybody can reply.
- ) : settings[0].type === 'nobody' ? (
- Replies to this thread are disabled.
- ) : (
-
- Only{' '}
- {settings.map((rule, i) => (
- <>
-
-
- >
- ))}{' '}
- can reply.
-
- )}
+ <>
+
+
+ >
+ )
+}
+
+export function WhoCanReplyBlock({
+ post,
+ isThreadAuthor,
+ style,
+}: WhoCanReplyProps) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const infoDialogControl = useDialogControl()
+ const {settings, isRootPost, onPressEdit} = useWhoCanReply(post)
+
+ if (!isRootPost) {
+ return null
+ }
+ if (!settings.length && !isThreadAuthor) {
+ return null
+ }
+
+ const isEverybody = settings.length === 0
+ const isNobody = !!settings.find(gate => gate.type === 'nobody')
+ const description = isEverybody
+ ? _(msg`Everybody can reply`)
+ : isNobody
+ ? _(msg`Replies on this thread are disabled`)
+ : _(msg`Some people can reply`)
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+function Icon({
+ color,
+ width,
+ settings,
+}: {
+ color: string
+ width?: number
+ settings: ThreadgateSetting[]
+}) {
+ const isEverybody = settings.length === 0
+ const isNobody = !!settings.find(gate => gate.type === 'nobody')
+ const IconComponent = isEverybody ? Earth : isNobody ? CircleBanSign : Group
+ return
+}
+
+function InfoDialog({
+ control,
+ post,
+ settings,
+}: {
+ control: Dialog.DialogControlProps
+ post: AppBskyFeedDefs.PostView
+ settings: ThreadgateSetting[]
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function InfoDialogInner({
+ post,
+ settings,
+}: {
+ post: AppBskyFeedDefs.PostView
+ settings: ThreadgateSetting[]
+}) {
+ const {_} = useLingui()
+ return (
+
+
+
+ Who can reply?
+
- {isThreadAuthor && (
-
-
-
+
+ )
+}
+
+function Rules({
+ post,
+ settings,
+}: {
+ post: AppBskyFeedDefs.PostView
+ settings: ThreadgateSetting[]
+}) {
+ const t = useTheme()
+ return (
+
+ {!settings.length ? (
+ Everybody can reply
+ ) : settings[0].type === 'nobody' ? (
+ Replies to this thread are disabled
+ ) : (
+
+ Only{' '}
+ {settings.map((rule, i) => (
+ <>
+
+
+ >
+ ))}{' '}
+ can reply
+
)}
-
+
)
}
@@ -178,7 +265,7 @@ function Rule({
post: AppBskyFeedDefs.PostView
lists: AppBskyGraphDefs.ListViewBasic[] | undefined
}) {
- const pal = usePalette('default')
+ const t = useTheme()
if (rule.type === 'mention') {
return mentioned users
}
@@ -190,7 +277,7 @@ function Rule({
type="sm"
href={makeProfileLink(post.author)}
text={`@${post.author.handle}`}
- style={pal.link}
+ style={{color: t.palette.primary_500}}
/>
)
@@ -205,7 +292,7 @@ function Rule({
type="sm"
href={makeListLink(listUrip.hostname, listUrip.rkey)}
text={list.name}
- style={pal.link}
+ style={{color: t.palette.primary_500}}
/>{' '}
members
@@ -227,3 +314,78 @@ function Separator({i, length}: {i: number; length: number}) {
}
return <>, >
}
+
+function useWhoCanReply(post: AppBskyFeedDefs.PostView) {
+ const agent = useAgent()
+ const queryClient = useQueryClient()
+ const {openModal} = useModalControls()
+
+ const settings = React.useMemo(
+ () => threadgateViewToSettings(post.threadgate),
+ [post],
+ )
+ const isRootPost = !('reply' in post.record)
+
+ const onPressEdit = () => {
+ 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,
+ })
+ }
+ await whenAppViewReady(agent, post.uri, res => {
+ const thread = res.data.thread
+ if (AppBskyFeedDefs.isThreadViewPost(thread)) {
+ const fetchedSettings = threadgateViewToSettings(
+ thread.post.threadgate,
+ )
+ return (
+ JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
+ )
+ }
+ return false
+ })
+ Toast.show('Thread settings updated')
+ queryClient.invalidateQueries({
+ queryKey: [POST_THREAD_RQKEY_ROOT],
+ })
+ } 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 {settings, isRootPost, onPressEdit}
+}
+
+async function whenAppViewReady(
+ agent: BskyAgent,
+ uri: string,
+ fn: (res: AppBskyFeedGetPostThread.Response) => boolean,
+) {
+ await until(
+ 5, // 5 tries
+ 1e3, // 1s delay between tries
+ fn,
+ () =>
+ agent.app.bsky.feed.getPostThread({
+ uri,
+ depth: 0,
+ }),
+ )
+}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 55fb4a33..472ce404 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -15,7 +15,7 @@ import {
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
+import {POST_CTRL_HITSLOP} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
@@ -215,7 +215,7 @@ let PostCtrls = ({
other: 'Reply (# replies)',
})}
accessibilityHint=""
- hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+ hitSlop={POST_CTRL_HITSLOP}>
+ hitSlop={POST_CTRL_HITSLOP}>
{post.viewer?.like ? (
) : (
@@ -299,7 +299,7 @@ let PostCtrls = ({
}}
accessibilityLabel={_(msg`Share`)}
accessibilityHint=""
- hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+ hitSlop={POST_CTRL_HITSLOP}>
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 10bc369b..d49cda44 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
+import {POST_CTRL_HITSLOP} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {useRequireAuth} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
@@ -67,7 +67,7 @@ let RepostButton = ({
shape="round"
variant="ghost"
color="secondary"
- hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+ hitSlop={POST_CTRL_HITSLOP}>
{typeof repostCount !== 'undefined' && repostCount > 0 ? (