Merge branch 'bluesky-social:main' into zh

zio/stable
Kuwa Lee 2024-06-13 15:29:51 +08:00 committed by GitHub
commit ecd51bc6f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 178 additions and 102 deletions

View File

@ -1,14 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link} from '#/components/Link'
import {Link, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
const AVI_SIZE = 30
@ -29,9 +29,11 @@ export function shouldShowKnownFollowers(
export function KnownFollowers({
profile,
moderationOpts,
onLinkPress,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress']
}) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(),
@ -56,6 +58,7 @@ export function KnownFollowers({
profile={profile}
cachedKnownFollowers={cachedKnownFollowers}
moderationOpts={moderationOpts}
onLinkPress={onLinkPress}
/>
)
}
@ -67,10 +70,12 @@ function KnownFollowersInner({
profile,
moderationOpts,
cachedKnownFollowers,
onLinkPress,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress']
}) {
const t = useTheme()
const {_} = useLingui()
@ -82,15 +87,6 @@ function KnownFollowersInner({
t.atoms.text_contrast_medium,
]
// list of users, minus blocks
const returnedCount = cachedKnownFollowers.followers.length
// db count, includes blocks
const fullCount = cachedKnownFollowers.count
// knownFollowers can return up to 5 users, but will exclude blocks
// therefore, if we have less 5 users, use whichever count is lower
const count =
returnedCount < 5 ? Math.min(fullCount, returnedCount) : fullCount
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
const moderation = moderateProfile(f, moderationOpts)
return {
@ -104,12 +100,14 @@ function KnownFollowersInner({
moderation,
}
})
const count = cachedKnownFollowers.count - Math.min(slice.length, 2)
return (
<Link
label={_(
msg`Press to view followers of this account that you also follow`,
)}
onPress={onLinkPress}
to={makeProfileLink(profile, 'known-followers')}
style={[
a.flex_1,
@ -166,31 +164,37 @@ function KnownFollowersInner({
},
]}
numberOfLines={2}>
<Trans>Followed by</Trans>{' '}
{count > 2 ? (
<>
{slice.slice(0, 2).map(({profile: prof}, i) => (
<Text key={prof.did} style={textStyle}>
{prof.displayName}
{i === 0 && ', '}
</Text>
))}
{', '}
{plural(count - 2, {
one: 'and # other',
other: 'and # others',
})}
</>
) : count === 2 ? (
slice.map(({profile: prof}, i) => (
<Text key={prof.did} style={textStyle}>
{prof.displayName} {i === 0 ? _(msg`and`) + ' ' : ''}
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
))
,{' '}
<Text key={slice[1].profile.did} style={textStyle}>
{slice[1].profile.displayName}
</Text>
, and{' '}
<Plural value={count - 2} one="# other" other="# others" />
</Trans>
) : count === 2 ? (
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>{' '}
and{' '}
<Text key={slice[1].profile.did} style={textStyle}>
{slice[1].profile.displayName}
</Text>
</Trans>
) : (
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
</Trans>
)}
</Text>
</>

View File

@ -5,6 +5,7 @@ import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getModerationCauseKey} from '#/lib/moderation'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
@ -22,11 +23,16 @@ import {useFollowMethods} from '#/components/hooks/useFollowMethods'
import {useRichText} from '#/components/hooks/useRichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Portal} from '#/components/Portal'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {ProfileLabel} from '../moderation/ProfileHeaderAlerts'
import {ProfileHoverCardProps} from './types'
const floatingMiddlewares = [
@ -370,7 +376,10 @@ function Inner({
profile: profileShadow,
logContext: 'ProfileHoverCard',
})
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
const isBlockedUser =
profile.viewer?.blocking ||
profile.viewer?.blockedBy ||
profile.viewer?.blockingByList
const following = formatCount(profile.followsCount || 0)
const followers = formatCount(profile.followersCount || 0)
const pluralizedFollowers = plural(profile.followersCount || 0, {
@ -401,29 +410,41 @@ function Inner({
/>
</Link>
{!isMe && (
<Button
size="small"
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
variant="solid"
label={
profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)
}
style={[a.rounded_full]}
onPress={profileShadow.viewer?.following ? unfollow : follow}>
<ButtonIcon
position="left"
icon={profileShadow.viewer?.following ? Check : Plus}
/>
<ButtonText>
{profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)}
</ButtonText>
</Button>
)}
{!isMe &&
(isBlockedUser ? (
<Link
to={profileURL}
label={_(msg`View blocked user's profile`)}
onPress={hide}
size="small"
color="secondary"
variant="solid"
style={[a.rounded_full]}>
<ButtonText>{_(msg`View profile`)}</ButtonText>
</Link>
) : (
<Button
size="small"
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
variant="solid"
label={
profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)
}
style={[a.rounded_full]}
onPress={profileShadow.viewer?.following ? unfollow : follow}>
<ButtonIcon
position="left"
icon={profileShadow.viewer?.following ? Check : Plus}
/>
<ButtonText>
{profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)}
</ButtonText>
</Button>
))}
</View>
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
@ -439,7 +460,19 @@ function Inner({
</View>
</Link>
{!blockHide && (
{isBlockedUser && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{moderation.ui('profileView').alerts.map(cause => (
<ProfileLabel
key={getModerationCauseKey(cause)}
cause={cause}
disableDetailsDialog
/>
))}
</View>
)}
{!isBlockedUser && (
<>
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}>
<InlineLinkText
@ -473,6 +506,17 @@ function Inner({
/>
</View>
) : undefined}
{!isMe &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
<KnownFollowers
profile={profile}
moderationOpts={moderationOpts}
onLinkPress={hide}
/>
</View>
)}
</>
)}
</View>

View File

@ -43,7 +43,13 @@ export function ProfileHeaderAlerts({
)
}
function ProfileLabel({cause}: {cause: ModerationCause}) {
export function ProfileLabel({
cause,
disableDetailsDialog,
}: {
cause: ModerationCause
disableDetailsDialog?: boolean
}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
@ -51,6 +57,7 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
return (
<>
<Button
disabled={disableDetailsDialog}
label={desc.name}
onPress={() => {
control.open()
@ -87,7 +94,9 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
)}
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
{!disableDetailsDialog && (
<ModerationDetailsDialog control={control} modcause={cause} />
)}
</>
)
}

View File

@ -94,6 +94,7 @@ export function usePrefetchProfileQuery() {
const prefetchProfileQuery = useCallback(
async (did: string) => {
await queryClient.prefetchQuery({
staleTime: STALE.SECONDS.THIRTY,
queryKey: RQKEY(did),
queryFn: async () => {
const res = await agent.getProfile({actor: did || ''})

View File

@ -150,16 +150,27 @@ export const TextInput = React.forwardRef(function TextInputImpl(
attributes: {
class: modeClass,
},
handlePaste: (_, event) => {
const items = event.clipboardData?.items
handlePaste: (view, event) => {
const clipboardData = event.clipboardData
if (items === undefined) {
return
if (clipboardData) {
if (clipboardData.types.includes('text/html')) {
// Rich-text formatting is pasted, try retrieving plain text
const text = clipboardData.getData('text/plain')
// `pasteText` will invoke this handler again, but `clipboardData` will be null.
view.pasteText(text)
// Return `true` to prevent ProseMirror's default paste behavior.
return true
} else {
// Otherwise, try retrieving images from the clipboard
getImageFromUri(clipboardData.items, (uri: string) => {
textInputWebEmitter.emit('photo-pasted', uri)
})
}
}
getImageFromUri(items, (uri: string) => {
textInputWebEmitter.emit('photo-pasted', uri)
})
},
handleKeyDown: (_, event) => {
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {

View File

@ -11,6 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {niceDate} from 'lib/strings/time'
import {TypographyVariant} from 'lib/ThemeContext'
import {isAndroid, isWeb} from 'platform/detection'
import {atoms as a} from '#/alf'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {TextLinkOnWebOnly} from './Link'
import {Text} from './text/Text'
@ -39,9 +40,13 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const prefetchProfileQuery = usePrefetchProfileQuery()
const profileLink = makeProfileLink(opts.author)
const onPointerEnter = isWeb
? () => prefetchProfileQuery(opts.author.did)
: undefined
const prefetchedProfile = React.useRef(false)
const onPointerMove = React.useCallback(() => {
if (!prefetchedProfile.current) {
prefetchedProfile.current = true
prefetchProfileQuery(opts.author.did)
}
}, [opts.author.did, prefetchProfileQuery])
const queryClient = useQueryClient()
const onOpenAuthor = opts.onOpenAuthor
@ -66,37 +71,39 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
</View>
)}
<ProfileHoverCard inline did={opts.author.did}>
<Text
numberOfLines={1}
style={[styles.maxWidth, pal.textLight, opts.displayNameStyle]}>
<TextLinkOnWebOnly
type={opts.displayNameType || 'lg-bold'}
style={[pal.text]}
lineHeight={1.2}
disableMismatchWarning
text={
<>
{sanitizeDisplayName(
displayName,
opts.moderation?.ui('displayName'),
)}
</>
}
href={profileLink}
onBeforePress={onBeforePressAuthor}
onPointerEnter={onPointerEnter}
/>
<TextLinkOnWebOnly
type="md"
disableMismatchWarning
style={[pal.textLight, {flexShrink: 4}]}
text={'\xa0' + sanitizeHandle(handle, '@')}
href={profileLink}
onBeforePress={onBeforePressAuthor}
onPointerEnter={onPointerEnter}
anchorNoUnderline
/>
</Text>
<View
onPointerMove={isWeb ? onPointerMove : undefined}
style={[a.flex_1]}>
<Text
numberOfLines={1}
style={[styles.maxWidth, pal.textLight, opts.displayNameStyle]}>
<TextLinkOnWebOnly
type={opts.displayNameType || 'lg-bold'}
style={[pal.text]}
lineHeight={1.2}
disableMismatchWarning
text={
<>
{sanitizeDisplayName(
displayName,
opts.moderation?.ui('displayName'),
)}
</>
}
href={profileLink}
onBeforePress={onBeforePressAuthor}
/>
<TextLinkOnWebOnly
type="md"
disableMismatchWarning
style={[pal.textLight, {flexShrink: 4}]}
text={'\xa0' + sanitizeHandle(handle, '@')}
href={profileLink}
onBeforePress={onBeforePressAuthor}
anchorNoUnderline
/>
</Text>
</View>
</ProfileHoverCard>
{!isAndroid && (
<Text