Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

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