Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 3a9526d55eccacaf65fcbc885744d8ef4e50cf6a 267 lines 8.4 kB view raw
1import React, {useCallback, useEffect} from 'react' 2import {View} from 'react-native' 3import { 4 type AppBskyActorDefs, 5 moderateProfile, 6 type ModerationDecision, 7} from '@atproto/api' 8import {msg, Trans} from '@lingui/macro' 9import {useLingui} from '@lingui/react' 10import { 11 type RouteProp, 12 useFocusEffect, 13 useNavigation, 14 useRoute, 15} from '@react-navigation/native' 16import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 19import { 20 type CommonNavigatorParams, 21 type NavigationProp, 22} from '#/lib/routes/types' 23import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 24import {useEmail} from '#/state/email-verification' 25import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' 26import {ConvoStatus} from '#/state/messages/convo/types' 27import {useCurrentConvoId} from '#/state/messages/current-convo-id' 28import {useModerationOpts} from '#/state/preferences/moderation-opts' 29import {useProfileQuery} from '#/state/queries/profile' 30import {useSetMinimalShellMode} from '#/state/shell' 31import {MessagesList} from '#/screens/Messages/components/MessagesList' 32import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 33import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' 34import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 35import { 36 EmailDialogScreenID, 37 useEmailDialogControl, 38} from '#/components/dialogs/EmailDialog' 39import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 40import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 41import {Error} from '#/components/Error' 42import * as Layout from '#/components/Layout' 43import {Loader} from '#/components/Loader' 44import {IS_WEB} from '#/env' 45 46type Props = NativeStackScreenProps< 47 CommonNavigatorParams, 48 'MessagesConversation' 49> 50 51export function MessagesConversationScreen(props: Props) { 52 const {_} = useLingui() 53 const aaCopy = useAgeAssuranceCopy() 54 return ( 55 <AgeRestrictedScreen 56 screenTitle={_(msg`Conversation`)} 57 infoText={aaCopy.chatsInfoText}> 58 <MessagesConversationScreenInner {...props} /> 59 </AgeRestrictedScreen> 60 ) 61} 62 63export function MessagesConversationScreenInner({route}: Props) { 64 const {gtMobile} = useBreakpoints() 65 const setMinimalShellMode = useSetMinimalShellMode() 66 67 const convoId = route.params.conversation 68 const {setCurrentConvoId} = useCurrentConvoId() 69 70 useFocusEffect( 71 useCallback(() => { 72 setCurrentConvoId(convoId) 73 74 if (IS_WEB && !gtMobile) { 75 setMinimalShellMode(true) 76 } else { 77 setMinimalShellMode(false) 78 } 79 80 return () => { 81 setCurrentConvoId(undefined) 82 setMinimalShellMode(false) 83 } 84 }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]), 85 ) 86 87 return ( 88 <Layout.Screen testID="convoScreen" style={web([{minHeight: 0}, a.flex_1])}> 89 <ConvoProvider key={convoId} convoId={convoId}> 90 <Inner /> 91 </ConvoProvider> 92 </Layout.Screen> 93 ) 94} 95 96function Inner() { 97 const t = useTheme() 98 const convoState = useConvo() 99 const {_} = useLingui() 100 101 const moderationOpts = useModerationOpts() 102 const {data: recipientUnshadowed} = useProfileQuery({ 103 did: convoState.recipients?.[0].did, 104 }) 105 const recipient = useMaybeProfileShadow(recipientUnshadowed) 106 107 const moderation = React.useMemo(() => { 108 if (!recipient || !moderationOpts) return null 109 return moderateProfile(recipient, moderationOpts) 110 }, [recipient, moderationOpts]) 111 112 // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, 113 // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be 114 // empty. So, we also check for that possible state as well and render once we can. 115 const [hasScrolled, setHasScrolled] = React.useState(false) 116 const readyToShow = 117 hasScrolled || 118 (isConvoActive(convoState) && 119 !convoState.isFetchingHistory && 120 convoState.items.length === 0) 121 122 // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this 123 // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added. 124 React.useEffect(() => { 125 if (convoState.status === ConvoStatus.Initializing) { 126 setHasScrolled(false) 127 } 128 }, [convoState.status]) 129 130 if (convoState.status === ConvoStatus.Error) { 131 return ( 132 <> 133 <Layout.Center style={[a.flex_1]}> 134 {moderation ? ( 135 <MessagesListHeader moderation={moderation} profile={recipient} /> 136 ) : ( 137 <MessagesListHeader /> 138 )} 139 </Layout.Center> 140 <Error 141 title={_(msg`Something went wrong`)} 142 message={_(msg`We couldn't load this conversation`)} 143 onRetry={() => convoState.error.retry()} 144 sideBorders={false} 145 /> 146 </> 147 ) 148 } 149 150 return ( 151 <Layout.Center style={[a.flex_1]}> 152 {!readyToShow && 153 (moderation ? ( 154 <MessagesListHeader moderation={moderation} profile={recipient} /> 155 ) : ( 156 <MessagesListHeader /> 157 ))} 158 <View style={[a.flex_1]}> 159 {moderation && recipient ? ( 160 <InnerReady 161 moderation={moderation} 162 recipient={recipient} 163 hasScrolled={hasScrolled} 164 setHasScrolled={setHasScrolled} 165 /> 166 ) : ( 167 <View style={[a.align_center, a.gap_sm, a.flex_1]} /> 168 )} 169 {!readyToShow && ( 170 <View 171 style={[ 172 a.absolute, 173 a.z_10, 174 a.w_full, 175 a.h_full, 176 a.justify_center, 177 a.align_center, 178 t.atoms.bg, 179 ]}> 180 <View style={[{marginBottom: 75}]}> 181 <Loader size="xl" /> 182 </View> 183 </View> 184 )} 185 </View> 186 </Layout.Center> 187 ) 188} 189 190function InnerReady({ 191 moderation, 192 recipient, 193 hasScrolled, 194 setHasScrolled, 195}: { 196 moderation: ModerationDecision 197 recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed> 198 hasScrolled: boolean 199 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 200}) { 201 const convoState = useConvo() 202 const navigation = useNavigation<NavigationProp>() 203 const {params} = 204 useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>() 205 const {needsEmailVerification} = useEmail() 206 const emailDialogControl = useEmailDialogControl() 207 208 /** 209 * Must be non-reactive, otherwise the update to open the global dialog will 210 * cause a re-render loop. 211 */ 212 const maybeBlockForEmailVerification = useNonReactiveCallback(() => { 213 if (needsEmailVerification) { 214 /* 215 * HACKFIX 216 * 217 * Load bearing timeout, to bump this state update until the after the 218 * `navigator.addListener('state')` handler closes elements from 219 * `shell/index.*.tsx` - sfn & esb 220 */ 221 setTimeout(() => 222 emailDialogControl.open({ 223 id: EmailDialogScreenID.Verify, 224 instructions: [ 225 <Trans key="pre-compose"> 226 Before you can message another user, you must first verify your 227 email. 228 </Trans>, 229 ], 230 onCloseWithoutVerifying: () => { 231 if (navigation.canGoBack()) { 232 navigation.goBack() 233 } else { 234 navigation.navigate('Messages', {animation: 'pop'}) 235 } 236 }, 237 }), 238 ) 239 } 240 }) 241 242 useEffect(() => { 243 maybeBlockForEmailVerification() 244 }, [maybeBlockForEmailVerification]) 245 246 return ( 247 <> 248 <MessagesListHeader profile={recipient} moderation={moderation} /> 249 {isConvoActive(convoState) && ( 250 <MessagesList 251 hasScrolled={hasScrolled} 252 setHasScrolled={setHasScrolled} 253 blocked={moderation?.blocked} 254 hasAcceptOverride={!!params.accept} 255 footer={ 256 <MessagesListBlockedFooter 257 recipient={recipient} 258 convoId={convoState.convo.id} 259 hasMessages={convoState.items.length > 0} 260 moderation={moderation} 261 /> 262 } 263 /> 264 )} 265 </> 266 ) 267}