Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 187 lines 5.0 kB view raw
1import {createContext, useContext, useMemo, useState} from 'react' 2import { 3 type AppBskyActorDefs, 4 type AppBskyFeedDefs, 5 type AppBskyUnspeccedGetPostThreadV2, 6 type BlobRef, 7 type ModerationDecision, 8} from '@atproto/api' 9import {msg} from '@lingui/core/macro' 10import {useLingui} from '@lingui/react' 11import {useQueryClient} from '@tanstack/react-query' 12 13import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 14import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers' 15import {purgeTemporaryImageFiles} from '#/state/gallery' 16import { 17 precacheResolveLinkQuery, 18 RQKEY_GIF_ROOT, 19 RQKEY_LINK_ROOT, 20} from '#/state/queries/resolve-link' 21import * as Toast from '#/components/Toast' 22 23export interface ComposerOptsPostRef { 24 uri: string 25 cid: string 26 text: string 27 langs?: string[] 28 // remove this after updating atproto api package 29 author: AppBskyActorDefs.ProfileViewBasic & { 30 pronouns?: string 31 } 32 embed?: AppBskyFeedDefs.PostView['embed'] 33 moderation?: ModerationDecision 34} 35 36export type OnPostSuccessData = 37 | { 38 replyToUri?: string 39 posts: AppBskyUnspeccedGetPostThreadV2.ThreadItem[] 40 } 41 | undefined 42 43export type ComposerLogContext = 44 | 'Fab' 45 | 'PostReply' 46 | 'QuotePost' 47 | 'ProfileFeed' 48 | 'Deeplink' 49 | 'Other' 50 51export interface ComposerOpts { 52 activeAccountDid?: string 53 replyTo?: ComposerOptsPostRef 54 onPost?: (postUri: string | undefined) => void 55 onPostSuccess?: (data: OnPostSuccessData) => void 56 quote?: AppBskyFeedDefs.PostView 57 mention?: string // handle of user to mention 58 text?: string 59 imageUris?: { 60 uri: string 61 width: number 62 height: number 63 altText?: string 64 blobRef?: BlobRef 65 }[] 66 videoUri?: { 67 uri: string 68 width: number 69 height: number 70 blobRef?: BlobRef 71 altText?: string 72 } 73 openGallery?: boolean 74 logContext?: ComposerLogContext 75} 76 77type StateContext = ComposerOpts | undefined 78type ControlsContext = { 79 openComposer: (opts: ComposerOpts) => void 80 closeComposer: () => boolean 81} 82 83const stateContext = createContext<StateContext>(undefined) 84stateContext.displayName = 'ComposerStateContext' 85const controlsContext = createContext<ControlsContext>({ 86 openComposer(_opts: ComposerOpts) {}, 87 closeComposer() { 88 return false 89 }, 90}) 91controlsContext.displayName = 'ComposerControlsContext' 92 93export function Provider({children}: React.PropsWithChildren<{}>) { 94 const {_} = useLingui() 95 const [state, setState] = useState<StateContext>() 96 const queryClient = useQueryClient() 97 98 const openComposer = useNonReactiveCallback((opts: ComposerOpts) => { 99 if (opts.quote) { 100 const path = postUriToRelativePath(opts.quote.uri) 101 if (path) { 102 const appUrl = toBskyAppUrl(path) 103 precacheResolveLinkQuery(queryClient, appUrl, { 104 type: 'record', 105 kind: 'post', 106 record: { 107 cid: opts.quote.cid, 108 uri: opts.quote.uri, 109 }, 110 view: opts.quote, 111 }) 112 } 113 } 114 const author = opts.replyTo?.author || opts.quote?.author 115 const isBlocked = Boolean( 116 author && 117 (author.viewer?.blocking || 118 author.viewer?.blockedBy || 119 author.viewer?.blockingByList), 120 ) 121 if (isBlocked) { 122 Toast.show(_(msg`Cannot interact with a blocked user`), { 123 type: 'warning', 124 }) 125 } else { 126 setState(prevOpts => { 127 if (prevOpts) { 128 // Never replace an already open composer. 129 return prevOpts 130 } 131 return opts 132 }) 133 } 134 }) 135 136 const closeComposer = useNonReactiveCallback(() => { 137 let wasOpen = !!state 138 if (wasOpen) { 139 setState(undefined) 140 purgeTemporaryImageFiles() 141 // Purging deletes cached thumbnails on disk, so remove the query 142 // caches that may hold references to those now-deleted file paths. 143 // Without this, restoring a draft would serve stale ResolvedLink 144 // data pointing at missing files, causing "Failed to load blob". 145 queryClient.removeQueries({queryKey: [RQKEY_LINK_ROOT]}) 146 queryClient.removeQueries({queryKey: [RQKEY_GIF_ROOT]}) 147 } 148 149 return wasOpen 150 }) 151 152 const api = useMemo( 153 () => ({ 154 openComposer, 155 closeComposer, 156 }), 157 [openComposer, closeComposer], 158 ) 159 160 return ( 161 <stateContext.Provider value={state}> 162 <controlsContext.Provider value={api}> 163 {children} 164 </controlsContext.Provider> 165 </stateContext.Provider> 166 ) 167} 168 169export function useComposerState() { 170 return useContext(stateContext) 171} 172 173export function useComposerControls() { 174 const {closeComposer} = useContext(controlsContext) 175 return useMemo(() => ({closeComposer}), [closeComposer]) 176} 177 178/** 179 * DO NOT USE DIRECTLY. The deprecation notice as a warning only, it's not 180 * actually deprecated. 181 * 182 * @deprecated use `#/lib/hooks/useOpenComposer` instead 183 */ 184export function useOpenComposer() { 185 const {openComposer} = useContext(controlsContext) 186 return useMemo(() => ({openComposer}), [openComposer]) 187}