Lists updates: curate lists and blocklists (#1689)
* Add lists screen * Update Lists screen and List create/edit modal to support curate lists * Rework the ProfileList screen and add curatelist support * More ProfileList progress * Update list modals * Rename mutelists to modlists * Layout updates/fixes * More layout fixes * Modal fixes * List list screen updates * Update feed page to give more info * Layout fixes to ListAddUser modal * Layout fixes to FlatList and Feed on desktop * Layout fix to LoadLatestBtn on Web * Handle did resolution before showing the ProfileList screen * Rename the CustomFeed routes to ProfileFeed for consistency * Fix layout issues with the pager and feeds * Factor out some common code * Fix UIs for mobile * Fix user list rendering * Fix: dont bubble custom feed errors in the merge feed * Refactor feed models to reduce usage of the SavedFeeds model * Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists * Add the ability to pin lists * Add pinned lists to mobile * Remove dead code * Rework the ProfileScreenHeader to create more real-estate for action buttons * Improve layout behavior on web mobile breakpoints * Refactor feed & list pages to use new Tabs layout component * Refactor to ProfileSubpageHeader * Implement modlist block and mute * Switch to new api and just modify state on modlist actions * Fix some UI overflows * Fix: dont show edit buttons on lists you dont own * Fix alignment issue on long titles * Improve loading and error states for feeds & lists * Update list dropdown icons for ios * Fetch feed display names in the mergefeed * Improve rendering off offline feeds in the feed-listing page * Update Feeds listing UI to react to changes in saved/pinned state * Refresh list and feed on posts tab press * Fix pinned feed ordering UI * Fixes to list pinning * Remove view=simple qp * Add list to feed tuners * Render richtext * Add list href * Add 'view avatar' * Remove unused import * Fix missing import * Correctly reflect block by list state * Replace the <Tabs> component with the more effective <PagerWithHeader> component * Improve the responsiveness of the PagerWithHeader * Fix visual jank in the feed loading state * Improve performance of the PagerWithHeader * Fix a case that would cause the header to animate too aggressively * Add the ability to scroll to top by tapping the selected tab * Fix unit test runner * Update modlists test * Add curatelist tests * Fix: remove link behavior in ListAddUser modal * Fix some layout jank in the PagerWithHeader on iOS * Simplify ListItems header rendering * Wait for the appview to recognize the list before proceeding with list creation * Fix glitch in the onPageSelecting index of the Pager * Fix until() * Copy fix Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
parent
f9944b55e2
commit
f57a8cf8ba
87 changed files with 4090 additions and 1988 deletions
|
@ -1,10 +1,11 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import React from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {TabBar} from 'view/com/pager/TabBar'
|
||||
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {useStores} from 'state/index'
|
||||
import {useHomeTabs} from 'lib/hooks/useHomeTabs'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
|
||||
|
@ -27,10 +28,7 @@ const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl(
|
|||
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||
) {
|
||||
const store = useStores()
|
||||
const items = useMemo(
|
||||
() => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
|
||||
[store.me.savedFeeds.pinnedFeedNames],
|
||||
)
|
||||
const items = useHomeTabs(store.preferences.pinnedFeeds)
|
||||
const pal = usePalette('default')
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {TabBar} from 'view/com/pager/TabBar'
|
||||
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {useStores} from 'state/index'
|
||||
import {useHomeTabs} from 'lib/hooks/useHomeTabs'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {Link} from '../util/Link'
|
||||
|
@ -18,9 +19,9 @@ import Animated from 'react-native-reanimated'
|
|||
export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
||||
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||
) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const store = useStores()
|
||||
const items = useHomeTabs(store.preferences.pinnedFeeds)
|
||||
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
|
||||
|
@ -28,15 +29,6 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
|||
store.shell.openDrawer()
|
||||
}, [store])
|
||||
|
||||
const items = useMemo(
|
||||
() => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
|
||||
[store.me.savedFeeds.pinnedFeedNames],
|
||||
)
|
||||
|
||||
const tabBarKey = useMemo(() => {
|
||||
return items.join(',')
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
|
@ -81,7 +73,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
|||
</View>
|
||||
</View>
|
||||
<TabBar
|
||||
key={tabBarKey}
|
||||
key={items.join(',')}
|
||||
onPressSelected={props.onPressSelected}
|
||||
selectedPage={props.selectedPage}
|
||||
onSelect={props.onSelect}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React, {forwardRef} from 'react'
|
||||
import {Animated, View} from 'react-native'
|
||||
import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
|
||||
import PagerView, {
|
||||
PagerViewOnPageSelectedEvent,
|
||||
PagerViewOnPageScrollEvent,
|
||||
PageScrollStateChangedNativeEvent,
|
||||
} from 'react-native-pager-view'
|
||||
import {s} from 'lib/styles'
|
||||
|
||||
export type PageSelectedEvent = PagerViewOnPageSelectedEvent
|
||||
|
@ -21,6 +25,7 @@ interface Props {
|
|||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
onPageSelected?: (index: number) => void
|
||||
onPageSelecting?: (index: number) => void
|
||||
testID?: string
|
||||
}
|
||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||
|
@ -31,11 +36,15 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
onPageSelecting,
|
||||
testID,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||
const lastOffset = React.useRef(0)
|
||||
const lastDirection = React.useRef(0)
|
||||
const scrollState = React.useRef('')
|
||||
const pagerView = React.useRef<PagerView>(null)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
|
@ -50,15 +59,61 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||
[setSelectedPage, onPageSelected],
|
||||
)
|
||||
|
||||
const onPageScroll = React.useCallback(
|
||||
(e: PagerViewOnPageScrollEvent) => {
|
||||
const {position, offset} = e.nativeEvent
|
||||
if (offset === 0) {
|
||||
// offset hits 0 in some awkward spots so we ignore it
|
||||
return
|
||||
}
|
||||
// NOTE
|
||||
// we want to call `onPageSelecting` as soon as the scroll-gesture
|
||||
// enters the "settling" phase, which means the user has released it
|
||||
// we can't infer directionality from the scroll information, so we
|
||||
// track the offset changes. if the offset delta is consistent with
|
||||
// the existing direction during the settling phase, we can say for
|
||||
// certain where it's going and can fire
|
||||
// -prf
|
||||
if (scrollState.current === 'settling') {
|
||||
if (lastDirection.current === -1 && offset < lastOffset.current) {
|
||||
onPageSelecting?.(position)
|
||||
lastDirection.current = 0
|
||||
} else if (
|
||||
lastDirection.current === 1 &&
|
||||
offset > lastOffset.current
|
||||
) {
|
||||
onPageSelecting?.(position + 1)
|
||||
lastDirection.current = 0
|
||||
}
|
||||
} else {
|
||||
if (offset < lastOffset.current) {
|
||||
lastDirection.current = -1
|
||||
} else if (offset > lastOffset.current) {
|
||||
lastDirection.current = 1
|
||||
}
|
||||
}
|
||||
lastOffset.current = offset
|
||||
},
|
||||
[lastOffset, lastDirection, onPageSelecting],
|
||||
)
|
||||
|
||||
const onPageScrollStateChanged = React.useCallback(
|
||||
(e: PageScrollStateChangedNativeEvent) => {
|
||||
scrollState.current = e.nativeEvent.pageScrollState
|
||||
},
|
||||
[scrollState],
|
||||
)
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
onPageSelecting?.(index)
|
||||
},
|
||||
[pagerView],
|
||||
[pagerView, onPageSelecting],
|
||||
)
|
||||
|
||||
return (
|
||||
<View testID={testID}>
|
||||
<View testID={testID} style={s.flex1}>
|
||||
{tabBarPosition === 'top' &&
|
||||
renderTabBar({
|
||||
selectedPage,
|
||||
|
@ -66,9 +121,11 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||
})}
|
||||
<AnimatedPagerView
|
||||
ref={pagerView}
|
||||
style={s.h100pct}
|
||||
style={s.flex1}
|
||||
initialPage={initialPage}
|
||||
onPageSelected={onPageSelectedInner}>
|
||||
onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
onPageScroll={onPageScroll}>
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
{tabBarPosition === 'bottom' &&
|
||||
|
|
|
@ -13,6 +13,7 @@ interface Props {
|
|||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
onPageSelected?: (index: number) => void
|
||||
onPageSelecting?: (index: number) => void
|
||||
}
|
||||
export const Pager = React.forwardRef(function PagerImpl(
|
||||
{
|
||||
|
@ -21,6 +22,7 @@ export const Pager = React.forwardRef(function PagerImpl(
|
|||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
onPageSelecting,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
|
@ -34,21 +36,20 @@ export const Pager = React.forwardRef(function PagerImpl(
|
|||
(index: number) => {
|
||||
setSelectedPage(index)
|
||||
onPageSelected?.(index)
|
||||
onPageSelecting?.(index)
|
||||
},
|
||||
[setSelectedPage, onPageSelected],
|
||||
[setSelectedPage, onPageSelected, onPageSelecting],
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={s.hContentRegion}>
|
||||
{tabBarPosition === 'top' &&
|
||||
renderTabBar({
|
||||
selectedPage,
|
||||
onSelect: onTabBarSelect,
|
||||
})}
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<View
|
||||
style={selectedPage === i ? undefined : s.hidden}
|
||||
key={`page-${i}`}>
|
||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
||||
{child}
|
||||
</View>
|
||||
))}
|
||||
|
|
212
src/view/com/pager/PagerWithHeader.tsx
Normal file
212
src/view/com/pager/PagerWithHeader.tsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
import * as React from 'react'
|
||||
import {LayoutChangeEvent, StyleSheet} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedReaction,
|
||||
useAnimatedScrollHandler,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
runOnJS,
|
||||
} from 'react-native-reanimated'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {TabBar} from './TabBar'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||
|
||||
const SCROLLED_DOWN_LIMIT = 200
|
||||
|
||||
interface PagerWithHeaderChildParams {
|
||||
headerHeight: number
|
||||
onScroll: OnScrollCb
|
||||
isScrolledDown: boolean
|
||||
}
|
||||
|
||||
export interface PagerWithHeaderProps {
|
||||
testID?: string
|
||||
children:
|
||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||
| ((props: PagerWithHeaderChildParams) => JSX.Element)
|
||||
items: string[]
|
||||
renderHeader?: () => JSX.Element
|
||||
initialPage?: number
|
||||
onPageSelected?: (index: number) => void
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
}
|
||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
function PageWithHeaderImpl(
|
||||
{
|
||||
children,
|
||||
testID,
|
||||
items,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
onCurrentPageSelected,
|
||||
}: PagerWithHeaderProps,
|
||||
ref,
|
||||
) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const scrollYs = React.useRef<Record<number, number>>({})
|
||||
const scrollY = useSharedValue(scrollYs.current[currentPage] || 0)
|
||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
||||
const [headerHeight, setHeaderHeight] = React.useState(0)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(
|
||||
scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT,
|
||||
)
|
||||
|
||||
// react to scroll updates
|
||||
function onScrollUpdate(v: number) {
|
||||
// track each page's current scroll position
|
||||
scrollYs.current[currentPage] = Math.min(v, headerHeight - tabBarHeight)
|
||||
// update the 'is scrolled down' value
|
||||
setIsScrolledDown(v > SCROLLED_DOWN_LIMIT)
|
||||
}
|
||||
useAnimatedReaction(
|
||||
() => scrollY.value,
|
||||
v => runOnJS(onScrollUpdate)(v),
|
||||
)
|
||||
|
||||
// capture the header bar sizing
|
||||
const onTabBarLayout = React.useCallback(
|
||||
(evt: LayoutChangeEvent) => {
|
||||
setTabBarHeight(evt.nativeEvent.layout.height)
|
||||
},
|
||||
[setTabBarHeight],
|
||||
)
|
||||
const onHeaderLayout = React.useCallback(
|
||||
(evt: LayoutChangeEvent) => {
|
||||
setHeaderHeight(evt.nativeEvent.layout.height)
|
||||
},
|
||||
[setHeaderHeight],
|
||||
)
|
||||
|
||||
// render the the header and tab bar
|
||||
const headerTransform = useAnimatedStyle(
|
||||
() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(
|
||||
Math.min(scrollY.value, headerHeight - tabBarHeight) * -1,
|
||||
0,
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
[scrollY, headerHeight, tabBarHeight],
|
||||
)
|
||||
const renderTabBar = React.useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<Animated.View
|
||||
onLayout={onHeaderLayout}
|
||||
style={[
|
||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
||||
headerTransform,
|
||||
]}>
|
||||
{renderHeader?.()}
|
||||
<TabBar
|
||||
items={items}
|
||||
selectedPage={currentPage}
|
||||
onSelect={props.onSelect}
|
||||
onPressSelected={onCurrentPageSelected}
|
||||
onLayout={onTabBarLayout}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
[
|
||||
items,
|
||||
renderHeader,
|
||||
headerTransform,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
isMobile,
|
||||
onTabBarLayout,
|
||||
onHeaderLayout,
|
||||
],
|
||||
)
|
||||
|
||||
// props to pass into children render functions
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onScroll(e) {
|
||||
scrollY.value = e.contentOffset.y
|
||||
},
|
||||
})
|
||||
const childProps = React.useMemo<PagerWithHeaderChildParams>(() => {
|
||||
return {
|
||||
headerHeight,
|
||||
onScroll,
|
||||
isScrolledDown,
|
||||
}
|
||||
}, [headerHeight, onScroll, isScrolledDown])
|
||||
|
||||
const onPageSelectedInner = React.useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
|
||||
const onPageSelecting = React.useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
if (scrollY.value > headerHeight) {
|
||||
scrollY.value = headerHeight
|
||||
}
|
||||
scrollY.value = withTiming(scrollYs.current[index] || 0, {
|
||||
duration: 170,
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
})
|
||||
},
|
||||
[scrollY, setCurrentPage, scrollYs, headerHeight],
|
||||
)
|
||||
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
onPageSelecting={onPageSelecting}
|
||||
renderTabBar={renderTabBar}
|
||||
tabBarPosition="top">
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map(child => {
|
||||
if (child) {
|
||||
return child(childProps)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBarMobile: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
},
|
||||
tabBarDesktop: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
// @ts-ignore Web only -prf
|
||||
left: 'calc(50% - 299px)',
|
||||
width: 598,
|
||||
},
|
||||
})
|
||||
|
||||
function toArray<T>(v: T | T[]): T[] {
|
||||
if (Array.isArray(v)) {
|
||||
return v
|
||||
}
|
||||
return [v]
|
||||
}
|
|
@ -13,7 +13,8 @@ export interface TabBarProps {
|
|||
items: string[]
|
||||
indicatorColor?: string
|
||||
onSelect?: (index: number) => void
|
||||
onPressSelected?: () => void
|
||||
onPressSelected?: (index: number) => void
|
||||
onLayout?: (evt: LayoutChangeEvent) => void
|
||||
}
|
||||
|
||||
export function TabBar({
|
||||
|
@ -23,6 +24,7 @@ export function TabBar({
|
|||
indicatorColor,
|
||||
onSelect,
|
||||
onPressSelected,
|
||||
onLayout,
|
||||
}: TabBarProps) {
|
||||
const pal = usePalette('default')
|
||||
const scrollElRef = useRef<ScrollView>(null)
|
||||
|
@ -44,7 +46,7 @@ export function TabBar({
|
|||
(index: number) => {
|
||||
onSelect?.(index)
|
||||
if (index === selectedPage) {
|
||||
onPressSelected?.()
|
||||
onPressSelected?.(index)
|
||||
}
|
||||
},
|
||||
[onSelect, selectedPage, onPressSelected],
|
||||
|
@ -66,7 +68,7 @@ export function TabBar({
|
|||
const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
|
||||
|
||||
return (
|
||||
<View testID={testID} style={[pal.view, styles.outer]}>
|
||||
<View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}>
|
||||
<DraggableScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
|
@ -118,10 +120,7 @@ const desktopStyles = StyleSheet.create({
|
|||
|
||||
const mobileStyles = StyleSheet.create({
|
||||
outer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'transparent',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
contentContainer: {
|
||||
columnGap: isWeb ? 0 : 20,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue