about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/KnownFollowers.tsx68
-rw-r--r--src/components/ProfileHoverCard/index.web.tsx94
-rw-r--r--src/components/moderation/ProfileHeaderAlerts.tsx13
-rw-r--r--src/state/queries/profile.ts1
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx27
-rw-r--r--src/view/com/util/PostMeta.tsx75
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