Calculate topbar and composer height dynamically (#362)
* dynamically calculate topbar and composer height * simplify scroll position calculation * update AnchorActions component * update initial content scroll position * add if condition
This commit is contained in:
parent
dda3cf1dfe
commit
00f97e4d3b
|
@ -15,7 +15,8 @@
|
|||
"@tamagui/core": "1.7.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-native-web": "^0.18.12"
|
||||
"react-native-web": "^0.18.12",
|
||||
"use-resize-observer": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tamagui/vite-plugin": "1.7.7",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import {
|
||||
AnchorActions,
|
||||
|
@ -11,7 +11,9 @@ import {
|
|||
useAppDispatch,
|
||||
useAppState,
|
||||
} from '@status-im/components'
|
||||
import { useBlur } from '@status-im/components/hooks'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
|
||||
import { useScrollPosition } from './hooks/use-scroll-position'
|
||||
|
||||
const COMMUNITY = {
|
||||
name: 'Rarible',
|
||||
|
@ -22,15 +24,13 @@ const COMMUNITY = {
|
|||
'https://images.unsplash.com/photo-1574786527860-f2e274867c91?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1764&q=80',
|
||||
}
|
||||
|
||||
const updateProperty = (property: string, value: number) => {
|
||||
document.documentElement.style.setProperty(property, `${value}px`)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [showMembers, setShowMembers] = useState(false)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { shouldBlurTop, shouldBlurBottom } = useBlur({
|
||||
ref: containerRef,
|
||||
})
|
||||
|
||||
const appState = useAppState()
|
||||
const appDispatch = useAppDispatch()
|
||||
|
||||
|
@ -45,6 +45,32 @@ function App() {
|
|||
}
|
||||
}, [appState.channelId])
|
||||
|
||||
const topbarRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const composerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useResizeObserver<HTMLDivElement>({
|
||||
ref: topbarRef,
|
||||
onResize({ height }) {
|
||||
updateProperty('--topbar-height', height)
|
||||
},
|
||||
})
|
||||
|
||||
useResizeObserver<HTMLDivElement>({
|
||||
ref: composerRef,
|
||||
onResize({ height }) {
|
||||
updateProperty('--composer-height', height)
|
||||
},
|
||||
})
|
||||
|
||||
const scrollPosition = useScrollPosition({
|
||||
ref: contentRef,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
||||
}, [selectedChannel])
|
||||
|
||||
return (
|
||||
<div id="app">
|
||||
<div id="sidebar" style={{ zIndex: 200 }}>
|
||||
|
@ -58,23 +84,28 @@ function App() {
|
|||
</div>
|
||||
|
||||
<main id="main">
|
||||
<Topbar
|
||||
blur={shouldBlurTop}
|
||||
channel={selectedChannel}
|
||||
showMembers={showMembers}
|
||||
onMembersPress={() => setShowMembers(show => !show)}
|
||||
/>
|
||||
<div id="topbar" ref={topbarRef}>
|
||||
<Topbar
|
||||
blur={scrollPosition !== 'top'}
|
||||
channel={selectedChannel}
|
||||
showMembers={showMembers}
|
||||
onMembersPress={() => setShowMembers(show => !show)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="content" ref={containerRef}>
|
||||
<div id="content" ref={contentRef}>
|
||||
<div id="messages">
|
||||
<Messages />
|
||||
</div>
|
||||
<div id="composer">
|
||||
</div>
|
||||
|
||||
<div id="composer" ref={composerRef}>
|
||||
{scrollPosition !== 'bottom' && (
|
||||
<div id="anchor-actions">
|
||||
<AnchorActions scrolled={shouldBlurBottom} />
|
||||
<AnchorActions />
|
||||
</div>
|
||||
<Composer blur={shouldBlurBottom} />
|
||||
</div>
|
||||
)}
|
||||
<Composer blur={scrollPosition !== 'bottom'} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Position = 'top' | 'middle' | 'bottom'
|
||||
|
||||
type Options = {
|
||||
ref: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export function useScrollPosition(options: Options) {
|
||||
const { ref } = options
|
||||
|
||||
const [position, setPosition] = useState<Position>('bottom')
|
||||
const positionRef = useRef(position)
|
||||
|
||||
// Using ref for storing position because don't want to recreate the event listener
|
||||
positionRef.current = position
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!node) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = node
|
||||
|
||||
if (scrollTop === 0) {
|
||||
setPosition('top')
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollTop + clientHeight === scrollHeight) {
|
||||
setPosition('bottom')
|
||||
return
|
||||
}
|
||||
|
||||
if (positionRef.current !== 'middle') {
|
||||
setPosition('middle')
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
return () => {
|
||||
node.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return position
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
:root {
|
||||
--topbar-height: 56px;
|
||||
--composer-height: 100px;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
|
@ -19,37 +24,30 @@ body,
|
|||
|
||||
#main {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 96px 1fr 100px; /* 56px 1fr 100px without pinned messages */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* #main,
|
||||
#sidebar,
|
||||
#members,
|
||||
#main > div {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
} */
|
||||
|
||||
.composer-input::placeholder {
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
overflow: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#content {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
padding: 40px 0px 0px 0px;
|
||||
padding-top: var(--topbar-height);
|
||||
padding-bottom: var(--composer-height);
|
||||
height: 100vh;
|
||||
margin-top: -96px; /* -56px without pinned messages */
|
||||
}
|
||||
|
||||
#messages {
|
||||
padding: 32px 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#anchor-actions {
|
||||
|
@ -59,9 +57,8 @@ body,
|
|||
}
|
||||
|
||||
#composer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
inset: auto 0 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
export { useBlur } from './use-blur'
|
||||
export { useImageUpload } from './use-image-uploader'
|
||||
export { useThrottle } from './use-throttle'
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useThrottle } from './use-throttle'
|
||||
|
||||
interface UseBlurReturn {
|
||||
shouldBlurBottom: boolean
|
||||
shouldBlurTop: boolean
|
||||
}
|
||||
|
||||
type UseBlurProps = {
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
marginBlurBottom?: number
|
||||
heightTop?: number
|
||||
throttle?: number
|
||||
}
|
||||
|
||||
const useBlur = (props: UseBlurProps): UseBlurReturn => {
|
||||
const {
|
||||
marginBlurBottom = 32,
|
||||
heightTop = 96,
|
||||
throttle = 100,
|
||||
ref,
|
||||
} = props || {}
|
||||
|
||||
const [shouldBlurTop, setShouldBlurTop] = useState(false)
|
||||
const [shouldBlurBottom, setShouldBlurBottom] = useState(false)
|
||||
|
||||
const handleScroll = useThrottle(() => {
|
||||
const scrollPosition = ref.current!.scrollTop
|
||||
const elementHeight = ref.current!.clientHeight
|
||||
const scrollHeight = ref.current?.scrollHeight || 0
|
||||
|
||||
if (scrollPosition >= heightTop) {
|
||||
setShouldBlurTop(true)
|
||||
}
|
||||
|
||||
if (scrollPosition < heightTop) {
|
||||
setShouldBlurTop(false)
|
||||
}
|
||||
|
||||
if (scrollPosition < scrollHeight - (elementHeight + marginBlurBottom)) {
|
||||
setShouldBlurBottom(true)
|
||||
}
|
||||
|
||||
if (scrollPosition >= scrollHeight - (elementHeight + marginBlurBottom)) {
|
||||
setShouldBlurBottom(false)
|
||||
}
|
||||
}, throttle)
|
||||
|
||||
useEffect(() => {
|
||||
const element = props.ref
|
||||
if (!element.current) {
|
||||
throw new Error('useBlur ref not set correctly')
|
||||
}
|
||||
|
||||
element.current!.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
element.current!.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [handleScroll, props.ref])
|
||||
|
||||
return { shouldBlurBottom, shouldBlurTop }
|
||||
}
|
||||
|
||||
export { useBlur }
|
|
@ -1,38 +0,0 @@
|
|||
import { useMemo } from 'react'
|
||||
|
||||
const throttle = <Args extends unknown[]>(
|
||||
fn: (...args: Args) => void,
|
||||
cooldown: number
|
||||
) => {
|
||||
let lastArgs: Args | undefined
|
||||
|
||||
const run = () => {
|
||||
if (lastArgs) {
|
||||
fn(...lastArgs)
|
||||
lastArgs = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const throttled = (...args: Args) => {
|
||||
const isOnCooldown = !!lastArgs
|
||||
|
||||
lastArgs = args
|
||||
|
||||
if (isOnCooldown) {
|
||||
return
|
||||
}
|
||||
|
||||
window.setTimeout(run, cooldown)
|
||||
}
|
||||
|
||||
return throttled
|
||||
}
|
||||
|
||||
const useThrottle = <Args extends unknown[]>(
|
||||
cb: (...args: Args) => void,
|
||||
cooldown: number
|
||||
) => {
|
||||
return useMemo(() => throttle(cb, cooldown), [cb, cooldown])
|
||||
}
|
||||
|
||||
export { useThrottle }
|
|
@ -2,17 +2,13 @@ import { Stack } from 'tamagui'
|
|||
|
||||
import { DynamicButton } from '../dynamic-button'
|
||||
|
||||
type Props = {
|
||||
scrolled: boolean
|
||||
}
|
||||
|
||||
const AnchorActions = (props: Props) => {
|
||||
const { scrolled } = props
|
||||
// type Props = {}
|
||||
|
||||
const AnchorActions = () => {
|
||||
return (
|
||||
<Stack flexDirection="row" space={8}>
|
||||
{scrolled && <DynamicButton type="mention" count={1} />}
|
||||
{scrolled && <DynamicButton type="notification" count={0} />}
|
||||
<DynamicButton type="mention" count={1} />
|
||||
<DynamicButton type="notification" count={0} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -2648,6 +2648,11 @@
|
|||
"@jridgewell/resolve-uri" "3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||
|
||||
"@juggle/resize-observer@^3.3.1":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@leichtgewicht/base64-codec@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/base64-codec/-/base64-codec-1.0.0.tgz#f5d730be74bd41564cf23c6d332044ae88fc31d8"
|
||||
|
@ -6359,7 +6364,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-dom@18.0.11", "@types/react-dom@^18.0.11":
|
||||
"@types/react-dom@^18.0.11":
|
||||
version "18.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
|
||||
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
|
||||
|
@ -6373,7 +6378,7 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@18.0.28", "@types/react@>=16", "@types/react@^18.0.28":
|
||||
"@types/react@*", "@types/react@>=16", "@types/react@^18.0.28":
|
||||
version "18.0.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
|
||||
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
|
||||
|
@ -16685,6 +16690,13 @@ use-latest-callback@^0.1.5:
|
|||
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
|
||||
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
|
||||
|
||||
use-resize-observer@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c"
|
||||
integrity sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==
|
||||
dependencies:
|
||||
"@juggle/resize-observer" "^3.3.1"
|
||||
|
||||
use-sidecar@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
||||
|
|
Loading…
Reference in New Issue