Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

New Web Layout (#2126)

* Rip out virtualization on the web

* Screw around with layout

* onEndReached

* scrollToOffset

* Fix background

* onScroll

* Shell bars

* More scroll

* Fixes

* position: sticky

* Clean up 1

* Clean up 2

* Undo PagerWithHeader changes and fork it

* Trim down both versions

* Cleanup 3

* Memoize, lint

* Don't scroll away modal or lightbox

* Add content-visibility for rows

* Fix composer

* Fix types

* Fix borked scroll animation

* Fixes to layout

* More FlatList parity

* Layout fixes

* Fix more layout

* More layout

* More layouts

* Fix profile layout

* Remove onScroll

* Display: none inactive pages

* Add an intermediate List component

* Fix type

* Add onScrolledDownChange

* Port pager to use onScrolledDownChange

* Fix on mobile

* Don't pass down onScroll (replacement TBD)

* Remove resetMainScroll

* Replace onMainScroll with MainScrollProvider

* Hook ScrollProvider to pager

* Fix the remaining special case

* Optimize a bit

* Enforce that onScroll cannot be passed

* Keep value updated even if no handler

* Also memo it

* Move the fork to List.web

* Add scroll handler

* Consolidate List props a bit

* More stuff

* Rm unused

* Simplify

* Make isScrolledDown work

* Oops

* Fixes

* Hook up context scroll handlers

* Scroll restore for tabs

* Route scroll restoration POC

* Fix some issues with restoration

* Remove bad idea

* Fix pager scroll restoration

* Undo accidental locale changes

* onContentSizeChange

* Scroll to post

* Better positioning

* Layout fixes

* Factor out navigation stuff

* Cleanup

* Oops

* Cleanup

* Fixes and types

* Naming etc

* Fix crash

* Match FL semantics

* Snap the header scroll on the web

* Add body scroll lock

* Scroll to top on search

* Fix types

* Typos

* Fix Safari overflow

* Fix search positioning

* Add border

* Patch react navigation

* Revert "Patch react navigation"

This reverts commit 62516ed9c20410d166e1582b43b656c819495ddc.

* fixes

* scroll

* scrollbar

* cleanup unrelated

* undo unrel

* flatter

* Fix css

* twk

authored by danabra.mov and committed by

GitHub f015229a cd02922b

+849 -97
+1 -1
bskyweb/templates/base.html
··· 33 33 } 34 34 35 35 html { 36 - scroll-behavior: smooth; 37 36 /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */ 38 37 -webkit-text-size-adjust: 100%; 39 38 height: calc(100% + env(safe-area-inset-top)); 39 + scrollbar-gutter: stable both-edges; 40 40 } 41 41 42 42 /* Remove autofill styles on Webkit */
+1
package.json
··· 206 206 "@types/lodash.shuffle": "^4.2.7", 207 207 "@types/psl": "^1.1.1", 208 208 "@types/react-avatar-editor": "^13.0.0", 209 + "@types/react-dom": "^18.2.18", 209 210 "@types/react-responsive": "^8.0.5", 210 211 "@types/react-test-renderer": "^17.0.1", 211 212 "@typescript-eslint/eslint-plugin": "^5.48.2",
+4 -1
src/Navigation.tsx
··· 39 39 setEmailConfirmationRequested, 40 40 } from './state/shell/reminders' 41 41 import {init as initAnalytics} from './lib/analytics/analytics' 42 + import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' 42 43 43 44 import {HomeScreen} from './view/screens/Home' 44 45 import {SearchScreen} from './view/screens/Search' ··· 413 414 const FlatNavigator = () => { 414 415 const pal = usePalette('default') 415 416 const numUnread = useUnreadNotifications() 416 - 417 + const screenListeners = useWebScrollRestoration() 417 418 const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread) 419 + 418 420 return ( 419 421 <Flat.Navigator 422 + screenListeners={screenListeners} 420 423 screenOptions={{ 421 424 gestureEnabled: true, 422 425 fullScreenGestureEnabled: true,
-1
src/lib/batchedUpdates.web.ts
··· 1 - // @ts-ignore 2 1 export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
+28
src/lib/hooks/useWebBodyScrollLock.ts
··· 1 + import {useEffect} from 'react' 2 + import {isWeb} from '#/platform/detection' 3 + 4 + let refCount = 0 5 + 6 + function incrementRefCount() { 7 + if (refCount === 0) { 8 + document.body.style.overflow = 'hidden' 9 + } 10 + refCount++ 11 + } 12 + 13 + function decrementRefCount() { 14 + refCount-- 15 + if (refCount === 0) { 16 + document.body.style.overflow = '' 17 + } 18 + } 19 + 20 + export function useWebBodyScrollLock(isLockActive: boolean) { 21 + useEffect(() => { 22 + if (!isWeb || !isLockActive) { 23 + return 24 + } 25 + incrementRefCount() 26 + return () => decrementRefCount() 27 + }) 28 + }
+3
src/lib/hooks/useWebScrollRestoration.native.ts
··· 1 + export function useWebScrollRestoration() { 2 + return undefined 3 + }
+52
src/lib/hooks/useWebScrollRestoration.ts
··· 1 + import {useMemo, useState, useEffect} from 'react' 2 + import {EventArg, useNavigation} from '@react-navigation/core' 3 + 4 + if ('scrollRestoration' in history) { 5 + // Tell the brower not to mess with the scroll. 6 + // We're doing that manually below. 7 + history.scrollRestoration = 'manual' 8 + } 9 + 10 + function createInitialScrollState() { 11 + return { 12 + scrollYs: new Map(), 13 + focusedKey: null as string | null, 14 + } 15 + } 16 + 17 + export function useWebScrollRestoration() { 18 + const [state] = useState(createInitialScrollState) 19 + const navigation = useNavigation() 20 + 21 + useEffect(() => { 22 + function onDispatch() { 23 + if (state.focusedKey) { 24 + // Remember where we were for later. 25 + state.scrollYs.set(state.focusedKey, window.scrollY) 26 + // TODO: Strictly speaking, this is a leak. We never clean up. 27 + // This is because I'm not sure when it's appropriate to clean it up. 28 + // It doesn't seem like popstate is enough because it can still Forward-Back again. 29 + // Maybe we should use sessionStorage. Or check what Next.js is doing? 30 + } 31 + } 32 + // We want to intercept any push/pop/replace *before* the re-render. 33 + // There is no official way to do this yet, but this works okay for now. 34 + // https://twitter.com/satya164/status/1737301243519725803 35 + navigation.addListener('__unsafe_action__' as any, onDispatch) 36 + return () => { 37 + navigation.removeListener('__unsafe_action__' as any, onDispatch) 38 + } 39 + }, [state, navigation]) 40 + 41 + const screenListeners = useMemo( 42 + () => ({ 43 + focus(e: EventArg<'focus', boolean | undefined, unknown>) { 44 + const scrollY = state.scrollYs.get(e.target) ?? 0 45 + window.scrollTo(0, scrollY) 46 + state.focusedKey = e.target ?? null 47 + }, 48 + }), 49 + [state], 50 + ) 51 + return screenListeners 52 + }
+2 -2
src/lib/styles.ts
··· 1 1 import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' 2 2 import {Theme, TypographyVariant} from './ThemeContext' 3 - import {isMobileWeb} from 'platform/detection' 3 + import {isWeb} from 'platform/detection' 4 4 5 5 // 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest 6 6 export const colors = { ··· 175 175 // dimensions 176 176 w100pct: {width: '100%'}, 177 177 h100pct: {height: '100%'}, 178 - hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'}, 178 + hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'}, 179 179 window: { 180 180 width: Dimensions.get('window').width, 181 181 height: Dimensions.get('window').height,
+2 -1
src/view/com/composer/text-input/web/EmojiPicker.web.tsx
··· 121 121 122 122 const styles = StyleSheet.create({ 123 123 mask: { 124 - position: 'absolute', 124 + // @ts-ignore web ony 125 + position: 'fixed', 125 126 top: 0, 126 127 left: 0, 127 128 right: 0,
+1 -10
src/view/com/feeds/FeedPage.tsx
··· 210 210 const {isDesktop, isTablet} = useWebMediaQueries() 211 211 const {fontScale} = useWindowDimensions() 212 212 const {hasSession} = useSession() 213 - 214 - if (isDesktop) { 213 + if (isDesktop || isTablet) { 215 214 return 0 216 215 } 217 - if (isTablet) { 218 - if (hasSession) { 219 - return 50 220 - } else { 221 - return 0 222 - } 223 - } 224 - 225 216 if (hasSession) { 226 217 const navBarPad = 16 227 218 const navBarText = 21 * fontScale
+14 -6
src/view/com/lightbox/Lightbox.web.tsx
··· 1 1 import React, {useCallback, useEffect, useState} from 'react' 2 2 import { 3 3 Image, 4 + ImageStyle, 4 5 TouchableOpacity, 5 6 TouchableWithoutFeedback, 6 7 StyleSheet, 7 8 View, 8 9 Pressable, 9 10 } from 'react-native' 10 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 + import { 12 + FontAwesomeIcon, 13 + FontAwesomeIconStyle, 14 + } from '@fortawesome/react-native-fontawesome' 11 15 import {colors, s} from 'lib/styles' 12 16 import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' 13 17 import {Text} from '../util/text/Text' ··· 19 23 ImagesLightbox, 20 24 ProfileImageLightbox, 21 25 } from '#/state/lightbox' 26 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 22 27 23 28 interface Img { 24 29 uri: string ··· 28 33 export function Lightbox() { 29 34 const {activeLightbox} = useLightbox() 30 35 const {closeLightbox} = useLightboxControls() 36 + const isActive = !!activeLightbox 37 + useWebBodyScrollLock(isActive) 31 38 32 - if (!activeLightbox) { 39 + if (!isActive) { 33 40 return null 34 41 } 35 42 ··· 116 123 <Image 117 124 accessibilityIgnoresInvertColors 118 125 source={imgs[index]} 119 - style={styles.image} 126 + style={styles.image as ImageStyle} 120 127 accessibilityLabel={imgs[index].alt} 121 128 accessibilityHint="" 122 129 /> ··· 129 136 accessibilityHint=""> 130 137 <FontAwesomeIcon 131 138 icon="angle-left" 132 - style={styles.icon} 139 + style={styles.icon as FontAwesomeIconStyle} 133 140 size={40} 134 141 /> 135 142 </TouchableOpacity> ··· 143 150 accessibilityHint=""> 144 151 <FontAwesomeIcon 145 152 icon="angle-right" 146 - style={styles.icon} 153 + style={styles.icon as FontAwesomeIconStyle} 147 154 size={40} 148 155 /> 149 156 </TouchableOpacity> ··· 178 185 179 186 const styles = StyleSheet.create({ 180 187 mask: { 181 - position: 'absolute', 188 + // @ts-ignore 189 + position: 'fixed', 182 190 top: 0, 183 191 left: 0, 184 192 width: '100%',
+4 -1
src/view/com/modals/Modal.web.tsx
··· 3 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 4 import {usePalette} from 'lib/hooks/usePalette' 5 5 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 6 7 7 8 import {useModals, useModalControls} from '#/state/modals' 8 9 import type {Modal as ModalIface} from '#/state/modals' ··· 38 39 39 40 export function ModalsContainer() { 40 41 const {isModalActive, activeModals} = useModals() 42 + useWebBodyScrollLock(isModalActive) 41 43 42 44 if (!isModalActive) { 43 45 return null ··· 166 168 167 169 const styles = StyleSheet.create({ 168 170 mask: { 169 - position: 'absolute', 171 + // @ts-ignore 172 + position: 'fixed', 170 173 top: 0, 171 174 left: 0, 172 175 width: '100%',
+7 -4
src/view/com/pager/FeedsTabBar.web.tsx
··· 117 117 return ( 118 118 // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 119 119 <Animated.View 120 - style={[pal.view, styles.tabBar, headerMinimalShellTransform]} 120 + style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 121 121 onLayout={e => { 122 122 headerHeight.value = e.nativeEvent.layout.height 123 123 }}> ··· 134 134 135 135 const styles = StyleSheet.create({ 136 136 tabBar: { 137 - position: 'absolute', 137 + // @ts-ignore Web only 138 + position: 'sticky', 138 139 zIndex: 1, 139 140 // @ts-ignore Web only -prf 140 - left: 'calc(50% - 299px)', 141 - width: 598, 141 + left: 'calc(50% - 300px)', 142 + width: 600, 142 143 top: 0, 143 144 flexDirection: 'row', 144 145 alignItems: 'center', 146 + borderLeftWidth: 1, 147 + borderRightWidth: 1, 145 148 }, 146 149 })
+2 -1
src/view/com/pager/FeedsTabBarMobile.tsx
··· 142 142 143 143 const styles = StyleSheet.create({ 144 144 tabBar: { 145 - position: 'absolute', 145 + // @ts-ignore web-only 146 + position: isWeb ? 'fixed' : 'absolute', 146 147 zIndex: 1, 147 148 left: 0, 148 149 right: 0,
+1
src/view/com/pager/Pager.tsx
··· 17 17 export interface RenderTabBarFnProps { 18 18 selectedPage: number 19 19 onSelect?: (index: number) => void 20 + tabBarAnchor?: JSX.Element | null | undefined // Ignored on native. 20 21 } 21 22 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element 22 23
+35 -16
src/view/com/pager/Pager.web.tsx
··· 1 1 import React from 'react' 2 + import {flushSync} from 'react-dom' 2 3 import {View} from 'react-native' 3 4 import {s} from 'lib/styles' 4 5 5 6 export interface RenderTabBarFnProps { 6 7 selectedPage: number 7 8 onSelect?: (index: number) => void 9 + tabBarAnchor?: JSX.Element 8 10 } 9 11 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element 10 12 ··· 27 29 ref, 28 30 ) { 29 31 const [selectedPage, setSelectedPage] = React.useState(initialPage) 32 + const scrollYs = React.useRef<Array<number | null>>([]) 33 + const anchorRef = React.useRef(null) 30 34 31 35 React.useImperativeHandle(ref, () => ({ 32 36 setPage: (index: number) => setSelectedPage(index), ··· 34 38 35 39 const onTabBarSelect = React.useCallback( 36 40 (index: number) => { 37 - setSelectedPage(index) 38 - onPageSelected?.(index) 39 - onPageSelecting?.(index) 41 + const scrollY = window.scrollY 42 + // We want to determine if the tabbar is already "sticking" at the top (in which 43 + // case we should preserve and restore scroll), or if it is somewhere below in the 44 + // viewport (in which case a scroll jump would be jarring). We determine this by 45 + // measuring where the "anchor" element is (which we place just above the tabbar). 46 + let anchorTop = anchorRef.current 47 + ? (anchorRef.current as Element).getBoundingClientRect().top 48 + : -scrollY // If there's no anchor, treat the top of the page as one. 49 + const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable. 50 + 51 + if (isSticking) { 52 + scrollYs.current[selectedPage] = window.scrollY 53 + } else { 54 + scrollYs.current[selectedPage] = null 55 + } 56 + flushSync(() => { 57 + setSelectedPage(index) 58 + onPageSelected?.(index) 59 + onPageSelecting?.(index) 60 + }) 61 + if (isSticking) { 62 + const restoredScrollY = scrollYs.current[index] 63 + if (restoredScrollY != null) { 64 + window.scrollTo(0, restoredScrollY) 65 + } else { 66 + window.scrollTo(0, scrollY + anchorTop) 67 + } 68 + } 40 69 }, 41 - [setSelectedPage, onPageSelected, onPageSelecting], 70 + [selectedPage, setSelectedPage, onPageSelected, onPageSelecting], 42 71 ) 43 72 44 73 return ( ··· 46 75 {tabBarPosition === 'top' && 47 76 renderTabBar({ 48 77 selectedPage, 78 + tabBarAnchor: <View ref={anchorRef} />, 49 79 onSelect: onTabBarSelect, 50 80 })} 51 81 {React.Children.map(children, (child, i) => ( 52 - <View 53 - style={ 54 - selectedPage === i 55 - ? s.flex1 56 - : { 57 - position: 'absolute', 58 - pointerEvents: 'none', 59 - // @ts-ignore web-only 60 - visibility: 'hidden', 61 - } 62 - } 63 - key={`page-${i}`}> 82 + <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}> 64 83 {child} 65 84 </View> 66 85 ))}
+1 -14
src/view/com/pager/PagerWithHeader.tsx
··· 18 18 } from 'react-native-reanimated' 19 19 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 20 20 import {TabBar} from './TabBar' 21 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 22 21 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 23 22 import {ListMethods} from '../util/List' 24 23 import {ScrollProvider} from '#/lib/ScrollContext' ··· 235 234 onCurrentPageSelected?: (index: number) => void 236 235 onSelect?: (index: number) => void 237 236 }): React.ReactNode => { 238 - const {isMobile} = useWebMediaQueries() 239 237 const headerTransform = useAnimatedStyle(() => ({ 240 238 transform: [ 241 239 { ··· 246 244 return ( 247 245 <Animated.View 248 246 pointerEvents="box-none" 249 - style={[ 250 - isMobile ? styles.tabBarMobile : styles.tabBarDesktop, 251 - headerTransform, 252 - ]}> 247 + style={[styles.tabBarMobile, headerTransform]}> 253 248 <View onLayout={onHeaderOnlyLayout} pointerEvents="box-none"> 254 249 {renderHeader?.()} 255 250 </View> ··· 324 319 top: 0, 325 320 left: 0, 326 321 width: '100%', 327 - }, 328 - tabBarDesktop: { 329 - position: 'absolute', 330 - zIndex: 1, 331 - top: 0, 332 - // @ts-ignore Web only -prf 333 - left: 'calc(50% - 299px)', 334 - width: 598, 335 322 }, 336 323 }) 337 324
+194
src/view/com/pager/PagerWithHeader.web.tsx
··· 1 + import * as React from 'react' 2 + import {FlatList, ScrollView, StyleSheet, View} from 'react-native' 3 + import {useAnimatedRef} from 'react-native-reanimated' 4 + import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 5 + import {TabBar} from './TabBar' 6 + import {usePalette} from '#/lib/hooks/usePalette' 7 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 + import {ListMethods} from '../util/List' 9 + 10 + export interface PagerWithHeaderChildParams { 11 + headerHeight: number 12 + isFocused: boolean 13 + scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> 14 + } 15 + 16 + export interface PagerWithHeaderProps { 17 + testID?: string 18 + children: 19 + | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] 20 + | ((props: PagerWithHeaderChildParams) => JSX.Element) 21 + items: string[] 22 + isHeaderReady: boolean 23 + renderHeader?: () => JSX.Element 24 + initialPage?: number 25 + onPageSelected?: (index: number) => void 26 + onCurrentPageSelected?: (index: number) => void 27 + } 28 + export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( 29 + function PageWithHeaderImpl( 30 + { 31 + children, 32 + testID, 33 + items, 34 + renderHeader, 35 + initialPage, 36 + onPageSelected, 37 + onCurrentPageSelected, 38 + }: PagerWithHeaderProps, 39 + ref, 40 + ) { 41 + const [currentPage, setCurrentPage] = React.useState(0) 42 + 43 + const renderTabBar = React.useCallback( 44 + (props: RenderTabBarFnProps) => { 45 + return ( 46 + <PagerTabBar 47 + items={items} 48 + renderHeader={renderHeader} 49 + currentPage={currentPage} 50 + onCurrentPageSelected={onCurrentPageSelected} 51 + onSelect={props.onSelect} 52 + tabBarAnchor={props.tabBarAnchor} 53 + testID={testID} 54 + /> 55 + ) 56 + }, 57 + [items, renderHeader, currentPage, onCurrentPageSelected, testID], 58 + ) 59 + 60 + const onPageSelectedInner = React.useCallback( 61 + (index: number) => { 62 + setCurrentPage(index) 63 + onPageSelected?.(index) 64 + }, 65 + [onPageSelected, setCurrentPage], 66 + ) 67 + 68 + const onPageSelecting = React.useCallback((index: number) => { 69 + setCurrentPage(index) 70 + }, []) 71 + 72 + return ( 73 + <Pager 74 + ref={ref} 75 + testID={testID} 76 + initialPage={initialPage} 77 + onPageSelected={onPageSelectedInner} 78 + onPageSelecting={onPageSelecting} 79 + renderTabBar={renderTabBar} 80 + tabBarPosition="top"> 81 + {toArray(children) 82 + .filter(Boolean) 83 + .map((child, i) => { 84 + return ( 85 + <View key={i} collapsable={false}> 86 + <PagerItem isFocused={i === currentPage} renderTab={child} /> 87 + </View> 88 + ) 89 + })} 90 + </Pager> 91 + ) 92 + }, 93 + ) 94 + 95 + let PagerTabBar = ({ 96 + currentPage, 97 + items, 98 + testID, 99 + renderHeader, 100 + onCurrentPageSelected, 101 + onSelect, 102 + tabBarAnchor, 103 + }: { 104 + currentPage: number 105 + items: string[] 106 + testID?: string 107 + renderHeader?: () => JSX.Element 108 + onCurrentPageSelected?: (index: number) => void 109 + onSelect?: (index: number) => void 110 + tabBarAnchor?: JSX.Element | null | undefined 111 + }): React.ReactNode => { 112 + const pal = usePalette('default') 113 + const {isMobile} = useWebMediaQueries() 114 + return ( 115 + <> 116 + <View style={[!isMobile && styles.headerContainerDesktop, pal.border]}> 117 + {renderHeader?.()} 118 + </View> 119 + {tabBarAnchor} 120 + <View 121 + style={[ 122 + styles.tabBarContainer, 123 + isMobile 124 + ? styles.tabBarContainerMobile 125 + : styles.tabBarContainerDesktop, 126 + pal.border, 127 + ]}> 128 + <TabBar 129 + testID={testID} 130 + items={items} 131 + selectedPage={currentPage} 132 + onSelect={onSelect} 133 + onPressSelected={onCurrentPageSelected} 134 + /> 135 + </View> 136 + </> 137 + ) 138 + } 139 + PagerTabBar = React.memo(PagerTabBar) 140 + 141 + function PagerItem({ 142 + isFocused, 143 + renderTab, 144 + }: { 145 + isFocused: boolean 146 + renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null 147 + }) { 148 + const scrollElRef = useAnimatedRef() 149 + if (renderTab == null) { 150 + return null 151 + } 152 + return renderTab({ 153 + headerHeight: 0, 154 + isFocused, 155 + scrollElRef: scrollElRef as React.MutableRefObject< 156 + ListMethods | ScrollView | null 157 + >, 158 + }) 159 + } 160 + 161 + const styles = StyleSheet.create({ 162 + headerContainerDesktop: { 163 + marginLeft: 'auto', 164 + marginRight: 'auto', 165 + width: 600, 166 + borderLeftWidth: 1, 167 + borderRightWidth: 1, 168 + }, 169 + tabBarContainer: { 170 + // @ts-ignore web-only 171 + position: 'sticky', 172 + overflow: 'hidden', 173 + top: 0, 174 + zIndex: 1, 175 + }, 176 + tabBarContainerDesktop: { 177 + marginLeft: 'auto', 178 + marginRight: 'auto', 179 + width: 600, 180 + borderLeftWidth: 1, 181 + borderRightWidth: 1, 182 + }, 183 + tabBarContainerMobile: { 184 + paddingLeft: 14, 185 + paddingRight: 14, 186 + }, 187 + }) 188 + 189 + function toArray<T>(v: T | T[]): T[] { 190 + if (Array.isArray(v)) { 191 + return v 192 + } 193 + return [v] 194 + }
+28 -10
src/view/com/post-thread/PostThread.tsx
··· 139 139 const {hasSession} = useSession() 140 140 const {_} = useLingui() 141 141 const pal = usePalette('default') 142 - const {isTablet, isDesktop} = useWebMediaQueries() 142 + const {isTablet, isDesktop, isTabletOrMobile} = useWebMediaQueries() 143 143 const ref = useRef<ListMethods>(null) 144 144 const highlightedPostRef = useRef<View | null>(null) 145 145 const needsScrollAdjustment = useRef<boolean>( ··· 197 197 198 198 // wait for loading to finish 199 199 if (thread.type === 'post' && !!thread.parent) { 200 - highlightedPostRef.current?.measure( 201 - (_x, _y, _width, _height, _pageX, pageY) => { 202 - ref.current?.scrollToOffset({ 203 - animated: false, 204 - offset: pageY - (isDesktop ? 0 : 50), 205 - }) 206 - }, 207 - ) 200 + function onMeasure(pageY: number) { 201 + let spinnerHeight = 0 202 + if (isDesktop) { 203 + spinnerHeight = 40 204 + } else if (isTabletOrMobile) { 205 + spinnerHeight = 82 206 + } 207 + ref.current?.scrollToOffset({ 208 + animated: false, 209 + offset: pageY - spinnerHeight, 210 + }) 211 + } 212 + if (isNative) { 213 + highlightedPostRef.current?.measure( 214 + (_x, _y, _width, _height, _pageX, pageY) => { 215 + onMeasure(pageY) 216 + }, 217 + ) 218 + } else { 219 + // Measure synchronously to avoid a layout jump. 220 + const domNode = highlightedPostRef.current 221 + if (domNode) { 222 + const pageY = (domNode as any as Element).getBoundingClientRect().top 223 + onMeasure(pageY) 224 + } 225 + } 208 226 needsScrollAdjustment.current = false 209 227 } 210 - }, [thread, isDesktop]) 228 + }, [thread, isDesktop, isTabletOrMobile]) 211 229 212 230 const onPTR = React.useCallback(async () => { 213 231 setIsPTRing(true)
+2 -4
src/view/com/util/List.tsx
··· 1 - import React, {memo, startTransition} from 'react' 1 + import React, {memo} from 'react' 2 2 import {FlatListProps, RefreshControl} from 'react-native' 3 3 import {FlatList_INTERNAL} from './Views' 4 4 import {addStyle} from 'lib/styles' ··· 39 39 const pal = usePalette('default') 40 40 41 41 function handleScrolledDownChange(didScrollDown: boolean) { 42 - startTransition(() => { 43 - onScrolledDownChange?.(didScrollDown) 44 - }) 42 + onScrolledDownChange?.(didScrollDown) 45 43 } 46 44 47 45 const scrollHandler = useAnimatedScrollHandler({
+341
src/view/com/util/List.web.tsx
··· 1 + import React, {isValidElement, memo, useRef, startTransition} from 'react' 2 + import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native' 3 + import {addStyle} from 'lib/styles' 4 + import {usePalette} from 'lib/hooks/usePalette' 5 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 + import {useScrollHandlers} from '#/lib/ScrollContext' 7 + import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 8 + import {batchedUpdates} from '#/lib/batchedUpdates' 9 + 10 + export type ListMethods = any // TODO: Better types. 11 + export type ListProps<ItemT> = Omit< 12 + FlatListProps<ItemT>, 13 + | 'onScroll' // Use ScrollContext instead. 14 + | 'refreshControl' // Pass refreshing and/or onRefresh instead. 15 + | 'contentOffset' // Pass headerOffset instead. 16 + > & { 17 + onScrolledDownChange?: (isScrolledDown: boolean) => void 18 + headerOffset?: number 19 + refreshing?: boolean 20 + onRefresh?: () => void 21 + desktopFixedHeight: any // TODO: Better types. 22 + } 23 + export type ListRef = React.MutableRefObject<any | null> // TODO: Better types. 24 + 25 + function ListImpl<ItemT>( 26 + { 27 + ListHeaderComponent, 28 + ListFooterComponent, 29 + contentContainerStyle, 30 + data, 31 + desktopFixedHeight, 32 + headerOffset, 33 + keyExtractor, 34 + refreshing: _unsupportedRefreshing, 35 + onEndReached, 36 + onEndReachedThreshold = 0, 37 + onRefresh: _unsupportedOnRefresh, 38 + onScrolledDownChange, 39 + onContentSizeChange, 40 + renderItem, 41 + extraData, 42 + style, 43 + ...props 44 + }: ListProps<ItemT>, 45 + ref: React.Ref<ListMethods>, 46 + ) { 47 + const contextScrollHandlers = useScrollHandlers() 48 + const pal = usePalette('default') 49 + const {isMobile} = useWebMediaQueries() 50 + if (!isMobile) { 51 + contentContainerStyle = addStyle( 52 + contentContainerStyle, 53 + styles.containerScroll, 54 + ) 55 + } 56 + 57 + let header: JSX.Element | null = null 58 + if (ListHeaderComponent != null) { 59 + if (isValidElement(ListHeaderComponent)) { 60 + header = ListHeaderComponent 61 + } else { 62 + // @ts-ignore Nah it's fine. 63 + header = <ListHeaderComponent /> 64 + } 65 + } 66 + 67 + let footer: JSX.Element | null = null 68 + if (ListFooterComponent != null) { 69 + if (isValidElement(ListFooterComponent)) { 70 + footer = ListFooterComponent 71 + } else { 72 + // @ts-ignore Nah it's fine. 73 + footer = <ListFooterComponent /> 74 + } 75 + } 76 + 77 + if (headerOffset != null) { 78 + style = addStyle(style, { 79 + paddingTop: headerOffset, 80 + }) 81 + } 82 + 83 + const nativeRef = React.useRef(null) 84 + React.useImperativeHandle( 85 + ref, 86 + () => 87 + ({ 88 + scrollToTop() { 89 + window.scrollTo({top: 0}) 90 + }, 91 + scrollToOffset({ 92 + animated, 93 + offset, 94 + }: { 95 + animated: boolean 96 + offset: number 97 + }) { 98 + window.scrollTo({ 99 + left: 0, 100 + top: offset, 101 + behavior: animated ? 'smooth' : 'instant', 102 + }) 103 + }, 104 + } as any), // TODO: Better types. 105 + [], 106 + ) 107 + 108 + // --- onContentSizeChange --- 109 + const containerRef = useRef(null) 110 + useResizeObserver(containerRef, onContentSizeChange) 111 + 112 + // --- onScroll --- 113 + const [isInsideVisibleTree, setIsInsideVisibleTree] = React.useState(false) 114 + const handleWindowScroll = useNonReactiveCallback(() => { 115 + if (isInsideVisibleTree) { 116 + contextScrollHandlers.onScroll?.( 117 + { 118 + contentOffset: { 119 + x: Math.max(0, window.scrollX), 120 + y: Math.max(0, window.scrollY), 121 + }, 122 + } as any, // TODO: Better types. 123 + null as any, 124 + ) 125 + } 126 + }) 127 + React.useEffect(() => { 128 + if (!isInsideVisibleTree) { 129 + // Prevents hidden tabs from firing scroll events. 130 + // Only one list is expected to be firing these at a time. 131 + return 132 + } 133 + window.addEventListener('scroll', handleWindowScroll) 134 + return () => { 135 + window.removeEventListener('scroll', handleWindowScroll) 136 + } 137 + }, [isInsideVisibleTree, handleWindowScroll]) 138 + 139 + // --- onScrolledDownChange --- 140 + const isScrolledDown = useRef(false) 141 + function handleAboveTheFoldVisibleChange(isAboveTheFold: boolean) { 142 + const didScrollDown = !isAboveTheFold 143 + if (isScrolledDown.current !== didScrollDown) { 144 + isScrolledDown.current = didScrollDown 145 + startTransition(() => { 146 + onScrolledDownChange?.(didScrollDown) 147 + }) 148 + } 149 + } 150 + 151 + // --- onEndReached --- 152 + const onTailVisibilityChange = useNonReactiveCallback( 153 + (isTailVisible: boolean) => { 154 + if (isTailVisible) { 155 + onEndReached?.({ 156 + distanceFromEnd: onEndReachedThreshold || 0, 157 + }) 158 + } 159 + }, 160 + ) 161 + 162 + return ( 163 + <View {...props} style={style} ref={nativeRef}> 164 + <Visibility 165 + onVisibleChange={setIsInsideVisibleTree} 166 + style={ 167 + // This has position: fixed, so it should always report as visible 168 + // unless we're within a display: none tree (like a hidden tab). 169 + styles.parentTreeVisibilityDetector 170 + } 171 + /> 172 + <View 173 + ref={containerRef} 174 + style={[ 175 + styles.contentContainer, 176 + contentContainerStyle, 177 + desktopFixedHeight ? styles.minHeightViewport : null, 178 + pal.border, 179 + ]}> 180 + <Visibility 181 + onVisibleChange={handleAboveTheFoldVisibleChange} 182 + style={[styles.aboveTheFoldDetector, {height: headerOffset}]} 183 + /> 184 + {header} 185 + {(data as Array<ItemT>).map((item, index) => ( 186 + <Row<ItemT> 187 + key={keyExtractor!(item, index)} 188 + item={item} 189 + index={index} 190 + renderItem={renderItem} 191 + extraData={extraData} 192 + /> 193 + ))} 194 + {onEndReached && ( 195 + <Visibility 196 + topMargin={(onEndReachedThreshold ?? 0) * 100 + '%'} 197 + onVisibleChange={onTailVisibilityChange} 198 + /> 199 + )} 200 + {footer} 201 + </View> 202 + </View> 203 + ) 204 + } 205 + 206 + function useResizeObserver( 207 + ref: React.RefObject<Element>, 208 + onResize: undefined | ((w: number, h: number) => void), 209 + ) { 210 + const handleResize = useNonReactiveCallback(onResize ?? (() => {})) 211 + const isActive = !!onResize 212 + React.useEffect(() => { 213 + if (!isActive) { 214 + return 215 + } 216 + const resizeObserver = new ResizeObserver(entries => { 217 + batchedUpdates(() => { 218 + for (let entry of entries) { 219 + const rect = entry.contentRect 220 + handleResize(rect.width, rect.height) 221 + } 222 + }) 223 + }) 224 + const node = ref.current! 225 + resizeObserver.observe(node) 226 + return () => { 227 + resizeObserver.unobserve(node) 228 + } 229 + }, [handleResize, isActive, ref]) 230 + } 231 + 232 + let Row = function RowImpl<ItemT>({ 233 + item, 234 + index, 235 + renderItem, 236 + extraData: _unused, 237 + }: { 238 + item: ItemT 239 + index: number 240 + renderItem: 241 + | null 242 + | undefined 243 + | ((data: {index: number; item: any; separators: any}) => React.ReactNode) 244 + extraData: any 245 + }): React.ReactNode { 246 + if (!renderItem) { 247 + return null 248 + } 249 + return ( 250 + <View style={styles.row}> 251 + {renderItem({item, index, separators: null as any})} 252 + </View> 253 + ) 254 + } 255 + Row = React.memo(Row) 256 + 257 + let Visibility = ({ 258 + topMargin = '0px', 259 + onVisibleChange, 260 + style, 261 + }: { 262 + topMargin?: string 263 + onVisibleChange: (isVisible: boolean) => void 264 + style?: ViewProps['style'] 265 + }): React.ReactNode => { 266 + const tailRef = React.useRef(null) 267 + const isIntersecting = React.useRef(false) 268 + 269 + const handleIntersection = useNonReactiveCallback( 270 + (entries: IntersectionObserverEntry[]) => { 271 + batchedUpdates(() => { 272 + entries.forEach(entry => { 273 + if (entry.isIntersecting !== isIntersecting.current) { 274 + isIntersecting.current = entry.isIntersecting 275 + onVisibleChange(entry.isIntersecting) 276 + } 277 + }) 278 + }) 279 + }, 280 + ) 281 + 282 + React.useEffect(() => { 283 + const observer = new IntersectionObserver(handleIntersection, { 284 + rootMargin: `${topMargin} 0px 0px 0px`, 285 + }) 286 + const tail: Element | null = tailRef.current! 287 + observer.observe(tail) 288 + return () => { 289 + observer.unobserve(tail) 290 + } 291 + }, [handleIntersection, topMargin]) 292 + 293 + return ( 294 + <View ref={tailRef} style={addStyle(styles.visibilityDetector, style)} /> 295 + ) 296 + } 297 + Visibility = React.memo(Visibility) 298 + 299 + export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( 300 + props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>}, 301 + ) => React.ReactElement 302 + 303 + const styles = StyleSheet.create({ 304 + contentContainer: { 305 + borderLeftWidth: 1, 306 + borderRightWidth: 1, 307 + }, 308 + containerScroll: { 309 + width: '100%', 310 + maxWidth: 600, 311 + marginLeft: 'auto', 312 + marginRight: 'auto', 313 + }, 314 + row: { 315 + // @ts-ignore web only 316 + contentVisibility: 'auto', 317 + }, 318 + minHeightViewport: { 319 + // @ts-ignore web only 320 + minHeight: '100vh', 321 + }, 322 + parentTreeVisibilityDetector: { 323 + // @ts-ignore web only 324 + position: 'fixed', 325 + top: 0, 326 + left: 0, 327 + right: 0, 328 + bottom: 0, 329 + }, 330 + aboveTheFoldDetector: { 331 + position: 'absolute', 332 + top: 0, 333 + left: 0, 334 + right: 0, 335 + // Bottom is dynamic. 336 + }, 337 + visibilityDetector: { 338 + pointerEvents: 'none', 339 + zIndex: -1, 340 + }, 341 + })
+35 -2
src/view/com/util/MainScrollProvider.tsx
··· 1 - import React, {useCallback} from 'react' 1 + import React, {useCallback, useEffect} from 'react' 2 + import EventEmitter from 'eventemitter3' 2 3 import {ScrollProvider} from '#/lib/ScrollContext' 3 4 import {NativeScrollEvent} from 'react-native' 4 5 import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' 5 6 import {useShellLayout} from '#/state/shell/shell-layout' 6 - import {isNative} from 'platform/detection' 7 + import {isNative, isWeb} from 'platform/detection' 7 8 import {useSharedValue, interpolate} from 'react-native-reanimated' 8 9 9 10 const WEB_HIDE_SHELL_THRESHOLD = 200 ··· 19 20 const setMode = useSetMinimalShellMode() 20 21 const startDragOffset = useSharedValue<number | null>(null) 21 22 const startMode = useSharedValue<number | null>(null) 23 + 24 + useEffect(() => { 25 + if (isWeb) { 26 + return listenToForcedWindowScroll(() => { 27 + startDragOffset.value = null 28 + startMode.value = null 29 + }) 30 + } 31 + }) 22 32 23 33 const onBeginDrag = useCallback( 24 34 (e: NativeScrollEvent) => { ··· 100 110 </ScrollProvider> 101 111 ) 102 112 } 113 + 114 + const emitter = new EventEmitter() 115 + 116 + if (isWeb) { 117 + const originalScroll = window.scroll 118 + window.scroll = function () { 119 + emitter.emit('forced-scroll') 120 + return originalScroll.apply(this, arguments as any) 121 + } 122 + 123 + const originalScrollTo = window.scrollTo 124 + window.scrollTo = function () { 125 + emitter.emit('forced-scroll') 126 + return originalScrollTo.apply(this, arguments as any) 127 + } 128 + } 129 + 130 + function listenToForcedWindowScroll(listener: () => void) { 131 + emitter.addListener('forced-scroll', listener) 132 + return () => { 133 + emitter.removeListener('forced-scroll', listener) 134 + } 135 + }
+15 -1
src/view/com/util/SimpleViewHeader.tsx
··· 14 14 import {useAnalytics} from 'lib/analytics/analytics' 15 15 import {NavigationProp} from 'lib/routes/types' 16 16 import {useSetDrawerOpen} from '#/state/shell' 17 + import {isWeb} from '#/platform/detection' 17 18 18 19 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 19 20 ··· 47 48 48 49 const Container = isMobile ? View : CenteredView 49 50 return ( 50 - <Container style={[styles.header, isMobile && styles.headerMobile, style]}> 51 + <Container 52 + style={[ 53 + styles.header, 54 + isMobile && styles.headerMobile, 55 + isWeb && styles.headerWeb, 56 + pal.view, 57 + style, 58 + ]}> 51 59 {showBackButton ? ( 52 60 <TouchableOpacity 53 61 testID="viewHeaderDrawerBtn" ··· 88 96 headerMobile: { 89 97 paddingHorizontal: 12, 90 98 paddingVertical: 10, 99 + }, 100 + headerWeb: { 101 + // @ts-ignore web-only 102 + position: 'sticky', 103 + top: 0, 104 + zIndex: 1, 91 105 }, 92 106 backBtn: { 93 107 width: 30,
+2 -1
src/view/com/util/Toast.web.tsx
··· 64 64 65 65 const styles = StyleSheet.create({ 66 66 container: { 67 - position: 'absolute', 67 + // @ts-ignore web only 68 + position: 'fixed', 68 69 left: 20, 69 70 bottom: 20, 70 71 // @ts-ignore web only
+3 -1
src/view/com/util/fab/FABInner.tsx
··· 6 6 import {useSafeAreaInsets} from 'react-native-safe-area-context' 7 7 import {clamp} from 'lib/numbers' 8 8 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 9 + import {isWeb} from '#/platform/detection' 9 10 import Animated from 'react-native-reanimated' 10 11 11 12 export interface FABProps ··· 64 65 borderRadius: 35, 65 66 }, 66 67 outer: { 67 - position: 'absolute', 68 + // @ts-ignore web-only 69 + position: isWeb ? 'fixed' : 'absolute', 68 70 zIndex: 1, 69 71 }, 70 72 inner: {
+3 -1
src/view/screens/PostThread.tsx
··· 25 25 import {CenteredView} from '../com/util/Views' 26 26 import {useComposerControls} from '#/state/shell/composer' 27 27 import {useSession} from '#/state/session' 28 + import {isWeb} from '#/platform/detection' 28 29 29 30 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> 30 31 export function PostThreadScreen({route}: Props) { ··· 112 113 113 114 const styles = StyleSheet.create({ 114 115 prompt: { 115 - position: 'absolute', 116 + // @ts-ignore web-only 117 + position: isWeb ? 'fixed' : 'absolute', 116 118 left: 0, 117 119 right: 0, 118 120 },
+32 -3
src/view/screens/Search/Search.tsx
··· 334 334 tabBarPosition="top" 335 335 onPageSelected={onPageSelected} 336 336 renderTabBar={props => ( 337 - <CenteredView sideBorders style={pal.border}> 337 + <CenteredView 338 + sideBorders 339 + style={[pal.border, pal.view, styles.tabBarContainer]}> 338 340 <TabBar items={SECTIONS_LOGGEDIN} {...props} /> 339 341 </CenteredView> 340 342 )} ··· 375 377 tabBarPosition="top" 376 378 onPageSelected={onPageSelected} 377 379 renderTabBar={props => ( 378 - <CenteredView sideBorders style={pal.border}> 380 + <CenteredView 381 + sideBorders 382 + style={[pal.border, pal.view, styles.tabBarContainer]}> 379 383 <TabBar items={SECTIONS_LOGGEDOUT} {...props} /> 380 384 </CenteredView> 381 385 )} ··· 466 470 setDrawerOpen(true) 467 471 }, [track, setDrawerOpen]) 468 472 const onPressCancelSearch = React.useCallback(() => { 473 + scrollToTopWeb() 469 474 textInput.current?.blur() 470 475 setQuery('') 471 476 setShowAutocompleteResults(false) ··· 473 478 clearTimeout(searchDebounceTimeout.current) 474 479 }, [textInput]) 475 480 const onPressClearQuery = React.useCallback(() => { 481 + scrollToTopWeb() 476 482 setQuery('') 477 483 setShowAutocompleteResults(false) 478 484 }, [setQuery]) 479 485 const onChangeText = React.useCallback( 480 486 async (text: string) => { 487 + scrollToTopWeb() 481 488 setQuery(text) 482 489 483 490 if (text.length > 0) { ··· 506 513 [setQuery, search, setSearchResults], 507 514 ) 508 515 const onSubmit = React.useCallback(() => { 516 + scrollToTopWeb() 509 517 setShowAutocompleteResults(false) 510 518 }, [setShowAutocompleteResults]) 511 519 512 520 const onSoftReset = React.useCallback(() => { 521 + scrollToTopWeb() 513 522 onPressCancelSearch() 514 523 }, [onPressCancelSearch]) 515 524 ··· 526 535 ) 527 536 528 537 return ( 529 - <View style={{flex: 1}}> 538 + <View style={isWeb ? null : {flex: 1}}> 530 539 <CenteredView 531 540 style={[ 532 541 styles.header, 533 542 pal.border, 543 + pal.view, 534 544 isTabletOrDesktop && {paddingTop: 10}, 535 545 ]} 536 546 sideBorders={isTabletOrDesktop}> ··· 661 671 ) 662 672 } 663 673 674 + function scrollToTopWeb() { 675 + if (isWeb) { 676 + window.scrollTo(0, 0) 677 + } 678 + } 679 + 680 + const HEADER_HEIGHT = 50 681 + 664 682 const styles = StyleSheet.create({ 665 683 header: { 666 684 flexDirection: 'row', 667 685 alignItems: 'center', 668 686 paddingHorizontal: 12, 669 687 paddingVertical: 4, 688 + height: HEADER_HEIGHT, 689 + // @ts-ignore web only 690 + position: isWeb ? 'sticky' : '', 691 + top: 0, 692 + zIndex: 1, 670 693 }, 671 694 headerMenuBtn: { 672 695 width: 30, ··· 695 718 }, 696 719 headerCancelBtn: { 697 720 paddingLeft: 10, 721 + }, 722 + tabBarContainer: { 723 + // @ts-ignore web only 724 + position: isWeb ? 'sticky' : '', 725 + top: isWeb ? HEADER_HEIGHT : 0, 726 + zIndex: 1, 698 727 }, 699 728 })
+6 -2
src/view/shell/Composer.web.tsx
··· 5 5 import {useComposerState} from 'state/shell/composer' 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 7 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 8 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 8 9 import { 9 10 EmojiPicker, 10 11 EmojiPickerState, ··· 16 17 const pal = usePalette('default') 17 18 const {isMobile} = useWebMediaQueries() 18 19 const state = useComposerState() 20 + const isActive = !!state 21 + useWebBodyScrollLock(isActive) 19 22 20 23 const [pickerState, setPickerState] = React.useState<EmojiPickerState>({ 21 24 isOpen: false, ··· 40 43 // rendering 41 44 // = 42 45 43 - if (!state) { 46 + if (!isActive) { 44 47 return <View /> 45 48 } 46 49 ··· 75 78 76 79 const styles = StyleSheet.create({ 77 80 mask: { 78 - position: 'absolute', 81 + // @ts-ignore 82 + position: 'fixed', 79 83 top: 0, 80 84 left: 0, 81 85 width: '100%',
+4
src/view/shell/bottom-bar/BottomBarStyles.tsx
··· 12 12 paddingLeft: 5, 13 13 paddingRight: 10, 14 14 }, 15 + bottomBarWeb: { 16 + // @ts-ignore web-only 17 + position: 'fixed', 18 + }, 15 19 ctrl: { 16 20 flex: 1, 17 21 paddingTop: 13,
+1
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 57 57 <Animated.View 58 58 style={[ 59 59 styles.bottomBar, 60 + styles.bottomBarWeb, 60 61 pal.view, 61 62 pal.border, 62 63 {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
+3 -2
src/view/shell/desktop/LeftNav.tsx
··· 442 442 443 443 const styles = StyleSheet.create({ 444 444 leftNav: { 445 - position: 'absolute', 445 + // @ts-ignore web only 446 + position: 'fixed', 446 447 top: 10, 447 448 // @ts-ignore web only 448 - right: 'calc(50vw + 312px)', 449 + left: 'calc(50vw - 300px - 220px - 20px)', 449 450 width: 220, 450 451 // @ts-ignore web only 451 452 maxHeight: 'calc(100vh - 10px)',
+3 -2
src/view/shell/desktop/RightNav.tsx
··· 177 177 178 178 const styles = StyleSheet.create({ 179 179 rightNav: { 180 - position: 'absolute', 180 + // @ts-ignore web only 181 + position: 'fixed', 181 182 // @ts-ignore web only 182 - left: 'calc(50vw + 320px)', 183 + left: 'calc(50vw + 300px + 20px)', 183 184 width: 300, 184 185 maxHeight: '100%', 185 186 overflowY: 'auto',
+11 -9
src/view/shell/index.web.tsx
··· 15 15 import {t} from '@lingui/macro' 16 16 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' 17 17 import {useCloseAllActiveElements} from '#/state/util' 18 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 18 19 import {Outlet as PortalOutlet} from '#/components/Portal' 19 20 20 21 function ShellInner() { ··· 24 25 const navigator = useNavigation<NavigationProp>() 25 26 const closeAllActiveElements = useCloseAllActiveElements() 26 27 28 + useWebBodyScrollLock(isDrawerOpen) 27 29 useAuxClick() 28 30 29 31 useEffect(() => { ··· 34 36 }, [navigator, closeAllActiveElements]) 35 37 36 38 return ( 37 - <View style={[s.hContentRegion, {overflow: 'hidden'}]}> 38 - <View style={s.hContentRegion}> 39 - <ErrorBoundary> 40 - <FlatNavigator /> 41 - </ErrorBoundary> 42 - </View> 39 + <> 40 + <ErrorBoundary> 41 + <FlatNavigator /> 42 + </ErrorBoundary> 43 43 <Composer winHeight={0} /> 44 44 <ModalsContainer /> 45 45 <PortalOutlet /> ··· 55 55 </View> 56 56 </TouchableOpacity> 57 57 )} 58 - </View> 58 + </> 59 59 ) 60 60 } 61 61 ··· 78 78 backgroundColor: colors.black, // TODO 79 79 }, 80 80 drawerMask: { 81 - position: 'absolute', 81 + // @ts-ignore web only 82 + position: 'fixed', 82 83 width: '100%', 83 84 height: '100%', 84 85 top: 0, ··· 87 88 }, 88 89 drawerContainer: { 89 90 display: 'flex', 90 - position: 'absolute', 91 + // @ts-ignore web only 92 + position: 'fixed', 91 93 top: 0, 92 94 left: 0, 93 95 height: '100%',
+1 -1
web/index.html
··· 37 37 } 38 38 39 39 html { 40 - scroll-behavior: smooth; 41 40 /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */ 42 41 -webkit-text-size-adjust: 100%; 43 42 height: calc(100% + env(safe-area-inset-top)); 43 + scrollbar-gutter: stable; 44 44 } 45 45 46 46 /* Remove autofill styles on Webkit */
+7
yarn.lock
··· 7525 7525 dependencies: 7526 7526 "@types/react" "*" 7527 7527 7528 + "@types/react-dom@^18.2.18": 7529 + version "18.2.18" 7530 + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" 7531 + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== 7532 + dependencies: 7533 + "@types/react" "*" 7534 + 7528 7535 "@types/react-responsive@^8.0.5": 7529 7536 version "8.0.5" 7530 7537 resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a"