Files
bsky-social-app/src/screens/Search/components/SearchHistory.tsx
T
2026-06-26 16:16:48 -07:00

242 lines
7.0 KiB
TypeScript

import {Pressable, ScrollView, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createHitslop, HITSLOP_10} from '#/lib/constants'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {
countActiveFilters,
parseHistoryEntry,
} from '#/screens/Search/searchParams'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function SearchHistory({
searchHistory,
selectedProfiles,
onItemClick,
onProfileClick,
onRemoveItemClick,
onRemoveProfileClick,
}: {
searchHistory: string[]
selectedProfiles: bsky.profile.AnyProfileView[]
onItemClick: (item: string) => void
onProfileClick: (profile: bsky.profile.AnyProfileView) => void
onRemoveItemClick: (item: string) => void
onRemoveProfileClick: (profile: bsky.profile.AnyProfileView) => void
}) {
const ax = useAnalytics()
const moderationOpts = useModerationOpts()
return (
<Layout.Content
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled">
<View style={[a.w_full, a.gap_md]}>
{(searchHistory.length > 0 || selectedProfiles.length > 0) && (
<View style={[a.px_lg, a.pt_sm]}>
<Text style={[a.text_md, a.font_semi_bold]}>
<Trans>Recent searches</Trans>
</Text>
</View>
)}
{selectedProfiles.length > 0 && (
<View>
<BlockDrawerGesture>
<ScrollView
horizontal
keyboardShouldPersistTaps="handled"
showsHorizontalScrollIndicator={false}
contentContainerStyle={[
a.px_lg,
a.flex_row,
a.flex_nowrap,
a.gap_xl,
]}>
{moderationOpts &&
selectedProfiles.map((profile, index) => (
<RecentProfileItem
key={profile.did}
profile={profile}
moderationOpts={moderationOpts}
onPress={() => {
ax.metric('search:recent:press', {
profileDid: profile.did,
position: index,
})
onProfileClick(profile)
}}
onRemove={() => onRemoveProfileClick(profile)}
/>
))}
</ScrollView>
</BlockDrawerGesture>
</View>
)}
{searchHistory.length > 0 && (
<View style={[a.px_lg, a.pt_sm]}>
{searchHistory.slice(0, 5).map((historyItem, index) => {
const {q, filters} = parseHistoryEntry(historyItem)
const filterCount = countActiveFilters(filters)
return (
<SearchHistoryItem
key={index}
q={q}
filterCount={filterCount}
onPress={() => {
ax.metric('search:query', {
source: 'history',
filterCount,
})
onItemClick(historyItem)
}}
onRemove={() => onRemoveItemClick(historyItem)}
/>
)
})}
</View>
)}
</View>
</Layout.Content>
)
}
function SearchHistoryItem({
q,
filterCount,
onPress,
onRemove,
}: {
q: string
filterCount: number
onPress: () => void
onRemove: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
return (
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<Pressable
accessibilityRole="button"
onPress={onPress}
hitSlop={HITSLOP_10}
style={[a.flex_1, a.py_sm, a.flex_row, a.align_center, a.gap_sm]}>
<Text style={[a.text_md, a.flex_shrink]} numberOfLines={1}>
{q}
</Text>
{filterCount > 0 ? (
<View
style={[
a.flex_shrink_0,
a.rounded_sm,
a.px_sm,
a.py_2xs,
t.atoms.bg_contrast_25,
]}>
<Text
style={[a.text_xs, a.font_medium, t.atoms.text_contrast_medium]}>
<Plural value={filterCount} one="+# filter" other="+# filters" />
</Text>
</View>
) : null}
</Pressable>
<Button
label={l`Remove ${q}`}
onPress={onRemove}
size="small"
variant="ghost"
color="secondary"
shape="round">
<ButtonIcon icon={XIcon} />
</Button>
</View>
)
}
function RecentProfileItem({
profile,
moderationOpts,
onPress,
onRemove,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
onPress: () => void
onRemove: () => void
}) {
const {t: l} = useLingui()
const width = 80
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
return (
<View style={[a.relative]}>
<Link
to={makeProfileLink(profile)}
label={profile.handle}
onPress={onPress}
style={[
a.flex_col,
a.align_center,
a.gap_xs,
{
width,
},
]}>
<UserAvatar
avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'}
size={width - 8}
moderation={moderation.ui('avatar')}
/>
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
<Text emoji style={[a.text_xs, a.leading_snug]} numberOfLines={1}>
{name}
</Text>
<ProfileBadges profile={profile} size="xs" style={[a.pl_xs]} />
</View>
</Link>
<Button
label={l`Remove profile`}
hitSlop={createHitslop(6)}
size="tiny"
variant="outline"
color="secondary"
shape="round"
onPress={onRemove}
style={[
a.absolute,
{
top: 0,
right: 0,
height: 18,
width: 18,
},
]}>
<ButtonIcon icon={XIcon} />
</Button>
</View>
)
}