diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 305 | ||||
-rw-r--r-- | src/view/com/threadgate/WhoCanReply.tsx | 391 |
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]}>·</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, - }), - ) -} |