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",
|
"@tamagui/core": "1.7.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@tamagui/vite-plugin": "1.7.7",
|
"@tamagui/vite-plugin": "1.7.7",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnchorActions,
|
AnchorActions,
|
||||||
|
@ -11,7 +11,9 @@ import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
useAppState,
|
useAppState,
|
||||||
} from '@status-im/components'
|
} 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 = {
|
const COMMUNITY = {
|
||||||
name: 'Rarible',
|
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',
|
'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() {
|
function App() {
|
||||||
const [showMembers, setShowMembers] = useState(false)
|
const [showMembers, setShowMembers] = useState(false)
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const { shouldBlurTop, shouldBlurBottom } = useBlur({
|
|
||||||
ref: containerRef,
|
|
||||||
})
|
|
||||||
|
|
||||||
const appState = useAppState()
|
const appState = useAppState()
|
||||||
const appDispatch = useAppDispatch()
|
const appDispatch = useAppDispatch()
|
||||||
|
|
||||||
|
@ -45,6 +45,32 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [appState.channelId])
|
}, [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 (
|
return (
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="sidebar" style={{ zIndex: 200 }}>
|
<div id="sidebar" style={{ zIndex: 200 }}>
|
||||||
|
@ -58,23 +84,28 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<Topbar
|
<div id="topbar" ref={topbarRef}>
|
||||||
blur={shouldBlurTop}
|
<Topbar
|
||||||
channel={selectedChannel}
|
blur={scrollPosition !== 'top'}
|
||||||
showMembers={showMembers}
|
channel={selectedChannel}
|
||||||
onMembersPress={() => setShowMembers(show => !show)}
|
showMembers={showMembers}
|
||||||
/>
|
onMembersPress={() => setShowMembers(show => !show)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="content" ref={containerRef}>
|
<div id="content" ref={contentRef}>
|
||||||
<div id="messages">
|
<div id="messages">
|
||||||
<Messages />
|
<Messages />
|
||||||
</div>
|
</div>
|
||||||
<div id="composer">
|
</div>
|
||||||
|
|
||||||
|
<div id="composer" ref={composerRef}>
|
||||||
|
{scrollPosition !== 'bottom' && (
|
||||||
<div id="anchor-actions">
|
<div id="anchor-actions">
|
||||||
<AnchorActions scrolled={shouldBlurBottom} />
|
<AnchorActions />
|
||||||
</div>
|
</div>
|
||||||
<Composer blur={shouldBlurBottom} />
|
)}
|
||||||
</div>
|
<Composer blur={scrollPosition !== 'bottom'} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
|
@ -19,37 +24,30 @@ body,
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 96px 1fr 100px; /* 56px 1fr 100px without pinned messages */
|
|
||||||
height: 100vh;
|
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 {
|
#sidebar {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 40px 0px 0px 0px;
|
padding-top: var(--topbar-height);
|
||||||
|
padding-bottom: var(--composer-height);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin-top: -96px; /* -56px without pinned messages */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages {
|
#messages {
|
||||||
padding: 32px 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#anchor-actions {
|
#anchor-actions {
|
||||||
|
@ -59,9 +57,8 @@ body,
|
||||||
}
|
}
|
||||||
|
|
||||||
#composer {
|
#composer {
|
||||||
position: sticky;
|
position: absolute;
|
||||||
bottom: 0;
|
inset: auto 0 0;
|
||||||
left: 0;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
export { useBlur } from './use-blur'
|
|
||||||
export { useImageUpload } from './use-image-uploader'
|
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'
|
import { DynamicButton } from '../dynamic-button'
|
||||||
|
|
||||||
type Props = {
|
// type Props = {}
|
||||||
scrolled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnchorActions = (props: Props) => {
|
|
||||||
const { scrolled } = props
|
|
||||||
|
|
||||||
|
const AnchorActions = () => {
|
||||||
return (
|
return (
|
||||||
<Stack flexDirection="row" space={8}>
|
<Stack flexDirection="row" space={8}>
|
||||||
{scrolled && <DynamicButton type="mention" count={1} />}
|
<DynamicButton type="mention" count={1} />
|
||||||
{scrolled && <DynamicButton type="notification" count={0} />}
|
<DynamicButton type="notification" count={0} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -2648,6 +2648,11 @@
|
||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@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":
|
"@leichtgewicht/base64-codec@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/base64-codec/-/base64-codec-1.0.0.tgz#f5d730be74bd41564cf23c6d332044ae88fc31d8"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
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"
|
version "18.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
|
||||||
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
|
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
|
||||||
|
@ -6373,7 +6378,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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"
|
version "18.0.28"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
|
||||||
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
|
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"
|
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
|
||||||
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
|
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:
|
use-sidecar@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
||||||
|
|
Loading…
Reference in New Issue