about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
15 files changed, 119 insertions, 104 deletions
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>