Remove the 'Who can reply' element except when viewing root, and add "edit" (#4615)
* Remove the 'Who can reply' element except when viewing root, and add the edit text to authors * Switch to icon
This commit is contained in:
		
							parent
							
								
									0a0c738790
								
							
						
					
					
						commit
						f769564edf
					
				
					 2 changed files with 165 additions and 237 deletions
				
			
		| 
						 | 
				
			
			@ -34,8 +34,8 @@ import {ContentHider} from '../../../components/moderation/ContentHider'
 | 
			
		|||
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
 | 
			
		||||
import {PostAlerts} from '../../../components/moderation/PostAlerts'
 | 
			
		||||
import {PostHider} from '../../../components/moderation/PostHider'
 | 
			
		||||
import {WhoCanReply} from '../../../components/WhoCanReply'
 | 
			
		||||
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
 | 
			
		||||
import {WhoCanReplyBlock, WhoCanReplyInline} from '../threadgate/WhoCanReply'
 | 
			
		||||
import {ErrorMessage} from '../util/error/ErrorMessage'
 | 
			
		||||
import {Link, TextLink} from '../util/Link'
 | 
			
		||||
import {formatCount} from '../util/numeric/format'
 | 
			
		||||
| 
						 | 
				
			
			@ -406,177 +406,172 @@ let PostThreadItemLoaded = ({
 | 
			
		|||
    const isThreadedChildAdjacentBot =
 | 
			
		||||
      isThreadedChild && nextPost?.ctx.depth === depth
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <PostOuterWrapper
 | 
			
		||||
          post={post}
 | 
			
		||||
          depth={depth}
 | 
			
		||||
          showParentReplyLine={!!showParentReplyLine}
 | 
			
		||||
          treeView={treeView}
 | 
			
		||||
          hasPrecedingItem={hasPrecedingItem}
 | 
			
		||||
          hideTopBorder={hideTopBorder}>
 | 
			
		||||
          <PostHider
 | 
			
		||||
            testID={`postThreadItem-by-${post.author.handle}`}
 | 
			
		||||
            href={postHref}
 | 
			
		||||
            disabled={overrideBlur}
 | 
			
		||||
            style={[pal.view]}
 | 
			
		||||
            modui={moderation.ui('contentList')}
 | 
			
		||||
            iconSize={isThreadedChild ? 26 : 38}
 | 
			
		||||
            iconStyles={
 | 
			
		||||
              isThreadedChild
 | 
			
		||||
                ? {marginRight: 4}
 | 
			
		||||
                : {marginLeft: 2, marginRight: 2}
 | 
			
		||||
            }
 | 
			
		||||
            profile={post.author}
 | 
			
		||||
            interpretFilterAsBlur>
 | 
			
		||||
            <View
 | 
			
		||||
              style={{
 | 
			
		||||
                flexDirection: 'row',
 | 
			
		||||
                gap: 10,
 | 
			
		||||
                paddingLeft: 8,
 | 
			
		||||
                height: isThreadedChildAdjacentTop ? 8 : 16,
 | 
			
		||||
              }}>
 | 
			
		||||
              <View style={{width: 38}}>
 | 
			
		||||
                {!isThreadedChild && showParentReplyLine && (
 | 
			
		||||
      <PostOuterWrapper
 | 
			
		||||
        post={post}
 | 
			
		||||
        depth={depth}
 | 
			
		||||
        showParentReplyLine={!!showParentReplyLine}
 | 
			
		||||
        treeView={treeView}
 | 
			
		||||
        hasPrecedingItem={hasPrecedingItem}
 | 
			
		||||
        hideTopBorder={hideTopBorder}>
 | 
			
		||||
        <PostHider
 | 
			
		||||
          testID={`postThreadItem-by-${post.author.handle}`}
 | 
			
		||||
          href={postHref}
 | 
			
		||||
          disabled={overrideBlur}
 | 
			
		||||
          style={[pal.view]}
 | 
			
		||||
          modui={moderation.ui('contentList')}
 | 
			
		||||
          iconSize={isThreadedChild ? 26 : 38}
 | 
			
		||||
          iconStyles={
 | 
			
		||||
            isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
 | 
			
		||||
          }
 | 
			
		||||
          profile={post.author}
 | 
			
		||||
          interpretFilterAsBlur>
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              flexDirection: 'row',
 | 
			
		||||
              gap: 10,
 | 
			
		||||
              paddingLeft: 8,
 | 
			
		||||
              height: isThreadedChildAdjacentTop ? 8 : 16,
 | 
			
		||||
            }}>
 | 
			
		||||
            <View style={{width: 38}}>
 | 
			
		||||
              {!isThreadedChild && showParentReplyLine && (
 | 
			
		||||
                <View
 | 
			
		||||
                  style={[
 | 
			
		||||
                    styles.replyLine,
 | 
			
		||||
                    {
 | 
			
		||||
                      flexGrow: 1,
 | 
			
		||||
                      backgroundColor: pal.colors.replyLine,
 | 
			
		||||
                      marginBottom: 4,
 | 
			
		||||
                    },
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </View>
 | 
			
		||||
          </View>
 | 
			
		||||
 | 
			
		||||
          <View
 | 
			
		||||
            style={[
 | 
			
		||||
              styles.layout,
 | 
			
		||||
              {
 | 
			
		||||
                paddingBottom:
 | 
			
		||||
                  showChildReplyLine && !isThreadedChild
 | 
			
		||||
                    ? 0
 | 
			
		||||
                    : isThreadedChildAdjacentBot
 | 
			
		||||
                    ? 4
 | 
			
		||||
                    : 8,
 | 
			
		||||
              },
 | 
			
		||||
            ]}>
 | 
			
		||||
            {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
 | 
			
		||||
            {!isThreadedChild && (
 | 
			
		||||
              <View style={styles.layoutAvi}>
 | 
			
		||||
                <PreviewableUserAvatar
 | 
			
		||||
                  size={38}
 | 
			
		||||
                  profile={post.author}
 | 
			
		||||
                  moderation={moderation.ui('avatar')}
 | 
			
		||||
                  type={post.author.associated?.labeler ? 'labeler' : 'user'}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                {showChildReplyLine && (
 | 
			
		||||
                  <View
 | 
			
		||||
                    style={[
 | 
			
		||||
                      styles.replyLine,
 | 
			
		||||
                      {
 | 
			
		||||
                        flexGrow: 1,
 | 
			
		||||
                        backgroundColor: pal.colors.replyLine,
 | 
			
		||||
                        marginBottom: 4,
 | 
			
		||||
                        marginTop: 4,
 | 
			
		||||
                      },
 | 
			
		||||
                    ]}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
              </View>
 | 
			
		||||
            </View>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <View
 | 
			
		||||
              style={[
 | 
			
		||||
                styles.layout,
 | 
			
		||||
                {
 | 
			
		||||
                  paddingBottom:
 | 
			
		||||
                    showChildReplyLine && !isThreadedChild
 | 
			
		||||
                      ? 0
 | 
			
		||||
                      : isThreadedChildAdjacentBot
 | 
			
		||||
                      ? 4
 | 
			
		||||
                      : 8,
 | 
			
		||||
                },
 | 
			
		||||
              ]}>
 | 
			
		||||
              {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
 | 
			
		||||
              {!isThreadedChild && (
 | 
			
		||||
                <View style={styles.layoutAvi}>
 | 
			
		||||
                  <PreviewableUserAvatar
 | 
			
		||||
                    size={38}
 | 
			
		||||
                    profile={post.author}
 | 
			
		||||
                    moderation={moderation.ui('avatar')}
 | 
			
		||||
                    type={post.author.associated?.labeler ? 'labeler' : 'user'}
 | 
			
		||||
              style={
 | 
			
		||||
                isThreadedChild
 | 
			
		||||
                  ? styles.layoutContentThreaded
 | 
			
		||||
                  : styles.layoutContent
 | 
			
		||||
              }>
 | 
			
		||||
              <PostMeta
 | 
			
		||||
                author={post.author}
 | 
			
		||||
                moderation={moderation}
 | 
			
		||||
                authorHasWarning={!!post.author.labels?.length}
 | 
			
		||||
                timestamp={post.indexedAt}
 | 
			
		||||
                postHref={postHref}
 | 
			
		||||
                showAvatar={isThreadedChild}
 | 
			
		||||
                avatarModeration={moderation.ui('avatar')}
 | 
			
		||||
                avatarSize={28}
 | 
			
		||||
                displayNameType="md-bold"
 | 
			
		||||
                displayNameStyle={isThreadedChild && s.ml2}
 | 
			
		||||
                style={
 | 
			
		||||
                  isThreadedChild && {
 | 
			
		||||
                    alignItems: 'center',
 | 
			
		||||
                    paddingBottom: isWeb ? 5 : 2,
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <LabelsOnMyPost post={post} />
 | 
			
		||||
              <PostAlerts
 | 
			
		||||
                modui={moderation.ui('contentList')}
 | 
			
		||||
                style={[a.pt_2xs, a.pb_2xs]}
 | 
			
		||||
              />
 | 
			
		||||
              {richText?.text ? (
 | 
			
		||||
                <View style={styles.postTextContainer}>
 | 
			
		||||
                  <RichText
 | 
			
		||||
                    enableTags
 | 
			
		||||
                    value={richText}
 | 
			
		||||
                    style={[a.flex_1, a.text_md]}
 | 
			
		||||
                    numberOfLines={limitLines ? MAX_POST_LINES : undefined}
 | 
			
		||||
                    authorHandle={post.author.handle}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  {showChildReplyLine && (
 | 
			
		||||
                    <View
 | 
			
		||||
                      style={[
 | 
			
		||||
                        styles.replyLine,
 | 
			
		||||
                        {
 | 
			
		||||
                          flexGrow: 1,
 | 
			
		||||
                          backgroundColor: pal.colors.replyLine,
 | 
			
		||||
                          marginTop: 4,
 | 
			
		||||
                        },
 | 
			
		||||
                      ]}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                </View>
 | 
			
		||||
              ) : undefined}
 | 
			
		||||
              {limitLines ? (
 | 
			
		||||
                <TextLink
 | 
			
		||||
                  text={_(msg`Show More`)}
 | 
			
		||||
                  style={pal.link}
 | 
			
		||||
                  onPress={onPressShowMore}
 | 
			
		||||
                  href="#"
 | 
			
		||||
                />
 | 
			
		||||
              ) : undefined}
 | 
			
		||||
              {post.embed && (
 | 
			
		||||
                <View style={[a.pb_xs]}>
 | 
			
		||||
                  <PostEmbeds embed={post.embed} moderation={moderation} />
 | 
			
		||||
                </View>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <View
 | 
			
		||||
                style={
 | 
			
		||||
                  isThreadedChild
 | 
			
		||||
                    ? styles.layoutContentThreaded
 | 
			
		||||
                    : styles.layoutContent
 | 
			
		||||
                }>
 | 
			
		||||
                <PostMeta
 | 
			
		||||
                  author={post.author}
 | 
			
		||||
                  moderation={moderation}
 | 
			
		||||
                  authorHasWarning={!!post.author.labels?.length}
 | 
			
		||||
                  timestamp={post.indexedAt}
 | 
			
		||||
                  postHref={postHref}
 | 
			
		||||
                  showAvatar={isThreadedChild}
 | 
			
		||||
                  avatarModeration={moderation.ui('avatar')}
 | 
			
		||||
                  avatarSize={28}
 | 
			
		||||
                  displayNameType="md-bold"
 | 
			
		||||
                  displayNameStyle={isThreadedChild && s.ml2}
 | 
			
		||||
                  style={
 | 
			
		||||
                    isThreadedChild && {
 | 
			
		||||
                      alignItems: 'center',
 | 
			
		||||
                      paddingBottom: isWeb ? 5 : 2,
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                <LabelsOnMyPost post={post} />
 | 
			
		||||
                <PostAlerts
 | 
			
		||||
                  modui={moderation.ui('contentList')}
 | 
			
		||||
                  style={[a.pt_2xs, a.pb_2xs]}
 | 
			
		||||
                />
 | 
			
		||||
                {richText?.text ? (
 | 
			
		||||
                  <View style={styles.postTextContainer}>
 | 
			
		||||
                    <RichText
 | 
			
		||||
                      enableTags
 | 
			
		||||
                      value={richText}
 | 
			
		||||
                      style={[a.flex_1, a.text_md]}
 | 
			
		||||
                      numberOfLines={limitLines ? MAX_POST_LINES : undefined}
 | 
			
		||||
                      authorHandle={post.author.handle}
 | 
			
		||||
                    />
 | 
			
		||||
                  </View>
 | 
			
		||||
                ) : undefined}
 | 
			
		||||
                {limitLines ? (
 | 
			
		||||
                  <TextLink
 | 
			
		||||
                    text={_(msg`Show More`)}
 | 
			
		||||
                    style={pal.link}
 | 
			
		||||
                    onPress={onPressShowMore}
 | 
			
		||||
                    href="#"
 | 
			
		||||
                  />
 | 
			
		||||
                ) : undefined}
 | 
			
		||||
                {post.embed && (
 | 
			
		||||
                  <View style={[a.pb_xs]}>
 | 
			
		||||
                    <PostEmbeds embed={post.embed} moderation={moderation} />
 | 
			
		||||
                  </View>
 | 
			
		||||
                )}
 | 
			
		||||
                <PostCtrls
 | 
			
		||||
                  post={post}
 | 
			
		||||
                  record={record}
 | 
			
		||||
                  richText={richText}
 | 
			
		||||
                  onPressReply={onPressReply}
 | 
			
		||||
                  logContext="PostThreadItem"
 | 
			
		||||
                />
 | 
			
		||||
              </View>
 | 
			
		||||
              <PostCtrls
 | 
			
		||||
                post={post}
 | 
			
		||||
                record={record}
 | 
			
		||||
                richText={richText}
 | 
			
		||||
                onPressReply={onPressReply}
 | 
			
		||||
                logContext="PostThreadItem"
 | 
			
		||||
              />
 | 
			
		||||
            </View>
 | 
			
		||||
            {hasMore ? (
 | 
			
		||||
              <Link
 | 
			
		||||
                style={[
 | 
			
		||||
                  styles.loadMore,
 | 
			
		||||
                  {
 | 
			
		||||
                    paddingLeft: treeView ? 8 : 70,
 | 
			
		||||
                    paddingTop: 0,
 | 
			
		||||
                    paddingBottom: treeView ? 4 : 12,
 | 
			
		||||
                  },
 | 
			
		||||
                ]}
 | 
			
		||||
                href={postHref}
 | 
			
		||||
                title={itemTitle}
 | 
			
		||||
                noFeedback>
 | 
			
		||||
                <Text type="sm-medium" style={pal.textLight}>
 | 
			
		||||
                  <Trans>More</Trans>
 | 
			
		||||
                </Text>
 | 
			
		||||
                <FontAwesomeIcon
 | 
			
		||||
                  icon="angle-right"
 | 
			
		||||
                  color={pal.colors.textLight}
 | 
			
		||||
                  size={14}
 | 
			
		||||
                />
 | 
			
		||||
              </Link>
 | 
			
		||||
            ) : undefined}
 | 
			
		||||
          </PostHider>
 | 
			
		||||
        </PostOuterWrapper>
 | 
			
		||||
        <WhoCanReplyBlock post={post} isThreadAuthor={isThreadAuthor} />
 | 
			
		||||
      </>
 | 
			
		||||
          </View>
 | 
			
		||||
          {hasMore ? (
 | 
			
		||||
            <Link
 | 
			
		||||
              style={[
 | 
			
		||||
                styles.loadMore,
 | 
			
		||||
                {
 | 
			
		||||
                  paddingLeft: treeView ? 8 : 70,
 | 
			
		||||
                  paddingTop: 0,
 | 
			
		||||
                  paddingBottom: treeView ? 4 : 12,
 | 
			
		||||
                },
 | 
			
		||||
              ]}
 | 
			
		||||
              href={postHref}
 | 
			
		||||
              title={itemTitle}
 | 
			
		||||
              noFeedback>
 | 
			
		||||
              <Text type="sm-medium" style={pal.textLight}>
 | 
			
		||||
                <Trans>More</Trans>
 | 
			
		||||
              </Text>
 | 
			
		||||
              <FontAwesomeIcon
 | 
			
		||||
                icon="angle-right"
 | 
			
		||||
                color={pal.colors.textLight}
 | 
			
		||||
                size={14}
 | 
			
		||||
              />
 | 
			
		||||
            </Link>
 | 
			
		||||
          ) : undefined}
 | 
			
		||||
        </PostHider>
 | 
			
		||||
      </PostOuterWrapper>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -671,7 +666,7 @@ function ExpandedPostDetails({
 | 
			
		|||
        s.mb10,
 | 
			
		||||
      ]}>
 | 
			
		||||
      <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
 | 
			
		||||
      <WhoCanReplyInline post={post} isThreadAuthor={isThreadAuthor} />
 | 
			
		||||
      <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
 | 
			
		||||
      {needsTranslation && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Text style={[a.text_sm, pal.textLight]}>·</Text>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,391 +0,0 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
 | 
			
		||||
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 {createThreadgate} from '#/lib/api'
 | 
			
		||||
import {until} from '#/lib/async/until'
 | 
			
		||||
import {HITSLOP_10} from '#/lib/constants'
 | 
			
		||||
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
 | 
			
		||||
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 {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'
 | 
			
		||||
 | 
			
		||||
interface WhoCanReplyProps {
 | 
			
		||||
  post: AppBskyFeedDefs.PostView
 | 
			
		||||
  isThreadAuthor: boolean
 | 
			
		||||
  style?: StyleProp<ViewStyle>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  }
 | 
			
		||||
  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 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,
 | 
			
		||||
      }),
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue