about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx16
-rw-r--r--src/view/com/discover/WhoToFollow.tsx27
-rw-r--r--src/view/com/post/Post.tsx5
-rw-r--r--src/view/com/profile/ProfileCard.tsx4
-rw-r--r--src/view/com/search/HeaderWithInput.tsx146
-rw-r--r--src/view/com/search/SearchResults.tsx110
-rw-r--r--src/view/com/search/Suggestions.tsx50
-rw-r--r--src/view/com/util/TabBar.tsx1
-rw-r--r--src/view/com/util/UserBanner.tsx1
9 files changed, 319 insertions, 41 deletions
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index 7a64a15f6..bce224231 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -15,7 +15,7 @@ export const SuggestedFollows = ({
 }) => {
   const pal = usePalette('default')
   return (
-    <View style={[styles.container, pal.view]}>
+    <View style={[styles.container, pal.view, pal.border]}>
       <Text type="title" style={[styles.heading, pal.text]}>
         {title}
       </Text>
@@ -45,24 +45,16 @@ export const SuggestedFollows = ({
 
 const styles = StyleSheet.create({
   container: {
-    paddingVertical: 10,
-    paddingHorizontal: 4,
+    borderBottomWidth: 1,
   },
 
   heading: {
     fontWeight: 'bold',
-    paddingHorizontal: 4,
+    paddingHorizontal: 12,
     paddingBottom: 8,
   },
 
   card: {
-    borderRadius: 12,
-    marginBottom: 2,
-    borderWidth: 1,
-  },
-
-  loadMore: {
-    paddingLeft: 16,
-    paddingVertical: 12,
+    borderTopWidth: 1,
   },
 })
diff --git a/src/view/com/discover/WhoToFollow.tsx b/src/view/com/discover/WhoToFollow.tsx
index 17c10ca7e..715fadae2 100644
--- a/src/view/com/discover/WhoToFollow.tsx
+++ b/src/view/com/discover/WhoToFollow.tsx
@@ -1,10 +1,5 @@
 import React from 'react'
-import {
-  ActivityIndicator,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {useStores} from 'state/index'
 import {SuggestedActorsViewModel} from 'state/models/suggested-actors-view'
@@ -17,7 +12,7 @@ export const WhoToFollow = observer(() => {
   const pal = usePalette('default')
   const store = useStores()
   const suggestedActorsView = React.useMemo<SuggestedActorsViewModel>(
-    () => new SuggestedActorsViewModel(store, {pageSize: 5}),
+    () => new SuggestedActorsViewModel(store, {pageSize: 15}),
     [store],
   )
 
@@ -25,9 +20,6 @@ export const WhoToFollow = observer(() => {
     suggestedActorsView.loadMore(true)
   }, [store, suggestedActorsView])
 
-  const onPressLoadMoreSuggestedActors = () => {
-    suggestedActorsView.loadMore()
-  }
   return (
     <>
       {(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
@@ -50,15 +42,6 @@ export const WhoToFollow = observer(() => {
               />
             ))}
           </View>
-          {!suggestedActorsView.isLoading && suggestedActorsView.hasMore && (
-            <TouchableOpacity
-              onPress={onPressLoadMoreSuggestedActors}
-              style={styles.loadMore}>
-              <Text type="lg" style={pal.link}>
-                Show more
-              </Text>
-            </TouchableOpacity>
-          )}
         </>
       )}
       {suggestedActorsView.isLoading && (
@@ -74,16 +57,10 @@ const styles = StyleSheet.create({
   heading: {
     fontWeight: 'bold',
     paddingHorizontal: 12,
-    paddingTop: 16,
     paddingBottom: 8,
   },
 
   bottomBorder: {
     borderBottomWidth: 1,
   },
-
-  loadMore: {
-    paddingLeft: 16,
-    paddingVertical: 12,
-  },
 })
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index ac7d1cc55..a6c66d143 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -30,11 +30,13 @@ export const Post = observer(function Post({
   uri,
   initView,
   showReplyLine,
+  hideError,
   style,
 }: {
   uri: string
   initView?: PostThreadViewModel
   showReplyLine?: boolean
+  hideError?: boolean
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
@@ -70,6 +72,9 @@ export const Post = observer(function Post({
   // error
   // =
   if (view.hasError || !view.thread || !view.thread?.postRecord) {
+    if (hideError) {
+      return <View />
+    }
     return (
       <View style={pal.view}>
         <Text>{view.error || 'Thread not found'}</Text>
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 7b454cc8b..748648742 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -184,7 +184,7 @@ const styles = StyleSheet.create({
     paddingRight: 10,
   },
   details: {
-    paddingLeft: 60,
+    paddingLeft: 54,
     paddingRight: 10,
     paddingBottom: 10,
   },
@@ -202,7 +202,7 @@ const styles = StyleSheet.create({
 
   followedBy: {
     flexDirection: 'row',
-    alignItems: 'flex-start',
+    alignItems: 'center',
     paddingLeft: 54,
     paddingRight: 20,
     marginBottom: 10,
diff --git a/src/view/com/search/HeaderWithInput.tsx b/src/view/com/search/HeaderWithInput.tsx
new file mode 100644
index 000000000..cc0b90af7
--- /dev/null
+++ b/src/view/com/search/HeaderWithInput.tsx
@@ -0,0 +1,146 @@
+import React from 'react'
+import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {Text} from 'view/com/util/text/Text'
+import {MagnifyingGlassIcon} from 'lib/icons'
+import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {useAnalytics} from 'lib/analytics'
+
+const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
+
+interface Props {
+  isInputFocused: boolean
+  query: string
+  setIsInputFocused: (v: boolean) => void
+  onChangeQuery: (v: string) => void
+  onPressClearQuery: () => void
+  onPressCancelSearch: () => void
+  onSubmitQuery: () => void
+}
+export function HeaderWithInput({
+  isInputFocused,
+  query,
+  setIsInputFocused,
+  onChangeQuery,
+  onPressClearQuery,
+  onPressCancelSearch,
+  onSubmitQuery,
+}: Props) {
+  const store = useStores()
+  const theme = useTheme()
+  const pal = usePalette('default')
+  const {track} = useAnalytics()
+  const textInput = React.useRef<TextInput>(null)
+
+  const onPressMenu = React.useCallback(() => {
+    track('ViewHeader:MenuButtonClicked')
+    store.shell.openDrawer()
+  }, [track, store])
+
+  const onPressCancelSearchInner = React.useCallback(() => {
+    onPressCancelSearch()
+    textInput.current?.blur()
+  }, [onPressCancelSearch, textInput])
+
+  return (
+    <View style={[pal.view, pal.border, styles.header]}>
+      <TouchableOpacity
+        testID="viewHeaderBackOrMenuBtn"
+        onPress={onPressMenu}
+        hitSlop={MENU_HITSLOP}
+        style={styles.headerMenuBtn}>
+        <UserAvatar size={30} 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]}
+          keyboardAppearance={theme.colorScheme}
+          onFocus={() => setIsInputFocused(true)}
+          onBlur={() => setIsInputFocused(false)}
+          onChangeText={onChangeQuery}
+          onSubmitEditing={onSubmitQuery}
+        />
+        {query ? (
+          <TouchableOpacity onPress={onPressClearQuery}>
+            <FontAwesomeIcon
+              icon="xmark"
+              size={16}
+              style={pal.textLight as FontAwesomeIconStyle}
+            />
+          </TouchableOpacity>
+        ) : undefined}
+      </View>
+      {query || isInputFocused ? (
+        <View style={styles.headerCancelBtn}>
+          <TouchableOpacity onPress={onPressCancelSearchInner}>
+            <Text style={pal.text}>Cancel</Text>
+          </TouchableOpacity>
+        </View>
+      ) : undefined}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingVertical: 4,
+  },
+  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,
+  },
+
+  suggestions: {
+    marginBottom: 8,
+  },
+})
diff --git a/src/view/com/search/SearchResults.tsx b/src/view/com/search/SearchResults.tsx
new file mode 100644
index 000000000..062b703ee
--- /dev/null
+++ b/src/view/com/search/SearchResults.tsx
@@ -0,0 +1,110 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {SearchUIModel} from 'state/models/ui/search'
+import {CenteredView, ScrollView} from '../util/Views'
+import {Pager, RenderTabBarFnProps} from 'view/com/util/pager/Pager'
+import {TabBar} from 'view/com/util/TabBar'
+import {Post} from 'view/com/post/Post'
+import {ProfileCardWithFollowBtn} from 'view/com/profile/ProfileCard'
+import {
+  PostFeedLoadingPlaceholder,
+  ProfileCardFeedLoadingPlaceholder,
+} from 'view/com/util/LoadingPlaceholder'
+import {Text} from 'view/com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+
+const SECTIONS = ['Posts', 'Users']
+
+export const SearchResults = observer(({model}: {model: SearchUIModel}) => {
+  const pal = usePalette('default')
+
+  const renderTabBar = React.useCallback(
+    (props: RenderTabBarFnProps) => {
+      return (
+        <CenteredView style={[pal.border, styles.tabBar]}>
+          <TabBar {...props} items={SECTIONS} />
+        </CenteredView>
+      )
+    },
+    [pal],
+  )
+
+  return (
+    <Pager renderTabBar={renderTabBar} tabBarPosition="top" initialPage={0}>
+      <PostResults key="0" model={model} />
+      <Profiles key="1" model={model} />
+    </Pager>
+  )
+})
+
+const PostResults = observer(({model}: {model: SearchUIModel}) => {
+  const pal = usePalette('default')
+  if (model.isPostsLoading) {
+    return <PostFeedLoadingPlaceholder />
+  }
+
+  if (model.postUris.length === 0) {
+    return (
+      <Text type="xl" style={[styles.empty, pal.text]}>
+        No posts found for "{model.query}"
+      </Text>
+    )
+  }
+
+  return (
+    <ScrollView style={pal.view}>
+      {model.postUris.map(uri => (
+        <Post key={uri} uri={uri} hideError />
+      ))}
+      <View style={s.footerSpacer} />
+      <View style={s.footerSpacer} />
+      <View style={s.footerSpacer} />
+    </ScrollView>
+  )
+})
+
+const Profiles = observer(({model}: {model: SearchUIModel}) => {
+  const pal = usePalette('default')
+  if (model.isProfilesLoading) {
+    return <ProfileCardFeedLoadingPlaceholder />
+  }
+
+  if (model.profiles.length === 0) {
+    return (
+      <Text type="xl" style={[styles.empty, pal.text]}>
+        No users found for "{model.query}"
+      </Text>
+    )
+  }
+
+  return (
+    <ScrollView style={pal.view}>
+      {model.profiles.map(item => (
+        <ProfileCardWithFollowBtn
+          key={item.did}
+          did={item.did}
+          declarationCid={item.declaration.cid}
+          handle={item.handle}
+          displayName={item.displayName}
+          avatar={item.avatar}
+          description={item.description}
+        />
+      ))}
+      <View style={s.footerSpacer} />
+      <View style={s.footerSpacer} />
+      <View style={s.footerSpacer} />
+    </ScrollView>
+  )
+})
+
+const styles = StyleSheet.create({
+  tabBar: {
+    borderBottomWidth: 1,
+  },
+  empty: {
+    paddingHorizontal: 14,
+    paddingVertical: 16,
+  },
+})
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
new file mode 100644
index 000000000..1747036ba
--- /dev/null
+++ b/src/view/com/search/Suggestions.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {FoafsModel} from 'state/models/discovery/foafs'
+import {WhoToFollow} from 'view/com/discover/WhoToFollow'
+import {SuggestedFollows} from 'view/com/discover/SuggestedFollows'
+import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+
+export const Suggestions = observer(({foafs}: {foafs: FoafsModel}) => {
+  if (foafs.isLoading) {
+    return <ProfileCardFeedLoadingPlaceholder />
+  }
+  if (foafs.hasContent) {
+    return (
+      <>
+        {foafs.popular.length > 0 && (
+          <View style={styles.suggestions}>
+            <SuggestedFollows
+              title="In your network"
+              suggestions={foafs.popular}
+            />
+          </View>
+        )}
+        <WhoToFollow />
+        {foafs.sources.map((source, i) => {
+          const item = foafs.foafs.get(source)
+          if (!item || item.follows.length === 0) {
+            return <View key={`sf-${item?.did || i}`} />
+          }
+          return (
+            <View key={`sf-${item.did}`} style={styles.suggestions}>
+              <SuggestedFollows
+                title={`Followed by ${item.displayName || item.handle}`}
+                suggestions={item.follows.slice(0, 10)}
+              />
+            </View>
+          )
+        })}
+      </>
+    )
+  }
+  return <WhoToFollow />
+})
+
+const styles = StyleSheet.create({
+  suggestions: {
+    marginTop: 10,
+    marginBottom: 20,
+  },
+})
diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx
index 4b67b8a80..545a6b742 100644
--- a/src/view/com/util/TabBar.tsx
+++ b/src/view/com/util/TabBar.tsx
@@ -157,6 +157,5 @@ const styles = isDesktopWeb
         left: 0,
         width: 1,
         height: 3,
-        borderRadius: 4,
       },
     })
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 847ef6dba..1752c260e 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import Svg, {Rect} from 'react-native-svg'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import Image from 'view/com/util/images/Image'