about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/api/feed/demo.ts20
-rw-r--r--src/lib/demo.ts202
-rw-r--r--src/screens/Settings/AboutSettings.tsx31
-rw-r--r--src/state/queries/post-feed.ts22
-rw-r--r--src/storage/hooks/demo-mode.ts7
-rw-r--r--src/storage/schema.ts1
-rw-r--r--src/view/com/util/post-embeds/index.tsx16
-rw-r--r--src/view/screens/Home.tsx65
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx14
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}