Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
122
fork

Configure Feed

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

Improve the profile preview with "swipe up to view" and local cache optimization (#1096)

* Update the ProfilePreview to use a swipe-up to navigate

* Use the profile cache to optimize load performance

* Hack to align the header in the profile preview against the screen view

* Fix profiles cache logic to ensure cache is used

* Fix dark mode on profile preview

authored by

Paul Frazee and committed by
GitHub
96280d5f 1211c353

+98 -61
+2
package.json
··· 96 96 "lodash.debounce": "^4.0.8", 97 97 "lodash.isequal": "^4.5.0", 98 98 "lodash.omit": "^4.5.0", 99 + "lodash.once": "^4.1.1", 99 100 "lodash.samplesize": "^4.2.0", 100 101 "lodash.set": "^4.3.2", 101 102 "lodash.shuffle": "^4.2.0", ··· 161 162 "@types/lodash.debounce": "^4.0.7", 162 163 "@types/lodash.isequal": "^4.5.6", 163 164 "@types/lodash.omit": "^4.5.7", 165 + "@types/lodash.once": "^4.1.7", 164 166 "@types/lodash.samplesize": "^4.2.7", 165 167 "@types/lodash.set": "^4.3.7", 166 168 "@types/lodash.shuffle": "^4.2.7",
+4 -1
src/Navigation.tsx
··· 125 125 <Stack.Screen 126 126 name="Profile" 127 127 component={ProfileScreen} 128 - options={({route}) => ({title: title(`@${route.params.name}`)})} 128 + options={({route}) => ({ 129 + title: title(`@${route.params.name}`), 130 + animation: 'none', 131 + })} 129 132 /> 130 133 <Stack.Screen 131 134 name="ProfileFollowers"
+1 -3
src/state/models/cache/profiles-view.ts
··· 45 45 } 46 46 47 47 overwrite(did: string, res: GetProfile.Response) { 48 - if (this.cache.has(did)) { 49 - this.cache.set(did, res) 50 - } 48 + this.cache.set(did, res) 51 49 } 52 50 }
+24 -2
src/state/models/content/profile.ts
··· 103 103 // = 104 104 105 105 async setup() { 106 - await this._load() 106 + const precache = await this.rootStore.profiles.cache.get(this.params.actor) 107 + if (precache) { 108 + await this._loadWithCache(precache) 109 + } else { 110 + await this._load() 111 + } 107 112 } 108 113 109 114 async refresh() { ··· 252 257 this._xLoading(isRefreshing) 253 258 try { 254 259 const res = await this.rootStore.agent.getProfile(this.params) 255 - this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation 260 + this.rootStore.profiles.overwrite(this.params.actor, res) 256 261 if (res.data.handle) { 257 262 this.rootStore.handleResolutions.cache.set( 258 263 res.data.handle, ··· 262 267 this._replaceAll(res) 263 268 await this._createRichText() 264 269 this._xIdle() 270 + } catch (e: any) { 271 + this._xIdle(e) 272 + } 273 + } 274 + 275 + async _loadWithCache(precache: GetProfile.Response) { 276 + // use cached value 277 + this._replaceAll(precache) 278 + await this._createRichText() 279 + this._xIdle() 280 + 281 + // fetch latest 282 + try { 283 + const res = await this.rootStore.agent.getProfile(this.params) 284 + this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation 285 + this._replaceAll(res) 286 + await this._createRichText() 265 287 } catch (e: any) { 266 288 this._xIdle(e) 267 289 }
+19 -3
src/view/com/modals/Modal.tsx
··· 6 6 import {useStores} from 'state/index' 7 7 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 8 8 import {usePalette} from 'lib/hooks/usePalette' 9 + import {navigate} from '../../../Navigation' 10 + import once from 'lodash.once' 9 11 10 12 import * as ConfirmModal from './Confirm' 11 13 import * as EditProfileModal from './EditProfile' ··· 35 37 const store = useStores() 36 38 const bottomSheetRef = useRef<BottomSheet>(null) 37 39 const pal = usePalette('default') 40 + 41 + const activeModal = 42 + store.shell.activeModals[store.shell.activeModals.length - 1] 43 + 44 + const navigateOnce = once(navigate) 45 + 46 + const onBottomSheetAnimate = (fromIndex: number, toIndex: number) => { 47 + if (activeModal?.name === 'profile-preview' && toIndex === 1) { 48 + // begin loading the profile screen behind the scenes 49 + navigateOnce('Profile', {name: activeModal.did}) 50 + } 51 + } 38 52 const onBottomSheetChange = (snapPoint: number) => { 39 53 if (snapPoint === -1) { 40 54 store.shell.closeModal() 55 + } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) { 56 + // ensure we navigate to Profile and close the modal 57 + navigateOnce('Profile', {name: activeModal.did}) 58 + store.shell.closeModal() 41 59 } 42 60 } 43 61 const onClose = () => { 44 62 bottomSheetRef.current?.close() 45 63 store.shell.closeModal() 46 64 } 47 - 48 - const activeModal = 49 - store.shell.activeModals[store.shell.activeModals.length - 1] 50 65 51 66 useEffect(() => { 52 67 if (store.shell.isModalActive) { ··· 146 161 } 147 162 handleIndicatorStyle={{backgroundColor: pal.text.color}} 148 163 handleStyle={[styles.handle, pal.view]} 164 + onAnimate={onBottomSheetAnimate} 149 165 onChange={onBottomSheetChange}> 150 166 {element} 151 167 </BottomSheet>
+40 -51
src/view/com/modals/ProfilePreview.tsx
··· 1 - import React, {useState, useEffect, useCallback} from 'react' 2 - import {StyleSheet, View} from 'react-native' 1 + import React, {useState, useEffect} from 'react' 2 + import {ActivityIndicator, StyleSheet, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 - import {useNavigation, StackActions} from '@react-navigation/native' 5 - import {Text} from '../util/text/Text' 4 + import {ThemedText} from '../util/text/ThemedText' 6 5 import {useStores} from 'state/index' 7 6 import {ProfileModel} from 'state/models/content/profile' 8 7 import {usePalette} from 'lib/hooks/usePalette' 9 8 import {useAnalytics} from 'lib/analytics/analytics' 10 9 import {ProfileHeader} from '../profile/ProfileHeader' 11 - import {Button} from '../util/forms/Button' 12 - import {NavigationProp} from 'lib/routes/types' 10 + import {InfoCircleIcon} from 'lib/icons' 11 + import {useNavigationState} from '@react-navigation/native' 12 + import {isIOS} from 'platform/detection' 13 + import {s} from 'lib/styles' 13 14 14 - export const snapPoints = [560] 15 + export const snapPoints = [520, '100%'] 15 16 16 17 export const Component = observer(({did}: {did: string}) => { 17 18 const store = useStores() 18 19 const pal = usePalette('default') 19 - const palInverted = usePalette('inverted') 20 - const navigation = useNavigation<NavigationProp>() 21 20 const [model] = useState(new ProfileModel(store, {actor: did})) 22 21 const {screen} = useAnalytics() 23 22 23 + // track the navigator state to detect if a page-load occurred 24 + const navState = useNavigationState(s => s) 25 + const [initNavState] = useState(navState) 26 + const isLoading = initNavState !== navState 27 + 24 28 useEffect(() => { 25 29 screen('Profile:Preview') 26 30 model.setup() 27 31 }, [model, screen]) 28 32 29 - const onPressViewProfile = useCallback(() => { 30 - navigation.dispatch(StackActions.push('Profile', {name: model.handle})) 31 - store.shell.closeModal() 32 - }, [navigation, store, model]) 33 - 34 33 return ( 35 - <View style={pal.view}> 36 - <View style={styles.headerWrapper}> 34 + <View style={[pal.view, s.flex1]}> 35 + <View 36 + style={[ 37 + styles.headerWrapper, 38 + isLoading && isIOS && styles.headerPositionAdjust, 39 + ]}> 37 40 <ProfileHeader view={model} hideBackButton onRefreshAll={() => {}} /> 38 41 </View> 39 - <View style={[styles.buttonsContainer, pal.view]}> 40 - <View style={styles.buttons}> 41 - <Button 42 - type="inverted" 43 - style={[styles.button, styles.buttonWide]} 44 - onPress={onPressViewProfile} 45 - accessibilityLabel="View profile" 46 - accessibilityHint=""> 47 - <Text type="button-lg" style={palInverted.text}> 48 - View Profile 49 - </Text> 50 - </Button> 51 - <Button 52 - type="default" 53 - style={styles.button} 54 - onPress={() => store.shell.closeModal()} 55 - accessibilityLabel="Close this preview" 56 - accessibilityHint=""> 57 - <Text type="button-lg" style={pal.text}> 58 - Close 59 - </Text> 60 - </Button> 42 + <View style={[styles.hintWrapper, pal.view]}> 43 + <View style={styles.hint}> 44 + {isLoading ? ( 45 + <ActivityIndicator /> 46 + ) : ( 47 + <> 48 + <InfoCircleIcon size={21} style={pal.textLight} /> 49 + <ThemedText type="xl" fg="light"> 50 + Swipe up to see more 51 + </ThemedText> 52 + </> 53 + )} 61 54 </View> 62 55 </View> 63 56 </View> ··· 68 61 headerWrapper: { 69 62 height: 440, 70 63 }, 71 - buttonsContainer: { 72 - height: 120, 64 + headerPositionAdjust: { 65 + // HACK align the header for the profilescreen transition -prf 66 + paddingTop: 23, 73 67 }, 74 - buttons: { 75 - flexDirection: 'row', 76 - gap: 8, 77 - paddingHorizontal: 14, 78 - paddingTop: 16, 68 + hintWrapper: { 69 + height: 80, 79 70 }, 80 - button: { 81 - flex: 2, 71 + hint: { 82 72 flexDirection: 'row', 83 73 justifyContent: 'center', 84 - paddingVertical: 12, 85 - }, 86 - buttonWide: { 87 - flex: 3, 74 + gap: 8, 75 + paddingHorizontal: 14, 76 + borderRadius: 6, 88 77 }, 89 78 })
+8 -1
yarn.lock
··· 6343 6343 dependencies: 6344 6344 "@types/lodash" "*" 6345 6345 6346 + "@types/lodash.once@^4.1.7": 6347 + version "4.1.7" 6348 + resolved "https://registry.yarnpkg.com/@types/lodash.once/-/lodash.once-4.1.7.tgz#84bc1f711725f6cd6d8be04365623141e09bc007" 6349 + integrity sha512-XWhnXzWkxoleOoXKmzUtep8vT+wiiQQgmPD+wzG0yO0bdlszmnqHRb2WiY5hK/8V0DTet1+z9DJj9cnbdAhWng== 6350 + dependencies: 6351 + "@types/lodash" "*" 6352 + 6346 6353 "@types/lodash.samplesize@^4.2.7": 6347 6354 version "4.2.7" 6348 6355 resolved "https://registry.yarnpkg.com/@types/lodash.samplesize/-/lodash.samplesize-4.2.7.tgz#15784dd9e54aa1bf043552bdb533b83fcf50b82f" ··· 13912 13919 resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" 13913 13920 integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== 13914 13921 13915 - lodash.once@^4.0.0: 13922 + lodash.once@^4.0.0, lodash.once@^4.1.1: 13916 13923 version "4.1.1" 13917 13924 resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 13918 13925 integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==