An ATproto social media client -- with an independent Appview.

[🐴] 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>

authored by samuel.fm

Eric Bailey and committed by
GitHub
ed892228 2121b5f8

+61 -17
+3 -1
src/components/Lists.tsx
··· 136 136 onGoBack, 137 137 hideBackButton, 138 138 sideBorders, 139 + topBorder = true, 139 140 }: { 140 141 isLoading: boolean 141 142 noEmpty?: boolean ··· 149 150 onGoBack?: () => void 150 151 hideBackButton?: boolean 151 152 sideBorders?: boolean 153 + topBorder?: boolean 152 154 }): React.ReactNode => { 153 155 const t = useTheme() 154 156 const {_} = useLingui() ··· 165 167 {paddingTop: 175, paddingBottom: 110}, 166 168 ]} 167 169 sideBorders={sideBorders ?? gtMobile} 168 - topBorder={!gtTablet}> 170 + topBorder={topBorder && !gtTablet}> 169 171 <View style={[a.w_full, a.align_center, {top: 100}]}> 170 172 <Loader size="xl" /> 171 173 </View>
+1
src/components/dialogs/GifSelect.tsx
··· 197 197 onGoBack={onGoBack} 198 198 emptyType="results" 199 199 sideBorders={false} 200 + topBorder={false} 200 201 errorTitle={_(msg`Failed to load GIFs`)} 201 202 errorMessage={_(msg`There was an issue connecting to Tenor.`)} 202 203 emptyMessage={
+30 -7
src/components/dms/NewChat.tsx
··· 10 10 import {isWeb} from '#/platform/detection' 11 11 import {useModerationOpts} from '#/state/preferences/moderation-opts' 12 12 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' 13 + import {useSession} from '#/state/session' 13 14 import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' 14 15 import {FAB} from '#/view/com/util/fab/FAB' 15 16 import * as Toast from '#/view/com/util/Toast' ··· 23 24 import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope' 24 25 import {ListMaybePlaceholder} from '../Lists' 25 26 import {Text} from '../Typography' 27 + import {canBeMessaged} from './util' 26 28 27 29 export function NewChat({ 28 30 control, ··· 82 84 const moderationOpts = useModerationOpts() 83 85 const control = Dialog.useDialogContext() 84 86 const listRef = useRef<BottomSheetFlatListMethods>(null) 87 + const {currentAccount} = useSession() 85 88 86 89 const [searchText, setSearchText] = useState('') 87 90 ··· 95 98 const renderItem = useCallback( 96 99 ({item: profile}: {item: AppBskyActorDefs.ProfileView}) => { 97 100 if (!moderationOpts) return null 101 + 98 102 const moderation = moderateProfile(profile, moderationOpts) 103 + 104 + const disabled = !canBeMessaged(profile) 105 + const handle = sanitizeHandle(profile.handle, '@') 106 + 99 107 return ( 100 108 <Button 101 109 label={profile.displayName || sanitizeHandle(profile.handle)} 102 - onPress={() => onCreateChat(profile.did)}> 103 - {({hovered, pressed}) => ( 110 + onPress={() => !disabled && onCreateChat(profile.did)}> 111 + {({hovered, pressed, focused}) => ( 104 112 <View 105 113 style={[ 106 114 a.flex_1, ··· 110 118 a.align_center, 111 119 a.flex_row, 112 120 a.rounded_sm, 113 - pressed 121 + disabled 122 + ? {opacity: 0.5} 123 + : pressed || focused 114 124 ? t.atoms.bg_contrast_25 115 125 : hovered 116 126 ? t.atoms.bg_contrast_50 ··· 131 141 moderation.ui('displayName'), 132 142 )} 133 143 </Text> 134 - <Text style={t.atoms.text_contrast_high} numberOfLines={1}> 135 - {sanitizeHandle(profile.handle, '@')} 144 + <Text style={t.atoms.text_contrast_high} numberOfLines={2}> 145 + {disabled ? ( 146 + <Trans>{handle} can't be messaged</Trans> 147 + ) : ( 148 + handle 149 + )} 136 150 </Text> 137 151 </View> 138 152 </View> ··· 166 180 t.atoms.bg, 167 181 ]} 168 182 /> 169 - <Dialog.Close /> 170 183 <Text 171 184 style={[ 172 185 a.text_2xl, ··· 201 214 autoFocus 202 215 /> 203 216 </TextField.Root> 217 + <Dialog.Close /> 204 218 </View> 205 219 ) 206 220 }, [t.atoms.bg, _, control, searchText]) 207 221 222 + const dataWithoutSelf = useMemo(() => { 223 + return ( 224 + actorAutocompleteData?.filter( 225 + profile => profile.did !== currentAccount?.did, 226 + ) ?? [] 227 + ) 228 + }, [actorAutocompleteData, currentAccount?.did]) 229 + 208 230 return ( 209 231 <Dialog.InnerFlatList 210 232 ref={listRef} 211 - data={actorAutocompleteData} 233 + data={dataWithoutSelf} 212 234 renderItem={renderItem} 213 235 ListHeaderComponent={ 214 236 <> ··· 235 257 hideBackButton={true} 236 258 emptyType="results" 237 259 sideBorders={false} 260 + topBorder={false} 238 261 emptyMessage={ 239 262 isError 240 263 ? _(msg`No search results found for "${searchText}".`)
+18
src/components/dms/util.ts
··· 1 + import {AppBskyActorDefs} from '@atproto/api' 2 + 3 + export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) { 4 + switch (profile.associated?.chat?.allowIncoming) { 5 + case 'none': 6 + return false 7 + case 'all': 8 + return true 9 + // if unset, treat as following 10 + case 'following': 11 + case undefined: 12 + return Boolean(profile.viewer?.followedBy) 13 + // any other values are invalid according to the lexicon, so 14 + // let's treat as false to be safe 15 + default: 16 + return false 17 + } 18 + }
+9 -9
src/screens/Messages/List/ChatListItem.tsx
··· 1 - import React from 'react' 1 + import React, {useCallback, useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import { 4 4 AppBskyActorDefs, ··· 88 88 } 89 89 90 90 const navigation = useNavigation<NavigationProp>() 91 - const [showActions, setShowActions] = React.useState(false) 91 + const [showActions, setShowActions] = useState(false) 92 92 93 - const onMouseEnter = React.useCallback(() => { 93 + const onMouseEnter = useCallback(() => { 94 94 setShowActions(true) 95 95 }, []) 96 96 97 - const onMouseLeave = React.useCallback(() => { 97 + const onMouseLeave = useCallback(() => { 98 98 setShowActions(false) 99 99 }, []) 100 100 101 - const onFocus = React.useCallback<React.FocusEventHandler>(e => { 101 + const onFocus = useCallback<React.FocusEventHandler>(e => { 102 102 if (e.nativeEvent.relatedTarget == null) return 103 103 setShowActions(true) 104 104 }, []) 105 105 106 - const onPress = React.useCallback(() => { 106 + const onPress = useCallback(() => { 107 107 navigation.push('MessagesConversation', { 108 108 conversation: convo.id, 109 109 }) ··· 119 119 <Button 120 120 label={profile.displayName || profile.handle} 121 121 onPress={onPress} 122 - style={a.flex_1} 122 + style={[a.flex_1]} 123 123 onLongPress={isNative ? menuControl.open : undefined}> 124 - {({hovered, pressed}) => ( 124 + {({hovered, pressed, focused}) => ( 125 125 <View 126 126 style={[ 127 127 a.flex_row, ··· 129 129 a.px_lg, 130 130 a.py_md, 131 131 a.gap_md, 132 - (hovered || pressed) && t.atoms.bg_contrast_25, 132 + (hovered || pressed || focused) && t.atoms.bg_contrast_25, 133 133 t.atoms.border_contrast_low, 134 134 ]}> 135 135 <UserAvatar