aboutsummaryrefslogtreecommitdiff
path: root/vnext
diff options
context:
space:
mode:
Diffstat (limited to 'vnext')
-rw-r--r--vnext/.eslintrc1
-rw-r--r--vnext/package.json1
-rw-r--r--vnext/src/App.js8
-rw-r--r--vnext/src/api/index.js102
-rw-r--r--vnext/src/index.js2
-rw-r--r--vnext/src/ui/Avatar.css4
-rw-r--r--vnext/src/ui/Avatar.js24
-rw-r--r--vnext/src/ui/Button.js3
-rw-r--r--vnext/src/ui/Chat.js44
-rw-r--r--vnext/src/ui/Contact.js19
-rw-r--r--vnext/src/ui/Feeds.js13
-rw-r--r--vnext/src/ui/Header.js17
-rw-r--r--vnext/src/ui/Icon.js26
-rw-r--r--vnext/src/ui/Input.js18
-rw-r--r--vnext/src/ui/Login.js27
-rw-r--r--vnext/src/ui/Message.js48
-rw-r--r--vnext/src/ui/MessageInput.js33
-rw-r--r--vnext/src/ui/NavigationIcon.js9
-rw-r--r--vnext/src/ui/PM.js7
-rw-r--r--vnext/src/ui/Post.js6
-rw-r--r--vnext/src/ui/Settings.js310
-rw-r--r--vnext/src/ui/Thread.js46
-rw-r--r--vnext/src/ui/Types.js15
-rw-r--r--vnext/src/ui/UploadButton.js22
-rw-r--r--vnext/src/ui/UserInfo.css4
-rw-r--r--vnext/src/ui/UserInfo.js40
-rw-r--r--vnext/src/ui/Users.js28
-rw-r--r--vnext/src/ui/helpers/BubbleStyle.js10
-rw-r--r--vnext/src/utils/embed.js2
29 files changed, 520 insertions, 369 deletions
diff --git a/vnext/.eslintrc b/vnext/.eslintrc
index 4413d33d..62404fed 100644
--- a/vnext/.eslintrc
+++ b/vnext/.eslintrc
@@ -69,6 +69,7 @@
"jest/prefer-to-have-length": "warn",
"jest/valid-expect": "error",
+ "react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
diff --git a/vnext/package.json b/vnext/package.json
index c8dcabd0..d60d042f 100644
--- a/vnext/package.json
+++ b/vnext/package.json
@@ -36,7 +36,6 @@
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
- "prop-types": "^15.7.2",
"react-router-prop-types": "^1.0.4",
"react-test-renderer": "^16.8.6",
"style-loader": "^0.23.1",
diff --git a/vnext/src/App.js b/vnext/src/App.js
index ea63ae18..7e0c4007 100644
--- a/vnext/src/App.js
+++ b/vnext/src/App.js
@@ -67,12 +67,20 @@ export default function App() {
});
}, [hash]);
+ /**
+ * @param {{ pathname: any; search: string; }[]} history
+ * @param {any} pathname
+ * @param {any} searchString
+ */
let search = (history, pathname, searchString) => {
let location = {};
location.pathname = pathname;
location.search = `?search=${searchString}`;
history.push(location);
};
+ /**
+ * @param {import("./api").SecureUser} visitor
+ */
let auth = (visitor) => {
setVisitor(prevState => {
if (visitor.hash != prevState.hash) {
diff --git a/vnext/src/api/index.js b/vnext/src/api/index.js
index e6b1d2ef..330b3f9e 100644
--- a/vnext/src/api/index.js
+++ b/vnext/src/api/index.js
@@ -3,6 +3,63 @@ import cookies from 'react-cookies';
const apiBaseUrl = 'https://juick.com';
+/**
+ * @typedef {Object} Token
+ * @property {string} type
+ * @property {string} token
+ */
+
+/**
+ * @typedef {Object} User
+ * @property {string} uname
+ * @property {number} uid
+ * @property {number=} unreadCount
+ * @property {string=} avatar
+ * @property {User[]=} read
+ * @property {User[]=} readers
+ * @property {number=} statsMyBL
+ * @property {string=} uri
+ */
+
+/**
+ * @typedef {Object} SecureUserProperties
+ * @property {string} hash
+ * @property {Token[]} tokens
+ */
+
+/**
+ * @typedef {User & SecureUserProperties} SecureUser
+ */
+
+/**
+ * @typedef {Object} ChatProperties
+ * @property {number=} unreadCount
+ * @property {string=} lastMessageText
+ */
+
+/**
+ * @typedef {User & ChatProperties} Chat
+ */
+
+/**
+ * @typedef {Object} Message
+ * @property {string} body
+ * @property {number=} mid
+ * @property {number=} rid
+ * @property {boolean=} service
+ * @property {User} user
+ * @property {User=} to
+ * @property {string=} replyQuote
+ * @property {string[]=} tags
+ * @property {number=} likes
+ * @property {number=} replies
+ * @property {string=} photo
+ * @property {string=} attach
+ * @property {string=} timestamp
+ * @property {boolean=} ReadOnly
+ */
+
+
const client = axios.create({
baseURL: apiBaseUrl
});
@@ -13,6 +70,12 @@ client.interceptors.request.use(config => {
return config;
});
+/**
+ * fetch my info
+ * @param {string} username
+ * @param {string} password
+ * @return {Promise<SecureUser, Error>} me object
+ */
export function me(username = '', password = '') {
return new Promise((resolve, reject) => {
client.get('/api/me', {
@@ -30,14 +93,21 @@ export function me(username = '', password = '') {
});
}
+/**
+ * @param {string} username
+ */
export function info(username) {
return client.get(`/api/info/${username}`);
}
+
export function getChats() {
return client.get('/api/groups_pms');
}
+/**
+ * @param {string} userName
+ */
export function getChat(userName) {
return client.get('/api/pm', {
params: {
@@ -46,6 +116,10 @@ export function getChat(userName) {
});
}
+/**
+ * @param {string} userName
+ * @param {string} body
+ */
export function pm(userName, body) {
let form = new FormData();
form.set('uname', userName);
@@ -53,12 +127,20 @@ export function pm(userName, body) {
return client.post('/api/pm', form);
}
+/**
+ * @param {string} path
+ * @param {{ mid: any; }} params
+ */
export function getMessages(path, params) {
return client.get(path, {
params: params
});
}
+/**
+ * @param {string} body
+ * @param {string} attach
+ */
export function post(body, attach) {
let form = new FormData();
form.append('attach', attach);
@@ -66,10 +148,16 @@ export function post(body, attach) {
return client.post('/api/post', form);
}
+/**
+ * @param {number} mid
+ * @param {number} rid
+ * @param {string} body
+ * @param {string} attach
+ */
export function comment(mid, rid, body, attach) {
let form = new FormData();
- form.append('mid', mid);
- form.append('rid', rid);
+ form.append('mid', mid.toString());
+ form.append('rid', rid.toString());
form.append('body', body);
form.append('attach', attach);
return client.post('/api/comment', form);
@@ -89,6 +177,9 @@ export function updateAvatar(newAvatar) {
return client.post('/api/me/upload', form);
}
+/**
+ * @param {string} network
+ */
function socialLink(network) {
return `${apiBaseUrl}/api/_${network}login?state=${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
@@ -101,10 +192,17 @@ export function vkLink() {
return socialLink('vk');
}
+/**
+ * @param {Message} msg
+ * @param {SecureUser} visitor
+ */
export function markReadTracker(msg, visitor) {
return `${apiBaseUrl}/api/thread/mark_read/${msg.mid}-${msg.rid || 0}.gif?hash=${visitor.hash}`;
}
+/**
+ * @param {string} dataUri
+ */
export function fetchUserUri(dataUri) {
return new Promise((resolve, reject) => {
let form = new FormData();
diff --git a/vnext/src/index.js b/vnext/src/index.js
index e48d004c..5f45ecc2 100644
--- a/vnext/src/index.js
+++ b/vnext/src/index.js
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import './index.css';
-function LoadingView(props) {
+function LoadingView() {
return (
<div id="content">
<div className="lds-ripple"><div></div><div></div></div>
diff --git a/vnext/src/ui/Avatar.css b/vnext/src/ui/Avatar.css
index f48f2b9e..9f7d22e3 100644
--- a/vnext/src/ui/Avatar.css
+++ b/vnext/src/ui/Avatar.css
@@ -13,8 +13,8 @@
}
.info-avatar img {
- max-height: 24px;
- max-width: 24px;
+ max-height: 48px;
+ max-width: 48px;
padding: 6px;
vertical-align: middle;
}
diff --git a/vnext/src/ui/Avatar.js b/vnext/src/ui/Avatar.js
index ecce4e9f..e08c1ba4 100644
--- a/vnext/src/ui/Avatar.js
+++ b/vnext/src/ui/Avatar.js
@@ -1,14 +1,23 @@
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}) {
+/**
+ * @typedef {Object} AvatarProps
+ * @property {import('../api').User} user
+ * @property {React.CSSProperties=} style
+ * @property {string=} link
+ * @property {React.ReactNode=} children
+ */
+
+/**
+ * Avatar component
+ * @param {AvatarProps} props
+ */
+function Avatar({ user, style, link, children }) {
return (
<div className="Avatar" style={style}>
<div className="msg-avatar">
@@ -35,10 +44,3 @@ function Avatar({ user, style, link, 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.js b/vnext/src/ui/Button.js
index 18cab0a7..033c972c 100644
--- a/vnext/src/ui/Button.js
+++ b/vnext/src/ui/Button.js
@@ -2,6 +2,9 @@ import React from 'react';
import './Button.css';
+/**
+ * @param {React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement>} props
+ */
function Button(props) {
return (
<button className="Button" {...props} />
diff --git a/vnext/src/ui/Chat.js b/vnext/src/ui/Chat.js
index a1254a10..00e8eb3c 100644
--- a/vnext/src/ui/Chat.js
+++ b/vnext/src/ui/Chat.js
@@ -1,7 +1,4 @@
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';
@@ -12,20 +9,19 @@ import { getChat, pm } from '../api';
import './Chat.css';
+/**
+ * @typedef {Object} ChatProps
+ * @property {import('../api').SecureUser} visitor
+ * @property {EventSource} connection
+ * @property {import('react-router').match} match
+ */
+
+/**
+ * Chat component
+ * @param {ChatProps} props
+ */
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;
@@ -53,6 +49,18 @@ export default function Chat(props) {
loadChat(props.match.params.user);
}).catch(console.log);
};
+ 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]);
const uname = props.match.params.user;
return (
<div className="msg-cont">
@@ -77,9 +85,3 @@ export default function Chat(props) {
</div>
);
}
-
-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
index 24aabe15..9e6416bb 100644
--- a/vnext/src/ui/Contact.js
+++ b/vnext/src/ui/Contact.js
@@ -1,10 +1,18 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import { UserType } from './Types';
import Avatar from './Avatar';
-function Contact({ user, style, ...rest }) {
+/**
+ * @typedef {Object} ContactProps
+ * @property {import('../api').Chat} user
+ * @property {React.CSSProperties} style
+ */
+
+/**
+ * Contact component
+ * @param {ContactProps} props
+ */
+function Contact({ user, style }) {
return (
<Avatar user={user} link={`/pm/${user.uname}`} style={style}>
{user.unreadCount && <span className="badge">{user.unreadCount}</span>}
@@ -14,8 +22,3 @@ function Contact({ user, style, ...rest }) {
}
export default React.memo(Contact);
-
-Contact.propTypes = {
- user: UserType,
- style: PropTypes.object
-};
diff --git a/vnext/src/ui/Feeds.js b/vnext/src/ui/Feeds.js
index e44de1ec..39c1296b 100644
--- a/vnext/src/ui/Feeds.js
+++ b/vnext/src/ui/Feeds.js
@@ -1,6 +1,5 @@
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';
@@ -11,7 +10,6 @@ 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));
@@ -31,6 +29,10 @@ export function Discussions(props) {
return (<Feed authRequired={false} query={query} {...props} />);
}
+/**
+ * Blog page
+ * @param {{match: import('react-router').match, location: import('history').Location}} props
+ */
export function Blog(props) {
const { user } = props.match.params;
let search = qs.parse(props.location.search.substring(1));
@@ -113,7 +115,7 @@ function Feed(props) {
newFilter[pageParam] = pageValue;
return `?${qs.stringify(newFilter)}`;
};
- const { tag } = qs.parse(location.search.substring(1) || {});
+ const { tag } = qs.parse(location.search.substring(1)) || {};
const nodes = (
<>
{
@@ -142,7 +144,7 @@ function Feed(props) {
<div className="msgs">{nodes}</div>
) : error ? <div>error</div> : loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div>;
}
-
+/*
Discover.propTypes = {
location: ReactRouterPropTypes.location.isRequired,
search: PropTypes.string
@@ -170,3 +172,4 @@ Feed.propTypes = {
pageParam: PropTypes.string.isRequired
})
};
+*/ \ No newline at end of file
diff --git a/vnext/src/ui/Header.js b/vnext/src/ui/Header.js
index b25879d4..2d042bfe 100644
--- a/vnext/src/ui/Header.js
+++ b/vnext/src/ui/Header.js
@@ -13,11 +13,10 @@ export default function Header({ children }) {
let wScrollBefore = useRef(0);
let wScrollDiff = useRef(0);
- useEffect(() => {
- window.addEventListener('scroll', () => (!window.requestAnimationFrame)
- ? throttle(250, updateHeader)
- : window.requestAnimationFrame(updateHeader), false);
- }, [updateHeader]);
+ /**
+ * @param {number} delay
+ * @param {{ (): void; apply?: any; }} fn
+ */
let throttle = (delay, fn) => {
var last, deferTimer;
return function () {
@@ -45,7 +44,7 @@ export default function Header({ children }) {
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)) {
+ } else if (wScrollDiff.current > 0 && header.classList.contains(elClassHidden)) {
// scrolled up; element slides in
header.classList.remove(elClassHidden);
} else if (wScrollDiff.current < 0) {
@@ -60,6 +59,12 @@ export default function Header({ children }) {
}
wScrollBefore.current = wScrollCurrent.current;
}, []);
+
+ useEffect(() => {
+ window.addEventListener('scroll', () => (!window.requestAnimationFrame)
+ ? throttle(250, updateHeader)
+ : window.requestAnimationFrame(updateHeader), false);
+ }, [updateHeader]);
return ReactDOM.createPortal(children, header);
}
diff --git a/vnext/src/ui/Icon.js b/vnext/src/ui/Icon.js
index faf1a704..255bba34 100644
--- a/vnext/src/ui/Icon.js
+++ b/vnext/src/ui/Icon.js
@@ -1,7 +1,19 @@
-import React from 'react';
+import React, { memo } from 'react';
import PropTypes from 'prop-types';
-const Icon = React.memo(props => {
+/**
+ * @typedef {Object} IconProps
+ * @property {string} size
+ * @property {string=} className
+ * @property {string} name
+ * @property {boolean=} noFill
+ */
+
+ /**
+ * Icon inner component
+ * @param {IconProps} props - icon props
+ */
+function IconElement(props) {
var size = props.size ? ' icon--' + props.size : '';
var className = props.className ? ' ' + props.className : '';
var klass = 'icon' + (!props.noFill ? ' icon--' + props.name : '') + size + className;
@@ -14,8 +26,12 @@ const Icon = React.memo(props => {
{ className: klass },
wrapSpinner(Icon, klass)
);
-});
+}
+/**
+ * @param {React.ReactElement} Html
+ * @param {string} klass
+ */
function wrapSpinner(Html, klass) {
if (klass.indexOf('spinner') > -1) {
return React.createElement(
@@ -28,9 +44,9 @@ function wrapSpinner(Html, klass) {
}
}
-export default Icon;
+export default memo(IconElement);
-Icon.propTypes = {
+IconElement.propTypes = {
size: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
className: PropTypes.string,
diff --git a/vnext/src/ui/Input.js b/vnext/src/ui/Input.js
index c74d595d..e4fdefa0 100644
--- a/vnext/src/ui/Input.js
+++ b/vnext/src/ui/Input.js
@@ -1,17 +1,23 @@
import React from 'react';
-import PropTypes from 'prop-types';
import './Input.css';
+/**
+ * @typedef {Object} InputProps
+ * @property {string} name
+ * @property {string} value
+ * @property {string=} placeholder
+ * @property {React.CSSProperties=} rest
+ */
+
+/**
+ * Input component
+ * @param {InputProps} props
+ */
function Input({ name, value, ...rest }) {
return (
<input className="input" name={name} value={value} {...rest} />
);
}
-Input.propTypes = {
- name: PropTypes.string.isRequired,
- value: PropTypes.string.isRequired
-};
-
export default React.memo(Input);
diff --git a/vnext/src/ui/Login.js b/vnext/src/ui/Login.js
index 649fadd4..883a4eab 100644
--- a/vnext/src/ui/Login.js
+++ b/vnext/src/ui/Login.js
@@ -1,10 +1,7 @@
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
+import React, { useEffect } from 'react';
-import ReactRouterPropTypes from 'react-router-prop-types';
import { withRouter } from 'react-router-dom';
-import { UserType } from './Types';
import Icon from './Icon';
import Button from './Button';
import Input from './Input';
@@ -14,6 +11,18 @@ import { me, facebookLink, vkLink } from '../api';
import './Login.css';
+/**
+ * @typedef {Object} LoginProps
+ * @property {import('../api').SecureUser} visitor
+ * @property {import('history').History} history
+ * @property {import('history').Location} location
+ * @property {any} onAuth
+ */
+
+/**
+ * Login page
+ * @param {LoginProps} props
+ */
function Login({ visitor, history, location, onAuth }) {
useEffect(() => {
@@ -26,6 +35,9 @@ function Login({ visitor, history, location, onAuth }) {
const [formState, { text, password }] = useFormState();
+ /**
+ * @param {React.SyntheticEvent} event
+ */
let onSubmit = (event) => {
event.preventDefault();
me(formState.values.username, formState.values.password)
@@ -66,13 +78,6 @@ function Login({ visitor, history, location, onAuth }) {
export default withRouter(Login);
-Login.propTypes = {
- visitor: UserType,
- history: ReactRouterPropTypes.history.isRequired,
- location: ReactRouterPropTypes.location,
- onAuth: PropTypes.func
-};
-
const socialButtonsStyle = {
display: 'flex',
justifyContent: 'space-evenly',
diff --git a/vnext/src/ui/Message.js b/vnext/src/ui/Message.js
index fd225282..51ff12ec 100644
--- a/vnext/src/ui/Message.js
+++ b/vnext/src/ui/Message.js
@@ -1,9 +1,8 @@
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';
@@ -12,11 +11,21 @@ import { format, embedUrls } from '../utils/embed';
import './Message.css';
-export default function Message({ data, visitor, children, ...rest }) {
+/**
+ * Message component
+ * @param {{data: import('../api').Message, visitor: import('../api').User, children: Element}} props
+ */
+export default function Message({ data, visitor, children }) {
const isCode = (data.tags || []).indexOf('code') >= 0;
const likesSummary = data.likes ? `${data.likes}` : 'Recommend';
const commentsSummary = data.replies ? `${data.replies}` : 'Comment';
+ /**
+ * @type {React.RefObject<HTMLDivElement>}
+ */
const embedRef = useRef();
+ /**
+ * @type {React.RefObject<HTMLDivElement>}
+ */
const msgRef = useRef();
useEffect(() => {
if (msgRef.current) {
@@ -26,6 +35,7 @@ export default function Message({ data, visitor, children, ...rest }) {
}
}
}, []);
+ const canComment = visitor.uid === data.user.uid || !data.ReadOnly;
return (
<div className="msg-cont">
<Recommendations forMessage={data} />
@@ -53,7 +63,7 @@ export default function Message({ data, visitor, children, ...rest }) {
{
data.body &&
<div className="msg-txt" ref={msgRef}>
- <MessageContainer isCode={isCode} data={{ __html: format(data.body, data.mid, isCode) }} />
+ <MessageContainer isCode={isCode} data={{ __html: format(data.body, data.mid.toString(), isCode) }} />
</div>
}
{
@@ -82,7 +92,7 @@ export default function Message({ data, visitor, children, ...rest }) {
<span>{likesSummary}</span>
</a>
)}
- {!data.ReadOnly | (visitor.uid === data.user.uid) && (
+ {canComment && (
<>
<Link to={{ pathname: `/${data.user.uname}/${data.mid}`, state: { msg: data } }} className="a-comment msg-button">
<Icon name="ei-comment" size="s" />
@@ -96,11 +106,18 @@ export default function Message({ data, visitor, children, ...rest }) {
);
}
+/**
+ * @param {{isCode: boolean, data: {__html: string}}} props
+ */
function MessageContainer({ isCode, data }) {
return isCode ? (<pre dangerouslySetInnerHTML={data} />) : (<p dangerouslySetInnerHTML={data} />);
}
-function Tags({ data, user, ...rest }) {
+/**
+ * Tags component
+ * @param {{user: import('../api').User, data: string[]}} props
+ */
+function Tags({ data, user }) {
return data.length > 0 && (
<div className="msg-tags">
{
@@ -114,7 +131,7 @@ function Tags({ data, user, ...rest }) {
const TagsList = React.memo(Tags);
-function Recommends({ forMessage, ...rest }) {
+function Recommends({ forMessage }) {
const { likes, recommendations } = forMessage;
return recommendations && recommendations.length > 0 && (
<div className="msg-recomms">{'♡ by '}
@@ -131,20 +148,3 @@ function Recommends({ forMessage, ...rest }) {
}
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
index 63c2ee79..bc9ddd54 100644
--- a/vnext/src/ui/MessageInput.js
+++ b/vnext/src/ui/MessageInput.js
@@ -1,7 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-
-import { MessageType } from './Types';
import Icon from './Icon';
import Button from './Button';
@@ -9,7 +6,10 @@ import Button from './Button';
import UploadButton from './UploadButton';
-// StackOverflow-driven development: https://stackoverflow.com/a/10158364/1097384
+/**
+ * StackOverflow-driven development: https://stackoverflow.com/a/10158364/1097384
+ * @param {HTMLTextAreaElement} el
+ */
function moveCaretToEnd(el) {
if (typeof el.selectionStart == 'number') {
el.selectionStart = el.selectionEnd = el.value.length;
@@ -21,8 +21,25 @@ function moveCaretToEnd(el) {
}
}
+/**
+ * @typedef {Object} MessageInputProps
+ * @property {string} text
+ * @property {import('../api').Message} data
+ * @property {function} onSend
+ */
+
+/**
+ * MessageInput
+ * @param {HTMLTextAreaElement & MessageInputProps} props
+ */
export default function MessageInput({ text, data, rows, children, onSend }) {
+ /**
+ * @type {React.MutableRefObject<HTMLTextAreaElement>}
+ */
let textareaRef = useRef();
+ /**
+ * @type {React.MutableRefObject<HTMLInputElement>}
+ */
let fileinput = useRef();
let updateFocus = () => {
@@ -114,11 +131,3 @@ const textInputStyle = {
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/NavigationIcon.js b/vnext/src/ui/NavigationIcon.js
index 0a22ac57..9594d61f 100644
--- a/vnext/src/ui/NavigationIcon.js
+++ b/vnext/src/ui/NavigationIcon.js
@@ -5,6 +5,15 @@ import Icon from './Icon';
import './NavigationIcon.css';
+/**
+ * @typedef {Object} NavigationIconProps
+ * @property {(event: React.MouseEvent) => void} onToggle
+ */
+
+ /**
+ * Navigation icon
+ * @param {NavigationIconProps} props
+ */
function NavigationIcon(props) {
return (
<div id="navicon" className="mobile" onClick={props.onToggle}>
diff --git a/vnext/src/ui/PM.js b/vnext/src/ui/PM.js
index 08db523f..a25580e5 100644
--- a/vnext/src/ui/PM.js
+++ b/vnext/src/ui/PM.js
@@ -1,7 +1,5 @@
import React from 'react';
-import { UserType, MessageType } from './Types';
-
import Avatar from './Avatar';
import { format } from '../utils/embed';
import { bubbleStyle, chatItemStyle } from './helpers/BubbleStyle';
@@ -13,7 +11,7 @@ function PM(props) {
<div style={chatItemStyle(props.visitor, chat)}>
<Avatar user={chat.user} />
<div style={bubbleStyle(props.visitor, chat)}>
- <p dangerouslySetInnerHTML={{ __html: format(chat.body) }} />
+ <p dangerouslySetInnerHTML={{ __html: format(chat.body, 0, false) }} />
</div>
</div>
</li>
@@ -21,8 +19,9 @@ function PM(props) {
}
export default React.memo(PM);
-
+/*
PM.propTypes = {
chat: MessageType.isRequired,
visitor: UserType.isRequired
};
+*/
diff --git a/vnext/src/ui/Post.js b/vnext/src/ui/Post.js
index 662f9f78..99189632 100644
--- a/vnext/src/ui/Post.js
+++ b/vnext/src/ui/Post.js
@@ -1,8 +1,5 @@
import React, { useState } from 'react';
-import ReactRouterPropTypes from 'react-router-prop-types';
-import { UserType } from './Types';
-
import qs from 'qs';
import Button from './Button';
@@ -49,9 +46,10 @@ export default function Post({ location, visitor, history }) {
</div>
);
}
-
+/*
Post.propTypes = {
location: ReactRouterPropTypes.location,
history: ReactRouterPropTypes.history.isRequired,
visitor: UserType
};
+*/ \ No newline at end of file
diff --git a/vnext/src/ui/Settings.js b/vnext/src/ui/Settings.js
index cf6926f8..61ac91d4 100644
--- a/vnext/src/ui/Settings.js
+++ b/vnext/src/ui/Settings.js
@@ -1,17 +1,13 @@
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 }) {
+function ChangeAvatarForm({ visitor, onChange }) {
const [avatar, setAvatar] = useState('');
const [preview, setPreview] = useState();
const avatarInput = useRef();
@@ -37,7 +33,7 @@ function ChangeAvatarForm({ visitor }) {
updateAvatar(avatarInput.current.files[0]).then(() => {
avatarChanged('');
me().then(visitor => {
- this.props.onChange(visitor);
+ onChange(visitor);
});
});
};
@@ -52,185 +48,173 @@ function ChangeAvatarForm({ visitor }) {
</form>
);
}
-
+/*
ChangeAvatarForm.propTypes = {
- visitor: UserType.isRequired
+ visitor: UserType.isRequired,
+ onChange: PropTypes.func.isRequired
};
+*/
+export default function Settings({ visitor, onChange }) {
-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) => {
+ let passwordChanged = (event) => {
+ console.log('password changed');
+ };
+ let 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);
+ };
+ let emailChanged = (event) => {
console.log('email update');
- }
- disableTelegram = () => {
+ };
+ let disableTelegram = () => {
console.log('telegram disable');
- }
- disableFacebook = (event) => {
+ };
+ let disableFacebook = (event) => {
if (event.preventDefault) {
event.preventDefault();
}
console.log('facebook disable');
- }
- enableFacebook = (event) => {
+ };
+ let enableFacebook = (event) => {
if (event.preventDefault) {
event.preventDefault();
}
console.log('facebook enable');
- }
- disableTwitter = () => {
+ };
+ let disableTwitter = () => {
console.log('twitter disable');
- }
+ };
+ let deleteJid = () => {
- render() {
- const { me, settings } = this.state;
- return (
- <div className="msg-cont">
- <fieldset>
- <legend><Icon name="ei-user" size="m" />Changing your avatar</legend>
- <ChangeAvatarForm visitor={me} />
- </fieldset>
- <fieldset>
- <legend><Icon name="ei-unlock" size="m" />Changing your password</legend>
+ };
+ let addEmail = () => {
+
+ };
+ let deleteEmail = () => {
+
+ };
+ return (
+ <div className="msg-cont">
+ <fieldset>
+ <legend><Icon name="ei-user" size="m" />Changing your avatar</legend>
+ <ChangeAvatarForm visitor={visitor} onChange={onChange} />
+ </fieldset>
+ <fieldset>
+ <legend><Icon name="ei-unlock" size="m" />Changing your password</legend>
+ <form>
+ <p>Change password: <input type="password" name="password" size="8"
+ onChange={passwordChanged} />
+ <Button onClick={onSubmitPassword}>Update</Button><br />
+ <i>(max. length - 16 symbols)</i></p>
+ </form>
+ </fieldset>
+ <fieldset>
+ <legend><Icon name="ei-sc-telegram" size="m" />Telegram</legend>
+ {visitor.telegramName ? (
<form>
- <p>Change password: <input type="password" name="password" size="8" onChange={this.passwordChanged} />
- <Button onClick={this.onSubmitPassword}>Update</Button><br />
- <i>(max. length - 16 symbols)</i></p>
- </form>
- </fieldset>
- <fieldset>
- <legend><Icon name="ei-sc-telegram" size="m" />Telegram</legend>
- {me.telegramName ? (
- <form>
- <div>Telegram: <b> {me.telegramName} </b>&mdash;
+ <div>Telegram: <b> {visitor.telegramName} </b> &mdash;
<Button onClick={this.disableTelegram}>Disable</Button>
- </div>
- </form>
- ) : (
- <p>To connect Telegram account: send any text message to <a href="https://telegram.me/Juick_bot">@Juick_bot</a>
- </p>
- )}
- </fieldset>
- {me.jids && (
- <form>
- <fieldset>
- <legend>XMPP accounts
- </legend>
- <p>Your accounts:</p>
- <p>
- {
- me.jids.map(jid =>
- <React.Fragment key={jid}>
- <label><input type="radio" name="delete" value={jid} />{jid}</label><br />
- </React.Fragment>
- )
- }
- </p>
- {
- me.jids && me.jids.length > 1 &&
- <p><Button onClick={this.deleteJid}>Delete</Button></p>
- }
- <p>To add new jabber account: send any text message to <a href="xmpp:juick@juick.com?message;body=login">juick@juick.com</a>
- </p>
- </fieldset>
+ </div>
</form>
- )}
- <fieldset>
- <legend><Icon name="ei-envelope" size="m" />E-mail</legend>
- <form>
- <p>Add account:<br />
- <input type="text" name="account" />
- <input type="hidden" name="page" value="email-add" />
- <Button onClick={this.addEmail}>Add</Button>
+ ) : (
+ <p>To connect Telegram account: send any text message to <a href="https://telegram.me/Juick_bot">@Juick_bot</a>
</p>
- </form>
- <form>
+ )}
+ </fieldset>
+ {me.jids && (
+ <form>
+ <fieldset>
+ <legend>XMPP accounts
+ </legend>
<p>Your accounts:</p>
<p>
{
- me.emails ? me.emails.map(email =>
- <React.Fragment key={email}>
- <label>
- <input type="radio" name="account" value={email} />{email}
- </label>
- <br />
+ visitor.jids.map(jid =>
+ <React.Fragment key={jid}>
+ <label><input type="radio" name="delete" value={jid} />{jid}</label><br />
</React.Fragment>
- ) : '-'
+ )
}
</p>
{
- me.emails && me.emails.length > 1 &&
- <Button onClick={this.deleteEmail}>Delete</Button>
+ visitor.jids && visitor.jids.length > 1 &&
+ <p><Button onClick={deleteJid}>Delete</Button></p>
}
- </form>
+ <p>To add new jabber account: send any text message to <span><a href="xmpp:juick@juick.com?message;body=login">juick@juick.com</a></span>
+ </p>
+ </fieldset>
+ </form>
+ )}
+ <fieldset>
+ <legend><Icon name="ei-envelope" size="m" />E-mail</legend>
+ <form>
+ <p>Add account:<br />
+ <input type="text" name="account" />
+ <input type="hidden" name="page" value="email-add" />
+ <Button onClick={addEmail}>Add</Button>
+ </p>
+ </form>
+ <form>
+ <p>Your accounts:</p>
+ <p>
+ {
+ visitor.emails ? visitor.emails.map(email =>
+ <React.Fragment key={email}>
+ <label>
+ <input type="radio" name="account" value={email} />{email}
+ </label>
+ <br />
+ </React.Fragment>
+ ) : '-'
+ }
+ </p>
{
- me.emails &&
- <>
- {/** email_off **/}
- <form>
- You can receive notifications to email:<br />
- Sent to <select name="account" value={me.activeEmail || 'Disabled'} onChange={this.emailChanged}>
- <option value="">Disabled</option>
- {
- me.emails.map(email =>
- <option key={email} value={email}>
- {email}
- </option>
- )
- }
- </select>
- </form>
- {/** /email_off **/}
- <p>&nbsp;</p>
- <p>You can post to Juick via e-mail. Send your <u>plain text</u> messages to <span><a href="mailto:juick@juick.com">juick@juick.com</a></span>. You can attach one photo or video file.</p>
- </>
+ visitor.emails && visitor.emails.length > 1 &&
+ <Button onClick={deleteEmail}>Delete</Button>
}
- </fieldset>
- <fieldset>
- <legend><Icon name="ei-sc-facebook" size="m" />Facebook</legend>
- {me.facebookStatus && me.facebookStatus.connected ? (
- me.facebookStatus.crosspostEnabled ?
+ </form>
+ {
+ visitor.emails &&
+ <>
+ {/** email_off **/}
+ <form>
+ You can receive notifications to email:<br />
+ Sent to <select name="account" value={me.activeEmail || 'Disabled'} onChange={emailChanged}>
+ <option value="">Disabled</option>
+ {
+ visitor.emails.map(email =>
+ <option key={email} value={email}>
+ {email}
+ </option>
+ )
+ }
+ </select>
+ </form>
+ {/** /email_off **/}
+ <p>&nbsp;</p>
+ <p>You can post to Juick via e-mail. Send your <u>plain text</u> messages to <a href="mailto:juick@juick.com">juick@juick.com</a>.
+ You can attach one photo or video file.</p>
+ </>
+ }
+ </fieldset>
+ <fieldset>
+ <legend><Icon name="ei-sc-facebook" size="m" />Facebook</legend>
+ {
+ visitor.facebookStatus && visitor.facebookStatus.connected ? (
+ visitor.facebookStatus.crosspostEnabled ?
<form>
<div>
- Facebook: <b>Enabled</b> &mdash;
- <Button onClick={this.disableFacebook}>Disable</Button>
+ Facebook: <b>Enabled</b>&mdash;
+ <Button onClick={disableFacebook}>Disable</Button>
</div>
</form>
:
<form>
<div>
- Facebook: <b>Disabled</b> &mdash;
- <Button onClick={this.enableFacebook}>Enable</Button>
+ Facebook: <b>Disabled</b>&mdash;
+ <Button onClick={enableFacebook}>Enable</Button>
</div>
</form>
) : (
@@ -240,29 +224,29 @@ export default class Settings extends React.Component {
</a>
</p>
)}
- </fieldset>
- <fieldset>
- <legend><Icon name="ei-sc-twitter" size="m" />Twitter</legend>
- {me.twitterName ?
- <form action="/settings" method="post">
- <div>Twitter: <b>{me.twitterName}</b> &mdash;
+ </fieldset>
+ <fieldset>
+ <legend><Icon name="ei-sc-twitter" size="m" />Twitter</legend>
+ {visitor.twitterName ?
+ <form action="/settings" method="post">
+ <div>Twitter: <b>{visitor.twitterName}</b>&mdash;
<input type="hidden" name="page" value="twitter-del" />
- <Button onClick={this.disableTwitter}>Disable</Button>
- </div>
- </form>
- :
- <p>Cross-posting to Twitter: <a href="/_twitter"><img src="//static.juick.com/twitter-connect.png"
- alt="Connect to Twitter" /></a></p>
- }
- </fieldset>
+ <Button onClick={disableTwitter}>Disable</Button>
+ </div>
+ </form>
+ :
+ <p>Cross-posting to Twitter: <a href="/_twitter"><img src="//static.juick.com/twitter-connect.png"
+ alt="Connect to Twitter" /></a></p>
+ }
+ </fieldset>
- </div>
- );
- }
+ </div>
+ );
}
-
+/*
Settings.propTypes = {
visitor: UserType.isRequired,
onChange: PropTypes.func.isRequired
};
+*/
diff --git a/vnext/src/ui/Thread.js b/vnext/src/ui/Thread.js
index f965e80c..ae5e4f3c 100644
--- a/vnext/src/ui/Thread.js
+++ b/vnext/src/ui/Thread.js
@@ -1,8 +1,4 @@
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';
@@ -101,7 +97,7 @@ function Comment({ msg, draft, visitor, active, setActive, onStartEditing, postC
</div>
);
}
-
+/*
Comment.propTypes = {
msg: MessageType.isRequired,
draft: PropTypes.string.isRequired,
@@ -110,7 +106,7 @@ Comment.propTypes = {
setActive: PropTypes.func.isRequired,
onStartEditing: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired
-};
+};*/
export default function Thread(props) {
const [message, setMessage] = useState((props.location.state || {}).msg || {});
@@ -118,22 +114,8 @@ export default function Thread(props) {
const [loading, setLoading] = useState(false);
const [active, setActive] = useState(0);
const [editing, setEditing] = useState({});
- 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(() => {
+ let loadReplies = () => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
@@ -156,7 +138,7 @@ export default function Thread(props) {
).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) {
@@ -165,6 +147,7 @@ export default function Thread(props) {
});
}
}, [message]);
+
let postComment = (template) => {
const { mid, rid, body, attach } = template;
let commentAction = editing.rid ? update(mid, editing.rid, body) : comment(mid, rid, body, attach);
@@ -180,6 +163,21 @@ export default function Thread(props) {
setEditing(reply);
};
+ useEffect(() => {
+ setActive(0);
+ 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]);
+
const loaders = Math.min(message.replies || 0, 10);
return (
<>
@@ -214,11 +212,11 @@ export default function Thread(props) {
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
deleted file mode 100644
index 9bf7b513..00000000
--- a/vnext/src/ui/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/ui/UploadButton.js b/vnext/src/ui/UploadButton.js
index 73cbbfcf..28a1f340 100644
--- a/vnext/src/ui/UploadButton.js
+++ b/vnext/src/ui/UploadButton.js
@@ -1,8 +1,18 @@
import React from 'react';
-import PropTypes from 'prop-types';
import Icon from './Icon';
+/**
+ * @typedef {Object} UploadButtonProps
+ * @property {string} value
+ * @property {React.MutableRefObject<HTMLInputElement>} inputRef
+ * @property {function} onChange
+ */
+
+/**
+ * Upload button
+ * @param {UploadButtonProps} props
+ */
export default function UploadButton(props) {
let openfile = () => {
const input = props.inputRef.current;
@@ -12,6 +22,10 @@ export default function UploadButton(props) {
input.click();
}
};
+
+ /**
+ * @param {React.ChangeEvent<HTMLInputElement>} event
+ */
let attachmentChanged = (event) => {
props.onChange(event.target.value);
};
@@ -26,12 +40,6 @@ export default function UploadButton(props) {
);
}
-UploadButton.propTypes = {
- value: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- inputRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) })
-};
-
const inactiveStyle = {
cursor: 'pointer',
color: '#888'
diff --git a/vnext/src/ui/UserInfo.css b/vnext/src/ui/UserInfo.css
index 92cfdb6c..3e692a86 100644
--- a/vnext/src/ui/UserInfo.css
+++ b/vnext/src/ui/UserInfo.css
@@ -4,8 +4,8 @@
margin: 12px;
}
.info-avatar img {
- max-height: 24px;
- max-width: 24px;
+ max-height: 36px;
+ max-width: 36px;
padding: 6px;
vertical-align: middle;
}
diff --git a/vnext/src/ui/UserInfo.js b/vnext/src/ui/UserInfo.js
index 0d06d134..faa2ebd6 100644
--- a/vnext/src/ui/UserInfo.js
+++ b/vnext/src/ui/UserInfo.js
@@ -1,9 +1,6 @@
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';
@@ -14,8 +11,20 @@ import './UserInfo.css';
let isMounted;
+/**
+ * Wrapper for dumb VSCode
+ * @param {import('../api').User} user
+ */
+function useUserState(user) {
+ return useState(user);
+}
+
+/**
+ * User info component
+ * @param {{user: string, onUpdate?: function, children?: Element}} props
+ */
export default function UserInfo(props) {
- const [user, setUser] = useState({ uname: props.user, uid: 0 });
+ const [user, setUser] = useUserState({ uname: props.user, uid: 0 });
const { onUpdate } = props;
useEffect(() => {
isMounted = true;
@@ -63,6 +72,10 @@ export default function UserInfo(props) {
);
}
+/**
+ * User summary component
+ * @param {{user: import('../api').User}} props
+ */
function Summary({ user }) {
const readUrl = `/${user.uname}/friends`;
const readersUrl = `/${user.uname}/readers`;
@@ -78,12 +91,13 @@ function Summary({ user }) {
);
}
-Summary.propTypes = {
- user: UserType.isRequired
-};
-
const UserSummary = React.memo(Summary);
+
+/**
+ * Link to user
+ * @param {{ user: import('../api').User}} props
+ */
export function UserLink(props) {
const [user, setUser] = useState(props.user);
useEffect(() => {
@@ -105,13 +119,3 @@ export function UserLink(props) {
: <a href={user.uri} className="info-avatar"><img src={user.avatar} />{user.uname}</a>
);
}
-
-UserInfo.propTypes = {
- user: PropTypes.string.isRequired,
- onUpdate: PropTypes.func,
- children: PropTypes.node
-};
-
-UserLink.propTypes = {
- user: UserType.isRequired
-};
diff --git a/vnext/src/ui/Users.js b/vnext/src/ui/Users.js
index a10bba7f..4c09318f 100644
--- a/vnext/src/ui/Users.js
+++ b/vnext/src/ui/Users.js
@@ -1,18 +1,28 @@
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';
+/**
+ * Friends feed
+ * @param {{match: import('react-router').match }} match
+ */
export function Friends({ match }) {
return <Users user={match.params.user} prop='read' />;
}
+/**
+ * Readers feed
+ * @param {{match: import('react-router').match }} match
+ */
export function Readers({ match }) {
return <Users user={match.params.user} prop='readers' />;
}
+/**
+ * UserInfo list component
+ * @param {{user: import('../api').User, prop: string}} props
+ */
function Users(props) {
const [user, setUser] = useState({ uid: 0, uname: props.user });
return (
@@ -28,17 +38,3 @@ function Users(props) {
</UserInfo>
);
}
-
-
-Friends.propTypes = {
- match: ReactRouterPropTypes.match.isRequired
-};
-
-Readers.propTypes = {
- match: ReactRouterPropTypes.match.isRequired
-};
-
-Users.propTypes = {
- user: PropTypes.string.isRequired,
- prop: PropTypes.string.isRequired
-};
diff --git a/vnext/src/ui/helpers/BubbleStyle.js b/vnext/src/ui/helpers/BubbleStyle.js
index f44f726e..f8b0f4eb 100644
--- a/vnext/src/ui/helpers/BubbleStyle.js
+++ b/vnext/src/ui/helpers/BubbleStyle.js
@@ -1,3 +1,8 @@
+/**
+ * @param {import('../../api').User} me
+ * @param {import('../../api').Message} msg
+ * @returns {React.CSSProperties}
+ */
export function bubbleStyle(me, msg) {
const isMe = me.uid === msg.user.uid;
const color = isMe ? '#fff' : '#222';
@@ -10,6 +15,11 @@ export function bubbleStyle(me, msg) {
};
}
+/**
+ * @param {import('../../api').User} me
+ * @param {import('../../api').Message} msg
+ * @returns {React.CSSProperties}
+ */
export function chatItemStyle(me, msg) {
const isMe = me.uid === msg.user.uid;
const alignment = isMe ? 'flex-end' : 'flex-start';
diff --git a/vnext/src/utils/embed.js b/vnext/src/utils/embed.js
index 6a92521a..be96d036 100644
--- a/vnext/src/utils/embed.js
+++ b/vnext/src/utils/embed.js
@@ -63,7 +63,7 @@ function makeIframe(src, w, h, scrolling='no') {
let iframe = document.createElement('iframe');
iframe.style.width = w;
iframe.style.height = h;
- iframe.frameBorder = 0;
+ iframe.frameBorder = '0';
iframe.scrolling = scrolling;
iframe.setAttribute('allowFullScreen', '');
iframe.src = src;