about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorMinseo Lee <itoupluk427@gmail.com>2024-03-02 13:04:51 +0900
committerGitHub <noreply@github.com>2024-03-02 13:04:51 +0900
commitab2b454be8f15ccd4176edce2d28abdce501274b (patch)
tree41e198f85a4372950ce39a6613d231b2d5932be1 /src/view
parent537ae578d6501319e07132ea8b12c280e0755fca (diff)
parentb70c404d4b369d6fab0dfbafd6b31390ffd20014 (diff)
downloadvoidsky-ab2b454be8f15ccd4176edce2d28abdce501274b.tar.zst
Merge branch 'bluesky-social:main' into patch-3
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/text-input/web/LinkDecorator.ts5
-rw-r--r--src/view/com/composer/text-input/web/TagDecorator.ts22
-rw-r--r--src/view/com/home/HomeHeader.tsx2
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx76
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx1
-rw-r--r--src/view/com/util/Link.tsx10
-rw-r--r--src/view/com/util/ViewHeader.tsx97
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx2
-rw-r--r--src/view/com/util/moderation/PostHider.tsx2
-rw-r--r--src/view/com/util/text/RichText.tsx1
-rw-r--r--src/view/screens/Home.tsx3
-rw-r--r--src/view/screens/Profile.tsx2
-rw-r--r--src/view/screens/ProfileList.tsx15
-rw-r--r--src/view/screens/Storybook/Links.tsx28
-rw-r--r--src/view/screens/Storybook/Palette.tsx85
-rw-r--r--src/view/shell/desktop/LeftNav.tsx6
16 files changed, 196 insertions, 161 deletions
diff --git a/src/view/com/composer/text-input/web/LinkDecorator.ts b/src/view/com/composer/text-input/web/LinkDecorator.ts
index 19945de08..e36ac80e4 100644
--- a/src/view/com/composer/text-input/web/LinkDecorator.ts
+++ b/src/view/com/composer/text-input/web/LinkDecorator.ts
@@ -18,6 +18,8 @@ import {Mark} from '@tiptap/core'
 import {Plugin, PluginKey} from '@tiptap/pm/state'
 import {Node as ProsemirrorNode} from '@tiptap/pm/model'
 import {Decoration, DecorationSet} from '@tiptap/pm/view'
+import {URL_REGEX} from '@atproto/api'
+
 import {isValidDomain} from 'lib/strings/url-helpers'
 
 export const LinkDecorator = Mark.create({
@@ -78,8 +80,7 @@ function linkDecorator() {
 
 function iterateUris(str: string, cb: (from: number, to: number) => void) {
   let match
-  const re =
-    /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
+  const re = URL_REGEX
   while ((match = re.exec(str))) {
     let uri = match[2]
     if (!uri.startsWith('http')) {
diff --git a/src/view/com/composer/text-input/web/TagDecorator.ts b/src/view/com/composer/text-input/web/TagDecorator.ts
index d820ec3f0..2bf3184a8 100644
--- a/src/view/com/composer/text-input/web/TagDecorator.ts
+++ b/src/view/com/composer/text-input/web/TagDecorator.ts
@@ -18,28 +18,36 @@ import {Mark} from '@tiptap/core'
 import {Plugin, PluginKey} from '@tiptap/pm/state'
 import {Node as ProsemirrorNode} from '@tiptap/pm/model'
 import {Decoration, DecorationSet} from '@tiptap/pm/view'
+import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api'
 
 function getDecorations(doc: ProsemirrorNode) {
   const decorations: Decoration[] = []
 
   doc.descendants((node, pos) => {
     if (node.isText && node.text) {
-      const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
+      const regex = TAG_REGEX
       const textContent = node.textContent
 
       let match
       while ((match = regex.exec(textContent))) {
-        const [matchedString, tag] = match
+        const [matchedString, _, tag] = match
 
-        if (tag.length > 66) continue
+        if (!tag || tag.replace(TRAILING_PUNCTUATION_REGEX, '').length > 64)
+          continue
 
-        const [trailingPunc = ''] = tag.match(/\p{P}+$/u) || []
+        const [trailingPunc = ''] = tag.match(TRAILING_PUNCTUATION_REGEX) || []
+        const matchedFrom = match.index + matchedString.indexOf(tag)
+        const matchedTo = matchedFrom + (tag.length - trailingPunc.length)
 
-        const from = match.index + matchedString.indexOf(tag)
-        const to = from + (tag.length - trailingPunc.length)
+        /*
+         * The match is exclusive of `#` so we need to adjust the start of the
+         * highlight by -1 to include the `#`
+         */
+        const start = pos + matchedFrom - 1
+        const end = pos + matchedTo
 
         decorations.push(
-          Decoration.inline(pos + from, pos + to, {
+          Decoration.inline(start, end, {
             class: 'autolink',
           }),
         )
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
index bbd16465a..aa3ecb7fc 100644
--- a/src/view/com/home/HomeHeader.tsx
+++ b/src/view/com/home/HomeHeader.tsx
@@ -52,7 +52,7 @@ export function HomeHeader(
   )
 
   return (
-    <HomeHeaderLayout>
+    <HomeHeaderLayout tabBarAnchor={props.tabBarAnchor}>
       <TabBar
         key={items.join(',')}
         onPressSelected={props.onPressSelected}
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index fbb55e6bc..6145081af 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,13 +1,10 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import Animated from 'react-native-reanimated'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
-import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
-import {useShellLayout} from '#/state/shell/shell-layout'
 import {Logo} from '#/view/icons/Logo'
-import {Link, TextLink} from '../util/Link'
+import {Link} from '../util/Link'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -16,41 +13,42 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {CogIcon} from '#/lib/icons'
 
-export function HomeHeaderLayout({children}: {children: React.ReactNode}) {
+export function HomeHeaderLayout(props: {
+  children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
+}) {
   const {isMobile} = useWebMediaQueries()
   if (isMobile) {
-    return <HomeHeaderLayoutMobile>{children}</HomeHeaderLayoutMobile>
+    return <HomeHeaderLayoutMobile {...props} />
   } else {
-    return <HomeHeaderLayoutTablet>{children}</HomeHeaderLayoutTablet>
+    return <HomeHeaderLayoutDesktopAndTablet {...props} />
   }
 }
 
-function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) {
+function HomeHeaderLayoutDesktopAndTablet({
+  children,
+  tabBarAnchor,
+}: {
+  children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
+}) {
   const pal = usePalette('default')
-  const {headerMinimalShellTransform} = useMinimalShellMode()
-  const {headerHeight} = useShellLayout()
   const {_} = useLingui()
 
   return (
-    // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
-    <Animated.View
-      style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
-      onLayout={e => {
-        headerHeight.value = e.nativeEvent.layout.height
-      }}>
-      <View style={[pal.view, styles.topBar]}>
-        <TextLink
-          type="title-lg"
+    <>
+      <View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
+        <Link
           href="/settings/following-feed"
+          hitSlop={10}
+          accessibilityRole="button"
           accessibilityLabel={_(msg`Following Feed Preferences`)}
-          accessibilityHint=""
-          text={
-            <FontAwesomeIcon
-              icon="sliders"
-              style={pal.textLight as FontAwesomeIconStyle}
-            />
-          }
-        />
+          accessibilityHint="">
+          <FontAwesomeIcon
+            icon="sliders"
+            style={pal.textLight as FontAwesomeIconStyle}
+          />
+        </Link>
         <Logo width={28} />
         <Link
           href="/settings/saved-feeds"
@@ -61,32 +59,38 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) {
           <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
         </Link>
       </View>
-      {children}
-    </Animated.View>
+      {tabBarAnchor}
+      <View style={[pal.view, pal.border, styles.bar, styles.tabBar]}>
+        {children}
+      </View>
+    </>
   )
 }
 
 const styles = StyleSheet.create({
+  bar: {
+    // @ts-ignore Web only
+    left: 'calc(50% - 300px)',
+    width: 600,
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
+  },
   topBar: {
     flexDirection: 'row',
     justifyContent: 'space-between',
     alignItems: 'center',
     paddingHorizontal: 18,
-    paddingVertical: 8,
-    marginTop: 8,
-    width: '100%',
+    paddingTop: 16,
+    paddingBottom: 8,
   },
   tabBar: {
     // @ts-ignore Web only
     position: 'sticky',
-    zIndex: 1,
-    // @ts-ignore Web only -prf
-    left: 'calc(50% - 300px)',
-    width: 600,
     top: 0,
     flexDirection: 'column',
     alignItems: 'center',
     borderLeftWidth: 1,
     borderRightWidth: 1,
+    zIndex: 1,
   },
 })
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index f51efb7b4..d7b7231c6 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -23,6 +23,7 @@ export function HomeHeaderLayoutMobile({
   children,
 }: {
   children: React.ReactNode
+  tabBarAnchor: JSX.Element | null | undefined
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index d52d3c0e6..e50fb7f09 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -159,7 +159,7 @@ export const TextLink = memo(function TextLink({
   dataSet,
   title,
   onPress,
-  warnOnMismatchingLabel,
+  disableMismatchWarning,
   navigationAction,
   ...orgProps
 }: {
@@ -172,7 +172,7 @@ export const TextLink = memo(function TextLink({
   lineHeight?: number
   dataSet?: any
   title?: string
-  warnOnMismatchingLabel?: boolean
+  disableMismatchWarning?: boolean
   navigationAction?: 'push' | 'replace' | 'navigate'
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
@@ -180,14 +180,14 @@ export const TextLink = memo(function TextLink({
   const {openModal, closeModal} = useModalControls()
   const openLink = useOpenLink()
 
-  if (warnOnMismatchingLabel && typeof text !== 'string') {
+  if (!disableMismatchWarning && typeof text !== 'string') {
     console.error('Unable to detect mismatching label')
   }
 
   props.onPress = React.useCallback(
     (e?: Event) => {
       const requiresWarning =
-        warnOnMismatchingLabel &&
+        !disableMismatchWarning &&
         linkRequiresWarning(href, typeof text === 'string' ? text : '')
       if (requiresWarning) {
         e?.preventDefault?.()
@@ -227,7 +227,7 @@ export const TextLink = memo(function TextLink({
       navigation,
       href,
       text,
-      warnOnMismatchingLabel,
+      disableMismatchWarning,
       navigationAction,
       openLink,
     ],
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 1ccfcf56c..872e10eef 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -13,11 +13,13 @@ import Animated from 'react-native-reanimated'
 import {useSetDrawerOpen} from '#/state/shell'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useTheme} from '#/alf'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
 export function ViewHeader({
   title,
+  subtitle,
   canGoBack,
   showBackButton = true,
   hideOnScroll,
@@ -26,6 +28,7 @@ export function ViewHeader({
   renderButton,
 }: {
   title: string
+  subtitle?: string
   canGoBack?: boolean
   showBackButton?: boolean
   hideOnScroll?: boolean
@@ -39,6 +42,7 @@ export function ViewHeader({
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
   const {isDesktop, isTablet} = useWebMediaQueries()
+  const t = useTheme()
 
   const onPressBack = React.useCallback(() => {
     if (navigation.canGoBack()) {
@@ -71,42 +75,60 @@ export function ViewHeader({
 
     return (
       <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
-        {showBackButton ? (
-          <TouchableOpacity
-            testID="viewHeaderDrawerBtn"
-            onPress={canGoBack ? onPressBack : onPressMenu}
-            hitSlop={BACK_HITSLOP}
-            style={canGoBack ? styles.backBtn : styles.backBtnWide}
-            accessibilityRole="button"
-            accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
-            accessibilityHint={
-              canGoBack ? '' : _(msg`Access navigation links and settings`)
-            }>
-            {canGoBack ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="angle-left"
-                style={[styles.backIcon, pal.text]}
-              />
-            ) : !isTablet ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="bars"
-                style={[styles.backIcon, pal.textLight]}
-              />
+        <View style={{flex: 1}}>
+          <View style={{flexDirection: 'row', alignItems: 'center'}}>
+            {showBackButton ? (
+              <TouchableOpacity
+                testID="viewHeaderDrawerBtn"
+                onPress={canGoBack ? onPressBack : onPressMenu}
+                hitSlop={BACK_HITSLOP}
+                style={canGoBack ? styles.backBtn : styles.backBtnWide}
+                accessibilityRole="button"
+                accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
+                accessibilityHint={
+                  canGoBack ? '' : _(msg`Access navigation links and settings`)
+                }>
+                {canGoBack ? (
+                  <FontAwesomeIcon
+                    size={18}
+                    icon="angle-left"
+                    style={[styles.backIcon, pal.text]}
+                  />
+                ) : !isTablet ? (
+                  <FontAwesomeIcon
+                    size={18}
+                    icon="bars"
+                    style={[styles.backIcon, pal.textLight]}
+                  />
+                ) : null}
+              </TouchableOpacity>
             ) : null}
-          </TouchableOpacity>
-        ) : null}
-        <View style={styles.titleContainer} pointerEvents="none">
-          <Text type="title" style={[pal.text, styles.title]}>
-            {title}
-          </Text>
+            <View style={styles.titleContainer} pointerEvents="none">
+              <Text type="title" style={[pal.text, styles.title]}>
+                {title}
+              </Text>
+            </View>
+            {renderButton ? (
+              renderButton()
+            ) : showBackButton ? (
+              <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
+            ) : null}
+          </View>
+          {subtitle ? (
+            <View
+              style={[styles.titleContainer, {marginTop: -3}]}
+              pointerEvents="none">
+              <Text
+                style={[
+                  pal.text,
+                  styles.subtitle,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                {subtitle}
+              </Text>
+            </View>
+          ) : undefined}
         </View>
-        {renderButton ? (
-          renderButton()
-        ) : showBackButton ? (
-          <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
-        ) : null}
       </Container>
     )
   }
@@ -185,7 +207,6 @@ function Container({
 const styles = StyleSheet.create({
   header: {
     flexDirection: 'row',
-    alignItems: 'center',
     paddingHorizontal: 12,
     paddingVertical: 6,
     width: '100%',
@@ -207,12 +228,14 @@ const styles = StyleSheet.create({
   titleContainer: {
     marginLeft: 'auto',
     marginRight: 'auto',
-    paddingRight: 10,
+    alignItems: 'center',
   },
   title: {
     fontWeight: 'bold',
   },
-
+  subtitle: {
+    fontSize: 13,
+  },
   backBtn: {
     width: 30,
     height: 30,
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index b3a563116..cd2545290 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -46,7 +46,7 @@ export function ContentHider({
     )
   }
 
-  const isMute = moderation.cause?.type === 'muted'
+  const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
   const desc = describeModerationCause(moderation.cause, 'content')
   return (
     <View testID={testID} style={[styles.outer, style]}>
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index b1fa71d4a..ede62e988 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -47,7 +47,7 @@ export function PostHider({
     )
   }
 
-  const isMute = moderation.cause?.type === 'muted'
+  const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
   const desc = describeModerationCause(moderation.cause, 'content')
   return !override ? (
     <Pressable
diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx
index 0ec3f3181..f4ade30e5 100644
--- a/src/view/com/util/text/RichText.tsx
+++ b/src/view/com/util/text/RichText.tsx
@@ -114,7 +114,6 @@ export function RichText({
             href={link.uri}
             style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
             dataSet={WORD_WRAP}
-            warnOnMismatchingLabel
             selectable={selectable}
           />,
         )
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 7ad9beb56..99ac8c44a 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -123,8 +123,7 @@ function HomeScreenReady({
       return (
         <HomeHeader
           key="FEEDS_TAB_BAR"
-          selectedPage={props.selectedPage}
-          onSelect={props.onSelect}
+          {...props}
           testID="homeScreenFeedTabs"
           onPressSelected={onPressSelected}
           feeds={pinnedFeedInfos}
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 64e067593..b30b4491b 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -491,6 +491,8 @@ const styles = StyleSheet.create({
   container: {
     flexDirection: 'column',
     height: '100%',
+    // @ts-ignore Web-only.
+    overflowAnchor: 'none', // Fixes jumps when switching tabs while scrolled down.
   },
   loading: {
     paddingVertical: 10,
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index b3072c397..3c675ee0a 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -60,7 +60,7 @@ import {
 import {logger} from '#/logger'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {listenSoftReset} from '#/state/events'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -699,6 +699,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
     ref,
   ) {
     const pal = usePalette('default')
+    const t = useTheme()
     const {_} = useLingui()
     const {isMobile} = useWebMediaQueries()
     const {currentAccount} = useSession()
@@ -792,7 +793,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 paddingBottom: isMobile ? 14 : 18,
               },
             ]}>
-            <Text type="lg-bold">
+            <Text type="lg-bold" style={t.atoms.text}>
               <Trans>Users</Trans>
             </Text>
             {isOwner && (
@@ -817,14 +818,18 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
         </View>
       )
     }, [
-      pal,
-      list,
       isMobile,
+      pal.border,
+      pal.textLight,
+      pal.colors.link,
+      pal.link,
       descriptionRT,
       isCurateList,
       isOwner,
-      onPressAddUser,
+      list.creator,
+      t.atoms.text,
       _,
+      onPressAddUser,
     ])
 
     const renderEmptyState = useCallback(() => {
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
index 3f1806906..f9ecfba55 100644
--- a/src/view/screens/Storybook/Links.tsx
+++ b/src/view/screens/Storybook/Links.tsx
@@ -4,7 +4,7 @@ import {View} from 'react-native'
 import {useTheme, atoms as a} from '#/alf'
 import {ButtonText} from '#/components/Button'
 import {InlineLink, Link} from '#/components/Link'
-import {H1, H3, Text} from '#/components/Typography'
+import {H1, Text} from '#/components/Typography'
 
 export function Links() {
   const t = useTheme()
@@ -13,31 +13,19 @@ export function Links() {
       <H1>Links</H1>
 
       <View style={[a.gap_md, a.align_start]}>
-        <InlineLink
-          to="https://bsky.social"
-          warnOnMismatchingTextChild
-          style={[a.text_md]}>
-          External
+        <InlineLink to="https://google.com" style={[a.text_lg]}>
+          https://google.com
         </InlineLink>
-        <InlineLink to="https://bsky.social" style={[a.text_md, t.atoms.text]}>
-          <H3>External with custom children</H3>
+        <InlineLink to="https://google.com" style={[a.text_lg]}>
+          External with custom children (google.com)
         </InlineLink>
         <InlineLink
           to="https://bsky.social"
           style={[a.text_md, t.atoms.text_contrast_low]}>
-          External with custom children
-        </InlineLink>
-        <InlineLink
-          to="https://bsky.social"
-          warnOnMismatchingTextChild
-          style={[a.text_lg]}>
-          https://bsky.social
+          Internal (bsky.social)
         </InlineLink>
-        <InlineLink
-          to="https://bsky.app/profile/bsky.app"
-          warnOnMismatchingTextChild
-          style={[a.text_md]}>
-          Internal
+        <InlineLink to="https://bsky.app/profile/bsky.app" style={[a.text_md]}>
+          Internal (bsky.app)
         </InlineLink>
 
         <Link
diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx
index 900754681..42000aa81 100644
--- a/src/view/screens/Storybook/Palette.tsx
+++ b/src/view/screens/Storybook/Palette.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
 
-import * as tokens from '#/alf/tokens'
 import {atoms as a, useTheme} from '#/alf'
 
 export function Palette() {
@@ -28,79 +27,79 @@ export function Palette() {
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_25},
+            {height: 60, backgroundColor: t.palette.primary_25},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_50},
+            {height: 60, backgroundColor: t.palette.primary_50},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_100},
+            {height: 60, backgroundColor: t.palette.primary_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_200},
+            {height: 60, backgroundColor: t.palette.primary_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_300},
+            {height: 60, backgroundColor: t.palette.primary_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_400},
+            {height: 60, backgroundColor: t.palette.primary_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_500},
+            {height: 60, backgroundColor: t.palette.primary_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_600},
+            {height: 60, backgroundColor: t.palette.primary_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_700},
+            {height: 60, backgroundColor: t.palette.primary_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_800},
+            {height: 60, backgroundColor: t.palette.primary_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_900},
+            {height: 60, backgroundColor: t.palette.primary_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_950},
+            {height: 60, backgroundColor: t.palette.primary_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.blue_975},
+            {height: 60, backgroundColor: t.palette.primary_975},
           ]}
         />
       </View>
@@ -108,153 +107,159 @@ export function Palette() {
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_25},
+            {height: 60, backgroundColor: t.palette.positive_25},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_50},
+            {height: 60, backgroundColor: t.palette.positive_50},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_100},
+            {height: 60, backgroundColor: t.palette.positive_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_200},
+            {height: 60, backgroundColor: t.palette.positive_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_300},
+            {height: 60, backgroundColor: t.palette.positive_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_400},
+            {height: 60, backgroundColor: t.palette.positive_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_500},
+            {height: 60, backgroundColor: t.palette.positive_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_600},
+            {height: 60, backgroundColor: t.palette.positive_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_700},
+            {height: 60, backgroundColor: t.palette.positive_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_800},
+            {height: 60, backgroundColor: t.palette.positive_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_900},
+            {height: 60, backgroundColor: t.palette.positive_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_950},
+            {height: 60, backgroundColor: t.palette.positive_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.green_975},
+            {height: 60, backgroundColor: t.palette.positive_975},
           ]}
         />
       </View>
       <View style={[a.flex_row, a.gap_md]}>
         <View
-          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]}
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: t.palette.negative_25},
+          ]}
         />
         <View
-          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]}
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: t.palette.negative_50},
+          ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_100},
+            {height: 60, backgroundColor: t.palette.negative_100},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_200},
+            {height: 60, backgroundColor: t.palette.negative_200},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_300},
+            {height: 60, backgroundColor: t.palette.negative_300},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_400},
+            {height: 60, backgroundColor: t.palette.negative_400},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_500},
+            {height: 60, backgroundColor: t.palette.negative_500},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_600},
+            {height: 60, backgroundColor: t.palette.negative_600},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_700},
+            {height: 60, backgroundColor: t.palette.negative_700},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_800},
+            {height: 60, backgroundColor: t.palette.negative_800},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_900},
+            {height: 60, backgroundColor: t.palette.negative_900},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_950},
+            {height: 60, backgroundColor: t.palette.negative_950},
           ]}
         />
         <View
           style={[
             a.flex_1,
-            {height: 60, backgroundColor: tokens.color.red_975},
+            {height: 60, backgroundColor: t.palette.negative_975},
           ]}
         />
       </View>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index fbc90bfc6..def0333c7 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -200,10 +200,10 @@ function ComposeBtn() {
   const fetchHandle = useFetchHandle()
 
   const getProfileHandle = async () => {
-    const {routes} = getState()
-    const currentRoute = routes[routes.length - 1]
+    const routes = getState()?.routes
+    const currentRoute = routes?.[routes?.length - 1]
 
-    if (currentRoute.name === 'Profile') {
+    if (currentRoute?.name === 'Profile') {
       let handle: string | undefined = (
         currentRoute.params as CommonNavigatorParams['Profile']
       ).name