diff options
-rw-r--r-- | src/components/KnownFollowers.tsx | 68 | ||||
-rw-r--r-- | src/components/ProfileHoverCard/index.web.tsx | 94 | ||||
-rw-r--r-- | src/components/moderation/ProfileHeaderAlerts.tsx | 13 | ||||
-rw-r--r-- | src/state/queries/profile.ts | 1 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 27 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 75 |
6 files changed, 177 insertions, 101 deletions
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index b99fe3398..a8bdb763d 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -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', - })} - </> + <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 ? ( - 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>{' '} + 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> </> diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 024867b1a..e17977af4 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -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> diff --git a/src/components/moderation/ProfileHeaderAlerts.tsx b/src/components/moderation/ProfileHeaderAlerts.tsx index 287a0bdde..4b48b142d 100644 --- a/src/components/moderation/ProfileHeaderAlerts.tsx +++ b/src/components/moderation/ProfileHeaderAlerts.tsx @@ -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} /> + )} </> ) } diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 7cc9f6911..6f7f2de79 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -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 || ''}) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 7f8dc2ed5..a91524974 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -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(items, (uri: string) => { - textInputWebEmitter.emit('photo-pasted', uri) - }) + getImageFromUri(clipboardData.items, (uri: string) => { + textInputWebEmitter.emit('photo-pasted', uri) + }) + } + } }, handleKeyDown: (_, event) => { if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index b6fe6d374..df45174b9 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -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 |