Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Chat] Remove convoState dependency from header (#10293)

authored by

Samuel Newman and committed by
GitHub
bc3672ce 35411e88

+66 -142
+1 -1
src/components/dms/ConvoMenu.tsx
··· 190 190 const isDeletedAccount = profile.handle === 'missing.invalid' 191 191 192 192 const convoId = initialConvo.id 193 - const {data: convo} = useConvoQuery(initialConvo) 193 + const {data: convo} = useConvoQuery({convoId}) 194 194 195 195 const onNavigateToProfile = useCallback(() => { 196 196 navigation.navigate('Profile', {name: profile.did})
+29 -68
src/components/dms/MessagesListHeader.tsx
··· 1 1 import {useMemo} from 'react' 2 2 import {View} from 'react-native' 3 3 import { 4 - type AppBskyActorDefs, 5 4 ChatBskyConvoDefs, 6 - type ModerationCause, 7 - type ModerationDecision, 5 + moderateProfile, 6 + type ModerationOpts, 8 7 } from '@atproto/api' 9 8 import {useLingui} from '@lingui/react/macro' 10 9 import {useNavigation} from '@react-navigation/native' ··· 12 11 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 13 12 import {makeProfileLink} from '#/lib/routes/links' 14 13 import {type NavigationProp} from '#/lib/routes/types' 15 - import {type Shadow} from '#/state/cache/profile-shadow' 14 + import {useProfileShadow} from '#/state/cache/profile-shadow' 15 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 16 import {useSession} from '#/state/session' 17 17 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 18 18 import {atoms as a, useTheme} from '#/alf' ··· 30 30 31 31 const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE 32 32 33 - export function MessagesListHeader({ 34 - convo, 35 - profile, 36 - moderation, 37 - }: { 38 - convo?: ConvoWithDetails | null 39 - profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 40 - moderation?: ModerationDecision | null 41 - }) { 33 + export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) { 42 34 const t = useTheme() 43 - 44 - const isGroupChat = convo?.kind === 'group' 45 - 46 - const blockInfo = useMemo(() => { 47 - if (!moderation) return 48 - const modui = moderation.ui('profileView') 49 - const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 50 - const listBlocks = blocks.filter(alert => alert.source.type === 'list') 51 - const userBlock = blocks.find(alert => alert.source.type === 'user') 52 - return { 53 - listBlocks, 54 - userBlock, 55 - } 56 - }, [moderation]) 35 + const moderationOpts = useModerationOpts() 57 36 58 37 return ( 59 38 <Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}> ··· 61 40 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 62 41 <Layout.Header.BackButton /> 63 42 </View> 64 - {convo ? ( 65 - moderation && blockInfo && profile && !isGroupChat ? ( 66 - <ProfileHeaderReady 67 - convo={convo} 68 - profile={profile} 69 - moderation={moderation} 70 - blockInfo={blockInfo} 71 - /> 43 + {convo && moderationOpts ? ( 44 + convo.kind === 'direct' ? ( 45 + <ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} /> 72 46 ) : ( 73 - <GroupHeaderReady 74 - convo={convo} 75 - profile={profile} 76 - moderation={moderation} 77 - /> 47 + <GroupHeaderReady convo={convo} /> 78 48 ) 79 49 ) : ( 80 50 <> ··· 108 78 109 79 function ProfileHeaderReady({ 110 80 convo, 111 - profile, 112 - moderation, 113 - blockInfo, 81 + moderationOpts, 114 82 }: { 115 - convo: ConvoWithDetails 116 - profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 117 - moderation: ModerationDecision 118 - blockInfo: { 119 - listBlocks: ModerationCause[] 120 - userBlock?: ModerationCause 121 - } 83 + convo: Extract<ConvoWithDetails, {kind: 'direct'}> 84 + moderationOpts: ModerationOpts 122 85 }) { 123 86 const {t: l} = useLingui() 124 87 const {currentAccount} = useSession() 88 + const profile = useProfileShadow(convo.primaryMember) 89 + 90 + const moderation = moderateProfile(profile, moderationOpts) 91 + 92 + const blockInfo = useMemo(() => { 93 + const modui = moderation.ui('profileView') 94 + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 95 + const listBlocks = blocks.filter(alert => alert.source.type === 'list') 96 + const userBlock = blocks.find(alert => alert.source.type === 'user') 97 + return { 98 + listBlocks, 99 + userBlock, 100 + } 101 + }, [moderation]) 125 102 126 103 const isDeletedAccount = profile?.handle === 'missing.invalid' 127 104 const displayName = isDeletedAccount ··· 171 148 172 149 function GroupHeaderReady({ 173 150 convo, 174 - profile, 175 - moderation, 176 151 }: { 177 - convo: ConvoWithDetails 178 - profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 179 - moderation?: ModerationDecision | null 152 + convo: Extract<ConvoWithDetails, {kind: 'group'}> 180 153 }) { 181 154 const {t: l} = useLingui() 182 155 183 156 const navigation = useNavigation<NavigationProp>() 184 157 185 - const groupInfo = convo.kind === 'group' ? convo.details : undefined 186 - 187 - const isDeletedAccount = profile?.handle === 'missing.invalid' 188 - const displayName = isDeletedAccount 189 - ? l`Deleted Account` 190 - : profile 191 - ? createSanitizedDisplayName(profile, true, moderation?.ui('displayName')) 192 - : undefined 193 - const groupName = 194 - groupInfo?.name ?? 195 - (displayName ? l`${displayName}’s group chat` : l`Group chat`) 196 - 197 158 const handleNavigateToSettings = () => { 198 159 navigation.navigate('MessagesConversationSettings', { 199 160 conversation: convo.view.id, ··· 206 167 <> 207 168 <AvatarBubbles size="small" profiles={convo.members} /> 208 169 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 209 - {groupName} 170 + {convo.details.name} 210 171 </Text> 211 172 </> 212 173 }
+2 -2
src/components/dms/util.ts
··· 56 56 return myReactions.length >= EMOJI_REACTION_LIMIT 57 57 } 58 58 59 - type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & { 59 + export type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & { 60 60 // can be missing if account deleted 61 61 kind?: $Typed<ChatBskyActorDefs.GroupConvoMember> 62 62 } 63 63 64 - type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & { 64 + export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & { 65 65 kind: $Typed<ChatBskyActorDefs.DirectConvoMember> 66 66 } 67 67
+28 -58
src/screens/Messages/Conversation.tsx
··· 1 1 import {useCallback, useEffect, useMemo, useState} from 'react' 2 2 import {type LayoutChangeEvent, View} from 'react-native' 3 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 - import { 5 - type AppBskyActorDefs, 6 - moderateProfile, 7 - type ModerationDecision, 8 - } from '@atproto/api' 4 + import {moderateProfile} from '@atproto/api' 9 5 import { 10 6 ScrollEdgeEffect, 11 7 ScrollEdgeEffectProvider, ··· 28 24 type CommonNavigatorParams, 29 25 type NavigationProp, 30 26 } from '#/lib/routes/types' 31 - import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 27 + import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' 32 28 import {useEmail} from '#/state/email-verification' 33 29 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' 34 30 import {ConvoStatus} from '#/state/messages/convo/types' 35 31 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 36 32 import {useModerationOpts} from '#/state/preferences/moderation-opts' 37 - import {useProfileQuery} from '#/state/queries/profile' 33 + import {useConvoQuery} from '#/state/queries/messages/conversation' 38 34 import {useSession} from '#/state/session' 39 35 import {useSetMinimalShellMode} from '#/state/shell' 40 36 import {MessagesList} from '#/screens/Messages/components/MessagesList' ··· 52 48 import * as Layout from '#/components/Layout' 53 49 import {Loader} from '#/components/Loader' 54 50 import {IS_LIQUID_GLASS, IS_WEB} from '#/env' 51 + import {ChatDisabled} from './components/ChatDisabled' 55 52 56 53 type Props = NativeStackScreenProps< 57 54 CommonNavigatorParams, ··· 95 92 style={web([{minHeight: 0}, a.flex_1])}> 96 93 <ScrollEdgeEffectProvider> 97 94 <ConvoProvider key={convoId} convoId={convoId}> 98 - <Inner /> 95 + <Inner convoId={convoId} /> 99 96 </ConvoProvider> 100 97 </ScrollEdgeEffectProvider> 101 98 </Layout.Screen> 102 99 ) 103 100 } 104 101 105 - function Inner() { 102 + function Inner({convoId}: {convoId: string}) { 106 103 const t = useTheme() 107 104 const convoState = useConvo() 108 105 const {_} = useLingui() 109 106 const {currentAccount} = useSession() 110 107 const isFocused = useIsFocused() 111 108 const {top: topInset} = useSafeAreaInsets() 109 + const {data: convoData} = useConvoQuery({convoId}) 112 110 113 - const convo = convoState.convo 114 - ? parseConvoView(convoState.convo, currentAccount?.did) 111 + const convo = convoData 112 + ? parseConvoView(convoData, currentAccount?.did) 115 113 : null 116 114 117 - const moderationOpts = useModerationOpts() 118 - const {data: recipientUnshadowed} = useProfileQuery({ 119 - did: convoState.getPrimaryMember?.()?.did, 120 - }) 121 - const recipient = useMaybeProfileShadow(recipientUnshadowed) 122 - 123 - const moderation = useMemo(() => { 124 - if (!recipient || !moderationOpts) return null 125 - return moderateProfile(recipient, moderationOpts) 126 - }, [recipient, moderationOpts]) 127 - 128 115 // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, 129 116 // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be 130 117 // empty. So, we also check for that possible state as well and render once we can. ··· 150 137 <> 151 138 <Layout.Center 152 139 style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}> 153 - {moderation ? ( 154 - <MessagesListHeader 155 - convo={convo} 156 - profile={recipient} 157 - moderation={moderation} 158 - /> 159 - ) : ( 160 - <MessagesListHeader convo={convo} /> 161 - )} 140 + <MessagesListHeader convo={convo} /> 162 141 </Layout.Center> 163 142 <Error 164 143 title={_(msg`Something went wrong`)} ··· 176 155 {isFocused && IS_WEB && <RemoveScrollBar />} 177 156 {!readyToShow && ( 178 157 <View style={IS_LIQUID_GLASS && {paddingTop: topInset}}> 179 - {moderation ? ( 180 - <MessagesListHeader 181 - convo={convo} 182 - profile={recipient} 183 - moderation={moderation} 184 - /> 185 - ) : ( 186 - <MessagesListHeader convo={convo} /> 187 - )} 158 + <MessagesListHeader convo={convo} /> 188 159 </View> 189 160 )} 190 161 <View style={[a.flex_1]}> 191 162 <InnerReady 192 - moderation={moderation} 193 - recipient={recipient} 163 + convo={convo} 194 164 hasScrolled={hasScrolled} 195 165 setHasScrolled={setHasScrolled} 196 - convo={convo} 197 166 isActive={isConvoActive(convoState)} 167 + isDisabled={convoState.status === ConvoStatus.Disabled} 198 168 hasMessages={isConvoActive(convoState) && convoState.items.length > 0} 199 169 /> 200 170 {!readyToShow && ( ··· 219 189 } 220 190 221 191 function InnerReady({ 222 - moderation, 223 - recipient, 224 192 hasScrolled, 225 193 setHasScrolled, 226 194 convo, 227 195 isActive, 196 + isDisabled, 228 197 hasMessages, 229 198 }: { 230 - moderation: ModerationDecision | null 231 - recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed> | undefined 232 199 hasScrolled: boolean 233 200 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 234 201 convo: ConvoWithDetails | null 235 202 isActive: boolean 203 + isDisabled: boolean 236 204 hasMessages: boolean 237 205 }) { 238 206 const navigation = useNavigation<NavigationProp>() ··· 284 252 maybeBlockForEmailVerification() 285 253 }, [maybeBlockForEmailVerification]) 286 254 287 - const header = ( 288 - <MessagesListHeader 289 - convo={convo} 290 - profile={recipient} 291 - moderation={moderation} 292 - /> 293 - ) 255 + const primaryMember = useMaybeProfileShadow(convo?.primaryMember) 256 + const moderationOpts = useModerationOpts() 257 + const primaryMemberModeration = useMemo(() => { 258 + if (!primaryMember || !moderationOpts) return null 259 + return moderateProfile(primaryMember, moderationOpts) 260 + }, [primaryMember, moderationOpts]) 261 + 262 + const header = <MessagesListHeader convo={convo} /> 294 263 295 264 return ( 296 265 <> ··· 308 277 <MessagesList 309 278 hasScrolled={hasScrolled} 310 279 setHasScrolled={setHasScrolled} 311 - blocked={moderation?.blocked} 312 280 hasAcceptOverride={!!params.accept} 313 281 transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0} 314 282 footer={ 315 - moderation && recipient && convo ? ( 283 + isDisabled ? ( 284 + <ChatDisabled /> 285 + ) : convo && primaryMember && primaryMemberModeration?.blocked ? ( 316 286 <MessagesListBlockedFooter 317 - recipient={recipient} 287 + recipient={primaryMember} 318 288 convoId={convo.view.id} 319 289 hasMessages={hasMessages} 320 - moderation={moderation} 290 + moderation={primaryMemberModeration} 321 291 /> 322 292 ) : null 323 293 }
+1
src/screens/Messages/components/ChatListItem.tsx
··· 482 482 ] 483 483 : undefined 484 484 } 485 + onPressIn={() => precacheConvoQuery(queryClient, convo)} 485 486 onPress={onPress} 486 487 onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined} 487 488 onAccessibilityAction={showMenu ? onLongPress : undefined}>
+1 -8
src/screens/Messages/components/MessagesList.tsx
··· 50 50 import {useGetPost} from '#/state/queries/post' 51 51 import {useAgent} from '#/state/session' 52 52 import {List, type ListMethods} from '#/view/com/util/List' 53 - import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled' 54 53 import {MessageComposer} from '#/screens/Messages/components/MessageComposer' 55 54 import {MessageInput} from '#/screens/Messages/components/MessageInput' 56 55 import {MessageListError} from '#/screens/Messages/components/MessageListError' ··· 93 92 export function MessagesList({ 94 93 hasScrolled, 95 94 setHasScrolled, 96 - blocked, 97 95 footer, 98 96 hasAcceptOverride, 99 97 transparentHeaderHeight, 100 98 }: { 101 99 hasScrolled: boolean 102 100 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 103 - blocked?: boolean 104 101 footer?: React.ReactNode 105 102 hasAcceptOverride?: boolean 106 103 transparentHeaderHeight?: number ··· 489 486 }), 490 487 opened: 0, 491 488 }}> 492 - {convoState.status === ConvoStatus.Disabled ? ( 493 - <ChatDisabled /> 494 - ) : blocked ? ( 495 - footer 496 - ) : ( 489 + {footer ?? ( 497 490 <ConversationFooter 498 491 convoState={convoState} 499 492 hasAcceptOverride={hasAcceptOverride}>
+4 -5
src/state/queries/messages/conversation.ts
··· 19 19 const RQKEY_ROOT = 'convo' 20 20 export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] 21 21 22 - export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { 22 + export function useConvoQuery({convoId}: {convoId: string}) { 23 23 const agent = useAgent() 24 24 25 25 return useQuery({ 26 - queryKey: RQKEY(convo.id), 26 + queryKey: RQKEY(convoId), 27 27 queryFn: async () => { 28 28 const {data} = await agent.chat.bsky.convo.getConvo( 29 - {convoId: convo.id}, 29 + {convoId}, 30 30 {headers: DM_SERVICE_HEADERS}, 31 31 ) 32 32 return data.convo 33 33 }, 34 - initialData: convo, 35 34 staleTime: STALE.INFINITY, 36 35 }) 37 36 } ··· 58 57 }) => { 59 58 if (!convoId) throw new Error('No convoId provided') 60 59 61 - await agent.api.chat.bsky.convo.updateRead( 60 + await agent.chat.bsky.convo.updateRead( 62 61 { 63 62 convoId, 64 63 messageId,