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