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

Configure Feed

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

at main 311 lines 8.6 kB view raw
1import {createContext, useContext, useMemo} from 'react' 2import {type GestureResponderEvent, View} from 'react-native' 3 4import {atoms as a, select, useAlf, useTheme} from '#/alf' 5import { 6 Button, 7 type ButtonProps, 8 type UninheritableButtonProps, 9} from '#/components/Button' 10import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' 11import { 12 CircleInfo_Stroke2_Corner0_Rounded as CircleInfo, 13 CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon, 14} from '#/components/icons/CircleInfo' 15import {type Props as SVGIconProps} from '#/components/icons/common' 16import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 17import {dismiss} from '#/components/Toast/sonner' 18import {type ToastType} from '#/components/Toast/types' 19import {Text as BaseText} from '#/components/Typography' 20 21export const ICONS = { 22 default: CircleCheck, 23 success: CircleCheck, 24 error: ErrorIcon, 25 warning: WarningIcon, 26 info: CircleInfo, 27} 28 29const ToastConfigContext = createContext<{ 30 id: string 31 type: ToastType 32}>({ 33 id: '', 34 type: 'default', 35}) 36ToastConfigContext.displayName = 'ToastConfigContext' 37 38export function ToastConfigProvider({ 39 children, 40 id, 41 type, 42}: { 43 children: React.ReactNode 44 id: string 45 type: ToastType 46}) { 47 return ( 48 <ToastConfigContext.Provider 49 value={useMemo(() => ({id, type}), [id, type])}> 50 {children} 51 </ToastConfigContext.Provider> 52 ) 53} 54 55export function Outer({children}: {children: React.ReactNode}) { 56 const t = useTheme() 57 const {type} = useContext(ToastConfigContext) 58 const styles = useToastStyles({type}) 59 60 return ( 61 <View 62 style={[ 63 a.flex_1, 64 a.p_lg, 65 a.rounded_md, 66 a.border, 67 a.flex_row, 68 a.gap_sm, 69 t.atoms.shadow_sm, 70 { 71 paddingVertical: 14, // 16 seems too big 72 backgroundColor: styles.backgroundColor, 73 borderColor: styles.borderColor, 74 }, 75 ]}> 76 {children} 77 </View> 78 ) 79} 80 81export function Icon({icon}: {icon?: React.ComponentType<SVGIconProps>}) { 82 const {type} = useContext(ToastConfigContext) 83 const styles = useToastStyles({type}) 84 const IconComponent = icon || ICONS[type] 85 return <IconComponent size="md" fill={styles.iconColor} /> 86} 87 88export function Text({children}: {children: React.ReactNode}) { 89 const {type} = useContext(ToastConfigContext) 90 const {textColor} = useToastStyles({type}) 91 const {fontScaleCompensation} = useToastFontScaleCompensation() 92 return ( 93 <View 94 style={[ 95 a.flex_1, 96 a.pr_lg, 97 { 98 top: fontScaleCompensation, 99 }, 100 ]}> 101 <BaseText 102 selectable={false} 103 style={[ 104 a.text_md, 105 a.font_medium, 106 a.leading_snug, 107 a.pointer_events_none, 108 { 109 color: textColor, 110 }, 111 ]}> 112 {children} 113 </BaseText> 114 </View> 115 ) 116} 117 118export function Action( 119 props: Omit<ButtonProps, UninheritableButtonProps | 'children'> & { 120 children: React.ReactNode 121 }, 122) { 123 const t = useTheme() 124 const {fontScaleCompensation} = useToastFontScaleCompensation() 125 const {type} = useContext(ToastConfigContext) 126 const {id} = useContext(ToastConfigContext) 127 const styles = useMemo(() => { 128 const base = { 129 base: { 130 textColor: t.palette.contrast_600, 131 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 132 }, 133 interacted: { 134 textColor: t.atoms.text.color, 135 backgroundColor: t.atoms.bg_contrast_50.backgroundColor, 136 }, 137 } 138 return { 139 default: base, 140 success: { 141 base: { 142 textColor: select(t.name, { 143 light: t.palette.primary_800, 144 dim: t.palette.primary_900, 145 dark: t.palette.primary_900, 146 }), 147 backgroundColor: t.palette.primary_25, 148 }, 149 interacted: { 150 textColor: select(t.name, { 151 light: t.palette.primary_900, 152 dim: t.palette.primary_975, 153 dark: t.palette.primary_975, 154 }), 155 backgroundColor: t.palette.primary_50, 156 }, 157 }, 158 error: { 159 base: { 160 textColor: select(t.name, { 161 light: t.palette.negative_700, 162 dim: t.palette.negative_900, 163 dark: t.palette.negative_900, 164 }), 165 backgroundColor: t.palette.negative_25, 166 }, 167 interacted: { 168 textColor: select(t.name, { 169 light: t.palette.negative_900, 170 dim: t.palette.negative_975, 171 dark: t.palette.negative_975, 172 }), 173 backgroundColor: t.palette.negative_50, 174 }, 175 }, 176 warning: base, 177 info: base, 178 }[type] 179 }, [t, type]) 180 181 const onPress = (e: GestureResponderEvent) => { 182 console.log('Toast Action pressed, dismissing toast', id) 183 dismiss(id) 184 props.onPress?.(e) 185 } 186 187 return ( 188 <View style={{top: fontScaleCompensation}}> 189 <Button {...props} onPress={onPress}> 190 {s => { 191 const interacted = s.pressed || s.hovered || s.focused 192 return ( 193 <> 194 <View 195 style={[ 196 a.absolute, 197 a.curve_continuous, 198 { 199 // tiny button styles 200 top: -5, 201 bottom: -5, 202 left: -9, 203 right: -9, 204 borderRadius: 6, 205 backgroundColor: interacted 206 ? styles.interacted.backgroundColor 207 : styles.base.backgroundColor, 208 }, 209 ]} 210 /> 211 <BaseText 212 style={[ 213 a.text_md, 214 a.font_medium, 215 a.leading_snug, 216 { 217 color: interacted 218 ? styles.interacted.textColor 219 : styles.base.textColor, 220 }, 221 ]}> 222 {props.children} 223 </BaseText> 224 </> 225 ) 226 }} 227 </Button> 228 </View> 229 ) 230} 231 232/** 233 * Vibes-based number, provides t `top` value to wrap the text to compensate 234 * for different type sizes and keep the first line of text aligned with the 235 * icon. - esb 236 */ 237function useToastFontScaleCompensation() { 238 const {fonts} = useAlf() 239 const fontScaleCompensation = useMemo( 240 () => parseInt(fonts.scale) * -1 * 0.65, 241 [fonts.scale], 242 ) 243 return useMemo( 244 () => ({ 245 fontScaleCompensation, 246 }), 247 [fontScaleCompensation], 248 ) 249} 250 251function useToastStyles({type}: {type: ToastType}) { 252 const t = useTheme() 253 return useMemo(() => { 254 return { 255 default: { 256 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 257 borderColor: t.atoms.border_contrast_low.borderColor, 258 iconColor: t.atoms.text.color, 259 textColor: t.atoms.text.color, 260 }, 261 success: { 262 backgroundColor: t.palette.primary_25, 263 borderColor: select(t.name, { 264 light: t.palette.primary_300, 265 dim: t.palette.primary_200, 266 dark: t.palette.primary_100, 267 }), 268 iconColor: select(t.name, { 269 light: t.palette.primary_600, 270 dim: t.palette.primary_700, 271 dark: t.palette.primary_700, 272 }), 273 textColor: select(t.name, { 274 light: t.palette.primary_600, 275 dim: t.palette.primary_700, 276 dark: t.palette.primary_700, 277 }), 278 }, 279 error: { 280 backgroundColor: t.palette.negative_25, 281 borderColor: select(t.name, { 282 light: t.palette.negative_200, 283 dim: t.palette.negative_200, 284 dark: t.palette.negative_100, 285 }), 286 iconColor: select(t.name, { 287 light: t.palette.negative_700, 288 dim: t.palette.negative_900, 289 dark: t.palette.negative_900, 290 }), 291 textColor: select(t.name, { 292 light: t.palette.negative_700, 293 dim: t.palette.negative_900, 294 dark: t.palette.negative_900, 295 }), 296 }, 297 warning: { 298 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 299 borderColor: t.atoms.border_contrast_low.borderColor, 300 iconColor: t.atoms.text.color, 301 textColor: t.atoms.text.color, 302 }, 303 info: { 304 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 305 borderColor: t.atoms.border_contrast_low.borderColor, 306 iconColor: t.atoms.text.color, 307 textColor: t.atoms.text.color, 308 }, 309 }[type] 310 }, [t, type]) 311}