about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-05-24 18:46:55 -0500
committerPaul Frazee <pfrazee@gmail.com>2023-05-24 18:46:55 -0500
commit0a18229784ea6c67e5ac17e4bae1a79a24f80d7f (patch)
treeaf829bdacd5489e9e012b74a3778752ed3efbce0
parent4e1876fe85ab3a70eba50466a62bff8a9d01c16c (diff)
parent32c9dabb7467149baf39d8f5c2eb3d0b81236d92 (diff)
downloadvoidsky-0a18229784ea6c67e5ac17e4bae1a79a24f80d7f.tar.zst
Merge branch 'custom-algos' of github.com:bluesky-social/social-app into custom-algos
-rw-r--r--.eslintrc.js6
-rw-r--r--src/lib/constants.ts9
-rw-r--r--src/lib/hooks/useDraggableScrollView.ts84
-rw-r--r--src/lib/merge-refs.ts27
-rw-r--r--src/view/com/pager/DraggableScrollView.tsx15
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx2
-rw-r--r--src/view/com/pager/TabBar.tsx5
7 files changed, 141 insertions, 7 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index 2d59d36dd..19fcf2308 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,6 +1,10 @@
 module.exports = {
   root: true,
-  extends: ['@react-native-community', 'plugin:react-native-a11y/ios'],
+  extends: [
+    '@react-native-community',
+    'plugin:react-native-a11y/ios',
+    'prettier',
+  ],
   parser: '@typescript-eslint/parser',
   plugins: ['@typescript-eslint', 'detox'],
   ignorePatterns: [
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index e492dd61a..c42e6f3a9 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -102,13 +102,15 @@ export async function DEFAULT_FEEDS(
   serviceUrl: string,
   resolveHandle: (name: string) => Promise<string>,
 ) {
-  if (serviceUrl.includes('localhost')) { // local dev
+  if (serviceUrl.includes('localhost')) {
+    // local dev
     const aliceDid = await resolveHandle('alice.test')
     return {
       pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
       saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
     }
-  } else if (serviceUrl.includes('staging')) { // staging
+  } else if (serviceUrl.includes('staging')) {
+    // staging
     return {
       pinned: [STAGING_DEFAULT_FEED('whats-hot')],
       saved: [
@@ -118,7 +120,8 @@ export async function DEFAULT_FEEDS(
         STAGING_DEFAULT_FEED('hot-classic'),
       ],
     }
-  } else { // production
+  } else {
+    // production
     return {
       pinned: [PROD_DEFAULT_FEED('whats-hot')],
       saved: [
diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts
new file mode 100644
index 000000000..b0f7465d7
--- /dev/null
+++ b/src/lib/hooks/useDraggableScrollView.ts
@@ -0,0 +1,84 @@
+import {useEffect, useRef, useMemo, ForwardedRef} from 'react'
+import {Platform, findNodeHandle} from 'react-native'
+import type {ScrollView} from 'react-native'
+import {mergeRefs} from 'lib/merge-refs'
+
+type Props<Scrollable extends ScrollView = ScrollView> = {
+  cursor?: string
+  outerRef?: ForwardedRef<Scrollable>
+}
+
+export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
+  outerRef,
+  cursor = 'grab',
+}: Props<Scrollable> = {}) {
+  const ref = useRef<Scrollable>(null)
+
+  useEffect(() => {
+    if (Platform.OS !== 'web' || !ref.current) {
+      return
+    }
+    const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement
+    if (!slider) {
+      return
+    }
+    let isDragging = false
+    let isMouseDown = false
+    let startX = 0
+    let scrollLeft = 0
+
+    const mouseDown = (e: MouseEvent) => {
+      isMouseDown = true
+      startX = e.pageX - slider.offsetLeft
+      scrollLeft = slider.scrollLeft
+
+      slider.style.cursor = cursor
+    }
+
+    const mouseUp = () => {
+      if (isDragging) {
+        slider.addEventListener('click', e => e.stopPropagation(), {once: true})
+      }
+
+      isMouseDown = false
+      isDragging = false
+      slider.style.cursor = 'default'
+    }
+
+    const mouseMove = (e: MouseEvent) => {
+      if (!isMouseDown) {
+        return
+      }
+
+      // Require n pixels momement before start of drag (3 in this case )
+      const x = e.pageX - slider.offsetLeft
+      if (Math.abs(x - startX) < 3) {
+        return
+      }
+
+      isDragging = true
+      e.preventDefault()
+      const walk = x - startX
+      slider.scrollLeft = scrollLeft - walk
+    }
+
+    slider.addEventListener('mousedown', mouseDown)
+    window.addEventListener('mouseup', mouseUp)
+    window.addEventListener('mousemove', mouseMove)
+
+    return () => {
+      slider.removeEventListener('mousedown', mouseDown)
+      window.removeEventListener('mouseup', mouseUp)
+      window.removeEventListener('mousemove', mouseMove)
+    }
+  }, [cursor])
+
+  const refs = useMemo(
+    () => mergeRefs(outerRef ? [ref, outerRef] : [ref]),
+    [ref, outerRef],
+  )
+
+  return {
+    refs,
+  }
+}
diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts
new file mode 100644
index 000000000..4617b5260
--- /dev/null
+++ b/src/lib/merge-refs.ts
@@ -0,0 +1,27 @@
+/**
+ * This TypeScript function merges multiple React refs into a single ref callback.
+ * When developing low level UI components, it is common to have to use a local ref
+ * but also support an external one using React.forwardRef.
+ * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility.
+ * Today a ref can be a function or an object, tomorrow it could be another thing, who knows.
+ * This utility handles compatibility for you.
+ * This function is inspired by https://github.com/gregberge/react-merge-refs
+ * @param refs - An array of React refs, which can be either `React.MutableRefObject<T>` or
+ * `React.LegacyRef<T>`. These refs are used to store references to DOM elements or React components.
+ * The `mergeRefs` function takes in an array of these refs and returns a callback function that
+ * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and
+ * returns a ref callback function that can be used to merge multiple refs into a single ref.
+ */
+export function mergeRefs<T = any>(
+  refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>,
+): React.RefCallback<T> {
+  return value => {
+    refs.forEach(ref => {
+      if (typeof ref === 'function') {
+        ref(value)
+      } else if (ref != null) {
+        ;(ref as React.MutableRefObject<T | null>).current = value
+      }
+    })
+  }
+}
diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx
new file mode 100644
index 000000000..4b7396eaa
--- /dev/null
+++ b/src/view/com/pager/DraggableScrollView.tsx
@@ -0,0 +1,15 @@
+import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView'
+import React, {ComponentProps} from 'react'
+import {ScrollView} from 'react-native'
+
+export const DraggableScrollView = React.forwardRef<
+  ScrollView,
+  ComponentProps<typeof ScrollView>
+>(function DraggableScrollView(props, ref) {
+  const {refs} = useDraggableScroll<ScrollView>({
+    outerRef: ref,
+    cursor: 'grab', // optional, default
+  })
+
+  return <ScrollView ref={refs} horizontal {...props} />
+})
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index fc04c3b2c..b51db1741 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -53,8 +53,8 @@ const FeedsTabBarDesktop = observer(
       // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
       <Animated.View style={[pal.view, styles.tabBar, transform]}>
         <TabBar
-          {...props}
           key={items.join(',')}
+          {...props}
           items={items}
           indicatorColor={pal.colors.link}
         />
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 485219730..cebf58b48 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -11,6 +11,7 @@ import {Text} from '../util/text/Text'
 import {PressableWithHover} from '../util/PressableWithHover'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isDesktopWeb} from 'platform/detection'
+import {DraggableScrollView} from './DraggableScrollView'
 
 export interface TabBarProps {
   testID?: string
@@ -75,7 +76,7 @@ export function TabBar({
 
   return (
     <View testID={testID} style={[pal.view, styles.outer]}>
-      <ScrollView
+      <DraggableScrollView
         horizontal={true}
         showsHorizontalScrollIndicator={false}
         ref={scrollElRef}
@@ -98,7 +99,7 @@ export function TabBar({
             </PressableWithHover>
           )
         })}
-      </ScrollView>
+      </DraggableScrollView>
     </View>
   )
 }