Bluesky app fork with some witchin' additions 💫

fix: @mentions; loading spinner; text select theme

![](https://i.postimg.cc/MHnsb0tL/Screenshot-2026-01-20-at-16-53-33-Screenshot-20260120-164952-Witchsky-jpg-(JPEG-Image-1440-633-pix.png)
![](https://i.postimg.cc/x8XsyGgt/Screenshot-2026-01-20-at-16-53-41-Screenshot-20260120-165009-Witchsky-jpg-(JPEG-Image-973-906-pixe.png)

authored by shi.gg and committed by tangled.org cf6d7575 518c6e3d

+47 -13
+2 -1
src/components/Loader.tsx
··· 37 ]}> 38 <Icon 39 {...props} 40 - style={[a.absolute, a.inset_0, t.atoms.text_contrast_high, props.style]} 41 /> 42 </Animated.View> 43 )
··· 37 ]}> 38 <Icon 39 {...props} 40 + style={[a.absolute, a.inset_0, props.style]} 41 + color={t.palette.primary_500} 42 /> 43 </Animated.View> 44 )
+4
src/components/ProgressGuide/FollowDialog.tsx
··· 24 native, 25 useBreakpoints, 26 useTheme, 27 type ViewStyleProp, 28 web, 29 } from '#/alf' ··· 664 onChangeText={onChangeText} 665 onFocus={onFocus} 666 onBlur={onBlur} 667 style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]} 668 placeholderTextColor={t.palette.contrast_500} 669 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
··· 24 native, 25 useBreakpoints, 26 useTheme, 27 + utils, 28 type ViewStyleProp, 29 web, 30 } from '#/alf' ··· 665 onChangeText={onChangeText} 666 onFocus={onFocus} 667 onBlur={onBlur} 668 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 669 + cursorColor={t.palette.primary_500} 670 + selectionHandleColor={t.palette.primary_500} 671 style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]} 672 placeholderTextColor={t.palette.contrast_500} 673 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
+4
src/components/forms/TextField.tsx
··· 20 tokens, 21 useAlf, 22 useTheme, 23 web, 24 } from '#/alf' 25 import {useInteractionState} from '#/components/hooks/useInteractionState' ··· 275 <Component 276 accessibilityHint={undefined} 277 hitSlop={HITSLOP_20} 278 {...rest} 279 accessibilityLabel={label} 280 ref={refs}
··· 20 tokens, 21 useAlf, 22 useTheme, 23 + utils, 24 web, 25 } from '#/alf' 26 import {useInteractionState} from '#/components/hooks/useInteractionState' ··· 276 <Component 277 accessibilityHint={undefined} 278 hitSlop={HITSLOP_20} 279 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 280 + cursorColor={t.palette.primary_500} 281 + selectionHandleColor={t.palette.primary_500} 282 {...rest} 283 accessibilityLabel={label} 284 ref={refs}
+4 -1
src/screens/Messages/components/MessageInput.tsx
··· 26 import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 27 import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker' 28 import * as Toast from '#/view/com/util/Toast' 29 - import {android, atoms as a, useTheme} from '#/alf' 30 import {useSharedInputStyles} from '#/components/forms/TextField' 31 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 32 import {IS_IOS, IS_WEB} from '#/env' ··· 181 {paddingBottom: IS_IOS ? 5 : 0}, 182 animatedStyle, 183 ]} 184 keyboardAppearance={t.scheme} 185 submitBehavior="newline" 186 onFocus={() => setIsFocused(true)}
··· 26 import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 27 import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker' 28 import * as Toast from '#/view/com/util/Toast' 29 + import {android, atoms as a, useTheme, utils} from '#/alf' 30 import {useSharedInputStyles} from '#/components/forms/TextField' 31 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 32 import {IS_IOS, IS_WEB} from '#/env' ··· 181 {paddingBottom: IS_IOS ? 5 : 0}, 182 animatedStyle, 183 ]} 184 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 185 + cursorColor={t.palette.primary_500} 186 + selectionHandleColor={t.palette.primary_500} 187 keyboardAppearance={t.scheme} 188 submitBehavior="newline" 189 onFocus={() => setIsFocused(true)}
+2 -2
src/style.css
··· 108 pointer-events: none; 109 } 110 .ProseMirror .mention { 111 - color: #ed5345; 112 } 113 .ProseMirror a, 114 .ProseMirror .autolink { 115 - color: #ed5345; 116 } 117 /* OLLIE: TODO -- this is not accessible */ 118 /* Remove focus state on inputs */
··· 108 pointer-events: none; 109 } 110 .ProseMirror .mention { 111 + color: var(--mention-color, #ed5345); 112 } 113 .ProseMirror a, 114 .ProseMirror .autolink { 115 + color: var(--mention-color, #ed5345); 116 } 117 /* OLLIE: TODO -- this is not accessible */ 118 /* Remove focus state on inputs */
+4 -1
src/view/com/composer/text-input/TextInput.tsx
··· 29 type LinkFacetMatch, 30 suggestLinkCardUri, 31 } from '#/view/com/composer/text-input/text-input-util' 32 - import {atoms as a, useAlf} from '#/alf' 33 import {normalizeTextStyles} from '#/alf/typography' 34 import {IS_ANDROID, IS_NATIVE} from '#/env' 35 import {Autocomplete} from './mobile/Autocomplete' ··· 283 // Note: should be the default value, but as of v1.104 284 // it switched to "none" on Android 285 autoCapitalize="sentences" 286 {...props} 287 style={[ 288 inputTextStyle,
··· 29 type LinkFacetMatch, 30 suggestLinkCardUri, 31 } from '#/view/com/composer/text-input/text-input-util' 32 + import {atoms as a, useAlf, utils} from '#/alf' 33 import {normalizeTextStyles} from '#/alf/typography' 34 import {IS_ANDROID, IS_NATIVE} from '#/env' 35 import {Autocomplete} from './mobile/Autocomplete' ··· 283 // Note: should be the default value, but as of v1.104 284 // it switched to "none" on Android 285 autoCapitalize="sentences" 286 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 287 + cursorColor={t.palette.primary_500} 288 + selectionHandleColor={t.palette.primary_500} 289 {...props} 290 style={[ 291 inputTextStyle,
+9 -1
src/view/com/composer/text-input/TextInput.web.tsx
··· 393 394 return ( 395 <> 396 - <View style={[styles.container, hasRightPadding && styles.rightPadding]}> 397 {/* @ts-ignore inputStyle is fine */} 398 <EditorContent editor={editor} style={inputStyle} /> 399 </View>
··· 393 394 return ( 395 <> 396 + <View 397 + style={[ 398 + styles.container, 399 + hasRightPadding && styles.rightPadding, 400 + { 401 + // @ts-ignore 402 + '--mention-color': t.palette.primary_500, 403 + }, 404 + ]}> 405 {/* @ts-ignore inputStyle is fine */} 406 <EditorContent editor={editor} style={inputStyle} /> 407 </View>
+9 -3
src/view/com/modals/DeleteAccount.tsx
··· 19 import {useModalControls} from '#/state/modals' 20 import {useAgent, useSession, useSessionApi} from '#/state/session' 21 import {pdsAgent} from '#/state/session/agent' 22 - import {atoms as a, useTheme as useNewTheme} from '#/alf' 23 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 24 import {Text as NewText} from '#/components/Typography' 25 import {IS_ANDROID, IS_WEB} from '#/env' ··· 135 ) : undefined} 136 {isProcessing ? ( 137 <View style={[styles.btn, s.mt10]}> 138 - <ActivityIndicator /> 139 </View> 140 ) : ( 141 <> ··· 221 placeholder={_(msg`Confirmation code`)} 222 placeholderTextColor={pal.textLight.color} 223 keyboardAppearance={theme.colorScheme} 224 value={confirmCode} 225 onChangeText={setConfirmCode} 226 accessibilityLabelledBy="confirmationCode" ··· 240 placeholder={_(msg`Password`)} 241 placeholderTextColor={pal.textLight.color} 242 keyboardAppearance={theme.colorScheme} 243 secureTextEntry 244 value={password} 245 onChangeText={setPassword} ··· 254 ) : undefined} 255 {isProcessing ? ( 256 <View style={[styles.btn, s.mt10]}> 257 - <ActivityIndicator /> 258 </View> 259 ) : ( 260 <>
··· 19 import {useModalControls} from '#/state/modals' 20 import {useAgent, useSession, useSessionApi} from '#/state/session' 21 import {pdsAgent} from '#/state/session/agent' 22 + import {atoms as a, useTheme as useNewTheme, utils} from '#/alf' 23 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 24 import {Text as NewText} from '#/components/Typography' 25 import {IS_ANDROID, IS_WEB} from '#/env' ··· 135 ) : undefined} 136 {isProcessing ? ( 137 <View style={[styles.btn, s.mt10]}> 138 + <ActivityIndicator color={t.palette.primary_500} /> 139 </View> 140 ) : ( 141 <> ··· 221 placeholder={_(msg`Confirmation code`)} 222 placeholderTextColor={pal.textLight.color} 223 keyboardAppearance={theme.colorScheme} 224 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 225 + cursorColor={t.palette.primary_500} 226 + selectionHandleColor={t.palette.primary_500} 227 value={confirmCode} 228 onChangeText={setConfirmCode} 229 accessibilityLabelledBy="confirmationCode" ··· 243 placeholder={_(msg`Password`)} 244 placeholderTextColor={pal.textLight.color} 245 keyboardAppearance={theme.colorScheme} 246 + selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 247 + cursorColor={t.palette.primary_500} 248 + selectionHandleColor={t.palette.primary_500} 249 secureTextEntry 250 value={password} 251 onChangeText={setPassword} ··· 260 ) : undefined} 261 {isProcessing ? ( 262 <View style={[styles.btn, s.mt10]}> 263 + <ActivityIndicator color={t.palette.primary_500} /> 264 </View> 265 ) : ( 266 <>
+3 -2
src/view/com/posts/PostFeed.tsx
··· 59 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 60 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 61 import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' 62 - import {useBreakpoints, useLayoutBreakpoints} from '#/alf' 63 import { 64 AgeAssuranceDismissibleFeedBanner, 65 useInternalState as useAgeAssuranceBannerState, ··· 303 isVideoFeed?: boolean 304 useRepostCarousel?: boolean 305 }): React.ReactNode => { 306 const {_} = useLingui() 307 const queryClient = useQueryClient() 308 const {currentAccount, hasSession} = useSession() ··· 989 990 return isFetchingNextPage ? ( 991 <View style={[styles.feedFooter]}> 992 - <ActivityIndicator /> 993 <View style={{height: offset}} /> 994 </View> 995 ) : shouldRenderEndOfFeed ? (
··· 59 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 60 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 61 import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' 62 + import {useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' 63 import { 64 AgeAssuranceDismissibleFeedBanner, 65 useInternalState as useAgeAssuranceBannerState, ··· 303 isVideoFeed?: boolean 304 useRepostCarousel?: boolean 305 }): React.ReactNode => { 306 + const t = useTheme() 307 const {_} = useLingui() 308 const queryClient = useQueryClient() 309 const {currentAccount, hasSession} = useSession() ··· 990 991 return isFetchingNextPage ? ( 992 <View style={[styles.feedFooter]}> 993 + <ActivityIndicator color={t.palette.primary_500} /> 994 <View style={{height: offset}} /> 995 </View> 996 ) : shouldRenderEndOfFeed ? (
+3 -1
src/view/screens/Feeds.tsx
··· 106 107 export function FeedsScreen(_props: Props) { 108 const pal = usePalette('default') 109 const {openComposer} = useOpenComposer() 110 const {isMobile} = useWebMediaQueries() 111 const [query, setQuery] = React.useState('') ··· 417 } else if (item.type === 'popularFeedsLoadingMore') { 418 return ( 419 <View style={s.p10}> 420 - <ActivityIndicator size="large" /> 421 </View> 422 ) 423 } else if (item.type === 'savedFeedsHeader') { ··· 494 }, 495 [ 496 _, 497 pal.border, 498 pal.textLight, 499 query,
··· 106 107 export function FeedsScreen(_props: Props) { 108 const pal = usePalette('default') 109 + const t = useTheme() 110 const {openComposer} = useOpenComposer() 111 const {isMobile} = useWebMediaQueries() 112 const [query, setQuery] = React.useState('') ··· 418 } else if (item.type === 'popularFeedsLoadingMore') { 419 return ( 420 <View style={s.p10}> 421 + <ActivityIndicator size="large" color={t.palette.primary_500} /> 422 </View> 423 ) 424 } else if (item.type === 'savedFeedsHeader') { ··· 495 }, 496 [ 497 _, 498 + t.palette.primary_500, 499 pal.border, 500 pal.textLight, 501 query,
+3 -1
src/view/screens/Home.tsx
··· 35 import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' 36 import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 37 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 38 import * as Layout from '#/components/Layout' 39 import {IS_WEB} from '#/env' 40 import {useDemoMode} from '#/storage/hooks/demo-mode' ··· 46 const {currentAccount} = useSession() 47 const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = 48 usePinnedFeedsInfos() 49 50 React.useEffect(() => { 51 if (IS_WEB && !currentAccount) { ··· 91 return ( 92 <Layout.Screen> 93 <Layout.Center style={styles.loading}> 94 - <ActivityIndicator size="large" /> 95 </Layout.Center> 96 </Layout.Screen> 97 )
··· 35 import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' 36 import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 37 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 38 + import {useTheme} from '#/alf' 39 import * as Layout from '#/components/Layout' 40 import {IS_WEB} from '#/env' 41 import {useDemoMode} from '#/storage/hooks/demo-mode' ··· 47 const {currentAccount} = useSession() 48 const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = 49 usePinnedFeedsInfos() 50 + const t = useTheme() 51 52 React.useEffect(() => { 53 if (IS_WEB && !currentAccount) { ··· 93 return ( 94 <Layout.Screen> 95 <Layout.Center style={styles.loading}> 96 + <ActivityIndicator size="large" color={t.palette.primary_500} /> 97 </Layout.Center> 98 </Layout.Screen> 99 )