Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

refactor: perf improvements & less broken requests

used squoosh.app on the kawaii logos!
removed logic for requests to live-events.workers.bsky.app & events.bsky.app
try to not bundle extra dependencies/duplicate fonts

xan.lol 6b20efc3 0c65f149

+27 -109
assets/kawaii.avif

This is a binary file and will not be displayed.

assets/kawaii.png

This is a binary file and will not be displayed.

assets/kawaii_smol.avif

This is a binary file and will not be displayed.

assets/kawaii_smol.png

This is a binary file and will not be displayed.

+9 -1
rspack.config.ts
··· 365 365 splitChunks: { 366 366 chunks: 'all', 367 367 cacheGroups: { 368 + framework: { 369 + test: /[\\/]node_modules[\\/](react|react-dom|react-native-web|@react-navigation|expo|@expo)[\\/]/, 370 + name: 'framework', 371 + chunks: 'initial', 372 + priority: 20, 373 + reuseExistingChunk: true, 374 + }, 368 375 vendor: { 369 376 test: /[\\/]node_modules[\\/]/, 370 377 name: 'vendor', 371 - chunks: 'all', 378 + chunks: 'initial', 372 379 priority: -10, 380 + reuseExistingChunk: true, 373 381 }, 374 382 }, 375 383 },
+8
src/alf/util/unusedUseFonts.web.ts
··· 1 + /* 2 + * Web fonts are loaded via `web/index.html` using stable asset paths. 3 + * Keeping this stub prevents expo-font's extraction helper from pulling 4 + * duplicate woff2 files into the JS bundle on web. 5 + */ 6 + export function DO_NOT_USE() { 7 + return [true] as const 8 + }
+4 -29
src/analytics/features/index.ts
··· 2 2 import {setPolyfills} from '@growthbook/growthbook' 3 3 import {GrowthBook} from '@growthbook/growthbook-react' 4 4 5 - import {Logger} from '#/logger' 6 5 import {getNavigationMetadata, type Metadata} from '#/analytics/metadata' 7 6 import * as env from '#/env' 8 7 9 8 export {Features} from '#/analytics/features/types' 10 9 11 - const logger = Logger.create(Logger.Context.Growthbook) 12 10 const CACHE = new MMKV({id: 'bsky_features_cache'}) 13 11 14 12 setPolyfills({ ··· 28 26 */ 29 27 export type FeatureFetchStrategy = 'prefer-low-latency' | 'prefer-fresh-gates' 30 28 31 - const TIMEOUT_INIT = 2000 // TODO should base on p99 or something 32 - const TIMEOUT_PREFER_LOW_LATENCY = 250 33 - const TIMEOUT_PREFER_FRESH_GATES = 1500 34 - 35 29 export const features = new GrowthBook({ 36 30 apiHost: env.GROWTHBOOK_API_HOST, 37 31 clientKey: env.GROWTHBOOK_CLIENT_KEY, 38 32 }) 39 33 40 34 /** 41 - * Initializer promise that must be awaited before using the GrowthBook 42 - * instance or rendering the `AnalyticsFeaturesContext`. Note: this may not be 43 - * fully initialized if it takes longer than `TIMEOUT_INIT` to initialize. In 44 - * that case, we may see a flash of uncustomized content until the 45 - * initialization completes. 35 + * Kept as a resolved promise so existing startup code can await it without 36 + * triggering any remote GrowthBook fetches. 46 37 */ 47 - export const init = new Promise<void>(async y => { 48 - const res = await features.init({timeout: TIMEOUT_INIT}) 49 - if (!res.success) { 50 - logger.warn('GrowthBook initialization failed or timed out', { 51 - source: res.source, 52 - safeMessage: res.error?.toString(), 53 - }) 54 - } 55 - y() 56 - }) 38 + export const init = Promise.resolve() 57 39 58 40 /** 59 41 * Refresh feature gates from GrowthBook. Updates attributes based on the 60 42 * provided account, if any. 61 43 */ 62 - export async function refresh({strategy}: {strategy: FeatureFetchStrategy}) { 63 - await features.refreshFeatures({ 64 - timeout: 65 - strategy === 'prefer-low-latency' 66 - ? TIMEOUT_PREFER_LOW_LATENCY 67 - : TIMEOUT_PREFER_FRESH_GATES, 68 - }) 69 - } 44 + export async function refresh(_: {strategy: FeatureFetchStrategy}) {} 70 45 71 46 /** 72 47 * Converts our metadata into GrowthBook attributes and sets them. GrowthBook
+4 -77
src/features/liveEvents/context.tsx
··· 1 - import {createContext, useContext, useMemo} from 'react' 2 - import {hasMutedWord} from '@atproto/api' 3 - import {QueryClient, useQuery} from '@tanstack/react-query' 1 + import {createContext, useContext} from 'react' 4 2 5 - import {useOnAppStateChange} from '#/lib/appState' 6 - import {useIsBskyTeam} from '#/lib/hooks/useIsBskyTeam' 7 3 import { 8 4 convertBskyAppUrlIfNeeded, 9 5 isBskyCustomFeedUrl, 10 6 makeRecordUri, 11 7 } from '#/lib/strings/url-helpers' 12 - import {usePreferencesQuery} from '#/state/queries/preferences' 13 - import {IS_DEV, LIVE_EVENTS_URL} from '#/env' 14 8 import {useLiveEventPreferences} from '#/features/liveEvents/preferences' 15 9 import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' 16 - import {useDevMode} from '#/storage/hooks/dev-mode' 17 - 18 - const qc = new QueryClient() 19 - const liveEventsQueryKey = ['live-events'] 20 10 21 11 export const DEFAULT_LIVE_EVENTS = { 22 12 feeds: [], 23 13 } 24 14 25 - async function fetchLiveEvents(): Promise<LiveEventsWorkerResponse | null> { 26 - try { 27 - const res = await fetch(`${LIVE_EVENTS_URL}/config`) 28 - if (!res.ok) return null 29 - const data = await res.json() 30 - return data 31 - } catch { 32 - return null 33 - } 34 - } 35 - 36 15 const Context = createContext<LiveEventsWorkerResponse>(DEFAULT_LIVE_EVENTS) 37 16 38 17 export function Provider({children}: React.PropsWithChildren<{}>) { 39 - const [isDevMode] = useDevMode() 40 - const isBskyTeam = useIsBskyTeam() 41 - const {data: preferences} = usePreferencesQuery() 42 - const mutedWords = useMemo( 43 - () => preferences?.moderationPrefs?.mutedWords ?? [], 44 - [preferences?.moderationPrefs?.mutedWords], 18 + return ( 19 + <Context.Provider value={DEFAULT_LIVE_EVENTS}>{children}</Context.Provider> 45 20 ) 46 - 47 - const {data, refetch} = useQuery( 48 - { 49 - // keep this, prefectching handles initial load 50 - staleTime: 1000 * 15, 51 - queryKey: liveEventsQueryKey, 52 - refetchInterval: 1000 * 60 * 5, // refetch every 5 minutes 53 - async queryFn() { 54 - return fetchLiveEvents() 55 - }, 56 - }, 57 - qc, 58 - ) 59 - 60 - useOnAppStateChange(state => { 61 - if (state === 'active') void refetch() 62 - }) 63 - 64 - const ctx = useMemo(() => { 65 - if (!data) return DEFAULT_LIVE_EVENTS 66 - const skipMuteFilter = isBskyTeam || IS_DEV 67 - const feeds = data.feeds.filter(f => { 68 - if (f.preview && !isBskyTeam) return false 69 - if (!skipMuteFilter && mutedWords.length > 0) { 70 - const text = [ 71 - f.title, 72 - f.layouts?.wide?.title, 73 - f.layouts?.compact?.title, 74 - ] 75 - .filter(Boolean) 76 - .join(' ') 77 - if (hasMutedWord({mutedWords, text})) return false 78 - } 79 - return true 80 - }) 81 - return { 82 - ...data, 83 - // only one at a time for now, unless bsky team and dev mode 84 - feeds: isBskyTeam && isDevMode ? feeds : feeds.slice(0, 1), 85 - } 86 - }, [data, isBskyTeam, isDevMode, mutedWords]) 87 - 88 - return <Context.Provider value={ctx}>{children}</Context.Provider> 89 21 } 90 22 91 - export async function prefetchLiveEvents() { 92 - const data = await fetchLiveEvents() 93 - if (data) { 94 - qc.setQueryData(liveEventsQueryKey, data) 95 - } 96 - } 23 + export async function prefetchLiveEvents() {} 97 24 98 25 export function useLiveEvents() { 99 26 const ctx = useContext(Context)
+2 -2
src/view/icons/Logo.tsx
··· 38 38 <Image 39 39 source={ 40 40 size > 100 41 - ? require('../../../assets/kawaii.png') 42 - : require('../../../assets/kawaii_smol.png') 41 + ? require('../../../assets/kawaii.avif') 42 + : require('../../../assets/kawaii_smol.avif') 43 43 } 44 44 accessibilityLabel="Witchsky" 45 45 accessibilityHint=""