about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app.config.js4
-rw-r--r--babel.config.js18
-rw-r--r--eas.json8
-rw-r--r--metro.config.js18
-rw-r--r--package.json4
-rw-r--r--patches/babel-preset-expo+9.5.2.patch14
-rw-r--r--patches/babel-preset-fbjs+3.4.0.patch12
-rw-r--r--src/App.native.tsx11
-rw-r--r--src/Navigation.tsx93
-rw-r--r--src/lib/analytics/analytics.tsx6
-rw-r--r--src/lib/analytics/analytics.web.tsx6
-rw-r--r--src/lib/strings/headings.ts2
-rw-r--r--src/state/models/feeds/post.ts14
-rw-r--r--src/view/com/auth/create/Step2.tsx14
-rw-r--r--src/view/com/posts/FeedItem.tsx6
-rw-r--r--src/view/com/profile/ProfileHeader.tsx17
-rw-r--r--src/view/com/util/Link.tsx13
-rw-r--r--src/view/com/util/PostMeta.tsx6
-rw-r--r--src/view/com/util/UserInfoText.tsx4
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx12
-rw-r--r--src/view/shell/desktop/LeftNav.tsx12
-rw-r--r--src/view/shell/index.tsx7
-rw-r--r--yarn.lock59
23 files changed, 218 insertions, 142 deletions
diff --git a/app.config.js b/app.config.js
index 1f4de0370..e5d7fdf41 100644
--- a/app.config.js
+++ b/app.config.js
@@ -6,7 +6,7 @@ module.exports = function () {
       slug: 'bluesky',
       scheme: 'bluesky',
       owner: 'blueskysocial',
-      version: '1.53.0',
+      version: '1.55.0',
       runtimeVersion: {
         policy: 'appVersion',
       },
@@ -43,7 +43,7 @@ module.exports = function () {
         backgroundColor: '#ffffff',
       },
       android: {
-        versionCode: 42,
+        versionCode: 44,
         adaptiveIcon: {
           foregroundImage: './assets/adaptive-icon.png',
           backgroundColor: '#ffffff',
diff --git a/babel.config.js b/babel.config.js
index 706fdff5c..78edf5749 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,7 +1,23 @@
 module.exports = function (api) {
   api.cache(true)
+  const isTestEnv = process.env.NODE_ENV === 'test'
   return {
-    presets: ['babel-preset-expo'],
+    presets: [
+      [
+        'babel-preset-expo',
+        {
+          lazyImports: true,
+          native: {
+            // We should be able to remove this after upgrading Expo
+            // to a version that includes https://github.com/expo/expo/pull/24672.
+            unstable_transformProfile: 'hermes-stable',
+            // Disable ESM -> CJS compilation because Metro takes care of it.
+            // However, we need it in Jest tests since those run without Metro.
+            disableImportExportTransform: !isTestEnv,
+          },
+        },
+      ],
+    ],
     plugins: [
       [
         'module:react-native-dotenv',
diff --git a/eas.json b/eas.json
index 402abeccd..69e5c94d6 100644
--- a/eas.json
+++ b/eas.json
@@ -9,7 +9,7 @@
       "distribution": "internal",
       "ios": {
         "simulator": true,
-        "resourceClass": "large"
+        "resourceClass": "m-large"
       },
       "channel": "development"
     },
@@ -17,20 +17,20 @@
       "developmentClient": true,
       "distribution": "internal",
       "ios": {
-        "resourceClass": "large"
+        "resourceClass": "m-large"
       },
       "channel": "development"
     },
     "preview": {
       "distribution": "internal",
       "ios": {
-        "resourceClass": "large"
+        "resourceClass": "m-large"
       },
       "channel": "preview"
     },
     "production": {
       "ios": {
-        "resourceClass": "large"
+        "resourceClass": "m-large"
       },
       "channel": "production"
     },
diff --git a/metro.config.js b/metro.config.js
index b1714479f..a49d95f9a 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -1,7 +1,25 @@
 // Learn more https://docs.expo.io/guides/customizing-metro
 const {getDefaultConfig} = require('expo/metro-config')
 const cfg = getDefaultConfig(__dirname)
+
 cfg.resolver.sourceExts = process.env.RN_SRC_EXT
   ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
   : cfg.resolver.sourceExts
+
+cfg.transformer.getTransformOptions = async () => ({
+  transform: {
+    experimentalImportSupport: true,
+    inlineRequires: true,
+    nonInlinedRequires: [
+      // We can remove this option and rely on the default after
+      // https://github.com/facebook/metro/pull/1126 is released.
+      'React',
+      'react',
+      'react/jsx-dev-runtime',
+      'react/jsx-runtime',
+      'react-native',
+    ],
+  },
+})
+
 module.exports = cfg
diff --git a/package.json b/package.json
index c058c5ce6..3a91528cc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.53.0",
+  "version": "1.55.0",
   "private": true,
   "scripts": {
     "prepare": "is-ci || husky install",
@@ -31,7 +31,7 @@
     "build:apk": "eas build -p android --profile dev-android-apk"
   },
   "dependencies": {
-    "@atproto/api": "^0.6.20",
+    "@atproto/api": "^0.6.21",
     "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@braintree/sanitize-url": "^6.0.2",
     "@emoji-mart/react": "^1.1.1",
diff --git a/patches/babel-preset-expo+9.5.2.patch b/patches/babel-preset-expo+9.5.2.patch
new file mode 100644
index 000000000..5e328c224
--- /dev/null
+++ b/patches/babel-preset-expo+9.5.2.patch
@@ -0,0 +1,14 @@
+diff --git a/node_modules/babel-preset-expo/index.js b/node_modules/babel-preset-expo/index.js
+index 2099ee3..2b9e092 100644
+--- a/node_modules/babel-preset-expo/index.js
++++ b/node_modules/babel-preset-expo/index.js
+@@ -105,7 +105,8 @@ module.exports = function (api, options = {}) {
+       ],
+     ],
+     plugins: [
+-      getObjectRestSpreadPlugin(),
++      // - dan: This will be disabled anyway when we upgrade Expo, but let's do it now.
++      // getObjectRestSpreadPlugin(),
+       ...extraPlugins,
+       getAliasPlugin(),
+       [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
diff --git a/patches/babel-preset-fbjs+3.4.0.patch b/patches/babel-preset-fbjs+3.4.0.patch
new file mode 100644
index 000000000..a66f7c7d8
--- /dev/null
+++ b/patches/babel-preset-fbjs+3.4.0.patch
@@ -0,0 +1,12 @@
+diff --git a/node_modules/babel-preset-fbjs/plugins/inline-requires.js b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
+index b11fc83..e18661a 100644
+--- a/node_modules/babel-preset-fbjs/plugins/inline-requires.js
++++ b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
+@@ -256,6 +256,7 @@ function getInlineableModule(path, state) {
+ 
+   return moduleName == null ||
+     state.ignoredRequires.has(moduleName) ||
++    moduleName.startsWith('@babel/runtime/') ||
+     isRequireInScope
+     ? null
+     : { moduleName, requireFnName: fnName };
diff --git a/src/App.native.tsx b/src/App.native.tsx
index f99e976ce..f4298c461 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -2,7 +2,6 @@ import 'react-native-url-polyfill/auto'
 import React, {useState, useEffect} from 'react'
 import 'lib/sentry' // must be relatively on top
 import {withSentry} from 'lib/sentry'
-import {Linking} from 'react-native'
 import {RootSiblingParent} from 'react-native-root-siblings'
 import * as SplashScreen from 'expo-splash-screen'
 import {GestureHandlerRootView} from 'react-native-gesture-handler'
@@ -15,7 +14,6 @@ import {Shell} from './view/shell'
 import * as notifications from 'lib/notifications/notifications'
 import * as analytics from 'lib/analytics/analytics'
 import * as Toast from './view/com/util/Toast'
-import {handleLink} from './Navigation'
 import {QueryClientProvider} from '@tanstack/react-query'
 import {queryClient} from 'lib/react-query'
 import {TestCtrls} from 'view/com/testing/TestCtrls'
@@ -34,15 +32,6 @@ const App = observer(function AppImpl() {
       setRootStore(store)
       analytics.init(store)
       notifications.init(store)
-      SplashScreen.hideAsync()
-      Linking.getInitialURL().then((url: string | null) => {
-        if (url) {
-          handleLink(url)
-        }
-      })
-      Linking.addEventListener('url', ({url}) => {
-        handleLink(url)
-      })
       store.onSessionDropped(() => {
         Toast.show('Sorry! Your session expired. Please log in again.')
       })
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index e1d5e76aa..a75651987 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react'
 import {StyleSheet} from 'react-native'
+import * as SplashScreen from 'expo-splash-screen'
 import {observer} from 'mobx-react-lite'
 import {
   NavigationContainer,
@@ -91,42 +92,42 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
     <>
       <Stack.Screen
         name="NotFound"
-        component={NotFoundScreen}
+        getComponent={() => NotFoundScreen}
         options={{title: title('Not Found')}}
       />
       <Stack.Screen
         name="Moderation"
-        component={ModerationScreen}
+        getComponent={() => ModerationScreen}
         options={{title: title('Moderation')}}
       />
       <Stack.Screen
         name="ModerationMuteLists"
-        component={ModerationMuteListsScreen}
+        getComponent={() => ModerationMuteListsScreen}
         options={{title: title('Mute Lists')}}
       />
       <Stack.Screen
         name="ModerationMutedAccounts"
-        component={ModerationMutedAccounts}
+        getComponent={() => ModerationMutedAccounts}
         options={{title: title('Muted Accounts')}}
       />
       <Stack.Screen
         name="ModerationBlockedAccounts"
-        component={ModerationBlockedAccounts}
+        getComponent={() => ModerationBlockedAccounts}
         options={{title: title('Blocked Accounts')}}
       />
       <Stack.Screen
         name="Settings"
-        component={SettingsScreen}
+        getComponent={() => SettingsScreen}
         options={{title: title('Settings')}}
       />
       <Stack.Screen
         name="LanguageSettings"
-        component={LanguageSettingsScreen}
+        getComponent={() => LanguageSettingsScreen}
         options={{title: title('Language Settings')}}
       />
       <Stack.Screen
         name="Profile"
-        component={ProfileScreen}
+        getComponent={() => ProfileScreen}
         options={({route}) => ({
           title: title(`@${route.params.name}`),
           animation: 'none',
@@ -134,101 +135,101 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
       />
       <Stack.Screen
         name="ProfileFollowers"
-        component={ProfileFollowersScreen}
+        getComponent={() => ProfileFollowersScreen}
         options={({route}) => ({
           title: title(`People following @${route.params.name}`),
         })}
       />
       <Stack.Screen
         name="ProfileFollows"
-        component={ProfileFollowsScreen}
+        getComponent={() => ProfileFollowsScreen}
         options={({route}) => ({
           title: title(`People followed by @${route.params.name}`),
         })}
       />
       <Stack.Screen
         name="ProfileList"
-        component={ProfileListScreen}
+        getComponent={() => ProfileListScreen}
         options={{title: title('Mute List')}}
       />
       <Stack.Screen
         name="PostThread"
-        component={PostThreadScreen}
+        getComponent={() => PostThreadScreen}
         options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
       />
       <Stack.Screen
         name="PostLikedBy"
-        component={PostLikedByScreen}
+        getComponent={() => PostLikedByScreen}
         options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
       />
       <Stack.Screen
         name="PostRepostedBy"
-        component={PostRepostedByScreen}
+        getComponent={() => PostRepostedByScreen}
         options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
       />
       <Stack.Screen
         name="CustomFeed"
-        component={CustomFeedScreen}
+        getComponent={() => CustomFeedScreen}
         options={{title: title('Feed')}}
       />
       <Stack.Screen
         name="CustomFeedLikedBy"
-        component={CustomFeedLikedByScreen}
+        getComponent={() => CustomFeedLikedByScreen}
         options={{title: title('Liked by')}}
       />
       <Stack.Screen
         name="Debug"
-        component={DebugScreen}
+        getComponent={() => DebugScreen}
         options={{title: title('Debug')}}
       />
       <Stack.Screen
         name="Log"
-        component={LogScreen}
+        getComponent={() => LogScreen}
         options={{title: title('Log')}}
       />
       <Stack.Screen
         name="Support"
-        component={SupportScreen}
+        getComponent={() => SupportScreen}
         options={{title: title('Support')}}
       />
       <Stack.Screen
         name="PrivacyPolicy"
-        component={PrivacyPolicyScreen}
+        getComponent={() => PrivacyPolicyScreen}
         options={{title: title('Privacy Policy')}}
       />
       <Stack.Screen
         name="TermsOfService"
-        component={TermsOfServiceScreen}
+        getComponent={() => TermsOfServiceScreen}
         options={{title: title('Terms of Service')}}
       />
       <Stack.Screen
         name="CommunityGuidelines"
-        component={CommunityGuidelinesScreen}
+        getComponent={() => CommunityGuidelinesScreen}
         options={{title: title('Community Guidelines')}}
       />
       <Stack.Screen
         name="CopyrightPolicy"
-        component={CopyrightPolicyScreen}
+        getComponent={() => CopyrightPolicyScreen}
         options={{title: title('Copyright Policy')}}
       />
       <Stack.Screen
         name="AppPasswords"
-        component={AppPasswords}
+        getComponent={() => AppPasswords}
         options={{title: title('App Passwords')}}
       />
       <Stack.Screen
         name="SavedFeeds"
-        component={SavedFeeds}
+        getComponent={() => SavedFeeds}
         options={{title: title('Edit My Feeds')}}
       />
       <Stack.Screen
         name="PreferencesHomeFeed"
-        component={PreferencesHomeFeed}
+        getComponent={() => PreferencesHomeFeed}
         options={{title: title('Home Feed Preferences')}}
       />
       <Stack.Screen
         name="PreferencesThreads"
-        component={PreferencesThreads}
+        getComponent={() => PreferencesThreads}
         options={{title: title('Threads Preferences')}}
       />
     </>
@@ -253,14 +254,17 @@ function TabsNavigator() {
       backBehavior="initialRoute"
       screenOptions={{headerShown: false, lazy: true}}
       tabBar={tabBar}>
-      <Tab.Screen name="HomeTab" component={HomeTabNavigator} />
-      <Tab.Screen name="SearchTab" component={SearchTabNavigator} />
-      <Tab.Screen name="FeedsTab" component={FeedsTabNavigator} />
+      <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
+      <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
+      <Tab.Screen name="FeedsTab" getComponent={() => FeedsTabNavigator} />
       <Tab.Screen
         name="NotificationsTab"
-        component={NotificationsTabNavigator}
+        getComponent={() => NotificationsTabNavigator}
+      />
+      <Tab.Screen
+        name="MyProfileTab"
+        getComponent={() => MyProfileTabNavigator}
       />
-      <Tab.Screen name="MyProfileTab" component={MyProfileTabNavigator} />
     </Tab.Navigator>
   )
 }
@@ -277,7 +281,7 @@ function HomeTabNavigator() {
         animationDuration: 250,
         contentStyle,
       }}>
-      <HomeTab.Screen name="Home" component={HomeScreen} />
+      <HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
       {commonScreens(HomeTab)}
     </HomeTab.Navigator>
   )
@@ -294,7 +298,7 @@ function SearchTabNavigator() {
         animationDuration: 250,
         contentStyle,
       }}>
-      <SearchTab.Screen name="Search" component={SearchScreen} />
+      <SearchTab.Screen name="Search" getComponent={() => SearchScreen} />
       {commonScreens(SearchTab as typeof HomeTab)}
     </SearchTab.Navigator>
   )
@@ -311,7 +315,7 @@ function FeedsTabNavigator() {
         animationDuration: 250,
         contentStyle,
       }}>
-      <FeedsTab.Screen name="Feeds" component={FeedsScreen} />
+      <FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
       {commonScreens(FeedsTab as typeof HomeTab)}
     </FeedsTab.Navigator>
   )
@@ -330,7 +334,7 @@ function NotificationsTabNavigator() {
       }}>
       <NotificationsTab.Screen
         name="Notifications"
-        component={NotificationsScreen}
+        getComponent={() => NotificationsScreen}
       />
       {commonScreens(NotificationsTab as typeof HomeTab)}
     </NotificationsTab.Navigator>
@@ -352,7 +356,7 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
       <MyProfileTab.Screen
         name="MyProfile"
         // @ts-ignore // TODO: fix this broken type in ProfileScreen
-        component={ProfileScreen}
+        getComponent={() => ProfileScreen}
         initialParams={{
           name: store.me.did,
         }}
@@ -383,22 +387,22 @@ const FlatNavigator = observer(function FlatNavigatorImpl() {
       }}>
       <Flat.Screen
         name="Home"
-        component={HomeScreen}
+        getComponent={() => HomeScreen}
         options={{title: title('Home')}}
       />
       <Flat.Screen
         name="Search"
-        component={SearchScreen}
+        getComponent={() => SearchScreen}
         options={{title: title('Search')}}
       />
       <Flat.Screen
         name="Feeds"
-        component={FeedsScreen}
+        getComponent={() => FeedsScreen}
         options={{title: title('Feeds')}}
       />
       <Flat.Screen
         name="Notifications"
-        component={NotificationsScreen}
+        getComponent={() => NotificationsScreen}
         options={{title: title('Notifications')}}
       />
       {commonScreens(Flat as typeof HomeTab, unreadCountLabel)}
@@ -462,6 +466,13 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
       linking={LINKING}
       theme={theme}
       onReady={() => {
+        SplashScreen.hideAsync()
+        const initMs = Math.round(
+          // @ts-ignore Emitted by Metro in the bundle prelude
+          performance.now() - global.__BUNDLE_START_TIME__,
+        )
+        console.log(`Time to first paint: ${initMs} ms`)
+
         // Register the navigation container with the Sentry instrumentation (only works on native)
         if (isNative) {
           const routingInstrumentation = getRoutingInstrumentation()
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx
index d1eb50f8a..b3db9149c 100644
--- a/src/lib/analytics/analytics.tsx
+++ b/src/lib/analytics/analytics.tsx
@@ -51,10 +51,10 @@ export function init(store: RootStoreModel) {
   store.onSessionLoaded(() => {
     const sess = store.session.currentSession
     if (sess) {
-      if (sess.email) {
+      if (sess.did) {
+        const did_hashed = sha256(sess.did)
+        segmentClient.identify(did_hashed, {did_hashed})
         store.log.debug('Ping w/hash')
-        const email_hashed = sha256(sess.email)
-        segmentClient.identify(email_hashed, {email_hashed})
       } else {
         store.log.debug('Ping w/o hash')
         segmentClient.identify()
diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx
index db9d86e3c..78bd9b42b 100644
--- a/src/lib/analytics/analytics.web.tsx
+++ b/src/lib/analytics/analytics.web.tsx
@@ -46,10 +46,10 @@ export function init(store: RootStoreModel) {
   store.onSessionLoaded(() => {
     const sess = store.session.currentSession
     if (sess) {
-      if (sess.email) {
+      if (sess.did) {
+        const did_hashed = sha256(sess.did)
+        segmentClient.identify(did_hashed, {did_hashed})
         store.log.debug('Ping w/hash')
-        const email_hashed = sha256(sess.email)
-        segmentClient.identify(email_hashed, {email_hashed})
       } else {
         store.log.debug('Ping w/o hash')
         segmentClient.identify()
diff --git a/src/lib/strings/headings.ts b/src/lib/strings/headings.ts
index a88a69645..f9a062492 100644
--- a/src/lib/strings/headings.ts
+++ b/src/lib/strings/headings.ts
@@ -1,4 +1,4 @@
 export function bskyTitle(page: string, unreadCountLabel?: string) {
   const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : ''
-  return `${unreadPrefix}${page} - Bluesky`
+  return `${unreadPrefix}${page} — Bluesky`
 }
diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts
index ae4f29105..d46cced75 100644
--- a/src/state/models/feeds/post.ts
+++ b/src/state/models/feeds/post.ts
@@ -116,6 +116,7 @@ export class PostsFeedItemModel {
           },
           () => this.rootStore.agent.deleteLike(url),
         )
+        track('Post:Unlike')
       } else {
         // like
         await updateDataOptimistically(
@@ -129,11 +130,10 @@ export class PostsFeedItemModel {
             this.post.viewer!.like = res.uri
           },
         )
+        track('Post:Like')
       }
     } catch (error) {
       this.rootStore.log.error('Failed to toggle like', error)
-    } finally {
-      track(this.post.viewer.like ? 'Post:Unlike' : 'Post:Like')
     }
   }
 
@@ -141,6 +141,7 @@ export class PostsFeedItemModel {
     this.post.viewer = this.post.viewer || {}
     try {
       if (this.post.viewer?.repost) {
+        // unrepost
         const url = this.post.viewer.repost
         await updateDataOptimistically(
           this.post,
@@ -150,7 +151,9 @@ export class PostsFeedItemModel {
           },
           () => this.rootStore.agent.deleteRepost(url),
         )
+        track('Post:Unrepost')
       } else {
+        // repost
         await updateDataOptimistically(
           this.post,
           () => {
@@ -162,11 +165,10 @@ export class PostsFeedItemModel {
             this.post.viewer!.repost = res.uri
           },
         )
+        track('Post:Repost')
       }
     } catch (error) {
       this.rootStore.log.error('Failed to toggle repost', error)
-    } finally {
-      track(this.post.viewer.repost ? 'Post:Unrepost' : 'Post:Repost')
     }
   }
 
@@ -174,13 +176,13 @@ export class PostsFeedItemModel {
     try {
       if (this.isThreadMuted) {
         this.rootStore.mutedThreads.uris.delete(this.rootUri)
+        track('Post:ThreadUnmute')
       } else {
         this.rootStore.mutedThreads.uris.add(this.rootUri)
+        track('Post:ThreadMute')
       }
     } catch (error) {
       this.rootStore.log.error('Failed to toggle thread mute', error)
-    } finally {
-      track(this.isThreadMuted ? 'Post:ThreadUnmute' : 'Post:ThreadMute')
     }
   }
 
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
index 83b0aee40..60e197564 100644
--- a/src/view/com/auth/create/Step2.tsx
+++ b/src/view/com/auth/create/Step2.tsx
@@ -11,6 +11,7 @@ import {TextInput} from '../util/TextInput'
 import {Policies} from './Policies'
 import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
 import {useStores} from 'state/index'
+import {isWeb} from 'platform/detection'
 
 /** STEP 2: Your account
  * @field Invite code or waitlist
@@ -60,10 +61,11 @@ export const Step2 = observer(function Step2Impl({
           Don't have an invite code?{' '}
           <TouchableWithoutFeedback
             onPress={onPressWaitlist}
-            accessibilityRole="button"
-            accessibilityLabel="Waitlist"
-            accessibilityHint="Opens Bluesky waitlist form">
-            <Text style={pal.link}>Join the waitlist.</Text>
+            accessibilityLabel="Join the waitlist."
+            accessibilityHint="">
+            <View style={styles.touchable}>
+              <Text style={pal.link}>Join the waitlist.</Text>
+            </View>
           </TouchableWithoutFeedback>
         </Text>
       ) : (
@@ -151,4 +153,8 @@ const styles = StyleSheet.create({
     borderRadius: 6,
     paddingVertical: 14,
   },
+  // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
+  touchable: {
+    ...(isWeb && {cursor: 'pointer'}),
+  },
 })
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 77c71f26d..441621638 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -9,7 +9,7 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {PostsFeedItemModel} from 'state/models/feeds/post'
 import {FeedSourceInfo} from 'lib/api/feed/types'
-import {Link, DesktopWebTextLink, TextLink} from '../util/Link'
+import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
@@ -198,7 +198,7 @@ export const FeedItem = observer(function FeedItemImpl({
                 lineHeight={1.2}
                 numberOfLines={1}>
                 From{' '}
-                <DesktopWebTextLink
+                <TextLinkOnWebOnly
                   type="sm-bold"
                   style={pal.textLight}
                   lineHeight={1.2}
@@ -229,7 +229,7 @@ export const FeedItem = observer(function FeedItemImpl({
                 lineHeight={1.2}
                 numberOfLines={1}>
                 Reposted by{' '}
-                <DesktopWebTextLink
+                <TextLinkOnWebOnly
                   type="sm-bold"
                   style={pal.textLight}
                   lineHeight={1.2}
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index df19ecad5..6bb3bc5f6 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -119,7 +119,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
   const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
 
   const onPressBack = React.useCallback(() => {
-    navigation.goBack()
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
   }, [navigation])
 
   const onPressAvi = React.useCallback(() => {
@@ -132,20 +136,19 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
   }, [store, view])
 
   const onPressToggleFollow = React.useCallback(() => {
-    track(
-      view.viewer.following
-        ? 'ProfileHeader:FollowButtonClicked'
-        : 'ProfileHeader:UnfollowButtonClicked',
-    )
     view?.toggleFollowing().then(
       () => {
         setShowSuggestedFollows(Boolean(view.viewer.following))
-
         Toast.show(
           `${
             view.viewer.following ? 'Following' : 'No longer following'
           } ${sanitizeDisplayName(view.displayName || view.handle)}`,
         )
+        track(
+          view.viewer.following
+            ? 'ProfileHeader:FollowButtonClicked'
+            : 'ProfileHeader:UnfollowButtonClicked',
+        )
       },
       err => store.log.error('Failed to toggle follow', err),
     )
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 35524bcc6..1777f6659 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -27,11 +27,10 @@ import {
   isExternalUrl,
   linkRequiresWarning,
 } from 'lib/strings/url-helpers'
-import {isAndroid} from 'platform/detection'
+import {isAndroid, isWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {PressableWithHover} from './PressableWithHover'
 import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -222,7 +221,7 @@ export const TextLink = memo(function TextLink({
 /**
  * Only acts as a link on desktop web
  */
-interface DesktopWebTextLinkProps extends TextProps {
+interface TextLinkOnWebOnlyProps extends TextProps {
   testID?: string
   type?: TypographyVariant
   style?: StyleProp<TextStyle>
@@ -235,7 +234,7 @@ interface DesktopWebTextLinkProps extends TextProps {
   accessibilityHint?: string
   title?: string
 }
-export const DesktopWebTextLink = memo(function DesktopWebTextLink({
+export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   testID,
   type = 'md',
   style,
@@ -244,10 +243,8 @@ export const DesktopWebTextLink = memo(function DesktopWebTextLink({
   numberOfLines,
   lineHeight,
   ...props
-}: DesktopWebTextLinkProps) {
-  const {isDesktop} = useWebMediaQueries()
-
-  if (isDesktop) {
+}: TextLinkOnWebOnlyProps) {
+  if (isWeb) {
     return (
       <TextLink
         testID={testID}
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index b5c47dea5..c5e438f8d 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
 import {Text} from './text/Text'
-import {DesktopWebTextLink} from './Link'
+import {TextLinkOnWebOnly} from './Link'
 import {niceDate} from 'lib/strings/time'
 import {usePalette} from 'lib/hooks/usePalette'
 import {TypographyVariant} from 'lib/ThemeContext'
@@ -47,7 +47,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
         </View>
       )}
       <View style={styles.maxWidth}>
-        <DesktopWebTextLink
+        <TextLinkOnWebOnly
           type={opts.displayNameType || 'lg-bold'}
           style={[pal.text, opts.displayNameStyle]}
           numberOfLines={1}
@@ -78,7 +78,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
       )}
       <TimeElapsed timestamp={opts.timestamp}>
         {({timeElapsed}) => (
-          <DesktopWebTextLink
+          <TextLinkOnWebOnly
             type="md"
             style={pal.textLight}
             lineHeight={1.2}
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index 695711b2a..e4ca981d9 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,7 +1,7 @@
 import React, {useState, useEffect} from 'react'
 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
 import {StyleProp, StyleSheet, TextStyle} from 'react-native'
-import {DesktopWebTextLink} from './Link'
+import {TextLinkOnWebOnly} from './Link'
 import {Text} from './text/Text'
 import {LoadingPlaceholder} from './LoadingPlaceholder'
 import {useStores} from 'state/index'
@@ -65,7 +65,7 @@ export function UserInfoText({
     )
   } else if (profile) {
     inner = (
-      <DesktopWebTextLink
+      <TextLinkOnWebOnly
         type={type}
         style={style}
         lineHeight={1.2}
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 035e29c25..6cbcddc32 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -52,20 +52,20 @@ export function AutoSizedImage({
 
   if (onPress || onLongPress || onPressIn) {
     return (
+      // disable a11y rule because in this case we want the tags on the image (#1640)
+      // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
       <Pressable
         onPress={onPress}
         onLongPress={onLongPress}
         onPressIn={onPressIn}
-        style={[styles.container, style]}
-        accessible={true}
-        accessibilityRole="button"
-        accessibilityLabel={alt || 'Image'}
-        accessibilityHint="Tap to view fully">
+        style={[styles.container, style]}>
         <Image
           style={[styles.image, {aspectRatio}]}
           source={uri}
-          accessible={false} // Must set for `accessibilityLabel` to work
+          accessible={true} // Must set for `accessibilityLabel` to work
           accessibilityIgnoresInvertColors
+          accessibilityLabel={alt}
+          accessibilityHint="Tap to view fully"
         />
         {children}
       </Pressable>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index cbff3a1c4..2679a6648 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -460,18 +460,22 @@ const styles = StyleSheet.create({
     justifyContent: 'center',
     width: 140,
     borderRadius: 24,
-    paddingVertical: 10,
-    paddingHorizontal: 16,
+    paddingTop: 10,
+    paddingBottom: 12, // visually aligns the text vertically inside the button
+    paddingLeft: 16,
+    paddingRight: 18, // looks nicer like this
     backgroundColor: colors.blue3,
     marginLeft: 12,
     marginTop: 20,
     marginBottom: 10,
     gap: 8,
   },
-  newPostBtnIconWrapper: {},
+  newPostBtnIconWrapper: {
+    marginTop: 2, // aligns the icon visually with the text
+  },
   newPostBtnLabel: {
     color: colors.white,
     fontSize: 16,
-    fontWeight: 'bold',
+    fontWeight: '600',
   },
 })
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 3119715e9..b564f99f8 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -21,7 +21,10 @@ import {usePalette} from 'lib/hooks/usePalette'
 import * as backHandler from 'lib/routes/back-handler'
 import {RoutesContainer, TabsNavigator} from '../../Navigation'
 import {isStateAtTabRoot} from 'lib/routes/helpers'
-import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {
+  SafeAreaProvider,
+  initialWindowMetrics,
+} from 'react-native-safe-area-context'
 import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
 
 const ShellInner = observer(function ShellInnerImpl() {
@@ -87,7 +90,7 @@ export const Shell: React.FC = observer(function ShellImpl() {
   const pal = usePalette('default')
   const theme = useTheme()
   return (
-    <SafeAreaProvider style={pal.view}>
+    <SafeAreaProvider initialMetrics={initialWindowMetrics} style={pal.view}>
       <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
         <StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
         <RoutesContainer>
diff --git a/yarn.lock b/yarn.lock
index 819488e5b..906e84650 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -47,18 +47,19 @@
     tlds "^1.234.0"
     typed-emitter "^2.1.0"
 
-"@atproto/api@^0.6.20":
-  version "0.6.20"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.20.tgz#3a7eda60d73a5d5b6938e2dd016c24a7ba180c83"
-  integrity sha512-+peoKgkaxbglXQg9qEZcZIvyWm39yj0+syV3TBDrz5cWK4OIsdOyYBg2iISy+jvB5RzEUMe2WvOojP6Nq34mOg==
-  dependencies:
-    "@atproto/common-web" "^0.2.1"
-    "@atproto/lexicon" "^0.2.2"
-    "@atproto/syntax" "^0.1.2"
-    "@atproto/xrpc" "^0.3.2"
+"@atproto/api@^0.6.21":
+  version "0.6.21"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.21.tgz#6e5b00facf46f2556d9766290341aae7e6ef75c8"
+  integrity sha512-ZWVEnLhZ8nonkCVzeFgdUFZhTOUtPxvicZFuttvb2G2Q5u43RmJ5qXXZvox/S9XQEw7TubG6Jza1mesH7CjfVQ==
+  dependencies:
+    "@atproto/common-web" "^0.2.2"
+    "@atproto/lexicon" "^0.2.3"
+    "@atproto/syntax" "^0.1.3"
+    "@atproto/xrpc" "^0.3.3"
     multiformats "^9.9.0"
     tlds "^1.234.0"
     typed-emitter "^2.1.0"
+    zod "^3.21.4"
 
 "@atproto/bsky@^0.0.5":
   version "0.0.5"
@@ -105,10 +106,10 @@
     uint8arrays "3.0.0"
     zod "^3.21.4"
 
-"@atproto/common-web@^0.2.1":
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.1.tgz#97412cb241321fc6c56a2b8c0b2416b3240caf50"
-  integrity sha512-5AoDKkKz7JhXSiicjhPihA/MJMlSuTQ9Aed9fflPuoTuT6C3aXbxaUZEcqqipSwlCfGpOzPmJmWJjMWWsYr2ew==
+"@atproto/common-web@^0.2.2":
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.2.tgz#decc12584c84f3c34d077d1afe7442bfc21bcf6c"
+  integrity sha512-XWZHj82kWGdhm0y6e/DxLA5qK0LPHTozfPCH2ws1B/Qh9Hh5DD/gakvlIRT1FouwPM+hWcs8YHVJ8bjnehrhHA==
   dependencies:
     graphemer "^1.4.0"
     multiformats "^9.9.0"
@@ -219,13 +220,13 @@
     multiformats "^9.9.0"
     zod "^3.21.4"
 
-"@atproto/lexicon@^0.2.2":
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.2.2.tgz#938a39482ff41c6a908f4ad43274adba595f3643"
-  integrity sha512-CvmjaSDavHMOJTuNYE8VjYhL7TVxBYV8QSWh2jHCpzfmj02DvVD9UBIfnoVv67POJkEtWXddjoV9beaIbaq/Xg==
+"@atproto/lexicon@^0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.2.3.tgz#3f8ba24187d5628ec06b1bdbec90747f7cdc0948"
+  integrity sha512-1xUs0KNw4CopWI5HSlLYZ8UHW5nb6V7sldO5OPONiEVKjETrqqjfopezloYAIBNrekUNXwd1pbp05afkAxW5og==
   dependencies:
-    "@atproto/common-web" "^0.2.1"
-    "@atproto/syntax" "^0.1.2"
+    "@atproto/common-web" "^0.2.2"
+    "@atproto/syntax" "^0.1.3"
     iso-datestring-validator "^2.2.2"
     multiformats "^9.9.0"
     zod "^3.21.4"
@@ -297,12 +298,12 @@
   dependencies:
     "@atproto/common-web" "^0.2.0"
 
-"@atproto/syntax@^0.1.2":
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.2.tgz#417366d36b53ecf29d9d1f6e35179b1f3feef95b"
-  integrity sha512-n6VSuccMGouwftCvZBq9WNwI0qYCMOH/lTHSV+/dT232lX7pIrqisOlErUSBoOJ49B1Wxy1DjeeBS26ap9SsGQ==
+"@atproto/syntax@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.3.tgz#5cafd5d82eee939fde06a2eacd11b264fb2f3b13"
+  integrity sha512-Xbw+Rx15puW8wZ/ro40nAQVc7ymPqcGOinVt8Jxi+lcY/1iKpID9a86E6ZOzvw0ncFKONwILYk1+xGeUT6OUNA==
   dependencies:
-    "@atproto/common-web" "^0.2.1"
+    "@atproto/common-web" "^0.2.2"
 
 "@atproto/xrpc-server@^0.3.1":
   version "0.3.1"
@@ -329,12 +330,12 @@
     "@atproto/lexicon" "^0.2.1"
     zod "^3.21.4"
 
-"@atproto/xrpc@^0.3.2":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.3.2.tgz#432a364be4b3bf8660a088a07dadecac10209763"
-  integrity sha512-D9jGjcFnEMHuGQ56v6+78uX3RiytKLrA5ITLq6shy0Qj6Zvt5MqV+/cTFuNPKrNCrnWOtHFeRQwMqyGhNS9qZQ==
+"@atproto/xrpc@^0.3.3":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.3.3.tgz#05f1c431ccd366e950637b93acca85faa249f52b"
+  integrity sha512-o0VUrUGu5Y/1F+ujZKIJYpuHdfXaIDacxuiq2IjwR2rbHXlefh+9FJy5XNkq4do+jMj7U+gSiPrgqaqLYbc9ng==
   dependencies:
-    "@atproto/lexicon" "^0.2.2"
+    "@atproto/lexicon" "^0.2.3"
     zod "^3.21.4"
 
 "@babel/code-frame@7.10.4", "@babel/code-frame@~7.10.4":