about summary refs log tree commit diff
path: root/src/view/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/shell')
-rw-r--r--src/view/shell/Composer.web.tsx34
-rw-r--r--src/view/shell/Drawer.tsx11
-rw-r--r--src/view/shell/bottom-bar/BottomBarStyles.tsx4
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx1
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx3
-rw-r--r--src/view/shell/desktop/Feeds.tsx4
-rw-r--r--src/view/shell/desktop/LeftNav.tsx46
-rw-r--r--src/view/shell/desktop/RightNav.tsx37
-rw-r--r--src/view/shell/desktop/Search.tsx104
-rw-r--r--src/view/shell/index.tsx2
-rw-r--r--src/view/shell/index.web.tsx24
11 files changed, 198 insertions, 72 deletions
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 73f9f540e..99e659d62 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -5,6 +5,11 @@ import {ComposePost} from '../com/composer/Composer'
 import {useComposerState} from 'state/shell/composer'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
+import {
+  EmojiPicker,
+  EmojiPickerState,
+} from 'view/com/composer/text-input/web/EmojiPicker.web.tsx'
 
 const BOTTOM_BAR_HEIGHT = 61
 
@@ -12,11 +17,33 @@ export function Composer({}: {winHeight: number}) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const state = useComposerState()
+  const isActive = !!state
+  useWebBodyScrollLock(isActive)
+
+  const [pickerState, setPickerState] = React.useState<EmojiPickerState>({
+    isOpen: false,
+    pos: {top: 0, left: 0, right: 0, bottom: 0},
+  })
+
+  const onOpenPicker = React.useCallback((pos: DOMRect | undefined) => {
+    if (!pos) return
+    setPickerState({
+      isOpen: true,
+      pos,
+    })
+  }, [])
+
+  const onClosePicker = React.useCallback(() => {
+    setPickerState(prev => ({
+      ...prev,
+      isOpen: false,
+    }))
+  }, [])
 
   // rendering
   // =
 
-  if (!state) {
+  if (!isActive) {
     return <View />
   }
 
@@ -41,15 +68,18 @@ export function Composer({}: {winHeight: number}) {
           quote={state.quote}
           onPost={state.onPost}
           mention={state.mention}
+          openPicker={onOpenPicker}
         />
       </Animated.View>
+      <EmojiPicker state={pickerState} close={onClosePicker} />
     </Animated.View>
   )
 }
 
 const styles = StyleSheet.create({
   mask: {
-    position: 'absolute',
+    // @ts-ignore
+    position: 'fixed',
     top: 0,
     left: 0,
     width: '100%',
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 14bc6af26..c30874c2f 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -53,6 +53,8 @@ import {useInviteCodesQuery} from '#/state/queries/invites'
 import {NavSignupCard} from '#/view/shell/NavSignupCard'
 import {TextLink} from '../com/util/Link'
 
+import {useTheme as useAlfTheme} from '#/alf'
+
 let DrawerProfileCard = ({
   account,
   onPressProfile,
@@ -68,7 +70,7 @@ let DrawerProfileCard = ({
     <TouchableOpacity
       testID="profileCardButton"
       accessibilityLabel={_(msg`Profile`)}
-      accessibilityHint="Navigates to your profile"
+      accessibilityHint={_(msg`Navigates to your profile`)}
       onPress={onPressProfile}>
       <UserAvatar
         size={80}
@@ -106,6 +108,7 @@ export {DrawerProfileCard}
 
 let DrawerContent = ({}: {}): React.ReactNode => {
   const theme = useTheme()
+  const t = useAlfTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
   const setDrawerOpen = useSetDrawerOpen()
@@ -208,7 +211,7 @@ let DrawerContent = ({}: {}): React.ReactNode => {
       testID="drawer"
       style={[
         styles.view,
-        theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
+        theme.colorScheme === 'light' ? pal.view : t.atoms.bg_contrast_25,
       ]}>
       <SafeAreaView style={s.flex1}>
         <ScrollView style={styles.main}>
@@ -435,7 +438,9 @@ let NotificationsMenuItem = ({
       label={_(msg`Notifications`)}
       accessibilityLabel={_(msg`Notifications`)}
       accessibilityHint={
-        numUnreadNotifications === '' ? '' : `${numUnreadNotifications} unread`
+        numUnreadNotifications === ''
+          ? ''
+          : _(msg`${numUnreadNotifications} unread`)
       }
       count={numUnreadNotifications}
       bold={isActive}
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index ae9381440..f226406f5 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -12,6 +12,10 @@ export const styles = StyleSheet.create({
     paddingLeft: 5,
     paddingRight: 10,
   },
+  bottomBarWeb: {
+    // @ts-ignore web-only
+    position: 'fixed',
+  },
   ctrl: {
     flex: 1,
     paddingTop: 13,
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index c5dc376b7..b330c4b80 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -57,6 +57,7 @@ export function BottomBarWeb() {
     <Animated.View
       style={[
         styles.bottomBar,
+        styles.bottomBarWeb,
         pal.view,
         pal.border,
         {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index 43dc28159..9fea6e49f 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -124,6 +124,7 @@ function NativeStackNavigator({
       },
     }
   }
+
   return (
     <NavigationContent>
       <NativeStackView
@@ -136,7 +137,7 @@ function NativeStackNavigator({
       {isWeb && !isMobile && (
         <>
           <DesktopLeftNav />
-          <DesktopRightNav />
+          <DesktopRightNav routeName={activeRoute.name} />
         </>
       )}
     </NavigationContent>
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 93b96e704..a8f5f1c66 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -21,7 +21,7 @@ export function DesktopFeeds() {
   })
 
   return (
-    <View style={[styles.container, pal.view, pal.border]}>
+    <View style={[styles.container, pal.view]}>
       <FeedItem href="/" title="Following" current={route.name === 'Home'} />
       {feeds
         .filter(f => f.displayName !== 'Following')
@@ -91,7 +91,5 @@ const styles = StyleSheet.create({
     width: 300,
     paddingHorizontal: 12,
     paddingVertical: 18,
-    borderTopWidth: 1,
-    borderBottomWidth: 1,
   },
 })
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index f3c8c1d11..b27898828 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -239,24 +239,26 @@ function ComposeBtn() {
     return null
   }
   return (
-    <TouchableOpacity
-      disabled={isFetchingHandle}
-      style={[styles.newPostBtn]}
-      onPress={onPressCompose}
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`New post`)}
-      accessibilityHint="">
-      <View style={styles.newPostBtnIconWrapper}>
-        <ComposeIcon2
-          size={19}
-          strokeWidth={2}
-          style={styles.newPostBtnLabel}
-        />
-      </View>
-      <Text type="button" style={styles.newPostBtnLabel}>
-        <Trans>New Post</Trans>
-      </Text>
-    </TouchableOpacity>
+    <View style={styles.newPostBtnContainer}>
+      <TouchableOpacity
+        disabled={isFetchingHandle}
+        style={styles.newPostBtn}
+        onPress={onPressCompose}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`New post`)}
+        accessibilityHint="">
+        <View style={styles.newPostBtnIconWrapper}>
+          <ComposeIcon2
+            size={19}
+            strokeWidth={2}
+            style={styles.newPostBtnLabel}
+          />
+        </View>
+        <Text type="button" style={styles.newPostBtnLabel}>
+          <Trans context="action">New Post</Trans>
+        </Text>
+      </TouchableOpacity>
+    </View>
   )
 }
 
@@ -440,10 +442,11 @@ export function DesktopLeftNav() {
 
 const styles = StyleSheet.create({
   leftNav: {
-    position: 'absolute',
+    // @ts-ignore web only
+    position: 'fixed',
     top: 10,
     // @ts-ignore web only
-    right: 'calc(50vw + 312px)',
+    left: 'calc(50vw - 300px - 220px - 20px)',
     width: 220,
     // @ts-ignore web only
     maxHeight: 'calc(100vh - 10px)',
@@ -512,6 +515,9 @@ const styles = StyleSheet.create({
     fontSize: 14,
   },
 
+  newPostBtnContainer: {
+    flexDirection: 'row',
+  },
   newPostBtn: {
     flexDirection: 'row',
     alignItems: 'center',
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 8d9961a5f..328c527e4 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -16,7 +16,7 @@ import {Plural, Trans, msg, plural} from '@lingui/macro'
 import {useSession} from '#/state/session'
 import {useInviteCodesQuery} from '#/state/queries/invites'
 
-export function DesktopRightNav() {
+export function DesktopRightNav({routeName}: {routeName: string}) {
   const pal = usePalette('default')
   const palError = usePalette('error')
   const {_} = useLingui()
@@ -30,12 +30,20 @@ export function DesktopRightNav() {
   return (
     <View style={[styles.rightNav, pal.view]}>
       <View style={{paddingVertical: 20}}>
-        <DesktopSearch />
-
-        {hasSession && (
-          <View style={{paddingTop: 18, marginBottom: 18}}>
+        {routeName === 'Search' ? (
+          <View style={{marginBottom: 18}}>
             <DesktopFeeds />
           </View>
+        ) : (
+          <>
+            <DesktopSearch />
+
+            {hasSession && (
+              <View style={[pal.border, styles.desktopFeedsContainer]}>
+                <DesktopFeeds />
+              </View>
+            )}
+          </>
         )}
 
         <View
@@ -48,7 +56,7 @@ export function DesktopRightNav() {
           {isSandbox ? (
             <View style={[palError.view, styles.messageLine, s.p10]}>
               <Text type="md" style={[palError.text, s.bold]}>
-                SANDBOX. Posts and accounts are not permanent.
+                <Trans>SANDBOX. Posts and accounts are not permanent.</Trans>
               </Text>
             </View>
           ) : undefined}
@@ -169,17 +177,18 @@ function InviteCodes() {
 
 const styles = StyleSheet.create({
   rightNav: {
-    position: 'absolute',
     // @ts-ignore web only
-    left: 'calc(50vw + 320px)',
-    width: 304,
+    position: 'fixed',
+    // @ts-ignore web only
+    left: 'calc(50vw + 300px + 20px)',
+    width: 300,
     maxHeight: '100%',
     overflowY: 'auto',
   },
 
   message: {
     paddingVertical: 18,
-    paddingHorizontal: 10,
+    paddingHorizontal: 12,
   },
   messageLine: {
     marginBottom: 10,
@@ -187,7 +196,7 @@ const styles = StyleSheet.create({
 
   inviteCodes: {
     borderTopWidth: 1,
-    paddingHorizontal: 16,
+    paddingHorizontal: 12,
     paddingVertical: 12,
     flexDirection: 'row',
   },
@@ -196,4 +205,10 @@ const styles = StyleSheet.create({
     marginRight: 6,
     flexShrink: 0,
   },
+  desktopFeedsContainer: {
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+    marginTop: 18,
+    marginBottom: 18,
+  },
 })
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 6201f828f..4a9483733 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -29,13 +29,63 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
 import {useModerationOpts} from '#/state/queries/preferences'
 
-export function SearchResultCard({
-  profile,
+export const MATCH_HANDLE =
+  /@?([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))/
+
+export function SearchLinkCard({
+  label,
+  to,
+  onPress,
   style,
+}: {
+  label: string
+  to?: string
+  onPress?: () => void
+  style?: ViewStyle
+}) {
+  const pal = usePalette('default')
+
+  const inner = (
+    <View
+      style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
+      <Text type="md" style={[pal.text]}>
+        {label}
+      </Text>
+    </View>
+  )
+
+  if (onPress) {
+    return (
+      <TouchableOpacity
+        onPress={onPress}
+        accessibilityLabel={label}
+        accessibilityHint="">
+        {inner}
+      </TouchableOpacity>
+    )
+  }
+
+  return (
+    <Link href={to} asAnchor anchorNoUnderline>
+      <View
+        style={[
+          pal.border,
+          {paddingVertical: 16, paddingHorizontal: 12},
+          style,
+        ]}>
+        <Text type="md" style={[pal.text]}>
+          {label}
+        </Text>
+      </View>
+    </Link>
+  )
+}
+
+export function SearchProfileCard({
+  profile,
   moderation,
 }: {
   profile: AppBskyActorDefs.ProfileViewBasic
-  style: ViewStyle
   moderation: ProfileModeration
 }) {
   const pal = usePalette('default')
@@ -50,9 +100,7 @@ export function SearchResultCard({
       <View
         style={[
           pal.border,
-          style,
           {
-            borderTopWidth: 1,
             flexDirection: 'row',
             alignItems: 'center',
             gap: 12,
@@ -147,6 +195,11 @@ export function DesktopSearch() {
     navigation.dispatch(StackActions.push('Search', {q: query}))
   }, [query, navigation, setSearchResults])
 
+  const queryMaybeHandle = React.useMemo(() => {
+    const match = MATCH_HANDLE.exec(query)
+    return match && match[1]
+  }, [query])
+
   return (
     <View style={[styles.container, pal.view]}>
       <View
@@ -169,6 +222,9 @@ export function DesktopSearch() {
             accessibilityRole="search"
             accessibilityLabel={_(msg`Search`)}
             accessibilityHint=""
+            autoCorrect={false}
+            autoComplete="off"
+            autoCapitalize="none"
           />
           {query ? (
             <View style={styles.cancelBtn}>
@@ -176,7 +232,7 @@ export function DesktopSearch() {
                 onPress={onPressCancelSearch}
                 accessibilityRole="button"
                 accessibilityLabel={_(msg`Cancel search`)}
-                accessibilityHint="Exits inputting search query"
+                accessibilityHint={_(msg`Exits inputting search query`)}
                 onAccessibilityEscape={onPressCancelSearch}>
                 <Text type="lg" style={[pal.link]}>
                   <Trans>Cancel</Trans>
@@ -195,22 +251,26 @@ export function DesktopSearch() {
             </View>
           ) : (
             <>
-              {searchResults.length ? (
-                searchResults.map((item, i) => (
-                  <SearchResultCard
-                    key={item.did}
-                    profile={item}
-                    moderation={moderateProfile(item, moderationOpts)}
-                    style={i === 0 ? {borderTopWidth: 0} : {}}
-                  />
-                ))
-              ) : (
-                <View>
-                  <Text style={[pal.textLight, styles.noResults]}>
-                    <Trans>No results found for {query}</Trans>
-                  </Text>
-                </View>
-              )}
+              <SearchLinkCard
+                label={_(msg`Search for "${query}"`)}
+                to={`/search?q=${encodeURIComponent(query)}`}
+                style={{borderBottomWidth: 1}}
+              />
+
+              {queryMaybeHandle ? (
+                <SearchLinkCard
+                  label={_(msg`Go to @${queryMaybeHandle}`)}
+                  to={`/profile/${queryMaybeHandle}`}
+                />
+              ) : null}
+
+              {searchResults.map(item => (
+                <SearchProfileCard
+                  key={item.did}
+                  profile={item}
+                  moderation={moderateProfile(item, moderationOpts)}
+                />
+              ))}
             </>
           )}
         </View>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 51c03ae3d..5320aebfc 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -28,6 +28,7 @@ import {isAndroid} from 'platform/detection'
 import {useSession} from '#/state/session'
 import {useCloseAnyActiveElement} from '#/state/util'
 import * as notifications from 'lib/notifications/notifications'
+import {Outlet as PortalOutlet} from '#/components/Portal'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -94,6 +95,7 @@ function ShellInner() {
       </View>
       <Composer winHeight={winDim.height} />
       <ModalsContainer />
+      <PortalOutlet />
       <Lightbox />
     </>
   )
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 38da860bd..76f4f5c9b 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -15,6 +15,8 @@ import {useAuxClick} from 'lib/hooks/useAuxClick'
 import {t} from '@lingui/macro'
 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
 import {useCloseAllActiveElements} from '#/state/util'
+import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
+import {Outlet as PortalOutlet} from '#/components/Portal'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -23,6 +25,7 @@ function ShellInner() {
   const navigator = useNavigation<NavigationProp>()
   const closeAllActiveElements = useCloseAllActiveElements()
 
+  useWebBodyScrollLock(isDrawerOpen)
   useAuxClick()
 
   useEffect(() => {
@@ -33,27 +36,26 @@ function ShellInner() {
   }, [navigator, closeAllActiveElements])
 
   return (
-    <View style={[s.hContentRegion, {overflow: 'hidden'}]}>
-      <View style={s.hContentRegion}>
-        <ErrorBoundary>
-          <FlatNavigator />
-        </ErrorBoundary>
-      </View>
+    <>
+      <ErrorBoundary>
+        <FlatNavigator />
+      </ErrorBoundary>
       <Composer winHeight={0} />
       <ModalsContainer />
+      <PortalOutlet />
       <Lightbox />
       {!isDesktop && isDrawerOpen && (
         <TouchableOpacity
           onPress={() => setDrawerOpen(false)}
           style={styles.drawerMask}
           accessibilityLabel={t`Close navigation footer`}
-          accessibilityHint="Closes bottom navigation bar">
+          accessibilityHint={t`Closes bottom navigation bar`}>
           <View style={styles.drawerContainer}>
             <DrawerContent />
           </View>
         </TouchableOpacity>
       )}
-    </View>
+    </>
   )
 }
 
@@ -76,7 +78,8 @@ const styles = StyleSheet.create({
     backgroundColor: colors.black, // TODO
   },
   drawerMask: {
-    position: 'absolute',
+    // @ts-ignore web only
+    position: 'fixed',
     width: '100%',
     height: '100%',
     top: 0,
@@ -85,7 +88,8 @@ const styles = StyleSheet.create({
   },
   drawerContainer: {
     display: 'flex',
-    position: 'absolute',
+    // @ts-ignore web only
+    position: 'fixed',
     top: 0,
     left: 0,
     height: '100%',