diff options
Diffstat (limited to 'vnext/src')
-rw-r--r-- | vnext/src/App.js | 80 | ||||
-rw-r--r-- | vnext/src/ui/Chat.js | 15 | ||||
-rw-r--r-- | vnext/src/ui/Feeds.js | 111 | ||||
-rw-r--r-- | vnext/src/ui/Header.js | 20 | ||||
-rw-r--r-- | vnext/src/ui/Login.js | 12 | ||||
-rw-r--r-- | vnext/src/ui/Post.js | 7 | ||||
-rw-r--r-- | vnext/src/ui/SearchBox.js | 10 | ||||
-rw-r--r-- | vnext/src/ui/Thread.js | 16 | ||||
-rw-r--r-- | vnext/src/ui/UserInfo.js | 28 | ||||
-rw-r--r-- | vnext/src/ui/Users.js | 25 |
10 files changed, 175 insertions, 149 deletions
diff --git a/vnext/src/App.js b/vnext/src/App.js index 4c1e72d1..4251d52c 100644 --- a/vnext/src/App.js +++ b/vnext/src/App.js @@ -30,15 +30,14 @@ export default function App() { useEffect(() => { svg4everybody(); + let params = qs.parse(window.location.search.substring(1)); + if (params.hash) { + cookie.save('hash', params.hash, { path: '/' }); + let retpath = params.retpath || `${window.location.protocol}//${window.location.host}${window.location.pathname}`; + window.history.replaceState({}, document.title, retpath); + } }, []); - let params = qs.parse(window.location.search.substring(1)); - if (params.hash) { - cookie.save('hash', params.hash, { path: '/' }); - let retpath = params.retpath || `${window.location.protocol}//${window.location.host}${window.location.pathname}`; - window.history.replaceState({}, document.title, retpath); - } - /** * @type {import('./api').SecureUser} */ @@ -135,17 +134,6 @@ export default function App() { }, [hash]); /** - * @param {{ pathname: any; search: string; }[]} history - * @param {any} pathname - * @param {any} searchString - */ - let search = (history, pathname, searchString) => { - let location = {}; - location.pathname = pathname; - location.search = `?search=${searchString}`; - history.push(location); - }; - /** * @param {import("./api").SecureUser} visitor */ let auth = (visitor) => { @@ -163,23 +151,45 @@ export default function App() { <div id="wrapper"> <section id="content" ref={contentRef} className={scrollState.top ? elClassTop : ''}> <Switch> - <Route exact path="/" render={(props) => <Discussions visitor={visitor} {...props} />} /> - <Route exact path="/home" render={(props) => <Home visitor={visitor} {...props} />} /> - <Route exact path="/discover" render={(props) => - <Discover visitor={visitor} {...props} /> - } /> - <Route exact path="/settings" render={(props) => - <Settings visitor={visitor} {...props} onChange={auth} /> - } /> - <Route exact path="/login" render={(props) => <Login visitor={visitor} {...props} onAuth={auth} />} /> - <Route exact path="/post" render={(props) => <Post visitor={visitor} {...props} />} /> - <Route exact path="/pm" render={(props) => <Contacts visitor={visitor} {...props} />} /> - <Route exact path="/pm/:user" render={(props) => <Chat connection={eventSource} visitor={visitor} {...props} />} /> - <Route exact path="/:user/friends" render={(props) => <Friends visitor={visitor} {...props} />} /> - <Route exact path="/:user/readers" render={(props) => <Readers visitor={visitor} {...props} />} /> - <Route exact path="/:user" render={(props) => <Blog visitor={visitor} {...props} />} /> - <Route exact path="/tag/:tag" render={(props) => <Tag visitor={visitor} {...props} />} /> - <Route exact path="/:user/:mid" render={(props) => <Thread connection={eventSource} visitor={visitor} {...props} />} /> + <Route exact path="/"> + <Discussions visitor={visitor} /> + </Route> + <Route exact path="/home"> + <Home visitor={visitor} /> + </Route> + <Route exact path="/discover"> + <Discover visitor={visitor} /> + </Route> + <Route exact path="/settings"> + <Settings visitor={visitor} onChange={auth} /> + </Route> + <Route exact path="/login"> + <Login visitor={visitor} onAuth={auth} /> + </Route> + <Route exact path="/post"> + <Post visitor={visitor} /> + </Route> + <Route exact path="/pm"> + <Contacts visitor={visitor} /> + </Route> + <Route exact path="/pm/:user"> + <Chat connection={eventSource} visitor={visitor} /> + </Route> + <Route exact path="/:user/friends"> + <Friends visitor={visitor} /> + </Route> + <Route exact path="/:user/readers"> + <Readers visitor={visitor} /> + </Route> + <Route exact path="/:user"> + <Blog visitor={visitor} /> + </Route> + <Route exact path="/tag/:tag"> + <Tag visitor={visitor} /> + </Route> + <Route exact path="/:user/:mid"> + <Thread connection={eventSource} visitor={visitor} /> + </Route> </Switch> </section> { diff --git a/vnext/src/ui/Chat.js b/vnext/src/ui/Chat.js index 00e8eb3c..90c99c27 100644 --- a/vnext/src/ui/Chat.js +++ b/vnext/src/ui/Chat.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; import moment from 'moment'; import PM from './PM'; @@ -13,7 +14,6 @@ import './Chat.css'; * @typedef {Object} ChatProps * @property {import('../api').SecureUser} visitor * @property {EventSource} connection - * @property {import('react-router').match} match */ /** @@ -22,6 +22,7 @@ import './Chat.css'; */ export default function Chat(props) { const [chats, setChats] = useState([]); + const params = useParams(); let loadChat = useCallback((uname) => { const { hash } = props.visitor; @@ -36,32 +37,32 @@ export default function Chat(props) { let onMessage = useCallback((json) => { const msg = JSON.parse(json.data); - if (msg.user.uname === props.match.params.user) { + if (msg.user.uname === params.user) { setChats((oldChat) => { return [msg, ...oldChat]; }); } - }, [props.match.params.user]); + }, [params.user]); let onSend = (template) => { pm(template.to.uname, template.body) .then(res => { - loadChat(props.match.params.user); + loadChat(params.user); }).catch(console.log); }; useEffect(() => { if (props.connection.addEventListener) { props.connection.addEventListener('msg', onMessage); } - loadChat(props.match.params.user); + loadChat(params.user); console.log(props.connection); return () => { if (props.connection.removeEventListener) { props.connection.removeEventListener('msg', onMessage); } }; - }, [props.connection, onMessage, loadChat, props.match.params.user]); - const uname = props.match.params.user; + }, [props.connection, onMessage, loadChat, params.user]); + const uname = params.user; return ( <div className="msg-cont"> <UserInfo user={uname} /> diff --git a/vnext/src/ui/Feeds.js b/vnext/src/ui/Feeds.js index e687f1e2..27a8376f 100644 --- a/vnext/src/ui/Feeds.js +++ b/vnext/src/ui/Feeds.js @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; +import { Link, useLocation, useHistory, useParams } from 'react-router-dom'; -import { Link } from 'react-router-dom'; import qs from 'qs'; import moment from 'moment'; @@ -20,9 +20,6 @@ import { getMessages } from '../api'; /** * @typedef {Object} PageProps - * @property {import('history').History} history - * @property {import('history').Location} location - * @property {import('react-router').match} match * @property {string=} search * @property {import('../api').SecureUser} visitor * @property {import('../api').Message[]=} msgs @@ -31,34 +28,38 @@ import { getMessages } from '../api'; /** * @param {PageProps} props */ -export function Discover(props) { - let search = qs.parse(props.location.search.substring(1)); +export function Discover({ visitor }) { + const location = useLocation(); + let search = qs.parse(location.search.substring(1)); const query = { baseUrl: '/api/messages', search: search, pageParam: search.search ? 'page' : 'before_mid' }; - return (<Feed authRequired={false} query={query} {...props} />); + return (<Feed authRequired={false} query={query} visitor={visitor} />); } /** * @param {PageProps} props */ -export function Discussions(props) { +export function Discussions({ visitor }) { const query = { baseUrl: '/api/messages/discussions', pageParam: 'to' }; - return (<Feed authRequired={false} query={query} {...props} />); + return (<Feed authRequired={false} query={query} visitor={visitor} />); } /** * @param {PageProps} props */ -export function Blog(props) { - const { user } = props.match.params; - let search = qs.parse(props.location.search.substring(1)); - search.uname = user; +export function Blog({ visitor }) { + const { user } = useParams(); + const location = useLocation(); + const search = { + ...qs.parse(location.search.substring(1)), + uname: user + }; const query = { baseUrl: '/api/messages', search: search, @@ -67,9 +68,9 @@ export function Blog(props) { return ( <> <div className="msg-cont"> - <UserInfo user={user} /> + <UserInfo uname={user} /> </div> - <Feed authRequired={false} query={query} {...props} /> + <Feed authRequired={false} query={query} visitor={visitor} /> </> ); } @@ -77,8 +78,9 @@ export function Blog(props) { /** * @param {PageProps} props */ -export function Tag(props) { - const { tag } = props.match.params; +export function Tag({ visitor }) { + const params = useParams(); + const { tag } = params; const query = { baseUrl: '/api/messages', search: { @@ -86,50 +88,47 @@ export function Tag(props) { }, pageParam: 'before_mid' }; - return (<Feed authRequired={false} query={query} {...props} />); + return (<Feed authRequired={false} query={query} visitor={visitor} />); } /** * @param {PageProps} props */ -export function Home(props) { +export function Home({ visitor }) { const query = { baseUrl: '/api/home', pageParam: 'before_mid' }; - return (<Feed authRequired={true} query={query} {...props} />); + return (<Feed authRequired={true} query={query} visitor={visitor} />); } /** * @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 + * @property { Query} query */ /** * @param {FeedState} props */ -function Feed(props) { +function Feed({ visitor, query, authRequired }) { + const location = useLocation(); + const history = useHistory(); const [state, setState] = useState({ - history: props.history, - authRequired: props.authRequired, - query: props.query, - hash: props.visitor.hash, - filter: props.location.search.substring(1), + authRequired: authRequired, + hash: visitor.hash, msgs: [], - loading: true, nextpage: null, error: false, tag: '' }); - - const stateRef = useRef(state); + const [loading, setLoading] = useState(false); useEffect(() => { + setLoading(true); + const filter = location.search.substring(1); 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 }; @@ -138,35 +137,39 @@ function Feed(props) { }; 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; + const filterParams = qs.parse(filter); + let params = Object.assign({}, filterParams || {}, query.search || {}); + let url = query.baseUrl; + if (state.hash) { + params.hash = state.hash; } - if (!params.hash && stateRef.current.authRequired) { - stateRef.current.history.push('/'); + if (!params.hash && state.authRequired) { + history.push('/'); } getMessages(url, params) .then(response => { const { data } = response; - const { pageParam } = stateRef.current.query; + const { pageParam } = 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'] || '' + setState((prevState) => { + return { + ...prevState, + msgs: data, + nextpage: nextpage, + tag: qs.parse(location.search.substring(1))['tag'] || '' + }; }); + setLoading(false); }).catch(ex => { - setState({ - ...stateRef.current, - error: true + setState((prevState) => { + return { + ...prevState, + error: true + }; }); }); - }, []); + }, [location.search, state.hash, state.authRequired, history, query]); return (state.msgs.length > 0 ? ( <div className="msgs"> { @@ -180,16 +183,16 @@ function Feed(props) { } { state.msgs.map(msg => - <Message key={msg.mid} data={msg} visitor={props.visitor} />) + <Message key={msg.mid} data={msg} visitor={visitor} />) } { state.msgs.length >= 20 && ( <p className="page"> - <Link to={{ pathname: props.location.pathname, search: state.nextpage }} rel="prev">Next →</Link> + <Link to={{ pathname: 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> + ) : state.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 a7663dd3..d8fe23e0 100644 --- a/vnext/src/ui/Header.js +++ b/vnext/src/ui/Header.js @@ -1,11 +1,21 @@ -import React, { memo } from 'react'; -import { Link, withRouter } from 'react-router-dom'; +import React, { memo, useCallback } from 'react'; +import { Link, useHistory } from 'react-router-dom'; import Icon from './Icon'; import { UserLink } from './UserInfo'; import SearchBox from './SearchBox'; -function Header({ visitor, search, className }) { +function Header({ visitor, className }) { + const history = useHistory(); + /** + * @param {string} searchString + */ + let searchAll = useCallback((searchString) => { + let location = {}; + location.pathname = '/discover'; + location.search = `?search=${searchString}`; + history.push(location); + }, [history]); return ( <div id="header" className={className}> <div id="header_wrapper"> @@ -27,7 +37,7 @@ function Header({ visitor, search, className }) { visitor.uid >= 0 && <> <div id="search" className="desktop"> - <SearchBox pathname="/discover" onSearch={search} /> + <SearchBox onSearch={searchAll} /> </div> <nav id="global"> {visitor.uid > 0 ? @@ -68,4 +78,4 @@ function Header({ visitor, search, className }) { ); } -export default memo(withRouter(Header)); +export default memo(Header); diff --git a/vnext/src/ui/Login.js b/vnext/src/ui/Login.js index 883a4eab..0c6f5d0c 100644 --- a/vnext/src/ui/Login.js +++ b/vnext/src/ui/Login.js @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; - -import { withRouter } from 'react-router-dom'; +import { useLocation, useHistory } from 'react-router-dom'; import Icon from './Icon'; import Button from './Button'; @@ -14,8 +13,6 @@ import './Login.css'; /** * @typedef {Object} LoginProps * @property {import('../api').SecureUser} visitor - * @property {import('history').History} history - * @property {import('history').Location} location * @property {any} onAuth */ @@ -23,7 +20,10 @@ import './Login.css'; * Login page * @param {LoginProps} props */ -function Login({ visitor, history, location, onAuth }) { +function Login({ visitor, onAuth }) { + + const location = useLocation(); + const history = useHistory(); useEffect(() => { if (visitor.hash) { @@ -76,7 +76,7 @@ function Login({ visitor, history, location, onAuth }) { ); } -export default withRouter(Login); +export default Login; const socialButtonsStyle = { display: 'flex', diff --git a/vnext/src/ui/Post.js b/vnext/src/ui/Post.js index 35704f11..407f3b62 100644 --- a/vnext/src/ui/Post.js +++ b/vnext/src/ui/Post.js @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; import qs from 'qs'; @@ -8,9 +9,11 @@ import MessageInput from './MessageInput'; import { post, update } from '../api'; /** - * @param {{location: import('history').Location, visitor: import('../api').User, history: import('history').History}} props + * @param {{visitor: import('../api').User}} props */ -export default function Post({ location, visitor, history }) { +export default function Post({ visitor }) { + const location = useLocation(); + const history = useHistory(); let draftMessage = (location.state || {}).draft || {}; let [draft, setDraft] = useState(draftMessage.body); let params = qs.parse(window.location.search.substring(1)); diff --git a/vnext/src/ui/SearchBox.js b/vnext/src/ui/SearchBox.js index aab49757..dda97989 100644 --- a/vnext/src/ui/SearchBox.js +++ b/vnext/src/ui/SearchBox.js @@ -1,27 +1,25 @@ import React from 'react'; -import { withRouter } from 'react-router-dom'; import { useFormState } from 'react-use-form-state'; /** * @typedef {Object} SearchBoxPropsFields - * @property {string} pathname * @property {function} onSearch */ /** - * @typedef {import('react-router-dom').RouteComponentProps & SearchBoxPropsFields} SearchBoxProps + * @typedef {SearchBoxPropsFields} SearchBoxProps */ /** * @param {SearchBoxProps} props */ -function SearchBox({ onSearch, history, pathname }) { +function SearchBox({ onSearch }) { /** * @type {(React.FormEvent<HTMLFormElement>)} */ let onSubmit = (event) => { event.preventDefault(); - onSearch(history, pathname, formState.values.search); + onSearch(formState.values.search); }; const [formState, { text }] = useFormState(); return ( @@ -32,4 +30,4 @@ function SearchBox({ onSearch, history, pathname }) { ); } -export default withRouter(SearchBox); +export default SearchBox; diff --git a/vnext/src/ui/Thread.js b/vnext/src/ui/Thread.js index 63bb2dd6..4f53c4af 100644 --- a/vnext/src/ui/Thread.js +++ b/vnext/src/ui/Thread.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; import Message from './Message'; import MessageInput from './MessageInput'; @@ -11,7 +12,7 @@ import { format, embedUrls } from '../utils/embed'; import { getMessages, comment, update, markReadTracker, fetchUserUri, updateAvatar } from '../api'; -import { bubbleStyle, chatItemStyle } from './helpers/BubbleStyle'; +import { chatItemStyle } from './helpers/BubbleStyle'; import './Thread.css'; @@ -118,21 +119,21 @@ function Comment({ msg, draft, visitor, active, setActive, onStartEditing, postC /** * @param {{ - match: import('react-router').match, - location: import('history').Location, visitor: import('../api').SecureUser connection: EventSource }} props */ export default function Thread(props) { - const [message, setMessage] = useState((props.location.state || {}).msg || {}); + const location = useLocation(); + const params = useParams(); + const [message, setMessage] = useState((location.state || {}).msg || {}); const [replies, setReplies] = useState([]); const [loading, setLoading] = useState(false); const [active, setActive] = useState(0); const [editing, setEditing] = useState(emptyMessage); const [hash, setHash] = useState(props.visitor.hash); - const { mid } = props.match.params; + const { mid } = params; let loadReplies = useCallback(() => { document.body.scrollTop = 0; @@ -217,7 +218,10 @@ export default function Thread(props) { </li> )) : ( <> - {Array(loaders).fill().map((it, i) => <Spinner key={i} />)} + { + // @ts-ignore + Array(loaders).fill().map((it, i) => <Spinner key={i} />) + } </> ) } diff --git a/vnext/src/ui/UserInfo.js b/vnext/src/ui/UserInfo.js index fe9cee19..00465252 100644 --- a/vnext/src/ui/UserInfo.js +++ b/vnext/src/ui/UserInfo.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { info, fetchUserUri } from '../api'; +import { info, fetchUserUri, update } from '../api'; import Avatar from './Avatar'; import Icon from './Icon'; @@ -13,29 +13,25 @@ let isMounted; /** * User info component - * @param {{user: string, onUpdate?: function, children?: Element}} props + * @param {{uname: string, onUpdate?: function, children?: React.ReactHTMLElement}} props */ -export default function UserInfo(props) { +export default function UserInfo({ uname, onUpdate, children }) { const [user, setUser] = useState({ - uname: props.user, + uname: uname, uid: 0 }); - const { onUpdate } = props; - const userRef = useRef(user); useEffect(() => { isMounted = true; - if (!userRef.current.avatar) { - info(userRef.current.uname).then(response => { - if (isMounted) { - onUpdate && onUpdate(response.data); - setUser(response.data); - } - }); - } + info(uname).then(response => { + if (isMounted) { + onUpdate && onUpdate(response.data); + setUser(response.data); + } + }); return () => { isMounted = false; }; - }, [onUpdate, props.user]); + }, [onUpdate, uname]); return ( <> <div className="userinfo"> @@ -63,7 +59,7 @@ export default function UserInfo(props) { </> } </div> - {props.children} + {children} </> ); } diff --git a/vnext/src/ui/Users.js b/vnext/src/ui/Users.js index 4c09318f..4dd929fb 100644 --- a/vnext/src/ui/Users.js +++ b/vnext/src/ui/Users.js @@ -1,36 +1,37 @@ import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; import UserInfo from './UserInfo'; import Avatar from './Avatar'; /** * Friends feed - * @param {{match: import('react-router').match }} match */ -export function Friends({ match }) { - return <Users user={match.params.user} prop='read' />; +export function Friends() { + const params = useParams(); + return <Users uname={params.user} prop='read' />; } /** * Readers feed - * @param {{match: import('react-router').match }} match */ -export function Readers({ match }) { - return <Users user={match.params.user} prop='readers' />; +export function Readers() { + const params = useParams(); + return <Users uname={params.user} prop='readers' />; } /** * UserInfo list component - * @param {{user: import('../api').User, prop: string}} props + * @param {{uname: string, prop: string}} props */ -function Users(props) { - const [user, setUser] = useState({ uid: 0, uname: props.user }); +function Users({ uname, prop }) { + const [user, setUser] = useState({ uid: 0, uname: uname }); return ( - <UserInfo user={user.uname} onUpdate={setUser}> + <UserInfo uname={uname} onUpdate={setUser}> <div style={{ display: 'flex', flexWrap: 'wrap', flexDirection: 'row' }}> { - user[props.prop] && - user[props.prop].map(user => + user[prop] && + user[prop].map(user => <Avatar key={user.uid} user={user} /> ) } |