[🐴] Show if user can be messaged in new chat search (#4021)

* show if user can be messaged

* allow 2 lines in handle field due to new text

* cannot -> can't

* rework canBeMessaged logic and move to new file

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Samuel Newman 2024-05-15 16:05:17 +01:00 committed by GitHub
parent 2121b5f86f
commit ed8922281a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 17 deletions

View File

@ -136,6 +136,7 @@ let ListMaybePlaceholder = ({
onGoBack, onGoBack,
hideBackButton, hideBackButton,
sideBorders, sideBorders,
topBorder = true,
}: { }: {
isLoading: boolean isLoading: boolean
noEmpty?: boolean noEmpty?: boolean
@ -149,6 +150,7 @@ let ListMaybePlaceholder = ({
onGoBack?: () => void onGoBack?: () => void
hideBackButton?: boolean hideBackButton?: boolean
sideBorders?: boolean sideBorders?: boolean
topBorder?: boolean
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@ -165,7 +167,7 @@ let ListMaybePlaceholder = ({
{paddingTop: 175, paddingBottom: 110}, {paddingTop: 175, paddingBottom: 110},
]} ]}
sideBorders={sideBorders ?? gtMobile} sideBorders={sideBorders ?? gtMobile}
topBorder={!gtTablet}> topBorder={topBorder && !gtTablet}>
<View style={[a.w_full, a.align_center, {top: 100}]}> <View style={[a.w_full, a.align_center, {top: 100}]}>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>

View File

@ -197,6 +197,7 @@ function GifList({
onGoBack={onGoBack} onGoBack={onGoBack}
emptyType="results" emptyType="results"
sideBorders={false} sideBorders={false}
topBorder={false}
errorTitle={_(msg`Failed to load GIFs`)} errorTitle={_(msg`Failed to load GIFs`)}
errorMessage={_(msg`There was an issue connecting to Tenor.`)} errorMessage={_(msg`There was an issue connecting to Tenor.`)}
emptyMessage={ emptyMessage={

View File

@ -10,6 +10,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useSession} from '#/state/session'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@ -23,6 +24,7 @@ import {Button} from '../Button'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope' import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
import {ListMaybePlaceholder} from '../Lists' import {ListMaybePlaceholder} from '../Lists'
import {Text} from '../Typography' import {Text} from '../Typography'
import {canBeMessaged} from './util'
export function NewChat({ export function NewChat({
control, control,
@ -82,6 +84,7 @@ function SearchablePeopleList({
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null) const listRef = useRef<BottomSheetFlatListMethods>(null)
const {currentAccount} = useSession()
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
@ -95,12 +98,17 @@ function SearchablePeopleList({
const renderItem = useCallback( const renderItem = useCallback(
({item: profile}: {item: AppBskyActorDefs.ProfileView}) => { ({item: profile}: {item: AppBskyActorDefs.ProfileView}) => {
if (!moderationOpts) return null if (!moderationOpts) return null
const moderation = moderateProfile(profile, moderationOpts) const moderation = moderateProfile(profile, moderationOpts)
const disabled = !canBeMessaged(profile)
const handle = sanitizeHandle(profile.handle, '@')
return ( return (
<Button <Button
label={profile.displayName || sanitizeHandle(profile.handle)} label={profile.displayName || sanitizeHandle(profile.handle)}
onPress={() => onCreateChat(profile.did)}> onPress={() => !disabled && onCreateChat(profile.did)}>
{({hovered, pressed}) => ( {({hovered, pressed, focused}) => (
<View <View
style={[ style={[
a.flex_1, a.flex_1,
@ -110,7 +118,9 @@ function SearchablePeopleList({
a.align_center, a.align_center,
a.flex_row, a.flex_row,
a.rounded_sm, a.rounded_sm,
pressed disabled
? {opacity: 0.5}
: pressed || focused
? t.atoms.bg_contrast_25 ? t.atoms.bg_contrast_25
: hovered : hovered
? t.atoms.bg_contrast_50 ? t.atoms.bg_contrast_50
@ -131,8 +141,12 @@ function SearchablePeopleList({
moderation.ui('displayName'), moderation.ui('displayName'),
)} )}
</Text> </Text>
<Text style={t.atoms.text_contrast_high} numberOfLines={1}> <Text style={t.atoms.text_contrast_high} numberOfLines={2}>
{sanitizeHandle(profile.handle, '@')} {disabled ? (
<Trans>{handle} can't be messaged</Trans>
) : (
handle
)}
</Text> </Text>
</View> </View>
</View> </View>
@ -166,7 +180,6 @@ function SearchablePeopleList({
t.atoms.bg, t.atoms.bg,
]} ]}
/> />
<Dialog.Close />
<Text <Text
style={[ style={[
a.text_2xl, a.text_2xl,
@ -201,14 +214,23 @@ function SearchablePeopleList({
autoFocus autoFocus
/> />
</TextField.Root> </TextField.Root>
<Dialog.Close />
</View> </View>
) )
}, [t.atoms.bg, _, control, searchText]) }, [t.atoms.bg, _, control, searchText])
const dataWithoutSelf = useMemo(() => {
return (
actorAutocompleteData?.filter(
profile => profile.did !== currentAccount?.did,
) ?? []
)
}, [actorAutocompleteData, currentAccount?.did])
return ( return (
<Dialog.InnerFlatList <Dialog.InnerFlatList
ref={listRef} ref={listRef}
data={actorAutocompleteData} data={dataWithoutSelf}
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={ ListHeaderComponent={
<> <>
@ -235,6 +257,7 @@ function SearchablePeopleList({
hideBackButton={true} hideBackButton={true}
emptyType="results" emptyType="results"
sideBorders={false} sideBorders={false}
topBorder={false}
emptyMessage={ emptyMessage={
isError isError
? _(msg`No search results found for "${searchText}".`) ? _(msg`No search results found for "${searchText}".`)

View File

@ -0,0 +1,18 @@
import {AppBskyActorDefs} from '@atproto/api'
export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) {
switch (profile.associated?.chat?.allowIncoming) {
case 'none':
return false
case 'all':
return true
// if unset, treat as following
case 'following':
case undefined:
return Boolean(profile.viewer?.followedBy)
// any other values are invalid according to the lexicon, so
// let's treat as false to be safe
default:
return false
}
}

View File

@ -1,4 +1,4 @@
import React from 'react' import React, {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
@ -88,22 +88,22 @@ function ChatListItemReady({
} }
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const [showActions, setShowActions] = React.useState(false) const [showActions, setShowActions] = useState(false)
const onMouseEnter = React.useCallback(() => { const onMouseEnter = useCallback(() => {
setShowActions(true) setShowActions(true)
}, []) }, [])
const onMouseLeave = React.useCallback(() => { const onMouseLeave = useCallback(() => {
setShowActions(false) setShowActions(false)
}, []) }, [])
const onFocus = React.useCallback<React.FocusEventHandler>(e => { const onFocus = useCallback<React.FocusEventHandler>(e => {
if (e.nativeEvent.relatedTarget == null) return if (e.nativeEvent.relatedTarget == null) return
setShowActions(true) setShowActions(true)
}, []) }, [])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
navigation.push('MessagesConversation', { navigation.push('MessagesConversation', {
conversation: convo.id, conversation: convo.id,
}) })
@ -119,9 +119,9 @@ function ChatListItemReady({
<Button <Button
label={profile.displayName || profile.handle} label={profile.displayName || profile.handle}
onPress={onPress} onPress={onPress}
style={a.flex_1} style={[a.flex_1]}
onLongPress={isNative ? menuControl.open : undefined}> onLongPress={isNative ? menuControl.open : undefined}>
{({hovered, pressed}) => ( {({hovered, pressed, focused}) => (
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@ -129,7 +129,7 @@ function ChatListItemReady({
a.px_lg, a.px_lg,
a.py_md, a.py_md,
a.gap_md, a.gap_md,
(hovered || pressed) && t.atoms.bg_contrast_25, (hovered || pressed || focused) && t.atoms.bg_contrast_25,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
<UserAvatar <UserAvatar