about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-02-23 01:07:20 -0600
committerPaul Frazee <pfrazee@gmail.com>2023-02-23 01:07:20 -0600
commite828f380e779b7c6c0b6cb45508ed59516c0feda (patch)
tree06a311ebb2d1a39ef4ea70fc2882b8e315938957
parentc5f28376c86478b4c55d9f0910d8d0cb4a1feb06 (diff)
downloadvoidsky-e828f380e779b7c6c0b6cb45508ed59516c0feda.tar.zst
Update web header and search
-rw-r--r--babel.config.js1
-rw-r--r--src/view/com/util/ViewHeader.web.tsx138
-rw-r--r--src/view/com/util/Views.web.tsx13
-rw-r--r--src/view/screens/Home.tsx4
-rw-r--r--src/view/screens/Search.web.tsx217
-rw-r--r--src/view/shell/web/DesktopLeftColumn.tsx56
-rw-r--r--src/view/shell/web/DesktopRightColumn.tsx3
-rw-r--r--tsconfig.json1
8 files changed, 290 insertions, 143 deletions
diff --git a/babel.config.js b/babel.config.js
index 6ba90e98d..fa49ff866 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -20,6 +20,7 @@ module.exports = {
         alias: {
           // This needs to be mirrored in tsconfig.json
           lib: './src/lib',
+          platform: './src/platform',
           state: './src/state',
           view: './src/view',
         },
diff --git a/src/view/com/util/ViewHeader.web.tsx b/src/view/com/util/ViewHeader.web.tsx
index 5c0869e8b..ef70ecaba 100644
--- a/src/view/com/util/ViewHeader.web.tsx
+++ b/src/view/com/util/ViewHeader.web.tsx
@@ -1,69 +1,51 @@
 import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {CenteredView} from './Views'
 import {Text} from './text/Text'
-import {useStores} from 'state/index'
+import {Link} from './Link'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {ComposeIcon, MagnifyingGlassIcon} from 'lib/icons'
 import {colors} from 'lib/styles'
 
-const BACK_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
-
 export const ViewHeader = observer(function ViewHeader({
   title,
-  subtitle,
-  canGoBack,
 }: {
   title: string
-  subtitle?: string
   canGoBack?: boolean
 }) {
-  const pal = usePalette('default')
   const store = useStores()
-  const onPressBack = () => {
-    store.nav.tab.goBack()
-  }
-  if (typeof canGoBack === 'undefined') {
-    canGoBack = store.nav.tab.canGoBack
-  }
+  const pal = usePalette('default')
+  const onPressCompose = () => store.shell.openComposer({})
   return (
-    <CenteredView style={[styles.header, pal.view]}>
-      {canGoBack ? (
-        <>
-          <TouchableOpacity
-            testID="viewHeaderBackOrMenuBtn"
-            onPress={onPressBack}
-            hitSlop={BACK_HITSLOP}
-            style={styles.backBtn}>
-            <FontAwesomeIcon
-              size={18}
-              icon="angle-left"
-              style={[styles.backIcon, pal.text]}
-            />
-          </TouchableOpacity>
-          <View style={styles.titleContainer} pointerEvents="none">
-            <Text type="title" style={[pal.text, styles.title]}>
-              {title}
-            </Text>
-            {subtitle ? (
-              <Text
-                type="title-sm"
-                style={[styles.subtitle, pal.textLight]}
-                numberOfLines={1}>
-                {subtitle}
-              </Text>
-            ) : undefined}
-          </View>
-        </>
-      ) : (
-        <View style={styles.titleContainer} pointerEvents="none">
-          <Text type="title" style={[pal.text, styles.title]}>
-            Home
-          </Text>
+    <View style={[styles.header, pal.borderDark, pal.view]}>
+      <View style={styles.titleContainer} pointerEvents="none">
+        <Text type="title-2xl" style={[pal.text, styles.title]}>
+          {title}
+        </Text>
+      </View>
+      <TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
+        <View style={styles.newPostBtnIconWrapper}>
+          <ComposeIcon
+            size={16}
+            strokeWidth={1.5}
+            style={styles.newPostBtnLabel}
+          />
         </View>
-      )}
-    </CenteredView>
+        <Text type="md" style={styles.newPostBtnLabel}>
+          New Post
+        </Text>
+      </TouchableOpacity>
+      <Link href="/search" style={[pal.view, pal.borderDark, styles.search]}>
+        <MagnifyingGlassIcon
+          size={18}
+          style={[pal.textLight, styles.searchIconWrapper]}
+        />
+        <Text type="md-thin" style={pal.textLight}>
+          Search
+        </Text>
+      </Link>
+    </View>
   )
 })
 
@@ -71,44 +53,52 @@ const styles = StyleSheet.create({
   header: {
     flexDirection: 'row',
     alignItems: 'center',
-    paddingHorizontal: 16,
-    paddingVertical: 12,
+    paddingTop: 24,
+    paddingBottom: 18,
+    paddingLeft: 30,
+    paddingRight: 40,
+    marginLeft: 300,
+    borderBottomWidth: 1,
   },
 
   titleContainer: {
-    flexDirection: 'row',
-    alignItems: 'baseline',
     marginRight: 'auto',
   },
   title: {
     fontWeight: 'bold',
   },
-  subtitle: {
-    marginLeft: 4,
-    maxWidth: 200,
-    fontWeight: 'normal',
-  },
 
-  backBtn: {
-    width: 30,
+  search: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    width: 300,
+    borderRadius: 20,
+    paddingVertical: 8,
+    paddingHorizontal: 10,
+    borderWidth: 1,
   },
-  backIcon: {
-    position: 'relative',
-    top: -1,
+  searchIconWrapper: {
+    flexDirection: 'row',
+    width: 30,
+    justifyContent: 'center',
+    marginRight: 2,
   },
-  btn: {
+
+  newPostBtn: {
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'center',
-    width: 36,
-    height: 36,
-    borderRadius: 20,
-    marginLeft: 4,
+    borderRadius: 24,
+    paddingTop: 8,
+    paddingBottom: 9,
+    paddingHorizontal: 18,
+    backgroundColor: colors.blue3,
+    marginRight: 10,
+  },
+  newPostBtnIconWrapper: {
+    marginRight: 8,
   },
-  littleXIcon: {
-    color: colors.red3,
-    position: 'absolute',
-    right: 7,
-    bottom: 7,
+  newPostBtnLabel: {
+    color: colors.white,
   },
 })
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index f00d3c072..3d9abd893 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -22,7 +22,6 @@ import {
   View,
   ViewProps,
 } from 'react-native'
-import {useTheme} from 'lib/ThemeContext'
 import {addStyle, colors} from 'lib/styles'
 
 export function CenteredView({
@@ -40,15 +39,10 @@ export const FlatList = React.forwardRef(function <ItemT>(
   }: React.PropsWithChildren<FlatListProps<ItemT>>,
   ref: React.Ref<RNFlatList>,
 ) {
-  const theme = useTheme()
   contentContainerStyle = addStyle(
     contentContainerStyle,
     styles.containerScroll,
   )
-  contentContainerStyle = addStyle(
-    contentContainerStyle,
-    theme.colorScheme === 'dark' ? styles.containerDark : styles.containerLight,
-  )
   return (
     <RNFlatList
       contentContainerStyle={contentContainerStyle}
@@ -60,17 +54,12 @@ export const FlatList = React.forwardRef(function <ItemT>(
 
 export const ScrollView = React.forwardRef(function (
   {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
-  ref: React.Ref<RNFlatList>,
+  ref: React.Ref<RNScrollView>,
 ) {
-  const theme = useTheme()
   contentContainerStyle = addStyle(
     contentContainerStyle,
     styles.containerScroll,
   )
-  contentContainerStyle = addStyle(
-    contentContainerStyle,
-    theme.colorScheme === 'dark' ? styles.containerDark : styles.containerLight,
-  )
   return (
     <RNScrollView
       contentContainerStyle={contentContainerStyle}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index de7e61ba4..125c57406 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -11,6 +11,7 @@ import {ScreenParams} from '../routes'
 import {s} from 'lib/styles'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useAnalytics} from 'lib/analytics'
+import {isWeb} from 'platform/detection'
 
 const HEADER_HEIGHT = 42
 
@@ -91,6 +92,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
 
   return (
     <View style={s.h100pct}>
+      {isWeb && <ViewHeader title="Home Feed" canGoBack={false} />}
       <Feed
         testID="homeFeed"
         key="default"
@@ -102,7 +104,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
         onScroll={onMainScroll}
         headerOffset={HEADER_HEIGHT}
       />
-      <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
+      {!isWeb && <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />}
       {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
         <LoadLatestBtn onPress={onPressLoadLatest} />
       )}
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
new file mode 100644
index 000000000..38f7cefb8
--- /dev/null
+++ b/src/view/screens/Search.web.tsx
@@ -0,0 +1,217 @@
+import React from 'react'
+import {
+  Keyboard,
+  StyleSheet,
+  TextInput,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {ScrollView} from '../com/util/Views'
+import {observer} from 'mobx-react-lite'
+import {UserAvatar} from '../com/util/UserAvatar'
+import {Text} from '../com/util/text/Text'
+import {ScreenParams} from '../routes'
+import {useStores} from 'state/index'
+import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {s} from 'lib/styles'
+import {MagnifyingGlassIcon} from 'lib/icons'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {WhoToFollow} from '../com/discover/WhoToFollow'
+import {SuggestedPosts} from '../com/discover/SuggestedPosts'
+import {ProfileCard} from '../com/profile/ProfileCard'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {useAnalytics} from 'lib/analytics'
+
+const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
+const FIVE_MIN = 5 * 60 * 1e3
+
+export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
+  const pal = usePalette('default')
+  const store = useStores()
+  const {track} = useAnalytics()
+  const scrollElRef = React.useRef<ScrollView>(null)
+  const onMainScroll = useOnMainScroll(store)
+  const textInput = React.useRef<TextInput>(null)
+  const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
+  const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
+  const [query, setQuery] = React.useState<string>('')
+  const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
+    () => new UserAutocompleteViewModel(store),
+    [store],
+  )
+  const {name} = params
+
+  const onSoftReset = () => {
+    scrollElRef.current?.scrollTo({x: 0, y: 0})
+  }
+
+  React.useEffect(() => {
+    const softResetSub = store.onScreenSoftReset(onSoftReset)
+    const cleanup = () => {
+      softResetSub.remove()
+    }
+
+    if (visible) {
+      const now = Date.now()
+      if (now - lastRenderTime > FIVE_MIN) {
+        setRenderTime(Date.now()) // trigger reload of suggestions
+      }
+      store.shell.setMinimalShellMode(false)
+      autocompleteView.setup()
+      store.nav.setTitle(navIdx, 'Search')
+    }
+    return cleanup
+  }, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
+
+  const onPressMenu = () => {
+    track('ViewHeader:MenuButtonClicked')
+    store.shell.setMainMenuOpen(true)
+  }
+
+  const onChangeQuery = (text: string) => {
+    setQuery(text)
+    if (text.length > 0) {
+      autocompleteView.setActive(true)
+      autocompleteView.setPrefix(text)
+    } else {
+      autocompleteView.setActive(false)
+    }
+  }
+  const onPressCancelSearch = () => {
+    setQuery('')
+    autocompleteView.setActive(false)
+  }
+
+  return (
+    <View style={s.h100pct}>
+      <ViewHeader title="Explore" />
+      <ScrollView
+        ref={scrollElRef}
+        testID="searchScrollView"
+        style={[pal.view, styles.container]}
+        onScroll={onMainScroll}
+        scrollEventThrottle={100}>
+        <View style={[pal.view, pal.border, styles.header]}>
+          <TouchableOpacity
+            testID="viewHeaderBackOrMenuBtn"
+            onPress={onPressMenu}
+            hitSlop={MENU_HITSLOP}
+            style={styles.headerMenuBtn}>
+            <UserAvatar
+              size={30}
+              handle={store.me.handle}
+              displayName={store.me.displayName}
+              avatar={store.me.avatar}
+            />
+          </TouchableOpacity>
+          <View
+            style={[
+              {backgroundColor: pal.colors.backgroundLight},
+              styles.headerSearchContainer,
+            ]}>
+            <MagnifyingGlassIcon
+              style={[pal.icon, styles.headerSearchIcon]}
+              size={21}
+            />
+            <TextInput
+              testID="searchTextInput"
+              ref={textInput}
+              placeholder="Search"
+              placeholderTextColor={pal.colors.textLight}
+              selectTextOnFocus
+              returnKeyType="search"
+              value={query}
+              style={[pal.text, styles.headerSearchInput]}
+              onFocus={() => setIsInputFocused(true)}
+              onBlur={() => setIsInputFocused(false)}
+              onChangeText={onChangeQuery}
+            />
+          </View>
+          {query ? (
+            <View style={styles.headerCancelBtn}>
+              <TouchableOpacity onPress={onPressCancelSearch}>
+                <Text>Cancel</Text>
+              </TouchableOpacity>
+            </View>
+          ) : undefined}
+        </View>
+        {query && autocompleteView.searchRes.length ? (
+          <>
+            {autocompleteView.searchRes.map(item => (
+              <ProfileCard
+                key={item.did}
+                handle={item.handle}
+                displayName={item.displayName}
+                avatar={item.avatar}
+              />
+            ))}
+          </>
+        ) : query && !autocompleteView.searchRes.length ? (
+          <View>
+            <Text style={[pal.textLight, styles.searchPrompt]}>
+              No results found for {autocompleteView.prefix}
+            </Text>
+          </View>
+        ) : isInputFocused ? (
+          <View>
+            <Text style={[pal.textLight, styles.searchPrompt]}>
+              Search for users on the network
+            </Text>
+          </View>
+        ) : (
+          <ScrollView onScroll={Keyboard.dismiss}>
+            <WhoToFollow key={`wtf-${lastRenderTime}`} />
+            <SuggestedPosts key={`sp-${lastRenderTime}`} />
+            <View style={s.footerSpacer} />
+          </ScrollView>
+        )}
+        <View style={s.footerSpacer} />
+      </ScrollView>
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingTop: 4,
+    marginBottom: 14,
+  },
+  headerMenuBtn: {
+    width: 40,
+    height: 30,
+    marginLeft: 6,
+  },
+  headerSearchContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 30,
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+  },
+  headerSearchIcon: {
+    marginRight: 6,
+    alignSelf: 'center',
+  },
+  headerSearchInput: {
+    flex: 1,
+    fontSize: 17,
+  },
+  headerCancelBtn: {
+    width: 60,
+    paddingLeft: 10,
+  },
+
+  searchPrompt: {
+    textAlign: 'center',
+    paddingTop: 10,
+  },
+})
diff --git a/src/view/shell/web/DesktopLeftColumn.tsx b/src/view/shell/web/DesktopLeftColumn.tsx
index 819bcba6d..54e3e93e1 100644
--- a/src/view/shell/web/DesktopLeftColumn.tsx
+++ b/src/view/shell/web/DesktopLeftColumn.tsx
@@ -70,12 +70,7 @@ export const DesktopLeftColumn = observer(() => {
     styles.containerBgLight,
     styles.containerBgDark,
   )
-  const hoverBg = useColorSchemeStyle(
-    styles.navItemHoverBgLight,
-    styles.navItemHoverBgDark,
-  )
   const pal = usePalette('default')
-  const onPressCompose = () => store.shell.openComposer({})
   const avi = (
     <UserAvatar
       handle={store.me.handle}
@@ -90,15 +85,6 @@ export const DesktopLeftColumn = observer(() => {
         <Link style={styles.logo} href="/">
           <Text type="title-xl">Bluesky</Text>
         </Link>
-        <Link href="/search" style={[pal.view, pal.borderDark, styles.search]}>
-          <MagnifyingGlassIcon
-            size={18}
-            style={[pal.textLight, styles.searchIconWrapper]}
-          />
-          <Text type="md-thin" style={pal.textLight}>
-            Search
-          </Text>
-        </Link>
         <NavItem
           href="/"
           label="Home"
@@ -124,19 +110,6 @@ export const DesktopLeftColumn = observer(() => {
           icon={<CogIcon strokeWidth={2} size={21} />}
           iconFilled={<CogIcon strokeWidth={2.5} size={21} />}
         />
-        <View style={[pal.border, styles.separator]} />
-        <Pressable
-          style={state => [
-            // @ts-ignore Pressable state differs for RNW -prf
-            state.hovered && hoverBg,
-          ]}>
-          <TouchableOpacity style={styles.navItem} onPress={onPressCompose}>
-            <View style={styles.navItemIconWrapper}>
-              <ComposeIcon size={21} />
-            </View>
-            <Text type="xl-thin">New Post</Text>
-          </TouchableOpacity>
-        </Pressable>
       </View>
       <View style={[styles.footer, pal.borderDark]}>
         <NavItem
@@ -183,24 +156,8 @@ const styles = StyleSheet.create({
   },
 
   logo: {
-    paddingTop: 6,
-    paddingBottom: 12,
-  },
-
-  search: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    borderRadius: 8,
-    paddingVertical: 8,
-    paddingHorizontal: 6,
-    marginBottom: 10,
-    borderWidth: 1,
-  },
-  searchIconWrapper: {
-    flexDirection: 'row',
-    width: 30,
-    justifyContent: 'center',
-    marginRight: 6,
+    paddingTop: 8,
+    paddingBottom: 14,
   },
 
   navItem: {
@@ -240,13 +197,4 @@ const styles = StyleSheet.create({
     paddingHorizontal: 4,
     borderRadius: 6,
   },
-  composeBtn: {
-    marginTop: 20,
-    marginBottom: 10,
-    marginLeft: 10,
-    marginRight: 20,
-    borderRadius: 30,
-    paddingHorizontal: 20,
-    paddingVertical: 12,
-  },
 })
diff --git a/src/view/shell/web/DesktopRightColumn.tsx b/src/view/shell/web/DesktopRightColumn.tsx
index b051e6094..2f70a43d2 100644
--- a/src/view/shell/web/DesktopRightColumn.tsx
+++ b/src/view/shell/web/DesktopRightColumn.tsx
@@ -3,10 +3,8 @@ import {StyleSheet, View} from 'react-native'
 import {Text} from '../../com/util/text/Text'
 import {LiteSuggestedFollows} from '../../com/discover/LiteSuggestedFollows'
 import {s} from 'lib/styles'
-import {useStores} from 'state/index'
 
 export const DesktopRightColumn: React.FC = () => {
-  const store = useStores()
   return (
     <View style={styles.container}>
       <Text type="lg-bold" style={s.mb10}>
@@ -21,6 +19,7 @@ const styles = StyleSheet.create({
   container: {
     position: 'absolute',
     right: 0,
+    top: 90,
     width: '400px',
     paddingHorizontal: 16,
     paddingRight: 32,
diff --git a/tsconfig.json b/tsconfig.json
index 2b93dd0fb..cace91f5e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,7 @@
   "compilerOptions": {
     "paths": {
       "lib/*": ["./src/lib/*"],
+      "platform/*": ["./src/platform/*"],
       "state/*": ["./src/state/*"],
       "view/*": ["./src/view/*"]
     }