Compare commits

...

1 Commits

Author SHA1 Message Date
Samuel Newman 0569e02442 get monologue tab working against all odds 2024-10-22 23:03:53 +03:00
2 changed files with 283 additions and 189 deletions
+190 -158
View File
@@ -15,6 +15,7 @@ import Animated, {
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -44,179 +45,210 @@ export interface PagerWithHeaderProps {
onCurrentPageSelected?: (index: number) => void
allowHeaderOverScroll?: boolean
}
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
function PageWithHeaderImpl(
{
children,
testID,
export const PagerWithHeader = React.forwardRef<
PagerRef,
PagerWithHeaderProps & {
headerRef: React.Ref<{
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,
isHeaderReady,
renderHeader,
initialPage,
onPageSelected,
currentPage,
onCurrentPageSelected,
onTabBarLayout,
onHeaderOnlyLayout,
scrollY,
testID,
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 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 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 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,
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 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>(
null,
)
const queueThrottledOnScroll = useNonReactiveCallback(() => {
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)()
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
null,
)
const queueThrottledOnScroll = useNonReactiveCallback(() => {
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)()
}, 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(
(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 onPageSelectedInner = React.useCallback(
(index: number) => {
setCurrentPage(index)
onPageSelected?.(index)
},
[onPageSelected, setCurrentPage],
)
const onPageSelecting = React.useCallback((index: number) => {
const onPageSelectedInner = React.useCallback(
(index: number) => {
setCurrentPage(index)
}, [])
onPageSelected?.(index)
},
[onPageSelected, setCurrentPage],
)
return (
<Pager
ref={ref}
testID={testID}
initialPage={initialPage}
onPageSelected={onPageSelectedInner}
onPageSelecting={onPageSelecting}
renderTabBar={renderTabBar}>
{toArray(children)
.filter(Boolean)
.map((child, i) => {
const isReady =
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
return (
<View key={i} collapsable={false}>
<PagerItem
headerHeight={headerHeight}
index={i}
isReady={isReady}
isFocused={i === currentPage}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
registerRef={registerRef}
renderTab={child}
/>
</View>
)
})}
</Pager>
)
},
)
const onPageSelecting = React.useCallback((index: number) => {
setCurrentPage(index)
}, [])
return (
<Pager
ref={ref}
testID={testID}
initialPage={initialPage}
onPageSelected={onPageSelectedInner}
onPageSelecting={onPageSelecting}
renderTabBar={renderTabBar}>
{toArray(children)
.filter(Boolean)
.map((child, i) => {
const isReady =
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
return (
<View key={i} collapsable={false}>
<PagerItem
headerHeight={headerHeight}
index={i}
isReady={isReady}
isFocused={i === currentPage}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
registerRef={registerRef}
renderTab={child}
/>
</View>
)
})}
</Pager>
)
})
let PagerTabBar = ({
currentPage,
+93 -31
View File
@@ -25,6 +25,7 @@ import {isInvalidHandle} from '#/lib/strings/handles'
import {colors, s} from '#/lib/styles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {listenSoftReset} from '#/state/events'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
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 {ListRef} from '#/view/com/util/List'
import {CenteredView} from '#/view/com/util/Views'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
@@ -190,8 +192,9 @@ function ProfileScreenLoaded({
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
const postsSectionRef = React.useRef<SectionRef>(null)
const repliesSectionRef = React.useRef<SectionRef>(null)
const mediaSectionRef = React.useRef<SectionRef>(null)
// const repliesSectionRef = React.useRef<SectionRef>(null)
// const mediaSectionRef = React.useRef<SectionRef>(null)
const monologueSectionRef = React.useRef<SectionRef>(null)
const likesSectionRef = React.useRef<SectionRef>(null)
const feedsSectionRef = React.useRef<SectionRef>(null)
const listsSectionRef = React.useRef<SectionRef>(null)
@@ -213,8 +216,9 @@ function ProfileScreenLoaded({
const hasLabeler = !!profile.associated?.labeler
const showFiltersTab = hasLabeler
const showPostsTab = true
const showRepliesTab = hasSession
const showMediaTab = !hasLabeler
// const showRepliesTab = hasSession
// const showMediaTab = !hasLabeler
const showMonologueTab = !!currentAccount
const showLikesTab = isMe
const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0
const showStarterPacksTab =
@@ -226,8 +230,9 @@ function ProfileScreenLoaded({
showFiltersTab ? _(msg`Labels`) : undefined,
showListsTab && hasLabeler ? _(msg`Lists`) : undefined,
showPostsTab ? _(msg`Posts`) : undefined,
showRepliesTab ? _(msg`Replies`) : undefined,
showMediaTab ? _(msg`Media`) : undefined,
// showRepliesTab ? _(msg`Replies`) : undefined,
// showMediaTab ? _(msg`Media`) : undefined,
showMonologueTab ? _(msg`Monologue`) : undefined,
showLikesTab ? _(msg`Likes`) : undefined,
showFeedsTab ? _(msg`Feeds`) : undefined,
showStarterPacksTab ? _(msg`Starter Packs`) : undefined,
@@ -237,8 +242,9 @@ function ProfileScreenLoaded({
let nextIndex = 0
let filtersIndex: number | null = null
let postsIndex: number | null = null
let repliesIndex: number | null = null
let mediaIndex: number | null = null
// let repliesIndex: number | null = null
// let mediaIndex: number | null = null
let monologueIndex: number | null = null
let likesIndex: number | null = null
let feedsIndex: number | null = null
let starterPacksIndex: number | null = null
@@ -249,11 +255,14 @@ function ProfileScreenLoaded({
if (showPostsTab) {
postsIndex = nextIndex++
}
if (showRepliesTab) {
repliesIndex = nextIndex++
}
if (showMediaTab) {
mediaIndex = nextIndex++
// if (showRepliesTab) {
// repliesIndex = nextIndex++
// }
// if (showMediaTab) {
// mediaIndex = nextIndex++
// }
if (showMonologueTab) {
monologueIndex = nextIndex++
}
if (showLikesTab) {
likesIndex = nextIndex++
@@ -274,10 +283,12 @@ function ProfileScreenLoaded({
labelsSectionRef.current?.scrollToTop()
} else if (index === postsIndex) {
postsSectionRef.current?.scrollToTop()
} else if (index === repliesIndex) {
repliesSectionRef.current?.scrollToTop()
} else if (index === mediaIndex) {
mediaSectionRef.current?.scrollToTop()
// } else if (index === repliesIndex) {
// repliesSectionRef.current?.scrollToTop()
// } else if (index === mediaIndex) {
// mediaSectionRef.current?.scrollToTop()
} else if (index === monologueIndex) {
monologueSectionRef.current?.scrollToTop()
} else if (index === likesIndex) {
likesSectionRef.current?.scrollToTop()
} else if (index === feedsIndex) {
@@ -291,8 +302,9 @@ function ProfileScreenLoaded({
[
filtersIndex,
postsIndex,
repliesIndex,
mediaIndex,
// repliesIndex,
// mediaIndex,
monologueIndex,
likesIndex,
feedsIndex,
listsIndex,
@@ -330,7 +342,17 @@ function ProfileScreenLoaded({
openComposer({mention})
}
const headerRef = React.useRef<{
scrollHeaderAway: () => void
scrollHeaderBack: () => void
}>(null!)
const onPageSelected = (i: number) => {
if (showMonologueTab && i === monologueIndex) {
headerRef.current?.scrollHeaderAway()
} else {
headerRef.current?.scrollHeaderBack()
}
setCurrentPage(i)
}
@@ -363,6 +385,7 @@ function ProfileScreenLoaded({
screenDescription={_(msg`profile`)}
modui={moderation.ui('profileView')}>
<PagerWithHeader
headerRef={headerRef}
testID="profilePager"
isHeaderReady={!showPlaceholder}
items={sectionTitles}
@@ -410,7 +433,20 @@ function ProfileScreenLoaded({
/>
)
: 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}) => (
<ProfileFeedSection
ref={repliesSectionRef}
@@ -435,7 +471,7 @@ function ProfileScreenLoaded({
setScrollViewTag={setScrollViewTag}
/>
)
: null}
: null} */}
{showLikesTab
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedSection
@@ -487,20 +523,46 @@ function ProfileScreenLoaded({
)
: null}
</PagerWithHeader>
{hasSession && (
<FAB
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
)}
{hasSession &&
(showMonologueTab ? monologueIndex !== currentPage : true) && (
<FAB
testID="composeFAB"
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
)}
</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] {
const agent = useAgent()
const [prevText, setPrevText] = React.useState(text)