diff --git a/apps/web/package.json b/apps/web/package.json index 35033738..7ba9c395 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 7eec9bc8..4e0195bd 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -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(null) - - const { shouldBlurTop, shouldBlurBottom } = useBlur({ - ref: containerRef, - }) - const appState = useAppState() const appDispatch = useAppDispatch() @@ -45,6 +45,32 @@ function App() { } }, [appState.channelId]) + const topbarRef = useRef(null) + const contentRef = useRef(null) + const composerRef = useRef(null) + + useResizeObserver({ + ref: topbarRef, + onResize({ height }) { + updateProperty('--topbar-height', height) + }, + }) + + useResizeObserver({ + ref: composerRef, + onResize({ height }) { + updateProperty('--composer-height', height) + }, + }) + + const scrollPosition = useScrollPosition({ + ref: contentRef, + }) + + useEffect(() => { + contentRef.current.scrollTop = contentRef.current.scrollHeight + }, [selectedChannel]) + return (
- setShowMembers(show => !show)} - /> +
+ setShowMembers(show => !show)} + /> +
-
+
-
+
+ +
+ {scrollPosition !== 'bottom' && (
- +
- -
+ )} +
diff --git a/apps/web/src/hooks/use-scroll-position.tsx b/apps/web/src/hooks/use-scroll-position.tsx new file mode 100644 index 00000000..395c0887 --- /dev/null +++ b/apps/web/src/hooks/use-scroll-position.tsx @@ -0,0 +1,49 @@ +import { useEffect, useRef, useState } from 'react' + +type Position = 'top' | 'middle' | 'bottom' + +type Options = { + ref: React.RefObject +} + +export function useScrollPosition(options: Options) { + const { ref } = options + + const [position, setPosition] = useState('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 +} diff --git a/apps/web/styles/app.css b/apps/web/styles/app.css index 9b545c6d..f1e960bf 100644 --- a/apps/web/styles/app.css +++ b/apps/web/styles/app.css @@ -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; } diff --git a/packages/components/hooks/index.ts b/packages/components/hooks/index.ts index 0b801443..1907e5ce 100644 --- a/packages/components/hooks/index.ts +++ b/packages/components/hooks/index.ts @@ -1,3 +1 @@ -export { useBlur } from './use-blur' export { useImageUpload } from './use-image-uploader' -export { useThrottle } from './use-throttle' diff --git a/packages/components/hooks/use-blur.ts b/packages/components/hooks/use-blur.ts deleted file mode 100644 index 197ce512..00000000 --- a/packages/components/hooks/use-blur.ts +++ /dev/null @@ -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 - 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 } diff --git a/packages/components/hooks/use-throttle.ts b/packages/components/hooks/use-throttle.ts deleted file mode 100644 index 4ff2dae8..00000000 --- a/packages/components/hooks/use-throttle.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMemo } from 'react' - -const throttle = ( - 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 = ( - cb: (...args: Args) => void, - cooldown: number -) => { - return useMemo(() => throttle(cb, cooldown), [cb, cooldown]) -} - -export { useThrottle } diff --git a/packages/components/src/anchor-actions/index.tsx b/packages/components/src/anchor-actions/index.tsx index fabc5a8d..d04e0ce8 100644 --- a/packages/components/src/anchor-actions/index.tsx +++ b/packages/components/src/anchor-actions/index.tsx @@ -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 ( - {scrolled && } - {scrolled && } + + ) } diff --git a/yarn.lock b/yarn.lock index 8401d19d..b48e9863 100644 --- a/yarn.lock +++ b/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"