Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

fix: mobile video downloads and misc

xan.lol 43a37122 f28351e6

+95 -48
+6 -4
src/alf/typography.tsx
··· 1 1 import {Children} from 'react' 2 - import {type TextProps as RNTextProps} from 'react-native' 3 - import {type StyleProp, type TextStyle} from 'react-native' 2 + import { 3 + type StyleProp, 4 + type TextProps as RNTextProps, 5 + type TextStyle, 6 + } from 'react-native' 4 7 import {UITextView} from 'react-native-uitextview' 5 8 import createEmojiRegex from 'emoji-regex' 6 9 7 10 import {type Alf, applyFonts, atoms, flatten} from '#/alf' 8 - import {IS_NATIVE} from '#/env' 9 - import {IS_IOS} from '#/env' 11 + import {IS_IOS, IS_NATIVE} from '#/env' 10 12 11 13 /** 12 14 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
+7 -2
src/components/Layout/index.tsx
··· 1 1 import {forwardRef, memo, useContext, useMemo} from 'react' 2 - import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native' 3 - import {type StyleProp} from 'react-native' 2 + import { 3 + type StyleProp, 4 + StyleSheet, 5 + View, 6 + type ViewProps, 7 + type ViewStyle, 8 + } from 'react-native' 4 9 import { 5 10 KeyboardAwareScrollView, 6 11 type KeyboardAwareScrollViewProps,
+1 -2
src/components/Select/index.web.tsx
··· 3 3 import {Select as RadixSelect} from 'radix-ui' 4 4 5 5 import {useA11y} from '#/state/a11y' 6 - import {flatten, useTheme, web} from '#/alf' 7 - import {atoms as a} from '#/alf' 6 + import {atoms as a, flatten, useTheme, web} from '#/alf' 8 7 import {useInteractionState} from '#/components/hooks/useInteractionState' 9 8 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 10 9 import {
+4 -2
src/components/Toast/Toast.tsx
··· 8 8 type UninheritableButtonProps, 9 9 } from '#/components/Button' 10 10 import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' 11 - import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 12 - import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 11 + import { 12 + CircleInfo_Stroke2_Corner0_Rounded as CircleInfo, 13 + CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon, 14 + } from '#/components/icons/CircleInfo' 13 15 import {type Props as SVGIconProps} from '#/components/icons/common' 14 16 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 15 17 import {dismiss} from '#/components/Toast/sonner'
+1 -1
src/geolocation/service.ts
··· 25 25 async function fetchGeolocationServiceData(): Promise<Geolocation | undefined> { 26 26 if (debug.enabled) return debug.resolve(debug.geolocation) 27 27 // Return local geolocation data instead of making HTTP request 28 - return geolocationData as Geolocation 28 + return geolocationData 29 29 } 30 30 31 31 /**
+3 -2
src/lib/api/resolve.ts
··· 1 1 import { 2 2 type AppBskyFeedDefs, 3 3 type AppBskyGraphDefs, 4 + type BskyAgent, 4 5 type ComAtprotoRepoStrongRef, 5 6 } from '@atproto/api' 6 7 import {AtUri} from '@atproto/api' 7 - import {type BskyAgent} from '@atproto/api' 8 8 9 9 import {POST_IMG_MAX} from '#/lib/constants' 10 10 import {getLinkMeta} from '#/lib/link-meta/link-meta' ··· 15 15 parseStarterPackUri, 16 16 } from '#/lib/strings/starter-pack' 17 17 import { 18 + convertBskyAppUrlIfNeeded, 18 19 isBskyCustomFeedUrl, 19 20 isBskyListUrl, 20 21 isBskyPostUrl, 21 22 isBskyStarterPackUrl, 22 23 isBskyStartUrl, 23 24 isShortLink, 25 + makeRecordUri, 24 26 } from '#/lib/strings/url-helpers' 25 27 import {type ComposerImage} from '#/state/gallery' 26 28 import {createComposerImage} from '#/state/gallery' 27 29 import {type Gif} from '#/state/queries/tenor' 28 30 import {createGIFDescription} from '../gif-alt-text' 29 - import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' 30 31 31 32 type ResolvedExternalLink = { 32 33 type: 'external'
+1 -1
src/lib/hooks/useAnimatedValue.ts
··· 8 8 lazyRef.current = new Animated.Value(initialValue) 9 9 } 10 10 11 - return lazyRef.current as Animated.Value 11 + return lazyRef.current 12 12 }
+40 -14
src/lib/media/manip.ts
··· 176 176 177 177 export async function saveVideoToMediaLibrary({uri}: {uri: string}) { 178 178 // download the file to cache 179 - const downloadResponse = await RNFetchBlob.config({ 180 - fileCache: true, 181 - }) 182 - .fetch('GET', uri) 183 - .catch(() => null) 184 - if (downloadResponse == null) return false 185 - let videoPath = downloadResponse.path() 186 - let extension = mimeToExt(downloadResponse.respInfo.headers['content-type']) 187 - videoPath = normalizePath( 188 - await moveToPermanentPath(videoPath, '.' + extension), 189 - true, 190 - ) 179 + const tempPath = `${cacheDirectory ?? ''}/${String(uuid.v4())}.bin` 180 + const dlResumable = createDownloadResumable(uri, tempPath, {cache: true}) 181 + const dlRes = await dlResumable.downloadAsync().catch(() => null) 182 + if (!dlRes?.uri) return false 183 + 184 + const contentType = 185 + dlRes.headers['content-type'] ?? dlRes.headers['Content-Type'] 186 + if (!contentType) { 187 + void safeDeleteAsync(dlRes.uri) 188 + return false 189 + } 190 + 191 + let extension: string 192 + try { 193 + extension = mimeToExt(contentType) 194 + } catch { 195 + void safeDeleteAsync(dlRes.uri) 196 + return false 197 + } 198 + 199 + const videoPath = await moveToPermanentPath(dlRes.uri, '.' + extension) 191 200 192 201 // save 193 - await MediaLibrary.createAssetAsync(videoPath) 194 - safeDeleteAsync(videoPath) 202 + try { 203 + if (IS_ANDROID) { 204 + await MediaLibrary.createAlbumAsync( 205 + ALBUM_NAME, 206 + undefined, 207 + undefined, 208 + videoPath, 209 + ) 210 + } else { 211 + await MediaLibrary.saveToLibraryAsync(videoPath) 212 + } 213 + } catch (err) { 214 + logger.error(err instanceof Error ? err : String(err), { 215 + message: 'Failed to save video to media library', 216 + }) 217 + throw err 218 + } finally { 219 + void safeDeleteAsync(videoPath) 220 + } 195 221 return true 196 222 } 197 223
+2 -2
src/lib/routes/links.ts
··· 24 24 export function makeCustomFeedLink( 25 25 did: string, 26 26 rkey: string, 27 - segment?: string | undefined, 28 - feedCacheKey?: 'discover' | 'explore' | undefined, 27 + segment?: string, 28 + feedCacheKey?: 'discover' | 'explore', 29 29 ) { 30 30 return ( 31 31 [`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') +
+2 -3
src/screens/Messages/components/MessagesList.tsx
··· 15 15 AppBskyRichtextFacet, 16 16 RichText, 17 17 } from '@atproto/api' 18 - import { isNative } from '@bsky.app/alf' 18 + import {isNative} from '@bsky.app/alf' 19 19 20 20 import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' 21 21 import {ScrollProvider} from '#/lib/ScrollContext' ··· 51 51 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 52 52 import {Loader} from '#/components/Loader' 53 53 import {Text} from '#/components/Typography' 54 - import {IS_NATIVE} from '#/env' 55 - import {IS_WEB} from '#/env' 54 + import {IS_NATIVE, IS_WEB} from '#/env' 56 55 import {ChatStatusInfo} from './ChatStatusInfo' 57 56 import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' 58 57
+6 -1
src/screens/Profile/Header/EditProfileDialog.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {Plural, Trans} from '@lingui/react/macro' 7 7 8 - import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, HITSLOP_10, urls} from '#/lib/constants' 8 + import { 9 + HITSLOP_10, 10 + MAX_DESCRIPTION, 11 + MAX_DISPLAY_NAME, 12 + urls, 13 + } from '#/lib/constants' 9 14 import {cleanError} from '#/lib/strings/errors' 10 15 import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' 11 16 import {isValidWebsiteFormat} from '#/lib/strings/website'
+1 -2
src/screens/VideoFeed/components/Scrubber.tsx
··· 23 23 import {useEventListener} from 'expo' 24 24 import {type VideoPlayer} from 'expo-video' 25 25 26 - import {tokens} from '#/alf' 27 - import {atoms as a} from '#/alf' 26 + import {atoms as a, tokens} from '#/alf' 28 27 import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' 29 28 import {Text} from '#/components/Typography' 30 29
+1 -1
src/state/preferences/kawaii.tsx
··· 1 1 import { 2 2 createContext, 3 - PropsWithChildren, 3 + type PropsWithChildren, 4 4 useCallback, 5 5 useContext, 6 6 useEffect,
+1 -1
src/state/queries/notifications/feed.ts
··· 298 298 if (AppBskyFeedDefs.isPostView(item.subject)) { 299 299 const quotedPost = getEmbeddedPost(item.subject?.embed) 300 300 if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { 301 - yield embedViewRecordToPostView(quotedPost!) 301 + yield embedViewRecordToPostView(quotedPost) 302 302 } 303 303 } 304 304 }
+1 -1
src/state/queries/usePostThread/traversal.ts
··· 196 196 * `repliesSeenCounter` later on, since `repliesSeenCounter` 197 197 * is 1-indexed and `replyIndex` is 0-indexed. 198 198 */ 199 - childMetadata!.replyIndex = 199 + childMetadata.replyIndex = 200 200 childParentMetadata.repliesSeenCounter 201 201 } 202 202
+9 -4
src/state/shell/color-mode.tsx
··· 1 - import {createContext, PropsWithChildren, useContext, useEffect, useMemo, useState} from 'react' 1 + import { 2 + createContext, 3 + type PropsWithChildren, 4 + useContext, 5 + useEffect, 6 + useMemo, 7 + useState, 8 + } from 'react' 2 9 3 10 import * as persisted from '#/state/persisted' 4 11 ··· 28 35 export function Provider({children}: PropsWithChildren<{}>) { 29 36 const [colorMode, setColorMode] = useState(() => persisted.get('colorMode')) 30 37 const [darkTheme, setDarkTheme] = useState(() => persisted.get('darkTheme')) 31 - const [colorScheme, setColorScheme] = useState( 32 - persisted.get('colorScheme'), 33 - ) 38 + const [colorScheme, setColorScheme] = useState(persisted.get('colorScheme')) 34 39 const [hue, setHue] = useState(persisted.get('hue')) 35 40 36 41 const stateContextValue = useMemo(
+8 -4
src/view/com/notifications/NotificationFeedItem.tsx
··· 54 54 ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, 55 55 } from '#/components/icons/Chevron' 56 56 import {Contacts_Filled_Corner2_Rounded as ContactsIconFilled} from '#/components/icons/Contacts' 57 - import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2' 58 - import {LikeRepost_Stroke2_Corner2_Rounded as RepostHeartIcon} from '#/components/icons/Heart2' 57 + import { 58 + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, 59 + LikeRepost_Stroke2_Corner2_Rounded as RepostHeartIcon, 60 + } from '#/components/icons/Heart2' 59 61 import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 60 62 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 61 - import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/Repost' 62 - import {RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon} from '#/components/icons/Repost' 63 + import { 64 + Repost_Stroke2_Corner3_Rounded as RepostIcon, 65 + RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, 66 + } from '#/components/icons/Repost' 63 67 import {StarterPack} from '#/components/icons/StarterPack' 64 68 import {VerifiedCheck} from '#/components/icons/VerifiedCheck' 65 69 import {InlineLinkText, Link} from '#/components/Link'
+1 -1
src/view/com/util/PressableWithHover.tsx
··· 3 3 Pressable, 4 4 type PressableProps, 5 5 type StyleProp, 6 + type View, 6 7 type ViewStyle, 7 8 } from 'react-native' 8 - import {type View} from 'react-native' 9 9 10 10 import {addStyle} from '#/lib/styles' 11 11 import {useInteractionState} from '#/components/hooks/useInteractionState'