From 80197556f176723b619349dc060d1b7001472d47 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 19 Jun 2024 18:39:45 -0700 Subject: [PATCH] Rework "Who can reply" to blend more nicely into the UI (#4578) * Rework WhoCanReply controls in threads to blend more nicely * Fix layout * Fix post control hitslops * Move dialog content to separate component --------- Co-authored-by: Dan Abramov --- src/lib/constants.ts | 1 + src/view/com/post-thread/PostThreadItem.tsx | 38 +- src/view/com/threadgate/WhoCanReply.tsx | 447 +++++++++++------- src/view/com/util/post-ctrls/PostCtrls.tsx | 10 +- src/view/com/util/post-ctrls/RepostButton.tsx | 4 +- 5 files changed, 314 insertions(+), 186 deletions(-) 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 7e3528d9..3f9970f5 100644 --- a/src/view/com/threadgate/WhoCanReply.tsx +++ b/src/view/com/threadgate/WhoCanReply.tsx @@ -11,13 +11,10 @@ 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 {until} from '#/lib/async/until' -import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' -import {usePalette} from '#/lib/hooks/usePalette' +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' @@ -28,45 +25,301 @@ 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() +} + +export function WhoCanReplyInline({ + post, + isThreadAuthor, + style, +}: WhoCanReplyProps) { const {_} = useLingui() - const pal = usePalette('default') + 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 disabled`) + : _(msg`Some people can reply`) + + return ( + <> + + + + ) +} + +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? + + + + + ) +} + +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 + + )} + + ) +} + +function Rule({ + rule, + post, + lists, +}: { + rule: ThreadgateSetting + post: AppBskyFeedDefs.PostView + lists: AppBskyGraphDefs.ListViewBasic[] | undefined +}) { + const t = useTheme() + if (rule.type === 'mention') { + return mentioned users + } + if (rule.type === 'following') { + return ( + + users followed by{' '} + + + ) + } + if (rule.type === 'list') { + const list = lists?.find(l => l.uri === rule.list) + if (list) { + const listUrip = new AtUri(list.uri) + return ( + + {' '} + members + + ) + } + } +} + +function Separator({i, length}: {i: number; length: number}) { + if (length < 2 || i === length - 1) { + return null + } + if (i === length - 2) { + return ( + <> + {length > 2 ? ',' : ''} and{' '} + + ) + } + return <>, +} + +function useWhoCanReply(post: AppBskyFeedDefs.PostView) { 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], @@ -74,7 +327,6 @@ export function WhoCanReply({ const isRootPost = !('reply' in post.record) const onPressEdit = () => { - track('Post:EditThreadgateOpened') if (isNative && Keyboard.isVisible()) { Keyboard.dismiss() } @@ -108,7 +360,6 @@ export function WhoCanReply({ 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.', @@ -119,131 +370,7 @@ export function WhoCanReply({ }) } - if (!isRootPost) { - return null - } - if (!settings.length && !isThreadAuthor) { - return null - } - - return ( - - - - {!settings.length ? ( - Everybody can reply. - ) : settings[0].type === 'nobody' ? ( - Replies to this thread are disabled. - ) : ( - - Only{' '} - {settings.map((rule, i) => ( - - - - - ))}{' '} - can reply. - - )} - - - {isThreadAuthor && ( - - - - )} - - ) -} - -function Rule({ - rule, - post, - lists, -}: { - rule: ThreadgateSetting - post: AppBskyFeedDefs.PostView - lists: AppBskyGraphDefs.ListViewBasic[] | undefined -}) { - const pal = usePalette('default') - if (rule.type === 'mention') { - return mentioned users - } - if (rule.type === 'following') { - return ( - - users followed by{' '} - - - ) - } - if (rule.type === 'list') { - const list = lists?.find(l => l.uri === rule.list) - if (list) { - const listUrip = new AtUri(list.uri) - return ( - - {' '} - members - - ) - } - } -} - -function Separator({i, length}: {i: number; length: number}) { - if (length < 2 || i === length - 1) { - return null - } - if (i === length - 2) { - return ( - <> - {length > 2 ? ',' : ''} and{' '} - - ) - } - return <>, + return {settings, isRootPost, onPressEdit} } async function whenAppViewReady( 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 ? (