diff options
Diffstat (limited to 'vnext/src')
-rw-r--r-- | vnext/src/api/index.js | 8 | ||||
-rw-r--r-- | vnext/src/ui/Message.js | 8 | ||||
-rw-r--r-- | vnext/src/ui/MessageInput.js | 9 | ||||
-rw-r--r-- | vnext/src/ui/Post.js | 13 | ||||
-rw-r--r-- | vnext/src/ui/Thread.js | 61 | ||||
-rw-r--r-- | vnext/src/ui/__tests__/MessageInput-test.js | 59 |
6 files changed, 99 insertions, 59 deletions
diff --git a/vnext/src/api/index.js b/vnext/src/api/index.js index 2332ed48..e6b1d2ef 100644 --- a/vnext/src/api/index.js +++ b/vnext/src/api/index.js @@ -75,6 +75,14 @@ export function comment(mid, rid, body, attach) { return client.post('/api/comment', form); } +export function update(mid, rid, body) { + let form = new FormData(); + form.append('mid', mid); + form.append('rid', rid); + form.append('body', body); + return client.post('/api/update', form); +} + export function updateAvatar(newAvatar) { let form = new FormData(); form.append('avatar', newAvatar); diff --git a/vnext/src/ui/Message.js b/vnext/src/ui/Message.js index eb008bfe..fd225282 100644 --- a/vnext/src/ui/Message.js +++ b/vnext/src/ui/Message.js @@ -38,7 +38,15 @@ export default function Message({ data, visitor, children, ...rest }) { {moment.utc(data.timestamp).fromNow()} </time> </Link> + { + visitor.uid == data.user.uid && + <> + <span> · </span> + <Link to={{ pathname: '/post', state: { draft: data }}}>Edit</Link> + </> + } </div> + </Avatar> <TagsList user={data.user} data={data.tags || []} /> </header> diff --git a/vnext/src/ui/MessageInput.js b/vnext/src/ui/MessageInput.js index e4988d59..fc3596cc 100644 --- a/vnext/src/ui/MessageInput.js +++ b/vnext/src/ui/MessageInput.js @@ -21,9 +21,8 @@ export default function MessageInput({ text, data, rows, children, onSend }) { } }; useEffect(() => { - textareaRef.current.value = text || ''; updateFocus(); - }, [text]); + }, []); let handleCtrlEnter = (event) => { if (event.ctrlKey && (event.charCode == 10 || event.charCode == 13)) { @@ -37,7 +36,9 @@ export default function MessageInput({ text, data, rows, children, onSend }) { el.style.height = `${height + offset}px`; }; const [attach, setAttach] = useState(''); - const [formState, { textarea }] = useFormState(); + const [formState, { textarea }] = useFormState({ + body: text + }); let uploadValueChanged = (attach) => { setAttach(attach); }; @@ -62,7 +63,7 @@ export default function MessageInput({ text, data, rows, children, onSend }) { <form className="msg-comment-target" style={{ padding: '12px' }} onSubmit={onSubmit}> <div style={commentStyle}> <textarea onChange={textChanged} onKeyPress={handleCtrlEnter} - ref={textareaRef} style={textInputStyle} value={formState.values.body} + ref={textareaRef} style={textInputStyle} rows={rows || '1'} placeholder={children} {...textarea('body')} /> <div style={inputBarStyle}> <UploadButton inputRef={fileinput} value={attach} onChange={uploadValueChanged} /> diff --git a/vnext/src/ui/Post.js b/vnext/src/ui/Post.js index 3dc23613..dc1c7a9d 100644 --- a/vnext/src/ui/Post.js +++ b/vnext/src/ui/Post.js @@ -7,23 +7,25 @@ import qs from 'qs'; import MessageInput from './MessageInput'; -import { post } from '../api'; +import { post, update } from '../api'; -function PostComponent(props) { +function PostComponent({ location, visitor, history }) { + let draftMessage = (location.state || {}).draft || {}; let params = qs.parse(window.location.search.substring(1)); let postMessage = (template) => { const { attach, body } = template; - post(body, attach) + const postAction = draftMessage.mid ? update(draftMessage.mid, 0, body) : post(body, attach); + postAction .then(response => { if (response.status === 200) { const msg = response.data.newMessage; - this.props.history.push(`/${this.props.visitor.uname}/${msg.mid}`); + history.push(`/${visitor.uname}/${msg.mid}`); } }).catch(console.log); }; return ( <div className="msg-cont"> - <MessageInput rows="7" text={params.body || ''} data={{ mid: 0, timestamp: '0' }} onSend={postMessage}> + <MessageInput rows="7" text={params.body || draftMessage.body || ''} data={{ mid: 0, timestamp: '0' }} onSend={postMessage}> *weather It is very cold today! </MessageInput> </div> @@ -33,6 +35,7 @@ function PostComponent(props) { export default memo(PostComponent); PostComponent.propTypes = { + location: ReactRouterPropTypes.location, history: ReactRouterPropTypes.history.isRequired, visitor: UserType }; diff --git a/vnext/src/ui/Thread.js b/vnext/src/ui/Thread.js index 30fa0723..6cbb5188 100644 --- a/vnext/src/ui/Thread.js +++ b/vnext/src/ui/Thread.js @@ -12,13 +12,13 @@ import Button from './Button'; import { format, embedUrls } from '../utils/embed'; -import { getMessages, comment, markReadTracker, fetchUserUri } from '../api'; +import { getMessages, comment, update, markReadTracker, fetchUserUri, updateAvatar } from '../api'; import './Thread.css'; let isMounted; -function Comment({ msg, visitor, active, setActive, postComment }) { +function Comment({ msg, draft, visitor, active, setActive, onStartEditing, postComment }) { const embedRef = useRef(); const msgRef = useRef(); const [author, setAuthor] = useState(msg.user); @@ -33,16 +33,16 @@ function Comment({ msg, visitor, active, setActive, postComment }) { useEffect(() => { isMounted = true; if (author.uri) { - fetchUserUri(author.uri).then(response => { - if (isMounted) { - setAuthor(response.data); - } - }); + fetchUserUri(author.uri).then(response => { + if (isMounted) { + setAuthor(response.data); + } + }); } return () => { - isMounted = false; + isMounted = false; }; -}, [author.uri]); + }, [author.uri]); return ( <div> <div className="msg-header" style={{ padding: '6px', display: 'flex', alignItems: 'flex-start' }}> @@ -59,6 +59,13 @@ function Comment({ msg, visitor, active, setActive, postComment }) { visitor.uid > 0 ? ( <> {active === msg.rid || <span style={linkStyle} onClick={() => setActive(msg.rid)}>Reply</span>} + { + visitor.uid == msg.user.uid && + <> + <span> · </span> + <span style={linkStyle} onClick={() => onStartEditing(msg)}>Edit</span> + </> + } </> ) : ( <> @@ -77,18 +84,18 @@ function Comment({ msg, visitor, active, setActive, postComment }) { </div> } </div> - { - msg.photo && - <div className="msg-media"> - <a href={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} data-fname={`${msg.mid}-${msg.rid}.${msg.attach}`}> - <img src={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} alt="" /> - </a> - </div> - } + { + msg.photo && + <div className="msg-media"> + <a href={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} data-fname={`${msg.mid}-${msg.rid}.${msg.attach}`}> + <img src={`//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}`} alt="" /> + </a> + </div> + } <div className="embedContainer" ref={embedRef} /> <div className="msg-links"> { - active === msg.rid && <MessageInput data={msg} onSend={postComment}>Write a comment...</MessageInput> + active === msg.rid && <MessageInput data={msg} text={draft || ''} onSend={postComment}>Write a comment...</MessageInput> } </div> </div> @@ -97,9 +104,11 @@ function Comment({ msg, visitor, active, setActive, postComment }) { Comment.propTypes = { msg: MessageType.isRequired, + draft: PropTypes.string.isRequired, visitor: UserType.isRequired, active: PropTypes.number.isRequired, setActive: PropTypes.func.isRequired, + onStartEditing: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired }; @@ -108,6 +117,7 @@ export default function Thread(props) { const [replies, setReplies] = useState([]); const [loading, setLoading] = useState(false); const [active, setActive] = useState(0); + const [editing, setEditing] = useState({}); useEffect(() => { setActive(0); loadReplies(); @@ -157,19 +167,26 @@ export default function Thread(props) { }, [message]); let postComment = (template) => { const { mid, rid, body, attach } = template; - comment(mid, rid, body, attach).then(res => { + let commentAction = editing.rid ? update(mid, editing.rid, body) : comment(mid, rid, body, attach); + commentAction.then(res => { + setEditing({}); loadReplies(); }) .catch(console.log); }; + let startEditing = (reply) => { + setActive(reply.replyto); + setEditing(reply); + }; + const loaders = Math.min(message.replies || 0, 10); return ( <> { message.mid ? ( <Message data={message} visitor={props.visitor}> - {active === (message.rid || 0) && <MessageInput data={message} onSend={postComment}>Write a comment...</MessageInput>} + {active === (message.rid || 0) && <MessageInput data={message} text={editing.body || ''} onSend={postComment}>Write a comment...</MessageInput>} </Message> ) : ( <Spinner /> @@ -179,7 +196,7 @@ export default function Thread(props) { { !loading ? replies.map((msg) => ( <li id={msg.rid} key={msg.rid}> - <Comment msg={msg} visitor={props.visitor} active={active} setActive={setActive} postComment={postComment} /> + <Comment msg={msg} draft={msg.rid === editing.replyto && editing.body} visitor={props.visitor} active={active} setActive={setActive} onStartEditing={startEditing} postComment={postComment} /> </li> )) : ( <> @@ -201,5 +218,5 @@ Thread.propTypes = { history: ReactRouterPropTypes.history, match: ReactRouterPropTypes.match, visitor: UserType.isRequired, - connection: PropTypes.object.isRequired + connection: PropTypes.object.isRequired, }; diff --git a/vnext/src/ui/__tests__/MessageInput-test.js b/vnext/src/ui/__tests__/MessageInput-test.js index 7ac69ed0..9603d1fe 100644 --- a/vnext/src/ui/__tests__/MessageInput-test.js +++ b/vnext/src/ui/__tests__/MessageInput-test.js @@ -12,31 +12,35 @@ const testMessage = { to: {} }; -window.matchMedia = window.matchMedia || function () { +window.matchMedia = window.matchMedia || function() { return { matches: true, - addListener: function () { }, - removeListener: function () { } + addListener: function() { }, + removeListener: function() { } }; }; +function createMessageInput(data, onFocus, onSend, draft) { + return create(<MessageInput data={data} onSend={onSend} text={draft} />, { + createNodeMock: (element) => { + if (element.type === 'textarea') { + // mock a focus function + return { + focus: onFocus, + style: {} + }; + } + return null; + } + }); +} + it('Gives immediate focus on to textarea on load', () => { let focused = false; act(() => { - create(<MessageInput data={testMessage} onSend={() => { }} />, { - createNodeMock: (element) => { - if (element.type === 'textarea') { - // mock a focus function - return { - focus: () => { - focused = true; - }, - style: {} - }; - } - return null; - } - }); + createMessageInput(testMessage, () => { + focused = true; + }, () => { }); }); expect(focused).toEqual(true, 'textarea was not focused'); }); @@ -46,17 +50,7 @@ it('Submits on ctrl-enter', () => { const onSend = jest.fn(); var messageInput = null; act(() => { - messageInput = create(<MessageInput data={testMessage} onSend={onSend} />, { - createNodeMock: (element) => { - if (element.type === 'textarea') { - return { - focus: () => { }, - style: {} - }; - } - return null; - } - }); + messageInput = createMessageInput(testMessage, () => {}, onSend); }); let textarea = messageInput.root.findByType('textarea'); act(() => { @@ -93,3 +87,12 @@ it('Submits on ctrl-enter', () => { }); expect(textarea.props.value).toEqual('', 'Value should be cleared after submit'); }); + +it('Show draft text', () => { + var messageInput; + act(() => { + messageInput = createMessageInput(testMessage, () => {}, () => {}, 'yo'); + }); + let textarea = messageInput.root.findByType('textarea'); + expect(textarea.props.value).toEqual('yo', 'Value should match draft'); +}); |