about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx49
-rw-r--r--src/view/com/util/PostMeta.tsx11
-rw-r--r--src/view/com/util/TabBar.tsx162
-rw-r--r--src/view/com/util/WelcomeBanner.tsx101
-rw-r--r--src/view/com/util/pager/Pager.tsx87
-rw-r--r--src/view/com/util/pager/Pager.web.tsx69
6 files changed, 376 insertions, 103 deletions
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 9e72640d2..2f653ee09 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -128,6 +128,46 @@ export function NotificationFeedLoadingPlaceholder() {
   )
 }
 
+export function ProfileCardLoadingPlaceholder({
+  style,
+}: {
+  style?: StyleProp<ViewStyle>
+}) {
+  const pal = usePalette('default')
+  return (
+    <View style={[styles.profileCard, pal.view, style]}>
+      <LoadingPlaceholder
+        width={40}
+        height={40}
+        style={styles.profileCardAvi}
+      />
+      <View>
+        <LoadingPlaceholder width={140} height={8} style={[s.mb5]} />
+        <LoadingPlaceholder width={120} height={8} style={[s.mb10]} />
+        <LoadingPlaceholder width={220} height={8} style={[s.mb5]} />
+      </View>
+    </View>
+  )
+}
+
+export function ProfileCardFeedLoadingPlaceholder() {
+  return (
+    <>
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+      <ProfileCardLoadingPlaceholder />
+    </>
+  )
+}
+
 const styles = StyleSheet.create({
   loadingPlaceholder: {
     borderRadius: 6,
@@ -147,6 +187,15 @@ const styles = StyleSheet.create({
     paddingLeft: 46,
     margin: 1,
   },
+  profileCard: {
+    flexDirection: 'row',
+    padding: 10,
+    margin: 1,
+  },
+  profileCardAvi: {
+    borderRadius: 20,
+    marginRight: 10,
+  },
   smallAvatar: {
     borderRadius: 15,
     marginRight: 10,
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 0bb402100..c53de5c1f 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -44,7 +44,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
     // two-liner with follow button
     return (
       <View style={styles.metaTwoLine}>
-        <View>
+        <View style={styles.metaTwoLineLeft}>
           <View style={styles.metaTwoLineTop}>
             <DesktopWebTextLink
               type="lg-bold"
@@ -69,6 +69,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
             type="md"
             style={[styles.metaItem, pal.textLight]}
             lineHeight={1.2}
+            numberOfLines={1}
             text={`@${handle}`}
             href={`/profile/${opts.authorHandle}`}
           />
@@ -76,6 +77,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
 
         <View>
           <FollowButton
+            type="default"
             did={opts.did}
             declarationCid={opts.declarationCid}
             onToggleFollow={onToggleFollow}
@@ -134,7 +136,12 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'space-between',
-    paddingBottom: 2,
+    width: '100%',
+    paddingBottom: 4,
+  },
+  metaTwoLineLeft: {
+    flex: 1,
+    paddingRight: 40,
   },
   metaTwoLineTop: {
     flexDirection: 'row',
diff --git a/src/view/com/util/TabBar.tsx b/src/view/com/util/TabBar.tsx
new file mode 100644
index 000000000..4b67b8a80
--- /dev/null
+++ b/src/view/com/util/TabBar.tsx
@@ -0,0 +1,162 @@
+import React, {createRef, useState, useMemo} from 'react'
+import {
+  Animated,
+  StyleSheet,
+  TouchableWithoutFeedback,
+  View,
+} from 'react-native'
+import {Text} from './text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+
+interface Layout {
+  x: number
+  width: number
+}
+
+export interface TabBarProps {
+  selectedPage: number
+  items: string[]
+  position: Animated.Value
+  offset: Animated.Value
+  indicatorPosition?: 'top' | 'bottom'
+  indicatorColor?: string
+  onSelect?: (index: number) => void
+  onPressSelected?: () => void
+}
+
+export function TabBar({
+  selectedPage,
+  items,
+  position,
+  offset,
+  indicatorPosition = 'bottom',
+  indicatorColor,
+  onSelect,
+  onPressSelected,
+}: TabBarProps) {
+  const pal = usePalette('default')
+  const [itemLayouts, setItemLayouts] = useState<Layout[]>(
+    items.map(() => ({x: 0, width: 0})),
+  )
+  const itemRefs = useMemo(
+    () => Array.from({length: items.length}).map(() => createRef<View>()),
+    [items.length],
+  )
+  const panX = Animated.add(position, offset)
+
+  const indicatorStyle = {
+    backgroundColor: indicatorColor || pal.colors.link,
+    bottom:
+      indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined,
+    top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined,
+    transform: [
+      {
+        translateX: panX.interpolate({
+          inputRange: items.map((_item, i) => i),
+          outputRange: itemLayouts.map(l => l.x + l.width / 2),
+        }),
+      },
+      {
+        scaleX: panX.interpolate({
+          inputRange: items.map((_item, i) => i),
+          outputRange: itemLayouts.map(l => l.width),
+        }),
+      },
+    ],
+  }
+
+  const onLayout = () => {
+    const promises = []
+    for (let i = 0; i < items.length; i++) {
+      promises.push(
+        new Promise<Layout>(resolve => {
+          itemRefs[i].current?.measure(
+            (x: number, _y: number, width: number) => {
+              resolve({x, width})
+            },
+          )
+        }),
+      )
+    }
+    Promise.all(promises).then((layouts: Layout[]) => {
+      setItemLayouts(layouts)
+    })
+  }
+
+  const onPressItem = (index: number) => {
+    onSelect?.(index)
+    if (index === selectedPage) {
+      onPressSelected?.()
+    }
+  }
+
+  return (
+    <View style={[pal.view, styles.outer]} onLayout={onLayout}>
+      <Animated.View style={[styles.indicator, indicatorStyle]} />
+      {items.map((item, i) => {
+        const selected = i === selectedPage
+        return (
+          <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
+            <View
+              style={
+                indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
+              }
+              ref={itemRefs[i]}>
+              <Text type="xl-bold" style={selected ? pal.text : pal.textLight}>
+                {item}
+              </Text>
+            </View>
+          </TouchableWithoutFeedback>
+        )
+      })}
+    </View>
+  )
+}
+
+const styles = isDesktopWeb
+  ? StyleSheet.create({
+      outer: {
+        flexDirection: 'row',
+        paddingHorizontal: 18,
+      },
+      itemTop: {
+        paddingTop: 16,
+        paddingBottom: 14,
+        marginRight: 24,
+      },
+      itemBottom: {
+        paddingTop: 14,
+        paddingBottom: 16,
+        marginRight: 24,
+      },
+      indicator: {
+        position: 'absolute',
+        left: 0,
+        width: 1,
+        height: 3,
+      },
+    })
+  : StyleSheet.create({
+      outer: {
+        flexDirection: 'row',
+        paddingHorizontal: 14,
+      },
+      itemTop: {
+        paddingTop: 10,
+        paddingBottom: 10,
+        marginRight: 24,
+      },
+      itemBottom: {
+        paddingTop: 8,
+        paddingBottom: 12,
+        marginRight: 24,
+      },
+      indicator: {
+        position: 'absolute',
+        left: 0,
+        width: 1,
+        height: 3,
+        borderRadius: 4,
+      },
+    })
diff --git a/src/view/com/util/WelcomeBanner.tsx b/src/view/com/util/WelcomeBanner.tsx
deleted file mode 100644
index 428a30764..000000000
--- a/src/view/com/util/WelcomeBanner.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from './text/Text'
-import {Button} from './forms/Button'
-import {s} from 'lib/styles'
-import {useStores} from 'state/index'
-import {SUGGESTED_FOLLOWS} from 'lib/constants'
-// @ts-ignore no type definition -prf
-import ProgressBar from 'react-native-progress/Bar'
-import {CenteredView} from './Views'
-
-export const WelcomeBanner = observer(() => {
-  const pal = usePalette('default')
-  const store = useStores()
-  const [isReady, setIsReady] = React.useState(false)
-
-  const numFollows = Math.min(
-    SUGGESTED_FOLLOWS(String(store.agent.service)).length,
-    5,
-  )
-  const remaining = numFollows - store.me.follows.numFollows
-
-  React.useEffect(() => {
-    if (remaining <= 0) {
-      // wait 500ms for the progress bar anim to finish
-      const ti = setTimeout(() => {
-        setIsReady(true)
-      }, 500)
-      return () => clearTimeout(ti)
-    } else {
-      setIsReady(false)
-    }
-  }, [remaining])
-
-  const onPressDone = React.useCallback(() => {
-    store.shell.setOnboarding(false)
-  }, [store])
-
-  return (
-    <CenteredView
-      testID="welcomeBanner"
-      style={[pal.view, styles.container, pal.border]}>
-      <Text
-        type="title-lg"
-        style={[pal.text, s.textCenter, s.bold, s.pb5]}
-        lineHeight={1.1}>
-        Welcome to Bluesky!
-      </Text>
-      {isReady ? (
-        <View style={styles.controls}>
-          <Button
-            type="primary"
-            style={[s.flexRow, s.alignCenter]}
-            onPress={onPressDone}>
-            <Text type="md-bold" style={s.white}>
-              See my feed!
-            </Text>
-            <FontAwesomeIcon icon="angle-right" size={14} style={s.white} />
-          </Button>
-        </View>
-      ) : (
-        <>
-          <Text type="lg" style={[pal.text, s.textCenter]}>
-            Follow at least {remaining} {remaining === 1 ? 'person' : 'people'}{' '}
-            to build your feed.
-          </Text>
-          <View style={[styles.controls, styles.progress]}>
-            <ProgressBar
-              progress={Math.max(
-                store.me.follows.numFollows / numFollows,
-                0.05,
-              )}
-            />
-          </View>
-        </>
-      )}
-    </CenteredView>
-  )
-})
-
-const styles = StyleSheet.create({
-  container: {
-    paddingTop: 16,
-    paddingBottom: 16,
-    paddingHorizontal: 20,
-    borderTopWidth: 1,
-    borderBottomWidth: 1,
-  },
-  controls: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    marginTop: 10,
-  },
-  progress: {
-    marginTop: 12,
-  },
-})
diff --git a/src/view/com/util/pager/Pager.tsx b/src/view/com/util/pager/Pager.tsx
new file mode 100644
index 000000000..416828a27
--- /dev/null
+++ b/src/view/com/util/pager/Pager.tsx
@@ -0,0 +1,87 @@
+import React from 'react'
+import {Animated, View} from 'react-native'
+import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {s} from 'lib/styles'
+
+export type PageSelectedEvent = PagerViewOnPageSelectedEvent
+const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
+
+export interface RenderTabBarFnProps {
+  selectedPage: number
+  position: Animated.Value
+  offset: Animated.Value
+  onSelect?: (index: number) => void
+}
+export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
+
+interface Props {
+  tabBarPosition?: 'top' | 'bottom'
+  initialPage?: number
+  renderTabBar: RenderTabBarFn
+  onPageSelected?: (index: number) => void
+}
+export const Pager = ({
+  children,
+  tabBarPosition = 'top',
+  initialPage = 0,
+  renderTabBar,
+  onPageSelected,
+}: React.PropsWithChildren<Props>) => {
+  const [selectedPage, setSelectedPage] = React.useState(0)
+  const position = useAnimatedValue(0)
+  const offset = useAnimatedValue(0)
+  const pagerView = React.useRef<PagerView>()
+
+  const onPageSelectedInner = React.useCallback(
+    (e: PageSelectedEvent) => {
+      setSelectedPage(e.nativeEvent.position)
+      onPageSelected?.(e.nativeEvent.position)
+    },
+    [setSelectedPage, onPageSelected],
+  )
+
+  const onTabBarSelect = React.useCallback(
+    (index: number) => {
+      pagerView.current?.setPage(index)
+    },
+    [pagerView],
+  )
+
+  return (
+    <View>
+      {tabBarPosition === 'top' &&
+        renderTabBar({
+          selectedPage,
+          position,
+          offset,
+          onSelect: onTabBarSelect,
+        })}
+      <AnimatedPagerView
+        ref={pagerView}
+        style={s.h100pct}
+        initialPage={initialPage}
+        onPageSelected={onPageSelectedInner}
+        onPageScroll={Animated.event(
+          [
+            {
+              nativeEvent: {
+                position: position,
+                offset: offset,
+              },
+            },
+          ],
+          {useNativeDriver: true},
+        )}>
+        {children}
+      </AnimatedPagerView>
+      {tabBarPosition === 'bottom' &&
+        renderTabBar({
+          selectedPage,
+          position,
+          offset,
+          onSelect: onTabBarSelect,
+        })}
+    </View>
+  )
+}
diff --git a/src/view/com/util/pager/Pager.web.tsx b/src/view/com/util/pager/Pager.web.tsx
new file mode 100644
index 000000000..3c2805833
--- /dev/null
+++ b/src/view/com/util/pager/Pager.web.tsx
@@ -0,0 +1,69 @@
+import React from 'react'
+import {Animated, View} from 'react-native'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {s} from 'lib/styles'
+
+export interface RenderTabBarFnProps {
+  selectedPage: number
+  position: Animated.Value
+  offset: Animated.Value
+  onSelect?: (index: number) => void
+}
+export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
+
+interface Props {
+  tabBarPosition?: 'top' | 'bottom'
+  initialPage?: number
+  renderTabBar: RenderTabBarFn
+  onPageSelected?: (index: number) => void
+}
+export const Pager = ({
+  children,
+  tabBarPosition = 'top',
+  initialPage = 0,
+  renderTabBar,
+  onPageSelected,
+}: React.PropsWithChildren<Props>) => {
+  const [selectedPage, setSelectedPage] = React.useState(initialPage)
+  const position = useAnimatedValue(0)
+  const offset = useAnimatedValue(0)
+
+  const onTabBarSelect = React.useCallback(
+    (index: number) => {
+      setSelectedPage(index)
+      onPageSelected?.(index)
+      Animated.timing(position, {
+        toValue: index,
+        duration: 200,
+        useNativeDriver: true,
+      }).start()
+    },
+    [setSelectedPage, onPageSelected, position],
+  )
+
+  return (
+    <View>
+      {tabBarPosition === 'top' &&
+        renderTabBar({
+          selectedPage,
+          position,
+          offset,
+          onSelect: onTabBarSelect,
+        })}
+      {children.map((child, i) => (
+        <View
+          style={selectedPage === i ? undefined : s.hidden}
+          key={`page-${i}`}>
+          {child}
+        </View>
+      ))}
+      {tabBarPosition === 'bottom' &&
+        renderTabBar({
+          selectedPage,
+          position,
+          offset,
+          onSelect: onTabBarSelect,
+        })}
+    </View>
+  )
+}