diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 8148a1f3b..050f0e6bf 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -9,7 +9,7 @@ import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {
type FollowedByMetricsDisplay,
- shouldShowFollowedByOverflowCount,
+ shouldShowFollowedByExactText,
shouldShowFollowedByOverflowPlus,
shouldShowFollowedByText,
} from '#/lib/metrics-display'
@@ -52,7 +52,7 @@ export function KnownFollowers({
onLinkPress,
minimal,
showIfEmpty,
- followedByDisplay = 'names',
+ followedByDisplay = 'visible',
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
@@ -123,6 +123,7 @@ function KnownFollowersInner({
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
const showText = shouldShowFollowedByText(followedByDisplay)
+ const showExactText = shouldShowFollowedByExactText(followedByDisplay)
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
const moderation = moderateProfile(f, moderationOpts)
@@ -150,18 +151,11 @@ function KnownFollowersInner({
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
const dim = SIZE + AVI_BORDER * 2
const radius = avatarBorderRadius(SIZE, enableSquareAvatars ?? true)
- const overflowCount = serverCount - slice.length
- const showOverflowCount = shouldShowFollowedByOverflowCount(
- followedByDisplay,
- serverCount,
- slice.length,
- )
const showOverflowPlus = shouldShowFollowedByOverflowPlus(
followedByDisplay,
serverCount,
slice.length,
)
- const showEndCap = showOverflowCount || showOverflowPlus
return (
0 ? -8 : 0,
overflow: 'hidden',
},
@@ -212,7 +206,7 @@ function KnownFollowersInner({
/>
))}
- {showEndCap ? (
+ {showOverflowPlus ? (
- {showOverflowCount && overflowCount > 0 ? (
-
-
- +{overflowCount}
-
-
- ) : (
-
- )}
+
) : null}
@@ -265,60 +245,18 @@ function KnownFollowersInner({
},
]}
numberOfLines={2}>
- {slice.length >= 2 ? (
- // 2-n followers, including blocks
- // only 2
- serverCount > 2 ? (
-
- Followed by{' '}
-
- {slice[0].profile.displayName}
-
- ,{' '}
-
- {slice[1].profile.displayName}
-
- , and{' '}
-
-
- ) : (
-
- Followed by{' '}
-
- {slice[0].profile.displayName}
- {' '}
- and{' '}
-
- {slice[1].profile.displayName}
-
-
- )
- ) : serverCount > 1 ? (
- // 1-n followers, including blocks
-
- Followed by{' '}
-
- {slice[0].profile.displayName}
- {' '}
- and{' '}
-
-
+ {showExactText ? (
+
) : (
- // only 1
-
- Followed by{' '}
-
- {slice[0].profile.displayName}
-
-
+
)}
) : null}
@@ -328,6 +266,130 @@ function KnownFollowersInner({
)
}
+function FollowedByNamesText({
+ serverCount,
+ slice,
+ textStyle,
+}: {
+ serverCount: number
+ slice: Array<{profile: {did: string; displayName: string}}>
+ textStyle: object[]
+}) {
+ if (slice.length >= 2) {
+ // 2-n followers, including blocks
+ // only 2
+ if (serverCount > 2) {
+ return (
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+
+ ,{' '}
+
+ {slice[1].profile.displayName}
+
+ , and{' '}
+
+
+ )
+ }
+ return (
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+ {' '}
+ and{' '}
+
+ {slice[1].profile.displayName}
+
+
+ )
+ }
+
+ if (serverCount > 1) {
+ // 1-n followers, including blocks
+ return (
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+ {' '}
+ and
+
+ )
+ }
+
+ // only 1
+ return (
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+
+
+ )
+}
+
+function FollowedByExactText({
+ serverCount,
+ slice,
+ textStyle,
+}: {
+ serverCount: number
+ slice: Array<{profile: {did: string; displayName: string}}>
+ textStyle: object[]
+}) {
+ if (slice.length >= 3) {
+ return (
+
+ Followed by{' '}
+ ,
+ including{' '}
+
+ {slice[0].profile.displayName}
+
+ ,{' '}
+
+ {slice[1].profile.displayName}
+
+ , and{' '}
+
+ {slice[2].profile.displayName}
+
+
+ )
+ }
+
+ if (slice.length === 2) {
+ return (
+
+ Followed by{' '}
+ ,
+ including{' '}
+
+ {slice[0].profile.displayName}
+ {' '}
+ and{' '}
+
+ {slice[1].profile.displayName}
+
+
+ )
+ }
+
+ return (
+
+ Followed by{' '}
+ , including{' '}
+
+ {slice[0].profile.displayName}
+
+
+ )
+}
+
function EmptyFallback({show}: {show?: boolean}) {
const t = useTheme()
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index e49e7da17..378f6e877 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -35,6 +35,7 @@ import {
useFollowersMetricsDisplay,
useFollowingMetricsDisplay,
} from '#/state/preferences/metrics-display-preference'
+import {useShowFollowedByOnOwnProfile} from '#/state/preferences/show-followed-by-on-own-profile'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
@@ -515,6 +516,7 @@ function Inner({
const followersMetricsDisplay = useFollowersMetricsDisplay()
const followingMetricsDisplay = useFollowingMetricsDisplay()
const followedByMetricsDisplay = useFollowedByMetricsDisplay()
+ const showFollowedByOnOwnProfile = useShowFollowedByOnOwnProfile()
const followersCount = profile.followersCount || 0
const followingCount = profile.followsCount || 0
const pluralizedFollowers = plural(profile.followersCount || 0, {
@@ -752,7 +754,7 @@ function Inner({
) : undefined}
- {!isMe &&
+ {(!isMe || showFollowedByOnOwnProfile) &&
!isFollowedByMetricHidden(followedByMetricsDisplay) &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
diff --git a/src/lib/metrics-display.ts b/src/lib/metrics-display.ts
index ca9789c20..0e62eab9c 100644
--- a/src/lib/metrics-display.ts
+++ b/src/lib/metrics-display.ts
@@ -18,13 +18,15 @@ const followedByMetricsDisplayValues = [
'hidden',
'lite',
'visible',
- 'names',
+ 'exact',
] as const
-export const followedByMetricsDisplaySchema = z.preprocess(
- val => (val === 'accessible' ? 'lite' : val),
- z.enum(followedByMetricsDisplayValues),
-)
+export const followedByMetricsDisplaySchema = z.preprocess(val => {
+ if (val === 'accessible') return 'lite'
+ // Legacy "names" is the current "visible" (named followers) behavior.
+ if (val === 'names') return 'visible'
+ return val
+}, z.enum(followedByMetricsDisplayValues))
export type FollowedByMetricsDisplay = z.infer<
typeof followedByMetricsDisplaySchema
>
@@ -55,7 +57,7 @@ export function migrateFollowedByMetricsDisplay(
if (legacyDisabled === true) {
return 'hidden'
}
- return 'names'
+ return 'visible'
}
export function isCountsMetricHidden(mode: CountsMetricsDisplay): boolean {
@@ -136,15 +138,13 @@ export function isFollowedByMetricHidden(
export function shouldShowFollowedByText(
mode: FollowedByMetricsDisplay,
): boolean {
- return mode === 'names'
+ return mode === 'visible' || mode === 'exact'
}
-export function shouldShowFollowedByOverflowCount(
+export function shouldShowFollowedByExactText(
mode: FollowedByMetricsDisplay,
- serverCount: number,
- shownCount: number,
): boolean {
- return mode === 'visible' && serverCount > shownCount
+ return mode === 'exact'
}
export function shouldShowFollowedByOverflowPlus(
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index cdfd2dc75..2d8b9d24b 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -25,6 +25,7 @@ import {useShowGermDmButton} from '#/state/preferences'
import {useConfirmFollowUnfollow} from '#/state/preferences/confirm-follow-unfollow'
import {useHideScaryFollowButtons} from '#/state/preferences/hide-scary-follow-buttons'
import {useFollowedByMetricsDisplay} from '#/state/preferences/metrics-display-preference'
+import {useShowFollowedByOnOwnProfile} from '#/state/preferences/show-followed-by-on-own-profile'
import {
useProfileBlockMutationQueue,
useProfileFollowMutationQueue,
@@ -156,6 +157,7 @@ let ProfileHeaderStandard = ({
// disable metrics
const followedByMetricsDisplay = useFollowedByMetricsDisplay()
+ const showFollowedByOnOwnProfile = useShowFollowedByOnOwnProfile()
return (
<>
@@ -221,7 +223,7 @@ let ProfileHeaderStandard = ({
)}
- {!isMe &&
+ {(!isMe || showFollowedByOnOwnProfile) &&
!isFollowedByMetricHidden(followedByMetricsDisplay) &&
!isBlockedUser &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
diff --git a/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx b/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx
index e467c32a5..91cb0f17a 100644
--- a/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx
+++ b/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx
@@ -1,11 +1,8 @@
-import {type ComponentProps, type ReactNode} from 'react'
+import {type ComponentProps} from 'react'
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
-import {
- type CountsMetricsDisplay,
- type FollowedByMetricsDisplay,
-} from '#/lib/metrics-display'
+import {type CountsMetricsDisplay} from '#/lib/metrics-display'
import {
useFollowedByMetricsDisplay,
useFollowersMetricsDisplay,
@@ -26,12 +23,16 @@ import {
useSetRepostsMetricsDisplay,
useSetSavesMetricsDisplay,
} from '#/state/preferences/metrics-display-preference'
+import {
+ useSetShowFollowedByOnOwnProfile,
+ useShowFollowedByOnOwnProfile,
+} from '#/state/preferences/show-followed-by-on-own-profile'
import {
useSetShowFollowsYouBadge,
useShowFollowsYouBadge,
} from '#/state/preferences/show-follows-you-badge'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Person_Stroke2_Corner0_Rounded as PersonIcon} from '#/components/icons/Person'
@@ -39,6 +40,8 @@ import {Reply as ReplyIcon} from '#/components/icons/Reply'
import {Text} from '#/components/Typography'
import {RunesScreenLayout} from './components/RunesScreenLayout'
+type MetricsDisplayMode = CountsMetricsDisplay
+
export function RunesImpressionsSettingsScreen() {
const {t: l} = useLingui()
@@ -62,75 +65,94 @@ export function RunesImpressionsSettingsScreen() {
const setFollowedByMetricsDisplay = useSetFollowedByMetricsDisplay()
const showFollowsYouBadge = useShowFollowsYouBadge()
const setShowFollowsYouBadge = useSetShowFollowsYouBadge()
+ const showFollowedByOnOwnProfile = useShowFollowedByOnOwnProfile()
+ const setShowFollowedByOnOwnProfile = useSetShowFollowedByOnOwnProfile()
- const countsLabels = useCountsDisplayLabels()
- const followedByLabels = useFollowedByDisplayLabels()
+ const labels = useMetricDisplayLabels()
return (
-
-
-
-
-
-
-
-
-
-
+
+
+ Show “Followed by” on own profile
+
+
+
+ style={[a.w_full, a.px_lg, a.py_md]}>
+
+
+ Enable extra "Follows you" label
+
+
)
}
@@ -150,71 +172,22 @@ function ImpressionsSectionHeader({
)
}
-function FollowsYouLabelToggle({
- enabled,
- onChange,
-}: {
- enabled: boolean
- onChange: (value: boolean) => void
-}) {
- const {t: l} = useLingui()
- const t = useTheme()
-
- return (
-
-
-
-
- Enable extra "Follows you" label
-
-
-
-
- {enabled ? Enabled : Disabled}
-
-
-
-
-
-
-
- )
-}
-
-function CountsMetricRow({
+function MetricRow({
name,
value,
labels,
onChange,
}: {
name: string
- value: CountsMetricsDisplay
- labels: Record
- onChange: (value: CountsMetricsDisplay) => void
+ value: MetricsDisplayMode
+ labels: Record
+ onChange: (value: MetricsDisplayMode) => void
}) {
const {t: l} = useLingui()
const {gtPhone} = useBreakpoints()
const handleChange = (values: string[]) => {
- const next = values[0] as CountsMetricsDisplay | undefined
+ const next = values[0] as MetricsDisplayMode | undefined
if (
next === 'hidden' ||
next === 'lite' ||
@@ -226,7 +199,16 @@ function CountsMetricRow({
}
return (
-
+
{name}
@@ -251,84 +233,11 @@ function CountsMetricRow({
-
- )
-}
-
-function FollowedByMetricRow({
- name,
- value,
- labels,
- onChange,
-}: {
- name: string
- value: FollowedByMetricsDisplay
- labels: Record
- onChange: (value: FollowedByMetricsDisplay) => void
-}) {
- const {t: l} = useLingui()
- const {gtPhone} = useBreakpoints()
-
- const handleChange = (values: string[]) => {
- const next = values[0] as FollowedByMetricsDisplay | undefined
- if (
- next === 'hidden' ||
- next === 'lite' ||
- next === 'visible' ||
- next === 'names'
- ) {
- onChange(next)
- }
- }
-
- return (
-
-
-
- {name}
-
-
-
-
-
- {labels.hidden}
-
-
- {labels.lite}
-
-
- {labels.visible}
-
-
- {labels.names}
-
-
-
-
- )
-}
-
-function MetricRowLayout({children}: {children: ReactNode}) {
- return (
-
- {children}
)
}
-function useCountsDisplayLabels(): Record {
+function useMetricDisplayLabels(): Record {
const {t: l} = useLingui()
return {
hidden: l`Hidden`,
@@ -337,16 +246,3 @@ function useCountsDisplayLabels(): Record {
exact: l`Exact`,
}
}
-
-function useFollowedByDisplayLabels(): Record<
- FollowedByMetricsDisplay,
- string
-> {
- const {t: l} = useLingui()
- return {
- hidden: l`Hidden`,
- lite: l`Lite`,
- visible: l`Visible`,
- names: l`Names`,
- }
-}
diff --git a/src/state/persisted/__tests__/schema.test.ts b/src/state/persisted/__tests__/schema.test.ts
index be8aa8ebf..68d8b0e9b 100644
--- a/src/state/persisted/__tests__/schema.test.ts
+++ b/src/state/persisted/__tests__/schema.test.ts
@@ -88,4 +88,16 @@ describe('persisted schema helpers', () => {
expect(parsed!.likesMetricsDisplay).toBe('lite')
expect(parsed!.followedByMetricsDisplay).toBe('lite')
})
+
+ it('migrates legacy followed-by names display mode to visible', () => {
+ const parsed = tryParse(
+ JSON.stringify({
+ ...partialState,
+ followedByMetricsDisplay: 'names',
+ }),
+ )
+
+ expect(parsed).toBeDefined()
+ expect(parsed!.followedByMetricsDisplay).toBe('visible')
+ })
})
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 56bc2b3ee..d0d9c963d 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -216,6 +216,7 @@ const schema = z.object({
followedByMetricsDisplay: followedByMetricsDisplaySchema.optional(),
postsMetricsDisplay: countsMetricsDisplaySchema.optional(),
showFollowsYouBadge: z.boolean().optional(),
+ showFollowedByOnOwnProfile: z.boolean().optional(),
hideSimilarAccountsRecomm: z.boolean().optional(),
hideScaryFollowButtons: z.boolean().optional(),
showGermDmButton: z.boolean().optional(),
@@ -370,9 +371,10 @@ export const defaults: Schema = {
replyMetricsDisplay: 'visible',
followersMetricsDisplay: 'visible',
followingMetricsDisplay: 'visible',
- followedByMetricsDisplay: 'names',
+ followedByMetricsDisplay: 'visible',
postsMetricsDisplay: 'visible',
showFollowsYouBadge: false,
+ showFollowedByOnOwnProfile: true,
hideSimilarAccountsRecomm: true,
hideScaryFollowButtons: false,
showGermDmButton: false,
diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx
index e32e2dd10..c6b42fb4b 100644
--- a/src/state/preferences/index.tsx
+++ b/src/state/preferences/index.tsx
@@ -50,6 +50,7 @@ import {Provider as PostNameReplacementProvider} from './post-name-replacement.t
import {Provider as RepostCarouselProvider} from './repost-carousel-enabled'
import {Provider as SettingsSyncProvider} from './settings-sync'
import {Provider as ShowAvatarFollowButtonProvider} from './show-avatar-follow-button'
+import {Provider as ShowFollowedByOnOwnProfileProvider} from './show-followed-by-on-own-profile'
import {Provider as ShowFollowsYouBadgeProvider} from './show-follows-you-badge'
import {Provider as ShowGermDmButtonProvider} from './show-germ-dm-button'
import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle'
@@ -186,6 +187,7 @@ export function Provider({children}: PropsWithChildren<{}>) {
+
@@ -233,6 +235,7 @@ export function Provider({children}: PropsWithChildren<{}>) {
+
diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx
index 20c9ce0ff..27890116d 100644
--- a/src/state/preferences/settings-sync.tsx
+++ b/src/state/preferences/settings-sync.tsx
@@ -63,6 +63,7 @@ export const SYNCED_PREFS_KEYS = [
'followedByMetricsDisplay',
'postsMetricsDisplay',
'showFollowsYouBadge',
+ 'showFollowedByOnOwnProfile',
'hideSimilarAccountsRecomm',
'hideScaryFollowButtons',
'showGermDmButton',
diff --git a/src/state/preferences/show-followed-by-on-own-profile.tsx b/src/state/preferences/show-followed-by-on-own-profile.tsx
new file mode 100644
index 000000000..854217d95
--- /dev/null
+++ b/src/state/preferences/show-followed-by-on-own-profile.tsx
@@ -0,0 +1,55 @@
+import {
+ createContext,
+ type PropsWithChildren,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from 'react'
+
+import * as persisted from '#/state/persisted'
+
+type StateContext = boolean
+type SetContext = (v: boolean) => void
+
+const stateContext = createContext(
+ persisted.defaults.showFollowedByOnOwnProfile ?? true,
+)
+const setContext = createContext((_: boolean) => {})
+
+export function Provider({children}: PropsWithChildren<{}>) {
+ const [state, setState] = useState(
+ persisted.get('showFollowedByOnOwnProfile') ??
+ persisted.defaults.showFollowedByOnOwnProfile ??
+ true,
+ )
+
+ const setStateWrapped = useCallback((showFollowedByOnOwnProfile: boolean) => {
+ setState(showFollowedByOnOwnProfile)
+ persisted.write('showFollowedByOnOwnProfile', showFollowedByOnOwnProfile)
+ }, [])
+
+ useEffect(() => {
+ return persisted.onUpdate('showFollowedByOnOwnProfile', next => {
+ setState(
+ next ?? persisted.defaults.showFollowedByOnOwnProfile ?? true,
+ )
+ })
+ }, [])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function useShowFollowedByOnOwnProfile() {
+ return useContext(stateContext)
+}
+
+export function useSetShowFollowedByOnOwnProfile() {
+ return useContext(setContext)
+}