Merge branch 'main' of github.com:bluesky-social/social-app into main
commit
d25b17ab19
|
@ -33,10 +33,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
|
||||||
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
height: calc(100% + env(safe-area-inset-top));
|
height: calc(100% + env(safe-area-inset-top));
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
/* Remove autofill styles on Webkit */
|
||||||
|
|
|
@ -206,6 +206,7 @@
|
||||||
"@types/lodash.shuffle": "^4.2.7",
|
"@types/lodash.shuffle": "^4.2.7",
|
||||||
"@types/psl": "^1.1.1",
|
"@types/psl": "^1.1.1",
|
||||||
"@types/react-avatar-editor": "^13.0.0",
|
"@types/react-avatar-editor": "^13.0.0",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-responsive": "^8.0.5",
|
"@types/react-responsive": "^8.0.5",
|
||||||
"@types/react-test-renderer": "^17.0.1",
|
"@types/react-test-renderer": "^17.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
setEmailConfirmationRequested,
|
setEmailConfirmationRequested,
|
||||||
} from './state/shell/reminders'
|
} from './state/shell/reminders'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
|
|
||||||
import {HomeScreen} from './view/screens/Home'
|
import {HomeScreen} from './view/screens/Home'
|
||||||
import {SearchScreen} from './view/screens/Search'
|
import {SearchScreen} from './view/screens/Search'
|
||||||
|
@ -413,10 +414,12 @@ function MyProfileTabNavigator() {
|
||||||
const FlatNavigator = () => {
|
const FlatNavigator = () => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const numUnread = useUnreadNotifications()
|
const numUnread = useUnreadNotifications()
|
||||||
|
const screenListeners = useWebScrollRestoration()
|
||||||
const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
|
const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flat.Navigator
|
<Flat.Navigator
|
||||||
|
screenListeners={screenListeners}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
// @ts-ignore
|
|
||||||
export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
|
export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
|
||||||
|
|
|
@ -91,7 +91,6 @@ export const COUNTRY_CODES: CountryCodeMap[] = [
|
||||||
{code2: 'DE', name: 'Germany (+49)'},
|
{code2: 'DE', name: 'Germany (+49)'},
|
||||||
{code2: 'GH', name: 'Ghana (+233)'},
|
{code2: 'GH', name: 'Ghana (+233)'},
|
||||||
{code2: 'GI', name: 'Gibraltar (+350)'},
|
{code2: 'GI', name: 'Gibraltar (+350)'},
|
||||||
{code2: 'GB', name: 'Great Britain (+44)'},
|
|
||||||
{code2: 'GR', name: 'Greece (+30)'},
|
{code2: 'GR', name: 'Greece (+30)'},
|
||||||
{code2: 'GL', name: 'Greenland (+299)'},
|
{code2: 'GL', name: 'Greenland (+299)'},
|
||||||
{code2: 'GD', name: 'Grenada (+1)'},
|
{code2: 'GD', name: 'Grenada (+1)'},
|
||||||
|
@ -237,14 +236,18 @@ export const COUNTRY_CODES: CountryCodeMap[] = [
|
||||||
{code2: 'UG', name: 'Uganda (+256)'},
|
{code2: 'UG', name: 'Uganda (+256)'},
|
||||||
{code2: 'UA', name: 'Ukraine (+380)'},
|
{code2: 'UA', name: 'Ukraine (+380)'},
|
||||||
{code2: 'AE', name: 'United Arab Emirates (+971)'},
|
{code2: 'AE', name: 'United Arab Emirates (+971)'},
|
||||||
|
{
|
||||||
|
code2: 'GB',
|
||||||
|
name: 'United Kingdom of Great Britain and Northern Ireland (+44)',
|
||||||
|
},
|
||||||
{code2: 'US', name: 'United States of America (+1)'},
|
{code2: 'US', name: 'United States of America (+1)'},
|
||||||
{code2: 'UY', name: 'Uruguay (+598)'},
|
{code2: 'UY', name: 'Uruguay (+598)'},
|
||||||
{code2: 'UZ', name: 'Uzbekistan (+998)'},
|
{code2: 'UZ', name: 'Uzbekistan (+998)'},
|
||||||
{code2: 'VU', name: 'Vanuatu (+678)'},
|
{code2: 'VU', name: 'Vanuatu (+678)'},
|
||||||
{code2: 'VE', name: 'Venezuela (Bolivarian Republic of) (+58)'},
|
{code2: 'VE', name: 'Venezuela (Bolivarian Republic of) (+58)'},
|
||||||
{code2: 'VN', name: 'Viet Nam (+84)'},
|
{code2: 'VN', name: 'Viet Nam (+84)'},
|
||||||
{code2: 'VI', name: 'Virgin Islands (+1)'},
|
|
||||||
{code2: 'VG', name: 'Virgin Islands (British) (+1)'},
|
{code2: 'VG', name: 'Virgin Islands (British) (+1)'},
|
||||||
|
{code2: 'VI', name: 'Virgin Islands (U.S.) (+1)'},
|
||||||
{code2: 'WF', name: 'Wallis and Futuna (+681)'},
|
{code2: 'WF', name: 'Wallis and Futuna (+681)'},
|
||||||
{code2: 'EH', name: 'Western Sahara (+212)'},
|
{code2: 'EH', name: 'Western Sahara (+212)'},
|
||||||
{code2: 'YE', name: 'Yemen (+967)'},
|
{code2: 'YE', name: 'Yemen (+967)'},
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import {useEffect} from 'react'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
|
let refCount = 0
|
||||||
|
|
||||||
|
function incrementRefCount() {
|
||||||
|
if (refCount === 0) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
refCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementRefCount() {
|
||||||
|
refCount--
|
||||||
|
if (refCount === 0) {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWebBodyScrollLock(isLockActive: boolean) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWeb || !isLockActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
incrementRefCount()
|
||||||
|
return () => decrementRefCount()
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function useWebScrollRestoration() {
|
||||||
|
return undefined
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {useMemo, useState, useEffect} from 'react'
|
||||||
|
import {EventArg, useNavigation} from '@react-navigation/core'
|
||||||
|
|
||||||
|
if ('scrollRestoration' in history) {
|
||||||
|
// Tell the brower not to mess with the scroll.
|
||||||
|
// We're doing that manually below.
|
||||||
|
history.scrollRestoration = 'manual'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialScrollState() {
|
||||||
|
return {
|
||||||
|
scrollYs: new Map(),
|
||||||
|
focusedKey: null as string | null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWebScrollRestoration() {
|
||||||
|
const [state] = useState(createInitialScrollState)
|
||||||
|
const navigation = useNavigation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onDispatch() {
|
||||||
|
if (state.focusedKey) {
|
||||||
|
// Remember where we were for later.
|
||||||
|
state.scrollYs.set(state.focusedKey, window.scrollY)
|
||||||
|
// TODO: Strictly speaking, this is a leak. We never clean up.
|
||||||
|
// This is because I'm not sure when it's appropriate to clean it up.
|
||||||
|
// It doesn't seem like popstate is enough because it can still Forward-Back again.
|
||||||
|
// Maybe we should use sessionStorage. Or check what Next.js is doing?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We want to intercept any push/pop/replace *before* the re-render.
|
||||||
|
// There is no official way to do this yet, but this works okay for now.
|
||||||
|
// https://twitter.com/satya164/status/1737301243519725803
|
||||||
|
navigation.addListener('__unsafe_action__' as any, onDispatch)
|
||||||
|
return () => {
|
||||||
|
navigation.removeListener('__unsafe_action__' as any, onDispatch)
|
||||||
|
}
|
||||||
|
}, [state, navigation])
|
||||||
|
|
||||||
|
const screenListeners = useMemo(
|
||||||
|
() => ({
|
||||||
|
focus(e: EventArg<'focus', boolean | undefined, unknown>) {
|
||||||
|
const scrollY = state.scrollYs.get(e.target) ?? 0
|
||||||
|
window.scrollTo(0, scrollY)
|
||||||
|
state.focusedKey = e.target ?? null
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[state],
|
||||||
|
)
|
||||||
|
return screenListeners
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
|
import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
|
||||||
import {Theme, TypographyVariant} from './ThemeContext'
|
import {Theme, TypographyVariant} from './ThemeContext'
|
||||||
import {isMobileWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
|
||||||
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
|
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
|
||||||
export const colors = {
|
export const colors = {
|
||||||
|
@ -175,7 +175,7 @@ export const s = StyleSheet.create({
|
||||||
// dimensions
|
// dimensions
|
||||||
w100pct: {width: '100%'},
|
w100pct: {width: '100%'},
|
||||||
h100pct: {height: '100%'},
|
h100pct: {height: '100%'},
|
||||||
hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'},
|
hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'},
|
||||||
window: {
|
window: {
|
||||||
width: Dimensions.get('window').width,
|
width: Dimensions.get('window').width,
|
||||||
height: Dimensions.get('window').height,
|
height: Dimensions.get('window').height,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -121,7 +121,8 @@ export function EmojiPicker({state, close}: IProps) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mask: {
|
mask: {
|
||||||
position: 'absolute',
|
// @ts-ignore web ony
|
||||||
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
|
|
@ -210,18 +210,9 @@ function useHeaderOffset() {
|
||||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||||
const {fontScale} = useWindowDimensions()
|
const {fontScale} = useWindowDimensions()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
if (isDesktop || isTablet) {
|
||||||
if (isDesktop) {
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if (isTablet) {
|
|
||||||
if (hasSession) {
|
|
||||||
return 50
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasSession) {
|
if (hasSession) {
|
||||||
const navBarPad = 16
|
const navBarPad = 16
|
||||||
const navBarText = 21 * fontScale
|
const navBarText = 21 * fontScale
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import React, {useCallback, useEffect, useState} from 'react'
|
import React, {useCallback, useEffect, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
|
ImageStyle,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
Pressable,
|
Pressable,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {colors, s} from 'lib/styles'
|
import {colors, s} from 'lib/styles'
|
||||||
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
@ -19,6 +23,7 @@ import {
|
||||||
ImagesLightbox,
|
ImagesLightbox,
|
||||||
ProfileImageLightbox,
|
ProfileImageLightbox,
|
||||||
} from '#/state/lightbox'
|
} from '#/state/lightbox'
|
||||||
|
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||||
|
|
||||||
interface Img {
|
interface Img {
|
||||||
uri: string
|
uri: string
|
||||||
|
@ -28,8 +33,10 @@ interface Img {
|
||||||
export function Lightbox() {
|
export function Lightbox() {
|
||||||
const {activeLightbox} = useLightbox()
|
const {activeLightbox} = useLightbox()
|
||||||
const {closeLightbox} = useLightboxControls()
|
const {closeLightbox} = useLightboxControls()
|
||||||
|
const isActive = !!activeLightbox
|
||||||
|
useWebBodyScrollLock(isActive)
|
||||||
|
|
||||||
if (!activeLightbox) {
|
if (!isActive) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +123,7 @@ function LightboxInner({
|
||||||
<Image
|
<Image
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
source={imgs[index]}
|
source={imgs[index]}
|
||||||
style={styles.image}
|
style={styles.image as ImageStyle}
|
||||||
accessibilityLabel={imgs[index].alt}
|
accessibilityLabel={imgs[index].alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
|
@ -129,7 +136,7 @@ function LightboxInner({
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-left"
|
icon="angle-left"
|
||||||
style={styles.icon}
|
style={styles.icon as FontAwesomeIconStyle}
|
||||||
size={40}
|
size={40}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -143,7 +150,7 @@ function LightboxInner({
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="angle-right"
|
icon="angle-right"
|
||||||
style={styles.icon}
|
style={styles.icon as FontAwesomeIconStyle}
|
||||||
size={40}
|
size={40}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -178,7 +185,8 @@ function LightboxInner({
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mask: {
|
mask: {
|
||||||
position: 'absolute',
|
// @ts-ignore
|
||||||
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
|
||||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||||
|
|
||||||
import {useModals, useModalControls} from '#/state/modals'
|
import {useModals, useModalControls} from '#/state/modals'
|
||||||
import type {Modal as ModalIface} from '#/state/modals'
|
import type {Modal as ModalIface} from '#/state/modals'
|
||||||
|
@ -38,6 +39,7 @@ import * as EmbedConsentModal from './EmbedConsent'
|
||||||
|
|
||||||
export function ModalsContainer() {
|
export function ModalsContainer() {
|
||||||
const {isModalActive, activeModals} = useModals()
|
const {isModalActive, activeModals} = useModals()
|
||||||
|
useWebBodyScrollLock(isModalActive)
|
||||||
|
|
||||||
if (!isModalActive) {
|
if (!isModalActive) {
|
||||||
return null
|
return null
|
||||||
|
@ -166,7 +168,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mask: {
|
mask: {
|
||||||
position: 'absolute',
|
// @ts-ignore
|
||||||
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
@ -167,11 +167,9 @@ let FeedItem = ({
|
||||||
icon = 'user-plus'
|
icon = 'user-plus'
|
||||||
iconStyle = [s.blue3 as FontAwesomeIconStyle]
|
iconStyle = [s.blue3 as FontAwesomeIconStyle]
|
||||||
} else if (item.type === 'feedgen-like') {
|
} else if (item.type === 'feedgen-like') {
|
||||||
action = _(
|
action = item.subjectUri
|
||||||
msg`liked your custom feed${
|
? _(msg`liked your custom feed '${new AtUri(item.subjectUri).rkey}'`)
|
||||||
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : ''
|
: _(msg`liked your custom feed`)
|
||||||
}`,
|
|
||||||
)
|
|
||||||
icon = 'HeartIconSolid'
|
icon = 'HeartIconSolid'
|
||||||
iconStyle = [
|
iconStyle = [
|
||||||
s.likeColor as FontAwesomeIconStyle,
|
s.likeColor as FontAwesomeIconStyle,
|
||||||
|
|
|
@ -117,7 +117,7 @@ function FeedsTabBarTablet(
|
||||||
return (
|
return (
|
||||||
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
|
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[pal.view, styles.tabBar, headerMinimalShellTransform]}
|
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||||
onLayout={e => {
|
onLayout={e => {
|
||||||
headerHeight.value = e.nativeEvent.layout.height
|
headerHeight.value = e.nativeEvent.layout.height
|
||||||
}}>
|
}}>
|
||||||
|
@ -134,13 +134,16 @@ function FeedsTabBarTablet(
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tabBar: {
|
tabBar: {
|
||||||
position: 'absolute',
|
// @ts-ignore Web only
|
||||||
|
position: 'sticky',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
// @ts-ignore Web only -prf
|
// @ts-ignore Web only -prf
|
||||||
left: 'calc(50% - 299px)',
|
left: 'calc(50% - 300px)',
|
||||||
width: 598,
|
width: 600,
|
||||||
top: 0,
|
top: 0,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -142,7 +142,8 @@ export function FeedsTabBar(
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tabBar: {
|
tabBar: {
|
||||||
position: 'absolute',
|
// @ts-ignore web-only
|
||||||
|
position: isWeb ? 'fixed' : 'absolute',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface PagerRef {
|
||||||
export interface RenderTabBarFnProps {
|
export interface RenderTabBarFnProps {
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
onSelect?: (index: number) => void
|
onSelect?: (index: number) => void
|
||||||
|
tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
|
||||||
}
|
}
|
||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {flushSync} from 'react-dom'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
|
|
||||||
export interface RenderTabBarFnProps {
|
export interface RenderTabBarFnProps {
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
onSelect?: (index: number) => void
|
onSelect?: (index: number) => void
|
||||||
|
tabBarAnchor?: JSX.Element
|
||||||
}
|
}
|
||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
|
@ -27,6 +29,8 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||||
|
const scrollYs = React.useRef<Array<number | null>>([])
|
||||||
|
const anchorRef = React.useRef(null)
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => setSelectedPage(index),
|
setPage: (index: number) => setSelectedPage(index),
|
||||||
|
@ -34,11 +38,36 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||||
|
// case we should preserve and restore scroll), or if it is somewhere below in the
|
||||||
|
// viewport (in which case a scroll jump would be jarring). We determine this by
|
||||||
|
// measuring where the "anchor" element is (which we place just above the tabbar).
|
||||||
|
let anchorTop = anchorRef.current
|
||||||
|
? (anchorRef.current as Element).getBoundingClientRect().top
|
||||||
|
: -scrollY // If there's no anchor, treat the top of the page as one.
|
||||||
|
const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable.
|
||||||
|
|
||||||
|
if (isSticking) {
|
||||||
|
scrollYs.current[selectedPage] = window.scrollY
|
||||||
|
} else {
|
||||||
|
scrollYs.current[selectedPage] = null
|
||||||
|
}
|
||||||
|
flushSync(() => {
|
||||||
setSelectedPage(index)
|
setSelectedPage(index)
|
||||||
onPageSelected?.(index)
|
onPageSelected?.(index)
|
||||||
onPageSelecting?.(index)
|
onPageSelecting?.(index)
|
||||||
|
})
|
||||||
|
if (isSticking) {
|
||||||
|
const restoredScrollY = scrollYs.current[index]
|
||||||
|
if (restoredScrollY != null) {
|
||||||
|
window.scrollTo(0, restoredScrollY)
|
||||||
|
} else {
|
||||||
|
window.scrollTo(0, scrollY + anchorTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setSelectedPage, onPageSelected, onPageSelecting],
|
[selectedPage, setSelectedPage, onPageSelected, onPageSelecting],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -46,21 +75,11 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||||
{tabBarPosition === 'top' &&
|
{tabBarPosition === 'top' &&
|
||||||
renderTabBar({
|
renderTabBar({
|
||||||
selectedPage,
|
selectedPage,
|
||||||
|
tabBarAnchor: <View ref={anchorRef} />,
|
||||||
onSelect: onTabBarSelect,
|
onSelect: onTabBarSelect,
|
||||||
})}
|
})}
|
||||||
{React.Children.map(children, (child, i) => (
|
{React.Children.map(children, (child, i) => (
|
||||||
<View
|
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
||||||
style={
|
|
||||||
selectedPage === i
|
|
||||||
? s.flex1
|
|
||||||
: {
|
|
||||||
position: 'absolute',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
// @ts-ignore web-only
|
|
||||||
visibility: 'hidden',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key={`page-${i}`}>
|
|
||||||
{child}
|
{child}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import Animated, {
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||||
import {TabBar} from './TabBar'
|
import {TabBar} from './TabBar'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {ListMethods} from '../util/List'
|
import {ListMethods} from '../util/List'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
|
@ -235,7 +234,6 @@ let PagerTabBar = ({
|
||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
onSelect?: (index: number) => void
|
onSelect?: (index: number) => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const headerTransform = useAnimatedStyle(() => ({
|
const headerTransform = useAnimatedStyle(() => ({
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
|
@ -246,10 +244,7 @@ let PagerTabBar = ({
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
pointerEvents="box-none"
|
pointerEvents="box-none"
|
||||||
style={[
|
style={[styles.tabBarMobile, headerTransform]}>
|
||||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
|
||||||
headerTransform,
|
|
||||||
]}>
|
|
||||||
<View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
|
<View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
|
||||||
{renderHeader?.()}
|
{renderHeader?.()}
|
||||||
</View>
|
</View>
|
||||||
|
@ -325,14 +320,6 @@ const styles = StyleSheet.create({
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
tabBarDesktop: {
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 1,
|
|
||||||
top: 0,
|
|
||||||
// @ts-ignore Web only -prf
|
|
||||||
left: 'calc(50% - 299px)',
|
|
||||||
width: 598,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function noop() {
|
function noop() {
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
|
||||||
|
import {useAnimatedRef} from 'react-native-reanimated'
|
||||||
|
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||||
|
import {TabBar} from './TabBar'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
import {ListMethods} from '../util/List'
|
||||||
|
|
||||||
|
export interface PagerWithHeaderChildParams {
|
||||||
|
headerHeight: number
|
||||||
|
isFocused: boolean
|
||||||
|
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagerWithHeaderProps {
|
||||||
|
testID?: string
|
||||||
|
children:
|
||||||
|
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||||
|
| ((props: PagerWithHeaderChildParams) => JSX.Element)
|
||||||
|
items: string[]
|
||||||
|
isHeaderReady: boolean
|
||||||
|
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 [currentPage, setCurrentPage] = React.useState(0)
|
||||||
|
|
||||||
|
const renderTabBar = React.useCallback(
|
||||||
|
(props: RenderTabBarFnProps) => {
|
||||||
|
return (
|
||||||
|
<PagerTabBar
|
||||||
|
items={items}
|
||||||
|
renderHeader={renderHeader}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onCurrentPageSelected={onCurrentPageSelected}
|
||||||
|
onSelect={props.onSelect}
|
||||||
|
tabBarAnchor={props.tabBarAnchor}
|
||||||
|
testID={testID}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[items, renderHeader, currentPage, onCurrentPageSelected, testID],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPageSelectedInner = React.useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
setCurrentPage(index)
|
||||||
|
onPageSelected?.(index)
|
||||||
|
},
|
||||||
|
[onPageSelected, setCurrentPage],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPageSelecting = React.useCallback((index: number) => {
|
||||||
|
setCurrentPage(index)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pager
|
||||||
|
ref={ref}
|
||||||
|
testID={testID}
|
||||||
|
initialPage={initialPage}
|
||||||
|
onPageSelected={onPageSelectedInner}
|
||||||
|
onPageSelecting={onPageSelecting}
|
||||||
|
renderTabBar={renderTabBar}
|
||||||
|
tabBarPosition="top">
|
||||||
|
{toArray(children)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((child, i) => {
|
||||||
|
return (
|
||||||
|
<View key={i} collapsable={false}>
|
||||||
|
<PagerItem isFocused={i === currentPage} renderTab={child} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Pager>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
let PagerTabBar = ({
|
||||||
|
currentPage,
|
||||||
|
items,
|
||||||
|
testID,
|
||||||
|
renderHeader,
|
||||||
|
onCurrentPageSelected,
|
||||||
|
onSelect,
|
||||||
|
tabBarAnchor,
|
||||||
|
}: {
|
||||||
|
currentPage: number
|
||||||
|
items: string[]
|
||||||
|
testID?: string
|
||||||
|
renderHeader?: () => JSX.Element
|
||||||
|
onCurrentPageSelected?: (index: number) => void
|
||||||
|
onSelect?: (index: number) => void
|
||||||
|
tabBarAnchor?: JSX.Element | null | undefined
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
|
||||||
|
{renderHeader?.()}
|
||||||
|
</View>
|
||||||
|
{tabBarAnchor}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.tabBarContainer,
|
||||||
|
isMobile
|
||||||
|
? styles.tabBarContainerMobile
|
||||||
|
: styles.tabBarContainerDesktop,
|
||||||
|
pal.border,
|
||||||
|
]}>
|
||||||
|
<TabBar
|
||||||
|
testID={testID}
|
||||||
|
items={items}
|
||||||
|
selectedPage={currentPage}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onPressSelected={onCurrentPageSelected}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PagerTabBar = React.memo(PagerTabBar)
|
||||||
|
|
||||||
|
function PagerItem({
|
||||||
|
isFocused,
|
||||||
|
renderTab,
|
||||||
|
}: {
|
||||||
|
isFocused: boolean
|
||||||
|
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
|
||||||
|
}) {
|
||||||
|
const scrollElRef = useAnimatedRef()
|
||||||
|
if (renderTab == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return renderTab({
|
||||||
|
headerHeight: 0,
|
||||||
|
isFocused,
|
||||||
|
scrollElRef: scrollElRef as React.MutableRefObject<
|
||||||
|
ListMethods | ScrollView | null
|
||||||
|
>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerContainerDesktop: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
width: 600,
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
tabBarContainer: {
|
||||||
|
// @ts-ignore web-only
|
||||||
|
position: 'sticky',
|
||||||
|
overflow: 'hidden',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
tabBarContainerDesktop: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
width: 600,
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
tabBarContainerMobile: {
|
||||||
|
paddingLeft: 14,
|
||||||
|
paddingRight: 14,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function toArray<T>(v: T | T[]): T[] {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return [v]
|
||||||
|
}
|
|
@ -139,7 +139,7 @@ function PostThreadLoaded({
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {isTablet, isDesktop} = useWebMediaQueries()
|
const {isTablet, isDesktop, isTabletOrMobile} = useWebMediaQueries()
|
||||||
const ref = useRef<ListMethods>(null)
|
const ref = useRef<ListMethods>(null)
|
||||||
const highlightedPostRef = useRef<View | null>(null)
|
const highlightedPostRef = useRef<View | null>(null)
|
||||||
const needsScrollAdjustment = useRef<boolean>(
|
const needsScrollAdjustment = useRef<boolean>(
|
||||||
|
@ -197,17 +197,35 @@ function PostThreadLoaded({
|
||||||
|
|
||||||
// wait for loading to finish
|
// wait for loading to finish
|
||||||
if (thread.type === 'post' && !!thread.parent) {
|
if (thread.type === 'post' && !!thread.parent) {
|
||||||
highlightedPostRef.current?.measure(
|
function onMeasure(pageY: number) {
|
||||||
(_x, _y, _width, _height, _pageX, pageY) => {
|
let spinnerHeight = 0
|
||||||
|
if (isDesktop) {
|
||||||
|
spinnerHeight = 40
|
||||||
|
} else if (isTabletOrMobile) {
|
||||||
|
spinnerHeight = 82
|
||||||
|
}
|
||||||
ref.current?.scrollToOffset({
|
ref.current?.scrollToOffset({
|
||||||
animated: false,
|
animated: false,
|
||||||
offset: pageY - (isDesktop ? 0 : 50),
|
offset: pageY - spinnerHeight,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
if (isNative) {
|
||||||
|
highlightedPostRef.current?.measure(
|
||||||
|
(_x, _y, _width, _height, _pageX, pageY) => {
|
||||||
|
onMeasure(pageY)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Measure synchronously to avoid a layout jump.
|
||||||
|
const domNode = highlightedPostRef.current
|
||||||
|
if (domNode) {
|
||||||
|
const pageY = (domNode as any as Element).getBoundingClientRect().top
|
||||||
|
onMeasure(pageY)
|
||||||
|
}
|
||||||
|
}
|
||||||
needsScrollAdjustment.current = false
|
needsScrollAdjustment.current = false
|
||||||
}
|
}
|
||||||
}, [thread, isDesktop])
|
}, [thread, isDesktop, isTabletOrMobile])
|
||||||
|
|
||||||
const onPTR = React.useCallback(async () => {
|
const onPTR = React.useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
|
|
|
@ -706,7 +706,7 @@ function ExpandedPostDetails({
|
||||||
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<>
|
<>
|
||||||
<Text style={[pal.textLight, s.ml5, s.mr5]}>•</Text>
|
<Text style={pal.textLight}> · </Text>
|
||||||
<Link href={translatorUrl} title={_(msg`Translate`)}>
|
<Link href={translatorUrl} title={_(msg`Translate`)}>
|
||||||
<Text style={pal.link}>
|
<Text style={pal.link}>
|
||||||
<Trans>Translate</Trans>
|
<Trans>Translate</Trans>
|
||||||
|
|
|
@ -205,7 +205,7 @@ let FeedItemInner = ({
|
||||||
title={_(
|
title={_(
|
||||||
msg`Reposted by ${sanitizeDisplayName(
|
msg`Reposted by ${sanitizeDisplayName(
|
||||||
reason.by.displayName || reason.by.handle,
|
reason.by.displayName || reason.by.handle,
|
||||||
)})`,
|
)}`,
|
||||||
)}>
|
)}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="retweet"
|
icon="retweet"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {memo, startTransition} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {FlatListProps, RefreshControl} from 'react-native'
|
import {FlatListProps, RefreshControl} from 'react-native'
|
||||||
import {FlatList_INTERNAL} from './Views'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
import {addStyle} from 'lib/styles'
|
import {addStyle} from 'lib/styles'
|
||||||
|
@ -39,9 +39,7 @@ function ListImpl<ItemT>(
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
function handleScrolledDownChange(didScrollDown: boolean) {
|
function handleScrolledDownChange(didScrollDown: boolean) {
|
||||||
startTransition(() => {
|
|
||||||
onScrolledDownChange?.(didScrollDown)
|
onScrolledDownChange?.(didScrollDown)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollHandler = useAnimatedScrollHandler({
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
|
|
|
@ -0,0 +1,341 @@
|
||||||
|
import React, {isValidElement, memo, useRef, startTransition} from 'react'
|
||||||
|
import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native'
|
||||||
|
import {addStyle} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
|
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||||
|
|
||||||
|
export type ListMethods = any // TODO: Better types.
|
||||||
|
export type ListProps<ItemT> = Omit<
|
||||||
|
FlatListProps<ItemT>,
|
||||||
|
| 'onScroll' // Use ScrollContext instead.
|
||||||
|
| 'refreshControl' // Pass refreshing and/or onRefresh instead.
|
||||||
|
| 'contentOffset' // Pass headerOffset instead.
|
||||||
|
> & {
|
||||||
|
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||||
|
headerOffset?: number
|
||||||
|
refreshing?: boolean
|
||||||
|
onRefresh?: () => void
|
||||||
|
desktopFixedHeight: any // TODO: Better types.
|
||||||
|
}
|
||||||
|
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
|
||||||
|
|
||||||
|
function ListImpl<ItemT>(
|
||||||
|
{
|
||||||
|
ListHeaderComponent,
|
||||||
|
ListFooterComponent,
|
||||||
|
contentContainerStyle,
|
||||||
|
data,
|
||||||
|
desktopFixedHeight,
|
||||||
|
headerOffset,
|
||||||
|
keyExtractor,
|
||||||
|
refreshing: _unsupportedRefreshing,
|
||||||
|
onEndReached,
|
||||||
|
onEndReachedThreshold = 0,
|
||||||
|
onRefresh: _unsupportedOnRefresh,
|
||||||
|
onScrolledDownChange,
|
||||||
|
onContentSizeChange,
|
||||||
|
renderItem,
|
||||||
|
extraData,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ListProps<ItemT>,
|
||||||
|
ref: React.Ref<ListMethods>,
|
||||||
|
) {
|
||||||
|
const contextScrollHandlers = useScrollHandlers()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
if (!isMobile) {
|
||||||
|
contentContainerStyle = addStyle(
|
||||||
|
contentContainerStyle,
|
||||||
|
styles.containerScroll,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let header: JSX.Element | null = null
|
||||||
|
if (ListHeaderComponent != null) {
|
||||||
|
if (isValidElement(ListHeaderComponent)) {
|
||||||
|
header = ListHeaderComponent
|
||||||
|
} else {
|
||||||
|
// @ts-ignore Nah it's fine.
|
||||||
|
header = <ListHeaderComponent />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let footer: JSX.Element | null = null
|
||||||
|
if (ListFooterComponent != null) {
|
||||||
|
if (isValidElement(ListFooterComponent)) {
|
||||||
|
footer = ListFooterComponent
|
||||||
|
} else {
|
||||||
|
// @ts-ignore Nah it's fine.
|
||||||
|
footer = <ListFooterComponent />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerOffset != null) {
|
||||||
|
style = addStyle(style, {
|
||||||
|
paddingTop: headerOffset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeRef = React.useRef(null)
|
||||||
|
React.useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
scrollToTop() {
|
||||||
|
window.scrollTo({top: 0})
|
||||||
|
},
|
||||||
|
scrollToOffset({
|
||||||
|
animated,
|
||||||
|
offset,
|
||||||
|
}: {
|
||||||
|
animated: boolean
|
||||||
|
offset: number
|
||||||
|
}) {
|
||||||
|
window.scrollTo({
|
||||||
|
left: 0,
|
||||||
|
top: offset,
|
||||||
|
behavior: animated ? 'smooth' : 'instant',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} as any), // TODO: Better types.
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- onContentSizeChange ---
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
useResizeObserver(containerRef, onContentSizeChange)
|
||||||
|
|
||||||
|
// --- onScroll ---
|
||||||
|
const [isInsideVisibleTree, setIsInsideVisibleTree] = React.useState(false)
|
||||||
|
const handleWindowScroll = useNonReactiveCallback(() => {
|
||||||
|
if (isInsideVisibleTree) {
|
||||||
|
contextScrollHandlers.onScroll?.(
|
||||||
|
{
|
||||||
|
contentOffset: {
|
||||||
|
x: Math.max(0, window.scrollX),
|
||||||
|
y: Math.max(0, window.scrollY),
|
||||||
|
},
|
||||||
|
} as any, // TODO: Better types.
|
||||||
|
null as any,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isInsideVisibleTree) {
|
||||||
|
// Prevents hidden tabs from firing scroll events.
|
||||||
|
// Only one list is expected to be firing these at a time.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleWindowScroll)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleWindowScroll)
|
||||||
|
}
|
||||||
|
}, [isInsideVisibleTree, handleWindowScroll])
|
||||||
|
|
||||||
|
// --- onScrolledDownChange ---
|
||||||
|
const isScrolledDown = useRef(false)
|
||||||
|
function handleAboveTheFoldVisibleChange(isAboveTheFold: boolean) {
|
||||||
|
const didScrollDown = !isAboveTheFold
|
||||||
|
if (isScrolledDown.current !== didScrollDown) {
|
||||||
|
isScrolledDown.current = didScrollDown
|
||||||
|
startTransition(() => {
|
||||||
|
onScrolledDownChange?.(didScrollDown)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- onEndReached ---
|
||||||
|
const onTailVisibilityChange = useNonReactiveCallback(
|
||||||
|
(isTailVisible: boolean) => {
|
||||||
|
if (isTailVisible) {
|
||||||
|
onEndReached?.({
|
||||||
|
distanceFromEnd: onEndReachedThreshold || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...props} style={style} ref={nativeRef}>
|
||||||
|
<Visibility
|
||||||
|
onVisibleChange={setIsInsideVisibleTree}
|
||||||
|
style={
|
||||||
|
// This has position: fixed, so it should always report as visible
|
||||||
|
// unless we're within a display: none tree (like a hidden tab).
|
||||||
|
styles.parentTreeVisibilityDetector
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
ref={containerRef}
|
||||||
|
style={[
|
||||||
|
styles.contentContainer,
|
||||||
|
contentContainerStyle,
|
||||||
|
desktopFixedHeight ? styles.minHeightViewport : null,
|
||||||
|
pal.border,
|
||||||
|
]}>
|
||||||
|
<Visibility
|
||||||
|
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||||
|
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||||
|
/>
|
||||||
|
{header}
|
||||||
|
{(data as Array<ItemT>).map((item, index) => (
|
||||||
|
<Row<ItemT>
|
||||||
|
key={keyExtractor!(item, index)}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
renderItem={renderItem}
|
||||||
|
extraData={extraData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{onEndReached && (
|
||||||
|
<Visibility
|
||||||
|
topMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||||
|
onVisibleChange={onTailVisibilityChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{footer}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useResizeObserver(
|
||||||
|
ref: React.RefObject<Element>,
|
||||||
|
onResize: undefined | ((w: number, h: number) => void),
|
||||||
|
) {
|
||||||
|
const handleResize = useNonReactiveCallback(onResize ?? (() => {}))
|
||||||
|
const isActive = !!onResize
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
batchedUpdates(() => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
const rect = entry.contentRect
|
||||||
|
handleResize(rect.width, rect.height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const node = ref.current!
|
||||||
|
resizeObserver.observe(node)
|
||||||
|
return () => {
|
||||||
|
resizeObserver.unobserve(node)
|
||||||
|
}
|
||||||
|
}, [handleResize, isActive, ref])
|
||||||
|
}
|
||||||
|
|
||||||
|
let Row = function RowImpl<ItemT>({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
renderItem,
|
||||||
|
extraData: _unused,
|
||||||
|
}: {
|
||||||
|
item: ItemT
|
||||||
|
index: number
|
||||||
|
renderItem:
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||||
|
extraData: any
|
||||||
|
}): React.ReactNode {
|
||||||
|
if (!renderItem) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.row}>
|
||||||
|
{renderItem({item, index, separators: null as any})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row = React.memo(Row)
|
||||||
|
|
||||||
|
let Visibility = ({
|
||||||
|
topMargin = '0px',
|
||||||
|
onVisibleChange,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
topMargin?: string
|
||||||
|
onVisibleChange: (isVisible: boolean) => void
|
||||||
|
style?: ViewProps['style']
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const tailRef = React.useRef(null)
|
||||||
|
const isIntersecting = React.useRef(false)
|
||||||
|
|
||||||
|
const handleIntersection = useNonReactiveCallback(
|
||||||
|
(entries: IntersectionObserverEntry[]) => {
|
||||||
|
batchedUpdates(() => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting !== isIntersecting.current) {
|
||||||
|
isIntersecting.current = entry.isIntersecting
|
||||||
|
onVisibleChange(entry.isIntersecting)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(handleIntersection, {
|
||||||
|
rootMargin: `${topMargin} 0px 0px 0px`,
|
||||||
|
})
|
||||||
|
const tail: Element | null = tailRef.current!
|
||||||
|
observer.observe(tail)
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(tail)
|
||||||
|
}
|
||||||
|
}, [handleIntersection, topMargin])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View ref={tailRef} style={addStyle(styles.visibilityDetector, style)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Visibility = React.memo(Visibility)
|
||||||
|
|
||||||
|
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
|
||||||
|
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
|
||||||
|
) => React.ReactElement
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
contentContainer: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
containerScroll: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 600,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
// @ts-ignore web only
|
||||||
|
contentVisibility: 'auto',
|
||||||
|
},
|
||||||
|
minHeightViewport: {
|
||||||
|
// @ts-ignore web only
|
||||||
|
minHeight: '100vh',
|
||||||
|
},
|
||||||
|
parentTreeVisibilityDetector: {
|
||||||
|
// @ts-ignore web only
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
aboveTheFoldDetector: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
// Bottom is dynamic.
|
||||||
|
},
|
||||||
|
visibilityDetector: {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,9 +1,10 @@
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback, useEffect} from 'react'
|
||||||
|
import EventEmitter from 'eventemitter3'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {NativeScrollEvent} from 'react-native'
|
import {NativeScrollEvent} from 'react-native'
|
||||||
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
import {useSharedValue, interpolate} from 'react-native-reanimated'
|
import {useSharedValue, interpolate} from 'react-native-reanimated'
|
||||||
|
|
||||||
const WEB_HIDE_SHELL_THRESHOLD = 200
|
const WEB_HIDE_SHELL_THRESHOLD = 200
|
||||||
|
@ -20,6 +21,15 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||||
const startDragOffset = useSharedValue<number | null>(null)
|
const startDragOffset = useSharedValue<number | null>(null)
|
||||||
const startMode = useSharedValue<number | null>(null)
|
const startMode = useSharedValue<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWeb) {
|
||||||
|
return listenToForcedWindowScroll(() => {
|
||||||
|
startDragOffset.value = null
|
||||||
|
startMode.value = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onBeginDrag = useCallback(
|
const onBeginDrag = useCallback(
|
||||||
(e: NativeScrollEvent) => {
|
(e: NativeScrollEvent) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
|
@ -100,3 +110,26 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter()
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
const originalScroll = window.scroll
|
||||||
|
window.scroll = function () {
|
||||||
|
emitter.emit('forced-scroll')
|
||||||
|
return originalScroll.apply(this, arguments as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalScrollTo = window.scrollTo
|
||||||
|
window.scrollTo = function () {
|
||||||
|
emitter.emit('forced-scroll')
|
||||||
|
return originalScrollTo.apply(this, arguments as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenToForcedWindowScroll(listener: () => void) {
|
||||||
|
emitter.addListener('forced-scroll', listener)
|
||||||
|
return () => {
|
||||||
|
emitter.removeListener('forced-scroll', listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||||
|
|
||||||
|
@ -47,7 +48,14 @@ export function SimpleViewHeader({
|
||||||
|
|
||||||
const Container = isMobile ? View : CenteredView
|
const Container = isMobile ? View : CenteredView
|
||||||
return (
|
return (
|
||||||
<Container style={[styles.header, isMobile && styles.headerMobile, style]}>
|
<Container
|
||||||
|
style={[
|
||||||
|
styles.header,
|
||||||
|
isMobile && styles.headerMobile,
|
||||||
|
isWeb && styles.headerWeb,
|
||||||
|
pal.view,
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
{showBackButton ? (
|
{showBackButton ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="viewHeaderDrawerBtn"
|
testID="viewHeaderDrawerBtn"
|
||||||
|
@ -89,6 +97,12 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
},
|
},
|
||||||
|
headerWeb: {
|
||||||
|
// @ts-ignore web-only
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
backBtn: {
|
backBtn: {
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
|
|
|
@ -64,7 +64,8 @@ export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
position: 'absolute',
|
// @ts-ignore web only
|
||||||
|
position: 'fixed',
|
||||||
left: 20,
|
left: 20,
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {clamp} from 'lib/numbers'
|
import {clamp} from 'lib/numbers'
|
||||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
import Animated from 'react-native-reanimated'
|
import Animated from 'react-native-reanimated'
|
||||||
|
|
||||||
export interface FABProps
|
export interface FABProps
|
||||||
|
@ -64,7 +65,8 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 35,
|
borderRadius: 35,
|
||||||
},
|
},
|
||||||
outer: {
|
outer: {
|
||||||
position: 'absolute',
|
// @ts-ignore web-only
|
||||||
|
position: isWeb ? 'fixed' : 'absolute',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
inner: {
|
inner: {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage'
|
||||||
import {CenteredView} from '../com/util/Views'
|
import {CenteredView} from '../com/util/Views'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
||||||
export function PostThreadScreen({route}: Props) {
|
export function PostThreadScreen({route}: Props) {
|
||||||
|
@ -112,7 +113,8 @@ export function PostThreadScreen({route}: Props) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
prompt: {
|
prompt: {
|
||||||
position: 'absolute',
|
// @ts-ignore web-only
|
||||||
|
position: isWeb ? 'fixed' : 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -334,7 +334,9 @@ export function SearchScreenInner({
|
||||||
tabBarPosition="top"
|
tabBarPosition="top"
|
||||||
onPageSelected={onPageSelected}
|
onPageSelected={onPageSelected}
|
||||||
renderTabBar={props => (
|
renderTabBar={props => (
|
||||||
<CenteredView sideBorders style={pal.border}>
|
<CenteredView
|
||||||
|
sideBorders
|
||||||
|
style={[pal.border, pal.view, styles.tabBarContainer]}>
|
||||||
<TabBar items={SECTIONS_LOGGEDIN} {...props} />
|
<TabBar items={SECTIONS_LOGGEDIN} {...props} />
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)}
|
)}
|
||||||
|
@ -375,7 +377,9 @@ export function SearchScreenInner({
|
||||||
tabBarPosition="top"
|
tabBarPosition="top"
|
||||||
onPageSelected={onPageSelected}
|
onPageSelected={onPageSelected}
|
||||||
renderTabBar={props => (
|
renderTabBar={props => (
|
||||||
<CenteredView sideBorders style={pal.border}>
|
<CenteredView
|
||||||
|
sideBorders
|
||||||
|
style={[pal.border, pal.view, styles.tabBarContainer]}>
|
||||||
<TabBar items={SECTIONS_LOGGEDOUT} {...props} />
|
<TabBar items={SECTIONS_LOGGEDOUT} {...props} />
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)}
|
)}
|
||||||
|
@ -466,6 +470,7 @@ export function SearchScreen(
|
||||||
setDrawerOpen(true)
|
setDrawerOpen(true)
|
||||||
}, [track, setDrawerOpen])
|
}, [track, setDrawerOpen])
|
||||||
const onPressCancelSearch = React.useCallback(() => {
|
const onPressCancelSearch = React.useCallback(() => {
|
||||||
|
scrollToTopWeb()
|
||||||
textInput.current?.blur()
|
textInput.current?.blur()
|
||||||
setQuery('')
|
setQuery('')
|
||||||
setShowAutocompleteResults(false)
|
setShowAutocompleteResults(false)
|
||||||
|
@ -473,11 +478,13 @@ export function SearchScreen(
|
||||||
clearTimeout(searchDebounceTimeout.current)
|
clearTimeout(searchDebounceTimeout.current)
|
||||||
}, [textInput])
|
}, [textInput])
|
||||||
const onPressClearQuery = React.useCallback(() => {
|
const onPressClearQuery = React.useCallback(() => {
|
||||||
|
scrollToTopWeb()
|
||||||
setQuery('')
|
setQuery('')
|
||||||
setShowAutocompleteResults(false)
|
setShowAutocompleteResults(false)
|
||||||
}, [setQuery])
|
}, [setQuery])
|
||||||
const onChangeText = React.useCallback(
|
const onChangeText = React.useCallback(
|
||||||
async (text: string) => {
|
async (text: string) => {
|
||||||
|
scrollToTopWeb()
|
||||||
setQuery(text)
|
setQuery(text)
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
|
@ -506,10 +513,12 @@ export function SearchScreen(
|
||||||
[setQuery, search, setSearchResults],
|
[setQuery, search, setSearchResults],
|
||||||
)
|
)
|
||||||
const onSubmit = React.useCallback(() => {
|
const onSubmit = React.useCallback(() => {
|
||||||
|
scrollToTopWeb()
|
||||||
setShowAutocompleteResults(false)
|
setShowAutocompleteResults(false)
|
||||||
}, [setShowAutocompleteResults])
|
}, [setShowAutocompleteResults])
|
||||||
|
|
||||||
const onSoftReset = React.useCallback(() => {
|
const onSoftReset = React.useCallback(() => {
|
||||||
|
scrollToTopWeb()
|
||||||
onPressCancelSearch()
|
onPressCancelSearch()
|
||||||
}, [onPressCancelSearch])
|
}, [onPressCancelSearch])
|
||||||
|
|
||||||
|
@ -526,11 +535,12 @@ export function SearchScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={isWeb ? null : {flex: 1}}>
|
||||||
<CenteredView
|
<CenteredView
|
||||||
style={[
|
style={[
|
||||||
styles.header,
|
styles.header,
|
||||||
pal.border,
|
pal.border,
|
||||||
|
pal.view,
|
||||||
isTabletOrDesktop && {paddingTop: 10},
|
isTabletOrDesktop && {paddingTop: 10},
|
||||||
]}
|
]}
|
||||||
sideBorders={isTabletOrDesktop}>
|
sideBorders={isTabletOrDesktop}>
|
||||||
|
@ -661,12 +671,25 @@ export function SearchScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToTopWeb() {
|
||||||
|
if (isWeb) {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_HEIGHT = 50
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
// @ts-ignore web only
|
||||||
|
position: isWeb ? 'sticky' : '',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
headerMenuBtn: {
|
headerMenuBtn: {
|
||||||
width: 30,
|
width: 30,
|
||||||
|
@ -696,4 +719,10 @@ const styles = StyleSheet.create({
|
||||||
headerCancelBtn: {
|
headerCancelBtn: {
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
},
|
},
|
||||||
|
tabBarContainer: {
|
||||||
|
// @ts-ignore web only
|
||||||
|
position: isWeb ? 'sticky' : '',
|
||||||
|
top: isWeb ? HEADER_HEIGHT : 0,
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {ComposePost} from '../com/composer/Composer'
|
||||||
import {useComposerState} from 'state/shell/composer'
|
import {useComposerState} from 'state/shell/composer'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||||
import {
|
import {
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerState,
|
EmojiPickerState,
|
||||||
|
@ -16,6 +17,8 @@ export function Composer({}: {winHeight: number}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
|
const isActive = !!state
|
||||||
|
useWebBodyScrollLock(isActive)
|
||||||
|
|
||||||
const [pickerState, setPickerState] = React.useState<EmojiPickerState>({
|
const [pickerState, setPickerState] = React.useState<EmojiPickerState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
@ -40,7 +43,7 @@ export function Composer({}: {winHeight: number}) {
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
if (!state) {
|
if (!isActive) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +78,8 @@ export function Composer({}: {winHeight: number}) {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mask: {
|
mask: {
|
||||||
position: 'absolute',
|
// @ts-ignore
|
||||||
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
@ -12,6 +12,10 @@ export const styles = StyleSheet.create({
|
||||||
paddingLeft: 5,
|
paddingLeft: 5,
|
||||||
paddingRight: 10,
|
paddingRight: 10,
|
||||||
},
|
},
|
||||||
|
bottomBarWeb: {
|
||||||
|
// @ts-ignore web-only
|
||||||
|
position: 'fixed',
|
||||||
|
},
|
||||||
ctrl: {
|
ctrl: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: 13,
|
paddingTop: 13,
|
||||||
|
|
|
@ -57,6 +57,7 @@ export function BottomBarWeb() {
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.bottomBar,
|
styles.bottomBar,
|
||||||
|
styles.bottomBarWeb,
|
||||||
pal.view,
|
pal.view,
|
||||||
pal.border,
|
pal.border,
|
||||||
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
|
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
|
||||||
|
|
|
@ -442,10 +442,11 @@ export function DesktopLeftNav() {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
leftNav: {
|
leftNav: {
|
||||||
position: 'absolute',
|
// @ts-ignore web only
|
||||||
|
position: 'fixed',
|
||||||
top: 10,
|
top: 10,
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
right: 'calc(50vw + 312px)',
|
left: 'calc(50vw - 300px - 220px - 20px)',
|
||||||
width: 220,
|
width: 220,
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
maxHeight: 'calc(100vh - 10px)',
|
maxHeight: 'calc(100vh - 10px)',
|
||||||
|
|
|
@ -177,9 +177,10 @@ function InviteCodes() {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
rightNav: {
|
rightNav: {
|
||||||
position: 'absolute',
|
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
left: 'calc(50vw + 320px)',
|
position: 'fixed',
|
||||||
|
// @ts-ignore web only
|
||||||
|
left: 'calc(50vw + 300px + 20px)',
|
||||||
width: 300,
|
width: 300,
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {useAuxClick} from 'lib/hooks/useAuxClick'
|
||||||
import {t} from '@lingui/macro'
|
import {t} from '@lingui/macro'
|
||||||
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
|
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
|
|
||||||
function ShellInner() {
|
function ShellInner() {
|
||||||
|
@ -24,6 +25,7 @@ function ShellInner() {
|
||||||
const navigator = useNavigation<NavigationProp>()
|
const navigator = useNavigation<NavigationProp>()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
|
|
||||||
|
useWebBodyScrollLock(isDrawerOpen)
|
||||||
useAuxClick()
|
useAuxClick()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -34,12 +36,10 @@ function ShellInner() {
|
||||||
}, [navigator, closeAllActiveElements])
|
}, [navigator, closeAllActiveElements])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.hContentRegion, {overflow: 'hidden'}]}>
|
<>
|
||||||
<View style={s.hContentRegion}>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<FlatNavigator />
|
<FlatNavigator />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
|
||||||
<Composer winHeight={0} />
|
<Composer winHeight={0} />
|
||||||
<ModalsContainer />
|
<ModalsContainer />
|
||||||
<PortalOutlet />
|
<PortalOutlet />
|
||||||
|
@ -55,7 +55,7 @@ function ShellInner() {
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,8 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: colors.black, // TODO
|
backgroundColor: colors.black, // TODO
|
||||||
},
|
},
|
||||||
drawerMask: {
|
drawerMask: {
|
||||||
position: 'absolute',
|
// @ts-ignore web only
|
||||||
|
position: 'fixed',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
@ -87,7 +88,8 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
drawerContainer: {
|
drawerContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
position: 'absolute',
|
// @ts-ignore web only
|
||||||
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
|
@ -37,10 +37,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
|
||||||
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
/* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
height: calc(100% + env(safe-area-inset-top));
|
height: calc(100% + env(safe-area-inset-top));
|
||||||
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
/* Remove autofill styles on Webkit */
|
||||||
|
|
|
@ -7525,6 +7525,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-dom@^18.2.18":
|
||||||
|
version "18.2.18"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
||||||
|
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-responsive@^8.0.5":
|
"@types/react-responsive@^8.0.5":
|
||||||
version "8.0.5"
|
version "8.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a"
|
resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a"
|
||||||
|
|
Loading…
Reference in New Issue