mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at rm-proxy 316 lines 9.4 kB view raw
1import React, {useCallback, useState} from 'react' 2import { 3 ActivityIndicator, 4 KeyboardAvoidingView, 5 ScrollView, 6 StyleSheet, 7 TextInput, 8 TouchableOpacity, 9 View, 10} from 'react-native' 11import {Image as RNImage} from 'react-native-image-crop-picker' 12import Animated, {FadeOut} from 'react-native-reanimated' 13import {LinearGradient} from 'expo-linear-gradient' 14import {AppBskyActorDefs} from '@atproto/api' 15import {msg, Trans} from '@lingui/macro' 16import {useLingui} from '@lingui/react' 17 18import {logger} from '#/logger' 19import {useModalControls} from '#/state/modals' 20import {useProfileUpdateMutation} from '#/state/queries/profile' 21import {useAnalytics} from 'lib/analytics/analytics' 22import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from 'lib/constants' 23import {usePalette} from 'lib/hooks/usePalette' 24import {compressIfNeeded} from 'lib/media/manip' 25import {cleanError} from 'lib/strings/errors' 26import {enforceLen} from 'lib/strings/helpers' 27import {colors, gradients, s} from 'lib/styles' 28import {useTheme} from 'lib/ThemeContext' 29import {isWeb} from 'platform/detection' 30import {ErrorMessage} from '../util/error/ErrorMessage' 31import {Text} from '../util/text/Text' 32import * as Toast from '../util/Toast' 33import {EditableUserAvatar} from '../util/UserAvatar' 34import {UserBanner} from '../util/UserBanner' 35 36const AnimatedTouchableOpacity = 37 Animated.createAnimatedComponent(TouchableOpacity) 38 39export const snapPoints = ['fullscreen'] 40 41export function Component({ 42 profile, 43 onUpdate, 44}: { 45 profile: AppBskyActorDefs.ProfileViewDetailed 46 onUpdate?: () => void 47}) { 48 const pal = usePalette('default') 49 const theme = useTheme() 50 const {track} = useAnalytics() 51 const {_} = useLingui() 52 const {closeModal} = useModalControls() 53 const updateMutation = useProfileUpdateMutation() 54 const [imageError, setImageError] = useState<string>('') 55 const [displayName, setDisplayName] = useState<string>( 56 profile.displayName || '', 57 ) 58 const [description, setDescription] = useState<string>( 59 profile.description || '', 60 ) 61 const [userBanner, setUserBanner] = useState<string | undefined | null>( 62 profile.banner, 63 ) 64 const [userAvatar, setUserAvatar] = useState<string | undefined | null>( 65 profile.avatar, 66 ) 67 const [newUserBanner, setNewUserBanner] = useState< 68 RNImage | undefined | null 69 >() 70 const [newUserAvatar, setNewUserAvatar] = useState< 71 RNImage | undefined | null 72 >() 73 const onPressCancel = () => { 74 closeModal() 75 } 76 const onSelectNewAvatar = useCallback( 77 async (img: RNImage | null) => { 78 setImageError('') 79 if (img === null) { 80 setNewUserAvatar(null) 81 setUserAvatar(null) 82 return 83 } 84 track('EditProfile:AvatarSelected') 85 try { 86 const finalImg = await compressIfNeeded(img, 1000000) 87 setNewUserAvatar(finalImg) 88 setUserAvatar(finalImg.path) 89 } catch (e: any) { 90 setImageError(cleanError(e)) 91 } 92 }, 93 [track, setNewUserAvatar, setUserAvatar, setImageError], 94 ) 95 96 const onSelectNewBanner = useCallback( 97 async (img: RNImage | null) => { 98 setImageError('') 99 if (!img) { 100 setNewUserBanner(null) 101 setUserBanner(null) 102 return 103 } 104 track('EditProfile:BannerSelected') 105 try { 106 const finalImg = await compressIfNeeded(img, 1000000) 107 setNewUserBanner(finalImg) 108 setUserBanner(finalImg.path) 109 } catch (e: any) { 110 setImageError(cleanError(e)) 111 } 112 }, 113 [track, setNewUserBanner, setUserBanner, setImageError], 114 ) 115 116 const onPressSave = useCallback(async () => { 117 track('EditProfile:Save') 118 setImageError('') 119 try { 120 await updateMutation.mutateAsync({ 121 profile, 122 updates: { 123 displayName, 124 description, 125 }, 126 newUserAvatar, 127 newUserBanner, 128 }) 129 Toast.show(_(msg`Profile updated`)) 130 onUpdate?.() 131 closeModal() 132 } catch (e: any) { 133 logger.error('Failed to update user profile', {message: String(e)}) 134 } 135 }, [ 136 track, 137 updateMutation, 138 profile, 139 onUpdate, 140 closeModal, 141 displayName, 142 description, 143 newUserAvatar, 144 newUserBanner, 145 setImageError, 146 _, 147 ]) 148 149 return ( 150 <KeyboardAvoidingView style={s.flex1} behavior="height"> 151 <ScrollView style={[pal.view]} testID="editProfileModal"> 152 <Text style={[styles.title, pal.text]}> 153 <Trans>Edit my profile</Trans> 154 </Text> 155 <View style={styles.photos}> 156 <UserBanner 157 banner={userBanner} 158 onSelectNewBanner={onSelectNewBanner} 159 /> 160 <View style={[styles.avi, {borderColor: pal.colors.background}]}> 161 <EditableUserAvatar 162 size={80} 163 avatar={userAvatar} 164 onSelectNewAvatar={onSelectNewAvatar} 165 /> 166 </View> 167 </View> 168 {updateMutation.isError && ( 169 <View style={styles.errorContainer}> 170 <ErrorMessage message={cleanError(updateMutation.error)} /> 171 </View> 172 )} 173 {imageError !== '' && ( 174 <View style={styles.errorContainer}> 175 <ErrorMessage message={imageError} /> 176 </View> 177 )} 178 <View style={styles.form}> 179 <View> 180 <Text style={[styles.label, pal.text]}> 181 <Trans>Display Name</Trans> 182 </Text> 183 <TextInput 184 testID="editProfileDisplayNameInput" 185 style={[styles.textInput, pal.border, pal.text]} 186 placeholder={_(msg`e.g. Alice Roberts`)} 187 placeholderTextColor={colors.gray4} 188 value={displayName} 189 onChangeText={v => 190 setDisplayName(enforceLen(v, MAX_DISPLAY_NAME)) 191 } 192 accessible={true} 193 accessibilityLabel={_(msg`Display name`)} 194 accessibilityHint={_(msg`Edit your display name`)} 195 /> 196 </View> 197 <View style={s.pb10}> 198 <Text style={[styles.label, pal.text]}> 199 <Trans>Description</Trans> 200 </Text> 201 <TextInput 202 testID="editProfileDescriptionInput" 203 style={[styles.textArea, pal.border, pal.text]} 204 placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)} 205 placeholderTextColor={colors.gray4} 206 keyboardAppearance={theme.colorScheme} 207 multiline 208 value={description} 209 onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} 210 accessible={true} 211 accessibilityLabel={_(msg`Description`)} 212 accessibilityHint={_(msg`Edit your profile description`)} 213 /> 214 </View> 215 {updateMutation.isPending ? ( 216 <View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}> 217 <ActivityIndicator /> 218 </View> 219 ) : ( 220 <TouchableOpacity 221 testID="editProfileSaveBtn" 222 style={s.mt10} 223 onPress={onPressSave} 224 accessibilityRole="button" 225 accessibilityLabel={_(msg`Save`)} 226 accessibilityHint={_(msg`Saves any changes to your profile`)}> 227 <LinearGradient 228 colors={[gradients.blueLight.start, gradients.blueLight.end]} 229 start={{x: 0, y: 0}} 230 end={{x: 1, y: 1}} 231 style={[styles.btn]}> 232 <Text style={[s.white, s.bold]}> 233 <Trans>Save Changes</Trans> 234 </Text> 235 </LinearGradient> 236 </TouchableOpacity> 237 )} 238 {!updateMutation.isPending && ( 239 <AnimatedTouchableOpacity 240 exiting={!isWeb ? FadeOut : undefined} 241 testID="editProfileCancelBtn" 242 style={s.mt5} 243 onPress={onPressCancel} 244 accessibilityRole="button" 245 accessibilityLabel={_(msg`Cancel profile editing`)} 246 accessibilityHint="" 247 onAccessibilityEscape={onPressCancel}> 248 <View style={[styles.btn]}> 249 <Text style={[s.black, s.bold, pal.text]}> 250 <Trans>Cancel</Trans> 251 </Text> 252 </View> 253 </AnimatedTouchableOpacity> 254 )} 255 </View> 256 </ScrollView> 257 </KeyboardAvoidingView> 258 ) 259} 260 261const styles = StyleSheet.create({ 262 title: { 263 textAlign: 'center', 264 fontWeight: 'bold', 265 fontSize: 24, 266 marginBottom: 18, 267 }, 268 label: { 269 fontWeight: 'bold', 270 paddingHorizontal: 4, 271 paddingBottom: 4, 272 marginTop: 20, 273 }, 274 form: { 275 paddingHorizontal: 14, 276 }, 277 textInput: { 278 borderWidth: 1, 279 borderRadius: 6, 280 paddingHorizontal: 14, 281 paddingVertical: 10, 282 fontSize: 16, 283 }, 284 textArea: { 285 borderWidth: 1, 286 borderRadius: 6, 287 paddingHorizontal: 12, 288 paddingTop: 10, 289 fontSize: 16, 290 height: 120, 291 textAlignVertical: 'top', 292 }, 293 btn: { 294 flexDirection: 'row', 295 alignItems: 'center', 296 justifyContent: 'center', 297 width: '100%', 298 borderRadius: 32, 299 padding: 10, 300 marginBottom: 10, 301 }, 302 avi: { 303 position: 'absolute', 304 top: 80, 305 left: 24, 306 width: 84, 307 height: 84, 308 borderWidth: 2, 309 borderRadius: 42, 310 }, 311 photos: { 312 marginBottom: 36, 313 marginHorizontal: -14, 314 }, 315 errorContainer: {marginTop: 20}, 316})