Bluesky app fork with some witchin' additions 💫

Merge branch 'demo' into main

Changed files
+349 -29
src
lib
api
feed
screens
state
queries
storage
view
com
util
post-embeds
screens
shell
bottom-bar
+20
src/lib/api/feed/demo.ts
··· 1 + import {type AppBskyFeedDefs, type BskyAgent} from '@atproto/api' 2 + 3 + import {DEMO_FEED} from '#/lib/demo' 4 + import {type FeedAPI, type FeedAPIResponse} from './types' 5 + 6 + export class DemoFeedAPI implements FeedAPI { 7 + agent: BskyAgent 8 + 9 + constructor({agent}: {agent: BskyAgent}) { 10 + this.agent = agent 11 + } 12 + 13 + async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { 14 + return DEMO_FEED.feed[0] 15 + } 16 + 17 + async fetch(): Promise<FeedAPIResponse> { 18 + return DEMO_FEED 19 + } 20 + }
+202
src/lib/demo.ts
··· 1 + import {type AppBskyFeedGetFeed} from '@atproto/api' 2 + import {subDays, subMinutes} from 'date-fns' 3 + 4 + const DID = `did:plc:z72i7hdynmk6r22z27h6tvur` 5 + const NOW = new Date() 6 + const POST_1_DATE = subMinutes(NOW, 2).toISOString() 7 + const POST_2_DATE = subMinutes(NOW, 4).toISOString() 8 + const POST_3_DATE = subMinutes(NOW, 5).toISOString() 9 + 10 + export const DEMO_FEED = { 11 + feed: [ 12 + { 13 + post: { 14 + uri: 'at://did:plc:pvooorihapc2lf2pijehgrdf/app.bsky.feed.post/3lniysofyll2d', 15 + cid: 'bafyreihwh3wxxme732ylbylhhdyz7ex6t4jtu6s3gjxxvnnh4feddhg3ku', 16 + author: { 17 + did: 'did:plc:pvooorihapc2lf2pijehgrdf', 18 + handle: 'forkedriverband.bsky.social', 19 + displayName: 'Forked River Band', 20 + avatar: 'https://bsky.social/about/adi/post_1_avi.jpg', 21 + viewer: { 22 + muted: false, 23 + blockedBy: false, 24 + following: `at://${DID}/app.bsky.graph.follow/post1`, 25 + }, 26 + labels: [], 27 + createdAt: POST_1_DATE, 28 + verification: { 29 + verifications: [ 30 + { 31 + issuer: DID, 32 + uri: `at://${DID}/app.bsky.graph.verification/post1`, 33 + isValid: true, 34 + createdAt: subDays(NOW, 11).toISOString(), 35 + }, 36 + ], 37 + verifiedStatus: 'valid', 38 + trustedVerifierStatus: 'none', 39 + }, 40 + }, 41 + record: { 42 + $type: 'app.bsky.feed.post', 43 + createdAt: POST_1_DATE, 44 + // embed: { 45 + // $type: 'app.bsky.embed.images', 46 + // images: [ 47 + // { 48 + // alt: 'Fake flier for Sebastapol Bluegrass Fest', 49 + // aspectRatio: { 50 + // height: 1350, 51 + // width: 900, 52 + // }, 53 + // image: { 54 + // $type: 'blob', 55 + // ref: { 56 + // $link: 57 + // 'bafkreig7gnirmz5guhhjutf3mqbjjzxzi3w4wvs5qy2gnxma5g3brbaidi', 58 + // }, 59 + // mimeType: 'image/jpeg', 60 + // size: 562871, 61 + // }, 62 + // }, 63 + // ], 64 + // }, 65 + langs: ['en'], 66 + text: 'Sonoma County folks: Come tip your hats our way and see us play new and old bluegrass tunes at Sebastopol Solstice Fest on June 20th.', 67 + }, 68 + embed: { 69 + $type: 'app.bsky.embed.images#view', 70 + images: [ 71 + { 72 + thumb: 'https://bsky.social/about/adi/post_1_image.jpg', 73 + fullsize: 'https://bsky.social/about/adi/post_1_image.jpg', 74 + alt: 'Fake flier for Sebastapol Bluegrass Fest', 75 + aspectRatio: { 76 + height: 1350, 77 + width: 900, 78 + }, 79 + }, 80 + ], 81 + }, 82 + replyCount: 1, 83 + repostCount: 4, 84 + likeCount: 18, 85 + quoteCount: 0, 86 + indexedAt: POST_1_DATE, 87 + viewer: { 88 + threadMuted: false, 89 + embeddingDisabled: false, 90 + }, 91 + labels: [], 92 + }, 93 + }, 94 + { 95 + post: { 96 + uri: 'at://did:plc:fhhqii56ppgyh5qcm2b3mokf/app.bsky.feed.post/3lnizc7fug52c', 97 + cid: 'bafyreienuabsr55rycirdf4ewue5tjcseg5lzqompcsh2brqzag6hvxllm', 98 + author: { 99 + did: 'did:plc:fhhqii56ppgyh5qcm2b3mokf', 100 + handle: 'dinh-designs.bsky.social', 101 + displayName: 'Rich Dinh Designs', 102 + avatar: 'https://bsky.social/about/adi/post_2_avi.jpg', 103 + viewer: { 104 + muted: false, 105 + blockedBy: false, 106 + following: `at://${DID}/app.bsky.graph.follow/post2`, 107 + }, 108 + labels: [], 109 + createdAt: POST_2_DATE, 110 + }, 111 + record: { 112 + $type: 'app.bsky.feed.post', 113 + createdAt: POST_2_DATE, 114 + // embed: { 115 + // $type: 'app.bsky.embed.images', 116 + // images: [ 117 + // { 118 + // alt: 'Placeholder image of interior design', 119 + // aspectRatio: { 120 + // height: 872, 121 + // width: 598, 122 + // }, 123 + // image: { 124 + // $type: 'blob', 125 + // ref: { 126 + // $link: 127 + // 'bafkreidcjc6bjb4jjjejruin5cldhj5zovsuu4tydulenyprneziq5rfeu', 128 + // }, 129 + // mimeType: 'image/jpeg', 130 + // size: 296003, 131 + // }, 132 + // }, 133 + // ], 134 + // }, 135 + langs: ['en'], 136 + text: 'Details from our install at the Lucas residence in Joshua Tree. We populated the space with rich, earthy tones and locally-sourced materials to suit the landscape.', 137 + }, 138 + embed: { 139 + $type: 'app.bsky.embed.images#view', 140 + images: [ 141 + { 142 + thumb: 'https://bsky.social/about/adi/post_2_image.jpg', 143 + fullsize: 'https://bsky.social/about/adi/post_2_image.jpg', 144 + alt: 'Placeholder image of interior design', 145 + aspectRatio: { 146 + height: 872, 147 + width: 598, 148 + }, 149 + }, 150 + ], 151 + }, 152 + replyCount: 3, 153 + repostCount: 1, 154 + likeCount: 4, 155 + quoteCount: 0, 156 + indexedAt: POST_2_DATE, 157 + viewer: { 158 + threadMuted: false, 159 + embeddingDisabled: false, 160 + }, 161 + labels: [], 162 + }, 163 + }, 164 + { 165 + post: { 166 + uri: 'at://did:plc:h7fwnfejmmifveeea5eyxgkc/app.bsky.feed.post/3lnizna3g4f2t', 167 + cid: 'bafyreiepn7obmlshliori4j34texpaukrqkyyu7cq6nmpzk4lkis7nqeae', 168 + author: { 169 + did: 'did:plc:h7fwnfejmmifveeea5eyxgkc', 170 + handle: 'rodyalbuerne.bsky.social', 171 + displayName: 'Rody Albuerne', 172 + avatar: 'https://bsky.social/about/adi/post_3_avi.jpg', 173 + viewer: { 174 + muted: false, 175 + blockedBy: false, 176 + following: `at://${DID}/app.bsky.graph.follow/post3`, 177 + }, 178 + labels: [], 179 + createdAt: POST_3_DATE, 180 + }, 181 + record: { 182 + $type: 'app.bsky.feed.post', 183 + createdAt: POST_3_DATE, 184 + langs: ['en'], 185 + text: 'Tinkering with the basics of traditional wooden joinery in my shop lately. Starting small with this ox, made using simple mortise and tenon joints.', 186 + }, 187 + replyCount: 11, 188 + repostCount: 97, 189 + likeCount: 399, 190 + quoteCount: 0, 191 + indexedAt: POST_3_DATE, 192 + viewer: { 193 + threadMuted: false, 194 + embeddingDisabled: false, 195 + }, 196 + labels: [], 197 + }, 198 + }, 199 + ], 200 + } satisfies AppBskyFeedGetFeed.OutputSchema 201 + 202 + export const BOTTOM_BAR_AVI = 'https://bsky.social/about/adi/user_avi.jpg'
+29 -2
src/screens/Settings/AboutSettings.tsx
··· 12 12 import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' 13 13 import {STATUS_PAGE_URL} from '#/lib/constants' 14 14 import {type CommonNavigatorParams} from '#/lib/routes/types' 15 - import {isAndroid, isNative} from '#/platform/detection' 15 + import {isAndroid, isIOS, isNative} from '#/platform/detection' 16 16 import * as Toast from '#/view/com/util/Toast' 17 17 import * as SettingsList from '#/screens/Settings/components/SettingsList' 18 + import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 18 19 import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle' 19 20 import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines' 20 21 import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' ··· 22 23 import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' 23 24 import * as Layout from '#/components/Layout' 24 25 import {Loader} from '#/components/Loader' 26 + import {useDemoMode} from '#/storage/hooks/demo-mode' 25 27 import {useDevMode} from '#/storage/hooks/dev-mode' 26 28 import {OTAInfo} from './components/OTAInfo' 27 29 ··· 29 31 export function AboutSettingsScreen({}: Props) { 30 32 const {_, i18n} = useLingui() 31 33 const [devModeEnabled, setDevModeEnabled] = useDevMode() 34 + const [demoModeEnabled, setDemoModeEnabled] = useDemoMode() 32 35 const stableID = useMemo(() => Statsig.getStableID(), []) 33 36 34 37 const {mutate: onClearImageCache, isPending: isClearingImageCache} = ··· 153 156 </SettingsList.ItemText> 154 157 <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> 155 158 </SettingsList.PressableItem> 156 - {devModeEnabled && <OTAInfo />} 159 + {devModeEnabled && ( 160 + <> 161 + <OTAInfo /> 162 + {isIOS && ( 163 + <SettingsList.PressableItem 164 + onPress={() => { 165 + const newDemoModeEnabled = !demoModeEnabled 166 + setDemoModeEnabled(newDemoModeEnabled) 167 + Toast.show( 168 + 'Demo mode ' + 169 + (newDemoModeEnabled ? 'enabled' : 'disabled'), 170 + ) 171 + }} 172 + label={ 173 + demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode' 174 + } 175 + disabled={isClearingImageCache}> 176 + <SettingsList.ItemIcon icon={AtomIcon} /> 177 + <SettingsList.ItemText> 178 + {demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode'} 179 + </SettingsList.ItemText> 180 + </SettingsList.PressableItem> 181 + )} 182 + </> 183 + )} 157 184 </SettingsList.Container> 158 185 </Layout.Content> 159 186 </Layout.Screen>
+13 -9
src/state/queries/post-feed.ts
··· 1 1 import React, {useCallback, useEffect, useRef} from 'react' 2 2 import {AppState} from 'react-native' 3 3 import { 4 - AppBskyActorDefs, 4 + type AppBskyActorDefs, 5 5 AppBskyFeedDefs, 6 - AppBskyFeedPost, 6 + type AppBskyFeedPost, 7 7 AtUri, 8 - BskyAgent, 8 + type BskyAgent, 9 9 moderatePost, 10 - ModerationDecision, 10 + type ModerationDecision, 11 11 } from '@atproto/api' 12 12 import { 13 - InfiniteData, 14 - QueryClient, 15 - QueryKey, 13 + type InfiniteData, 14 + type QueryClient, 15 + type QueryKey, 16 16 useInfiniteQuery, 17 17 } from '@tanstack/react-query' 18 18 19 19 import {AuthorFeedAPI} from '#/lib/api/feed/author' 20 20 import {CustomFeedAPI} from '#/lib/api/feed/custom' 21 + import {DemoFeedAPI} from '#/lib/api/feed/demo' 21 22 import {FollowingFeedAPI} from '#/lib/api/feed/following' 22 23 import {HomeFeedAPI} from '#/lib/api/feed/home' 23 24 import {LikesFeedAPI} from '#/lib/api/feed/likes' 24 25 import {ListFeedAPI} from '#/lib/api/feed/list' 25 26 import {MergeFeedAPI} from '#/lib/api/feed/merge' 26 - import {FeedAPI, ReasonFeedSource} from '#/lib/api/feed/types' 27 + import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types' 27 28 import {aggregateUserInterests} from '#/lib/api/feed/utils' 28 - import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip' 29 + import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip' 29 30 import {DISCOVER_FEED_URI} from '#/lib/constants' 30 31 import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' 31 32 import {logger} from '#/logger' ··· 59 60 | `feedgen|${FeedUri}` 60 61 | `likes|${ActorDid}` 61 62 | `list|${ListUri}` 63 + | 'demo' 62 64 export interface FeedParams { 63 65 mergeFeedEnabled?: boolean 64 66 mergeFeedSources?: string[] ··· 483 485 } else if (feedDesc.startsWith('list')) { 484 486 const [_, list] = feedDesc.split('|') 485 487 return new ListFeedAPI({agent, feedParams: {list}}) 488 + } else if (feedDesc === 'demo') { 489 + return new DemoFeedAPI({agent}) 486 490 } else { 487 491 // shouldnt happen 488 492 return new FollowingFeedAPI({agent})
+7
src/storage/hooks/demo-mode.ts
··· 1 + import {device, useStorage} from '#/storage' 2 + 3 + export function useDemoMode() { 4 + const [demoMode = false, setDemoMode] = useStorage(device, ['demoMode']) 5 + 6 + return [demoMode, setDemoMode] as const 7 + }
+1
src/storage/schema.ts
··· 10 10 } 11 11 trendingBetaEnabled: boolean 12 12 devMode: boolean 13 + demoMode: boolean 13 14 } 14 15 15 16 export type Account = {
+10 -6
src/view/com/util/post-embeds/index.tsx
··· 1 1 import React from 'react' 2 2 import { 3 3 InteractionManager, 4 - StyleProp, 4 + type StyleProp, 5 5 StyleSheet, 6 6 View, 7 - ViewStyle, 7 + type ViewStyle, 8 8 } from 'react-native' 9 - import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' 9 + import { 10 + type MeasuredDimensions, 11 + runOnJS, 12 + runOnUI, 13 + } from 'react-native-reanimated' 10 14 import {Image} from 'expo-image' 11 15 import { 12 16 AppBskyEmbedExternal, ··· 18 22 AppBskyGraphDefs, 19 23 moderateFeedGenerator, 20 24 moderateUserList, 21 - ModerationDecision, 25 + type ModerationDecision, 22 26 } from '@atproto/api' 23 27 24 - import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' 28 + import {type HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' 25 29 import {usePalette} from '#/lib/hooks/usePalette' 26 30 import {useLightboxControls} from '#/state/lightbox' 27 31 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 30 34 import * as ListCard from '#/components/ListCard' 31 35 import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 32 36 import {ContentHider} from '../../../../components/moderation/ContentHider' 33 - import {Dimensions} from '../../lightbox/ImageViewing/@types' 37 + import {type Dimensions} from '../../lightbox/ImageViewing/@types' 34 38 import {AutoSizedImage} from '../images/AutoSizedImage' 35 39 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 36 40 import {ExternalLinkEmbed} from './ExternalLinkEmbed'
+58 -7
src/view/screens/Home.tsx
··· 8 8 import {useSetTitle} from '#/lib/hooks/useSetTitle' 9 9 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 10 10 import { 11 - HomeTabNavigatorParams, 12 - NativeStackScreenProps, 11 + type HomeTabNavigatorParams, 12 + type NativeStackScreenProps, 13 13 } from '#/lib/routes/types' 14 14 import {logEvent} from '#/lib/statsig/statsig' 15 15 import {isWeb} from '#/platform/detection' 16 16 import {emitSoftReset} from '#/state/events' 17 - import {SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' 18 - import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' 17 + import { 18 + type SavedFeedSourceInfo, 19 + usePinnedFeedsInfos, 20 + } from '#/state/queries/feed' 21 + import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' 19 22 import {usePreferencesQuery} from '#/state/queries/preferences' 20 - import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 23 + import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 21 24 import {useSession} from '#/state/session' 22 25 import {useSetMinimalShellMode} from '#/state/shell' 23 26 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 24 27 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' 25 28 import {FeedPage} from '#/view/com/feeds/FeedPage' 26 29 import {HomeHeader} from '#/view/com/home/HomeHeader' 27 - import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' 30 + import { 31 + Pager, 32 + type PagerRef, 33 + type RenderTabBarFnProps, 34 + } from '#/view/com/pager/Pager' 28 35 import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState' 29 36 import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' 30 37 import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 31 38 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 32 39 import * as Layout from '#/components/Layout' 40 + import {useDemoMode} from '#/storage/hooks/demo-mode' 33 41 34 42 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> 35 43 export function HomeScreen(props: Props) { ··· 184 192 [setMinimalShellMode], 185 193 ) 186 194 195 + const [demoMode] = useDemoMode() 196 + 187 197 const renderTabBar = React.useCallback( 188 198 (props: RenderTabBarFnProps) => { 199 + if (demoMode) { 200 + return ( 201 + <HomeHeader 202 + key="FEEDS_TAB_BAR" 203 + {...props} 204 + testID="homeScreenFeedTabs" 205 + onPressSelected={onPressSelected} 206 + // @ts-ignore 207 + feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]} 208 + /> 209 + ) 210 + } 189 211 return ( 190 212 <HomeHeader 191 213 key="FEEDS_TAB_BAR" ··· 196 218 /> 197 219 ) 198 220 }, 199 - [onPressSelected, pinnedFeedInfos], 221 + [onPressSelected, pinnedFeedInfos, demoMode], 200 222 ) 201 223 202 224 const renderFollowingEmptyState = React.useCallback(() => { ··· 217 239 : [], 218 240 } 219 241 }, [preferences]) 242 + 243 + if (demoMode) { 244 + return ( 245 + <Pager 246 + ref={pagerRef} 247 + testID="homeScreen" 248 + onPageSelected={onPageSelected} 249 + onPageScrollStateChanged={onPageScrollStateChanged} 250 + renderTabBar={renderTabBar} 251 + initialPage={selectedIndex}> 252 + <FeedPage 253 + testID="demoFeedPage" 254 + isPageFocused 255 + isPageAdjacent={false} 256 + feed="demo" 257 + renderEmptyState={renderCustomFeedEmptyState} 258 + feedInfo={pinnedFeedInfos[0]} 259 + /> 260 + <FeedPage 261 + testID="customFeedPage" 262 + isPageFocused 263 + isPageAdjacent={false} 264 + feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`} 265 + renderEmptyState={renderCustomFeedEmptyState} 266 + feedInfo={pinnedFeedInfos[0]} 267 + /> 268 + </Pager> 269 + ) 270 + } 220 271 221 272 return hasSession ? ( 222 273 <Pager
+9 -5
src/view/shell/bottom-bar/BottomBar.tsx
··· 1 - import React, {ComponentProps} from 'react' 2 - import {GestureResponderEvent, View} from 'react-native' 1 + import React, {type ComponentProps} from 'react' 2 + import {type GestureResponderEvent, View} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 4 import {useSafeAreaInsets} from 'react-native-safe-area-context' 5 5 import {msg, plural, Trans} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 - import {BottomTabBarProps} from '@react-navigation/bottom-tabs' 7 + import {type BottomTabBarProps} from '@react-navigation/bottom-tabs' 8 8 import {StackActions} from '@react-navigation/native' 9 9 10 10 import {PressableScale} from '#/lib/custom-animations/PressableScale' 11 + import {BOTTOM_BAR_AVI} from '#/lib/demo' 11 12 import {useHaptics} from '#/lib/haptics' 12 13 import {useDedupe} from '#/lib/hooks/useDedupe' 13 14 import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' ··· 47 48 Message_Stroke2_Corner0_Rounded as Message, 48 49 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, 49 50 } from '#/components/icons/Message' 51 + import {useDemoMode} from '#/storage/hooks/demo-mode' 50 52 import {styles} from './BottomBarStyles' 51 53 52 54 type TabOptions = ··· 123 125 playHaptic() 124 126 accountSwitchControl.open() 125 127 }, [accountSwitchControl, playHaptic]) 128 + 129 + const [demoMode] = useDemoMode() 126 130 127 131 return ( 128 132 <> ··· 259 263 {borderColor: pal.text.color}, 260 264 ]}> 261 265 <UserAvatar 262 - avatar={profile?.avatar} 266 + avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} 263 267 size={iconWidth - 3} 264 268 // See https://github.com/bluesky-social/social-app/pull/1801: 265 269 usePlainRNImage={true} ··· 270 274 <View 271 275 style={[styles.ctrlIcon, pal.text, styles.profileIcon]}> 272 276 <UserAvatar 273 - avatar={profile?.avatar} 277 + avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} 274 278 size={iconWidth - 3} 275 279 // See https://github.com/bluesky-social/social-app/pull/1801: 276 280 usePlainRNImage={true}