diff options
author | 2019-05-04 21:13:12 +0300 | |
---|---|---|
committer | 2023-01-13 10:37:54 +0300 | |
commit | f470636a70943a8ecad8bddc791a1c2dddd28e1e (patch) | |
tree | c43d109d88adbde9a696084070cdd92c6b9a004b /vnext/src/components | |
parent | 3d7d213e3ddc5bf4f71d536f31677b768aa3b7c0 (diff) |
Components -> UI
Diffstat (limited to 'vnext/src/components')
39 files changed, 0 insertions, 2498 deletions
diff --git a/vnext/src/components/Avatar.css b/vnext/src/components/Avatar.css deleted file mode 100644 index 7bdb3115..00000000 --- a/vnext/src/components/Avatar.css +++ /dev/null @@ -1,27 +0,0 @@ -.Avatar { - display: flex; - width: 100%; -} -.msg-avatar { - max-height: 48px; - margin-right: 10px; - max-width: 48px; -} -.msg-avatar img { - max-height: 48px; - vertical-align: top; - max-width: 48px; -} - -.info-avatar img { - max-height: 24px; - max-width: 24px; - padding: 6px; - vertical-align: middle; -} - -@media screen and (min-width: 450px) { - .Avatar { - width: 300px; - } -} diff --git a/vnext/src/components/Avatar.js b/vnext/src/components/Avatar.js deleted file mode 100644 index dda5449f..00000000 --- a/vnext/src/components/Avatar.js +++ /dev/null @@ -1,44 +0,0 @@ -import React, { memo } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { UserType } from './Types'; - -import Icon from './Icon'; - -import './Avatar.css'; - -function Avatar({ user, style, link, children}) { - return ( - <div className="Avatar" style={style}> - <div className="msg-avatar"> - { - user.uname ? - <Link to={{ pathname: link || `/${user.uname}/` }}> - {user.avatar ? - <img src={user.avatar} alt={`${user.uname}`} /> - : <Icon name="ei-spinner" size="m" />} - </Link> - : <Icon name="ei-spinner" size="m" /> - } - </div> - <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> - <span> - <Link to={{ pathname: `/${user.uname}/` }}> - <span>{user.uname}</span> - </Link> - </span> - {children} - </div> - </div> - ); -} - -export default memo(Avatar); - -Avatar.propTypes = { - user: UserType, - link: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node -}; diff --git a/vnext/src/components/Button.css b/vnext/src/components/Button.css deleted file mode 100644 index 2acb87be..00000000 --- a/vnext/src/components/Button.css +++ /dev/null @@ -1,13 +0,0 @@ -.Button { - background: #fff; - border: 1px solid #eee; - color: #888; - cursor: pointer; - display: inline-block; - margin: 5px; - padding: 4px 10px; -} -.Button:hover { - background: #f8f8f8; - border-bottom: 1px solid #ff339a; -} diff --git a/vnext/src/components/Button.js b/vnext/src/components/Button.js deleted file mode 100644 index 18cab0a7..00000000 --- a/vnext/src/components/Button.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import './Button.css'; - -function Button(props) { - return ( - <button className="Button" {...props} /> - ); -} - -export default React.memo(Button); diff --git a/vnext/src/components/Chat.css b/vnext/src/components/Chat.css deleted file mode 100644 index 520a5c9b..00000000 --- a/vnext/src/components/Chat.css +++ /dev/null @@ -1,9 +0,0 @@ -.Chat_messages { - box-sizing: border-box; - padding: 0 20px; - overflow-y: auto; - height: 450px; - display: flex; - flex-direction: column-reverse; - width: 100%; -} diff --git a/vnext/src/components/Chat.js b/vnext/src/components/Chat.js deleted file mode 100644 index a1254a10..00000000 --- a/vnext/src/components/Chat.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import ReactRouterPropTypes from 'react-router-prop-types'; -import { UserType } from './Types'; -import moment from 'moment'; - -import PM from './PM'; -import MessageInput from './MessageInput'; -import UserInfo from './UserInfo'; - -import { getChat, pm } from '../api'; - -import './Chat.css'; - -export default function Chat(props) { - const [chats, setChats] = useState([]); - useEffect(() => { - if (props.connection.addEventListener) { - props.connection.addEventListener('msg', onMessage); - } - loadChat(props.match.params.user); - console.log(props.connection); - return () => { - if (props.connection.removeEventListener) { - props.connection.removeEventListener('msg', onMessage); - } - }; - }, [props.connection, onMessage, loadChat, props.match.params.user]); - - let loadChat = useCallback((uname) => { - const { hash } = props.visitor; - setChats([]); - if (hash && uname) { - getChat(uname) - .then(response => { - setChats(response.data); - }); - } - }, [props.visitor]); - - let onMessage = useCallback((json) => { - const msg = JSON.parse(json.data); - if (msg.user.uname === props.match.params.user) { - setChats((oldChat) => { - return [msg, ...oldChat]; - }); - } - }, [props.match.params.user]); - - let onSend = (template) => { - pm(template.to.uname, template.body) - .then(res => { - loadChat(props.match.params.user); - }).catch(console.log); - }; - const uname = props.match.params.user; - return ( - <div className="msg-cont"> - <UserInfo user={uname} /> - {uname ? ( - <div className="chatroom"> - <ul className="Chat_messages"> - { - chats.map((chat) => - <PM key={moment.utc(chat.timestamp).valueOf()} chat={chat} {...props} /> - ) - } - </ul> - <MessageInput data={{ mid: 0, timestamp: '0', to: { uname: uname } }} onSend={onSend}> - Reply... - </MessageInput> - </div> - ) : ( - <div className="chatroom no-selection"><p>No chat selected</p></div> - ) - } - </div> - ); -} - -Chat.propTypes = { - visitor: UserType.isRequired, - match: ReactRouterPropTypes.match.isRequired, - connection: PropTypes.object.isRequired -}; diff --git a/vnext/src/components/Contact.js b/vnext/src/components/Contact.js deleted file mode 100644 index 24aabe15..00000000 --- a/vnext/src/components/Contact.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { UserType } from './Types'; - -import Avatar from './Avatar'; - -function Contact({ user, style, ...rest }) { - return ( - <Avatar user={user} link={`/pm/${user.uname}`} style={style}> - {user.unreadCount && <span className="badge">{user.unreadCount}</span>} - <div className="msg-ts">{user.lastMessageText}</div> - </Avatar> - ); -} - -export default React.memo(Contact); - -Contact.propTypes = { - user: UserType, - style: PropTypes.object -}; diff --git a/vnext/src/components/Contacts.js b/vnext/src/components/Contacts.js deleted file mode 100644 index 3852b26f..00000000 --- a/vnext/src/components/Contacts.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { getChats } from '../api'; - - -import Contact from './Contact.js'; -import { ChatSpinner } from './Spinner'; - -export default function Contacts(props) { - const [pms, setPms] = useState([]); - useEffect(() => { - getChats() - .then(response => { - setPms(response.data.pms); - }); - }, []); - return ( - <div className="msg-cont"> - <div style={chatListStyle}> - { - pms.length ? pms.map((chat) => - <Contact key={chat.uname} user={chat} style={chatTitleStyle} /> - ) : <><ChatSpinner /><ChatSpinner /><ChatSpinner /><ChatSpinner /><ChatSpinner /></> - } - </div> - </div> - ); -} - -const wrapperStyle = { - display: 'flex', - backgroundColor: '#fff' -}; - -const chatListStyle = { - display: 'flex', - flexDirection: 'column', - width: '100%', - padding: '12px' -}; - -const chatTitleStyle = { - width: '100%', - padding: '12px', - textAlign: 'left', - background: '#fff', - color: '#222', - borderBottom: '1px solid #eee' -}; diff --git a/vnext/src/components/Feeds.js b/vnext/src/components/Feeds.js deleted file mode 100644 index c7b857b7..00000000 --- a/vnext/src/components/Feeds.js +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import ReactRouterPropTypes from 'react-router-prop-types'; -import { Link } from 'react-router-dom'; -import qs from 'qs'; -import moment from 'moment'; - -import Message from './Message'; -import Spinner from './Spinner'; - -import UserInfo from './UserInfo'; - -import { getMessages } from '../api'; -import { UserType } from './Types'; - -export function Discover(props) { - let search = qs.parse(props.location.search.substring(1)); - const query = { - baseUrl: '/api/messages', - search: search, - pageParam: search.search ? 'page' : 'before_mid' - }; - return (<Feed authRequired={false} query={query} {...props} />); -} - -export function Discussions(props) { - const query = { - baseUrl: '/api/messages/discussions', - pageParam: 'to' - }; - return (<Feed authRequired={false} query={query} {...props} />); -} - -export function Blog(props) { - const { user } = props.match.params; - let search = qs.parse(props.location.search.substring(1)); - search.uname = user; - const query = { - baseUrl: '/api/messages', - search: search, - pageParam: search.search ? 'page' : 'before_mid' - }; - return ( - <> - <div className="msg-cont"> - <UserInfo user={user} /> - </div> - <Feed authRequired={false} query={query} {...props} /> - </> - ); -} - -export function Tag(props) { - const { tag } = props.match.params; - const query = { - baseUrl: '/api/messages', - search: { - tag: tag - }, - pageParam: 'before_mid' - }; - return (<Feed authRequired={false} query={query} {...props} />); -} - -export function Home(props) { - const query = { - baseUrl: '/api/home', - pageParam: 'before_mid' - }; - return (<Feed authRequired={true} query={query} {...props} />); -} - -function Feed(props) { - const [msgs, setMsgs] = useState([]); - const [loading, setLoading] = useState(true); - const [nextpage, setNextpage] = useState(null); - const [error, setError] = useState(false); - - useEffect(() => { - let loadMessages = (hash = '', filter = '') => { - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - setMsgs([]); - 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); - }); - }; - 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 = ( - <> - { - tag && ( - <p className="page"> - <Link to={{ pathname: `/tag/${tag}` }}> - <span>← All posts with tag </span><b>{tag}</b> - </Link> - </p> - ) - } - { - msgs.map(msg => - <Message key={msg.mid} data={msg} visitor={props.visitor} />) - } - { - msgs.length >= 20 && ( - <p className="page"> - <Link to={{ pathname: props.location.pathname, search: nextpage }} rel="prev">Next →</Link> - </p> - ) - } - </> - ); - 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>; -} - -Discover.propTypes = { - location: ReactRouterPropTypes.location.isRequired, - search: PropTypes.string -}; - -Blog.propTypes = { - match: ReactRouterPropTypes.match.isRequired, - location: ReactRouterPropTypes.location.isRequired, - search: PropTypes.string -}; - -Tag.propTypes = { - match: ReactRouterPropTypes.match.isRequired -}; - -Feed.propTypes = { - authRequired: PropTypes.bool, - visitor: UserType, - history: ReactRouterPropTypes.history.isRequired, - location: ReactRouterPropTypes.location.isRequired, - msgs: PropTypes.array, - query: PropTypes.shape({ - baseUrl: PropTypes.string.isRequired, - search: PropTypes.object, - pageParam: PropTypes.string.isRequired - }) -}; diff --git a/vnext/src/components/Header.js b/vnext/src/components/Header.js deleted file mode 100644 index 48f89360..00000000 --- a/vnext/src/components/Header.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useEffect, useCallback, useRef } from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; - -const elClassHidden = 'header--hidden'; - -const header = document.getElementById('header'); -header.removeChild(document.getElementById('header_wrapper')); - -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); - - useEffect(() => { - window.addEventListener('scroll', () => (!window.requestAnimationFrame) - ? throttle(250, updateHeader) - : window.requestAnimationFrame(updateHeader), false); - }, [updateHeader]); - 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 > 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; - }, []); - return ReactDOM.createPortal(children, header); -} - -Header.propTypes = { - children: PropTypes.node -}; diff --git a/vnext/src/components/Icon.js b/vnext/src/components/Icon.js deleted file mode 100644 index faf1a704..00000000 --- a/vnext/src/components/Icon.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Icon = React.memo(props => { - var size = props.size ? ' icon--' + props.size : ''; - var className = props.className ? ' ' + props.className : ''; - var klass = 'icon' + (!props.noFill ? ' icon--' + props.name : '') + size + className; - - var name = '#' + props.name + '-icon'; - var useTag = '<use xlink:href=' + name + ' />'; - var Icon = React.createElement('svg', { className: 'icon__cnt', dangerouslySetInnerHTML: { __html: useTag } }); - return React.createElement( - 'div', - { className: klass }, - wrapSpinner(Icon, klass) - ); -}); - -function wrapSpinner(Html, klass) { - if (klass.indexOf('spinner') > -1) { - return React.createElement( - 'div', - { className: 'icon__spinner' }, - Html - ); - } else { - return Html; - } -} - -export default Icon; - -Icon.propTypes = { - size: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - className: PropTypes.string, - noFill: PropTypes.bool -}; diff --git a/vnext/src/components/Input.css b/vnext/src/components/Input.css deleted file mode 100644 index dbe55eae..00000000 --- a/vnext/src/components/Input.css +++ /dev/null @@ -1,8 +0,0 @@ -.input { - background: #FFF; - border: 1px solid #ccc; - outline: none !important; - padding: 4px; - -webkit-appearance: none; - border-radius: 0; -} diff --git a/vnext/src/components/Input.js b/vnext/src/components/Input.js deleted file mode 100644 index c74d595d..00000000 --- a/vnext/src/components/Input.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import './Input.css'; - -function Input({ name, value, ...rest }) { - return ( - <input className="input" name={name} value={value} {...rest} /> - ); -} - -Input.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired -}; - -export default React.memo(Input); diff --git a/vnext/src/components/LoginButton.js b/vnext/src/components/LoginButton.js deleted file mode 100644 index cd26252e..00000000 --- a/vnext/src/components/LoginButton.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import Icon from './Icon'; -import Modal from './Modal'; -import Button from './Button'; -import Input from './Input'; -import { useFormState } from 'react-use-form-state'; - -import { me, facebookLink, vkLink } from '../api'; - -function LoginButton({ onAuth, title }) { - const [open, setOpen] = useState(false); - const [formState, { text, password }] = useFormState(); - - let onToggle = (event) => { - if (event) { - event.preventDefault(); - } - setOpen(!open); - }; - let onSubmit = (event) => { - event.preventDefault(); - me(formState.values.username, formState.values.password) - .then(response => { - onToggle(); - onAuth(response); - } - ).catch(ex => { - console.log(ex); - }); - }; - return ( - <> - <a onClick={onToggle}> - <Icon name="ei-user" size="s" /> - <span className="desktop">{title}</span> - </a> - <Modal show={open} - onClose={onToggle}> - <div className="dialoglogin"> - <p>Please, introduce yourself:</p> - <div style={socialButtonsStyle}> - <a href={facebookLink()} style={facebookButtonStyle}> - <Icon name="ei-sc-facebook" size="s" noFill={true} />Log in - </a> - <a href={vkLink()} style={vkButtonStyle}> - <Icon name="ei-sc-vk" size="s" noFill={true} /> - Log in - </a> - </div> - <p>Already registered?</p> - <form onSubmit={onSubmit}> - <Input name="username" - placeholder="Username..." - value={formState.values.username} {...text('username')} /><br /> - <Input name="password" - placeholder="Password..." - value={formState.values.password} {...password('password')} /><br /> - <Button onClick={onSubmit}>OK</Button> - </form> - </div> - </Modal> - </> - ); -} - -LoginButton.propTypes = { - title: PropTypes.string.isRequired, - onAuth: PropTypes.func.isRequired -}; - -const socialButtonsStyle = { - display: 'flex', - justifyContent: 'space-evenly', - padding: '4px' -}; - -const facebookButtonStyle = { - color: '#fff', - padding: '2px 14px', - background: '#3b5998' -}; - -const vkButtonStyle = { - color: '#fff', - padding: '2px 14px', - background: '#4c75a3' -}; - -export default LoginButton; diff --git a/vnext/src/components/Message.css b/vnext/src/components/Message.css deleted file mode 100644 index 18d3d0d5..00000000 --- a/vnext/src/components/Message.css +++ /dev/null @@ -1,210 +0,0 @@ -.msg-cont .ir { - padding: 12px; -} -.msg-cont .ir img { - max-width: 100%; - height: auto; -} -.msg-cont > .h, -.msg-cont .msg-header { - padding: 12px; -} -.msg-cont > .l { - border-top: 1px solid #eee; - display: flex; - align-items: center; - justify-content: space-around; - background: #fdfdfe; -} -.msg-cont > .l a { - color: #88958d; - margin-right: 15px; - font-size: small; -} -.msg-tags { - color: #88958d; - margin-top: 12px; - min-height: 1px; -} -.badge, -.msg-tags > a { - color: #88958d; - display: inline-block; - font-size: small; -} -.msgthread { - margin-bottom: 0; -} -.msg-cont { - background: #FFF; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); - line-height: 140%; - margin-bottom: 12px; -} -.reply-new .msg-cont { - border-right: 5px solid #0C0; -} -.msg-ts { - font-size: small; - vertical-align: top; -} -.msg-ts, -.msg-ts > a { - color: #88958d; -} -.msg-txt { - margin: 0 0 12px; - padding: 12px; - word-wrap: break-word; - overflow-wrap: break-word; -} -q:before, -q:after { - content: ""; -} -q, -blockquote { - border-left: 3px solid #CCC; - color: #666; - display: block; - margin: 10px 0 10px 10px; - padding-left: 10px; - word-break: break-word; -} -.msg-media { - text-align: center; -} -.msg-links { - color: #88958d; - font-size: small; - margin: 5px 0 0 0; - padding: 12px; -} -.msg-comments { - color: #88958d; - font-size: small; - margin-top: 10px; - overflow: hidden; - text-indent: 10px; -} -.ta-wrapper { - border: 1px solid #DDD; - display: flex; - flex-grow: 1; -} -.msg-comment { - display: flex; - margin-top: 10px; -} -.msg-comment-hidden { - display: none; -} -.msg-comment textarea { - border: 0; - flex-grow: 1; - outline: none !important; - padding: 4px; - resize: vertical; - vertical-align: top; -} -.attach-photo { - cursor: pointer; -} -.attach-photo-active { - color: green; -} -.msg-comment input { - align-self: flex-start; - background: #EEE; - border: 1px solid #CCC; - color: #999; - margin: 0 0 0 6px; - position: sticky; - top: 70px; - vertical-align: top; - width: 50px; -} -.msg-recomms { - color: #88958d; - background: #fdfdfe; - font-size: small; - margin-bottom: 10px; - padding: 6px; - border-bottom: 1px solid #eee; - overflow: hidden; - text-indent: 10px; -} -.msg-summary, -.msg-summary a { - color: #88958d; - font-size: small; - padding: 12px; - text-align: right; -} -#replies .msg-txt, -#private-messages .msg-txt { - margin: 0; -} -.title2 { - background: #fff; - margin: 20px 0; - padding: 10px 20px; -} -.title2-right { - float: right; - line-height: 24px; -} -#content .title2 h2 { - font-size: x-large; - margin: 0; -} - -.embedContainer { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: center; - padding: 12px; - margin: 30px -3px 15px -3px; -} -.embedContainer > * { - box-sizing: border-box; - flex-grow: 1; - margin: 3px; - min-width: 49%; -} -.embedContainer > .compact { - flex-grow: 0; -} -.embedContainer .picture img { - display: block; -} -.embedContainer img, -.embedContainer video { - max-width: 100%; - max-height: 80vh; -} -.embedContainer > .audio, -.embedContainer > .youtube { - min-width: 90%; -} -.embedContainer audio { - width: 100%; -} -.embedContainer iframe { - overflow: hidden; - resize: vertical; - display: block; -} -.msg-cont .nsfw .embedContainer img, -.msg-cont .nsfw .embedContainer video, -.msg-cont .nsfw .embedContainer iframe, -.msg-cont .nsfw .ir img { - opacity: 0.1; -} -.msg-cont .nsfw .embedContainer img:hover, -.msg-cont .nsfw .embedContainer video:hover, -.msg-cont .nsfw .embedContainer iframe:hover, -.msg-cont .nsfw .ir img:hover { - opacity: 1; -} diff --git a/vnext/src/components/Message.js b/vnext/src/components/Message.js deleted file mode 100644 index eb008bfe..00000000 --- a/vnext/src/components/Message.js +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; -import moment from 'moment'; - -import { UserType, MessageType } from './Types'; -import Icon from './Icon'; -import Avatar from './Avatar'; -import { UserLink } from './UserInfo'; - -import { format, embedUrls } from '../utils/embed'; - -import './Message.css'; - -export default function Message({ data, visitor, children, ...rest }) { - const isCode = (data.tags || []).indexOf('code') >= 0; - const likesSummary = data.likes ? `${data.likes}` : 'Recommend'; - const commentsSummary = data.replies ? `${data.replies}` : 'Comment'; - const embedRef = useRef(); - const msgRef = useRef(); - useEffect(() => { - if (msgRef.current) { - embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current); - if (!embedRef.current.hasChildNodes()) { - embedRef.current.style.display = 'none'; - } - } - }, []); - return ( - <div className="msg-cont"> - <Recommendations forMessage={data} /> - <header className="h"> - <Avatar user={data.user}> - <div className="msg-ts"> - <Link to={{ pathname: `/${data.user.uname}/${data.mid}`, state: { msg: data } }}> - <time dateTime={data.timestamp} - title={moment.utc(data.timestamp).local().format('lll')}> - {moment.utc(data.timestamp).fromNow()} - </time> - </Link> - </div> - </Avatar> - <TagsList user={data.user} data={data.tags || []} /> - </header> - { - data.body && - <div className="msg-txt" ref={msgRef}> - <MessageContainer isCode={isCode} data={{ __html: format(data.body, data.mid, isCode) }} /> - </div> - } - { - data.photo && - <div className="msg-media"> - <a href={`//i.juick.com/p/${data.mid}.${data.attach}`} data-fname={`${data.mid}.${data.attach}`}> - <img src={`//i.juick.com/photos-512/${data.mid}.${data.attach}`} alt="" /> - </a> - </div> - } - <div className="embedContainer" ref={embedRef} /> - <nav className="l"> - {visitor.uid === data.user.uid ? ( - <Link to={{ pathname: `/${data.user.uname}/${data.mid}` }} className="a-like msg-button"> - <Icon name="ei-heart" size="s" /> - <span>{likesSummary}</span> - </Link> - ) : visitor.uid > 0 ? ( - <Link to={{ pathname: '/post', search: `?body=!+%23${data.mid}` }} className="a-like msg-button"> - <Icon name="ei-heart" size="s" /> - <span>{likesSummary}</span> - </Link> - ) : ( - <a href="/login" className="a-login msg-button"> - <Icon name="ei-heart" size="s" /> - <span>{likesSummary}</span> - </a> - )} - {!data.ReadOnly | (visitor.uid === data.user.uid) && ( - <> - <Link to={{ pathname: `/${data.user.uname}/${data.mid}`, state: { msg: data } }} className="a-comment msg-button"> - <Icon name="ei-comment" size="s" /> - <span>{commentsSummary}</span> - </Link> - </> - )} - </nav> - {children} - </div> - ); -} - -function MessageContainer({ isCode, data }) { - return isCode ? (<pre dangerouslySetInnerHTML={data} />) : (<p dangerouslySetInnerHTML={data} />); -} - -function Tags({ data, user, ...rest }) { - return data.length > 0 && ( - <div className="msg-tags"> - { - data.map(tag => { - return (<Link key={tag} to={{ pathname: `/${user.uname}`, search: `?tag=${tag}` }} title={tag}>{tag}</Link>); - }).reduce((prev, curr) => [prev, ', ', curr]) - } - </div> - ); -} - -const TagsList = React.memo(Tags); - -function Recommends({ forMessage, ...rest }) { - const { likes, recommendations } = forMessage; - return recommendations && recommendations.length > 0 && ( - <div className="msg-recomms">{'♡ by '} - { - recommendations.map(it => ( - <UserLink key={it.uri || it.uid} user={it} /> - )).reduce((prev, curr) => [prev, ', ', curr]) - } - { - likes > recommendations.length && (<span> and {likes - recommendations.length} others</span>) - } - </div> - ) || null; -} - -const Recommendations = React.memo(Recommends); - -Message.propTypes = { - data: MessageType, - visitor: UserType.isRequired, - children: PropTypes.node -}; - -MessageContainer.propTypes = { - isCode: PropTypes.bool.isRequired, - data: PropTypes.object.isRequired -}; - -Tags.propTypes = { - user: UserType.isRequired, - data: PropTypes.array -}; - diff --git a/vnext/src/components/MessageInput.js b/vnext/src/components/MessageInput.js deleted file mode 100644 index e4988d59..00000000 --- a/vnext/src/components/MessageInput.js +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; - -import { useFormState } from 'react-use-form-state'; - -import { MessageType } from './Types'; - -import Icon from './Icon'; -import Button from './Button'; - -import UploadButton from './UploadButton'; - -export default function MessageInput({ text, data, rows, children, onSend }) { - let textareaRef = useRef(); - let fileinput = useRef(); - - let updateFocus = () => { - const isDesktop = window.matchMedia('(min-width: 62.5rem)'); - if (isDesktop.matches) { - textareaRef.current.focus(); - } - }; - useEffect(() => { - textareaRef.current.value = text || ''; - updateFocus(); - }, [text]); - - let handleCtrlEnter = (event) => { - if (event.ctrlKey && (event.charCode == 10 || event.charCode == 13)) { - onSubmit({}); - } - }; - let textChanged = (event) => { - const el = textareaRef.current; - const offset = el.offsetHeight - el.clientHeight; - const height = el.scrollHeight + offset; - el.style.height = `${height + offset}px`; - }; - const [attach, setAttach] = useState(''); - const [formState, { textarea }] = useFormState(); - let uploadValueChanged = (attach) => { - setAttach(attach); - }; - let onSubmit = (event) => { - if (event.preventDefault) { - event.preventDefault(); - } - const input = fileinput.current; - onSend({ - mid: data.mid, - rid: data.rid || 0, - body: formState.values.body, - attach: attach ? input.files[0] : '', - to: data.to || {} - }); - setAttach(''); - formState.clearField('body'); - textareaRef.current.style.height = ''; - updateFocus(); - }; - return ( - <form className="msg-comment-target" style={{ padding: '12px' }} onSubmit={onSubmit}> - <div style={commentStyle}> - <textarea onChange={textChanged} onKeyPress={handleCtrlEnter} - ref={textareaRef} style={textInputStyle} value={formState.values.body} - rows={rows || '1'} placeholder={children} {...textarea('body')} /> - <div style={inputBarStyle}> - <UploadButton inputRef={fileinput} value={attach} onChange={uploadValueChanged} /> - <Button onClick={onSubmit}><Icon name="ei-envelope" size="s" />Send</Button> - </div> - </div> - </form> - ); -} - -const commentStyle = { - display: 'flex', - flexDirection: 'column', - borderTop: '1px #eee solid', - width: '100%', - marginTop: '10px' -}; - -const inputBarStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '3px' -}; - -const textInputStyle = { - overflow: 'hidden', - resize: 'none', - display: 'block', - boxSizing: 'border-box', - border: 0, - outline: 'none', - padding: '4px' -}; - -MessageInput.propTypes = { - children: PropTypes.node, - data: MessageType.isRequired, - onSend: PropTypes.func.isRequired, - rows: PropTypes.string, - text: PropTypes.string -}; diff --git a/vnext/src/components/Modal.css b/vnext/src/components/Modal.css deleted file mode 100644 index 931b9d81..00000000 --- a/vnext/src/components/Modal.css +++ /dev/null @@ -1,95 +0,0 @@ -#dialogt { - height: 100%; - left: 0; - position: fixed; - top: 0; - width: 100%; - z-index: 10; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.3); -} -#dialogw { - z-index: 11; - max-width: 96%; - max-height: calc(100% - 100px); - background-color: #fff; -} -#dialogw a { - display: block; -} -#dialogw img { - max-height: 100%; - max-height: 90vh; - max-width: 100%; -} -#dialog_header { - width: 100%; - height: 44px; - display: flex; - flex-direction: row-reverse; - align-items: center; -} -.header_image { - background: rgba(0, 0, 0, 0.28); -} -#dialogc { - cursor: pointer; - color: #ccc; - padding-right: 6px; -} -.dialoglogin { - background: #fff; - padding: 25px; -} -.dialog-opened { - overflow: hidden; -} -#signemail, -#signfb, -#signvk { - display: block; - line-height: 32px; - margin: 10px 0; - text-decoration: none; - width: 100%; -} -#signvk { - margin-bottom: 30px; -} -.dialoglogin form { - margin-top: 7px; -} -.signinput, -.signsubmit { - border: 1px solid #CCC; - margin: 3px 0; - padding: 3px; -} -.signsubmit { - width: 70px; -} -.dialogshare { - background: #fff; - min-width: 300px; - overflow: auto; - padding: 20px; -} -.dialogl { - background: #fff; - border: 1px solid #DDD; - margin: 3px 0 20px; - padding: 5px; -} -.dialogshare li { - float: left; - margin: 5px 10px 0 0; -} -.dialogshare a { - display: block; -} -.dialogtxt { - background: #fff; - padding: 20px; -} diff --git a/vnext/src/components/Modal.js b/vnext/src/components/Modal.js deleted file mode 100644 index 799a6f35..00000000 --- a/vnext/src/components/Modal.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Icon from './Icon'; - -import './Modal.css'; - -function Modal(props) { - return props.show ? ( - <div id="dialogt"> - <div id="dialogw"> - <div id="dialog_header"> - <div id="dialogc" onClick={props.onClose}> - <Icon name="ei-close" size="s" /> - </div> - </div> - {props.children} - </div> - </div> - ) : (null); -} - -export default React.memo(Modal); - -Modal.propTypes = { - onClose: PropTypes.func.isRequired, - show: PropTypes.bool, - children: PropTypes.node -}; diff --git a/vnext/src/components/NavigationIcon.css b/vnext/src/components/NavigationIcon.css deleted file mode 100644 index caff6195..00000000 --- a/vnext/src/components/NavigationIcon.css +++ /dev/null @@ -1,4 +0,0 @@ -#navicon { - padding: 12px; - color: #88958d; -} diff --git a/vnext/src/components/NavigationIcon.js b/vnext/src/components/NavigationIcon.js deleted file mode 100644 index 0a22ac57..00000000 --- a/vnext/src/components/NavigationIcon.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Icon from './Icon'; - -import './NavigationIcon.css'; - -function NavigationIcon(props) { - return ( - <div id="navicon" className="mobile" onClick={props.onToggle}> - <Icon name="ei-navicon" size="s"/> - </div> - ); -} - -export default React.memo(NavigationIcon); - -NavigationIcon.propTypes = { - onToggle: PropTypes.func.isRequired -}; - diff --git a/vnext/src/components/PM.js b/vnext/src/components/PM.js deleted file mode 100644 index a1e70ad5..00000000 --- a/vnext/src/components/PM.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -import { UserType, MessageType } from './Types'; - -import Avatar from './Avatar'; -import { format } from '../utils/embed'; - -function PM(props) { - const { chat } = props; - return ( - <li> - <div style={chatItemStyle(props.visitor, chat)}> - <Avatar user={chat.user} /> - <div style={bubbleStyle(props.visitor, chat)}> - <p dangerouslySetInnerHTML={{ __html: format(chat.body) }} /> - </div> - </div> - </li> - ); -} - -export default React.memo(PM); - -function bubbleStyle(me, msg) { - const isMe = me.uid === msg.user.uid; - const color = isMe ? '#fff' : '#222'; - const background = isMe ? '#ec4b98' : '#eee'; - return { - background: background, - color: color, - padding: '12px' - }; -} - -function chatItemStyle(me, msg) { - const isMe = me.uid === msg.user.uid; - const alignment = isMe ? 'flex-end' : 'flex-start'; - return { - padding: '3px 6px', - listStyle: 'none', - margin: '10px 0', - display: 'flex', - flexDirection: 'column', - alignItems: alignment - }; -} - -PM.propTypes = { - chat: MessageType.isRequired, - visitor: UserType.isRequired -}; diff --git a/vnext/src/components/Post.js b/vnext/src/components/Post.js deleted file mode 100644 index 3dc23613..00000000 --- a/vnext/src/components/Post.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { memo } from 'react'; - -import ReactRouterPropTypes from 'react-router-prop-types'; -import { UserType } from './Types'; - -import qs from 'qs'; - -import MessageInput from './MessageInput'; - -import { post } from '../api'; - -function PostComponent(props) { - let params = qs.parse(window.location.search.substring(1)); - let postMessage = (template) => { - const { attach, body } = template; - post(body, attach) - .then(response => { - if (response.status === 200) { - const msg = response.data.newMessage; - this.props.history.push(`/${this.props.visitor.uname}/${msg.mid}`); - } - }).catch(console.log); - }; - return ( - <div className="msg-cont"> - <MessageInput rows="7" text={params.body || ''} data={{ mid: 0, timestamp: '0' }} onSend={postMessage}> - *weather It is very cold today! - </MessageInput> - </div> - ); -} - -export default memo(PostComponent); - -PostComponent.propTypes = { - history: ReactRouterPropTypes.history.isRequired, - visitor: UserType -}; diff --git a/vnext/src/components/SearchBox.js b/vnext/src/components/SearchBox.js deleted file mode 100644 index a79100cd..00000000 --- a/vnext/src/components/SearchBox.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactRouterPropTypes from 'react-router-prop-types'; -import { withRouter } from 'react-router-dom'; -import { useFormState } from 'react-use-form-state'; - -function SearchBox({ onSearch, history, pathname }) { - let onSubmit = (event) => { - event.preventDefault(); - onSearch(history, pathname, formState.values.search); - }; - const [formState, { text }] = useFormState(); - return ( - <form onSubmit={onSubmit}> - <input name="search" className="text" - placeholder="Search..." value={ formState.values.search } {...text('search')} /> - </form> - ); -} - -SearchBox.propTypes = { - pathname: PropTypes.string.isRequired, - onSearch: PropTypes.func.isRequired, - history: ReactRouterPropTypes.history.isRequired -}; -export default withRouter(SearchBox); diff --git a/vnext/src/components/Settings.js b/vnext/src/components/Settings.js deleted file mode 100644 index cf6926f8..00000000 --- a/vnext/src/components/Settings.js +++ /dev/null @@ -1,268 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import ReactRouterPropTypes from 'react-router-prop-types'; - -import { me, updateAvatar } from '../api'; - -import { UserType } from './Types'; - -import Button from './Button'; -import Icon from './Icon'; -import UploadButton from './UploadButton'; -import Avatar from './Avatar'; - -function ChangeAvatarForm({ visitor }) { - const [avatar, setAvatar] = useState(''); - const [preview, setPreview] = useState(); - const avatarInput = useRef(); - let avatarChanged = (newAvatar) => { - setAvatar(newAvatar); - setPreview(''); - if (newAvatar) { - let reader = new FileReader(); - reader.onloadend = (preview) => { - setPreview(preview.target.result); - }; - reader.readAsDataURL(avatarInput.current.files[0]); - } - }; - let previewUser = { ...visitor, uname: '<preview>' }; - if (preview) { - previewUser = { ...visitor, avatar: preview, uname: '<preview>' }; - } - let onSubmitAvatar = (event) => { - if (event.preventDefault) { - event.preventDefault(); - } - updateAvatar(avatarInput.current.files[0]).then(() => { - avatarChanged(''); - me().then(visitor => { - this.props.onChange(visitor); - }); - }); - }; - return ( - <form> - <small>Recommendations: PNG, 96x96, <50Kb. Also, JPG and GIF supported.</small> - <UploadButton inputRef={avatarInput} - value={avatar} onChange={avatarChanged} /> - <Avatar user={visitor} /> - <Avatar user={previewUser} /> - <Button onClick={onSubmitAvatar}>Update</Button> - </form> - ); -} - -ChangeAvatarForm.propTypes = { - visitor: UserType.isRequired -}; - -export default class Settings extends React.Component { - constructor(props) { - super(props); - this.state = { - settings: { - }, - me: {} - }; - } - componentDidMount() { - me().then(visitor => { - this.setState({ - me: visitor - }); - }); - } - - passwordChanged = (event) => { - let newState = update(this.state, { - settings: { password: { $set: event.target.value } } - }); - this.setState(newState); - } - onSubmitPassword = (event) => { - if (event.preventDefault) { - event.preventDefault(); - } - console.log('password update'); - } - emailChanged = (event) => { - let newState = update(this.state, { - me: { activeEmail: { $set: event.target.value } } - }); - this.setState(newState); - console.log('email update'); - } - disableTelegram = () => { - console.log('telegram disable'); - } - disableFacebook = (event) => { - if (event.preventDefault) { - event.preventDefault(); - } - console.log('facebook disable'); - } - enableFacebook = (event) => { - if (event.preventDefault) { - event.preventDefault(); - } - console.log('facebook enable'); - } - disableTwitter = () => { - console.log('twitter disable'); - } - - render() { - const { me, settings } = this.state; - return ( - <div className="msg-cont"> - <fieldset> - <legend><Icon name="ei-user" size="m" />Changing your avatar</legend> - <ChangeAvatarForm visitor={me} /> - </fieldset> - <fieldset> - <legend><Icon name="ei-unlock" size="m" />Changing your password</legend> - <form> - <p>Change password: <input type="password" name="password" size="8" onChange={this.passwordChanged} /> - <Button onClick={this.onSubmitPassword}>Update</Button><br /> - <i>(max. length - 16 symbols)</i></p> - </form> - </fieldset> - <fieldset> - <legend><Icon name="ei-sc-telegram" size="m" />Telegram</legend> - {me.telegramName ? ( - <form> - <div>Telegram: <b> {me.telegramName} </b>— - <Button onClick={this.disableTelegram}>Disable</Button> - </div> - </form> - ) : ( - <p>To connect Telegram account: send any text message to <a href="https://telegram.me/Juick_bot">@Juick_bot</a> - </p> - )} - </fieldset> - {me.jids && ( - <form> - <fieldset> - <legend>XMPP accounts - </legend> - <p>Your accounts:</p> - <p> - { - me.jids.map(jid => - <React.Fragment key={jid}> - <label><input type="radio" name="delete" value={jid} />{jid}</label><br /> - </React.Fragment> - ) - } - </p> - { - me.jids && me.jids.length > 1 && - <p><Button onClick={this.deleteJid}>Delete</Button></p> - } - <p>To add new jabber account: send any text message to <a href="xmpp:juick@juick.com?message;body=login">juick@juick.com</a> - </p> - </fieldset> - </form> - )} - <fieldset> - <legend><Icon name="ei-envelope" size="m" />E-mail</legend> - <form> - <p>Add account:<br /> - <input type="text" name="account" /> - <input type="hidden" name="page" value="email-add" /> - <Button onClick={this.addEmail}>Add</Button> - </p> - </form> - <form> - <p>Your accounts:</p> - <p> - { - me.emails ? me.emails.map(email => - <React.Fragment key={email}> - <label> - <input type="radio" name="account" value={email} />{email} - </label> - <br /> - </React.Fragment> - ) : '-' - } - </p> - { - me.emails && me.emails.length > 1 && - <Button onClick={this.deleteEmail}>Delete</Button> - } - </form> - { - me.emails && - <> - {/** email_off **/} - <form> - You can receive notifications to email:<br /> - Sent to <select name="account" value={me.activeEmail || 'Disabled'} onChange={this.emailChanged}> - <option value="">Disabled</option> - { - me.emails.map(email => - <option key={email} value={email}> - {email} - </option> - ) - } - </select> - </form> - {/** /email_off **/} - <p> </p> - <p>You can post to Juick via e-mail. Send your <u>plain text</u> messages to <span><a href="mailto:juick@juick.com">juick@juick.com</a></span>. You can attach one photo or video file.</p> - </> - } - </fieldset> - <fieldset> - <legend><Icon name="ei-sc-facebook" size="m" />Facebook</legend> - {me.facebookStatus && me.facebookStatus.connected ? ( - me.facebookStatus.crosspostEnabled ? - <form> - <div> - Facebook: <b>Enabled</b> — - <Button onClick={this.disableFacebook}>Disable</Button> - </div> - </form> - : - <form> - <div> - Facebook: <b>Disabled</b> — - <Button onClick={this.enableFacebook}>Enable</Button> - </div> - </form> - ) : ( - <p>Cross-posting to Facebook: - <a href="/_fblogin"> - <img src="//static.juick.com/facebook-connect.png" alt="Connect to Facebook" /> - </a> - </p> - )} - </fieldset> - <fieldset> - <legend><Icon name="ei-sc-twitter" size="m" />Twitter</legend> - {me.twitterName ? - <form action="/settings" method="post"> - <div>Twitter: <b>{me.twitterName}</b> — - <input type="hidden" name="page" value="twitter-del" /> - <Button onClick={this.disableTwitter}>Disable</Button> - </div> - </form> - : - <p>Cross-posting to Twitter: <a href="/_twitter"><img src="//static.juick.com/twitter-connect.png" - alt="Connect to Twitter" /></a></p> - } - </fieldset> - - </div> - ); - } -} - -Settings.propTypes = { - visitor: UserType.isRequired, - onChange: PropTypes.func.isRequired -}; - diff --git a/vnext/src/components/Spinner.js b/vnext/src/components/Spinner.js deleted file mode 100644 index e866369f..00000000 --- a/vnext/src/components/Spinner.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import ContentLoader from 'react-content-loader'; - -function Spinner(props) { - return ( - <div className="msg-cont"> - <div className="msg-txt"> - <ContentLoader - speed={2} - primaryColor="#f8f8f8" - secondaryColor="#ecebeb" - {...props}> - <rect x="56" y="6" rx="0" ry="0" width="117" height="6.4" /> - <rect x="56" y="20" rx="0" ry="0" width="85" height="6.4" /> - <rect x="0" y="60" rx="0" ry="0" width="270" height="6.4" /> - <rect x="0" y="78" rx="0" ry="0" width="270" height="6.4" /> - <rect x="0" y="96" rx="0" ry="0" width="201" height="6.4" /> - <rect x="0" y="0" rx="0" ry="0" width="48" height="48" /> - <rect x="0" y="120" rx="0" ry="0" width="270" height="1" /> - </ContentLoader> - </div> - </div> - ); -} - -export default React.memo(Spinner); - -export function ChatSpinner(props) { - return ( - <ContentLoader - speed={2} - primaryColor="#f8f8f8" - secondaryColor="#ecebeb" - height="60px" - width="120px" - {...props}> - <rect x="56" y="6" rx="0" ry="0" width="117" height="6.4" /> - <rect x="56" y="20" rx="0" ry="0" width="85" height="6.4" /> - <rect x="0" y="0" rx="0" ry="0" width="48" height="48" /> - </ContentLoader> - ); -} diff --git a/vnext/src/components/Thread.js b/vnext/src/components/Thread.js deleted file mode 100644 index e7ccb032..00000000 --- a/vnext/src/components/Thread.js +++ /dev/null @@ -1,181 +0,0 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; -import PropTypes from 'prop-types'; - -import ReactRouterPropTypes from 'react-router-prop-types'; -import { UserType, MessageType } from './Types'; - -import Message from './Message'; -import MessageInput from './MessageInput'; -import Spinner from './Spinner'; -import Avatar from './Avatar'; -import Button from './Button'; - -import { format, embedUrls } from '../utils/embed'; - -import { getMessages, comment, markReadTracker } from '../api'; - -function Comment({ msg, visitor, active, setActive, postComment }) { - const embedRef = useRef(); - const msgRef = useRef(); - useEffect(() => { - if (msgRef.current) { - embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current); - if (!embedRef.current.hasChildNodes()) { - embedRef.current.style.display = 'none'; - } - } - }, []); - return ( - <div className="msg-cont"> - <div className="msg-header"> - <Avatar user={msg.user}> - <div className="msg-ts"> - {msg.replyto > 0 && - ( - <a href={`#${msg.replyto}`} className="info-avatar"><img src={msg.to.avatar} /> {msg.to.uname} </a> - )} - </div> - </Avatar> - </div> - { - msg.html ? <div className="msg-txt" dangerouslySetInnerHTML={{ __html: msg.body }} ref={msgRef} /> - : - <div className="msg-txt" ref={msgRef}> - <p dangerouslySetInnerHTML={{ __html: format(msg.body, msg.mid, (msg.tags || []).indexOf('code') >= 0) }} /> - </div> - } - { - msg.photo && - <div className="msg-media"> - <a href={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} data-fname={`${msg.mid}-${msg.rid}.${msg.attach}`}> - <img src={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} alt="" /> - </a> - </div> - } - <div className="embedContainer" ref={embedRef} /> - <div className="msg-links"> - { - visitor.uid > 0 ? ( - <> - {active === msg.rid || <span style={linkStyle} onClick={() => setActive(msg.rid)}>Reply</span>} - {active === msg.rid && <MessageInput data={msg} onSend={postComment}>Write a comment...</MessageInput>} - </> - ) : ( - <> - <span> · </span>{active === msg.rid || <Button>Reply</Button>} - </> - ) - } - </div> - </div> - ); -} - -Comment.propTypes = { - msg: MessageType.isRequired, - visitor: UserType.isRequired, - active: PropTypes.number.isRequired, - setActive: PropTypes.func.isRequired, - postComment: PropTypes.func.isRequired -}; - -export default function Thread(props) { - const [message, setMessage] = useState((props.location.state || {}).msg || {}); - const [replies, setReplies] = useState([]); - const [loading, setLoading] = useState(false); - const [active, setActive] = useState(0); - useEffect(() => { - setActive(0); - loadReplies(); - }, [loadReplies]); - useEffect(() => { - if (props.connection.addEventListener && message.mid) { - props.connection.addEventListener('msg', onReply); - } - return () => { - if (props.connection.removeEventListener && message.mid) { - props.connection.removeEventListener('msg', onReply); - } - }; - }, [props.connection, message.mid, onReply]); - - let loadReplies = useCallback(() => { - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - - setReplies([]); - setLoading(true); - const { mid } = props.match.params; - let params = { - mid: mid - }; - if (props.visitor && props.visitor.hash) { - params.hash = props.visitor.hash; - } - getMessages('/api/thread', params) - .then(response => { - setMessage(response.data.shift()); - setReplies(response.data); - setLoading(false); - setActive(0); - } - ).catch(ex => { - console.log(ex); - }); - }, [props.visitor, props.match.params]); - let onReply = useCallback((json) => { - const msg = JSON.parse(json.data); - if (msg.mid == message.mid) { - setReplies(oldReplies => { - return [...oldReplies, msg]; - }); - } - }, [message]); - let postComment = (template) => { - const { mid, rid, body, attach } = template; - comment(mid, rid, body, attach).then(res => { - loadReplies(); - }) - .catch(console.log); - }; - - const loaders = Math.min(message.replies || 0, 10); - return ( - <> - { - message.mid ? ( - <Message data={message} visitor={props.visitor}> - {active === (message.rid || 0) && <MessageInput data={message} onSend={postComment}>Write a comment...</MessageInput>} - </Message> - ) : ( - <Spinner /> - ) - } - <ul id="replies"> - { - !loading ? replies.map((msg) => ( - <li id={msg.rid} key={msg.rid} className="msg"> - <Comment msg={msg} visitor={props.visitor} active={active} setActive={setActive} postComment={postComment} /> - </li> - )) : ( - <> - {Array(loaders).fill().map((it, i) => <Spinner key={i} />)} - </> - ) - } - </ul> - </> - ); -} - -const linkStyle = { - cursor: 'pointer' -}; - -Thread.propTypes = { - location: ReactRouterPropTypes.location, - history: ReactRouterPropTypes.history, - match: ReactRouterPropTypes.match, - visitor: UserType.isRequired, - connection: PropTypes.object.isRequired -}; diff --git a/vnext/src/components/Types.js b/vnext/src/components/Types.js deleted file mode 100644 index 9bf7b513..00000000 --- a/vnext/src/components/Types.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export const UserType = PropTypes.shape({ - uid: PropTypes.number.isRequired, - uname: PropTypes.string, - avatar: PropTypes.string, - uri: PropTypes.string -}); - -export const MessageType = PropTypes.shape({ - mid: PropTypes.number, - user: UserType, - timestamp: PropTypes.string.isRequired, - body: PropTypes.string -}); diff --git a/vnext/src/components/UploadButton.js b/vnext/src/components/UploadButton.js deleted file mode 100644 index 73cbbfcf..00000000 --- a/vnext/src/components/UploadButton.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Icon from './Icon'; - -export default function UploadButton(props) { - let openfile = () => { - const input = props.inputRef.current; - if (props.value) { - props.onChange(''); - } else { - input.click(); - } - }; - let attachmentChanged = (event) => { - props.onChange(event.target.value); - }; - return ( - <div style={props.value ? activeStyle : inactiveStyle} - onClick={openfile}> - <Icon name="ei-camera" size="s" /> - <input type="file" accept="image/jpeg,image/png" onClick={e => e.stopPropagation()} - style={{ display: 'none' }} ref={props.inputRef} value={props.value} - onChange={attachmentChanged} /> - </div> - ); -} - -UploadButton.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - inputRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }) -}; - -const inactiveStyle = { - cursor: 'pointer', - color: '#888' -}; -const activeStyle = { - cursor: 'pointer', - color: 'green' -}; diff --git a/vnext/src/components/UserInfo.css b/vnext/src/components/UserInfo.css deleted file mode 100644 index 92cfdb6c..00000000 --- a/vnext/src/components/UserInfo.css +++ /dev/null @@ -1,11 +0,0 @@ -.userinfo { - padding: 40px; - background-color: #fdfdfe; - margin: 12px; -} -.info-avatar img { - max-height: 24px; - max-width: 24px; - padding: 6px; - vertical-align: middle; -} diff --git a/vnext/src/components/UserInfo.js b/vnext/src/components/UserInfo.js deleted file mode 100644 index 7d84488e..00000000 --- a/vnext/src/components/UserInfo.js +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { UserType } from './Types'; - -import { info, fetchUserUri } from '../api'; - -import Avatar from './Avatar'; -import Icon from './Icon'; -import SearchBox from './SearchBox'; - -import './UserInfo.css'; - -let isMounted; - -export default function UserInfo(props) { - const [user, setUser] = useState({ uname: props.user, uid: 0 }); - const { onUpdate } = props; - useEffect(() => { - isMounted = true; - if (!user.avatar) { - info(user.uname).then(response => { - if (isMounted) { - setUser(response.data); - onUpdate && onUpdate(response.data); - } - }); - } - return () => { - isMounted = false; - }; - }, [onUpdate, user, user.avatar]); - return ( - <> - <div className="userinfo"> - <Avatar user={user}> - <div className="msg-ts">Was online recently</div> - </Avatar> - </div> - <UserSummary user={user} /> - <div className="l"> - { - user.uid > 0 && - <> - <Link to={`/pm/${user.uname}`}> - <Icon name="ei-envelope" size="s" /> - <span className="desktop">PM</span> - </Link> - <Link to={`/${user.uname}/?show=recomm`} rel="nofollow"> - <Icon name="ei-heart" size="s" /> - <span className="desktop">Recommendations</span> - </Link> - <Link to={`/${user.uname}/?media=1`} rel="nofollow"> - <Icon name="ei-camera" size="s" /> - <span className="desktop">Photos</span> - </Link> - </> - } - </div> - {props.children} - </> - ); -} - -function Summary({ user }) { - const readUrl = `/${user.uname}/friends`; - const readersUrl = `/${user.uname}/readers`; - const blUrl = `/${user.uname}/bl`; - let read = user.read && <Link key={readUrl} to={readUrl}>I read: {user.read.length}</Link>; - let readers = user.readers && <Link key={readersUrl} to={readersUrl}>My readers: {user.readers.length}</Link>; - let mybl = user.statsMyBL && <Link key={blUrl} to={blUrl}>My blacklist: {user.statsMyBL}</Link>; - let presentItems = [read, readers, mybl].filter(Boolean); - return ( - <div className="msg-summary"> - {presentItems.length > 0 && presentItems.reduce((prev, curr) => [prev, ' ', curr])} - </div> - ); -} - -Summary.propTypes = { - user: UserType.isRequired -}; - -const UserSummary = React.memo(Summary); - -export function UserLink(props) { - const [user, setUser] = useState(props.user); - useEffect(() => { - isMounted = true; - if (!user.uid && user.uri) { - fetchUserUri(user.uri).then(response => { - if (isMounted) { - setUser({ ...response.data, uid: 66666666 }); - } - }); - } - return () => { - isMounted = false; - }; - }, [user.uid, user.uri]); - return ( - user.uid ? - <Link key={user.uid} to={`/${user.uname}/`} className="info-avatar"><img src={user.avatar} />{user.uname}</Link> - : <a href={user.uri} className="info-avatar"><img src={user.avatar} />{user.uname}</a> - ); -} - -UserInfo.propTypes = { - user: PropTypes.string.isRequired, - onUpdate: PropTypes.func, - children: PropTypes.node -}; - -UserLink.propTypes = { - user: UserType.isRequired -}; diff --git a/vnext/src/components/Users.js b/vnext/src/components/Users.js deleted file mode 100644 index a10bba7f..00000000 --- a/vnext/src/components/Users.js +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import ReactRouterPropTypes from 'react-router-prop-types'; - -import UserInfo from './UserInfo'; -import Avatar from './Avatar'; - -export function Friends({ match }) { - return <Users user={match.params.user} prop='read' />; -} - -export function Readers({ match }) { - return <Users user={match.params.user} prop='readers' />; -} - -function Users(props) { - const [user, setUser] = useState({ uid: 0, uname: props.user }); - return ( - <UserInfo user={user.uname} onUpdate={setUser}> - <div style={{ display: 'flex', flexWrap: 'wrap', flexDirection: 'row' }}> - { - user[props.prop] && - user[props.prop].map(user => - <Avatar key={user.uid} user={user} /> - ) - } - </div> - </UserInfo> - ); -} - - -Friends.propTypes = { - match: ReactRouterPropTypes.match.isRequired -}; - -Readers.propTypes = { - match: ReactRouterPropTypes.match.isRequired -}; - -Users.propTypes = { - user: PropTypes.string.isRequired, - prop: PropTypes.string.isRequired -}; diff --git a/vnext/src/components/__tests__/Avatar.test.js b/vnext/src/components/__tests__/Avatar.test.js deleted file mode 100644 index e7221871..00000000 --- a/vnext/src/components/__tests__/Avatar.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; - -import Avatar from '../Avatar'; -import renderer from 'react-test-renderer'; - -test('Avatar renders correctly', () => { - const component = renderer.create( - <MemoryRouter> - <Avatar user={{ uid: 1, uname: 'ugnich', avatar: 'https://juick.com/i/a/1-deadbeef.png' }} /> - </MemoryRouter> - ); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/vnext/src/components/__tests__/LoginButton.test.js b/vnext/src/components/__tests__/LoginButton.test.js deleted file mode 100644 index da80abb0..00000000 --- a/vnext/src/components/__tests__/LoginButton.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import LoginButton from '../LoginButton'; -import { create, act } from 'react-test-renderer'; - -test('Login button and form are renders correctly', () => { - var button = null; - act(() => { - button = create( - <LoginButton title="Log in" onAuth={() => { }} /> - ); - }); - let link = button.toJSON(); - expect(link).toMatchSnapshot(); - - act(() => { - button.root.findByType('a').props.onClick(); - }); - let modal = button.toJSON(); - expect(modal).toMatchSnapshot(); -}); diff --git a/vnext/src/components/__tests__/MessageInput-test.js b/vnext/src/components/__tests__/MessageInput-test.js deleted file mode 100644 index 7ac69ed0..00000000 --- a/vnext/src/components/__tests__/MessageInput-test.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { create, act } from 'react-test-renderer'; - -import MessageInput from '../MessageInput'; - -const testMessage = { - mid: 1, - rid: 0, - body: 'test message', - timestamp: new Date().toISOString(), - attach: '', - to: {} -}; - -window.matchMedia = window.matchMedia || function () { - return { - matches: true, - addListener: function () { }, - removeListener: function () { } - }; -}; - -it('Gives immediate focus on to textarea on load', () => { - let focused = false; - act(() => { - create(<MessageInput data={testMessage} onSend={() => { }} />, { - createNodeMock: (element) => { - if (element.type === 'textarea') { - // mock a focus function - return { - focus: () => { - focused = true; - }, - style: {} - }; - } - return null; - } - }); - }); - expect(focused).toEqual(true, 'textarea was not focused'); -}); - - -it('Submits on ctrl-enter', () => { - const onSend = jest.fn(); - var messageInput = null; - act(() => { - messageInput = create(<MessageInput data={testMessage} onSend={onSend} />, { - createNodeMock: (element) => { - if (element.type === 'textarea') { - return { - focus: () => { }, - style: {} - }; - } - return null; - } - }); - }); - let textarea = messageInput.root.findByType('textarea'); - act(() => { - - textarea.props.onKeyPress({ - charCode: 13, - which: 13, - keyCode: 13, - ctrlKey: false - }); - }); - expect(onSend).toHaveBeenCalledTimes(0); - act(() => { - textarea.props.onKeyPress({ - charCode: 13, - which: 13, - keyCode: 13, - ctrlKey: true - }); - }); - expect(onSend).toHaveBeenCalledTimes(1); - expect(textarea.props.value).toEqual(''); - act(() => { - textarea.props.onChange({ - target: { - value: ' ', - validity: {} - } - }); - }); - expect(textarea.props.value).toEqual(' '); - act(() => { - messageInput.root.findByType('form').props.onSubmit({ event: {} }); - }); - expect(textarea.props.value).toEqual('', 'Value should be cleared after submit'); -}); diff --git a/vnext/src/components/__tests__/UserLink.test.js b/vnext/src/components/__tests__/UserLink.test.js deleted file mode 100644 index a75344b0..00000000 --- a/vnext/src/components/__tests__/UserLink.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { MemoryRouter, Switch, Route } from 'react-router-dom'; - -import { UserLink } from '../UserInfo'; -import renderer from 'react-test-renderer'; - -test('UserLink renders correctly', async () => { - const component = renderer.create( - <MemoryRouter> - <> - <UserLink user={{ uid: 1, uname: 'ugnich', avatar: 'https://juick.com/i/a/1-deadbeef.png' }} /> - <UserLink user={{ uid: 1, uname: 'ugnich', avatar: 'https://juick.com/i/a/1-deadbeef.png', uri: '' }} /> - <UserLink user={{ uid: 0, uname: '', uri: 'https://example.com/u/test' }} /> - </> - </MemoryRouter> - ); - await Promise.resolve(); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/vnext/src/components/__tests__/__snapshots__/Avatar.test.js.snap b/vnext/src/components/__tests__/__snapshots__/Avatar.test.js.snap deleted file mode 100644 index 47614f5a..00000000 --- a/vnext/src/components/__tests__/__snapshots__/Avatar.test.js.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Avatar renders correctly 1`] = ` -<div - className="Avatar" -> - <div - className="msg-avatar" - > - <a - href="/ugnich/" - onClick={[Function]} - > - <img - alt="ugnich" - src="https://juick.com/i/a/1-deadbeef.png" - /> - </a> - </div> - <div - style={ - Object { - "display": "flex", - "flexDirection": "column", - "justifyContent": "center", - } - } - > - <span> - <a - href="/ugnich/" - onClick={[Function]} - > - <span> - ugnich - </span> - </a> - </span> - </div> -</div> -`; diff --git a/vnext/src/components/__tests__/__snapshots__/LoginButton.test.js.snap b/vnext/src/components/__tests__/__snapshots__/LoginButton.test.js.snap deleted file mode 100644 index cd08b1b4..00000000 --- a/vnext/src/components/__tests__/__snapshots__/LoginButton.test.js.snap +++ /dev/null @@ -1,178 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Login button and form are renders correctly 1`] = ` -<a - onClick={[Function]} -> - <div - className="icon icon--ei-user icon--s" - > - <svg - className="icon__cnt" - dangerouslySetInnerHTML={ - Object { - "__html": "<use xlink:href=#ei-user-icon />", - } - } - /> - </div> - <span - className="desktop" - > - Log in - </span> -</a> -`; - -exports[`Login button and form are renders correctly 2`] = ` -Array [ - <a - onClick={[Function]} - > - <div - className="icon icon--ei-user icon--s" - > - <svg - className="icon__cnt" - dangerouslySetInnerHTML={ - Object { - "__html": "<use xlink:href=#ei-user-icon />", - } - } - /> - </div> - <span - className="desktop" - > - Log in - </span> - </a>, - <div - id="dialogt" - > - <div - id="dialogw" - > - <div - id="dialog_header" - > - <div - id="dialogc" - onClick={[Function]} - > - <div - className="icon icon--ei-close icon--s" - > - <svg - className="icon__cnt" - dangerouslySetInnerHTML={ - Object { - "__html": "<use xlink:href=#ei-close-icon />", - } - } - /> - </div> - </div> - </div> - <div - className="dialoglogin" - > - <p> - Please, introduce yourself: - </p> - <div - style={ - Object { - "display": "flex", - "justifyContent": "space-evenly", - "padding": "4px", - } - } - > - <a - href="https://juick.com/api/_fblogin?state=http://localhost/" - style={ - Object { - "background": "#3b5998", - "color": "#fff", - "padding": "2px 14px", - } - } - > - <div - className="icon icon--s" - > - <svg - className="icon__cnt" - dangerouslySetInnerHTML={ - Object { - "__html": "<use xlink:href=#ei-sc-facebook-icon />", - } - } - /> - </div> - Log in - </a> - <a - href="https://juick.com/api/_vklogin?state=http://localhost/" - style={ - Object { - "background": "#4c75a3", - "color": "#fff", - "padding": "2px 14px", - } - } - > - <div - className="icon icon--s" - > - <svg - className="icon__cnt" - dangerouslySetInnerHTML={ - Object { - "__html": "<use xlink:href=#ei-sc-vk-icon />", - } - } - /> - </div> - Log in - </a> - </div> - <p> - Already registered? - </p> - <form - onSubmit={[Function]} - > - <input - className="input" - name="username" - onBlur={[Function]} - onChange={[Function]} - placeholder="Username..." - type="text" - value="" - /> - <br /> - <input - className="input" - name="password" - onBlur={[Function]} - onChange={[Function]} - placeholder="Password..." - type="password" - value="" - /> - <br /> - <button - className="Button" - onClick={[Function]} - > - OK - </button> - </form> - </div> - </div> - </div>, -] -`; diff --git a/vnext/src/components/__tests__/__snapshots__/UserLink.test.js.snap b/vnext/src/components/__tests__/__snapshots__/UserLink.test.js.snap deleted file mode 100644 index 15e25367..00000000 --- a/vnext/src/components/__tests__/__snapshots__/UserLink.test.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UserLink renders correctly 1`] = ` -Array [ - <a - className="info-avatar" - href="/ugnich/" - onClick={[Function]} - > - <img - src="https://juick.com/i/a/1-deadbeef.png" - /> - ugnich - </a>, - <a - className="info-avatar" - href="/ugnich/" - onClick={[Function]} - > - <img - src="https://juick.com/i/a/1-deadbeef.png" - /> - ugnich - </a>, - <a - className="info-avatar" - href="https://example.com/u/test" - > - <img /> - - </a>, -] -`; |