From ffb5d1beae77661b505a110e717c88aa44e1c912 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 28 Oct 2022 14:23:41 +0300 Subject: Merge `Message` and `MessageInput` components from Next version --- vnext/src/ui/Message.css | 242 ------------------------------------------- vnext/src/ui/Message.js | 160 ++++++++++++++++------------ vnext/src/ui/MessageInput.js | 81 +++++++++------ vnext/src/ui/Thread.js | 2 +- 4 files changed, 141 insertions(+), 344 deletions(-) delete mode 100644 vnext/src/ui/Message.css (limited to 'vnext/src') diff --git a/vnext/src/ui/Message.css b/vnext/src/ui/Message.css deleted file mode 100644 index 06ad5270..00000000 --- a/vnext/src/ui/Message.css +++ /dev/null @@ -1,242 +0,0 @@ -@custom-media --viewport-desktop (width >=62.5rem); -@custom-media --viewport-mobile (width < 62.5rem); - -.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 var(--border-color); - display: flex; - align-items: center; - justify-content: space-around; - background: var(--background-color); -} -.msg-cont > .l a { - color: var(--dimmed-link-color); - margin-right: 15px; - font-size: small; -} -.msg-tags { - color: var(--dimmed-link-color); - margin-top: 12px; - min-height: 1px; -} -.badge, -.msg-tags > a { - color: var(--dimmed-link-color); - display: inline-block; - font-size: small; -} -.msgthread { - margin-bottom: 0; -} -.msg-cont { - background: var(--text-background-color); - border: 1px solid var(--border-color); - line-height: 140%; - margin-bottom: 12px; -} -.reply-new .msg-cont { - border-right: 5px solid #0C0; -} -.msg-ts { - font-size: small; - vertical-align: top; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; -} -.msg-ts, -.msg-ts > a { - color: var(--dimmed-link-color); -} -.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-media img { - max-width: 100%; - height: auto; -} -.msg-links { - color: var(--dimmed-link-color); - font-size: small; - margin: 5px 0 0 0; - padding: 12px; -} -.msg-comments { - color: var(--dimmed-link-color); - 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: var(--background-color); - border: 1px solid var(--border-color); - color: #999; - margin: 0 0 0 6px; - position: sticky; - top: 70px; - vertical-align: top; - width: 50px; -} -.msg-recomms { - color: var(--dimmed-link-color); - background: var(--main-background-color); - font-size: small; - margin-bottom: 10px; - padding: 6px; - border-bottom: 1px solid var(--border-color); - overflow: hidden; - text-indent: 10px; -} -.msg-summary, -.msg-summary a { - color: var(--dimmed-link-color); - font-size: small; - padding: 12px; - text-align: right; -} -.msg-bubble { - padding: 12px; - display: inline-block; - background: var(--border-color); - color: #222; -} - -.msg-bubble-my { - color: #fff !important; - background: var(--link-color) !important; -} - -.msg-bubble-my a { - color: #fff !important; - text-decoration: underline; -} -#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; - position: relative; -} -.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; -} - -@media (--viewport-desktop) { - .msg-cont { - width: 640px; - } -} diff --git a/vnext/src/ui/Message.js b/vnext/src/ui/Message.js index 620fe017..f8d3dc51 100644 --- a/vnext/src/ui/Message.js +++ b/vnext/src/ui/Message.js @@ -1,104 +1,117 @@ -import { memo, useEffect, useRef } from 'react'; +import React, { Fragment, memo, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; import moment from 'moment'; +import { Link } from 'react-router-dom'; import Icon from './Icon'; import Avatar from './Avatar'; import { UserLink } from './UserInfo'; import { format, embedUrls } from '../utils/embed'; -import './Message.css'; +/** + * @typedef {object} MessageProps + * @property { import('../client').Message } data data + * @property { import('../client').SecureUser } visitor visitor + */ /** * Message component - * @param {{data: import('../api').Message, visitor: import('../api').User, children: React.ReactElement}} props + * + * @param {React.PropsWithChildren<{}> & MessageProps} props props */ -export default function Message({ data, visitor, children }) { +export default function Message({ visitor, data, 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} + * @type {React.MutableRefObject} */ - const embedRef = useRef(); + const embedRef = useRef(null); /** - * @type {React.RefObject} + * @type {React.MutableRefObject} */ - const msgRef = useRef(); + const msgRef = useRef(null); useEffect(() => { - if (msgRef.current) { - embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current); - if (!embedRef.current.hasChildNodes()) { - embedRef.current.style.display = 'none'; + const msg = msgRef.current; + const embed = embedRef.current; + if (msg && embed) { + embedUrls(msg.querySelectorAll('a'), embed); + if (!embed.hasChildNodes()) { + embed.style.display = 'none'; } } }, []); - const canComment = visitor.uid === data.user.uid || !data.ReadOnly; + const canComment = data.user && visitor.uid === data.user.uid || !data.ReadOnly; return (
- -
- - - - { - visitor.uid == data.user.uid && - <> -  ·  - Edit - - } -
- -
- + { + data.user && + +
+ + + + { + visitor.uid == data.user.uid && + <> +  ·  + Edit + + } +
+
+ }
{ - data.body && + data.body && data.user && data.mid &&
+
} { - data.photo && + data.photo && data.attachment && data.attachment.small && }
{children} @@ -107,39 +120,52 @@ export default function Message({ data, visitor, children }) { } /** - * @param {{isCode: boolean, data: {__html: string}}} props + * @param {{isCode: boolean, data: {__html: string}}} props props */ function MessageContainer({ isCode, data }) { - return isCode ? (
) : (

); + return isCode ? (

) : ();
 }
 
 /**
  * Tags component
- * @param {{user: import('../api').User, data: string[]}} props
+ *
+ * @param {{user: import('../client').User, data: string[]}} props props
  */
 function Tags({ data, user }) {
-    return data.length > 0 && (
-        
+ return data.length > 0 ? ( + { - data.map(tag => { - return ({tag}); - // @ts-ignore - }).reduce((prev, curr) => [prev, ', ', curr]) + data.map((tag, index) => ( + + {index > 0 && ' '} + + {tag} + + + )) } -
- ); +
+ ) : null; } const TagsList = memo(Tags); +/** + * + * @param {{forMessage: import('../client').Message}} props props + */ function Recommends({ forMessage }) { - const { likes, recommendations } = forMessage; + const { recommendations } = forMessage; + const likes = forMessage.likes || 0; return recommendations && recommendations.length > 0 && (
{'♡ by '} { - recommendations.map(it => ( - - )).reduce((prev, curr) => [prev, ', ', curr]) + recommendations.map((it, index) => ( + + {index > 0 && ', '} + + + )) } { likes > recommendations.length && ( and {likes - recommendations.length} others) diff --git a/vnext/src/ui/MessageInput.js b/vnext/src/ui/MessageInput.js index 6003a15c..aa4454a1 100644 --- a/vnext/src/ui/MessageInput.js +++ b/vnext/src/ui/MessageInput.js @@ -1,56 +1,63 @@ -import { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import Icon from './Icon'; import Button from './Button'; import UploadButton from './UploadButton'; +import toast from 'react-hot-toast'; /** * StackOverflow-driven development: https://stackoverflow.com/a/10158364/1097384 - * @param {HTMLTextAreaElement} el + * + * @param {HTMLTextAreaElement & {createTextRange?: Function}} el element */ function moveCaretToEnd(el) { if (typeof el.selectionStart == 'number') { el.selectionStart = el.selectionEnd = el.value.length; - // @ts-ignore } else if (typeof el.createTextRange != 'undefined') { + // Internet Explorer el.focus(); - // @ts-ignore var range = el.createTextRange(); range.collapse(false); range.select(); } } + +/** @external Promise */ + /** - * @typedef {Object} MessageInputProps - * @property {string} text - * @property {import('../api').Message=} data - * @property {function} onSend - * @property {number=} rows - * @property {string} children + * @typedef {object} MessageInputProps + * @property {string} text text + * @property {function({body:string, attach:string | File}):Promise} onSend onSend + * @property {number=} rows rows + * @property {string=} placeholder placeholder */ /** * MessageInput - * @param {React.ReactNode & MessageInputProps} props + * + * @param {React.ComponentProps & MessageInputProps} props props */ -export default function MessageInput({ text, data, rows, children, onSend }) { +export default function MessageInput({ text, rows, placeholder, onSend }) { /** - * @type {React.MutableRefObject} + * @type {React.MutableRefObject} */ - let textareaRef = useRef(); + let textareaRef = useRef(null); /** - * @type {React.MutableRefObject} + * @type {React.MutableRefObject} */ - let fileinput = useRef(); + let fileinput = useRef(null); let updateFocus = () => { const isDesktop = window.matchMedia('(min-width: 62.5rem)'); if (isDesktop.matches) { - textareaRef.current.focus(); - moveCaretToEnd(textareaRef.current); + const textarea = textareaRef.current; + if (textarea) { + textarea.focus(); + moveCaretToEnd(textarea); + } } }; useEffect(() => { @@ -72,37 +79,43 @@ export default function MessageInput({ text, data, rows, children, onSend }) { const textChanged = (event) => { setBody(event.target.value); const el = textareaRef.current; - const offset = el.offsetHeight - el.clientHeight; - const height = el.scrollHeight + offset; - el.style.height = `${height + offset}px`; + if (el) { + const offset = el.offsetHeight - el.clientHeight; + const height = el.scrollHeight + offset; + el.style.height = `${height + offset}px`; + } }; const [attach, setAttach] = useState(''); let uploadValueChanged = (attach) => { setAttach(attach); }; - let onSubmit = (event) => { + let onSubmit = async (event) => { if (event.preventDefault) { event.preventDefault(); } const input = fileinput.current; - onSend({ - mid: data.mid, - rid: data.rid || 0, - body: body, - attach: attach ? input.files[0] : '', - to: data.to || {} - }); - setAttach(''); - setBody(''); - textareaRef.current.style.height = ''; - updateFocus(); + if (input && input.files) { + if (await onSend({ + body: body, + attach: attach ? input.files[0] : '' + })) { + setAttach(''); + setBody(''); + if (textareaRef.current) { + textareaRef.current.style.height = ''; + } + updateFocus(); + } else { + toast('Can not update this message'); + } + } }; return (