Files
bsky-social-app/src/components/WhoCanReply.tsx
T
Eric Bailey a51fc8e434 @atproto/api@next integration (#7344)
* Bump SDK

* Use consistent type in profile query

* Omit  from constraint for profile shadow

* Replace isRecord with isValidRecord in QuoteEmbed

* Omit type from constraint for old ProfileCard

* Omit type from constraint in profile queries where appropriate

* Use correct type for update profile mutation

* Conslidate and fix check for isValidRecord in Post.tsx

* Replace isRecord with isValidRecord in PostThreadItem

* Remove redundant cast in PostThreadFollowBtn

* Ignore errors in DebugMod screen

* Use matching type in ProfileFollows screen

* Use matching type in ProfileFollowers screen

* Migrate to isValidRecord in PostFeedItem

* Use matching type if PostRepostedBy

* Omit type from constraint in avatar props

* Use matching types in NotificationFeedItem

* Todo

* Use isValidRecord in NotfyFeedItem

* Improve MediaPreview types

* Migrate another isValidRecord in NotificationFeedItem

* Migrate to isValidView in queries/util

* Migrate to isValidRecord in threadgate/util

* Fix types in threadgates

* Fix up types in starter-packs queries

* Todo

* Specify exact types in search-posts

* Use internal type util to align types

* Ditto last

* Migrate postgate/index

* Specify exact types in post-thread

* Use correct type in post-quotes

* FIX potential bug in post-thread

* Use correct type in post-feed

* Add correct type guards to notifications/feed

* Migrate a guard in notifications/util

* Migrate guard in Wizard/State

* [@next] Profile handling, migrate `ProfileCard` (#7347)

* Introduce new utils for profiles, migrate old ProfileCard

* Rename, reorg

* Add parseEmbed utils

* Expand AnyProfileView to include chat profile view, update post shadow to reflect this

* Cast for perf reasons

* Tighten up types now that we have AnyProfileView

* Add fastIsType util

* Use `assertDid`

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use util types

* Comment

* Use fastIsType where no validation was happening before

* suggestions (#7382)

* suggestions

* Revert unneeded changes

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Use new util

* Rename to dangerousIsType

* Convert object shape

* Use dangerous util

* Use dangerous util

* Use dangerous util, we can trust post records

* Use dangerous util

* Use AnyProfileVIew

* Convert object shape

* Clean up handling

* Patch moderateProfile to accept known profile views, to discuss

* Add AnyStarterPackView and related, implement in first usage

* Remove validation, fix type, fix ref

* Migrate over list-conversations

* Clarify intent behind precacheProfile and its unstable query cache

* Clean up unstable profile cache

* Fix types during label creation in PwiOptOut (#7346)

* Tighten types in queries/list

* Chat: use correct profile views

* Chat: fix log type check

* Chat: construct lexically correct shape, even though it's only internal usage

* Chat: use correct profile types

* Chat: fix type check in logs

* Starter: use correct profile types

* Starter: use correct profile types

* Starter: tighten types to match lex

* Any profile type will work in blocked-and-muted

* Use dangerous util

* Use dangerousIsType

* Update new ProfileCard to use AnyProfileView

* Use dangerousIsType

* Remove outdated todo

* Use correct profile type

* Use correct profile types

* Tighten up types

* Use dangerousIsType

* Chat: more type fixes

* Remove unused file

* Add a few  utils

* Remove unused file

* Ignore feedPost.__source

* Clean up types, leave validation in critical path

* Use dangerousIstype

* Use ANyProfileView

* Use isValidRecord

* Use dangerousIsType

* Fix types in ListCard

* Fix FeedInterstitials types

* Fix types in FeedCard

* Fix types in dms ReportDialog

* Fix types in SearchablePeopleList

* Fix bad type in composer opts

* Starter: ok these need to be loose too

* Clarify docs

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Less code

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

* Bump sdk, update $Typed imports

* Format

* Format

* Fix weird TS error

* Remove patch

* Beter name

* It's memo, can validate

* Tighten up parseEmbed, dogfood

* Bump sdk

* Use asPredicate

* Loosen types a bit

* use asPredicate

* Fix types

* Use asPredicate

* Use asPredicate

* Use asPredicate

* Clean up upsertProfile types

* Use asPredicate

* Use Un util

* Fix types

* Use new AnyProfileView

* Use dangerousIsType

* Use asPredicate

* Use asPredicate

* Add fallback content-type to pass typecheck

* Clean up upsertProfile types

* Align types

* Use dangerousIsType

* Use dangerousIsType

* Use asPredicate

* Align types

* Convert findLast

* Align types

* Just ignore type errors and use findLast

* Rename atproto -> bsky

* Add validate util

* Fix type error

* Loosen types

* Export post

* rename atp bsky

* Remove unused code

* minor changes

* Bump deps

* Fix types

* Tighten back up loose check

* Tighten back up loose check

* Fix small bug

* Update comment

* Revert change

---------

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>
Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
2025-02-18 08:54:25 -06:00

323 lines
8.6 KiB
TypeScript

import React from 'react'
import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
import {isNative} from '#/platform/detection'
import {
ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
} from '#/state/queries/threadgate'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {
PostInteractionSettingsDialog,
usePrefetchPostInteractionSettings,
} from '#/components/dialogs/PostInteractionSettingsDialog'
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 {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
interface WhoCanReplyProps {
post: AppBskyFeedDefs.PostView
isThreadAuthor: boolean
style?: StyleProp<ViewStyle>
}
export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
const {_} = useLingui()
const t = useTheme()
const infoDialogControl = useDialogControl()
const editDialogControl = useDialogControl()
/*
* `WhoCanReply` is only used for root posts atm, in case this changes
* unexpectedly, we should check to make sure it's for sure the root URI.
*/
const rootUri =
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
) && post.record.reply?.root
? post.record.reply.root.uri
: post.uri
const settings = React.useMemo(() => {
return threadgateViewToAllowUISetting(post.threadgate)
}, [post.threadgate])
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
postUri: post.uri,
rootPostUri: rootUri,
})
const anyoneCanReply =
settings.length === 1 && settings[0].type === 'everybody'
const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody'
const description = anyoneCanReply
? _(msg`Everybody can reply`)
: noOneCanReply
? _(msg`Replies disabled`)
: _(msg`Some people can reply`)
const onPressOpen = () => {
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
if (isThreadAuthor) {
editDialogControl.open()
} else {
infoDialogControl.open()
}
}
return (
<>
<Button
label={
isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
}
onPress={onPressOpen}
{...(isThreadAuthor
? Platform.select({
web: {
onHoverIn: prefetchPostInteractionSettings,
},
native: {
onPressIn: prefetchPostInteractionSettings,
},
})
: {})}
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>
{isThreadAuthor && (
<PencilLine width={12} fill={t.palette.primary_500} />
)}
</View>
)}
</Button>
{isThreadAuthor ? (
<PostInteractionSettingsDialog
postUri={post.uri}
rootPostUri={rootUri}
control={editDialogControl}
initialThreadgateView={post.threadgate}
/>
) : (
<WhoCanReplyDialog
control={infoDialogControl}
post={post}
settings={settings}
embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
/>
)}
</>
)
}
function Icon({
color,
width,
settings,
}: {
color: string
width?: number
settings: ThreadgateAllowUISetting[]
}) {
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 WhoCanReplyDialog({
control,
post,
settings,
embeddingDisabled,
}: {
control: Dialog.DialogControlProps
post: AppBskyFeedDefs.PostView
settings: ThreadgateAllowUISetting[]
embeddingDisabled: boolean
}) {
const {_} = useLingui()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Dialog: adjust who can interact with this post`)}
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_xl, a.pb_sm]}>
<Trans>Who can interact with this post?</Trans>
</Text>
<Rules
post={post}
settings={settings}
embeddingDisabled={embeddingDisabled}
/>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
function Rules({
post,
settings,
embeddingDisabled,
}: {
post: AppBskyFeedDefs.PostView
settings: ThreadgateAllowUISetting[]
embeddingDisabled: boolean
}) {
const t = useTheme()
return (
<>
<Text
style={[
a.text_sm,
a.leading_snug,
a.flex_wrap,
t.atoms.text_contrast_medium,
]}>
{settings.length === 0 ? (
<Trans>
This post has an unknown type of threadgate on it. Your app may be
out of date.
</Trans>
) : settings[0].type === 'everybody' ? (
<Trans>Everybody can reply to this post.</Trans>
) : settings[0].type === 'nobody' ? (
<Trans>Replies to this post are disabled.</Trans>
) : (
<Trans>
Only{' '}
{settings.map((rule, i) => (
<React.Fragment key={`rule-${i}`}>
<Rule rule={rule} post={post} lists={post.threadgate!.lists} />
<Separator i={i} length={settings.length} />
</React.Fragment>
))}{' '}
can reply.
</Trans>
)}{' '}
</Text>
{embeddingDisabled && (
<Text
style={[
a.text_sm,
a.leading_snug,
a.flex_wrap,
t.atoms.text_contrast_medium,
]}>
<Trans>No one but the author can quote this post.</Trans>
</Text>
)}
</>
)
}
function Rule({
rule,
post,
lists,
}: {
rule: ThreadgateAllowUISetting
post: AppBskyFeedDefs.PostView
lists: AppBskyGraphDefs.ListViewBasic[] | undefined
}) {
if (rule.type === 'mention') {
return <Trans>mentioned users</Trans>
}
if (rule.type === 'followers') {
return (
<Trans>
users following{' '}
<InlineLinkText
label={`@${post.author.handle}`}
to={makeProfileLink(post.author)}
style={[a.text_sm, a.leading_snug]}>
@{post.author.handle}
</InlineLinkText>
</Trans>
)
}
if (rule.type === 'following') {
return (
<Trans>
users followed by{' '}
<InlineLinkText
label={`@${post.author.handle}`}
to={makeProfileLink(post.author)}
style={[a.text_sm, a.leading_snug]}>
@{post.author.handle}
</InlineLinkText>
</Trans>
)
}
if (rule.type === 'list') {
const list = lists?.find(l => l.uri === rule.list)
if (list) {
const listUrip = new AtUri(list.uri)
return (
<Trans>
<InlineLinkText
label={list.name}
to={makeListLink(listUrip.hostname, listUrip.rkey)}
style={[a.text_sm, a.leading_snug]}>
{list.name}
</InlineLinkText>{' '}
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 <>, </>
}