Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 2d8e855e25e8722ee37c90a5413acd7a36d43318 292 lines 7.2 kB view raw
1import { 2 createContext, 3 useCallback, 4 useContext, 5 useLayoutEffect, 6 useMemo, 7 useState, 8} from 'react' 9import {View} from 'react-native' 10import {msg, Trans} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12 13import {useTheme} from '#/alf' 14import {atoms as a} from '#/alf' 15import {Button, ButtonIcon, ButtonText} from '#/components/Button' 16import * as Dialog from '#/components/Dialog' 17import {useInteractionState} from '#/components/hooks/useInteractionState' 18import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 19import {ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon} from '#/components/icons/Chevron' 20import {Text} from '#/components/Typography' 21import { 22 type ContentProps, 23 type IconProps, 24 type ItemIndicatorProps, 25 type ItemProps, 26 type ItemTextProps, 27 type RootProps, 28 type TriggerProps, 29 type ValueProps, 30} from './types' 31 32type ContextType = { 33 control: Dialog.DialogControlProps 34} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'> 35 36const Context = createContext<ContextType | null>(null) 37Context.displayName = 'SelectContext' 38 39const ValueTextContext = createContext< 40 [any, React.Dispatch<React.SetStateAction<any>>] 41>([undefined, () => {}]) 42ValueTextContext.displayName = 'ValueTextContext' 43 44function useSelectContext() { 45 const ctx = useContext(Context) 46 if (!ctx) { 47 throw new Error('Select components must must be used within a Select.Root') 48 } 49 return ctx 50} 51 52export function Root({children, value, onValueChange, disabled}: RootProps) { 53 const control = Dialog.useDialogControl() 54 const valueTextCtx = useState<any>() 55 56 const ctx = useMemo( 57 () => ({ 58 control, 59 value, 60 onValueChange, 61 disabled, 62 }), 63 [control, value, onValueChange, disabled], 64 ) 65 return ( 66 <Context.Provider value={ctx}> 67 <ValueTextContext.Provider value={valueTextCtx}> 68 {children} 69 </ValueTextContext.Provider> 70 </Context.Provider> 71 ) 72} 73 74export function Trigger({children, label}: TriggerProps) { 75 const {control} = useSelectContext() 76 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 77 const { 78 state: pressed, 79 onIn: onPressIn, 80 onOut: onPressOut, 81 } = useInteractionState() 82 83 if (typeof children === 'function') { 84 return children({ 85 isNative: true, 86 control, 87 state: { 88 hovered: false, 89 focused, 90 pressed, 91 }, 92 props: { 93 onPress: control.open, 94 onFocus, 95 onBlur, 96 onPressIn, 97 onPressOut, 98 accessibilityLabel: label, 99 }, 100 }) 101 } else { 102 return ( 103 <Button 104 label={label} 105 onPress={control.open} 106 style={[a.flex_1, a.justify_between]} 107 color="secondary" 108 size="small" 109 shape="rectangular"> 110 <>{children}</> 111 </Button> 112 ) 113 } 114} 115 116export function ValueText({ 117 placeholder, 118 children = value => value.label, 119 style, 120}: ValueProps) { 121 const [value] = useContext(ValueTextContext) 122 const t = useTheme() 123 124 let text = value && children(value) 125 if (typeof text !== 'string') text = placeholder 126 127 return ( 128 <ButtonText style={[t.atoms.text, a.font_normal, style]}>{text}</ButtonText> 129 ) 130} 131 132export function Icon({}: IconProps) { 133 return <ButtonIcon icon={ChevronUpDownIcon} /> 134} 135 136export function Content<T>({ 137 items, 138 valueExtractor = defaultItemValueExtractor, 139 ...props 140}: ContentProps<T>) { 141 const {control, ...context} = useSelectContext() 142 const [, setValue] = useContext(ValueTextContext) 143 144 useLayoutEffect(() => { 145 const item = items.find(item => valueExtractor(item) === context.value) 146 if (item) { 147 setValue(item) 148 } 149 }, [items, context.value, valueExtractor, setValue]) 150 151 return ( 152 <Dialog.Outer control={control}> 153 <ContentInner 154 control={control} 155 items={items} 156 valueExtractor={valueExtractor} 157 {...props} 158 {...context} 159 /> 160 </Dialog.Outer> 161 ) 162} 163 164function ContentInner<T>({ 165 items, 166 renderItem, 167 valueExtractor, 168 ...context 169}: ContentProps<T> & ContextType) { 170 const control = Dialog.useDialogContext() 171 172 const {_} = useLingui() 173 const [headerHeight, setHeaderHeight] = useState(50) 174 175 const render = useCallback( 176 ({item, index}: {item: T; index: number}) => { 177 return renderItem(item, index, context.value) 178 }, 179 [renderItem, context.value], 180 ) 181 182 const doneButton = useCallback( 183 () => ( 184 <Button 185 label={_(msg`Done`)} 186 onPress={() => control.close()} 187 size="small" 188 color="primary" 189 variant="ghost" 190 style={[a.rounded_full]}> 191 <ButtonText style={[a.text_md]}> 192 <Trans>Done</Trans> 193 </ButtonText> 194 </Button> 195 ), 196 [control, _], 197 ) 198 199 return ( 200 <Context.Provider value={context}> 201 <Dialog.Header 202 renderRight={doneButton} 203 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)} 204 style={[a.absolute, a.top_0, a.left_0, a.right_0, a.z_10]}> 205 <Dialog.HeaderText> 206 <Trans>Select an option</Trans> 207 </Dialog.HeaderText> 208 </Dialog.Header> 209 <Dialog.InnerFlatList 210 headerOffset={headerHeight} 211 data={items} 212 renderItem={render} 213 keyExtractor={valueExtractor} 214 /> 215 </Context.Provider> 216 ) 217} 218 219function defaultItemValueExtractor(item: any) { 220 return item.value 221} 222 223const ItemContext = createContext<{ 224 selected: boolean 225 hovered: boolean 226 focused: boolean 227 pressed: boolean 228}>({ 229 selected: false, 230 hovered: false, 231 focused: false, 232 pressed: false, 233}) 234ItemContext.displayName = 'SelectItemContext' 235 236export function useItemContext() { 237 return useContext(ItemContext) 238} 239 240export function Item({children, value, label, style}: ItemProps) { 241 const t = useTheme() 242 const control = Dialog.useDialogContext() 243 const {value: selected, onValueChange} = useSelectContext() 244 245 return ( 246 <Button 247 role="listitem" 248 label={label} 249 style={[a.flex_1]} 250 onPress={() => { 251 control.close(() => { 252 onValueChange?.(value) 253 }) 254 }}> 255 {({hovered, focused, pressed}) => ( 256 <ItemContext.Provider 257 value={{selected: value === selected, hovered, focused, pressed}}> 258 <View 259 style={[ 260 a.flex_1, 261 a.pl_md, 262 (focused || pressed) && t.atoms.bg_contrast_25, 263 a.flex_row, 264 a.align_center, 265 a.gap_sm, 266 style, 267 ]}> 268 {children} 269 </View> 270 </ItemContext.Provider> 271 )} 272 </Button> 273 ) 274} 275 276export function ItemText({children}: ItemTextProps) { 277 const {selected} = useItemContext() 278 const t = useTheme() 279 280 // eslint-disable-next-line bsky-internal/avoid-unwrapped-text 281 return ( 282 <View style={[a.flex_1, a.py_md, a.border_b, t.atoms.border_contrast_low]}> 283 <Text style={[a.text_md, selected && a.font_semi_bold]}>{children}</Text> 284 </View> 285 ) 286} 287 288export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { 289 const {selected} = useItemContext() 290 291 return <View style={{width: 24}}>{selected && <Icon size="md" />}</View> 292}