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 <dan.abramov@gmail.com>
This commit is contained in:
		
							parent
							
								
									75aec19230
								
							
						
					
					
						commit
						80197556f1
					
				
					 5 changed files with 314 additions and 186 deletions
				
			
		| 
						 | 
				
			
			@ -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 = ({
 | 
			
		|||
            </ContentHider>
 | 
			
		||||
            <ExpandedPostDetails
 | 
			
		||||
              post={post}
 | 
			
		||||
              isThreadAuthor={isThreadAuthor}
 | 
			
		||||
              translatorUrl={translatorUrl}
 | 
			
		||||
              needsTranslation={needsTranslation}
 | 
			
		||||
            />
 | 
			
		||||
| 
						 | 
				
			
			@ -396,11 +397,6 @@ let PostThreadItemLoaded = ({
 | 
			
		|||
            </View>
 | 
			
		||||
          </View>
 | 
			
		||||
        </View>
 | 
			
		||||
        <WhoCanReply
 | 
			
		||||
          post={post}
 | 
			
		||||
          isThreadAuthor={isThreadAuthor}
 | 
			
		||||
          style={{borderBottomWidth: isNative ? 1 : 0}}
 | 
			
		||||
        />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -579,14 +575,7 @@ let PostThreadItemLoaded = ({
 | 
			
		|||
            ) : undefined}
 | 
			
		||||
          </PostHider>
 | 
			
		||||
        </PostOuterWrapper>
 | 
			
		||||
        <WhoCanReply
 | 
			
		||||
          post={post}
 | 
			
		||||
          style={{
 | 
			
		||||
            marginTop: 4,
 | 
			
		||||
            borderBottomWidth: 1,
 | 
			
		||||
          }}
 | 
			
		||||
          isThreadAuthor={isThreadAuthor}
 | 
			
		||||
        />
 | 
			
		||||
        <WhoCanReplyBlock post={post} isThreadAuthor={isThreadAuthor} />
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <View style={[s.flexRow, s.mt2, s.mb10]}>
 | 
			
		||||
      <Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
 | 
			
		||||
    <View
 | 
			
		||||
      style={[
 | 
			
		||||
        a.flex_row,
 | 
			
		||||
        a.align_center,
 | 
			
		||||
        a.flex_wrap,
 | 
			
		||||
        a.gap_sm,
 | 
			
		||||
        s.mt2,
 | 
			
		||||
        s.mb10,
 | 
			
		||||
      ]}>
 | 
			
		||||
      <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
 | 
			
		||||
      <WhoCanReplyInline post={post} isThreadAuthor={isThreadAuthor} />
 | 
			
		||||
      {needsTranslation && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Text style={pal.textLight}> · </Text>
 | 
			
		||||
          <Text style={[a.text_sm, pal.textLight]}>·</Text>
 | 
			
		||||
 | 
			
		||||
          <Text
 | 
			
		||||
            style={pal.link}
 | 
			
		||||
            style={[a.text_sm, pal.link]}
 | 
			
		||||
            title={_(msg`Translate`)}
 | 
			
		||||
            onPress={onTranslatePress}>
 | 
			
		||||
            <Trans>Translate</Trans>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<ViewStyle>
 | 
			
		||||
}) {
 | 
			
		||||
  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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Button
 | 
			
		||||
        label={
 | 
			
		||||
          isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
 | 
			
		||||
        }
 | 
			
		||||
        onPress={isThreadAuthor ? onPressEdit : infoDialogControl.open}
 | 
			
		||||
        hitSlop={HITSLOP_10}>
 | 
			
		||||
        {({hovered}) => (
 | 
			
		||||
          <View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
 | 
			
		||||
            <Icon
 | 
			
		||||
              color={t.palette.contrast_400}
 | 
			
		||||
              width={16}
 | 
			
		||||
              settings={settings}
 | 
			
		||||
            />
 | 
			
		||||
            <Text
 | 
			
		||||
              style={[
 | 
			
		||||
                a.text_sm,
 | 
			
		||||
                a.leading_tight,
 | 
			
		||||
                t.atoms.text_contrast_medium,
 | 
			
		||||
                hovered && a.underline,
 | 
			
		||||
              ]}>
 | 
			
		||||
              {description}
 | 
			
		||||
            </Text>
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
      </Button>
 | 
			
		||||
      <InfoDialog control={infoDialogControl} post={post} settings={settings} />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Button
 | 
			
		||||
        label={
 | 
			
		||||
          isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
 | 
			
		||||
        }
 | 
			
		||||
        onPress={isThreadAuthor ? onPressEdit : infoDialogControl.open}
 | 
			
		||||
        hitSlop={HITSLOP_10}>
 | 
			
		||||
        {({hovered}) => (
 | 
			
		||||
          <View
 | 
			
		||||
            style={[
 | 
			
		||||
              a.flex_1,
 | 
			
		||||
              a.flex_row,
 | 
			
		||||
              a.align_center,
 | 
			
		||||
              a.py_sm,
 | 
			
		||||
              a.pr_lg,
 | 
			
		||||
              style,
 | 
			
		||||
            ]}>
 | 
			
		||||
            <View style={[{paddingLeft: 25, paddingRight: 18}]}>
 | 
			
		||||
              <Icon color={t.palette.contrast_300} settings={settings} />
 | 
			
		||||
            </View>
 | 
			
		||||
            <Text
 | 
			
		||||
              style={[
 | 
			
		||||
                a.text_sm,
 | 
			
		||||
                a.leading_tight,
 | 
			
		||||
                t.atoms.text_contrast_medium,
 | 
			
		||||
                hovered && a.underline,
 | 
			
		||||
              ]}>
 | 
			
		||||
              {description}
 | 
			
		||||
            </Text>
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
      </Button>
 | 
			
		||||
      <InfoDialog control={infoDialogControl} post={post} settings={settings} />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 <IconComponent fill={color} width={width} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InfoDialog({
 | 
			
		||||
  control,
 | 
			
		||||
  post,
 | 
			
		||||
  settings,
 | 
			
		||||
}: {
 | 
			
		||||
  control: Dialog.DialogControlProps
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  settings: ThreadgateSetting[]
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog.Outer control={control}>
 | 
			
		||||
      <Dialog.Handle />
 | 
			
		||||
      <InfoDialogInner post={post} settings={settings} />
 | 
			
		||||
    </Dialog.Outer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InfoDialogInner({
 | 
			
		||||
  post,
 | 
			
		||||
  settings,
 | 
			
		||||
}: {
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  settings: ThreadgateSetting[]
 | 
			
		||||
}) {
 | 
			
		||||
  const {_} = useLingui()
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog.ScrollableInner
 | 
			
		||||
      label={_(msg`Who can reply dialog`)}
 | 
			
		||||
      style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
 | 
			
		||||
      <View style={[a.gap_sm]}>
 | 
			
		||||
        <Text style={[a.font_bold, a.text_xl]}>
 | 
			
		||||
          <Trans>Who can reply?</Trans>
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Rules post={post} settings={settings} />
 | 
			
		||||
      </View>
 | 
			
		||||
    </Dialog.ScrollableInner>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Rules({
 | 
			
		||||
  post,
 | 
			
		||||
  settings,
 | 
			
		||||
}: {
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  settings: ThreadgateSetting[]
 | 
			
		||||
}) {
 | 
			
		||||
  const t = useTheme()
 | 
			
		||||
  return (
 | 
			
		||||
    <Text
 | 
			
		||||
      style={[
 | 
			
		||||
        a.text_md,
 | 
			
		||||
        a.leading_tight,
 | 
			
		||||
        a.flex_wrap,
 | 
			
		||||
        t.atoms.text_contrast_medium,
 | 
			
		||||
      ]}>
 | 
			
		||||
      {!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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Rule({
 | 
			
		||||
  rule,
 | 
			
		||||
  post,
 | 
			
		||||
  lists,
 | 
			
		||||
}: {
 | 
			
		||||
  rule: ThreadgateSetting
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  lists: AppBskyGraphDefs.ListViewBasic[] | undefined
 | 
			
		||||
}) {
 | 
			
		||||
  const t = useTheme()
 | 
			
		||||
  if (rule.type === 'mention') {
 | 
			
		||||
    return <Trans>mentioned users</Trans>
 | 
			
		||||
  }
 | 
			
		||||
  if (rule.type === 'following') {
 | 
			
		||||
    return (
 | 
			
		||||
      <Trans>
 | 
			
		||||
        users followed by{' '}
 | 
			
		||||
        <TextLink
 | 
			
		||||
          type="sm"
 | 
			
		||||
          href={makeProfileLink(post.author)}
 | 
			
		||||
          text={`@${post.author.handle}`}
 | 
			
		||||
          style={{color: t.palette.primary_500}}
 | 
			
		||||
        />
 | 
			
		||||
      </Trans>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (rule.type === 'list') {
 | 
			
		||||
    const list = lists?.find(l => l.uri === rule.list)
 | 
			
		||||
    if (list) {
 | 
			
		||||
      const listUrip = new AtUri(list.uri)
 | 
			
		||||
      return (
 | 
			
		||||
        <Trans>
 | 
			
		||||
          <TextLink
 | 
			
		||||
            type="sm"
 | 
			
		||||
            href={makeListLink(listUrip.hostname, listUrip.rkey)}
 | 
			
		||||
            text={list.name}
 | 
			
		||||
            style={{color: t.palette.primary_500}}
 | 
			
		||||
          />{' '}
 | 
			
		||||
          members
 | 
			
		||||
        </Trans>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Separator({i, length}: {i: number; length: number}) {
 | 
			
		||||
  if (length < 2 || i === length - 1) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  if (i === length - 2) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {length > 2 ? ',' : ''} <Trans>and</Trans>{' '}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  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 (
 | 
			
		||||
    <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) => (
 | 
			
		||||
                <React.Fragment key={`rule-${i}`}>
 | 
			
		||||
                  <Rule
 | 
			
		||||
                    rule={rule}
 | 
			
		||||
                    post={post}
 | 
			
		||||
                    lists={post.threadgate!.lists}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Separator key={`sep-${i}`} i={i} length={settings.length} />
 | 
			
		||||
                </React.Fragment>
 | 
			
		||||
              ))}{' '}
 | 
			
		||||
              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({
 | 
			
		||||
  rule,
 | 
			
		||||
  post,
 | 
			
		||||
  lists,
 | 
			
		||||
}: {
 | 
			
		||||
  rule: ThreadgateSetting
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  lists: AppBskyGraphDefs.ListViewBasic[] | undefined
 | 
			
		||||
}) {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
  if (rule.type === 'mention') {
 | 
			
		||||
    return <Trans>mentioned users</Trans>
 | 
			
		||||
  }
 | 
			
		||||
  if (rule.type === 'following') {
 | 
			
		||||
    return (
 | 
			
		||||
      <Trans>
 | 
			
		||||
        users followed by{' '}
 | 
			
		||||
        <TextLink
 | 
			
		||||
          type="sm"
 | 
			
		||||
          href={makeProfileLink(post.author)}
 | 
			
		||||
          text={`@${post.author.handle}`}
 | 
			
		||||
          style={pal.link}
 | 
			
		||||
        />
 | 
			
		||||
      </Trans>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (rule.type === 'list') {
 | 
			
		||||
    const list = lists?.find(l => l.uri === rule.list)
 | 
			
		||||
    if (list) {
 | 
			
		||||
      const listUrip = new AtUri(list.uri)
 | 
			
		||||
      return (
 | 
			
		||||
        <Trans>
 | 
			
		||||
          <TextLink
 | 
			
		||||
            type="sm"
 | 
			
		||||
            href={makeListLink(listUrip.hostname, listUrip.rkey)}
 | 
			
		||||
            text={list.name}
 | 
			
		||||
            style={pal.link}
 | 
			
		||||
          />{' '}
 | 
			
		||||
          members
 | 
			
		||||
        </Trans>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Separator({i, length}: {i: number; length: number}) {
 | 
			
		||||
  if (length < 2 || i === length - 1) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  if (i === length - 2) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {length > 2 ? ',' : ''} <Trans>and</Trans>{' '}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  return <>, </>
 | 
			
		||||
  return {settings, isRootPost, onPressEdit}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function whenAppViewReady(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}>
 | 
			
		||||
          <Bubble
 | 
			
		||||
            style={[defaultCtrlColor, {pointerEvents: 'none'}]}
 | 
			
		||||
            width={big ? 22 : 18}
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +258,7 @@ let PostCtrls = ({
 | 
			
		|||
                })
 | 
			
		||||
          }
 | 
			
		||||
          accessibilityHint=""
 | 
			
		||||
          hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
 | 
			
		||||
          hitSlop={POST_CTRL_HITSLOP}>
 | 
			
		||||
          {post.viewer?.like ? (
 | 
			
		||||
            <HeartIconFilled style={s.likeColor} width={big ? 22 : 18} />
 | 
			
		||||
          ) : (
 | 
			
		||||
| 
						 | 
				
			
			@ -299,7 +299,7 @@ let PostCtrls = ({
 | 
			
		|||
              }}
 | 
			
		||||
              accessibilityLabel={_(msg`Share`)}
 | 
			
		||||
              accessibilityHint=""
 | 
			
		||||
              hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
 | 
			
		||||
              hitSlop={POST_CTRL_HITSLOP}>
 | 
			
		||||
              <ArrowOutOfBox
 | 
			
		||||
                style={[defaultCtrlColor, {pointerEvents: 'none'}]}
 | 
			
		||||
                width={22}
 | 
			
		||||
| 
						 | 
				
			
			@ -325,7 +325,7 @@ let PostCtrls = ({
 | 
			
		|||
          record={record}
 | 
			
		||||
          richText={richText}
 | 
			
		||||
          style={{padding: 5}}
 | 
			
		||||
          hitSlop={big ? HITSLOP_20 : HITSLOP_10}
 | 
			
		||||
          hitSlop={POST_CTRL_HITSLOP}
 | 
			
		||||
          timestamp={post.indexedAt}
 | 
			
		||||
        />
 | 
			
		||||
      </View>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}>
 | 
			
		||||
        <Repost style={color} width={big ? 22 : 18} />
 | 
			
		||||
        {typeof repostCount !== 'undefined' && repostCount > 0 ? (
 | 
			
		||||
          <Text
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue