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 365 lines 8.5 kB view raw
1import {cloneElement, Fragment, isValidElement, useMemo} from 'react' 2import { 3 Pressable, 4 type StyleProp, 5 type TextStyle, 6 View, 7 type ViewStyle, 8} from 'react-native' 9import {msg} from '@lingui/core/macro' 10import {useLingui} from '@lingui/react' 11import {Trans} from '@lingui/react/macro' 12import flattenReactChildren from 'react-keyed-flatten-children' 13 14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 15import {atoms as a, useTheme} from '#/alf' 16import {Button, ButtonText} from '#/components/Button' 17import * as Dialog from '#/components/Dialog' 18import {useInteractionState} from '#/components/hooks/useInteractionState' 19import { 20 Context, 21 ItemContext, 22 useMenuContext, 23 useMenuItemContext, 24} from '#/components/Menu/context' 25import { 26 type ContextType, 27 type GroupProps, 28 type ItemIconProps, 29 type ItemProps, 30 type ItemTextProps, 31 type TriggerProps, 32} from '#/components/Menu/types' 33import {Text} from '#/components/Typography' 34import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env' 35 36export { 37 type DialogControlProps as MenuControlProps, 38 useDialogControl as useMenuControl, 39} from '#/components/Dialog' 40 41export {useMenuContext} 42 43export function Root({ 44 children, 45 control, 46}: React.PropsWithChildren<{ 47 control?: Dialog.DialogControlProps 48}>) { 49 const defaultControl = Dialog.useDialogControl() 50 const context = useMemo<ContextType>( 51 () => ({ 52 control: control || defaultControl, 53 }), 54 [control, defaultControl], 55 ) 56 57 return <Context.Provider value={context}>{children}</Context.Provider> 58} 59 60export function Trigger({ 61 children, 62 label, 63 role = 'button', 64 hint, 65}: TriggerProps) { 66 const context = useMenuContext() 67 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 68 const { 69 state: pressed, 70 onIn: onPressIn, 71 onOut: onPressOut, 72 } = useInteractionState() 73 74 return children({ 75 IS_NATIVE: true, 76 control: context.control, 77 state: { 78 hovered: false, 79 focused, 80 pressed, 81 }, 82 props: { 83 ref: null, 84 onPress: context.control.open, 85 onFocus, 86 onBlur, 87 onPressIn, 88 onPressOut, 89 accessibilityHint: hint, 90 accessibilityLabel: label, 91 accessibilityRole: role, 92 }, 93 }) 94} 95 96export function Outer({ 97 children, 98 showCancel, 99}: React.PropsWithChildren<{ 100 showCancel?: boolean 101 style?: StyleProp<ViewStyle> 102}>) { 103 const context = useMenuContext() 104 const {_} = useLingui() 105 106 return ( 107 <Dialog.Outer 108 control={context.control} 109 nativeOptions={{preventExpansion: true}}> 110 <Dialog.Handle /> 111 {/* Re-wrap with context since Dialogs are portal-ed to root */} 112 <Context.Provider value={context}> 113 <Dialog.ScrollableInner label={_(msg`Menu`)}> 114 <View style={[a.gap_lg]}> 115 {children} 116 {IS_NATIVE && showCancel && <Cancel />} 117 </View> 118 </Dialog.ScrollableInner> 119 </Context.Provider> 120 </Dialog.Outer> 121 ) 122} 123 124export function Item({children, label, style, onPress, ...rest}: ItemProps) { 125 const t = useTheme() 126 const context = useMenuContext() 127 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 128 const { 129 state: pressed, 130 onIn: onPressIn, 131 onOut: onPressOut, 132 } = useInteractionState() 133 134 return ( 135 <Pressable 136 {...rest} 137 accessibilityHint="" 138 accessibilityLabel={label} 139 onFocus={onFocus} 140 onBlur={onBlur} 141 onPress={async e => { 142 if (IS_ANDROID) { 143 /** 144 * Below fix for iOS doesn't work for Android, this does. 145 */ 146 onPress?.(e) 147 context.control.close() 148 } else if (IS_IOS) { 149 /** 150 * Fixes a subtle bug on iOS 151 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127} 152 */ 153 context.control.close(() => { 154 onPress?.(e) 155 }) 156 } 157 }} 158 onPressIn={e => { 159 onPressIn() 160 rest.onPressIn?.(e) 161 }} 162 onPressOut={e => { 163 onPressOut() 164 rest.onPressOut?.(e) 165 }} 166 style={[ 167 a.flex_row, 168 a.align_center, 169 a.gap_sm, 170 a.px_md, 171 a.rounded_md, 172 a.overflow_hidden, 173 a.border, 174 t.atoms.bg_contrast_25, 175 t.atoms.border_contrast_low, 176 {minHeight: 44, paddingVertical: 10}, 177 style, 178 (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], 179 ]}> 180 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 181 {children} 182 </ItemContext.Provider> 183 </Pressable> 184 ) 185} 186 187export function ItemText({children, style}: ItemTextProps) { 188 const t = useTheme() 189 const {disabled} = useMenuItemContext() 190 return ( 191 <Text 192 numberOfLines={1} 193 ellipsizeMode="middle" 194 style={[ 195 a.flex_1, 196 a.text_md, 197 a.font_semi_bold, 198 t.atoms.text_contrast_high, 199 style, 200 disabled && t.atoms.text_contrast_low, 201 ]}> 202 {children} 203 </Text> 204 ) 205} 206 207export function ItemIcon({icon: Comp, fill}: ItemIconProps) { 208 const t = useTheme() 209 const {disabled} = useMenuItemContext() 210 return ( 211 <Comp 212 size="lg" 213 fill={ 214 fill 215 ? fill({disabled}) 216 : disabled 217 ? t.atoms.text_contrast_low.color 218 : t.atoms.text_contrast_medium.color 219 } 220 /> 221 ) 222} 223 224export function ItemRadio({selected}: {selected: boolean}) { 225 const t = useTheme() 226 const enableSquareButtons = useEnableSquareButtons() 227 return ( 228 <View 229 style={[ 230 a.justify_center, 231 a.align_center, 232 enableSquareButtons ? a.rounded_sm : a.rounded_full, 233 t.atoms.border_contrast_high, 234 { 235 borderWidth: 1, 236 height: 20, 237 width: 20, 238 }, 239 ]}> 240 {selected ? ( 241 <View 242 style={[ 243 a.absolute, 244 enableSquareButtons ? a.rounded_sm : a.rounded_full, 245 {height: 14, width: 14}, 246 selected 247 ? { 248 backgroundColor: t.palette.primary_500, 249 } 250 : {}, 251 ]} 252 /> 253 ) : null} 254 </View> 255 ) 256} 257 258/** 259 * NATIVE ONLY - for adding non-pressable items to the menu 260 * 261 * @platform ios, android 262 */ 263export function ContainerItem({ 264 children, 265 style, 266}: { 267 children: React.ReactNode 268 style?: StyleProp<ViewStyle> 269}) { 270 const t = useTheme() 271 return ( 272 <View 273 style={[ 274 a.flex_row, 275 a.align_center, 276 a.gap_sm, 277 a.px_md, 278 a.rounded_lg, 279 a.curve_continuous, 280 a.border, 281 t.atoms.bg_contrast_25, 282 t.atoms.border_contrast_low, 283 {paddingVertical: 10}, 284 style, 285 ]}> 286 {children} 287 </View> 288 ) 289} 290 291export function LabelText({ 292 children, 293 style, 294}: { 295 children: React.ReactNode 296 style?: StyleProp<TextStyle> 297}) { 298 const t = useTheme() 299 return ( 300 <Text 301 style={[ 302 a.font_semi_bold, 303 t.atoms.text_contrast_medium, 304 {marginBottom: -8}, 305 style, 306 ]}> 307 {children} 308 </Text> 309 ) 310} 311 312export function Group({children, style}: GroupProps) { 313 const t = useTheme() 314 return ( 315 <View 316 style={[ 317 a.rounded_lg, 318 a.curve_continuous, 319 a.overflow_hidden, 320 a.border, 321 t.atoms.border_contrast_low, 322 style, 323 ]}> 324 {flattenReactChildren(children).map((child, i) => { 325 return isValidElement(child) && 326 (child.type === Item || child.type === ContainerItem) ? ( 327 <Fragment key={i}> 328 {i > 0 ? ( 329 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 330 ) : null} 331 {cloneElement(child, { 332 // @ts-expect-error cloneElement is not aware of the types 333 style: { 334 borderRadius: 0, 335 borderWidth: 0, 336 }, 337 })} 338 </Fragment> 339 ) : null 340 })} 341 </View> 342 ) 343} 344 345function Cancel() { 346 const {_} = useLingui() 347 const context = useMenuContext() 348 349 return ( 350 <Button 351 label={_(msg`Close this dialog`)} 352 size="small" 353 variant="ghost" 354 color="secondary" 355 onPress={() => context.control.close()}> 356 <ButtonText> 357 <Trans>Cancel</Trans> 358 </ButtonText> 359 </Button> 360 ) 361} 362 363export function Divider() { 364 return null 365}