aboutsummaryrefslogtreecommitdiff
path: root/vnext/src
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2019-10-29 17:11:07 +0300
committerGravatar Vitaly Takmazov2023-01-13 10:37:55 +0300
commit522a1cdbaf0f478fb23a9f770b9caf732ea13803 (patch)
tree2baf7497f4c4717ee423c227e2ba24a22b44c27c /vnext/src
parent713566a435fea6c00cbd2e37d7c8c2a54ef2895d (diff)
Hide header on scroll (mobile only)
Diffstat (limited to 'vnext/src')
-rw-r--r--vnext/src/App.js115
-rw-r--r--vnext/src/index.css4
-rw-r--r--vnext/src/ui/Feeds.js137
-rw-r--r--vnext/src/ui/Header.js134
4 files changed, 197 insertions, 193 deletions
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 (
<Router>
<>
- <div id="header">
- <div id="header_wrapper">
- {
- visitor.uid < 0 ?
- <>
- <div id="logo"><a href="/" /></div>
- <nav id="global">
- <a href="/">Loading...</a>
- </nav>
- </>
- : visitor.uid > 0 ?
- <UserLink user={visitor} />
- : <div id="logo">
- <Link to="/">Juick</Link>
- </div>
- }
- {
- visitor.uid >= 0 &&
- <>
- <div id="search" className="desktop">
- <SearchBox pathname="/discover" onSearch={search} />
- </div>
- <nav id="global">
- {visitor.uid > 0 ?
- <Link to={{ pathname: '/' }}>
- <Icon name="ei-bell" size="s" /><span className="desktop">Discuss</span>
- {
- visitor.unreadCount &&
- <span className="badge">{visitor.unreadCount}</span>
- }
- </Link>
- :
- <Link to='/?media=1' rel="nofollow">
- <Icon name="ei-camera" size="s" />
- <span className="desktop">Photos</span>
- </Link>
- }
- <Link to={{ pathname: '/discover' }} rel="nofollow">
- <Icon name="ei-search" size="s" />
- <span className="desktop">Discover</span>
- </Link>
-
- {visitor.uid > 0 ?
- <Link to={{ pathname: '/post' }}>
- <Icon name="ei-pencil" size="s" />
- <span className="desktop">Post</span>
- </Link>
- :
- <Link to={{ pathname: '/login', state: { retpath: window.location.pathname } }}>
- <Icon name="ei-user" size="s" />
- <span className="desktop">Login</span>
- </Link>
- }
- </nav>
- </>
- }
- </div>
- </div>
+ <Header visitor={visitor} className={scrollState.hidden ? elClassHidden : ''} />
<div id="wrapper">
- <section id="content">
+ <section id="content" ref={contentRef} className={scrollState.hidden || scrollState.bottom ? elClassFull : ''}>
<Switch>
<Route exact path="/" render={(props) => <Discussions visitor={visitor} {...props} />} />
<Route exact path="/home" render={(props) => <Home visitor={visitor} {...props} />} />
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 ? (
+ <div className="msgs">
{
- tag && (
+ state.tag && (
<p className="page">
- <Link to={{ pathname: `/tag/${tag}` }}>
- <span>← All posts with tag&nbsp;</span><b>{tag}</b>
+ <Link to={{ pathname: `/tag/${state.tag}` }}>
+ <span>← All posts with tag&nbsp;</span><b>{state.tag}</b>
</Link>
</p>
)
}
{
- msgs.map(msg =>
+ state.msgs.map(msg =>
<Message key={msg.mid} data={msg} visitor={props.visitor} />)
}
{
- msgs.length >= 20 && (
+ state.msgs.length >= 20 && (
<p className="page">
- <Link to={{ pathname: props.location.pathname, search: nextpage }} rel="prev">Next →</Link>
+ <Link to={{ pathname: props.location.pathname, search: state.nextpage }} rel="prev">Next →</Link>
</p>
)
}
- </>
+ </div>
+ ) : state.error ? <div>error</div> : state.loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div>
);
- return msgs.length > 0 ? (
- <div className="msgs">{nodes}</div>
- ) : error ? <div>error</div> : loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div>;
}
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 (
+ <div id="header" className={className}>
+ <div id="header_wrapper">
+ {
+ visitor.uid < 0 ?
+ <>
+ <div id="logo"><a href="/" /></div>
+ <nav id="global">
+ <a href="/">Loading...</a>
+ </nav>
+ </>
+ : visitor.uid > 0 ?
+ <UserLink user={visitor} />
+ : <div id="logo">
+ <Link to="/">Juick</Link>
+ </div>
+ }
+ {
+ visitor.uid >= 0 &&
+ <>
+ <div id="search" className="desktop">
+ <SearchBox pathname="/discover" onSearch={search} />
+ </div>
+ <nav id="global">
+ {visitor.uid > 0 ?
+ <Link to={{ pathname: '/' }}>
+ <Icon name="ei-bell" size="s" /><span className="desktop">Discuss</span>
+ {
+ visitor.unreadCount &&
+ <span className="badge">{visitor.unreadCount}</span>
+ }
+ </Link>
+ :
+ <Link to='/?media=1' rel="nofollow">
+ <Icon name="ei-camera" size="s" />
+ <span className="desktop">Photos</span>
+ </Link>
+ }
+ <Link to={{ pathname: '/discover' }} rel="nofollow">
+ <Icon name="ei-search" size="s" />
+ <span className="desktop">Discover</span>
+ </Link>
-export default function Header({ children }) {
- let dHeight = useRef(0);
- let wHeight = useRef(0);
- let wScrollCurrent = useRef(0);
- let wScrollBefore = useRef(0);
- let wScrollDiff = useRef(0);
-
- /**
- * @param {number} delay
- * @param {{ (): void; apply?: any; }} fn
- */
- let throttle = (delay, fn) => {
- var last, deferTimer;
- return function () {
- var context = this, args = arguments, now = +new Date;
- if (last && now < last + delay) {
- clearTimeout(deferTimer);
- deferTimer = setTimeout(
- function () {
- last = now;
- fn.apply(context, args);
- },
- delay);
- } else {
- last = now;
- fn.apply(context, args);
- }
- };
- };
- let updateHeader = useCallback(() => {
- dHeight.current = document.body.offsetHeight;
- wHeight.current = window.innerHeight;
- wScrollCurrent.current = window.pageYOffset;
- wScrollDiff.current = wScrollBefore.current - wScrollCurrent.current;
-
- if (wScrollCurrent.current <= 0) {
- // scrolled to the very top; element sticks to the top
- header.classList.remove(elClassHidden);
- } else if (wScrollDiff.current > 0 && header.classList.contains(elClassHidden)) {
- // scrolled up; element slides in
- header.classList.remove(elClassHidden);
- } else if (wScrollDiff.current < 0) {
- // scrolled down
- if (wScrollCurrent.current + wHeight.current >= dHeight.current && header.classList.contains(elClassHidden)) {
- // scrolled to the very bottom; element slides in
- header.classList.remove(elClassHidden);
- } else {
- // scrolled down; element slides out
- header.classList.add(elClassHidden);
- }
- }
- wScrollBefore.current = wScrollCurrent.current;
- }, []);
-
- useEffect(() => {
- window.addEventListener('scroll', () => (!window.requestAnimationFrame)
- ? throttle(250, updateHeader)
- : window.requestAnimationFrame(updateHeader), false);
- }, [updateHeader]);
- return ReactDOM.createPortal(children, header);
+ {visitor.uid > 0 ?
+ <Link to={{ pathname: '/post' }}>
+ <Icon name="ei-pencil" size="s" />
+ <span className="desktop">Post</span>
+ </Link>
+ :
+ <Link to={{ pathname: '/login', state: { retpath: window.location.pathname } }}>
+ <Icon name="ei-user" size="s" />
+ <span className="desktop">Login</span>
+ </Link>
+ }
+ </nav>
+ </>
+ }
+ </div>
+ </div>
+ );
}
-Header.propTypes = {
- children: PropTypes.node
-};
+export default memo(withRouter(Header));