about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx305
-rw-r--r--src/view/com/threadgate/WhoCanReply.tsx391
2 files changed, 150 insertions, 546 deletions
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 92b529db7..46c6c958e 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -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'}
-                  />
-
-                  {showChildReplyLine && (
-                    <View
-                      style={[
-                        styles.replyLine,
-                        {
-                          flexGrow: 1,
-                          backgroundColor: pal.colors.replyLine,
-                          marginTop: 4,
-                        },
-                      ]}
-                    />
-                  )}
-                </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
-                    ? 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,
-                    }
+                  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="#"
+                }
+              />
+              <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}
                   />
-                ) : 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>
+              ) : undefined}
+              {limitLines ? (
+                <TextLink
+                  text={_(msg`Show More`)}
+                  style={pal.link}
+                  onPress={onPressShowMore}
+                  href="#"
                 />
-              </View>
+              ) : 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>
-            {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]}>&middot;</Text>
diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx
deleted file mode 100644
index 3f9970f5f..000000000
--- a/src/view/com/threadgate/WhoCanReply.tsx
+++ /dev/null
@@ -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,
-      }),
-  )
-}