From f470636a70943a8ecad8bddc791a1c2dddd28e1e Mon Sep 17 00:00:00 2001
From: Vitaly Takmazov
Date: Sat, 4 May 2019 21:13:12 +0300
Subject: Components -> UI
---
vnext/src/App.js | 24 +-
vnext/src/components/Avatar.css | 27 ---
vnext/src/components/Avatar.js | 44 ----
vnext/src/components/Button.css | 13 -
vnext/src/components/Button.js | 11 -
vnext/src/components/Chat.css | 9 -
vnext/src/components/Chat.js | 85 -------
vnext/src/components/Contact.js | 21 --
vnext/src/components/Contacts.js | 49 ----
vnext/src/components/Feeds.js | 171 -------------
vnext/src/components/Header.js | 69 ------
vnext/src/components/Icon.js | 38 ---
vnext/src/components/Input.css | 8 -
vnext/src/components/Input.js | 17 --
vnext/src/components/LoginButton.js | 90 -------
vnext/src/components/Message.css | 210 ----------------
vnext/src/components/Message.js | 142 -----------
vnext/src/components/MessageInput.js | 107 --------
vnext/src/components/Modal.css | 95 --------
vnext/src/components/Modal.js | 29 ---
vnext/src/components/NavigationIcon.css | 4 -
vnext/src/components/NavigationIcon.js | 21 --
vnext/src/components/PM.js | 51 ----
vnext/src/components/Post.js | 38 ---
vnext/src/components/SearchBox.js | 26 --
vnext/src/components/Settings.js | 268 ---------------------
vnext/src/components/Spinner.js | 42 ----
vnext/src/components/Thread.js | 181 --------------
vnext/src/components/Types.js | 15 --
vnext/src/components/UploadButton.js | 42 ----
vnext/src/components/UserInfo.css | 11 -
vnext/src/components/UserInfo.js | 117 ---------
vnext/src/components/Users.js | 44 ----
vnext/src/components/__tests__/Avatar.test.js | 15 --
vnext/src/components/__tests__/LoginButton.test.js | 21 --
.../src/components/__tests__/MessageInput-test.js | 95 --------
vnext/src/components/__tests__/UserLink.test.js | 20 --
.../__tests__/__snapshots__/Avatar.test.js.snap | 41 ----
.../__snapshots__/LoginButton.test.js.snap | 178 --------------
.../__tests__/__snapshots__/UserLink.test.js.snap | 33 ---
vnext/src/index.html | 63 +++++
vnext/src/ui/Avatar.css | 27 +++
vnext/src/ui/Avatar.js | 44 ++++
vnext/src/ui/Button.css | 13 +
vnext/src/ui/Button.js | 11 +
vnext/src/ui/Chat.css | 9 +
vnext/src/ui/Chat.js | 85 +++++++
vnext/src/ui/Contact.js | 21 ++
vnext/src/ui/Contacts.js | 49 ++++
vnext/src/ui/Feeds.js | 171 +++++++++++++
vnext/src/ui/Header.js | 69 ++++++
vnext/src/ui/Icon.js | 38 +++
vnext/src/ui/Input.css | 8 +
vnext/src/ui/Input.js | 17 ++
vnext/src/ui/LoginButton.js | 90 +++++++
vnext/src/ui/Message.css | 210 ++++++++++++++++
vnext/src/ui/Message.js | 142 +++++++++++
vnext/src/ui/MessageInput.js | 107 ++++++++
vnext/src/ui/Modal.css | 95 ++++++++
vnext/src/ui/Modal.js | 29 +++
vnext/src/ui/NavigationIcon.css | 4 +
vnext/src/ui/NavigationIcon.js | 21 ++
vnext/src/ui/PM.js | 51 ++++
vnext/src/ui/Post.js | 38 +++
vnext/src/ui/SearchBox.js | 26 ++
vnext/src/ui/Settings.js | 268 +++++++++++++++++++++
vnext/src/ui/Spinner.js | 42 ++++
vnext/src/ui/Thread.js | 181 ++++++++++++++
vnext/src/ui/Types.js | 15 ++
vnext/src/ui/UploadButton.js | 42 ++++
vnext/src/ui/UserInfo.css | 11 +
vnext/src/ui/UserInfo.js | 117 +++++++++
vnext/src/ui/Users.js | 44 ++++
vnext/src/ui/__tests__/Avatar.test.js | 15 ++
vnext/src/ui/__tests__/LoginButton.test.js | 21 ++
vnext/src/ui/__tests__/MessageInput-test.js | 95 ++++++++
vnext/src/ui/__tests__/UserLink.test.js | 20 ++
.../ui/__tests__/__snapshots__/Avatar.test.js.snap | 41 ++++
.../__snapshots__/LoginButton.test.js.snap | 178 ++++++++++++++
.../__tests__/__snapshots__/UserLink.test.js.snap | 33 +++
vnext/src/views/index.html | 63 -----
81 files changed, 2573 insertions(+), 2573 deletions(-)
delete mode 100644 vnext/src/components/Avatar.css
delete mode 100644 vnext/src/components/Avatar.js
delete mode 100644 vnext/src/components/Button.css
delete mode 100644 vnext/src/components/Button.js
delete mode 100644 vnext/src/components/Chat.css
delete mode 100644 vnext/src/components/Chat.js
delete mode 100644 vnext/src/components/Contact.js
delete mode 100644 vnext/src/components/Contacts.js
delete mode 100644 vnext/src/components/Feeds.js
delete mode 100644 vnext/src/components/Header.js
delete mode 100644 vnext/src/components/Icon.js
delete mode 100644 vnext/src/components/Input.css
delete mode 100644 vnext/src/components/Input.js
delete mode 100644 vnext/src/components/LoginButton.js
delete mode 100644 vnext/src/components/Message.css
delete mode 100644 vnext/src/components/Message.js
delete mode 100644 vnext/src/components/MessageInput.js
delete mode 100644 vnext/src/components/Modal.css
delete mode 100644 vnext/src/components/Modal.js
delete mode 100644 vnext/src/components/NavigationIcon.css
delete mode 100644 vnext/src/components/NavigationIcon.js
delete mode 100644 vnext/src/components/PM.js
delete mode 100644 vnext/src/components/Post.js
delete mode 100644 vnext/src/components/SearchBox.js
delete mode 100644 vnext/src/components/Settings.js
delete mode 100644 vnext/src/components/Spinner.js
delete mode 100644 vnext/src/components/Thread.js
delete mode 100644 vnext/src/components/Types.js
delete mode 100644 vnext/src/components/UploadButton.js
delete mode 100644 vnext/src/components/UserInfo.css
delete mode 100644 vnext/src/components/UserInfo.js
delete mode 100644 vnext/src/components/Users.js
delete mode 100644 vnext/src/components/__tests__/Avatar.test.js
delete mode 100644 vnext/src/components/__tests__/LoginButton.test.js
delete mode 100644 vnext/src/components/__tests__/MessageInput-test.js
delete mode 100644 vnext/src/components/__tests__/UserLink.test.js
delete mode 100644 vnext/src/components/__tests__/__snapshots__/Avatar.test.js.snap
delete mode 100644 vnext/src/components/__tests__/__snapshots__/LoginButton.test.js.snap
delete mode 100644 vnext/src/components/__tests__/__snapshots__/UserLink.test.js.snap
create mode 100644 vnext/src/index.html
create mode 100644 vnext/src/ui/Avatar.css
create mode 100644 vnext/src/ui/Avatar.js
create mode 100644 vnext/src/ui/Button.css
create mode 100644 vnext/src/ui/Button.js
create mode 100644 vnext/src/ui/Chat.css
create mode 100644 vnext/src/ui/Chat.js
create mode 100644 vnext/src/ui/Contact.js
create mode 100644 vnext/src/ui/Contacts.js
create mode 100644 vnext/src/ui/Feeds.js
create mode 100644 vnext/src/ui/Header.js
create mode 100644 vnext/src/ui/Icon.js
create mode 100644 vnext/src/ui/Input.css
create mode 100644 vnext/src/ui/Input.js
create mode 100644 vnext/src/ui/LoginButton.js
create mode 100644 vnext/src/ui/Message.css
create mode 100644 vnext/src/ui/Message.js
create mode 100644 vnext/src/ui/MessageInput.js
create mode 100644 vnext/src/ui/Modal.css
create mode 100644 vnext/src/ui/Modal.js
create mode 100644 vnext/src/ui/NavigationIcon.css
create mode 100644 vnext/src/ui/NavigationIcon.js
create mode 100644 vnext/src/ui/PM.js
create mode 100644 vnext/src/ui/Post.js
create mode 100644 vnext/src/ui/SearchBox.js
create mode 100644 vnext/src/ui/Settings.js
create mode 100644 vnext/src/ui/Spinner.js
create mode 100644 vnext/src/ui/Thread.js
create mode 100644 vnext/src/ui/Types.js
create mode 100644 vnext/src/ui/UploadButton.js
create mode 100644 vnext/src/ui/UserInfo.css
create mode 100644 vnext/src/ui/UserInfo.js
create mode 100644 vnext/src/ui/Users.js
create mode 100644 vnext/src/ui/__tests__/Avatar.test.js
create mode 100644 vnext/src/ui/__tests__/LoginButton.test.js
create mode 100644 vnext/src/ui/__tests__/MessageInput-test.js
create mode 100644 vnext/src/ui/__tests__/UserLink.test.js
create mode 100644 vnext/src/ui/__tests__/__snapshots__/Avatar.test.js.snap
create mode 100644 vnext/src/ui/__tests__/__snapshots__/LoginButton.test.js.snap
create mode 100644 vnext/src/ui/__tests__/__snapshots__/UserLink.test.js.snap
delete mode 100644 vnext/src/views/index.html
(limited to 'vnext/src')
diff --git a/vnext/src/App.js b/vnext/src/App.js
index 17f87e45..1f7e715e 100644
--- a/vnext/src/App.js
+++ b/vnext/src/App.js
@@ -2,18 +2,18 @@ import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
import qs from 'qs';
-import Icon from './components/Icon';
-import { Discover, Discussions, Blog, Tag, Home } from './components/Feeds';
-import { Friends, Readers } from './components/Users';
-import Settings from './components/Settings';
-import Contacts from './components/Contacts';
-import Chat from './components/Chat';
-import Post from './components/Post';
-import Thread from './components/Thread';
-import LoginButton from './components/LoginButton';
-import { UserLink } from './components/UserInfo';
-import Header from './components/Header';
-import SearchBox from './components/SearchBox';
+import Icon from './ui/Icon';
+import { Discover, Discussions, Blog, Tag, Home } from './ui/Feeds';
+import { Friends, Readers } from './ui/Users';
+import Settings from './ui/Settings';
+import Contacts from './ui/Contacts';
+import Chat from './ui/Chat';
+import Post from './ui/Post';
+import Thread from './ui/Thread';
+import LoginButton from './ui/LoginButton';
+import { UserLink } from './ui/UserInfo';
+import Header from './ui/Header';
+import SearchBox from './ui/SearchBox';
import cookies from 'react-cookies';
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 (
-
-
- {
- user.uname ?
-
- {user.avatar ?
-
- :
}
-
- :
- }
-
-
-
-
- {user.uname}
-
-
- {children}
-
-
- );
-}
-
-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 (
-
- );
-}
-
-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 (
-
-
- {uname ? (
-
-
- {
- chats.map((chat) =>
-
- )
- }
-
-
- Reply...
-
-
- ) : (
-
- )
- }
-
- );
-}
-
-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 (
-
- {user.unreadCount && {user.unreadCount}}
- {user.lastMessageText}
-
- );
-}
-
-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 (
-
-
- {
- pms.length ? pms.map((chat) =>
-
- ) : <>>
- }
-
-
- );
-}
-
-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 ();
-}
-
-export function Discussions(props) {
- const query = {
- baseUrl: '/api/messages/discussions',
- pageParam: 'to'
- };
- return ();
-}
-
-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 (
- <>
-
-
-
-
- >
- );
-}
-
-export function Tag(props) {
- const { tag } = props.match.params;
- const query = {
- baseUrl: '/api/messages',
- search: {
- tag: tag
- },
- pageParam: 'before_mid'
- };
- return ();
-}
-
-export function Home(props) {
- const query = {
- baseUrl: '/api/home',
- pageParam: 'before_mid'
- };
- return ();
-}
-
-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 && (
-
-
- ← All posts with tag {tag}
-
-
- )
- }
- {
- msgs.map(msg =>
- )
- }
- {
- msgs.length >= 20 && (
-
- Next →
-
- )
- }
- >
- );
- return msgs.length > 0 ? (
- {nodes}
- ) : error ? error
: loading ?
: No more messages
;
-}
-
-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 = '';
- 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.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 (
- <>
-
-
- {title}
-
-
-
-
Please, introduce yourself:
-
-
Already registered?
-
-
-
- >
- );
-}
-
-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 (
-
-
-
- {
- data.body &&
-
-
-
- }
- {
- data.photo &&
-
- }
-
-
- {children}
-
- );
-}
-
-function MessageContainer({ isCode, data }) {
- return isCode ? () : ();
-}
-
-function Tags({ data, user, ...rest }) {
- return data.length > 0 && (
-
- {
- data.map(tag => {
- return ({tag});
- }).reduce((prev, curr) => [prev, ', ', curr])
- }
-
- );
-}
-
-const TagsList = React.memo(Tags);
-
-function Recommends({ forMessage, ...rest }) {
- const { likes, recommendations } = forMessage;
- return recommendations && recommendations.length > 0 && (
- {'♡ by '}
- {
- recommendations.map(it => (
-
- )).reduce((prev, curr) => [prev, ', ', curr])
- }
- {
- likes > recommendations.length && ( and {likes - recommendations.length} others)
- }
-
- ) || 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 (
-
- );
-}
-
-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 ? (
-
-
-
- {props.children}
-
-
- ) : (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 (
-
-
-
- );
-}
-
-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 (
-
-
-
- );
-}
-
-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 (
-
-
- *weather It is very cold today!
-
-
- );
-}
-
-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 (
-
- );
-}
-
-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: '' };
- if (preview) {
- previewUser = { ...visitor, avatar: preview, uname: '' };
- }
- let onSubmitAvatar = (event) => {
- if (event.preventDefault) {
- event.preventDefault();
- }
- updateAvatar(avatarInput.current.files[0]).then(() => {
- avatarChanged('');
- me().then(visitor => {
- this.props.onChange(visitor);
- });
- });
- };
- return (
-
- );
-}
-
-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 (
-
-
-
-
- {me.jids && (
-
- )}
-
-
-
-
-
- );
- }
-}
-
-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 (
-
- );
-}
-
-export default React.memo(Spinner);
-
-export function ChatSpinner(props) {
- return (
-
-
-
-
-
- );
-}
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 (
-
-
- {
- msg.html ?
- :
-
- }
- {
- msg.photo &&
-
- }
-
-
- {
- visitor.uid > 0 ? (
- <>
- {active === msg.rid || setActive(msg.rid)}>Reply}
- {active === msg.rid && Write a comment...}
- >
- ) : (
- <>
- · {active === msg.rid || }
- >
- )
- }
-
-
- );
-}
-
-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 ? (
-
- {active === (message.rid || 0) && Write a comment...}
-
- ) : (
-
- )
- }
-
- {
- !loading ? replies.map((msg) => (
- -
-
-
- )) : (
- <>
- {Array(loaders).fill().map((it, i) => )}
- >
- )
- }
-
- >
- );
-}
-
-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 (
-
-
- e.stopPropagation()}
- style={{ display: 'none' }} ref={props.inputRef} value={props.value}
- onChange={attachmentChanged} />
-
- );
-}
-
-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 (
- <>
-
-
- Was online recently
-
-
-
-
- {
- user.uid > 0 &&
- <>
-
-
- PM
-
-
-
- Recommendations
-
-
-
- Photos
-
- >
- }
-
- {props.children}
- >
- );
-}
-
-function Summary({ user }) {
- const readUrl = `/${user.uname}/friends`;
- const readersUrl = `/${user.uname}/readers`;
- const blUrl = `/${user.uname}/bl`;
- let read = user.read && I read: {user.read.length};
- let readers = user.readers && My readers: {user.readers.length};
- let mybl = user.statsMyBL && My blacklist: {user.statsMyBL};
- let presentItems = [read, readers, mybl].filter(Boolean);
- return (
-
- {presentItems.length > 0 && presentItems.reduce((prev, curr) => [prev, ' ', curr])}
-
- );
-}
-
-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 ?
- {user.uname}
- : {user.uname}
- );
-}
-
-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 ;
-}
-
-export function Readers({ match }) {
- return ;
-}
-
-function Users(props) {
- const [user, setUser] = useState({ uid: 0, uname: props.user });
- return (
-
-
- {
- user[props.prop] &&
- user[props.prop].map(user =>
-
- )
- }
-
-
- );
-}
-
-
-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(
-
-
-
- );
- 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(
- { }} />
- );
- });
- 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( { }} />, {
- 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(, {
- 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(
-
- <>
-
-
-
- >
-
- );
- 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`] = `
-
-`;
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`] = `
-
-
- ",
- }
- }
- />
-
-
- Log in
-
-
-`;
-
-exports[`Login button and form are renders correctly 2`] = `
-Array [
-
-
- ",
- }
- }
- />
-
-
- Log in
-
- ,
-
-
-
-
-
- Please, introduce yourself:
-
-
-
- Already registered?
-
-
-
-
-
,
-]
-`;
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 [
-
-
- ugnich
- ,
-
-
- ugnich
- ,
-
-
-
- ,
-]
-`;
diff --git a/vnext/src/index.html b/vnext/src/index.html
new file mode 100644
index 00000000..d3e185b5
--- /dev/null
+++ b/vnext/src/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vnext/src/ui/Avatar.css b/vnext/src/ui/Avatar.css
new file mode 100644
index 00000000..7bdb3115
--- /dev/null
+++ b/vnext/src/ui/Avatar.css
@@ -0,0 +1,27 @@
+.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/ui/Avatar.js b/vnext/src/ui/Avatar.js
new file mode 100644
index 00000000..dda5449f
--- /dev/null
+++ b/vnext/src/ui/Avatar.js
@@ -0,0 +1,44 @@
+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 (
+
+
+ {
+ user.uname ?
+
+ {user.avatar ?
+
+ :
}
+
+ :
+ }
+
+
+
+
+ {user.uname}
+
+
+ {children}
+
+
+ );
+}
+
+export default memo(Avatar);
+
+Avatar.propTypes = {
+ user: UserType,
+ link: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node
+};
diff --git a/vnext/src/ui/Button.css b/vnext/src/ui/Button.css
new file mode 100644
index 00000000..2acb87be
--- /dev/null
+++ b/vnext/src/ui/Button.css
@@ -0,0 +1,13 @@
+.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/ui/Button.js b/vnext/src/ui/Button.js
new file mode 100644
index 00000000..18cab0a7
--- /dev/null
+++ b/vnext/src/ui/Button.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import './Button.css';
+
+function Button(props) {
+ return (
+
+ );
+}
+
+export default React.memo(Button);
diff --git a/vnext/src/ui/Chat.css b/vnext/src/ui/Chat.css
new file mode 100644
index 00000000..520a5c9b
--- /dev/null
+++ b/vnext/src/ui/Chat.css
@@ -0,0 +1,9 @@
+.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/ui/Chat.js b/vnext/src/ui/Chat.js
new file mode 100644
index 00000000..a1254a10
--- /dev/null
+++ b/vnext/src/ui/Chat.js
@@ -0,0 +1,85 @@
+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 (
+
+
+ {uname ? (
+
+
+ {
+ chats.map((chat) =>
+
+ )
+ }
+
+
+ Reply...
+
+
+ ) : (
+
+ )
+ }
+
+ );
+}
+
+Chat.propTypes = {
+ visitor: UserType.isRequired,
+ match: ReactRouterPropTypes.match.isRequired,
+ connection: PropTypes.object.isRequired
+};
diff --git a/vnext/src/ui/Contact.js b/vnext/src/ui/Contact.js
new file mode 100644
index 00000000..24aabe15
--- /dev/null
+++ b/vnext/src/ui/Contact.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { UserType } from './Types';
+
+import Avatar from './Avatar';
+
+function Contact({ user, style, ...rest }) {
+ return (
+
+ {user.unreadCount && {user.unreadCount}}
+ {user.lastMessageText}
+
+ );
+}
+
+export default React.memo(Contact);
+
+Contact.propTypes = {
+ user: UserType,
+ style: PropTypes.object
+};
diff --git a/vnext/src/ui/Contacts.js b/vnext/src/ui/Contacts.js
new file mode 100644
index 00000000..3852b26f
--- /dev/null
+++ b/vnext/src/ui/Contacts.js
@@ -0,0 +1,49 @@
+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 (
+
+
+ {
+ pms.length ? pms.map((chat) =>
+
+ ) : <>>
+ }
+
+
+ );
+}
+
+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/ui/Feeds.js b/vnext/src/ui/Feeds.js
new file mode 100644
index 00000000..c7b857b7
--- /dev/null
+++ b/vnext/src/ui/Feeds.js
@@ -0,0 +1,171 @@
+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 ();
+}
+
+export function Discussions(props) {
+ const query = {
+ baseUrl: '/api/messages/discussions',
+ pageParam: 'to'
+ };
+ return ();
+}
+
+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 (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export function Tag(props) {
+ const { tag } = props.match.params;
+ const query = {
+ baseUrl: '/api/messages',
+ search: {
+ tag: tag
+ },
+ pageParam: 'before_mid'
+ };
+ return ();
+}
+
+export function Home(props) {
+ const query = {
+ baseUrl: '/api/home',
+ pageParam: 'before_mid'
+ };
+ return ();
+}
+
+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 && (
+
+
+ ← All posts with tag {tag}
+
+
+ )
+ }
+ {
+ msgs.map(msg =>
+ )
+ }
+ {
+ msgs.length >= 20 && (
+
+ Next →
+
+ )
+ }
+ >
+ );
+ return msgs.length > 0 ? (
+ {nodes}
+ ) : error ? error
: loading ?
: No more messages
;
+}
+
+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/ui/Header.js b/vnext/src/ui/Header.js
new file mode 100644
index 00000000..48f89360
--- /dev/null
+++ b/vnext/src/ui/Header.js
@@ -0,0 +1,69 @@
+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/ui/Icon.js b/vnext/src/ui/Icon.js
new file mode 100644
index 00000000..faf1a704
--- /dev/null
+++ b/vnext/src/ui/Icon.js
@@ -0,0 +1,38 @@
+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 = '';
+ 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/ui/Input.css b/vnext/src/ui/Input.css
new file mode 100644
index 00000000..dbe55eae
--- /dev/null
+++ b/vnext/src/ui/Input.css
@@ -0,0 +1,8 @@
+.input {
+ background: #FFF;
+ border: 1px solid #ccc;
+ outline: none !important;
+ padding: 4px;
+ -webkit-appearance: none;
+ border-radius: 0;
+}
diff --git a/vnext/src/ui/Input.js b/vnext/src/ui/Input.js
new file mode 100644
index 00000000..c74d595d
--- /dev/null
+++ b/vnext/src/ui/Input.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import './Input.css';
+
+function Input({ name, value, ...rest }) {
+ return (
+
+ );
+}
+
+Input.propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+};
+
+export default React.memo(Input);
diff --git a/vnext/src/ui/LoginButton.js b/vnext/src/ui/LoginButton.js
new file mode 100644
index 00000000..cd26252e
--- /dev/null
+++ b/vnext/src/ui/LoginButton.js
@@ -0,0 +1,90 @@
+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 (
+ <>
+
+
+ {title}
+
+
+
+
Please, introduce yourself:
+
+
Already registered?
+
+
+
+ >
+ );
+}
+
+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/ui/Message.css b/vnext/src/ui/Message.css
new file mode 100644
index 00000000..18d3d0d5
--- /dev/null
+++ b/vnext/src/ui/Message.css
@@ -0,0 +1,210 @@
+.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/ui/Message.js b/vnext/src/ui/Message.js
new file mode 100644
index 00000000..eb008bfe
--- /dev/null
+++ b/vnext/src/ui/Message.js
@@ -0,0 +1,142 @@
+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 (
+
+
+
+ {
+ data.body &&
+
+
+
+ }
+ {
+ data.photo &&
+
+ }
+
+
+ {children}
+
+ );
+}
+
+function MessageContainer({ isCode, data }) {
+ return isCode ? () : ();
+}
+
+function Tags({ data, user, ...rest }) {
+ return data.length > 0 && (
+
+ {
+ data.map(tag => {
+ return ({tag});
+ }).reduce((prev, curr) => [prev, ', ', curr])
+ }
+
+ );
+}
+
+const TagsList = React.memo(Tags);
+
+function Recommends({ forMessage, ...rest }) {
+ const { likes, recommendations } = forMessage;
+ return recommendations && recommendations.length > 0 && (
+ {'♡ by '}
+ {
+ recommendations.map(it => (
+
+ )).reduce((prev, curr) => [prev, ', ', curr])
+ }
+ {
+ likes > recommendations.length && ( and {likes - recommendations.length} others)
+ }
+
+ ) || 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/ui/MessageInput.js b/vnext/src/ui/MessageInput.js
new file mode 100644
index 00000000..e4988d59
--- /dev/null
+++ b/vnext/src/ui/MessageInput.js
@@ -0,0 +1,107 @@
+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 (
+
+ );
+}
+
+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/ui/Modal.css b/vnext/src/ui/Modal.css
new file mode 100644
index 00000000..931b9d81
--- /dev/null
+++ b/vnext/src/ui/Modal.css
@@ -0,0 +1,95 @@
+#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/ui/Modal.js b/vnext/src/ui/Modal.js
new file mode 100644
index 00000000..799a6f35
--- /dev/null
+++ b/vnext/src/ui/Modal.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Icon from './Icon';
+
+import './Modal.css';
+
+function Modal(props) {
+ return props.show ? (
+
+
+
+ {props.children}
+
+
+ ) : (null);
+}
+
+export default React.memo(Modal);
+
+Modal.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ show: PropTypes.bool,
+ children: PropTypes.node
+};
diff --git a/vnext/src/ui/NavigationIcon.css b/vnext/src/ui/NavigationIcon.css
new file mode 100644
index 00000000..caff6195
--- /dev/null
+++ b/vnext/src/ui/NavigationIcon.css
@@ -0,0 +1,4 @@
+#navicon {
+ padding: 12px;
+ color: #88958d;
+}
diff --git a/vnext/src/ui/NavigationIcon.js b/vnext/src/ui/NavigationIcon.js
new file mode 100644
index 00000000..0a22ac57
--- /dev/null
+++ b/vnext/src/ui/NavigationIcon.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Icon from './Icon';
+
+import './NavigationIcon.css';
+
+function NavigationIcon(props) {
+ return (
+
+
+
+ );
+}
+
+export default React.memo(NavigationIcon);
+
+NavigationIcon.propTypes = {
+ onToggle: PropTypes.func.isRequired
+};
+
diff --git a/vnext/src/ui/PM.js b/vnext/src/ui/PM.js
new file mode 100644
index 00000000..a1e70ad5
--- /dev/null
+++ b/vnext/src/ui/PM.js
@@ -0,0 +1,51 @@
+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 (
+
+
+
+ );
+}
+
+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/ui/Post.js b/vnext/src/ui/Post.js
new file mode 100644
index 00000000..3dc23613
--- /dev/null
+++ b/vnext/src/ui/Post.js
@@ -0,0 +1,38 @@
+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 (
+
+
+ *weather It is very cold today!
+
+
+ );
+}
+
+export default memo(PostComponent);
+
+PostComponent.propTypes = {
+ history: ReactRouterPropTypes.history.isRequired,
+ visitor: UserType
+};
diff --git a/vnext/src/ui/SearchBox.js b/vnext/src/ui/SearchBox.js
new file mode 100644
index 00000000..a79100cd
--- /dev/null
+++ b/vnext/src/ui/SearchBox.js
@@ -0,0 +1,26 @@
+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 (
+
+ );
+}
+
+SearchBox.propTypes = {
+ pathname: PropTypes.string.isRequired,
+ onSearch: PropTypes.func.isRequired,
+ history: ReactRouterPropTypes.history.isRequired
+};
+export default withRouter(SearchBox);
diff --git a/vnext/src/ui/Settings.js b/vnext/src/ui/Settings.js
new file mode 100644
index 00000000..cf6926f8
--- /dev/null
+++ b/vnext/src/ui/Settings.js
@@ -0,0 +1,268 @@
+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: '' };
+ if (preview) {
+ previewUser = { ...visitor, avatar: preview, uname: '' };
+ }
+ let onSubmitAvatar = (event) => {
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
+ updateAvatar(avatarInput.current.files[0]).then(() => {
+ avatarChanged('');
+ me().then(visitor => {
+ this.props.onChange(visitor);
+ });
+ });
+ };
+ return (
+
+ );
+}
+
+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 (
+
+
+
+
+ {me.jids && (
+
+ )}
+
+
+
+
+
+ );
+ }
+}
+
+Settings.propTypes = {
+ visitor: UserType.isRequired,
+ onChange: PropTypes.func.isRequired
+};
+
diff --git a/vnext/src/ui/Spinner.js b/vnext/src/ui/Spinner.js
new file mode 100644
index 00000000..e866369f
--- /dev/null
+++ b/vnext/src/ui/Spinner.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import ContentLoader from 'react-content-loader';
+
+function Spinner(props) {
+ return (
+
+ );
+}
+
+export default React.memo(Spinner);
+
+export function ChatSpinner(props) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/vnext/src/ui/Thread.js b/vnext/src/ui/Thread.js
new file mode 100644
index 00000000..e7ccb032
--- /dev/null
+++ b/vnext/src/ui/Thread.js
@@ -0,0 +1,181 @@
+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 (
+
+
+ {
+ msg.html ?
+ :
+
+ }
+ {
+ msg.photo &&
+
+ }
+
+
+ {
+ visitor.uid > 0 ? (
+ <>
+ {active === msg.rid || setActive(msg.rid)}>Reply}
+ {active === msg.rid && Write a comment...}
+ >
+ ) : (
+ <>
+ · {active === msg.rid || }
+ >
+ )
+ }
+
+
+ );
+}
+
+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 ? (
+
+ {active === (message.rid || 0) && Write a comment...}
+
+ ) : (
+
+ )
+ }
+
+ {
+ !loading ? replies.map((msg) => (
+ -
+
+
+ )) : (
+ <>
+ {Array(loaders).fill().map((it, i) => )}
+ >
+ )
+ }
+
+ >
+ );
+}
+
+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/ui/Types.js b/vnext/src/ui/Types.js
new file mode 100644
index 00000000..9bf7b513
--- /dev/null
+++ b/vnext/src/ui/Types.js
@@ -0,0 +1,15 @@
+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/ui/UploadButton.js b/vnext/src/ui/UploadButton.js
new file mode 100644
index 00000000..73cbbfcf
--- /dev/null
+++ b/vnext/src/ui/UploadButton.js
@@ -0,0 +1,42 @@
+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 (
+
+
+ e.stopPropagation()}
+ style={{ display: 'none' }} ref={props.inputRef} value={props.value}
+ onChange={attachmentChanged} />
+
+ );
+}
+
+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/ui/UserInfo.css b/vnext/src/ui/UserInfo.css
new file mode 100644
index 00000000..92cfdb6c
--- /dev/null
+++ b/vnext/src/ui/UserInfo.css
@@ -0,0 +1,11 @@
+.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/ui/UserInfo.js b/vnext/src/ui/UserInfo.js
new file mode 100644
index 00000000..7d84488e
--- /dev/null
+++ b/vnext/src/ui/UserInfo.js
@@ -0,0 +1,117 @@
+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 (
+ <>
+
+
+ Was online recently
+
+
+
+
+ {
+ user.uid > 0 &&
+ <>
+
+
+ PM
+
+
+
+ Recommendations
+
+
+
+ Photos
+
+ >
+ }
+
+ {props.children}
+ >
+ );
+}
+
+function Summary({ user }) {
+ const readUrl = `/${user.uname}/friends`;
+ const readersUrl = `/${user.uname}/readers`;
+ const blUrl = `/${user.uname}/bl`;
+ let read = user.read && I read: {user.read.length};
+ let readers = user.readers && My readers: {user.readers.length};
+ let mybl = user.statsMyBL && My blacklist: {user.statsMyBL};
+ let presentItems = [read, readers, mybl].filter(Boolean);
+ return (
+
+ {presentItems.length > 0 && presentItems.reduce((prev, curr) => [prev, ' ', curr])}
+
+ );
+}
+
+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 ?
+ {user.uname}
+ : {user.uname}
+ );
+}
+
+UserInfo.propTypes = {
+ user: PropTypes.string.isRequired,
+ onUpdate: PropTypes.func,
+ children: PropTypes.node
+};
+
+UserLink.propTypes = {
+ user: UserType.isRequired
+};
diff --git a/vnext/src/ui/Users.js b/vnext/src/ui/Users.js
new file mode 100644
index 00000000..a10bba7f
--- /dev/null
+++ b/vnext/src/ui/Users.js
@@ -0,0 +1,44 @@
+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 ;
+}
+
+export function Readers({ match }) {
+ return ;
+}
+
+function Users(props) {
+ const [user, setUser] = useState({ uid: 0, uname: props.user });
+ return (
+
+
+ {
+ user[props.prop] &&
+ user[props.prop].map(user =>
+
+ )
+ }
+
+
+ );
+}
+
+
+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/ui/__tests__/Avatar.test.js b/vnext/src/ui/__tests__/Avatar.test.js
new file mode 100644
index 00000000..e7221871
--- /dev/null
+++ b/vnext/src/ui/__tests__/Avatar.test.js
@@ -0,0 +1,15 @@
+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(
+
+
+
+ );
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/vnext/src/ui/__tests__/LoginButton.test.js b/vnext/src/ui/__tests__/LoginButton.test.js
new file mode 100644
index 00000000..da80abb0
--- /dev/null
+++ b/vnext/src/ui/__tests__/LoginButton.test.js
@@ -0,0 +1,21 @@
+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(
+ { }} />
+ );
+ });
+ 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/ui/__tests__/MessageInput-test.js b/vnext/src/ui/__tests__/MessageInput-test.js
new file mode 100644
index 00000000..7ac69ed0
--- /dev/null
+++ b/vnext/src/ui/__tests__/MessageInput-test.js
@@ -0,0 +1,95 @@
+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( { }} />, {
+ 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(, {
+ 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/ui/__tests__/UserLink.test.js b/vnext/src/ui/__tests__/UserLink.test.js
new file mode 100644
index 00000000..a75344b0
--- /dev/null
+++ b/vnext/src/ui/__tests__/UserLink.test.js
@@ -0,0 +1,20 @@
+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(
+
+ <>
+
+
+
+ >
+
+ );
+ await Promise.resolve();
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/vnext/src/ui/__tests__/__snapshots__/Avatar.test.js.snap b/vnext/src/ui/__tests__/__snapshots__/Avatar.test.js.snap
new file mode 100644
index 00000000..47614f5a
--- /dev/null
+++ b/vnext/src/ui/__tests__/__snapshots__/Avatar.test.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Avatar renders correctly 1`] = `
+
+`;
diff --git a/vnext/src/ui/__tests__/__snapshots__/LoginButton.test.js.snap b/vnext/src/ui/__tests__/__snapshots__/LoginButton.test.js.snap
new file mode 100644
index 00000000..cd08b1b4
--- /dev/null
+++ b/vnext/src/ui/__tests__/__snapshots__/LoginButton.test.js.snap
@@ -0,0 +1,178 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Login button and form are renders correctly 1`] = `
+
+
+ ",
+ }
+ }
+ />
+
+
+ Log in
+
+
+`;
+
+exports[`Login button and form are renders correctly 2`] = `
+Array [
+
+
+ ",
+ }
+ }
+ />
+
+
+ Log in
+
+ ,
+
+
+
+
+
+ Please, introduce yourself:
+
+
+
+ Already registered?
+
+
+
+
+
,
+]
+`;
diff --git a/vnext/src/ui/__tests__/__snapshots__/UserLink.test.js.snap b/vnext/src/ui/__tests__/__snapshots__/UserLink.test.js.snap
new file mode 100644
index 00000000..15e25367
--- /dev/null
+++ b/vnext/src/ui/__tests__/__snapshots__/UserLink.test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UserLink renders correctly 1`] = `
+Array [
+
+
+ ugnich
+ ,
+
+
+ ugnich
+ ,
+
+
+
+ ,
+]
+`;
diff --git a/vnext/src/views/index.html b/vnext/src/views/index.html
deleted file mode 100644
index d3e185b5..00000000
--- a/vnext/src/views/index.html
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
--
cgit v1.2.3