diff options
author | Paul Frazee <pfrazee@gmail.com> | 2025-05-02 10:36:43 -0700 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2025-05-02 10:36:43 -0700 |
commit | 46ea3fdbeeab4e31657638955401145683738fbf (patch) | |
tree | 70f3884b0f866cf402f4949f933716b8c45cbd1a /src | |
parent | 077fd843f0d854a07d2cee5dfc3980dbb91db86d (diff) | |
parent | 2aa30f52797f9cc923c674f60156ecc2fb4ed8a6 (diff) | |
download | voidsky-46ea3fdbeeab4e31657638955401145683738fbf.tar.zst |
Merge branch 'demo' into main
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/api/feed/demo.ts | 20 | ||||
-rw-r--r-- | src/lib/demo.ts | 202 | ||||
-rw-r--r-- | src/screens/Settings/AboutSettings.tsx | 31 | ||||
-rw-r--r-- | src/state/queries/post-feed.ts | 22 | ||||
-rw-r--r-- | src/storage/hooks/demo-mode.ts | 7 | ||||
-rw-r--r-- | src/storage/schema.ts | 1 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 16 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 65 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 14 |
9 files changed, 349 insertions, 29 deletions
diff --git a/src/lib/api/feed/demo.ts b/src/lib/api/feed/demo.ts new file mode 100644 index 000000000..049e0f116 --- /dev/null +++ b/src/lib/api/feed/demo.ts @@ -0,0 +1,20 @@ +import {type AppBskyFeedDefs, type BskyAgent} from '@atproto/api' + +import {DEMO_FEED} from '#/lib/demo' +import {type FeedAPI, type FeedAPIResponse} from './types' + +export class DemoFeedAPI implements FeedAPI { + agent: BskyAgent + + constructor({agent}: {agent: BskyAgent}) { + this.agent = agent + } + + async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { + return DEMO_FEED.feed[0] + } + + async fetch(): Promise<FeedAPIResponse> { + return DEMO_FEED + } +} diff --git a/src/lib/demo.ts b/src/lib/demo.ts new file mode 100644 index 000000000..5ead62c9d --- /dev/null +++ b/src/lib/demo.ts @@ -0,0 +1,202 @@ +import {type AppBskyFeedGetFeed} from '@atproto/api' +import {subDays, subMinutes} from 'date-fns' + +const DID = `did:plc:z72i7hdynmk6r22z27h6tvur` +const NOW = new Date() +const POST_1_DATE = subMinutes(NOW, 2).toISOString() +const POST_2_DATE = subMinutes(NOW, 4).toISOString() +const POST_3_DATE = subMinutes(NOW, 5).toISOString() + +export const DEMO_FEED = { + feed: [ + { + post: { + uri: 'at://did:plc:pvooorihapc2lf2pijehgrdf/app.bsky.feed.post/3lniysofyll2d', + cid: 'bafyreihwh3wxxme732ylbylhhdyz7ex6t4jtu6s3gjxxvnnh4feddhg3ku', + author: { + did: 'did:plc:pvooorihapc2lf2pijehgrdf', + handle: 'forkedriverband.bsky.social', + displayName: 'Forked River Band', + avatar: 'https://bsky.social/about/adi/post_1_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: `at://${DID}/app.bsky.graph.follow/post1`, + }, + labels: [], + createdAt: POST_1_DATE, + verification: { + verifications: [ + { + issuer: DID, + uri: `at://${DID}/app.bsky.graph.verification/post1`, + isValid: true, + createdAt: subDays(NOW, 11).toISOString(), + }, + ], + verifiedStatus: 'valid', + trustedVerifierStatus: 'none', + }, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_1_DATE, + // embed: { + // $type: 'app.bsky.embed.images', + // images: [ + // { + // alt: 'Fake flier for Sebastapol Bluegrass Fest', + // aspectRatio: { + // height: 1350, + // width: 900, + // }, + // image: { + // $type: 'blob', + // ref: { + // $link: + // 'bafkreig7gnirmz5guhhjutf3mqbjjzxzi3w4wvs5qy2gnxma5g3brbaidi', + // }, + // mimeType: 'image/jpeg', + // size: 562871, + // }, + // }, + // ], + // }, + langs: ['en'], + 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.', + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://bsky.social/about/adi/post_1_image.jpg', + fullsize: 'https://bsky.social/about/adi/post_1_image.jpg', + alt: 'Fake flier for Sebastapol Bluegrass Fest', + aspectRatio: { + height: 1350, + width: 900, + }, + }, + ], + }, + replyCount: 1, + repostCount: 4, + likeCount: 18, + quoteCount: 0, + indexedAt: POST_1_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + { + post: { + uri: 'at://did:plc:fhhqii56ppgyh5qcm2b3mokf/app.bsky.feed.post/3lnizc7fug52c', + cid: 'bafyreienuabsr55rycirdf4ewue5tjcseg5lzqompcsh2brqzag6hvxllm', + author: { + did: 'did:plc:fhhqii56ppgyh5qcm2b3mokf', + handle: 'dinh-designs.bsky.social', + displayName: 'Rich Dinh Designs', + avatar: 'https://bsky.social/about/adi/post_2_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: `at://${DID}/app.bsky.graph.follow/post2`, + }, + labels: [], + createdAt: POST_2_DATE, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_2_DATE, + // embed: { + // $type: 'app.bsky.embed.images', + // images: [ + // { + // alt: 'Placeholder image of interior design', + // aspectRatio: { + // height: 872, + // width: 598, + // }, + // image: { + // $type: 'blob', + // ref: { + // $link: + // 'bafkreidcjc6bjb4jjjejruin5cldhj5zovsuu4tydulenyprneziq5rfeu', + // }, + // mimeType: 'image/jpeg', + // size: 296003, + // }, + // }, + // ], + // }, + langs: ['en'], + 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.', + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://bsky.social/about/adi/post_2_image.jpg', + fullsize: 'https://bsky.social/about/adi/post_2_image.jpg', + alt: 'Placeholder image of interior design', + aspectRatio: { + height: 872, + width: 598, + }, + }, + ], + }, + replyCount: 3, + repostCount: 1, + likeCount: 4, + quoteCount: 0, + indexedAt: POST_2_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + { + post: { + uri: 'at://did:plc:h7fwnfejmmifveeea5eyxgkc/app.bsky.feed.post/3lnizna3g4f2t', + cid: 'bafyreiepn7obmlshliori4j34texpaukrqkyyu7cq6nmpzk4lkis7nqeae', + author: { + did: 'did:plc:h7fwnfejmmifveeea5eyxgkc', + handle: 'rodyalbuerne.bsky.social', + displayName: 'Rody Albuerne', + avatar: 'https://bsky.social/about/adi/post_3_avi.jpg', + viewer: { + muted: false, + blockedBy: false, + following: `at://${DID}/app.bsky.graph.follow/post3`, + }, + labels: [], + createdAt: POST_3_DATE, + }, + record: { + $type: 'app.bsky.feed.post', + createdAt: POST_3_DATE, + langs: ['en'], + 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.', + }, + replyCount: 11, + repostCount: 97, + likeCount: 399, + quoteCount: 0, + indexedAt: POST_3_DATE, + viewer: { + threadMuted: false, + embeddingDisabled: false, + }, + labels: [], + }, + }, + ], +} satisfies AppBskyFeedGetFeed.OutputSchema + +export const BOTTOM_BAR_AVI = 'https://bsky.social/about/adi/user_avi.jpg' diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx index 199d12e63..0ce127ff3 100644 --- a/src/screens/Settings/AboutSettings.tsx +++ b/src/screens/Settings/AboutSettings.tsx @@ -12,9 +12,10 @@ import {Statsig} from 'statsig-react-native-expo' import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' import {STATUS_PAGE_URL} from '#/lib/constants' import {type CommonNavigatorParams} from '#/lib/routes/types' -import {isAndroid, isNative} from '#/platform/detection' +import {isAndroid, isIOS, isNative} from '#/platform/detection' import * as Toast from '#/view/com/util/Toast' import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle' import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines' import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' @@ -22,6 +23,7 @@ import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/i import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' +import {useDemoMode} from '#/storage/hooks/demo-mode' import {useDevMode} from '#/storage/hooks/dev-mode' import {OTAInfo} from './components/OTAInfo' @@ -29,6 +31,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AboutSettings'> export function AboutSettingsScreen({}: Props) { const {_, i18n} = useLingui() const [devModeEnabled, setDevModeEnabled] = useDevMode() + const [demoModeEnabled, setDemoModeEnabled] = useDemoMode() const stableID = useMemo(() => Statsig.getStableID(), []) const {mutate: onClearImageCache, isPending: isClearingImageCache} = @@ -153,7 +156,31 @@ export function AboutSettingsScreen({}: Props) { </SettingsList.ItemText> <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> </SettingsList.PressableItem> - {devModeEnabled && <OTAInfo />} + {devModeEnabled && ( + <> + <OTAInfo /> + {isIOS && ( + <SettingsList.PressableItem + onPress={() => { + const newDemoModeEnabled = !demoModeEnabled + setDemoModeEnabled(newDemoModeEnabled) + Toast.show( + 'Demo mode ' + + (newDemoModeEnabled ? 'enabled' : 'disabled'), + ) + }} + label={ + demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode' + } + disabled={isClearingImageCache}> + <SettingsList.ItemIcon icon={AtomIcon} /> + <SettingsList.ItemText> + {demoModeEnabled ? 'Disable demo mode' : 'Enable demo mode'} + </SettingsList.ItemText> + </SettingsList.PressableItem> + )} + </> + )} </SettingsList.Container> </Layout.Content> </Layout.Screen> diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 82a118ec2..f3fa13cfb 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -1,31 +1,32 @@ import React, {useCallback, useEffect, useRef} from 'react' import {AppState} from 'react-native' import { - AppBskyActorDefs, + type AppBskyActorDefs, AppBskyFeedDefs, - AppBskyFeedPost, + type AppBskyFeedPost, AtUri, - BskyAgent, + type BskyAgent, moderatePost, - ModerationDecision, + type ModerationDecision, } from '@atproto/api' import { - InfiniteData, - QueryClient, - QueryKey, + type InfiniteData, + type QueryClient, + type QueryKey, useInfiniteQuery, } from '@tanstack/react-query' import {AuthorFeedAPI} from '#/lib/api/feed/author' import {CustomFeedAPI} from '#/lib/api/feed/custom' +import {DemoFeedAPI} from '#/lib/api/feed/demo' import {FollowingFeedAPI} from '#/lib/api/feed/following' import {HomeFeedAPI} from '#/lib/api/feed/home' import {LikesFeedAPI} from '#/lib/api/feed/likes' import {ListFeedAPI} from '#/lib/api/feed/list' import {MergeFeedAPI} from '#/lib/api/feed/merge' -import {FeedAPI, ReasonFeedSource} from '#/lib/api/feed/types' +import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types' import {aggregateUserInterests} from '#/lib/api/feed/utils' -import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip' +import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip' import {DISCOVER_FEED_URI} from '#/lib/constants' import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' import {logger} from '#/logger' @@ -59,6 +60,7 @@ export type FeedDescriptor = | `feedgen|${FeedUri}` | `likes|${ActorDid}` | `list|${ListUri}` + | 'demo' export interface FeedParams { mergeFeedEnabled?: boolean mergeFeedSources?: string[] @@ -483,6 +485,8 @@ function createApi({ } else if (feedDesc.startsWith('list')) { const [_, list] = feedDesc.split('|') return new ListFeedAPI({agent, feedParams: {list}}) + } else if (feedDesc === 'demo') { + return new DemoFeedAPI({agent}) } else { // shouldnt happen return new FollowingFeedAPI({agent}) diff --git a/src/storage/hooks/demo-mode.ts b/src/storage/hooks/demo-mode.ts new file mode 100644 index 000000000..b65dd147e --- /dev/null +++ b/src/storage/hooks/demo-mode.ts @@ -0,0 +1,7 @@ +import {device, useStorage} from '#/storage' + +export function useDemoMode() { + const [demoMode = false, setDemoMode] = useStorage(device, ['demoMode']) + + return [demoMode, setDemoMode] as const +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 0e9b1985c..7430532a9 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -10,6 +10,7 @@ export type Device = { } trendingBetaEnabled: boolean devMode: boolean + demoMode: boolean } export type Account = { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 6f5f9d3ab..431baa2b2 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,12 +1,16 @@ import React from 'react' import { InteractionManager, - StyleProp, + type StyleProp, StyleSheet, View, - ViewStyle, + type ViewStyle, } from 'react-native' -import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' +import { + type MeasuredDimensions, + runOnJS, + runOnUI, +} from 'react-native-reanimated' import {Image} from 'expo-image' import { AppBskyEmbedExternal, @@ -18,10 +22,10 @@ import { AppBskyGraphDefs, moderateFeedGenerator, moderateUserList, - ModerationDecision, + type ModerationDecision, } from '@atproto/api' -import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' +import {type HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useLightboxControls} from '#/state/lightbox' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -30,7 +34,7 @@ import {atoms as a, useTheme} from '#/alf' import * as ListCard from '#/components/ListCard' import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {ContentHider} from '../../../../components/moderation/ContentHider' -import {Dimensions} from '../../lightbox/ImageViewing/@types' +import {type Dimensions} from '../../lightbox/ImageViewing/@types' import {AutoSizedImage} from '../images/AutoSizedImage' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ExternalLinkEmbed} from './ExternalLinkEmbed' diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index a6e2595ee..e058e2883 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -8,28 +8,36 @@ import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import { - HomeTabNavigatorParams, - NativeStackScreenProps, + type HomeTabNavigatorParams, + type NativeStackScreenProps, } from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {isWeb} from '#/platform/detection' import {emitSoftReset} from '#/state/events' -import {SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' -import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' +import { + type SavedFeedSourceInfo, + usePinnedFeedsInfos, +} from '#/state/queries/feed' +import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' import {usePreferencesQuery} from '#/state/queries/preferences' -import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' +import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' import {FeedPage} from '#/view/com/feeds/FeedPage' import {HomeHeader} from '#/view/com/home/HomeHeader' -import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' +import { + Pager, + type PagerRef, + type RenderTabBarFnProps, +} from '#/view/com/pager/Pager' import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' import * as Layout from '#/components/Layout' +import {useDemoMode} from '#/storage/hooks/demo-mode' type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> export function HomeScreen(props: Props) { @@ -184,8 +192,22 @@ function HomeScreenReady({ [setMinimalShellMode], ) + const [demoMode] = useDemoMode() + const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { + if (demoMode) { + return ( + <HomeHeader + key="FEEDS_TAB_BAR" + {...props} + testID="homeScreenFeedTabs" + onPressSelected={onPressSelected} + // @ts-ignore + feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]} + /> + ) + } return ( <HomeHeader key="FEEDS_TAB_BAR" @@ -196,7 +218,7 @@ function HomeScreenReady({ /> ) }, - [onPressSelected, pinnedFeedInfos], + [onPressSelected, pinnedFeedInfos, demoMode], ) const renderFollowingEmptyState = React.useCallback(() => { @@ -218,6 +240,35 @@ function HomeScreenReady({ } }, [preferences]) + if (demoMode) { + return ( + <Pager + ref={pagerRef} + testID="homeScreen" + onPageSelected={onPageSelected} + onPageScrollStateChanged={onPageScrollStateChanged} + renderTabBar={renderTabBar} + initialPage={selectedIndex}> + <FeedPage + testID="demoFeedPage" + isPageFocused + isPageAdjacent={false} + feed="demo" + renderEmptyState={renderCustomFeedEmptyState} + feedInfo={pinnedFeedInfos[0]} + /> + <FeedPage + testID="customFeedPage" + isPageFocused + isPageAdjacent={false} + feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`} + renderEmptyState={renderCustomFeedEmptyState} + feedInfo={pinnedFeedInfos[0]} + /> + </Pager> + ) + } + return hasSession ? ( <Pager key={allFeeds.join(',')} diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 822547d93..df6a045dc 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -1,13 +1,14 @@ -import React, {ComponentProps} from 'react' -import {GestureResponderEvent, View} from 'react-native' +import React, {type ComponentProps} from 'react' +import {type GestureResponderEvent, View} from 'react-native' import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {BottomTabBarProps} from '@react-navigation/bottom-tabs' +import {type BottomTabBarProps} from '@react-navigation/bottom-tabs' import {StackActions} from '@react-navigation/native' import {PressableScale} from '#/lib/custom-animations/PressableScale' +import {BOTTOM_BAR_AVI} from '#/lib/demo' import {useHaptics} from '#/lib/haptics' import {useDedupe} from '#/lib/hooks/useDedupe' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' @@ -47,6 +48,7 @@ import { Message_Stroke2_Corner0_Rounded as Message, Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, } from '#/components/icons/Message' +import {useDemoMode} from '#/storage/hooks/demo-mode' import {styles} from './BottomBarStyles' type TabOptions = @@ -124,6 +126,8 @@ export function BottomBar({navigation}: BottomTabBarProps) { accountSwitchControl.open() }, [accountSwitchControl, playHaptic]) + const [demoMode] = useDemoMode() + return ( <> <SwitchAccountDialog control={accountSwitchControl} /> @@ -259,7 +263,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { {borderColor: pal.text.color}, ]}> <UserAvatar - avatar={profile?.avatar} + avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} size={iconWidth - 3} // See https://github.com/bluesky-social/social-app/pull/1801: usePlainRNImage={true} @@ -270,7 +274,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}> <UserAvatar - avatar={profile?.avatar} + avatar={demoMode ? BOTTOM_BAR_AVI : profile?.avatar} size={iconWidth - 3} // See https://github.com/bluesky-social/social-app/pull/1801: usePlainRNImage={true} |