Screen for searching user's posts (#7622)
* search user's posts screen * custom placeholder copy if self * navigate to /profile/:handle * add name to title * show header on desktop
This commit is contained in:
@@ -283,6 +283,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
||||||
|
e.GET("/profile/:handleOrDID/search", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import {VideoFeed} from '#/screens/VideoFeed'
|
|||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
|
||||||
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
|
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
|
||||||
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
|
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
|
||||||
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
|
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
|
||||||
@@ -207,6 +208,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ProfileListScreen}
|
getComponent={() => ProfileListScreen}
|
||||||
options={{title: title(msg`List`), requireAuth: true}}
|
options={{title: title(msg`List`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProfileSearch"
|
||||||
|
getComponent={() => ProfileSearchScreen}
|
||||||
|
options={({route}) => ({
|
||||||
|
title: title(msg`Search @${route.params.name}'s posts`),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PostThread"
|
name="PostThread"
|
||||||
getComponent={() => PostThreadScreen}
|
getComponent={() => PostThreadScreen}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type CommonNavigatorParams = {
|
|||||||
ProfileFollowers: {name: string}
|
ProfileFollowers: {name: string}
|
||||||
ProfileFollows: {name: string}
|
ProfileFollows: {name: string}
|
||||||
ProfileKnownFollowers: {name: string}
|
ProfileKnownFollowers: {name: string}
|
||||||
|
ProfileSearch: {name: string; q?: string}
|
||||||
ProfileList: {name: string; rkey: string}
|
ProfileList: {name: string; rkey: string}
|
||||||
PostThread: {name: string; rkey: string}
|
PostThread: {name: string; rkey: string}
|
||||||
PostLikedBy: {name: string; rkey: string}
|
PostLikedBy: {name: string; rkey: string}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const router = new Router({
|
|||||||
ProfileFollowers: '/profile/:name/followers',
|
ProfileFollowers: '/profile/:name/followers',
|
||||||
ProfileFollows: '/profile/:name/follows',
|
ProfileFollows: '/profile/:name/follows',
|
||||||
ProfileKnownFollowers: '/profile/:name/known-followers',
|
ProfileKnownFollowers: '/profile/:name/known-followers',
|
||||||
|
ProfileSearch: '/profile/:name/search',
|
||||||
ProfileList: '/profile/:name/lists/:rkey',
|
ProfileList: '/profile/:name/lists/:rkey',
|
||||||
PostThread: '/profile/:name/post/:rkey',
|
PostThread: '/profile/:name/post/:rkey',
|
||||||
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
|
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import {useMemo} from 'react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {SearchScreenShell} from '#/view/screens/Search/Search'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileSearch'>
|
||||||
|
export const ProfileSearchScreen = ({route}: Props) => {
|
||||||
|
const {name, q: queryParam = ''} = route.params
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const {data: resolvedDid} = useResolveDidQuery(name)
|
||||||
|
const {data: profile} = useProfileQuery({did: resolvedDid})
|
||||||
|
|
||||||
|
const fixedParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
from: profile?.handle ?? name,
|
||||||
|
}),
|
||||||
|
[profile?.handle, name],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchScreenShell
|
||||||
|
navButton="back"
|
||||||
|
inputPlaceholder={
|
||||||
|
profile
|
||||||
|
? currentAccount?.did === profile.did
|
||||||
|
? _(msg`Search my posts`)
|
||||||
|
: _(msg`Search @${profile.handle}'s posts`)
|
||||||
|
: _(msg`Search...`)
|
||||||
|
}
|
||||||
|
fixedParams={fixedParams}
|
||||||
|
queryParam={queryParam}
|
||||||
|
testID="searchPostsScreen"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import React, {memo} from 'react'
|
|||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {shareText, shareUrl} from '#/lib/sharing'
|
import {shareText, shareUrl} from '#/lib/sharing'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -26,6 +28,7 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons
|
|||||||
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
||||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
||||||
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +51,7 @@ let ProfileMenu = ({
|
|||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const isSelf = currentAccount?.did === profile.did
|
const isSelf = currentAccount?.did === profile.did
|
||||||
const isFollowing = profile.viewer?.following
|
const isFollowing = profile.viewer?.following
|
||||||
const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
|
const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
|
||||||
@@ -177,6 +181,10 @@ let ProfileMenu = ({
|
|||||||
shareText(profile.did)
|
shareText(profile.did)
|
||||||
}, [profile.did])
|
}, [profile.did])
|
||||||
|
|
||||||
|
const onPressSearch = React.useCallback(() => {
|
||||||
|
navigation.navigate('ProfileSearch', {name: profile.handle})
|
||||||
|
}, [navigation, profile.handle])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventStopper onKeyDown={false}>
|
<EventStopper onKeyDown={false}>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
@@ -215,6 +223,15 @@ let ProfileMenu = ({
|
|||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Share} />
|
<Menu.ItemIcon icon={Share} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownSearchBtn"
|
||||||
|
label={_(msg`Search Posts`)}
|
||||||
|
onPress={onPressSearch}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Search Posts</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={SearchIcon} />
|
||||||
|
</Menu.Item>
|
||||||
</Menu.Group>
|
</Menu.Group>
|
||||||
|
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
|
||||||
|
|
||||||
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
||||||
import {createHitslop, HITSLOP_20} from '#/lib/constants'
|
import {createHitslop, HITSLOP_20} from '#/lib/constants'
|
||||||
@@ -55,7 +55,7 @@ import {List} from '#/view/com/util/List'
|
|||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {Explore} from '#/view/screens/Search/Explore'
|
import {Explore} from '#/view/screens/Search/Explore'
|
||||||
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
|
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
|
||||||
import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
|
import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
native,
|
native,
|
||||||
@@ -419,7 +419,13 @@ function SearchLanguageDropdown({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useQueryManager({initialQuery}: {initialQuery: string}) {
|
function useQueryManager({
|
||||||
|
initialQuery,
|
||||||
|
fixedParams,
|
||||||
|
}: {
|
||||||
|
initialQuery: string
|
||||||
|
fixedParams?: Params
|
||||||
|
}) {
|
||||||
const {query, params: initialParams} = React.useMemo(() => {
|
const {query, params: initialParams} = React.useMemo(() => {
|
||||||
return parseSearchQuery(initialQuery || '')
|
return parseSearchQuery(initialQuery || '')
|
||||||
}, [initialQuery])
|
}, [initialQuery])
|
||||||
@@ -438,8 +444,9 @@ function useQueryManager({initialQuery}: {initialQuery: string}) {
|
|||||||
...initialParams,
|
...initialParams,
|
||||||
// managed stuff
|
// managed stuff
|
||||||
lang,
|
lang,
|
||||||
|
...fixedParams,
|
||||||
}),
|
}),
|
||||||
[lang, initialParams],
|
[lang, initialParams, fixedParams],
|
||||||
)
|
)
|
||||||
const handlers = React.useMemo(
|
const handlers = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -588,16 +595,34 @@ SearchScreenInner = React.memo(SearchScreenInner)
|
|||||||
export function SearchScreen(
|
export function SearchScreen(
|
||||||
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
|
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
|
||||||
) {
|
) {
|
||||||
|
const queryParam = props.route?.params?.q ?? ''
|
||||||
|
|
||||||
|
return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchScreenShell({
|
||||||
|
queryParam,
|
||||||
|
testID,
|
||||||
|
fixedParams,
|
||||||
|
navButton = 'menu',
|
||||||
|
inputPlaceholder,
|
||||||
|
}: {
|
||||||
|
queryParam: string
|
||||||
|
testID: string
|
||||||
|
fixedParams?: Params
|
||||||
|
navButton?: 'back' | 'menu'
|
||||||
|
inputPlaceholder?: string
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const route = useRoute()
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
// Query terms
|
// Query terms
|
||||||
const queryParam = props.route?.params?.q ?? ''
|
|
||||||
const [searchText, setSearchText] = React.useState<string>(queryParam)
|
const [searchText, setSearchText] = React.useState<string>(queryParam)
|
||||||
const {data: autocompleteData, isFetching: isAutocompleteFetching} =
|
const {data: autocompleteData, isFetching: isAutocompleteFetching} =
|
||||||
useActorAutocompleteQuery(searchText, true)
|
useActorAutocompleteQuery(searchText, true)
|
||||||
@@ -656,6 +681,7 @@ export function SearchScreen(
|
|||||||
|
|
||||||
const {params, query, queryWithParams} = useQueryManager({
|
const {params, query, queryWithParams} = useQueryManager({
|
||||||
initialQuery: queryParam,
|
initialQuery: queryParam,
|
||||||
|
fixedParams,
|
||||||
})
|
})
|
||||||
const showFilters = Boolean(queryWithParams && !showAutocomplete)
|
const showFilters = Boolean(queryWithParams && !showAutocomplete)
|
||||||
|
|
||||||
@@ -696,13 +722,14 @@ export function SearchScreen(
|
|||||||
updateSearchHistory(item)
|
updateSearchHistory(item)
|
||||||
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
navigation.push('Search', {q: item})
|
// @ts-expect-error route is not typesafe
|
||||||
|
navigation.push(route.name, {...route.params, q: item})
|
||||||
} else {
|
} else {
|
||||||
textInput.current?.blur()
|
textInput.current?.blur()
|
||||||
navigation.setParams({q: item})
|
navigation.setParams({q: item})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateSearchHistory, navigation],
|
[updateSearchHistory, navigation, route],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressCancelSearch = React.useCallback(() => {
|
const onPressCancelSearch = React.useCallback(() => {
|
||||||
@@ -751,13 +778,18 @@ export function SearchScreen(
|
|||||||
const onSoftReset = React.useCallback(() => {
|
const onSoftReset = React.useCallback(() => {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
// Empty params resets the URL to be /search rather than /search?q=
|
// Empty params resets the URL to be /search rather than /search?q=
|
||||||
navigation.replace('Search', {})
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {q: _q, ...parameters} = (route.params ?? {}) as {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
// @ts-expect-error route is not typesafe
|
||||||
|
navigation.replace(route.name, parameters)
|
||||||
} else {
|
} else {
|
||||||
setSearchText('')
|
setSearchText('')
|
||||||
navigation.setParams({q: ''})
|
navigation.setParams({q: ''})
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation, route])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
@@ -778,8 +810,10 @@ export function SearchScreen(
|
|||||||
}
|
}
|
||||||
}, [setShowAutocomplete])
|
}, [setShowAutocomplete])
|
||||||
|
|
||||||
|
const showHeader = !gtMobile || navButton !== 'menu'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="searchScreen">
|
<Layout.Screen testID={testID}>
|
||||||
<View
|
<View
|
||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
onLayout={evt => {
|
onLayout={evt => {
|
||||||
@@ -794,14 +828,18 @@ export function SearchScreen(
|
|||||||
}),
|
}),
|
||||||
]}>
|
]}>
|
||||||
<Layout.Center style={t.atoms.bg}>
|
<Layout.Center style={t.atoms.bg}>
|
||||||
{!gtMobile && (
|
{showHeader && (
|
||||||
<View
|
<View
|
||||||
// HACK: shift up search input. we can't remove the top padding
|
// HACK: shift up search input. we can't remove the top padding
|
||||||
// on the search input because it messes up the layout animation
|
// on the search input because it messes up the layout animation
|
||||||
// if we add it only when the header is hidden
|
// if we add it only when the header is hidden
|
||||||
style={{marginBottom: tokens.space.xs * -1}}>
|
style={{marginBottom: tokens.space.xs * -1}}>
|
||||||
<Layout.Header.Outer noBottomBorder>
|
<Layout.Header.Outer noBottomBorder>
|
||||||
<Layout.Header.MenuButton />
|
{navButton === 'menu' ? (
|
||||||
|
<Layout.Header.MenuButton />
|
||||||
|
) : (
|
||||||
|
<Layout.Header.BackButton />
|
||||||
|
)}
|
||||||
<Layout.Header.Content align="left">
|
<Layout.Header.Content align="left">
|
||||||
<Layout.Header.TitleText>
|
<Layout.Header.TitleText>
|
||||||
<Trans>Search</Trans>
|
<Trans>Search</Trans>
|
||||||
@@ -829,7 +867,10 @@ export function SearchScreen(
|
|||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onClearText={onPressClearQuery}
|
onClearText={onPressClearQuery}
|
||||||
onSubmitEditing={onSubmit}
|
onSubmitEditing={onSubmit}
|
||||||
placeholder={_(msg`Search for posts, users, or feeds`)}
|
placeholder={
|
||||||
|
inputPlaceholder ??
|
||||||
|
_(msg`Search for posts, users, or feeds`)
|
||||||
|
}
|
||||||
hitSlop={{...HITSLOP_20, top: 0}}
|
hitSlop={{...HITSLOP_20, top: 0}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -849,7 +890,7 @@ export function SearchScreen(
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showFilters && gtMobile && (
|
{showFilters && !showHeader && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
@@ -870,7 +911,7 @@ export function SearchScreen(
|
|||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
display: showAutocomplete ? 'flex' : 'none',
|
display: showAutocomplete && !fixedParams ? 'flex' : 'none',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}>
|
}}>
|
||||||
{searchText.length > 0 ? (
|
{searchText.length > 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user