Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 136 lines 3.9 kB view raw
1import {createContext, useContext, useMemo} from 'react' 2import {hasMutedWord} from '@atproto/api' 3import {QueryClient, useQuery} from '@tanstack/react-query' 4 5import {useOnAppStateChange} from '#/lib/appState' 6import {useIsBskyTeam} from '#/lib/hooks/useIsBskyTeam' 7import { 8 convertBskyAppUrlIfNeeded, 9 isBskyCustomFeedUrl, 10 makeRecordUri, 11} from '#/lib/strings/url-helpers' 12import {usePreferencesQuery} from '#/state/queries/preferences' 13import {IS_DEV, LIVE_EVENTS_URL} from '#/env' 14import {useLiveEventPreferences} from '#/features/liveEvents/preferences' 15import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' 16import {useDevMode} from '#/storage/hooks/dev-mode' 17 18const qc = new QueryClient() 19const liveEventsQueryKey = ['live-events'] 20 21export const DEFAULT_LIVE_EVENTS = { 22 feeds: [], 23} 24 25async 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 36const Context = createContext<LiveEventsWorkerResponse>(DEFAULT_LIVE_EVENTS) 37 38export 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], 45 ) 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} 90 91export async function prefetchLiveEvents() { 92 const data = await fetchLiveEvents() 93 if (data) { 94 qc.setQueryData(liveEventsQueryKey, data) 95 } 96} 97 98export function useLiveEvents() { 99 const ctx = useContext(Context) 100 if (!ctx) { 101 throw new Error('useLiveEventsContext must be used within a Provider') 102 } 103 return ctx 104} 105 106export function useUserPreferencedLiveEvents() { 107 const events = useLiveEvents() 108 const {data, isLoading} = useLiveEventPreferences() 109 if (isLoading) return DEFAULT_LIVE_EVENTS 110 const {hideAllFeeds, hiddenFeedIds} = data 111 return { 112 ...events, 113 feeds: hideAllFeeds 114 ? [] 115 : events.feeds.filter(f => { 116 const hidden = f?.id ? hiddenFeedIds.includes(f?.id || '') : false 117 return !hidden 118 }), 119 } 120} 121 122export function useActiveLiveEventFeedUris() { 123 const {feeds} = useLiveEvents() 124 125 return new Set( 126 feeds 127 // insurance 128 .filter(f => isBskyCustomFeedUrl(f.url)) 129 .map(f => { 130 const uri = convertBskyAppUrlIfNeeded(f.url) 131 const [_0, did, _1, rkey] = uri.split('/').filter(Boolean) 132 const urip = makeRecordUri(did, 'app.bsky.feed.generator', rkey) 133 return urip.toString() 134 }), 135 ) 136}