Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

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