Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Small logic cleanups (#3449)

* Small logic cleanups

* Small logic cleanups (#3451)

* remove a few things

* oops

* stop swallowing the error

* queue callbacks

* oops

* log error if caught

* no need to be nullable

* move isClosing=true up

* reset `isClosing` and `closeCallbacks` on close completion and open

* run queued callbacks on `open` if there are any pending

* rm unnecessary ref and check

* ensure order of calls is always correct

* call `snapToIndex()` on open

* add tester to storybook

---------

Co-authored-by: Hailey <me@haileyok.com>

authored by

Eric Bailey
Hailey
and committed by
GitHub
c96bc920 a49a5a35

+213 -54
+1 -2
src/components/Dialog/context.ts
··· 39 39 control.current.open() 40 40 }, 41 41 close: cb => { 42 - control.current.close() 43 - cb?.() 42 + control.current.close(cb) 44 43 }, 45 44 }), 46 45 [id, control],
+34 -20
src/components/Dialog/index.tsx
··· 83 83 const sheetOptions = nativeOptions?.sheet || {} 84 84 const hasSnapPoints = !!sheetOptions.snapPoints 85 85 const insets = useSafeAreaInsets() 86 - const closeCallback = React.useRef<() => void>() 86 + const closeCallbacks = React.useRef<(() => void)[]>([]) 87 87 const {setDialogIsOpen} = useDialogStateControlContext() 88 88 89 89 /* ··· 95 95 * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open. 96 96 */ 97 97 const isOpen = openIndex > -1 98 + 99 + const callQueuedCallbacks = React.useCallback(() => { 100 + for (const cb of closeCallbacks.current) { 101 + try { 102 + cb() 103 + } catch (e: any) { 104 + logger.error('Error running close callback', e) 105 + } 106 + } 107 + 108 + closeCallbacks.current = [] 109 + }, []) 98 110 99 111 const open = React.useCallback<DialogControlProps['open']>( 100 112 ({index} = {}) => { 113 + // Run any leftover callbacks that might have been queued up before calling `.open()` 114 + callQueuedCallbacks() 115 + 101 116 setDialogIsOpen(control.id, true) 102 117 // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" 103 118 setOpenIndex(index || 0) 119 + sheet.current?.snapToIndex(index || 0) 104 120 }, 105 - [setOpenIndex, setDialogIsOpen, control.id], 121 + [setDialogIsOpen, control.id, callQueuedCallbacks], 106 122 ) 107 123 124 + // This is the function that we call when we want to dismiss the dialog. 108 125 const close = React.useCallback<DialogControlProps['close']>(cb => { 109 - if (cb && typeof cb === 'function') { 110 - closeCallback.current = cb 126 + if (typeof cb === 'function') { 127 + closeCallbacks.current.push(cb) 111 128 } 112 129 sheet.current?.close() 113 130 }, []) 114 131 132 + // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to 133 + // happen before we run this. It is passed to the `BottomSheet` component. 134 + const onCloseAnimationComplete = React.useCallback(() => { 135 + // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this 136 + // tells us that we need to toggle the accessibility overlay setting 137 + setDialogIsOpen(control.id, false) 138 + setOpenIndex(-1) 139 + 140 + callQueuedCallbacks() 141 + onClose?.() 142 + }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) 143 + 115 144 useImperativeHandle( 116 145 control.ref, 117 146 () => ({ ··· 120 149 }), 121 150 [open, close], 122 151 ) 123 - 124 - const onCloseInner = React.useCallback(() => { 125 - try { 126 - closeCallback.current?.() 127 - } catch (e: any) { 128 - logger.error(`Dialog closeCallback failed`, { 129 - message: e.message, 130 - }) 131 - } finally { 132 - closeCallback.current = undefined 133 - } 134 - setDialogIsOpen(control.id, false) 135 - onClose?.() 136 - setOpenIndex(-1) 137 - }, [control.id, onClose, setDialogIsOpen]) 138 152 139 153 const context = React.useMemo(() => ({close}), [close]) 140 154 ··· 163 177 backdropComponent={Backdrop} 164 178 handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 165 179 handleStyle={{display: 'none'}} 166 - onClose={onCloseInner}> 180 + onClose={onCloseAnimationComplete}> 167 181 <Context.Provider value={context}> 168 182 <View 169 183 style={[
+27 -28
src/components/Dialog/index.web.tsx
··· 33 33 const t = useTheme() 34 34 const {gtMobile} = useBreakpoints() 35 35 const [isOpen, setIsOpen] = React.useState(false) 36 - const [isVisible, setIsVisible] = React.useState(true) 37 36 const {setDialogIsOpen} = useDialogStateControlContext() 38 37 39 38 const open = React.useCallback(() => { 40 - setIsOpen(true) 41 39 setDialogIsOpen(control.id, true) 40 + setIsOpen(true) 42 41 }, [setIsOpen, setDialogIsOpen, control.id]) 43 42 44 - const onCloseInner = React.useCallback(async () => { 45 - setIsVisible(false) 46 - await new Promise(resolve => setTimeout(resolve, 150)) 47 - setIsOpen(false) 48 - setIsVisible(true) 49 - setDialogIsOpen(control.id, false) 50 - onClose?.() 51 - }, [control.id, onClose, setDialogIsOpen]) 52 - 53 43 const close = React.useCallback<DialogControlProps['close']>( 54 44 cb => { 45 + setDialogIsOpen(control.id, false) 46 + setIsOpen(false) 47 + 55 48 try { 56 49 if (cb && typeof cb === 'function') { 57 - cb() 50 + // This timeout ensures that the callback runs at the same time as it would on native. I.e. 51 + // console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2') 52 + // This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output 53 + // 'Step 1', 'Step 3', 'Step 2'. 54 + setTimeout(cb) 58 55 } 59 56 } catch (e: any) { 60 57 logger.error(`Dialog closeCallback failed`, { 61 58 message: e.message, 62 59 }) 63 - } finally { 64 - onCloseInner() 65 60 } 61 + 62 + onClose?.() 66 63 }, 67 - [onCloseInner], 64 + [control.id, onClose, setDialogIsOpen], 68 65 ) 69 66 67 + const handleBackgroundPress = React.useCallback(async () => { 68 + close() 69 + }, [close]) 70 + 70 71 useImperativeHandle( 71 72 control.ref, 72 73 () => ({ ··· 103 104 <TouchableWithoutFeedback 104 105 accessibilityHint={undefined} 105 106 accessibilityLabel={_(msg`Close active dialog`)} 106 - onPress={onCloseInner}> 107 + onPress={handleBackgroundPress}> 107 108 <View 108 109 style={[ 109 110 web(a.fixed), ··· 113 114 gtMobile ? a.p_lg : a.p_md, 114 115 {overflowY: 'auto'}, 115 116 ]}> 116 - {isVisible && ( 117 - <Animated.View 118 - entering={FadeIn.duration(150)} 119 - // exiting={FadeOut.duration(150)} 120 - style={[ 121 - web(a.fixed), 122 - a.inset_0, 123 - {opacity: 0.8, backgroundColor: t.palette.black}, 124 - ]} 125 - /> 126 - )} 117 + <Animated.View 118 + entering={FadeIn.duration(150)} 119 + // exiting={FadeOut.duration(150)} 120 + style={[ 121 + web(a.fixed), 122 + a.inset_0, 123 + {opacity: 0.8, backgroundColor: t.palette.black}, 124 + ]} 125 + /> 127 126 128 127 <View 129 128 style={[ ··· 135 134 minHeight: web('calc(90vh - 36px)') || undefined, 136 135 }, 137 136 ]}> 138 - {isVisible ? children : null} 137 + {children} 139 138 </View> 140 139 </View> 141 140 </TouchableWithoutFeedback>
+14
src/components/Prompt.tsx
··· 123 123 cta, 124 124 testID, 125 125 }: { 126 + /** 127 + * Callback to run when the action is pressed. The method is called _after_ 128 + * the dialog closes. 129 + * 130 + * Note: The dialog will close automatically when the action is pressed, you 131 + * should NOT close the dialog as a side effect of this method. 132 + */ 126 133 onPress: () => void 127 134 color?: ButtonColor 128 135 /** ··· 165 172 description: string 166 173 cancelButtonCta?: string 167 174 confirmButtonCta?: string 175 + /** 176 + * Callback to run when the Confirm button is pressed. The method is called 177 + * _after_ the dialog closes. 178 + * 179 + * Note: The dialog will close automatically when the action is pressed, you 180 + * should NOT close the dialog as a side effect of this method. 181 + */ 168 182 onConfirm: () => void 169 183 confirmButtonColor?: ButtonColor 170 184 }>) {
+1 -3
src/view/com/composer/Composer.tsx
··· 507 507 control={discardPromptControl} 508 508 title={_(msg`Discard draft?`)} 509 509 description={_(msg`Are you sure you'd like to discard this draft?`)} 510 - onConfirm={() => { 511 - discardPromptControl.close(onClose) 512 - }} 510 + onConfirm={onClose} 513 511 confirmButtonCta={_(msg`Discard`)} 514 512 confirmButtonColor="negative" 515 513 />
+136 -1
src/view/screens/Storybook/Dialogs.tsx
··· 6 6 import {Button, ButtonText} from '#/components/Button' 7 7 import * as Dialog from '#/components/Dialog' 8 8 import * as Prompt from '#/components/Prompt' 9 - import {H3, P} from '#/components/Typography' 9 + import {H3, P, Text} from '#/components/Typography' 10 10 11 11 export function Dialogs() { 12 12 const scrollable = Dialog.useDialogControl() 13 13 const basic = Dialog.useDialogControl() 14 14 const prompt = Prompt.usePromptControl() 15 + const testDialog = Dialog.useDialogControl() 15 16 const {closeAllDialogs} = useDialogStateControlContext() 16 17 17 18 return ( ··· 60 61 <ButtonText>Open prompt</ButtonText> 61 62 </Button> 62 63 64 + <Button 65 + variant="solid" 66 + color="primary" 67 + size="small" 68 + onPress={testDialog.open} 69 + label="one"> 70 + <ButtonText>Open Tester</ButtonText> 71 + </Button> 72 + 63 73 <Prompt.Outer control={prompt}> 64 74 <Prompt.TitleText>This is a prompt</Prompt.TitleText> 65 75 <Prompt.DescriptionText> ··· 119 129 <ButtonText>Close dialog</ButtonText> 120 130 </Button> 121 131 </View> 132 + </View> 133 + </Dialog.ScrollableInner> 134 + </Dialog.Outer> 135 + 136 + <Dialog.Outer control={testDialog}> 137 + <Dialog.Handle /> 138 + 139 + <Dialog.ScrollableInner 140 + accessibilityDescribedBy="dialog-description" 141 + accessibilityLabelledBy="dialog-title"> 142 + <View style={[a.relative, a.gap_md, a.w_full]}> 143 + <Text> 144 + Watch the console logs to test each of these dialog edge cases. 145 + Functionality should be consistent across both native and web. If 146 + not then *sad face* something is wrong. 147 + </Text> 148 + 149 + <Button 150 + variant="outline" 151 + color="primary" 152 + size="small" 153 + onPress={() => { 154 + testDialog.close(() => { 155 + console.log('close callback') 156 + }) 157 + }} 158 + label="Close It"> 159 + <ButtonText>Normal Use (Should just log)</ButtonText> 160 + </Button> 161 + 162 + <Button 163 + variant="outline" 164 + color="primary" 165 + size="small" 166 + onPress={() => { 167 + testDialog.close(() => { 168 + console.log('close callback') 169 + }) 170 + 171 + setTimeout(() => { 172 + testDialog.open() 173 + }, 100) 174 + }} 175 + label="Close It"> 176 + <ButtonText> 177 + Calls `.open()` in 100ms (Should log when the animation switches 178 + to open) 179 + </ButtonText> 180 + </Button> 181 + 182 + <Button 183 + variant="outline" 184 + color="primary" 185 + size="small" 186 + onPress={() => { 187 + setTimeout(() => { 188 + testDialog.open() 189 + }, 2e3) 190 + 191 + testDialog.close(() => { 192 + console.log('close callback') 193 + }) 194 + }} 195 + label="Close It"> 196 + <ButtonText> 197 + Calls `.open()` in 2000ms (Should log after close animation and 198 + not log on open) 199 + </ButtonText> 200 + </Button> 201 + 202 + <Button 203 + variant="outline" 204 + color="primary" 205 + size="small" 206 + onPress={() => { 207 + testDialog.close(() => { 208 + console.log('close callback') 209 + }) 210 + setTimeout(() => { 211 + testDialog.close(() => { 212 + console.log('close callback after 100ms') 213 + }) 214 + }, 100) 215 + }} 216 + label="Close It"> 217 + <ButtonText> 218 + Calls `.close()` then again in 100ms (should log twice) 219 + </ButtonText> 220 + </Button> 221 + 222 + <Button 223 + variant="outline" 224 + color="primary" 225 + size="small" 226 + onPress={() => { 227 + testDialog.close(() => { 228 + console.log('close callback') 229 + }) 230 + testDialog.close(() => { 231 + console.log('close callback 2') 232 + }) 233 + }} 234 + label="Close It"> 235 + <ButtonText> 236 + Call `close()` twice immediately (should just log twice) 237 + </ButtonText> 238 + </Button> 239 + 240 + <Button 241 + variant="outline" 242 + color="primary" 243 + size="small" 244 + onPress={() => { 245 + console.log('Step 1') 246 + testDialog.close(() => { 247 + console.log('Step 3') 248 + }) 249 + console.log('Step 2') 250 + }} 251 + label="Close It"> 252 + <ButtonText> 253 + Log before `close()`, after `close()` and in the `close()` 254 + callback. Should be an order of 1 2 3 255 + </ButtonText> 256 + </Button> 122 257 </View> 123 258 </Dialog.ScrollableInner> 124 259 </Dialog.Outer>