From f707d3d524d8d16e2bb780764f029d85fc57ecc0 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 26 Jul 2019 13:22:00 +0300 Subject: prop-types -> jsdoc --- vnext/.eslintrc | 1 + vnext/package.json | 1 - vnext/src/App.js | 8 + vnext/src/api/index.js | 102 +++++++++++- vnext/src/index.js | 2 +- vnext/src/ui/Avatar.css | 4 +- vnext/src/ui/Avatar.js | 24 +-- vnext/src/ui/Button.js | 3 + vnext/src/ui/Chat.js | 44 ++--- vnext/src/ui/Contact.js | 19 ++- vnext/src/ui/Feeds.js | 13 +- vnext/src/ui/Header.js | 17 +- vnext/src/ui/Icon.js | 26 ++- vnext/src/ui/Input.js | 18 ++- vnext/src/ui/Login.js | 27 ++-- vnext/src/ui/Message.js | 48 +++--- vnext/src/ui/MessageInput.js | 33 ++-- vnext/src/ui/NavigationIcon.js | 9 ++ vnext/src/ui/PM.js | 7 +- vnext/src/ui/Post.js | 6 +- vnext/src/ui/Settings.js | 310 +++++++++++++++++------------------- vnext/src/ui/Thread.js | 46 +++--- vnext/src/ui/Types.js | 15 -- vnext/src/ui/UploadButton.js | 22 ++- vnext/src/ui/UserInfo.css | 4 +- vnext/src/ui/UserInfo.js | 40 ++--- vnext/src/ui/Users.js | 28 ++-- vnext/src/ui/helpers/BubbleStyle.js | 10 ++ vnext/src/utils/embed.js | 2 +- 29 files changed, 520 insertions(+), 369 deletions(-) delete mode 100644 vnext/src/ui/Types.js 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} 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 (
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 (
@@ -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 & React.ButtonHTMLAttributes} props + */ function Button(props) { return (
+ (max. length - 16 symbols)

+ + +
+ Telegram + {visitor.telegramName ? (
-

Change password: -
- (max. length - 16 symbols)

-
-
-
- Telegram - {me.telegramName ? ( -
-
Telegram: {me.telegramName} — +
Telegram: {visitor.telegramName} -
- - ) : ( -

To connect Telegram account: send any text message to @Juick_bot -

- )} -
- {me.jids && ( -
-
- XMPP accounts - -

Your accounts:

-

- { - me.jids.map(jid => - -
-
- ) - } -

- { - me.jids && me.jids.length > 1 && -

- } -

To add new jabber account: send any text message to juick@juick.com -

-
+
- )} -
- E-mail -
-

Add account:
- - - + ) : ( +

To connect Telegram account: send any text message to @Juick_bot

-
-
+ )} +
+ {me.jids && ( + +
+ XMPP accounts +

Your accounts:

{ - me.emails ? me.emails.map(email => - - -
+ visitor.jids.map(jid => + +
- ) : '-' + ) }

{ - me.emails && me.emails.length > 1 && - + visitor.jids && visitor.jids.length > 1 && +

} - +

To add new jabber account: send any text message to juick@juick.com +

+
+ + )} +
+ E-mail +
+

Add account:
+ + + +

+
+
+

Your accounts:

+

+ { + visitor.emails ? visitor.emails.map(email => + + +
+
+ ) : '-' + } +

{ - me.emails && - <> - {/** email_off **/} - - You can receive notifications to email:
- Sent to -
- {/** /email_off **/} -

 

-

You can post to Juick via e-mail. Send your plain text messages to juick@juick.com. You can attach one photo or video file.

- + visitor.emails && visitor.emails.length > 1 && + } -
-
- Facebook - {me.facebookStatus && me.facebookStatus.connected ? ( - me.facebookStatus.crosspostEnabled ? + + { + visitor.emails && + <> + {/** email_off **/} +
+ You can receive notifications to email:
+ Sent to +
+ {/** /email_off **/} +

 

+

You can post to Juick via e-mail. Send your plain text messages to juick@juick.com. + You can attach one photo or video file.

+ + } +
+
+ Facebook + { + visitor.facebookStatus && visitor.facebookStatus.connected ? ( + visitor.facebookStatus.crosspostEnabled ?
- Facebook: Enabled — - + Facebook: Enabled— +
:
- Facebook: Disabled — - + Facebook: Disabled— +
) : ( @@ -240,29 +224,29 @@ export default class Settings extends React.Component {

)} -
-
- Twitter - {me.twitterName ? -
-
Twitter: {me.twitterName} — +
+
+ Twitter + {visitor.twitterName ? + +
Twitter: {visitor.twitterName} - -
- - : -

Cross-posting to Twitter: Connect to Twitter

- } -
+ +
+ + : +

Cross-posting to Twitter: Connect to Twitter

+ } + -
- ); - } + + ); } - +/* 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 ); } - +/* 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} 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} 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) { : {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 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 ; } +/** + * Readers feed + * @param {{match: import('react-router').match }} match + */ export function Readers({ match }) { return ; } +/** + * 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) { ); } - - -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; -- cgit v1.2.3