Bluesky app fork with some witchin' additions 💫

Progress on desktoip

+331 -46
+15 -3
src/alf/index.tsx
··· 18 18 export const Context = React.createContext<{ 19 19 themeName: ThemeName 20 20 theme: Theme 21 + themes: ReturnType<typeof createThemes> 21 22 }>({ 22 23 themeName: 'light', 23 24 theme: defaultTheme, 25 + themes: createThemes({ 26 + hues: { 27 + primary: BLUE_HUE, 28 + negative: RED_HUE, 29 + positive: GREEN_HUE, 30 + }, 31 + }), 24 32 }) 25 33 26 34 export function ThemeProvider({ ··· 42 50 <Context.Provider 43 51 value={React.useMemo( 44 52 () => ({ 53 + themes, 45 54 themeName: themeName, 46 55 theme: theme, 47 56 }), 48 - [theme, themeName], 57 + [theme, themeName, themes], 49 58 )}> 50 59 {children} 51 60 </Context.Provider> 52 61 ) 53 62 } 54 63 55 - export function useTheme() { 56 - return React.useContext(Context).theme 64 + export function useTheme(theme?: ThemeName) { 65 + const ctx = React.useContext(Context) 66 + return React.useMemo(() => { 67 + return theme ? ctx.themes[theme] : ctx.theme 68 + }, [theme, ctx]) 57 69 } 58 70 59 71 export function useBreakpoints() {
+287 -43
src/components/dialogs/nudges/TenMillion.tsx
··· 1 1 import React from 'react' 2 - import {useLingui} from '@lingui/react' 3 - import {msg} from '@lingui/macro' 4 2 import {View} from 'react-native' 5 3 import ViewShot from 'react-native-view-shot' 4 + import {moderateProfile} from '@atproto/api' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 6 7 7 - import {atoms as a, useBreakpoints, tokens} from '#/alf' 8 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 9 + import {sanitizeHandle} from '#/lib/strings/handles' 10 + import {isNative} from '#/platform/detection' 11 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 12 + import {useProfileQuery} from '#/state/queries/profile' 13 + import {useSession} from '#/state/session' 14 + import {useComposerControls} from 'state/shell' 15 + import {formatCount} from '#/view/com/util/numeric/format' 16 + import {UserAvatar} from '#/view/com/util/UserAvatar' 17 + import {Logomark} from '#/view/icons/Logomark' 18 + import { 19 + atoms as a, 20 + ThemeProvider, 21 + tokens, 22 + useBreakpoints, 23 + useTheme, 24 + } from '#/alf' 25 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 8 26 import * as Dialog from '#/components/Dialog' 27 + import {useContext} from '#/components/dialogs/nudges' 28 + import {Divider} from '#/components/Divider' 29 + import {GradientFill} from '#/components/GradientFill' 30 + import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 31 + import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 32 + import {Loader} from '#/components/Loader' 9 33 import {Text} from '#/components/Typography' 10 - import {GradientFill} from '#/components/GradientFill' 11 - import {Button, ButtonText} from '#/components/Button' 12 - import {useComposerControls} from 'state/shell' 34 + 35 + const RATIO = 8 / 10 36 + const WIDTH = 2000 37 + const HEIGHT = WIDTH * RATIO 13 38 14 - import {useContext} from '#/components/dialogs/nudges' 39 + function getFontSize(count: number) { 40 + const length = count.toString().length 41 + if (length < 7) { 42 + return 80 43 + } else if (length < 5) { 44 + return 100 45 + } else { 46 + return 70 47 + } 48 + } 15 49 16 50 export function TenMillion() { 17 - const {_} = useLingui() 51 + const t = useTheme() 52 + const lightTheme = useTheme('light') 53 + const {_, i18n} = useLingui() 18 54 const {controls} = useContext() 19 55 const {gtMobile} = useBreakpoints() 20 56 const {openComposer} = useComposerControls() 57 + const imageRef = React.useRef<ViewShot>(null) 58 + const {currentAccount} = useSession() 59 + const {isLoading: isProfileLoading, data: profile} = useProfileQuery({ 60 + did: currentAccount!.did, 61 + }) // TODO PWI 62 + const moderationOpts = useModerationOpts() 63 + const moderation = React.useMemo(() => { 64 + return profile && moderationOpts 65 + ? moderateProfile(profile, moderationOpts) 66 + : undefined 67 + }, [profile, moderationOpts]) 21 68 22 - const imageRef = React.useRef<ViewShot>(null) 69 + const isLoading = isProfileLoading || !moderation || !profile 70 + 71 + const userNumber = 56738 23 72 24 73 const share = () => { 25 74 if (imageRef.current && imageRef.current.capture) { ··· 31 80 imageUris: [ 32 81 { 33 82 uri, 34 - width: 1000, 35 - height: 1000, 83 + width: WIDTH, 84 + height: HEIGHT, 36 85 }, 37 86 ], 38 87 }) ··· 48 97 49 98 <Dialog.ScrollableInner 50 99 label={_(msg`Ten Million`)} 51 - style={ 52 - [ 53 - // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, 54 - ] 55 - }> 100 + style={[ 101 + { 102 + padding: 0, 103 + }, 104 + // gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, 105 + ]}> 56 106 <View 57 107 style={[ 58 - a.relative, 59 - a.w_full, 108 + a.rounded_md, 60 109 a.overflow_hidden, 61 - { 62 - paddingTop: '100%', 110 + isNative && { 111 + borderTopLeftRadius: 40, 112 + borderTopRightRadius: 40, 63 113 }, 64 114 ]}> 65 - <ViewShot 66 - ref={imageRef} 67 - options={{width: 2e3, height: 2e3}} 68 - style={[a.absolute, a.inset_0]}> 115 + <ThemeProvider theme="light"> 69 116 <View 70 117 style={[ 71 - a.absolute, 72 - a.inset_0, 73 - a.align_center, 74 - a.justify_center, 118 + a.relative, 119 + a.w_full, 120 + a.overflow_hidden, 75 121 { 76 - top: -1, 77 - bottom: -1, 78 - left: -1, 79 - right: -1, 122 + paddingTop: '80%', 80 123 }, 81 124 ]}> 82 - <GradientFill gradient={tokens.gradients.midnight} /> 125 + <ViewShot 126 + ref={imageRef} 127 + options={{width: WIDTH, height: HEIGHT}} 128 + style={[a.absolute, a.inset_0]}> 129 + <View 130 + style={[ 131 + a.absolute, 132 + a.inset_0, 133 + a.align_center, 134 + a.justify_center, 135 + { 136 + top: -1, 137 + bottom: -1, 138 + left: -1, 139 + right: -1, 140 + paddingVertical: 32, 141 + paddingHorizontal: 48, 142 + }, 143 + ]}> 144 + <GradientFill gradient={tokens.gradients.bonfire} /> 83 145 84 - <Text>10 milly, babyyy</Text> 146 + {isLoading ? ( 147 + <Loader size="xl" fill="white" /> 148 + ) : ( 149 + <View 150 + style={[ 151 + a.flex_1, 152 + a.w_full, 153 + a.align_center, 154 + a.justify_center, 155 + a.rounded_md, 156 + { 157 + backgroundColor: 'white', 158 + shadowRadius: 32, 159 + shadowOpacity: 0.1, 160 + elevation: 24, 161 + shadowColor: tokens.gradients.bonfire.values[0][1], 162 + }, 163 + ]}> 164 + <View 165 + style={[ 166 + a.absolute, 167 + a.px_xl, 168 + a.py_xl, 169 + { 170 + top: 0, 171 + left: 0, 172 + }, 173 + ]}> 174 + <Logomark fill={t.palette.primary_500} width={36} /> 175 + </View> 176 + 177 + {/* Centered content */} 178 + <View 179 + style={[ 180 + { 181 + paddingBottom: 48, 182 + }, 183 + ]}> 184 + <Text 185 + style={[ 186 + a.text_md, 187 + a.font_bold, 188 + a.text_center, 189 + a.pb_xs, 190 + lightTheme.atoms.text_contrast_medium, 191 + ]}> 192 + <Trans> 193 + Celebrating {formatCount(i18n, 10000000)} users 194 + </Trans>{' '} 195 + 🎉 196 + </Text> 197 + <Text 198 + style={[ 199 + a.relative, 200 + a.text_center, 201 + { 202 + fontStyle: 'italic', 203 + fontSize: getFontSize(userNumber), 204 + fontWeight: '900', 205 + letterSpacing: -2, 206 + }, 207 + ]}> 208 + <Text 209 + style={[ 210 + a.absolute, 211 + { 212 + color: t.palette.primary_500, 213 + fontSize: 32, 214 + left: -18, 215 + top: 8, 216 + }, 217 + ]}> 218 + # 219 + </Text> 220 + {i18n.number(userNumber)} 221 + </Text> 222 + </View> 223 + {/* End centered content */} 224 + 225 + <View 226 + style={[ 227 + a.absolute, 228 + a.px_xl, 229 + a.py_xl, 230 + { 231 + bottom: 0, 232 + left: 0, 233 + right: 0, 234 + }, 235 + ]}> 236 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 237 + <UserAvatar 238 + size={36} 239 + avatar={profile.avatar} 240 + moderation={moderation.ui('avatar')} 241 + /> 242 + <View style={[a.gap_2xs, a.flex_1]}> 243 + <Text style={[a.text_sm, a.font_bold]}> 244 + {sanitizeDisplayName( 245 + profile.displayName || 246 + sanitizeHandle(profile.handle), 247 + moderation.ui('displayName'), 248 + )} 249 + </Text> 250 + <View style={[a.flex_row, a.justify_between]}> 251 + <Text 252 + style={[ 253 + a.text_sm, 254 + a.font_semibold, 255 + lightTheme.atoms.text_contrast_medium, 256 + ]}> 257 + {sanitizeHandle(profile.handle, '@')} 258 + </Text> 259 + 260 + {profile.createdAt && ( 261 + <Text 262 + style={[ 263 + a.text_sm, 264 + a.font_semibold, 265 + lightTheme.atoms.text_contrast_low, 266 + ]}> 267 + {i18n.date(profile.createdAt, { 268 + dateStyle: 'long', 269 + })} 270 + </Text> 271 + )} 272 + </View> 273 + </View> 274 + </View> 275 + </View> 276 + </View> 277 + )} 278 + </View> 279 + </ViewShot> 85 280 </View> 86 - </ViewShot> 281 + </ThemeProvider> 282 + 283 + <View style={[gtMobile ? a.p_2xl : a.p_xl]}> 284 + <Text 285 + style={[ 286 + a.text_5xl, 287 + a.pb_lg, 288 + { 289 + fontWeight: '900', 290 + }, 291 + ]}> 292 + You're part of the next wave of the internet. 293 + </Text> 294 + 295 + <Text style={[a.leading_snug, a.text_lg, a.pb_xl]}> 296 + Online culture is too important to be controlled by a few 297 + corporations.{' '} 298 + <Text style={[a.leading_snug, a.text_lg, a.italic]}> 299 + We’re dedicated to building an open foundation for the social 300 + internet so that we can all shape its future. 301 + </Text> 302 + </Text> 303 + 304 + <Divider /> 305 + 306 + <View 307 + style={[ 308 + a.flex_row, 309 + a.align_center, 310 + a.justify_end, 311 + a.gap_md, 312 + a.pt_xl, 313 + ]}> 314 + <Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}> 315 + Brag a little ;) 316 + </Text> 317 + 318 + <Button 319 + label={_(msg`Share image externally`)} 320 + size="large" 321 + variant="solid" 322 + color="secondary" 323 + shape="square" 324 + onPress={share}> 325 + <ButtonIcon icon={Share} /> 326 + </Button> 327 + <Button 328 + label={_(msg`Share image in post`)} 329 + size="large" 330 + variant="solid" 331 + color="primary" 332 + onPress={share}> 333 + <ButtonText>{_(msg`Share post`)}</ButtonText> 334 + <ButtonIcon position="right" icon={ImageIcon} /> 335 + </Button> 336 + </View> 337 + </View> 87 338 </View> 88 339 89 - <Button 90 - label={_(msg`Generate`)} 91 - size="medium" 92 - variant="solid" 93 - color="primary" 94 - onPress={share}> 95 - <ButtonText>{_(msg`Generate`)}</ButtonText> 96 - </Button> 340 + <Dialog.Close /> 97 341 </Dialog.ScrollableInner> 98 342 </Dialog.Outer> 99 343 )
+29
src/view/icons/Logomark.tsx
··· 1 + import React from 'react' 2 + import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' 3 + 4 + import {usePalette} from '#/lib/hooks/usePalette' 5 + 6 + const ratio = 54 / 61 7 + 8 + export function Logomark({ 9 + fill, 10 + ...rest 11 + }: {fill?: PathProps['fill']} & SvgProps) { 12 + const pal = usePalette('default') 13 + // @ts-ignore it's fiiiiine 14 + const size = parseInt(rest.width || 32) 15 + 16 + return ( 17 + <Svg 18 + fill="none" 19 + viewBox="0 0 61 54" 20 + {...rest} 21 + width={size} 22 + height={Number(size) * ratio}> 23 + <Path 24 + fill={fill || pal.text.color} 25 + d="M13.223 3.602C20.215 8.832 27.738 19.439 30.5 25.13c2.762-5.691 10.284-16.297 17.278-21.528C52.824-.172 61-3.093 61 6.2c0 1.856-1.068 15.59-1.694 17.82-2.178 7.752-10.112 9.73-17.17 8.532 12.337 2.092 15.475 9.021 8.697 15.95-12.872 13.159-18.5-3.302-19.943-7.52-.264-.773-.388-1.135-.39-.827-.002-.308-.126.054-.39.827-1.442 4.218-7.071 20.679-19.943 7.52-6.778-6.929-3.64-13.858 8.697-15.95-7.058 1.197-14.992-.78-17.17-8.532C1.068 21.79 0 8.056 0 6.2 0-3.093 8.176-.172 13.223 3.602Z" 26 + /> 27 + </Svg> 28 + ) 29 + }