Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Factor lightbox out into hook/context (#1919)

authored by

Paul Frazee and committed by
GitHub
e749f2f3 03b20c70

+152 -104
+4 -1
src/App.native.tsx
··· 25 25 import {TestCtrls} from 'view/com/testing/TestCtrls' 26 26 import {Provider as ShellStateProvider} from 'state/shell' 27 27 import {Provider as ModalStateProvider} from 'state/modals' 28 + import {Provider as LightboxStateProvider} from 'state/lightbox' 28 29 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 29 30 import {Provider as InvitesStateProvider} from 'state/invites' 30 31 import {Provider as PrefsStateProvider} from 'state/preferences' ··· 124 125 <MutedThreadsProvider> 125 126 <InvitesStateProvider> 126 127 <ModalStateProvider> 127 - <InnerApp /> 128 + <LightboxStateProvider> 129 + <InnerApp /> 130 + </LightboxStateProvider> 128 131 </ModalStateProvider> 129 132 </InvitesStateProvider> 130 133 </MutedThreadsProvider>
+4 -1
src/App.web.tsx
··· 22 22 import {defaultLocale, dynamicActivate} from './locale/i18n' 23 23 import {Provider as ShellStateProvider} from 'state/shell' 24 24 import {Provider as ModalStateProvider} from 'state/modals' 25 + import {Provider as LightboxStateProvider} from 'state/lightbox' 25 26 import {Provider as MutedThreadsProvider} from 'state/muted-threads' 26 27 import {Provider as InvitesStateProvider} from 'state/invites' 27 28 import {Provider as PrefsStateProvider} from 'state/preferences' ··· 111 112 <MutedThreadsProvider> 112 113 <InvitesStateProvider> 113 114 <ModalStateProvider> 114 - <InnerApp /> 115 + <LightboxStateProvider> 116 + <InnerApp /> 117 + </LightboxStateProvider> 115 118 </ModalStateProvider> 116 119 </InvitesStateProvider> 117 120 </MutedThreadsProvider>
+86
src/state/lightbox.tsx
··· 1 + import React from 'react' 2 + import {AppBskyActorDefs} from '@atproto/api' 3 + 4 + interface Lightbox { 5 + name: string 6 + } 7 + 8 + export class ProfileImageLightbox implements Lightbox { 9 + name = 'profile-image' 10 + constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {} 11 + } 12 + 13 + interface ImagesLightboxItem { 14 + uri: string 15 + alt?: string 16 + } 17 + 18 + export class ImagesLightbox implements Lightbox { 19 + name = 'images' 20 + constructor(public images: ImagesLightboxItem[], public index: number) {} 21 + setIndex(index: number) { 22 + this.index = index 23 + } 24 + } 25 + 26 + const LightboxContext = React.createContext<{ 27 + activeLightbox: Lightbox | null 28 + }>({ 29 + activeLightbox: null, 30 + }) 31 + 32 + const LightboxControlContext = React.createContext<{ 33 + openLightbox: (lightbox: Lightbox) => void 34 + closeLightbox: () => void 35 + }>({ 36 + openLightbox: () => {}, 37 + closeLightbox: () => {}, 38 + }) 39 + 40 + export function Provider({children}: React.PropsWithChildren<{}>) { 41 + const [activeLightbox, setActiveLightbox] = React.useState<Lightbox | null>( 42 + null, 43 + ) 44 + 45 + const openLightbox = React.useCallback( 46 + (lightbox: Lightbox) => { 47 + setActiveLightbox(lightbox) 48 + }, 49 + [setActiveLightbox], 50 + ) 51 + 52 + const closeLightbox = React.useCallback(() => { 53 + setActiveLightbox(null) 54 + }, [setActiveLightbox]) 55 + 56 + const state = React.useMemo( 57 + () => ({ 58 + activeLightbox, 59 + }), 60 + [activeLightbox], 61 + ) 62 + 63 + const methods = React.useMemo( 64 + () => ({ 65 + openLightbox, 66 + closeLightbox, 67 + }), 68 + [openLightbox, closeLightbox], 69 + ) 70 + 71 + return ( 72 + <LightboxContext.Provider value={state}> 73 + <LightboxControlContext.Provider value={methods}> 74 + {children} 75 + </LightboxControlContext.Provider> 76 + </LightboxContext.Provider> 77 + ) 78 + } 79 + 80 + export function useLightbox() { 81 + return React.useContext(LightboxContext) 82 + } 83 + 84 + export function useLightboxControls() { 85 + return React.useContext(LightboxControlContext) 86 + }
+1 -2
src/state/modals/index.tsx
··· 1 1 import React from 'react' 2 2 import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api' 3 - import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native' 3 + import {StyleProp, ViewStyle} from 'react-native' 4 4 import {Image as RNImage} from 'react-native-image-crop-picker' 5 5 6 6 import {ImageModel} from '#/state/models/media/image' ··· 232 232 233 233 const openModal = React.useCallback( 234 234 (modal: Modal) => { 235 - DeviceEventEmitter.emit('navigation') 236 235 setActiveModals(activeModals => [...activeModals, modal]) 237 236 setIsModalActive(true) 238 237 },
+1 -48
src/state/models/ui/shell.ts
··· 1 - import {AppBskyActorDefs} from '@atproto/api' 2 1 import {RootStoreModel} from '../root-store' 3 2 import {makeAutoObservable} from 'mobx' 4 3 import { ··· 11 10 12 11 export function isColorMode(v: unknown): v is ColorMode { 13 12 return v === 'system' || v === 'light' || v === 'dark' 14 - } 15 - 16 - interface LightboxModel {} 17 - 18 - export class ProfileImageLightbox implements LightboxModel { 19 - name = 'profile-image' 20 - constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) { 21 - makeAutoObservable(this) 22 - } 23 - } 24 - 25 - interface ImagesLightboxItem { 26 - uri: string 27 - alt?: string 28 - } 29 - 30 - export class ImagesLightbox implements LightboxModel { 31 - name = 'images' 32 - constructor(public images: ImagesLightboxItem[], public index: number) { 33 - makeAutoObservable(this) 34 - } 35 - setIndex(index: number) { 36 - this.index = index 37 - } 38 13 } 39 14 40 15 export class ShellUiModel { 41 - isLightboxActive = false 42 - activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null 43 - 44 16 constructor(public rootStore: RootStoreModel) { 45 17 makeAutoObservable(this, { 46 18 rootStore: false, ··· 54 26 * (used by the android hardware back btn) 55 27 */ 56 28 closeAnyActiveElement(): boolean { 57 - if (this.isLightboxActive) { 58 - this.closeLightbox() 59 - return true 60 - } 61 29 return false 62 30 } 63 31 64 32 /** 65 33 * used to clear out any modals, eg for a navigation 66 34 */ 67 - closeAllActiveElements() { 68 - if (this.isLightboxActive) { 69 - this.closeLightbox() 70 - } 71 - } 72 - 73 - openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) { 74 - this.rootStore.emitNavigation() 75 - this.isLightboxActive = true 76 - this.activeLightbox = lightbox 77 - } 78 - 79 - closeLightbox() { 80 - this.isLightboxActive = false 81 - this.activeLightbox = null 82 - } 35 + closeAllActiveElements() {} 83 36 84 37 setupLoginModals() { 85 38 this.rootStore.onSessionReady(() => {
+23 -23
src/view/com/lightbox/Lightbox.tsx
··· 1 1 import React from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import ImageView from './ImageViewing' 6 - import {useStores} from 'state/index' 7 - import * as models from 'state/models/ui/shell' 8 5 import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip' 9 6 import * as Toast from '../util/Toast' 10 7 import {Text} from '../util/text/Text' ··· 12 9 import {Button} from '../util/forms/Button' 13 10 import {isIOS} from 'platform/detection' 14 11 import * as MediaLibrary from 'expo-media-library' 12 + import { 13 + useLightbox, 14 + useLightboxControls, 15 + ProfileImageLightbox, 16 + ImagesLightbox, 17 + } from '#/state/lightbox' 15 18 16 - export const Lightbox = observer(function Lightbox() { 17 - const store = useStores() 19 + export function Lightbox() { 20 + const {activeLightbox} = useLightbox() 21 + const {closeLightbox} = useLightboxControls() 18 22 const onClose = React.useCallback(() => { 19 - store.shell.closeLightbox() 20 - }, [store]) 23 + closeLightbox() 24 + }, [closeLightbox]) 21 25 22 - if (!store.shell.activeLightbox) { 26 + if (!activeLightbox) { 23 27 return null 24 - } else if (store.shell.activeLightbox.name === 'profile-image') { 25 - const opts = store.shell.activeLightbox as models.ProfileImageLightbox 28 + } else if (activeLightbox.name === 'profile-image') { 29 + const opts = activeLightbox as ProfileImageLightbox 26 30 return ( 27 31 <ImageView 28 32 images={[{uri: opts.profile.avatar || ''}]} ··· 32 36 FooterComponent={LightboxFooter} 33 37 /> 34 38 ) 35 - } else if (store.shell.activeLightbox.name === 'images') { 36 - const opts = store.shell.activeLightbox as models.ImagesLightbox 39 + } else if (activeLightbox.name === 'images') { 40 + const opts = activeLightbox as ImagesLightbox 37 41 return ( 38 42 <ImageView 39 43 images={opts.images.map(img => ({...img}))} ··· 46 50 } else { 47 51 return null 48 52 } 49 - }) 53 + } 50 54 51 - const LightboxFooter = observer(function LightboxFooter({ 52 - imageIndex, 53 - }: { 54 - imageIndex: number 55 - }) { 56 - const store = useStores() 55 + function LightboxFooter({imageIndex}: {imageIndex: number}) { 56 + const {activeLightbox} = useLightbox() 57 57 const [isAltExpanded, setAltExpanded] = React.useState(false) 58 58 const [permissionResponse, requestPermission] = MediaLibrary.usePermissions() 59 59 ··· 81 81 [permissionResponse, requestPermission], 82 82 ) 83 83 84 - const lightbox = store.shell.activeLightbox 84 + const lightbox = activeLightbox 85 85 if (!lightbox) { 86 86 return null 87 87 } ··· 89 89 let altText = '' 90 90 let uri = '' 91 91 if (lightbox.name === 'images') { 92 - const opts = lightbox as models.ImagesLightbox 92 + const opts = lightbox as ImagesLightbox 93 93 uri = opts.images[imageIndex].uri 94 94 altText = opts.images[imageIndex].alt || '' 95 95 } else if (lightbox.name === 'profile-image') { 96 - const opts = lightbox as models.ProfileImageLightbox 96 + const opts = lightbox as ProfileImageLightbox 97 97 uri = opts.profile.avatar || '' 98 98 } 99 99 ··· 132 132 </View> 133 133 </View> 134 134 ) 135 - }) 135 + } 136 136 137 137 const styles = StyleSheet.create({ 138 138 footer: {
+19 -14
src/view/com/lightbox/Lightbox.web.tsx
··· 7 7 View, 8 8 Pressable, 9 9 } from 'react-native' 10 - import {observer} from 'mobx-react-lite' 11 10 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 - import {useStores} from 'state/index' 13 - import * as models from 'state/models/ui/shell' 14 11 import {colors, s} from 'lib/styles' 15 12 import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' 16 13 import {Text} from '../util/text/Text' 17 14 import {useLingui} from '@lingui/react' 18 15 import {msg} from '@lingui/macro' 16 + import { 17 + useLightbox, 18 + useLightboxControls, 19 + ImagesLightbox, 20 + ProfileImageLightbox, 21 + } from '#/state/lightbox' 19 22 20 23 interface Img { 21 24 uri: string 22 25 alt?: string 23 26 } 24 27 25 - export const Lightbox = observer(function Lightbox() { 26 - const store = useStores() 27 - 28 - const onClose = useCallback(() => store.shell.closeLightbox(), [store.shell]) 28 + export function Lightbox() { 29 + const {activeLightbox} = useLightbox() 30 + const {closeLightbox} = useLightboxControls() 29 31 30 - if (!store.shell.isLightboxActive) { 32 + if (!activeLightbox) { 31 33 return null 32 34 } 33 35 34 - const activeLightbox = store.shell.activeLightbox 35 36 const initialIndex = 36 - activeLightbox instanceof models.ImagesLightbox ? activeLightbox.index : 0 37 + activeLightbox instanceof ImagesLightbox ? activeLightbox.index : 0 37 38 38 39 let imgs: Img[] | undefined 39 - if (activeLightbox instanceof models.ProfileImageLightbox) { 40 + if (activeLightbox instanceof ProfileImageLightbox) { 40 41 const opts = activeLightbox 41 42 if (opts.profile.avatar) { 42 43 imgs = [{uri: opts.profile.avatar}] 43 44 } 44 - } else if (activeLightbox instanceof models.ImagesLightbox) { 45 + } else if (activeLightbox instanceof ImagesLightbox) { 45 46 const opts = activeLightbox 46 47 imgs = opts.images 47 48 } ··· 51 52 } 52 53 53 54 return ( 54 - <LightboxInner imgs={imgs} initialIndex={initialIndex} onClose={onClose} /> 55 + <LightboxInner 56 + imgs={imgs} 57 + initialIndex={initialIndex} 58 + onClose={closeLightbox} 59 + /> 55 60 ) 56 - }) 61 + } 57 62 58 63 function LightboxInner({ 59 64 imgs,
+4 -5
src/view/com/profile/ProfileHeader.tsx
··· 17 17 import {NavigationProp} from 'lib/routes/types' 18 18 import {isNative} from 'platform/detection' 19 19 import {BlurView} from '../util/BlurView' 20 - import {ProfileImageLightbox} from 'state/models/ui/shell' 21 20 import * as Toast from '../util/Toast' 22 21 import {LoadingPlaceholder} from '../util/LoadingPlaceholder' 23 22 import {Text} from '../util/text/Text' ··· 30 29 import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown' 31 30 import {Link} from '../util/Link' 32 31 import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' 33 - import {useStores} from 'state/index' 34 32 import {useModalControls} from '#/state/modals' 33 + import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox' 35 34 import { 36 35 useProfileFollowMutation, 37 36 useProfileUnfollowMutation, ··· 115 114 }: Props) { 116 115 const pal = usePalette('default') 117 116 const palInverted = usePalette('inverted') 118 - const store = useStores() 119 117 const {currentAccount} = useSession() 120 118 const {_} = useLingui() 121 119 const {openModal} = useModalControls() 120 + const {openLightbox} = useLightboxControls() 122 121 const navigation = useNavigation<NavigationProp>() 123 122 const {track} = useAnalytics() 124 123 const invalidHandle = isInvalidHandle(profile.handle) ··· 151 150 profile.avatar && 152 151 !(moderation.avatar.blur && moderation.avatar.noOverride) 153 152 ) { 154 - store.shell.openLightbox(new ProfileImageLightbox(profile)) 153 + openLightbox(new ProfileImageLightbox(profile)) 155 154 } 156 - }, [store, profile, moderation]) 155 + }, [openLightbox, profile, moderation]) 157 156 158 157 const onPressFollow = React.useCallback(async () => { 159 158 if (profile.viewer?.following) {
+4 -3
src/view/com/profile/ProfileSubpageHeader.tsx
··· 16 16 import {NavigationProp} from 'lib/routes/types' 17 17 import {BACK_HITSLOP} from 'lib/constants' 18 18 import {isNative} from 'platform/detection' 19 - import {ImagesLightbox} from 'state/models/ui/shell' 19 + import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' 20 20 import {useLingui} from '@lingui/react' 21 21 import {msg} from '@lingui/macro' 22 22 import {useSetDrawerOpen} from '#/state/shell' ··· 50 50 const navigation = useNavigation<NavigationProp>() 51 51 const {_} = useLingui() 52 52 const {isMobile} = useWebMediaQueries() 53 + const {openLightbox} = useLightboxControls() 53 54 const pal = usePalette('default') 54 55 const canGoBack = navigation.canGoBack() 55 56 ··· 69 70 if ( 70 71 avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) 71 72 ) { 72 - store.shell.openLightbox(new ImagesLightbox([{uri: avatar}], 0)) 73 + openLightbox(new ImagesLightbox([{uri: avatar}], 0)) 73 74 } 74 - }, [store, avatar]) 75 + }, [openLightbox, avatar]) 75 76 76 77 return ( 77 78 <CenteredView style={pal.view}>
+6 -7
src/view/com/util/post-embeds/index.tsx
··· 19 19 } from '@atproto/api' 20 20 import {Link} from '../Link' 21 21 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 22 - import {ImagesLightbox} from 'state/models/ui/shell' 23 - import {useStores} from 'state/index' 22 + import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' 24 23 import {usePalette} from 'lib/hooks/usePalette' 25 24 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 26 25 import {YoutubeEmbed} from './YoutubeEmbed' ··· 49 48 style?: StyleProp<ViewStyle> 50 49 }) { 51 50 const pal = usePalette('default') 52 - const store = useStores() 51 + const {openLightbox} = useLightboxControls() 53 52 const {isMobile} = useWebMediaQueries() 54 53 55 54 // quote post with media ··· 104 103 alt: img.alt, 105 104 aspectRatio: img.aspectRatio, 106 105 })) 107 - const openLightbox = (index: number) => { 108 - store.shell.openLightbox(new ImagesLightbox(items, index)) 106 + const _openLightbox = (index: number) => { 107 + openLightbox(new ImagesLightbox(items, index)) 109 108 } 110 109 const onPressIn = (_: number) => { 111 110 InteractionManager.runAfterInteractions(() => { ··· 121 120 alt={alt} 122 121 uri={thumb} 123 122 dimensionsHint={aspectRatio} 124 - onPress={() => openLightbox(0)} 123 + onPress={() => _openLightbox(0)} 125 124 onPressIn={() => onPressIn(0)} 126 125 style={[ 127 126 styles.singleImage, ··· 143 142 <View style={[styles.imagesContainer, style]}> 144 143 <ImageLayoutGrid 145 144 images={embed.images} 146 - onPress={openLightbox} 145 + onPress={_openLightbox} 147 146 onPressIn={onPressIn} 148 147 style={ 149 148 embed.images.length === 1