Better interest tabs (#8879)

* make tabs draggable

* move tab component to own file

* rm focused state from tab, improve label

* add focus styles, remove focus when dragging

* rm log

* add arrows to tabs

* rename Tabs -> InterestTabs

* try and simplify approach

* rename ref

* Update InterestTabs.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/components/InterestTabs.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/components/ProgressGuide/FollowDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/components/ProgressGuide/FollowDialog.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* add newline

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
Samuel Newman
2025-08-26 19:39:26 +03:00
committed by GitHub
parent 42b3012ed9
commit b77000dd18
7 changed files with 488 additions and 221 deletions
+12
View File
@@ -70,9 +70,21 @@ export const atoms = {
overflow_visible: {
overflow: 'visible',
},
overflow_x_visible: {
overflowX: 'visible',
},
overflow_y_visible: {
overflowY: 'visible',
},
overflow_hidden: {
overflow: 'hidden',
},
overflow_x_hidden: {
overflowX: 'hidden',
},
overflow_y_hidden: {
overflowY: 'hidden',
},
/**
* @platform web
*/
+391
View File
@@ -0,0 +1,391 @@
import {useEffect, useRef, useState} from 'react'
import {
type ScrollView,
type StyleProp,
View,
type ViewStyle,
} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {isWeb} from '#/platform/detection'
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {atoms as a, tokens, useTheme, web} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {Button, ButtonIcon} from '#/components/Button'
import {
ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft,
ArrowRight_Stroke2_Corner0_Rounded as ArrowRight,
} from '#/components/icons/Arrow'
import {Text} from '#/components/Typography'
/**
* Tab component that automatically scrolls the selected tab into view - used for interests
* in the Find Follows dialog, Explore screen, etc.
*/
export function InterestTabs({
onSelectTab,
interests,
selectedInterest,
disabled,
interestsDisplayNames,
TabComponent = Tab,
contentContainerStyle,
gutterWidth = tokens.space.lg,
}: {
onSelectTab: (tab: string) => void
interests: string[]
selectedInterest: string
interestsDisplayNames: Record<string, string>
/** still allows changing tab, but removes the active state from the selected tab */
disabled?: boolean
TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
contentContainerStyle?: StyleProp<ViewStyle>
gutterWidth?: number
}) {
const t = useTheme()
const {_} = useLingui()
const listRef = useRef<ScrollView>(null)
const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
const onInitialLayout = useNonReactiveCallback(() => {
const index = interests.indexOf(selectedInterest)
scrollIntoViewIfNeeded(index)
})
useEffect(() => {
if (tabOffsets) {
onInitialLayout()
}
}, [tabOffsets, onInitialLayout])
function scrollIntoViewIfNeeded(index: number) {
const btnLayout = tabOffsets[index]
if (!btnLayout) return
listRef.current?.scrollTo({
// centered
x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
animated: true,
})
}
function handleSelectTab(index: number) {
const tab = interests[index]
onSelectTab(tab)
scrollIntoViewIfNeeded(index)
}
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
setTabOffsets(pendingTabOffsets.current)
}
}
}
const canScrollLeft = scrollX > 0
const canScrollRight = scrollX < contentWidth - totalWidth
const cleanupRef = useRef<(() => void) | null>(null)
function scrollLeft() {
if (isContinuouslyScrollingRef.current) {
return
}
if (listRef.current && canScrollLeft) {
const newScrollX = Math.max(0, scrollX - 200)
listRef.current.scrollTo({x: newScrollX, animated: true})
}
}
function scrollRight() {
if (isContinuouslyScrollingRef.current) {
return
}
if (listRef.current && canScrollRight) {
const maxScroll = contentWidth - totalWidth
const newScrollX = Math.min(maxScroll, scrollX + 200)
listRef.current.scrollTo({x: newScrollX, animated: true})
}
}
const isContinuouslyScrollingRef = useRef(false)
function startContinuousScroll(direction: 'left' | 'right') {
// Clear any existing continuous scroll
if (cleanupRef.current) {
cleanupRef.current()
}
let holdTimeout: NodeJS.Timeout | null = null
let animationFrame: number | null = null
let isActive = true
isContinuouslyScrollingRef.current = false
const cleanup = () => {
isActive = false
if (holdTimeout) clearTimeout(holdTimeout)
if (animationFrame) cancelAnimationFrame(animationFrame)
cleanupRef.current = null
// Reset flag after a delay to prevent onPress from firing
setTimeout(() => {
isContinuouslyScrollingRef.current = false
}, 100)
}
cleanupRef.current = cleanup
// Start continuous scrolling after hold delay
holdTimeout = setTimeout(() => {
if (!isActive) return
isContinuouslyScrollingRef.current = true
let currentScrollPosition = scrollX
const scroll = () => {
if (!isActive || !listRef.current) return
const scrollAmount = 3
const maxScroll = contentWidth - totalWidth
let newScrollX: number
let canContinue = false
if (direction === 'left' && currentScrollPosition > 0) {
newScrollX = Math.max(0, currentScrollPosition - scrollAmount)
canContinue = newScrollX > 0
} else if (direction === 'right' && currentScrollPosition < maxScroll) {
newScrollX = Math.min(maxScroll, currentScrollPosition + scrollAmount)
canContinue = newScrollX < maxScroll
} else {
return
}
currentScrollPosition = newScrollX
listRef.current.scrollTo({x: newScrollX, animated: false})
if (canContinue && isActive) {
animationFrame = requestAnimationFrame(scroll)
}
}
scroll()
}, 500)
}
function stopContinuousScroll() {
if (cleanupRef.current) {
cleanupRef.current()
}
}
useEffect(() => {
return () => {
if (cleanupRef.current) {
cleanupRef.current()
}
}
}, [])
return (
<View style={[a.relative]}>
<DraggableScrollView
ref={listRef}
horizontal
contentContainerStyle={[
a.gap_sm,
{paddingHorizontal: gutterWidth},
contentContainerStyle,
]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
onContentSizeChange={width => setContentWidth(width)}
onScroll={evt => {
const newScrollX = evt.nativeEvent.contentOffset.x
setScrollX(newScrollX)
}}
scrollEventThrottle={16}>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !disabled
return (
<TabComponent
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</DraggableScrollView>
{isWeb && canScrollLeft && (
<View
style={[
a.absolute,
a.top_0,
a.left_0,
a.bottom_0,
a.justify_center,
{paddingLeft: gutterWidth},
a.pr_md,
a.z_10,
web({
background: `linear-gradient(to right, ${t.atoms.bg.backgroundColor} 0%, ${t.atoms.bg.backgroundColor} 70%, ${transparentifyColor(t.atoms.bg.backgroundColor, 0)} 100%)`,
}),
]}>
<Button
label={_(msg`Scroll left`)}
onPress={scrollLeft}
onPressIn={() => startContinuousScroll('left')}
onPressOut={stopContinuousScroll}
color="secondary"
size="small"
style={[
a.border,
t.atoms.border_contrast_low,
t.atoms.bg,
a.h_full,
{aspectRatio: 1},
a.rounded_full,
]}>
<ButtonIcon icon={ArrowLeft} />
</Button>
</View>
)}
{isWeb && canScrollRight && (
<View
style={[
a.absolute,
a.top_0,
a.right_0,
a.bottom_0,
a.justify_center,
{paddingRight: gutterWidth},
a.pl_md,
a.z_10,
web({
background: `linear-gradient(to left, ${t.atoms.bg.backgroundColor} 0%, ${t.atoms.bg.backgroundColor} 70%, ${transparentifyColor(t.atoms.bg.backgroundColor, 0)} 100%)`,
}),
]}>
<Button
label={_(msg`Scroll right`)}
onPress={scrollRight}
onPressIn={() => startContinuousScroll('right')}
onPressOut={stopContinuousScroll}
color="secondary"
size="small"
style={[
a.border,
t.atoms.border_contrast_low,
t.atoms.bg,
a.h_full,
{aspectRatio: 1},
a.rounded_full,
]}>
<ButtonIcon icon={ArrowRight} />
</Button>
</View>
)}
</View>
)
}
function Tab({
onSelectTab,
interest,
active,
index,
interestsDisplayName,
onLayout,
}: {
onSelectTab: (index: number) => void
interest: string
active: boolean
index: number
interestsDisplayName: string
onLayout: (index: number, x: number, width: number) => void
}) {
const t = useTheme()
const {_} = useLingui()
const label = active
? _(
msg({
message: `"${interestsDisplayName}" category (active)`,
comment:
'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is currently selected.',
}),
)
: _(
msg({
message: `Select "${interestsDisplayName}" category`,
comment:
'Accessibility label for a category (e.g. Art, Video Games, Sports, etc.) that shows suggested accounts for the user to follow. The tab is not currently active and can be selected.',
}),
)
return (
<View
key={interest}
onLayout={e =>
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
}>
<Button
label={label}
onPress={() => onSelectTab(index)}
// disable focus ring, we handle it
style={web({outline: 'none'})}>
{({hovered, pressed, focused}) => (
<View
style={[
a.rounded_full,
a.px_lg,
a.py_sm,
a.border,
active || hovered || pressed
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
: focused
? {
borderColor: t.palette.primary_300,
backgroundColor: t.palette.primary_25,
}
: [t.atoms.bg, t.atoms.border_contrast_low],
]}>
<Text
style={[
a.font_medium,
active || hovered || pressed
? t.atoms.text
: t.atoms.text_contrast_medium,
]}>
{interestsDisplayName}
</Text>
</View>
)}
</Button>
</View>
)
}
export function boostInterests(boosts?: string[]) {
return (_a: string, _b: string) => {
const indexA = boosts?.indexOf(_a) ?? -1
const indexB = boosts?.indexOf(_b) ?? -1
const rankA = indexA === -1 ? Infinity : indexA
const rankB = indexB === -1 ? Infinity : indexB
return rankA - rankB
}
}
+24 -122
View File
@@ -1,17 +1,9 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {
ScrollView,
type StyleProp,
TextInput,
useWindowDimensions,
View,
type ViewStyle,
} from 'react-native'
import {TextInput, useWindowDimensions, View} from 'react-native'
import {type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -28,7 +20,6 @@ import {
import {
atoms as a,
native,
tokens,
useBreakpoints,
useTheme,
type ViewStyleProp,
@@ -40,6 +31,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
@@ -337,12 +329,13 @@ let Header = ({
}}
onEscape={control.close}
/>
<Tabs
<InterestTabs
onSelectTab={onSelectTab}
interests={interests}
selectedInterest={selectedInterest}
hasSearchText={!!searchText}
disabled={!!searchText}
interestsDisplayNames={interestsDisplayNames}
TabComponent={Tab}
/>
</View>
</View>
@@ -403,99 +396,6 @@ function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
)
}
let Tabs = ({
onSelectTab,
interests,
selectedInterest,
hasSearchText,
interestsDisplayNames,
TabComponent = Tab,
contentContainerStyle,
}: {
onSelectTab: (tab: string) => void
interests: string[]
selectedInterest: string
hasSearchText: boolean
interestsDisplayNames: Record<string, string>
TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
contentContainerStyle?: StyleProp<ViewStyle>
}): React.ReactNode => {
const listRef = useRef<ScrollView>(null)
const [totalWidth, setTotalWidth] = useState(0)
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
const onInitialLayout = useNonReactiveCallback(() => {
const index = interests.indexOf(selectedInterest)
scrollIntoViewIfNeeded(index)
})
useEffect(() => {
if (tabOffsets) {
onInitialLayout()
}
}, [tabOffsets, onInitialLayout])
function scrollIntoViewIfNeeded(index: number) {
const btnLayout = tabOffsets[index]
if (!btnLayout) return
listRef.current?.scrollTo({
// centered
x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2),
animated: true,
})
}
function handleSelectTab(index: number) {
const tab = interests[index]
onSelectTab(tab)
scrollIntoViewIfNeeded(index)
}
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
setTabOffsets(pendingTabOffsets.current)
}
}
}
return (
<ScrollView
ref={listRef}
horizontal
contentContainerStyle={[a.gap_sm, a.px_lg, contentContainerStyle]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
scrollEventThrottle={200} // big throttle
>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !hasSearchText
return (
<TabComponent
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</ScrollView>
)
}
Tabs = memo(Tabs)
export {Tabs}
let Tab = ({
onSelectTab,
interest,
@@ -513,24 +413,36 @@ let Tab = ({
}): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const activeText = active ? _(msg` (active)`) : ''
const label = active
? _(
msg({
message: `Search for "${interestsDisplayName}" (active)`,
comment:
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected.',
}),
)
: _(
msg({
message: `Search for "${interestsDisplayName}`,
comment:
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected.',
}),
)
return (
<View
key={interest}
onLayout={e =>
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
}>
<Button
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
onPress={() => onSelectTab(index)}>
{({hovered, pressed, focused}) => (
<Button label={label} onPress={() => onSelectTab(index)}>
{({hovered, pressed}) => (
<View
style={[
a.rounded_full,
a.px_lg,
a.py_sm,
a.border,
active || hovered || pressed || focused
active || hovered || pressed
? [
t.atoms.bg_contrast_25,
{borderColor: t.atoms.bg_contrast_25.backgroundColor},
@@ -540,7 +452,7 @@ let Tab = ({
<Text
style={[
a.font_medium,
active || hovered || pressed || focused
active || hovered || pressed
? t.atoms.text
: t.atoms.text_contrast_medium,
]}>
@@ -759,13 +671,3 @@ function Empty({message}: {message: string}) {
</View>
)
}
export function boostInterests(boosts?: string[]) {
return (_a: string, _b: string) => {
const indexA = boosts?.indexOf(_a) ?? -1
const indexB = boosts?.indexOf(_b) ?? -1
const rankA = indexA === -1 ? Infinity : indexA
const rankB = indexB === -1 ? Infinity : indexB
return rankA - rankB
}
}
+3 -3
View File
@@ -20,9 +20,6 @@ export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
return
}
const slider = ref.current as unknown as HTMLDivElement
if (!slider) {
return
}
let isDragging = false
let isMouseDown = false
let startX = 0
@@ -61,6 +58,9 @@ export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
e.preventDefault()
const walk = x - startX
slider.scrollLeft = scrollLeft - walk
if (slider.contains(document.activeElement))
(document.activeElement as HTMLElement)?.blur?.()
}
slider.addEventListener('mousedown', mouseDown)
@@ -20,15 +20,14 @@ import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {SuggestedAccountsTabBar} from '#/screens/Search/modules/ExploreSuggestedAccounts'
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
import * as toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
@@ -146,7 +145,7 @@ export function StepSuggestedAccounts() {
a.flex_1,
a.justify_start,
]}>
<SuggestedAccountsTabBar
<TabBar
selectedInterest={selectedInterest}
onSelectInterest={setSelectedInterest}
defaultTabLabel={_(
@@ -155,8 +154,7 @@ export function StepSuggestedAccounts() {
comment: 'the default tab in the interests tab bar',
}),
)}
priorityInterests={state.interestsStepResults.selectedInterests}
leftPadding={isWeb ? 0 : tokens.space.xl}
selectedInterests={state.interestsStepResults.selectedInterests}
/>
{isLoading || !moderationOpts ? (
@@ -253,6 +251,52 @@ export function StepSuggestedAccounts() {
)
}
function TabBar({
selectedInterest,
onSelectInterest,
selectedInterests,
hideDefaultTab,
defaultTabLabel,
}: {
selectedInterest: string | null
onSelectInterest: (interest: string | null) => void
selectedInterests: string[]
hideDefaultTab?: boolean
defaultTabLabel?: string
}) {
const {_} = useLingui()
const interestsDisplayNames = useInterestsDisplayNames()
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
.sort(boostInterests(selectedInterests))
return (
<InterestTabs
interests={hideDefaultTab ? interests : ['all', ...interests]}
selectedInterest={
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
}
onSelectTab={tab => {
logger.metric(
'onboarding:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
onSelectInterest(tab === 'all' ? null : tab)
}}
interestsDisplayNames={
hideDefaultTab
? interestsDisplayNames
: {
all: defaultTabLabel || _(msg`For You`),
...interestsDisplayNames,
}
}
gutterWidth={isWeb ? 0 : tokens.space.xl}
/>
)
}
function SuggestedProfileCard({
profile,
moderationOpts,
+1 -2
View File
@@ -66,9 +66,9 @@ import {
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
import {StarterPack} from '#/components/icons/StarterPack'
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
import {boostInterests} from '#/components/InterestTabs'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import {boostInterests} from '#/components/ProgressGuide/FollowDialog'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import * as ModuleHeader from './components/ModuleHeader'
@@ -751,7 +751,6 @@ export function Explore({
selectedInterest={selectedInterest}
onSelectInterest={setSelectedInterest}
hideDefaultTab={item.hideDefaultTab}
logContext="Explore"
/>
</View>
)
@@ -12,13 +12,11 @@ import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {tokens, useTheme} from '#/alf'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button} from '#/components/Button'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import * as ProfileCard from '#/components/ProfileCard'
import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
export function useLoadEnoughProfiles({
@@ -55,23 +53,16 @@ export function useLoadEnoughProfiles({
}
}
// TODO: Move to own file -sfn
export function SuggestedAccountsTabBar({
selectedInterest,
onSelectInterest,
hideDefaultTab,
defaultTabLabel,
priorityInterests,
leftPadding = tokens.space.md,
logContext = 'Explore',
}: {
selectedInterest: string | null
onSelectInterest: (interest: string | null) => void
priorityInterests?: string[]
hideDefaultTab?: boolean
defaultTabLabel?: string
leftPadding?: number
logContext?: 'Explore' | 'Onboarding'
}) {
const {_} = useLingui()
const interestsDisplayNames = useInterestsDisplayNames()
@@ -80,32 +71,22 @@ export function SuggestedAccountsTabBar({
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
.sort(boostInterests(personalizedInterests))
.sort(boostInterests(priorityInterests))
return (
<BlockDrawerGesture>
<Tabs
<InterestTabs
interests={hideDefaultTab ? interests : ['all', ...interests]}
selectedInterest={
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
}
onSelectTab={tab => {
if (logContext === 'Explore') {
logger.metric(
'explore:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
} else {
logger.metric(
'onboarding:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
}
logger.metric(
'explore:suggestedAccounts:tabPressed',
{tab: tab},
{statsig: true},
)
onSelectInterest(tab === 'all' ? null : tab)
}}
hasSearchText={false}
interestsDisplayNames={
hideDefaultTab
? interestsDisplayNames
@@ -114,73 +95,11 @@ export function SuggestedAccountsTabBar({
...interestsDisplayNames,
}
}
TabComponent={Tab}
contentContainerStyle={[
{
// visual alignment
paddingLeft: leftPadding,
},
]}
/>
</BlockDrawerGesture>
)
}
let Tab = ({
onSelectTab,
interest,
active,
index,
interestsDisplayName,
onLayout,
}: {
onSelectTab: (index: number) => void
interest: string
active: boolean
index: number
interestsDisplayName: string
onLayout: (index: number, x: number, width: number) => void
}): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const activeText = active ? _(msg` (active)`) : ''
return (
<View
key={interest}
onLayout={e =>
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
}>
<Button
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
onPress={() => onSelectTab(index)}>
{({hovered, pressed, focused}) => (
<View
style={[
a.rounded_full,
a.px_lg,
a.py_sm,
a.border,
active || hovered || pressed || focused
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
: [t.atoms.bg, t.atoms.border_contrast_low],
]}>
<Text
style={[
a.font_medium,
active || hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_medium,
]}>
{interestsDisplayName}
</Text>
</View>
)}
</Button>
</View>
)
}
Tab = memo(Tab)
/**
* Profile card for suggested accounts. Note: border is on the bottom edge
*/