From 522a1cdbaf0f478fb23a9f770b9caf732ea13803 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 29 Oct 2019 17:11:07 +0300 Subject: Hide header on scroll (mobile only) --- vnext/src/App.js | 115 +++++++++++++++++++---------------------- vnext/src/index.css | 4 ++ vnext/src/ui/Feeds.js | 137 ++++++++++++++++++++++++++----------------------- vnext/src/ui/Header.js | 134 ++++++++++++++++++++++++----------------------- 4 files changed, 197 insertions(+), 193 deletions(-) (limited to 'vnext/src') diff --git a/vnext/src/App.js b/vnext/src/App.js index f39875c1..011e5cbd 100644 --- a/vnext/src/App.js +++ b/vnext/src/App.js @@ -1,5 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'; +import { useScroll, useRafState } from 'react-use'; import qs from 'qs'; import svg4everybody from 'svg4everybody'; @@ -10,18 +11,23 @@ import { Friends, Readers } from './ui/Users'; import Settings from './ui/Settings'; import Contacts from './ui/Contacts'; import Chat from './ui/Chat'; +import Header from './ui/Header'; import Post from './ui/Post'; import Thread from './ui/Thread'; import Login from './ui/Login'; -import { UserLink } from './ui/UserInfo'; -import SearchBox from './ui/SearchBox'; import cookie from 'react-cookies'; import { me } from './api'; +const elClassHidden = 'header--hidden'; + +const elClassFull = 'content--full'; + export default function App() { + let contentRef = useRef(null); + useEffect(() => { svg4everybody(); }, []); @@ -49,6 +55,48 @@ export default function App() { }); }; + const [scrollState, setScrollState] = useRafState({ + hidden: false, + bottom: false, + prevScroll: 0 + }); + + let { x, y } = useScroll(contentRef); + + useEffect(() => { + let dHeight = contentRef.current.scrollHeight; + let wHeight = contentRef.current.clientHeight; + setScrollState((scrollState) => { + let wScrollDiff = scrollState.prevScroll - y; + let hidden = scrollState.hidden; + let bottom = scrollState.bottom; + if (y <= 0) { + // scrolled to the very top; element sticks to the top + hidden = false; + bottom = false; + } else if ((wScrollDiff > 0) && hidden) { + // scrolled up; element slides in + hidden = false; + bottom = false; + } else if (wScrollDiff < 0) { + // scrolled down + if ((y + wHeight) >= dHeight && hidden) { + // scrolled to the very bottom; element slides in + hidden = false; + bottom = true; + } else { + // scrolled down; element slides out + hidden = true; + bottom = false; + } + } + return { + hidden: hidden, + bottom: bottom, + prevScroll: y + }; + }); + }, [x, y, setScrollState]); const [hash, setHash] = useState(cookie.load('hash')); const [eventSource, setEventSource] = useState({}); @@ -111,66 +159,9 @@ export default function App() { return ( <> - +
-
+
} /> } /> diff --git a/vnext/src/index.css b/vnext/src/index.css index a371ae55..109eb678 100644 --- a/vnext/src/index.css +++ b/vnext/src/index.css @@ -167,6 +167,10 @@ hr { overflow: auto; } +.content--full { + margin-top: 0 !important; +} + #footer { display: none; background: var(--background-color); diff --git a/vnext/src/ui/Feeds.js b/vnext/src/ui/Feeds.js index 8c79f779..e687f1e2 100644 --- a/vnext/src/ui/Feeds.js +++ b/vnext/src/ui/Feeds.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import qs from 'qs'; @@ -23,9 +23,9 @@ import { getMessages } from '../api'; * @property {import('history').History} history * @property {import('history').Location} location * @property {import('react-router').match} match - * @property {string} search + * @property {string=} search * @property {import('../api').SecureUser} visitor - * @property {import('../api').Message[]} msgs + * @property {import('../api').Message[]=} msgs */ /** @@ -101,84 +101,95 @@ export function Home(props) { } /** - * @param {{ - authRequired?: boolean, - visitor: import('../api').SecureUser, - history: import('history').History, - location: import('history').Location, - msgs: import('../api').Message[], - query: Query -}} props + * @typedef {Object} FeedState + * @property authRequired?: boolean + * @property visitor: import('../api').SecureUser + * @property history: import('history').History + * @property location: import('history').Location + * @property msgs: import('../api').Message[] + * @property query: Query + */ + +/** + * @param {FeedState} props */ function Feed(props) { - const [msgs, setMsgs] = useState([]); - const [loading, setLoading] = useState(true); - const [nextpage, setNextpage] = useState(null); - const [error, setError] = useState(false); + const [state, setState] = useState({ + history: props.history, + authRequired: props.authRequired, + query: props.query, + hash: props.visitor.hash, + filter: props.location.search.substring(1), + msgs: [], + loading: true, + nextpage: null, + error: false, + tag: '' + }); + + const stateRef = useRef(state); useEffect(() => { - let loadMessages = (hash = '', filter = '') => { - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - setMsgs([]); - setLoading(true); - const filterParams = qs.parse(filter); - let params = Object.assign({}, filterParams || {}, props.query.search || {}); - let url = props.query.baseUrl; - if (hash) { - params.hash = hash; - } - if (!params.hash && props.authRequired) { - props.history.push('/'); - } - getMessages(url, params) - .then(response => { - const { data } = response; - const { pageParam } = props.query; - const lastMessage = data.slice(-1)[0] || {}; - const nextpage = getPageParam(pageParam, lastMessage, filterParams); - setMsgs(data); - setLoading(false); - setNextpage(nextpage); - }).catch(ex => { - setError(true); - }); + let getPageParam = (pageParam, lastMessage, filterParams) => { + const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : moment.utc(lastMessage.updated).valueOf(); + let newFilter = { ...filterParams }; + newFilter[pageParam] = pageValue; + return `?${qs.stringify(newFilter)}`; }; - loadMessages(props.visitor.hash, props.location.search.substring(1)); - }, [props]); - - let getPageParam = (pageParam, lastMessage, filterParams) => { - const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : moment.utc(lastMessage.updated).valueOf(); - let newFilter = { ...filterParams }; - newFilter[pageParam] = pageValue; - return `?${qs.stringify(newFilter)}`; - }; - const { tag } = qs.parse(location.search.substring(1)) || {}; - const nodes = ( - <> + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + const filterParams = qs.parse(stateRef.current.filter); + let params = Object.assign({}, filterParams || {}, stateRef.current.query.search || {}); + let url = stateRef.current.query.baseUrl; + if (stateRef.current.hash) { + params.hash = stateRef.current.hash; + } + if (!params.hash && stateRef.current.authRequired) { + stateRef.current.history.push('/'); + } + getMessages(url, params) + .then(response => { + const { data } = response; + const { pageParam } = stateRef.current.query; + const lastMessage = data.slice(-1)[0] || {}; + const nextpage = getPageParam(pageParam, lastMessage, filterParams); + setState({ + ...stateRef.current, + msgs: data, + loading: false, + nextpage: nextpage, + tag: qs.parse(location.search.substring(1))['tag'] || '' + }); + }).catch(ex => { + setState({ + ...stateRef.current, + error: true + }); + }); + }, []); + return (state.msgs.length > 0 ? ( +
{ - tag && ( + state.tag && (

- - ← All posts with tag {tag} + + ← All posts with tag {state.tag}

) } { - msgs.map(msg => + state.msgs.map(msg => ) } { - msgs.length >= 20 && ( + state.msgs.length >= 20 && (

- Next → + Next →

) } - +
+ ) : state.error ?
error
: state.loading ?
:
No more messages
); - return msgs.length > 0 ? ( -
{nodes}
- ) : error ?
error
: loading ?
:
No more messages
; } diff --git a/vnext/src/ui/Header.js b/vnext/src/ui/Header.js index 2d042bfe..a7663dd3 100644 --- a/vnext/src/ui/Header.js +++ b/vnext/src/ui/Header.js @@ -1,73 +1,71 @@ -import React, { useEffect, useCallback, useRef } from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; +import React, { memo } from 'react'; +import { Link, withRouter } from 'react-router-dom'; -const elClassHidden = 'header--hidden'; +import Icon from './Icon'; +import { UserLink } from './UserInfo'; +import SearchBox from './SearchBox'; -const header = document.getElementById('header'); +function Header({ visitor, search, className }) { + return ( + + ); } -Header.propTypes = { - children: PropTypes.node -}; +export default memo(withRouter(Header)); -- cgit v1.2.3