get monologue tab working against all odds

This commit is contained in:
Samuel Newman
2024-10-22 23:03:53 +03:00
parent 090ac041c3
commit 0569e02442
2 changed files with 283 additions and 189 deletions
+190 -158
View File
@@ -15,6 +15,7 @@ import Animated, {
useAnimatedRef, useAnimatedRef,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -44,179 +45,210 @@ export interface PagerWithHeaderProps {
onCurrentPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void
allowHeaderOverScroll?: boolean allowHeaderOverScroll?: boolean
} }
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( export const PagerWithHeader = React.forwardRef<
function PageWithHeaderImpl( PagerRef,
{ PagerWithHeaderProps & {
children, headerRef: React.Ref<{
testID, scrollHeaderAway: () => void
scrollHeaderBack: () => void
}>
}
>(function PageWithHeaderImpl(
{
children,
testID,
items,
isHeaderReady,
renderHeader,
initialPage,
onPageSelected,
onCurrentPageSelected,
allowHeaderOverScroll,
headerRef,
},
ref,
) {
const [currentPage, setCurrentPage] = React.useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight
const maybePrevScrollY = useSharedValue<number | null>(null)
function scrollHeaderAway() {
'worklet'
maybePrevScrollY.value = scrollY.value
scrollY.value = withTiming(headerHeight)
}
function scrollHeaderBack() {
'worklet'
if (maybePrevScrollY.value !== null) {
scrollY.value = withTiming(maybePrevScrollY.value)
maybePrevScrollY.value = null
}
}
React.useImperativeHandle(headerRef, () => ({
scrollHeaderAway: () => {
runOnUI(scrollHeaderAway)()
},
scrollHeaderBack: () => {
runOnUI(scrollHeaderBack)()
},
}))
// capture the header bar sizing
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
const height = evt.nativeEvent.layout.height
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setTabBarHeight(Math.round(height * 2) / 2)
}
})
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setHeaderOnlyHeight(Math.round(height * 2) / 2)
}
})
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
return (
<PagerHeaderProvider scrollY={scrollY}>
<PagerTabBar
headerOnlyHeight={headerOnlyHeight}
items={items}
isHeaderReady={isHeaderReady}
renderHeader={renderHeader}
currentPage={currentPage}
onCurrentPageSelected={onCurrentPageSelected}
onTabBarLayout={onTabBarLayout}
onHeaderOnlyLayout={onHeaderOnlyLayout}
onSelect={props.onSelect}
scrollY={scrollY}
testID={testID}
allowHeaderOverScroll={allowHeaderOverScroll}
/>
</PagerHeaderProvider>
)
},
[
headerOnlyHeight,
items, items,
isHeaderReady, isHeaderReady,
renderHeader, renderHeader,
initialPage, currentPage,
onPageSelected,
onCurrentPageSelected, onCurrentPageSelected,
onTabBarLayout,
onHeaderOnlyLayout,
scrollY,
testID,
allowHeaderOverScroll, allowHeaderOverScroll,
}: PagerWithHeaderProps, ],
ref, )
) {
const [currentPage, setCurrentPage] = React.useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight
// capture the header bar sizing const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { const registerRef = React.useCallback(
const height = evt.nativeEvent.layout.height (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
if (height > 0) { scrollRefs.modify(refs => {
// The rounding is necessary to prevent jumps on iOS 'worklet'
setTabBarHeight(Math.round(height * 2) / 2) refs[atIndex] = scrollRef
} return refs
}) })
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { },
if (height > 0) { [scrollRefs],
// The rounding is necessary to prevent jumps on iOS )
setHeaderOnlyHeight(Math.round(height * 2) / 2)
}
})
const renderTabBar = React.useCallback( const lastForcedScrollY = useSharedValue(0)
(props: RenderTabBarFnProps) => { const adjustScrollForOtherPages = () => {
return ( 'worklet'
<PagerHeaderProvider scrollY={scrollY}> const currentScrollY = scrollY.value
<PagerTabBar const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
headerOnlyHeight={headerOnlyHeight} if (lastForcedScrollY.value !== forcedScrollY) {
items={items} lastForcedScrollY.value = forcedScrollY
isHeaderReady={isHeaderReady} const refs = scrollRefs.value
renderHeader={renderHeader} for (let i = 0; i < refs.length; i++) {
currentPage={currentPage} const scollRef = refs[i]
onCurrentPageSelected={onCurrentPageSelected} if (i !== currentPage && scollRef != null) {
onTabBarLayout={onTabBarLayout} scrollTo(scollRef, 0, forcedScrollY, false)
onHeaderOnlyLayout={onHeaderOnlyLayout}
onSelect={props.onSelect}
scrollY={scrollY}
testID={testID}
allowHeaderOverScroll={allowHeaderOverScroll}
/>
</PagerHeaderProvider>
)
},
[
headerOnlyHeight,
items,
isHeaderReady,
renderHeader,
currentPage,
onCurrentPageSelected,
onTabBarLayout,
onHeaderOnlyLayout,
scrollY,
testID,
allowHeaderOverScroll,
],
)
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
const registerRef = React.useCallback(
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
scrollRefs.modify(refs => {
'worklet'
refs[atIndex] = scrollRef
return refs
})
},
[scrollRefs],
)
const lastForcedScrollY = useSharedValue(0)
const adjustScrollForOtherPages = () => {
'worklet'
const currentScrollY = scrollY.value
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
if (lastForcedScrollY.value !== forcedScrollY) {
lastForcedScrollY.value = forcedScrollY
const refs = scrollRefs.value
for (let i = 0; i < refs.length; i++) {
const scollRef = refs[i]
if (i !== currentPage && scollRef != null) {
scrollTo(scollRef, 0, forcedScrollY, false)
}
} }
} }
} }
}
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>( const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
null, null,
) )
const queueThrottledOnScroll = useNonReactiveCallback(() => { const queueThrottledOnScroll = useNonReactiveCallback(() => {
if (!throttleTimeout.current) { if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => { throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)() runOnUI(adjustScrollForOtherPages)()
}, 80 /* Sync often enough you're unlikely to catch it unsynced */) }, 80 /* Sync often enough you're unlikely to catch it unsynced */)
}
})
const onScrollWorklet = React.useCallback(
(e: NativeScrollEvent) => {
'worklet'
const nextScrollY = e.contentOffset.y
// HACK: onScroll is reporting some strange values on load (negative header height).
// Highly improbable that you'd be overscrolled by over 400px -
// in fact, I actually can't do it, so let's just ignore those. -sfn
const isPossiblyInvalid =
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
if (!isPossiblyInvalid) {
scrollY.value = nextScrollY
runOnJS(queueThrottledOnScroll)()
} }
}) },
[scrollY, queueThrottledOnScroll, headerHeight],
)
const onScrollWorklet = React.useCallback( const onPageSelectedInner = React.useCallback(
(e: NativeScrollEvent) => { (index: number) => {
'worklet'
const nextScrollY = e.contentOffset.y
// HACK: onScroll is reporting some strange values on load (negative header height).
// Highly improbable that you'd be overscrolled by over 400px -
// in fact, I actually can't do it, so let's just ignore those. -sfn
const isPossiblyInvalid =
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
if (!isPossiblyInvalid) {
scrollY.value = nextScrollY
runOnJS(queueThrottledOnScroll)()
}
},
[scrollY, queueThrottledOnScroll, headerHeight],
)
const onPageSelectedInner = React.useCallback(
(index: number) => {
setCurrentPage(index)
onPageSelected?.(index)
},
[onPageSelected, setCurrentPage],
)
const onPageSelecting = React.useCallback((index: number) => {
setCurrentPage(index) setCurrentPage(index)
}, []) onPageSelected?.(index)
},
[onPageSelected, setCurrentPage],
)
return ( const onPageSelecting = React.useCallback((index: number) => {
<Pager setCurrentPage(index)
ref={ref} }, [])
testID={testID}
initialPage={initialPage} return (
onPageSelected={onPageSelectedInner} <Pager
onPageSelecting={onPageSelecting} ref={ref}
renderTabBar={renderTabBar}> testID={testID}
{toArray(children) initialPage={initialPage}
.filter(Boolean) onPageSelected={onPageSelectedInner}
.map((child, i) => { onPageSelecting={onPageSelecting}
const isReady = renderTabBar={renderTabBar}>
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 {toArray(children)
return ( .filter(Boolean)
<View key={i} collapsable={false}> .map((child, i) => {
<PagerItem const isReady =
headerHeight={headerHeight} isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
index={i} return (
isReady={isReady} <View key={i} collapsable={false}>
isFocused={i === currentPage} <PagerItem
onScrollWorklet={i === currentPage ? onScrollWorklet : noop} headerHeight={headerHeight}
registerRef={registerRef} index={i}
renderTab={child} isReady={isReady}
/> isFocused={i === currentPage}
</View> onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
) registerRef={registerRef}
})} renderTab={child}
</Pager> />
) </View>
}, )
) })}
</Pager>
)
})
let PagerTabBar = ({ let PagerTabBar = ({
currentPage, currentPage,
+93 -31
View File
@@ -25,6 +25,7 @@ import {isInvalidHandle} from '#/lib/strings/handles'
import {colors, s} from '#/lib/styles' import {colors, s} from '#/lib/styles'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
import {useLabelerInfoQuery} from '#/state/queries/labeler' import {useLabelerInfoQuery} from '#/state/queries/labeler'
@@ -41,6 +42,7 @@ import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {ListRef} from '#/view/com/util/List' import {ListRef} from '#/view/com/util/List'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
@@ -190,8 +192,9 @@ function ProfileScreenLoaded({
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null) const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
const postsSectionRef = React.useRef<SectionRef>(null) const postsSectionRef = React.useRef<SectionRef>(null)
const repliesSectionRef = React.useRef<SectionRef>(null) // const repliesSectionRef = React.useRef<SectionRef>(null)
const mediaSectionRef = React.useRef<SectionRef>(null) // const mediaSectionRef = React.useRef<SectionRef>(null)
const monologueSectionRef = React.useRef<SectionRef>(null)
const likesSectionRef = React.useRef<SectionRef>(null) const likesSectionRef = React.useRef<SectionRef>(null)
const feedsSectionRef = React.useRef<SectionRef>(null) const feedsSectionRef = React.useRef<SectionRef>(null)
const listsSectionRef = React.useRef<SectionRef>(null) const listsSectionRef = React.useRef<SectionRef>(null)
@@ -213,8 +216,9 @@ function ProfileScreenLoaded({
const hasLabeler = !!profile.associated?.labeler const hasLabeler = !!profile.associated?.labeler
const showFiltersTab = hasLabeler const showFiltersTab = hasLabeler
const showPostsTab = true const showPostsTab = true
const showRepliesTab = hasSession // const showRepliesTab = hasSession
const showMediaTab = !hasLabeler // const showMediaTab = !hasLabeler
const showMonologueTab = !!currentAccount
const showLikesTab = isMe const showLikesTab = isMe
const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0 const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0
const showStarterPacksTab = const showStarterPacksTab =
@@ -226,8 +230,9 @@ function ProfileScreenLoaded({
showFiltersTab ? _(msg`Labels`) : undefined, showFiltersTab ? _(msg`Labels`) : undefined,
showListsTab && hasLabeler ? _(msg`Lists`) : undefined, showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
showPostsTab ? _(msg`Posts`) : undefined, showPostsTab ? _(msg`Posts`) : undefined,
showRepliesTab ? _(msg`Replies`) : undefined, // showRepliesTab ? _(msg`Replies`) : undefined,
showMediaTab ? _(msg`Media`) : undefined, // showMediaTab ? _(msg`Media`) : undefined,
showMonologueTab ? _(msg`Monologue`) : undefined,
showLikesTab ? _(msg`Likes`) : undefined, showLikesTab ? _(msg`Likes`) : undefined,
showFeedsTab ? _(msg`Feeds`) : undefined, showFeedsTab ? _(msg`Feeds`) : undefined,
showStarterPacksTab ? _(msg`Starter Packs`) : undefined, showStarterPacksTab ? _(msg`Starter Packs`) : undefined,
@@ -237,8 +242,9 @@ function ProfileScreenLoaded({
let nextIndex = 0 let nextIndex = 0
let filtersIndex: number | null = null let filtersIndex: number | null = null
let postsIndex: number | null = null let postsIndex: number | null = null
let repliesIndex: number | null = null // let repliesIndex: number | null = null
let mediaIndex: number | null = null // let mediaIndex: number | null = null
let monologueIndex: number | null = null
let likesIndex: number | null = null let likesIndex: number | null = null
let feedsIndex: number | null = null let feedsIndex: number | null = null
let starterPacksIndex: number | null = null let starterPacksIndex: number | null = null
@@ -249,11 +255,14 @@ function ProfileScreenLoaded({
if (showPostsTab) { if (showPostsTab) {
postsIndex = nextIndex++ postsIndex = nextIndex++
} }
if (showRepliesTab) { // if (showRepliesTab) {
repliesIndex = nextIndex++ // repliesIndex = nextIndex++
} // }
if (showMediaTab) { // if (showMediaTab) {
mediaIndex = nextIndex++ // mediaIndex = nextIndex++
// }
if (showMonologueTab) {
monologueIndex = nextIndex++
} }
if (showLikesTab) { if (showLikesTab) {
likesIndex = nextIndex++ likesIndex = nextIndex++
@@ -274,10 +283,12 @@ function ProfileScreenLoaded({
labelsSectionRef.current?.scrollToTop() labelsSectionRef.current?.scrollToTop()
} else if (index === postsIndex) { } else if (index === postsIndex) {
postsSectionRef.current?.scrollToTop() postsSectionRef.current?.scrollToTop()
} else if (index === repliesIndex) { // } else if (index === repliesIndex) {
repliesSectionRef.current?.scrollToTop() // repliesSectionRef.current?.scrollToTop()
} else if (index === mediaIndex) { // } else if (index === mediaIndex) {
mediaSectionRef.current?.scrollToTop() // mediaSectionRef.current?.scrollToTop()
} else if (index === monologueIndex) {
monologueSectionRef.current?.scrollToTop()
} else if (index === likesIndex) { } else if (index === likesIndex) {
likesSectionRef.current?.scrollToTop() likesSectionRef.current?.scrollToTop()
} else if (index === feedsIndex) { } else if (index === feedsIndex) {
@@ -291,8 +302,9 @@ function ProfileScreenLoaded({
[ [
filtersIndex, filtersIndex,
postsIndex, postsIndex,
repliesIndex, // repliesIndex,
mediaIndex, // mediaIndex,
monologueIndex,
likesIndex, likesIndex,
feedsIndex, feedsIndex,
listsIndex, listsIndex,
@@ -330,7 +342,17 @@ function ProfileScreenLoaded({
openComposer({mention}) openComposer({mention})
} }
const headerRef = React.useRef<{
scrollHeaderAway: () => void
scrollHeaderBack: () => void
}>(null!)
const onPageSelected = (i: number) => { const onPageSelected = (i: number) => {
if (showMonologueTab && i === monologueIndex) {
headerRef.current?.scrollHeaderAway()
} else {
headerRef.current?.scrollHeaderBack()
}
setCurrentPage(i) setCurrentPage(i)
} }
@@ -363,6 +385,7 @@ function ProfileScreenLoaded({
screenDescription={_(msg`profile`)} screenDescription={_(msg`profile`)}
modui={moderation.ui('profileView')}> modui={moderation.ui('profileView')}>
<PagerWithHeader <PagerWithHeader
headerRef={headerRef}
testID="profilePager" testID="profilePager"
isHeaderReady={!showPlaceholder} isHeaderReady={!showPlaceholder}
items={sectionTitles} items={sectionTitles}
@@ -410,7 +433,20 @@ function ProfileScreenLoaded({
/> />
) )
: null} : null}
{showRepliesTab {showMonologueTab
? ({headerHeight, isFocused, scrollElRef}) => (
<ConvoProvider convoId="3ksogfbowfs27">
<ProfileMonologueSection
// ref={monologueSectionRef}
headerHeight={headerHeight}
isFocused={isFocused}
scrollElRef={scrollElRef as ListRef}
setScrollViewTag={setScrollViewTag}
/>
</ConvoProvider>
)
: null}
{/* {showRepliesTab
? ({headerHeight, isFocused, scrollElRef}) => ( ? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedSection <ProfileFeedSection
ref={repliesSectionRef} ref={repliesSectionRef}
@@ -435,7 +471,7 @@ function ProfileScreenLoaded({
setScrollViewTag={setScrollViewTag} setScrollViewTag={setScrollViewTag}
/> />
) )
: null} : null} */}
{showLikesTab {showLikesTab
? ({headerHeight, isFocused, scrollElRef}) => ( ? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedSection <ProfileFeedSection
@@ -487,20 +523,46 @@ function ProfileScreenLoaded({
) )
: null} : null}
</PagerWithHeader> </PagerWithHeader>
{hasSession && ( {hasSession &&
<FAB (showMonologueTab ? monologueIndex !== currentPage : true) && (
testID="composeFAB" <FAB
onPress={onPressCompose} testID="composeFAB"
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} onPress={onPressCompose}
accessibilityRole="button" icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityLabel={_(msg`New post`)} accessibilityRole="button"
accessibilityHint="" accessibilityLabel={_(msg`New post`)}
/> accessibilityHint=""
)} />
)}
</ScreenHider> </ScreenHider>
) )
} }
function ProfileMonologueSection({}: // isFocused,
// scrollElRef,
// headerHeight,
// setScrollViewTag,
{
headerHeight: number
isFocused: boolean
scrollElRef: ListRef
setScrollViewTag: (tag: number | null) => void
}) {
const [hasScrolled, setHasScrolled] = React.useState(false)
const convo = useConvo()
if (isConvoActive(convo)) {
return (
<MessagesList
hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled}
blocked={false}
footer={<></>}
/>
)
}
}
function useRichText(text: string): [RichTextAPI, boolean] { function useRichText(text: string): [RichTextAPI, boolean] {
const agent = useAgent() const agent = useAgent()
const [prevText, setPrevText] = React.useState(text) const [prevText, setPrevText] = React.useState(text)